From 23c4dc9dc71199ac5d3af502b11097088e6bcf55 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Thu, 3 Sep 2026 23:55:52 +0330 Subject: [PATCH] Polish dealers and shared buttons for clearer contacts and CTAs. --- src/components/PrimaryButton.css | 20 ++++-- src/components/PrimaryButton.tsx | 51 +++++++++++--- src/lib/phones.ts | 91 ++++++++++++++++++++++++- src/pages/CatalogPages.css | 49 ++++++++++--- src/pages/DealersPage.tsx | 36 +++++++--- src/pages/HomePage.tsx | 4 +- src/pages/RepresentationRequestPage.css | 12 +--- src/pages/RepresentationRequestPage.tsx | 16 ++--- 8 files changed, 219 insertions(+), 60 deletions(-) diff --git a/src/components/PrimaryButton.css b/src/components/PrimaryButton.css index b5ff70c..9d2c0be 100644 --- a/src/components/PrimaryButton.css +++ b/src/components/PrimaryButton.css @@ -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); } diff --git a/src/components/PrimaryButton.tsx b/src/components/PrimaryButton.tsx index 93706f1..82060ec 100644 --- a/src/components/PrimaryButton.tsx +++ b/src/components/PrimaryButton.tsx @@ -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 = ( + <> + {title} + + + ) + + if (!to) { + return ( + + ) + } + if (download || to.startsWith('http') || to.startsWith('#')) { return ( - {title} - + {content} ) } return ( - {title} - + {content} ) } diff --git a/src/lib/phones.ts b/src/lib/phones.ts index 558e4e2..93c0828 100644 --- a/src/lib/phones.ts +++ b/src/lib/phones.ts @@ -1,3 +1,5 @@ +import { toFaDigits } from './faDigits' + const FA_TO_EN: Record = { '۰': '0', '۱': '1', @@ -21,6 +23,56 @@ const FA_TO_EN: Record = { '٩': '9', } +/** Common Iranian city / province landline area codes (with leading 0). */ +const AREA_CODES: Record = { + تهران: '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() 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}`, }) } diff --git a/src/pages/CatalogPages.css b/src/pages/CatalogPages.css index 15f4db5..dfa3033 100644 --- a/src/pages/CatalogPages.css +++ b/src/pages/CatalogPages.css @@ -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 { diff --git a/src/pages/DealersPage.tsx b/src/pages/DealersPage.tsx index f05a557..3e6e267 100644 --- a/src/pages/DealersPage.tsx +++ b/src/pages/DealersPage.tsx @@ -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 ( -
+
نمایندگی‌های فعال در سراسر ایران

) } diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index d631863..f090faf 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -291,8 +291,8 @@ export default function HomePage() { {branch.ceo ?
  • {branch.ceo}
  • : null}
  • {toFaDigits(branch.address)}
  • {branch.phones.map((phone) => ( -
  • - {toFaDigits(phone)} +
  • + {phone}
  • ))} {branch.postalCode ? ( diff --git a/src/pages/RepresentationRequestPage.css b/src/pages/RepresentationRequestPage.css index 81d5bfc..698edf7 100644 --- a/src/pages/RepresentationRequestPage.css +++ b/src/pages/RepresentationRequestPage.css @@ -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; diff --git a/src/pages/RepresentationRequestPage.tsx b/src/pages/RepresentationRequestPage.tsx index f046171..a815e1f 100644 --- a/src/pages/RepresentationRequestPage.tsx +++ b/src/pages/RepresentationRequestPage.tsx @@ -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 ?

    {error}

    : null}
    - +
    )}