Polish dealers and shared buttons for clearer contacts and CTAs.
This commit is contained in:
@@ -2,17 +2,26 @@
|
||||
--btn-fill: var(--color-navy);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
min-width: 270px;
|
||||
min-width: 220px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-navy);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-white);
|
||||
color: var(--color-navy);
|
||||
font: inherit;
|
||||
font-size: 0.92rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
|
||||
button.primary-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.primary-btn--dark {
|
||||
--btn-fill: #000;
|
||||
border-color: #000;
|
||||
@@ -30,19 +39,22 @@
|
||||
}
|
||||
|
||||
.primary-btn__label {
|
||||
padding: 0.7rem 1rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.primary-btn__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 52px;
|
||||
height: 45px;
|
||||
align-self: stretch;
|
||||
width: 44px;
|
||||
min-height: 38px;
|
||||
background: var(--btn-fill);
|
||||
}
|
||||
|
||||
.primary-btn__icon img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,31 @@ import { Link } from 'react-router-dom'
|
||||
import arrowLeft from '../assets/images/icons/arrow-left.svg'
|
||||
import './PrimaryButton.css'
|
||||
|
||||
type Props = {
|
||||
to: string
|
||||
type SharedProps = {
|
||||
title?: string
|
||||
variant?: 'default' | 'dark'
|
||||
full?: boolean
|
||||
download?: boolean
|
||||
iconSrc?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type LinkProps = SharedProps & {
|
||||
to: string
|
||||
download?: boolean
|
||||
type?: never
|
||||
onClick?: never
|
||||
}
|
||||
|
||||
type ButtonProps = SharedProps & {
|
||||
to?: undefined
|
||||
download?: never
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
type Props = LinkProps | ButtonProps
|
||||
|
||||
export default function PrimaryButton({
|
||||
to,
|
||||
title = 'دوست دارم بیشتر بدونم',
|
||||
@@ -20,6 +35,9 @@ export default function PrimaryButton({
|
||||
download = false,
|
||||
iconSrc = arrowLeft,
|
||||
className = '',
|
||||
type = 'button',
|
||||
disabled = false,
|
||||
onClick,
|
||||
}: Props) {
|
||||
const classes = [
|
||||
'primary-btn',
|
||||
@@ -30,6 +48,23 @@ export default function PrimaryButton({
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<span className="primary-btn__label">{title}</span>
|
||||
<span className="primary-btn__icon" aria-hidden="true">
|
||||
<img src={iconSrc} alt="" width={18} height={18} />
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
|
||||
if (!to) {
|
||||
return (
|
||||
<button className={classes} type={type} disabled={disabled} onClick={onClick}>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (download || to.startsWith('http') || to.startsWith('#')) {
|
||||
return (
|
||||
<a
|
||||
@@ -41,20 +76,14 @@ export default function PrimaryButton({
|
||||
? { target: '_blank', rel: 'noreferrer' }
|
||||
: {})}
|
||||
>
|
||||
<span className="primary-btn__label">{title}</span>
|
||||
<span className="primary-btn__icon" aria-hidden="true">
|
||||
<img src={iconSrc} alt="" width={18} height={18} />
|
||||
</span>
|
||||
{content}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className={classes} to={to}>
|
||||
<span className="primary-btn__label">{title}</span>
|
||||
<span className="primary-btn__icon" aria-hidden="true">
|
||||
<img src={iconSrc} alt="" width={18} height={18} />
|
||||
</span>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
+88
-3
@@ -1,3 +1,5 @@
|
||||
import { toFaDigits } from './faDigits'
|
||||
|
||||
const FA_TO_EN: Record<string, string> = {
|
||||
'۰': '0',
|
||||
'۱': '1',
|
||||
@@ -21,6 +23,56 @@ const FA_TO_EN: Record<string, string> = {
|
||||
'٩': '9',
|
||||
}
|
||||
|
||||
/** Common Iranian city / province landline area codes (with leading 0). */
|
||||
const AREA_CODES: Record<string, string> = {
|
||||
تهران: '021',
|
||||
کرج: '026',
|
||||
البرز: '026',
|
||||
اصفهان: '031',
|
||||
مبارکه: '031',
|
||||
شیراز: '071',
|
||||
فارس: '071',
|
||||
مشهد: '051',
|
||||
نیشابور: '051',
|
||||
'خراسان رضوی': '051',
|
||||
تبریز: '041',
|
||||
'آذربایجان شرقی': '041',
|
||||
ارومیه: '044',
|
||||
'آذربایجان غربی': '044',
|
||||
رشت: '013',
|
||||
گیلان: '013',
|
||||
ساری: '011',
|
||||
چالوس: '011',
|
||||
مازندران: '011',
|
||||
اهواز: '061',
|
||||
شوش: '061',
|
||||
خوزستان: '061',
|
||||
کرمان: '034',
|
||||
یزد: '035',
|
||||
قم: '025',
|
||||
قزوین: '028',
|
||||
کرمانشاه: '083',
|
||||
بندرعباس: '076',
|
||||
کیش: '076',
|
||||
هرمزگان: '076',
|
||||
بوشهر: '077',
|
||||
گرگان: '017',
|
||||
'بندر ترکمن': '017',
|
||||
گلستان: '017',
|
||||
اراک: '086',
|
||||
محلات: '086',
|
||||
مرکزی: '086',
|
||||
همدان: '081',
|
||||
زنجان: '024',
|
||||
سمنان: '023',
|
||||
اردبیل: '045',
|
||||
بجنورد: '058',
|
||||
ایلام: '084',
|
||||
یاسوج: '074',
|
||||
شهرکرد: '038',
|
||||
بیرجند: '056',
|
||||
}
|
||||
|
||||
export type PhoneKind = 'mobile' | 'landline'
|
||||
|
||||
export type DealerPhone = {
|
||||
@@ -42,21 +94,54 @@ export function isMobileDigits(digits: string): boolean {
|
||||
return /^09\d{9}$/.test(digits)
|
||||
}
|
||||
|
||||
export function dealerPhones(mobile: string, landline: string): DealerPhone[] {
|
||||
function areaCodeFor(city?: string, province?: string): string | undefined {
|
||||
const keys = [city, province].map((v) => v?.trim()).filter(Boolean) as string[]
|
||||
for (const key of keys) {
|
||||
if (AREA_CODES[key]) return AREA_CODES[key]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function ensureLandlineAreaCode(digits: string, city?: string, province?: string): string {
|
||||
if (isMobileDigits(digits)) return digits
|
||||
if (digits.startsWith('0') && digits.length >= 10) return digits
|
||||
|
||||
const area = areaCodeFor(city, province)
|
||||
if (!area) {
|
||||
if (digits.length === 10) return `0${digits}`
|
||||
return digits
|
||||
}
|
||||
|
||||
if (digits.startsWith(area.slice(1))) return `0${digits}`
|
||||
if (digits.startsWith(area)) return digits
|
||||
if (digits.length <= 8) return `${area}${digits}`
|
||||
return digits.startsWith('0') ? digits : `0${digits}`
|
||||
}
|
||||
|
||||
export function dealerPhones(
|
||||
mobile: string,
|
||||
landline: string,
|
||||
opts?: { city?: string; province?: string },
|
||||
): DealerPhone[] {
|
||||
const seen = new Set<string>()
|
||||
const list: DealerPhone[] = []
|
||||
|
||||
const add = (raw: string | null | undefined) => {
|
||||
const trimmed = (raw ?? '').trim()
|
||||
if (!trimmed || trimmed === '\\N') return
|
||||
const digits = normalizePhoneDigits(trimmed)
|
||||
let digits = normalizePhoneDigits(trimmed)
|
||||
if (digits.length < 8) return
|
||||
|
||||
if (!isMobileDigits(digits)) {
|
||||
digits = ensureLandlineAreaCode(digits, opts?.city, opts?.province)
|
||||
}
|
||||
|
||||
if (seen.has(digits)) return
|
||||
seen.add(digits)
|
||||
const kind: PhoneKind = isMobileDigits(digits) ? 'mobile' : 'landline'
|
||||
list.push({
|
||||
kind,
|
||||
display: trimmed,
|
||||
display: toFaDigits(digits),
|
||||
href: `tel:${digits}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,22 +24,44 @@
|
||||
}
|
||||
|
||||
.dealers-search {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.dealers-search__icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-start: 14px;
|
||||
transform: translateY(-50%);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #8b95a8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dealers-search input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--color-navy);
|
||||
border-radius: var(--radius, 8px);
|
||||
border: 1px solid #d7dde8;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 14px 16px;
|
||||
padding-inline-start: 44px;
|
||||
font: inherit;
|
||||
font-size: 15px;
|
||||
color: var(--color-navy);
|
||||
outline: none;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.dealers-search input::-webkit-search-decoration,
|
||||
.dealers-search input::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.dealers-search input::placeholder {
|
||||
@@ -51,6 +73,10 @@
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 176, 0.12);
|
||||
}
|
||||
|
||||
.dealers-search:focus-within .dealers-search__icon {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -172,12 +198,16 @@
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.catalog--flush-cta {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dealers-cta {
|
||||
margin-top: 36px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 18px 0;
|
||||
background: #eef3f9;
|
||||
border-top: 1px solid #dbe4f0;
|
||||
border-bottom: 0;
|
||||
padding: 22px 0;
|
||||
}
|
||||
|
||||
.dealers-cta__inner {
|
||||
@@ -268,7 +298,10 @@
|
||||
text-decoration: none;
|
||||
font-family: var(--font-en);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.2;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.dealer-row__phone--mobile {
|
||||
|
||||
+25
-11
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import PrimaryButton from '../components/PrimaryButton'
|
||||
import SeoHead from '../components/SeoHead'
|
||||
import {
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
listPublicRepresentations,
|
||||
type PublicRepresentation,
|
||||
} from '../lib/api'
|
||||
import { toFaDigits } from '../lib/faDigits'
|
||||
import { paths } from '../lib/paths'
|
||||
import { dealerPhones, type PhoneKind } from '../lib/phones'
|
||||
import './CatalogPages.css'
|
||||
@@ -80,7 +78,7 @@ export default function DealersPage() {
|
||||
}, [items, city, query])
|
||||
|
||||
return (
|
||||
<section className="catalog">
|
||||
<section className="catalog catalog--flush-cta">
|
||||
<SeoHead
|
||||
title="نمایندگی های مادو موتور در سراسر ایران"
|
||||
description="با لیست کامل نمایندگی های مادو موتور در سراسر ایران آشنا شوید. آدرس، شماره تماس و خدمات ارائه شده توسط هر نمایندگی را به راحتی مشاهده کنید و نزدیکترین شعبه را انتخاب کنید."
|
||||
@@ -92,6 +90,25 @@ export default function DealersPage() {
|
||||
<p>نمایندگیهای فعال در سراسر ایران</p>
|
||||
<label className="dealers-search">
|
||||
<span className="visually-hidden">جستجوی نمایندگی</span>
|
||||
<span className="dealers-search__icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20">
|
||||
<circle
|
||||
cx="11"
|
||||
cy="11"
|
||||
r="6.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
/>
|
||||
<path
|
||||
d="M16.2 16.2 20 20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
@@ -149,7 +166,10 @@ export default function DealersPage() {
|
||||
<p>{item.address || '—'}</p>
|
||||
</div>
|
||||
<div className="dealer-row__contacts">
|
||||
{dealerPhones(item.mobile, item.landline).map((phone) => (
|
||||
{dealerPhones(item.mobile, item.landline, {
|
||||
city: item.city,
|
||||
province: item.province,
|
||||
}).map((phone) => (
|
||||
<a
|
||||
key={`${phone.kind}-${phone.href}`}
|
||||
className={`dealer-row__phone dealer-row__phone--${phone.kind}`}
|
||||
@@ -157,7 +177,7 @@ export default function DealersPage() {
|
||||
dir="ltr"
|
||||
>
|
||||
<PhoneIcon kind={phone.kind} />
|
||||
<span>{toFaDigits(phone.display)}</span>
|
||||
<span>{phone.display}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -174,12 +194,6 @@ export default function DealersPage() {
|
||||
<PrimaryButton to={paths.representationRequest} title="درخواست نمایندگی" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="catalog__foot">
|
||||
<Link to="/">بازگشت به صفحه اصلی</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -291,8 +291,8 @@ export default function HomePage() {
|
||||
{branch.ceo ? <li>{branch.ceo}</li> : null}
|
||||
<li>{toFaDigits(branch.address)}</li>
|
||||
{branch.phones.map((phone) => (
|
||||
<li key={phone} dir="ltr">
|
||||
{toFaDigits(phone)}
|
||||
<li key={phone} className="en" dir="ltr">
|
||||
{phone}
|
||||
</li>
|
||||
))}
|
||||
{branch.postalCode ? (
|
||||
|
||||
@@ -63,20 +63,10 @@
|
||||
|
||||
.rep-request__actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.rep-request__actions .primary-btn {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rep-request__actions .primary-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.rep-request__error {
|
||||
color: #d32f2f;
|
||||
margin: 16px 0 0;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useMemo, useState, type FormEvent } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import arrowLeft from '../assets/images/icons/arrow-left.svg'
|
||||
import PrimaryButton from '../components/PrimaryButton'
|
||||
import SeoHead from '../components/SeoHead'
|
||||
import { IRAN_PROVINCES } from '../data/provinces'
|
||||
import { ApiError, submitRepresentationRequest } from '../lib/api'
|
||||
import { paths } from '../lib/paths'
|
||||
import '../components/PrimaryButton.css'
|
||||
import './RepresentationRequestPage.css'
|
||||
|
||||
type FormState = {
|
||||
@@ -227,14 +226,11 @@ export default function RepresentationRequestPage() {
|
||||
{error ? <p className="rep-request__error">{error}</p> : null}
|
||||
|
||||
<div className="rep-request__actions">
|
||||
<button className="primary-btn" type="submit" disabled={saving}>
|
||||
<span className="primary-btn__label">
|
||||
{saving ? 'در حال ارسال...' : 'ثبت درخواست'}
|
||||
</span>
|
||||
<span className="primary-btn__icon" aria-hidden="true">
|
||||
<img src={arrowLeft} alt="" width={18} height={18} />
|
||||
</span>
|
||||
</button>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
title={saving ? 'در حال ارسال...' : 'ثبت درخواست'}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user