Commit Graph

95 Commits

Author SHA1 Message Date
c7fad362a5 feat(analytics): enrich empty-catalog signal with vehicle/category names
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
parts_panel_viewed carried only opaque vehicle_id/category_id, so the
'0 parça' empty-catalog breakdown (and its rate-spike alert) couldn't say
WHICH vehicle/category was empty — useless for prioritising catalog backfill.

- parts-panel: add vehicle_label + category_name to parts_panel_viewed, and
  fire a new empty_catalog_cta_clicked (with the same context) from the
  empty-state button — a demand/abandonment signal per vehicle+category.
- schema-viewer: forward the two optional props to PartsPanel.
- vehicle category route: pass the existing vehicleLabel + data.name.

Best-effort labels (undefined on routes not yet threaded); parts_count
unchanged so existing insight/alert keep working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:41:34 +03:00
2a26890d33 feat(web): escape Instagram/FB in-app browsers to the system browser
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Ad traffic from Instagram/FB lands in their in-app WebView, where Google
OAuth (signIn.social google) returns 403 disallowed_useragent — so the
30-day-trial signup with Google is broken for paid traffic. Also costs
password autofill, persistent session, and clean attribution.

- Android: hard-escape via intent:// (no package= → default browser),
  preserving the full URL so UTM/fbclid survive the hop.
- iOS: Apple allows no programmatic escape — dismissible banner guiding
  "••• → Safari'de Aç" with a copy-link fallback.

Mounted in __root so it runs on the landing page. Pure UA/intent helpers
unit-tested (in-app detection + URL preservation + no false positives).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 02:55:46 +03:00
b7dface50c style(web): make "bk. tablo" reference read clearly as a link
Always-underlined, semibold, primary, larger text + arrow, with a "Tabloya
git:" verb so the cross-reference reads unmistakably as a clickable link
instead of muted inline text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:29:03 +03:00
642139de9c fix(pl24): detect "bakınız tablo, konum:" reference phrasing too
PL24 translates "see table" two ways — "bk. tablo:" and "bakınız tablo,
konum:". Detection only matched the first, so the latter rows (e.g. evaporator
housing → 820-020) stayed dead. Broaden the name regex to match either, and
strip both phrasings from the displayed label. The code-in-remark gate still
prevents flagging real parts that merely mention "tablo".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:20:30 +03:00
c440252aa3 feat(pl24): on-demand drill to resolve unseeded "bk. tablo:" references
When a reference's target illustration isn't seeded yet (load-time index
miss → categoryId null), clicking it now calls a new resolve endpoint that
drills the relevant main-group root (its external_id = the code's first
digit; the illustration is a direct child) and re-resolves. One PL24 call in
the common case, bounded + cached; falls back to pre-filled search if not
found. UI shows a spinner on the button while drilling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:41:02 +03:00
232c9ccc7a feat(pl24): make "bk. tablo:" BOM cross-references navigable
PL24 BOM emits "see table NNN-NNN" reference rows (oem N/A, target code in
remark) with NO upstream link. Resolve the code against the vehicle's
illustration index (codes live in category names as {NNN-NNN}) and render
jump links. Unresolved targets (branch not seeded yet) deep-link a pre-filled
catalog search via ?q=.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 18:55:42 +03:00
6bcb7ac2a5 fix(web): instant loading feedback on category node clicks (#73)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Drilling the category tree gave a 0–300 ms feedback gap between click and
the next render — no disabled state, no visible change — so users re-tapped
(one real session: 9 of 22 clicks were rage clicks, 158 s, zero results).

Add an immediate, prominent pending state on all three category views:

- grid: track the clicked card in `pendingId`; the button goes
  disabled + aria-busy and a 28px centred spinner overlays the card (both
  card variants) until the route swap unmounts the grid.
- tree: disable the expand button + aria-busy while children fetch; grow
  the chevron-slot spinner 14px → 20px and tint it primary.
- columns: disable the row + aria-busy while the child column lazy-fetches;
  same 20px primary spinner.

Tests: grid click paints disabled+aria-busy on the tapped card only;
tree/columns rows lock + show a spinner while the fetch is in flight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:15:04 +03:00
e73b899e11 fix(web): parts table spills below the footer on mobile schema page (#76)
The parts-panel wrapper used max-h-[500px] on mobile, but a max-height parent
does not give the panel's `h-full` a definite height to resolve against, so
the panel resolved to the table's full intrinsic height (~2500px) and, because
the wrapper's overflow is visible, spilled out the bottom — rendering the whole
parts list a second time below the signup CTA and the page footer (reported
"ürün kodları → kaydol kartı → footer → ürün kodları" broken structure).

Drop the mobile height cap so the parts list flows in normal document order and
the page scrolls through it (the intended mobile pattern; also resolves the
triple-scroll-trap noted in #76). Desktop is unchanged: the 40% column still
sits inside the md:h-[700px] row with its own inner overflow scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:55:30 +03:00
e4b13c6abb fix(web): mobile schema page traps users above the parts list (#76)
On mobile (iOS Safari, 390px), the schema viewport's one-finger touchmove
handler called setPan() unconditionally — even at zoom 1 with nothing to
pan — which makes iOS suppress the native page scroll. Users couldn't reach
the parts list below the fold (real trial session: 16 rage clicks, 0 OEM
codes copied, no purchase).

- use-schema-interaction: gate one-finger pan on zoom > 1 so the gesture
  falls through to native page scroll when not zoomed in.
- schema-viewer: set touch-action (pan-y pinch-zoom at zoom 1, none when
  zoomed) on the viewport; shrink mobile schema height 400px -> 280px so the
  first parts rows peek below the fold and signal "more below".
- parts-panel: after a hotspot tap, scrollIntoView uses block:"start" on
  mobile (panel is below the fold) and block:"center" on desktop, so the
  selection is actually visible.

Tests: new Vitest hook test asserts no pan at zoom<=1, pans at zoom>1, and
pinch-zoom still works; new parts-panel scroll test asserts mobile vs desktop
block target. Also fixes a pre-existing typo in the empty-state test ("Geri
don" -> "Geri dön").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:34:47 +03:00
497aba24ab fix(ui): repair Turkish labels + hero preview model name
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Two cosmetic fixes the founder flagged in the same browse session:

* parts-panel.tsx — the catalog parts table was missing Turkish
  diacritics on every static label, while the actual part names rendered
  correctly. Looked like an encoding bug; was actually hard-coded ASCII
  strings. Repaired: Parcalar→Parçalar, parca listeleniyor→parça
  listeleniyor, Bu kategori icin parca bulunamadi→Bu kategori için
  parça bulunamadı, Yukleniyor→Yükleniyor, Parca Adi→Parça Adı,
  Geri don→Geri dön. OEM Kodu was already correct.

* index.tsx hero preview — pl24 ships model names with trailing
  duplicates ("Golf 1,6 GOLF"), and the card stitched year right next
  to the engine "(2003 4 silindirli…)" with no clear separator. Now:
  cleanModelName collapses the dup ("Golf 1,6"), year wraps in
  parentheses as a softer secondary element, and the visual flex gap
  guarantees breathing room before the engine pill.
2026-06-02 02:11:35 +03:00
7da12607ef fix(demo): wrap pages with global SiteHeader + SiteFooter
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
/demo and /demo/categories/$id rendered without the site chrome — no nav
to other marketing pages, no theme toggle, no KVKK/gizlilik/iletişim
links. Marketing-page parity (matches index/pricing/about) + B2B trust
(legal links visible, logo back to home).

* Both routes now use a flex-col page shell:
  SiteHeader (sticky top-0, z-50) → DemoBanner (sticky top-16, z-30) →
  main (flex-1) → SiteFooter.
* DemoBanner sticky offset changed top-0 → top-16 to sit under the site
  header instead of competing for the top edge.
2026-06-02 01:27:41 +03:00
c7527e4845 fix(demo): drop VIN input from footer CTA — text + button only
Footer carried a VIN input intended to let demo visitors signup with their
own vehicle pre-filled, but it duplicated the hero's VIN search and added
friction at a moment the visitor is already deciding "should I sign up?"
— not "let me re-type a VIN". Reverted to a clean B2B trust strip + single
"Hesap Aç" primary CTA.

* New DemoFooterCta replaces the prior DemoVinCta. Visitors who want to
  query their own VIN use the hero or the /register page itself.
* Component renamed (file + symbol) so the name reflects what it does;
  the old file is removed. Both /demo and /demo/categories/$id swapped.
* Copy & B2B framing unchanged from the spec ("Sınırsız şase sorgulamak
  için ücretsiz hesap aç" / trust strip).
2026-06-02 01:24:25 +03:00
6643a8edad feat(demo): VIN-input CTA card replaces plain "Hesap Aç" footer
Demo footer was a single-button conversion ("Hesap Aç" → /register with no
VIN). For a parts-shop owner who just browsed the example VW Golf, the
next natural action is "let me try MY customer's VIN" — not "let me click
register and re-type". Adds an inline VIN input to the footer CTA on both
/demo and /demo/categories/:id; submitting routes to /register?vin=… so
the register teaser (see ed74e2f) lifts off where this leaves off.

* New DemoVinCta component owns the form + analytics. Empty submit still
  works as plain "Hesap Aç" (no VIN); typing a VIN flips the button label
  to "Bu VIN için Hesap Aç".
* B2B copy retained from prior commits: "Sınırsız şase sorgulamak için
  ücretsiz hesap aç" headline, "Kart bilgisi gerekmez · 30 gün ücretsiz ·
  istediğin an iptal" trust strip — see [[sase-b2b-copy-not-consumer]].
* Input placeholder reads "Müşterinizin şase numarası (17 karakter)" — B2B
  framing ("müşterinizin") not "kendi aracınız", per the same memory.
* Mobile-friendly: input + button stack vertically <sm, side-by-side ≥sm;
  full-width button on mobile clears the Chatwoot widget.
* PostHog: emits demo_to_register_click with {source, has_vin, vin_length}
  so we can split conversion by "browsed → typed VIN → registered" vs
  "browsed → bare register".

DemoBanner stays a single-CTA sticky anchor (the "Yeni VIN sorgula" gate
lives in the footer card instead — banner real estate is too narrow on
mobile for a useful input).
2026-06-02 01:18:09 +03:00
a43c3492c5 fix(demo): B2B copy + mobile-friendly footer CTA layout
Founder caught two issues on the live /demo page:

1) Copy was framed as B2C ("Kendi aracınız için sınırsız erişim") but
   sase.tr's audience is parts shops / garages / e-commerce sellers — they
   decode 5-30 VINs/day for their customers, not for their own car. Rewrote
   banner + both footer CTAs to frame conversion around "sınırsız şase
   sorgulamak için ücretsiz hesap aç" — that's the actual unit they pay for.

2) Footer "Hesap Aç" button sat awkwardly on mobile: arbitrary-width inline
   element flex-stacked under the headline text, overlapping the Chatwoot
   widget in the bottom-right. Made the button `w-full sm:w-auto` so it
   reads as a clear primary action on mobile, and added `mb-20 sm:mb-0` to
   the card to clear the Chatwoot widget on small screens.
2026-06-02 00:27:01 +03:00
078076b619 feat(demo): public /demo namespace serving pre-warmed VW Golf 2003 catalog
Replaces the old marketing "guided tour" /demo with a real, fully-functional
catalog browsing experience for the pre-warmed example vehicle. No auth
required, no upstream calls — entirely served from prod DB.

Backend (apps/api/src/demo):
* New @Public() controller exposing five endpoints under /api/demo:
  - GET /vehicle                    → demo vehicle metadata
  - GET /categories/tree            → top-level category tree
  - GET /categories/search?q=       → cross-tree search
  - GET /categories/:id             → getCategoryWithParts (parts+schema+hotspots)
  - GET /categories/:id/children    → drill children
* DemoService validates every category id against DEMO_VEHICLE_ID before any
  downstream service call — the public surface can't be used to read an
  arbitrary vehicle's catalog (1-row SELECT, NotFound on miss or wrong owner).
* Vehicle id is env-driven (DEMO_VEHICLE_ID, defaults to the pre-warmed
  WVWZZZ1JZ3W597935 — VW Golf 2003 with 277 cats / 9841 parts / 178 schemas
  fully drilled in prod).
* Wires CategoriesModule (already exports CategoriesService) — zero new
  business logic, just a thin public façade.

Frontend (apps/web):
* /demo (replaces old marketing page): vehicle header + top categories grid
  reading /api/demo/* + sticky DemoBanner with sign-up CTA.
* /demo/categories/$categoryId: drill page rendering either a children grid
  (parent) or the existing SchemaViewer + parts panel (leaf) — same shape
  the dashboard uses, so hotspot overlay, breadcrumb trail, retry on
  upstream loadError all just work.
* DemoBanner: sticky top, "Örnek araç: {label} — Kayıt Ol" CTA. The
  "Yeni VIN sorgula" explicit paywall trigger lands in a follow-up task.
* PostHog events: demo_loaded (source query-param-aware),
  demo_category_clicked, demo_category_detail_viewed, demo_to_register_click
  (banner / footer / category_footer placements).
* usePageMeta gains an opt-in `noindex` flag — demo sets it to noindex,follow
  for the first 4-6 weeks per spec; cleaned up on unmount so SPA navigation
  doesn't carry it to the next route.
2026-06-02 00:07:29 +03:00
e5ed6b9f36 feat(web): /catalog — recently-used brands strip
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Adds a horizontal scrollable "Son ziyaret ettiklerin" strip above the main
brand grid. Renders only when the user has actually opened at least one
brand detail page before — silent in cold-start state.

Why localStorage, not a backend endpoint
- This is a behavioural shortcut, not authoritative state. Adding a
  user_recent_brands table for data we don't have yet is premature.
- Keying by `sase-recent-brands-${userId}` mirrors the trial-banner
  scoping pattern; a second user on the same browser doesn't inherit the
  first user's list.
- localStorage failures (private mode, quota) silently degrade — the
  strip just stays hidden, never throws.

How a brand gets added
- Tracked at the destination (`/catalog/:brandName` visit), not on the
  link click. A click that never resolves into a real visit (auth gate,
  slow nav cancel) shouldn't be a "recently used" signal.
- 12 entries stored, 8 surfaced. Headroom for future ranking (e.g.
  weight by frequency × recency) without re-recording history.

Plan-lock awareness
- The strip cross-references the `/catalog/brands` access map, so a
  brand the user opened while on Full and then lost on a downgrade
  shows the same lock chip + amber upgrade route used by the main grid.
- New PostHog event: `catalog_recent_brand_clicked`. Locked recent
  chips reuse `catalog_locked_brand_upgrade_clicked` with
  `surface: "recents"` for funnel distinction.

i18n: `catalog.recentSection` (TR: "Son ziyaret ettiklerin" / EN:
"Recently visited").
2026-06-01 00:36:50 +03:00
05de73dd41 feat(web): catalog shared primitives — ViewModeToggle, CatalogHeader, useViewMode
Three drift-prone catalog surfaces (main /catalog, /catalog/$brandName, and
the legacy CategoryViewToggle) each carried their own inline view-mode
toggle with subtly different aria labels, sizing, icon paths and even a
"Sutun" typo. New surfaces will inherit a single primitive.

- ViewModeToggle: grid/tree/columns radio-group with i18n labels driven by
  `catalog.view.*` keys; fan-out by passing `groupLabelKey`.
- useViewMode(key): centralises read/write of `brandViewMode`/`modelViewMode`
  in user-settings — page just owns its current value.
- CatalogHeader: title + subtitle + optional breadcrumb trail + back button +
  trailing actions slot, so every catalog drill page renders the same shell.

i18n: adds catalog.subtitle, catalog.view.{groupLabel,grid,list,columns},
catalog.{brandSearch,modelSearch}{Placeholder,NoMatch}, catalog.{hideLocked,
showLocked,lockedSection,lockedSectionHint,inPlanSection,goToModels,
selectBrandHint,columnsBrowseHint,sortLabel}, catalog.sort.{newest,oldest,
alphabetical}.

These primitives stand alone in this commit — the main /catalog and
brand-detail pages migrate to them in 10B and 10C.
2026-06-01 00:23:05 +03:00
1ea53920b0 refactor(web): brand selector controlled state + search; trial banner urgency tiers + progress
BrandSelector
- Now fully controlled — drops the internal `selected` state that could drift
  from the prop source of truth
- Full-plan auto-select effect compares against the live selection and only
  fires when out of sync, removing the infinite-loop risk if parents passed an
  unstable onSelectionChange (now also ref-stashed)
- Search input appears when there are more than 12 brands; Turkish-locale
  case-folding so "Volkswagen"/"volkswagen" both match
- No-match state for empty search results

TrialUrgencyBanner
- Three urgency tiers (soft 7d, urgent ≤3d, critical ≤1d) — colors and
  tighter copy escalate as the trial winds down
- Adds an elapsed-time progress bar so "3 days left" reads as 90% used at a
  glance (number alone undersells urgency at the tail)
- LS dismissal key now userId-scoped (mirrors subscription page)
- Banner emits `tier` alongside days for funnel segmentation
2026-05-31 23:10:08 +03:00
35871403c4 feat(web): billing & Stripe — empty state CTA, failed-payment hint, inline retry, card-brand trust
- Billing empty state now has a ReceiptText avatar and (when no subscription)
  a "choose plan" CTA — beats a bare "no payments yet" line
- Receipt buttons promoted from ghost to outline so users actually find them
- Failed payments show a help icon with a hint (card declined → bank / retry)
- Stripe button: Clock → Loader2 (Clock-as-spinner was awkward)
- Stripe init failure now also surfaces inline with a retry button — a toast
  vanishing in 4s left users with no recovery affordance
- Adds an accepted-cards trust strip (VISA / Mastercard / Troy / 3D Secure)
  below the pay button to ground the secure-checkout copy
2026-05-31 23:06:46 +03:00
984a02e9a3 fix(web): onboarding micro-polish — autofocus CTA, swap hard-coded red token
- Auto-focus the "Şase Aramaya Başla" button when the completed card
  mounts so keyboard users can Enter through without tabbing.
- Replace the hard-coded `text-red-600 dark:text-red-400` on the trial
  error message with the `text-destructive` design token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:27:50 +03:00
229ada8adf feat(web): onboarding — skip affordance, stable layout, robust timing, dismissible close
- Show a "Atla" link once the trial actually provisioned so impatient or
  returning users can jump straight to the confirmation card; captures
  onboarding_skipped.
- Reserve a min-height on the provisioning container so the dialog
  doesn't jerk when the loader/error blocks appear or disappear.
- Re-check animation-ended on visibilitychange — background tabs throttle
  setTimeout, which could leave a user stuck if they tabbed away during
  the 7.5s window.
- Show the default close X again when the modal is dismissible (completed
  or escape-hatch active) instead of hiding it for the whole lifecycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:26:45 +03:00
469a7af67e fix(web): onboarding — locale-aware end date, dynamic plan, completion event
- End date no longer hard-codes tr-TR; follows the active locale.
- The plan badge reads from subscription.plan.key (falls back to "full"),
  so a future change to what the trial provisions doesn't make the UI lie.
- The "Şase Aramaya Başla" CTA captures onboarding_completed, so the
  funnel can distinguish a user who actually clicked through vs one who
  dismissed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:25:10 +03:00
eb7b75fbe8 fix(web): onboarding modal — escape hatch + trial_failed analytics
When the trial mutation either fails repeatedly or just hangs past 30s,
new users were trapped staring at the spinner with no way out. Add:

- A `trial_failed` capture on every mutation error (Onboarding funnel can
  finally measure the activation failure rate).
- After 2 failures OR 30s in provisioning, surface a "still trying"
  panel with "Destek ile konuş" (opens Chatwoot) and "Yine de kapat"
  options, and let interact-outside / Esc dismiss the modal again.
- An openChatwoot() helper + typed `toggle` on the ChatwootApi so other
  components can open the support widget without poking the global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:24:20 +03:00
b9dd154bfd i18n(web): translate the vehicle catalog detail surface
Move every hard-coded Turkish string on /dashboard/vehicles/:id and the
drill-down /categories/:categoryId page into a vehicle.* message block
(tr + en) — page header, error/404 panels with retry, "Araç Bilgileri"
accordion, "Yedek Parça Kategorileri" + the new empty state, equipment
title and show-more/less toggle, summary labels — plus the leaf-category
catalog-error panel and the drill-down's loading/retry copy.

Also translate the CatalogSearch component (placeholder, arias,
searching/treeIncomplete/noMatch hints, section headings, parts-count
badge) under a new catalogSearch.* block, since it lives inside the same
surface and the rest of the page already speaks two languages.

PCAT_LABELS / EMEX_LABELS / VINAPI_FIELDS / LABEL_OVERRIDES are
source-specific data tables, not UI chrome — left as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:42:48 +03:00
f7d5f03572 feat(catalog): hide per-level category filter on vehicle page
The new cross-tree catalog search sits directly above the top-level grid on
the vehicle page, so the grid's own "Kategori ara…" filter was a redundant
second search box. Add a hideFilter prop to CategoryGrid and set it there;
catalog-browse and deep category levels (no global search) keep the filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:35:50 +03:00
8718b08415 feat(catalog): full-catalog search on vehicle page (leaf categories + OEM parts)
The vehicle page search previously only filtered category names at the
currently rendered level. Add a server-side cross-tree search over what's
already drilled into the DB.

New GET /categories/search/:vehicleId?q= returns two sections:
- categories: name-matched leaves UNION the leaf categories that contain a
  matching part (with hit count). "fren balatası" matches no leaf by name —
  the pads are parts under leaves like "Disk freni" — so the union surfaces
  the right leaves.
- parts: parts matching every token on name, or the raw query on oem_code,
  with OEM + leaf + breadcrumb.

Pure DB read (no upstream drill); a treeIncomplete hint is returned when the
vehicle's tree looks barely drilled. Frontend adds a debounced search box on
the vehicle page that hides the normal browse while active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:30:21 +03:00
a09182fb98 feat: Cloudflare Turnstile (register + contact) + captcha altyapısı
- Turnstile widget bileşeni (public site key gömülü, VITE_TURNSTILE_SITE_KEY ile override)
- register: signUp.email'e x-captcha-response header'ı
- contact: token body'de; ContactService Cloudflare siteverify ile doğrular
  (TURNSTILE_SECRET_KEY yoksa atlanır), contact.dto'ya turnstileToken
- @sase/config: TURNSTILE_SECRET_KEY env (opsiyonel)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:14:06 +03:00
10cd67911d refactor(web): move changelog out of settings into its own route
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Product changelog isn't an account setting. Give it a dedicated
/dashboard/changelog route and a "What's New" sidebar link (Support
section), and drop the changelog tab from settings. The header page
title now resolves support-section routes too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:37:28 +03:00
dc7f4627fb a11y(web): associate settings labels with inputs; fix delete-confirm hint
Add htmlFor/id to the email, member-since, referral code, share link and
delete-confirm fields so every Label is programmatically tied to its
Input. Drop the delete-confirm placeholder that echoed the exact confirm
word (which defeated the type-to-confirm friction) and disable autocomplete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:33:38 +03:00
239791290e feat(web): add Preferences tab (theme + language) to settings
New "Preferences" settings tab giving the theme a real home in settings
(light/dark/system selector wired to userSettings + applyTheme) alongside
the language switcher, instead of theme living only in the header toggle.
Theme group uses a fieldset/legend for accessible grouping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:32:15 +03:00
513b856bea feat(web): responsive settings nav — vertical sidebar on desktop
Restructure the 6-tab settings layout: a left vertical tab rail on
desktop (lg) with content beside it, and a horizontally scrollable row
on mobile with labels always visible (were icon-only, ambiguous). Tab
triggers are now mapped from a single TAB_ITEMS array instead of being
hand-repeated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:28:34 +03:00
9633888170 feat(web): settings form states — password reveal, inline validation, loading
- Add a PasswordField with show/hide toggle, hint text and inline error;
  the new-password hint and live "passwords do not match" message now
  render under the fields (submit disabled on mismatch) instead of only
  firing a toast on submit.
- Show skeletons while the connections and referral-stats queries load,
  instead of flashing "Not linked"/0.
- De-nest the referral stat cards (cards-inside-cards) into plain muted
  panels and use tabular-nums for the figures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:27:05 +03:00
c404ef22f4 fix(web): remove non-functional phone field; localize member-since date
The profile phone input was dead UI — there is no phone column in the
schema, PATCH /users/me only accepts { name, image }, and it was never
loaded, so saving the profile sent an empty phone that the API silently
dropped. Remove the field and stop sending phone. Real phone capture
needs a backend migration (follow-up).

Also fix the "member since" date hard-coding tr-TR; it now follows the
active locale like the changelog already does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:24:25 +03:00
d679907e2c feat(web): make settings tabs URL-addressable via ?tab=
Add an optional `tab` search param (validated against the known tab set)
to /dashboard/settings and drive the Tabs component from it, so each tab
is shareable/bookmarkable (e.g. /dashboard/settings?tab=referral). Tab
clicks update the URL with replace to avoid history spam; existing links
need no search param since `tab` is optional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:23:44 +03:00
681a25ec2e feat(web): language switcher in dashboard header + shared component
Extract the footer's TR/EN toggle into a reusable LanguageSwitcher with
two variants: `inline` (segmented row, used by the footer) and
`dropdown` (round Globe icon + menu with active check). Add the dropdown
variant to the dashboard header next to the theme toggle so app users
can switch language without the marketing footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:08:12 +03:00
70b530c020 feat(web): add social links to site footer
LinkedIn, Instagram and Facebook icon links in the footer brand column,
opening in a new tab with rel="noopener noreferrer" and per-icon
aria-labels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:41:15 +03:00
397aef8c35 feat(web): add language switcher to site footer
TR/EN toggle in the footer bottom bar, wired to the i18n store's
setLocale (persists to localStorage, updates <html lang>). Buttons use
aria-pressed within a labelled group; active locale is highlighted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:33:55 +03:00
e90f825ed7 refactor(web): unify dashboard footer via SiteFooter compact variant
Add a `compact` variant to SiteFooter (single-line copyright + legal
links) and use it in the dashboard layout, replacing the divergent
hard-coded inline footer. The dashboard footer is now i18n-aware,
contrast-corrected, and adds the KVKK link for parity with the site
footer. Both layouts now render from one component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:56:50 +03:00
d482d5db6c fix(web): footer content — real anchors, single email, drop fake status
- Point the three product links to distinct, real feature anchors
  (#feature-vin / #feature-oem / #feature-schema); add matching ids and
  scroll-mt to the feature cards in index.tsx (were all -> #features).
- De-duplicate the email: brand-column address is now the single
  mailto link; removed the redundant bottom-bar copy.
- Remove the static "all services operational" indicator that never
  reflected real status (misleading trust signal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:54:39 +03:00
a0e822c082 a11y(web): footer landmarks, headings & contrast
Wrap each footer link column in a labelled <nav aria-labelledby> with an
<h3> (was an ungrouped <h4>), add a top border to delineate the footer,
and raise low-contrast text from muted-foreground/70 to full
muted-foreground (column titles to text-foreground).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:52:13 +03:00
6cefd2d560 i18n(web): translate SiteFooter strings
Move all hard-coded Turkish footer copy (column titles, links, tagline,
copyright, status) into a footer.* message block in tr/en and resolve
them via t(), so the footer follows the selected locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:51:22 +03:00
b71b5d1de8 refactor(web): extract landing footer into shared SiteFooter component
Behavior-preserving move of the inline footer markup out of index.tsx
into components/site-footer.tsx, mirroring the site-header convention.
No visual or content change — sets up the footer for i18n, a11y and
content fixes in follow-up commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:50:17 +03:00
14a1af0f81 fix(web): kategori ağacını eski yapısına döndür (isim kesilmesi hariç) + toggle tree ikonu anında tepki
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
- category-tree: kılavuz çizgileri + grid-rows aç/kapa animasyonu geri alındı,
  orijinal smooth yapı (paddingLeft girinti) restore edildi; uzun isimlerin
  kesilmemesi için truncate yerine break-words korundu
- TreeViewIcon: 80ms gecikmeli/2px küçük hareket yerine gövdeden gecikmesiz
  scaleX büyüme (tree-grow keyframe, origin-left); grid/list ile aynı anında tepki

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:10:38 +03:00
3fe1201516 fix(web): kategori ağacını eski yapısına döndür + toggle tree ikonunu anında tepki verir yap
- category-tree: kılavuz çizgileri + grid-rows aç/kapa animasyonu geri alındı,
  orijinal smooth yapı (paddingLeft girinti) restore edildi
- TreeViewIcon: 80ms gecikmeli/2px küçük hareket yerine gövdeden gecikmesiz
  scaleX büyüme (tree-grow keyframe, origin-left); grid/list ile aynı anında tepki

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:09:40 +03:00
89da489532 fix(web): tree kategori isimleri kesilmesin + tree ikonu anında tepki versin
- category-tree: isim span'inde truncate yerine break-words; derin alt
  kategorilerde uzun isimler artık kesilmiyor, gerekirse alt satıra sarıyor
- TreeViewIcon: remount+keyframe (opacity 0'dan dolma) yerine grid/list ile
  aynı transition mekanizması; dallar hep görünür, aktifte anında kayar
- globals.css: kullanılmayan tree-branch-in keyframe kaldırıldı

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:55:10 +03:00
1db9a632cb feat(web): görünüm toggle için animasyonlu CSS ikonları (grid/tree/columns)
unlumen cards-view/list-view motion gerektirdiği için saf CSS eşdeğerleri:
- CardsViewIcon (grid): 2×2 kareler yer değiştirir
- TreeViewIcon (tree): bespoke dallanan ağaç, kademeli çizilir (tree-branch-in)
- ListViewIcon (columns): 3 çizgi birer slot kayar
isActive ile sürülür, motion-reduce saygılı. CategoryViewToggle + 4 inline
katalog toggle'ı bu ikonlara çevrildi; kullanılmayan lucide import'ları temizlendi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:44:03 +03:00
6499c98ae9 feat(web): kategori ağacına kılavuz çizgileri + yumuşak aç/kapa animasyonu
- Girinti satır-bazlı paddingLeft yerine açılan çocuk grubunda border-l ile;
  her seviyenin çizgisi parent satırın chevron merkeziyle hizalanır
- Aç/kapa CSS grid-rows 0fr↔1fr geçişiyle animasyonlu (motion-reduce saygılı)
- Lazy fetch, şema prefetch/preview, leaf→Link ve a11y korundu

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:43:54 +03:00
ac437eaadb feat(web): Chevrolet logosunu CarBrandLogo'ya bağla
cardog-icons ChevroletLogo'yu sağlıyor (kurulu paketteydi) ama haritaya
bağlanmamıştı; KL1 WMI'sı Chevrolet'e map'lendiği halde harf fallback'i
gösteriyordu. BRAND_MAP'e chevrolet eklendi. (Lancia cardog'da yok, fallback.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:15:01 +03:00
d72e7bb758 fix(web): VIN ikonundaki marka logolarını tutarlı 28px kareye sabitle
Tailwind Preflight img'lere height:auto uyguladığından statik-SVG logolar
en-boy oranına göre ölçekleniyordu (Opel ~17px küçük, Renault ~29px taşma).
VinBrandIcon artık sabit 28px kare + object-contain ile hem img hem svg
logolarını aynı boyutta gösteriyor; büyüteç optik denge için biraz küçük.
Playwright ile landing VIN textbox üzerinde doğrulandı.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:00:22 +03:00
77de860211 feat: VIN textbox WMI marka ikonu + WMI haritasını shared'e taşı/genişlet
- WMI_BRAND_MAP + getBrandFromWmi packages/shared'e taşındı (tek kaynak);
  corgi.service artık buradan import ediyor (davranış aynı, testler geçiyor)
- VinBrandIcon: VIN'in WMI'ı bilinen markaya denk gelince büyüteç yerine
  marka logosu pop animasyonuyla görünür (landing + search VIN textbox)
- WMI haritasına prod DB'de decode edilmiş VIN'lerden 23 eksik WMI eklendi
  (Türkiye fabrikaları NM4/NMT/NLA/NLH/NMB dahil; tüm DB WMI'ları artık tanınıyor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:51:40 +03:00