Commit Graph

116 Commits

Author SHA1 Message Date
34bbb01fed feat(web): align pricing cards with subscription page
Bring the public pricing cards in line with the in-app subscription page and
strengthen the CTA:
- Move the "Popüler" badge from 2 Marka to Full Paket (one consistent story
  across both surfaces).
- CTA "Başla" -> "Ücretsiz Başla" (leans on the free trial the flow grants).
- Yearly line now uses a consistent ₺ format ("veya ₺2.000,00/yıl") plus a
  "· 2 ay bedava" savings cue instead of the bare, differently-formatted
  "veya 2.000,00 TL/yıl".
- Plan-decision helper above the grid ("Hangi planı seçmeliyim? …").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:10:43 +03:00
d49964aac1 feat(web): value-focused pricing hero with trial + risk-reversal trust line
The pricing hero was purely procedural ("Fiyatlandırma" / "İhtiyacınıza
uygun planı seçin") with no value prop and no mention of the free trial —
the strongest conversion lever, present in the meta description and header
CTA but absent from the page body. Reworked into an eyebrow + outcome
headline ("Doğru parçayı saniyede bul") + supporting line, plus a trust
row: 30 gün ücretsiz · kredi kartı gerekmez · istediğin zaman iptal ·
7 gün iade garantisi (reusing existing subscription trust keys).

Also sorted pricing.tsx imports to satisfy biome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:08:35 +03:00
85f4252f7e feat(web): default to yearly billing, make Full Paket the popular plan
Per request: shift the "Popüler" emphasis from the monthly-anchored 2 Marka
to Full Paket, default the billing toggle to yearly, and put a "+2 ay hediye"
gift badge on the yearly tab (replacing the abstract -17%). Removed the
now-redundant "2 ay bedava" note under the toggle since the badge carries
that message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:49:56 +03:00
a631380e74 fix(web): stop mobile layout viewport expanding past device width
On mobile the dashboard opened "zoomed in" — the layout viewport reported
~548px instead of the device's 390px, so users had to pinch-zoom out to
fit. Root cause: the dashboard main column (flex flex-1 flex-col) is a flex
item with the default min-width:auto, so it refused to shrink below its
content's intrinsic width. The subscription feature-comparison table
(~514px: a w-48 label column + 4 plan columns) therefore dragged the whole
column — and the mobile layout viewport — wider than the screen, despite
the table sitting in an overflow-x-auto wrapper.

Adding min-w-0 lets the column shrink to the viewport; the wide table now
scrolls inside its own container. Verified with iPhone-class emulation
(390px): innerWidth/scrollWidth = 390 with no horizontal overflow across
/dashboard, /dashboard/search and /dashboard/subscription. Fix lives in the
shared dashboard layout, so it repairs every dashboard page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:43:33 +03:00
4ba06615a1 style(web): satisfy biome on subscription page (format + a11y)
Run biome --write formatting on the subscription page + new tests, and
swap the trial-progress banner's role="status" for aria-live="polite"
(keeps the polite live-region announcement without tripping
lint/a11y/useSemanticElements).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:31:08 +03:00
0bae337c69 feat(web): show persistent trial-progress banner for days 8-30
Trial users previously got no reminder of remaining days until the urgent
(<=7d) amber banner appeared, leaving most of the 30-day window with no
conversion nudge. Added a calm, brand-toned TrialProgressBanner shown when
more than 7 days remain: days left, a progress bar (elapsed share of the
30-day trial), and a "Planını seç" CTA that scrolls to checkout and fires
trial_progress_cta_clicked. The urgent banner still owns the final 7 days.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:29:42 +03:00
2912dc6eb2 feat(web): add credibility strip to subscription page
The page had no credibility signal at the point of conversion. Added a
CredibilityStrip under the header (shown only to converting users:
no-sub / trial / expired / pending) surfacing real, verifiable product
facts — 50+ marka, 1M+ parça, OEM + alternatif kodlar, saniyede sonuç,
KVKK uyumlu · SSL. Deliberately no fabricated customer counts or
testimonials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:27:43 +03:00
daaf616335 feat(web): add plan decision helper + trial value hook
The brand-count plan model leaves users unsure whether they need 1/2/3
brands. Added a PlanHelperHint above the grid ("Hangi planı seçmeliyim? …
4+ marka için Full Paket marka başına daha avantajlı"). Also added an
outcome-focused hook line to the trial card ("Şase numarasını gir, doğru
parçayı saniyede bul …") above its feature list, selling the result rather
than only listing features.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:25:55 +03:00
ef4ebec025 feat(web): reinforce value on subscription plan selection
Three CRO additions at the point of decision:
- Outcome-focused page subtitle ("Doğru parçayı saniyede bul …") replacing
  the purely procedural copy.
- Per-day price anchor on every plan card (≈ ₺X/gün), reframing the cost as
  a small daily number. Computed from the displayed monthly/yearly price.
- "2 ay bedava" note under the billing toggle when yearly is selected,
  making the otherwise-abstract -17% concrete (yearly = 10× monthly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:23:21 +03:00
02ea505af7 feat(web): add order summary to subscription payment step
At the payment step the user previously saw only the pay button with no
recap of what they were buying — a trust + friction gap right before
checkout. Added an OrderSummary block (plan, selected brands, billing
period, total) above the Stripe button, wired through the already-defined
but unused orderSummary* i18n keys. Brand names resolve from the cached
/brands query; the full plan shows "Tümü seçildi".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:19:37 +03:00
b32c777951 fix(web): render plan names + price in subscription feature matrix headers
The comparison table's column headers were empty <th/> cells (aria-label
only), so sighted users could not tell which column belonged to which plan.
Headers now show plan name + monthly price + the Popüler badge. Added a
"Marka sayısı" row exposing the brand limit (1/2/3/Tümü) — the attribute
that actually differentiates the brand1/brand2/brand3 tiers, which the
matrix previously omitted entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:16:57 +03:00
3926e276fd feat(referrals): rework reward engine + email-verified landing
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Reward engine:
- Recurring milestones (every 3 → +7d, every 5 → +14d) instead of one-time
  tiers capped at 5; idempotent + transactional grants serialised per
  referrer so concurrent qualifications can't double-count.
- Rewards now gated on the referred user's email verification
  (afterEmailVerification hook); already-verified referees (OAuth) qualify
  at apply time.
- Reward days banked as users.referral_credit_days when the referrer has no
  live subscription, consumed on next trial start / activation (no more
  silently lost rewards).
- Accurate cumulative rewardDays in stats; getMyReferrals returns referee
  name/masked email/status.

Hardening / cleanup:
- onConflictDoNothing makes apply idempotent (no unhandled unique violation).
- Anti-fraud: normalizeEmail blocks self-referral via gmail dot/+tag aliases.
- Collision-safe referral code generation at signup.
- Single apply path (welcome onboarding modal); removed duplicate calls in
  register + subscription pages. Input validation on the apply code.

Email verification UX:
- Verification link now lands on a dedicated /email-verified confirmation
  page instead of the deep-linked VIN/search page.

Schema: referrals.status + qualified_at, users.referral_credit_days (0009).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:28:32 +03:00
125c6fc431 feat(web): show vehicle-shaped shimmer while decoding instead of a frozen button
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
When Şase Çöz is clicked and the decode (reused pre-decode or fresh) is still in
flight, the search form is replaced by a vehicle-page-shaped skeleton with a
sweeping shimmer, so the wait reads as "the page is loading" rather than an
unresponsive button. On success we navigate; candidates/errors fall back to the
form. When the pre-decode already resolved, the skeleton is near-instant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:57:12 +03:00
e96f311651 fix(web): reuse speculative pre-decode on Şase Çöz instead of decoding twice
The 17-char speculative pre-decode fired a POST /vehicles/decode and discarded
the result, then the button click fired a second identical decode — two
requests per VIN. Keep the pre-decode promise and have runDecode reuse it on the
first attempt (awaiting it if still in flight), so the click navigates off the
already-running decode. Retries and the sub-350ms "clicked before warm" case
fall back to a fresh decode; a failed warm retries once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:45:23 +03:00
df85ebad83 feat(web): show retry prompt when a category fails to load from source
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
When getCategoryWithParts reports loadError (the PL24 catalog fetch failed
rather than the category being genuinely empty), the leaf view now renders a
"Katalog şu an yüklenemedi — tekrar dene" alert with a retry button instead of
the misleading "Bu kategori için parça bulunamadı" empty state. Genuine empty
leaves are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:36:46 +03:00
77947cd162 feat(web): clean repeated tokens from model names
Catalog sources double up tokens in model names — "Laguna (Laguna)",
"Golf 1,6 GOLF", "Golf TDI Variant GOLF". Add a shared cleanModelName helper
that drops a parenthetical repeating surrounding text and exact case-insensitive
duplicate tokens (order preserved, conservative on ambiguous cases). Applied to
the vehicle page h2, the collapsed summary, and the category breadcrumb. Unit
tested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:55:56 +03:00
0bd9165e66 feat(web): show engine code in summary for all decode sources
The collapsed summary's "Motor kodu" read only PL24's rawData.engineCode, so it
was blank for emex/pcat/vin-api. Resolve it per source: PL24 engineCode, emex
parsedOptions.engine_type, vin-api engineModel, or the pcat "engine" parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:48:10 +03:00
8fe731b7e0 feat(web): add emex + vin-api vehicle attribute extraction
Extend the source dispatcher to the remaining decode sources. emex surfaces its
parsedOptions codes (engine_type/gearbox_type/vehicle_type → Motor tipi /
Şanzıman tipi / Araç tipi), led by any readable DB columns (Vites, Kasa).
vin-api surfaces its flat fields (bodyClass, engineModel, transmissionStyle,
plantCountry → Kasa / Motor / Vites / Üretim ülkesi). Both replace the bare
3-field DB fallback these vehicles showed before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:47:42 +03:00
1549c14a09 feat(web): source-aware vehicle attributes — surface pcat parameters
Vehicle info only ever read PL24's vinfoBasic segment, so emex/pcat/vin-api
vehicles fell back to 3-4 sparse DB columns. Introduce a getDisplayAttributes
dispatcher keyed on the decode source. First source added: parts-catalogs,
which ships a structured pcatCar.parameters list (Year, Region, Steering,
Transmission type/code, Engine, Type, Configuration…) — sorted by sortOrder
with English names mapped to Turkish. A shared header-dedup drops the
brand/model/year rows that already live in the h2 and the collapsed summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:47:01 +03:00
dd95641497 feat(web): surface decoded equipment/PR-code list in vehicle card
The PL24 VIN decode already ships a readable description for every PR code in
the prNr segment (187 here for the VW Golf) — surface it instead of guessing a
dictionary. The expanded accordion now shows a "Donanım kodları" section: each
row pairs the code as a mono badge with its decoded Turkish description (paint
and color entries included), in a scrollable two-column list. Raw \r\n and \-
escapes are cleaned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:28:52 +03:00
2075631c65 feat(web): make vehicle info card a collapsible accordion
Collapse the Araç Bilgileri card by default. The always-visible header now
shows an at-a-glance summary (Model, Model yılı, Motor kodu); expanding reveals
the full attribute grid, which also gains the decoded drive type (Çekiş /
"Önden çekiş") from rawData. The loading skeleton now matches the collapsed
header so there's no layout jump when data lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:27:55 +03:00
531ab35a89 polish(web): tidy dashboard shell — emoji, dead bell, footer links, dvh
Remove the emoji from the header greeting and the duplicate avatar/name block
(the sidebar already shows the user). Remove the bell button that had no handler
or notification system behind it. Turn the footer's plain "Gizlilik Politikası,
Kullanım Koşulları" text into real /privacy and /terms links (matching the
landing footer). Switch min-h-screen to min-h-[100dvh] so the layout doesn't
jump on mobile Safari.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:13:54 +03:00
08aa3ac88f feat(web): add error/not-found state + safe back nav to vehicle page
The vehicle detail page had no error handling: a failed /vehicles/:id (404 or
network) rendered a broken header ("  ()") with no retry, while only the
categories sub-page had a proper error block. Now a failed load shows a
not-found vs. generic-error alert with retry (mirroring the category page), and
404s skip retries so the state appears immediately. The back button now uses
the router history and falls back to /dashboard/search on deep links instead of
window.history.back() leaving the site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:09:20 +03:00
45ac5783b5 feat(web): make vehicle info card readable instead of raw VIN codes
The Araç Bilgileri card showed only raw PL24 VIN-decoded codes (Satış tipi
1J10E4, Donanım 82, Motor kodu BCB…) and fully suppressed the friendlier DB
fields. Now it leads with the readable engine description + body type, then the
decoded codes; drops the Model/Model-yılı rows that duplicate the h2 header;
maps the known mojibake labels to clean Turkish; drops the uppercase transform
(which corrupted the Turkish dotted/dotless i); and lays out 2 cols on mobile
with long values spanning the full row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:07:38 +03:00
c2960013f0 feat(web): speculative pre-decode on a valid VIN (faster counter lookups)
The moment the VIN reaches 17 valid chars, warm the full decode in the background
(350ms debounce, deduped per VIN, fire-and-forget) so the "Şase Çöz" click hits
the backend lock / 24h positive cache and returns near-instantly. If the warm is
still in flight when the dealer clicks, the button's decode lock-waits on it and
navigates the moment it lands.

No analytics/state/navigation from the pre-decode — vin_decoded and the
success/candidates/error events still fire only from the button path, so the CRO
funnel stays = real intent. Raw fetch + AbortController (api.post takes no signal),
mirroring the existing 17-char preview effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:54:22 +03:00
70ee3d637f fix(web): fire checkout_started when the payment step is reached (all plans)
checkout_started only fired for the "full" plan (plus a mostly-unreachable
sticky-CTA branch), so non-full-plan users reached payment without it ever
firing — PostHog showed fewer checkout_started (1 person) than the downstream
payment_initiated (3), which is impossible in a clean funnel.

Move it to a step-driven effect that fires once when the payment step is
reached, via any path (plan card, sticky CTA, or a prefilled pending plan),
re-arming if the user returns to plan selection. Fixes the Monetization and
Ödeme funnels at the checkout step and keeps plan -> checkout meaningful
(brand-selection drop-off now shows up instead of a trivial 100% step).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:34:43 +03:00
195b3204c3 feat(web): land new signups on VIN search, activation as a modal
New signups were redirected to /dashboard/subscription (the paywall):
91% of them hit it and 78% bounced without ever searching a VIN (PostHog,
30d). Acquisition is strong (homepage -> register) but activation was the
cliff — only 13% of new users ever reached the search page.

- register: redirect signups to /dashboard/search?welcome=1 (value-first),
  carrying the landing-page VIN + referral code across.
- search: read welcome/vin/ref params; prefill a carried VIN and auto-decode
  it once the activation modal is dismissed (no re-typing); strip params after.
- WelcomeOnboardingModal: the old full-page provisioning view is now a modal
  over the search page. It applies the referral (covers the Google OAuth
  signup path), starts the trial, plays the Remotion onboarding motion, then
  steps aside so the user lands on their vehicle's parts.
- trial_started now fires for every eligible new user (was a rarely-clicked
  button) — this populates the Onboarding funnel, which previously read 0.
- tests: add useSearch to the router stub; refresh the stale retry-CTA tests
  to the shipped submit-doubles-as-retry behavior (retry CTA dropped in a33deef).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:24:53 +03:00
49891a8df6 chore(web): contact address label 'Hak Sahibi' → 'Firma Adı'
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:04:20 +03:00
ea96e7fc25 chore(web): update contact page address to THINXTRA LLC
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:58:13 +03:00
6f4d6eff5c fix(web): consistent SiteHeader across all public pages
Blog and contact (and about/pricing/legal) pages rendered a stripped-down
header (logo + auth buttons only) with no nav — the site nav "disappeared".
Extracted the landing header into a shared <SiteHeader> (logo, nav, theme
toggle, auth-aware actions, mobile menu) used on every public page for
consistent UX.

- new components/site-header.tsx (nav links work from any route via /#…)
- landing nav gains Blog + İletişim so all pages share the same 5-link nav
- about, contact, pricing, blog, blog/$slug, kvkk, privacy, terms → SiteHeader
- demo keeps its purpose-built demo header (sticky + theme + CTA)

typecheck + biome clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:45:21 +03:00
e5c852b537 feat(blog): data-driven blog API + DB for content-pipeline publishing
Makes the blog publishable from the Süper Panel content pipeline (via n8n).
Backend mirrors the changelog module; frontend keeps the existing hand-authored
posts and merges in API-backed ones (no content migration, no regression).

API (apps/api):
- blog_posts Drizzle table (slug unique, title, meta_description, body_markdown,
  tags, cover_image, status, source, published_at)
- blog module: GET /blog/posts (public list), GET /blog/posts/:slug (public),
  POST /blog/posts/internal (Bearer BLOG_AUTOMATION_TOKEN, mirrors
  changelog/internal) — returns { ...post, url }
- BlogService: Drizzle + Redis cache, defensive Turkish-aware slugify +
  uniqueness; registered in app.module

Web (apps/web):
- use-blog hooks (list + by-slug, react-query, 30m staleTime)
- blog list: static + API merged, dedup by slug, newest first
- blog detail: static post renders as before; API post renders body_markdown
  via react-markdown (remark-gfm) styled to match existing prose
- add react-markdown + remark-gfm

Deploy: set BLOG_AUTOMATION_TOKEN (and optional PUBLIC_WEB_URL) on the api,
run drizzle-kit db:push to create blog_posts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:48:10 +03:00
1b2cfd4a0f feat(dashboard): kategori sayfalarında ana alanı tam genişlik yap
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Araç ve katalog model detay sayfalarındaki max-w-4xl cap'i kaldırıldı;
kategori grid'i geniş monitörde xl:4 / 2xl:5 sütuna çıkıyor. Geniş
ekranlarda yanlardaki boşluk dolduruluyor ve kategori isimleri artık
kısaltılmadan sığıyor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:23:06 +03:00
8e57d20eaf chore(telemetry): tag vehicle + category interactions for Faro
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Only the search route was emitting data-faro-user-action-name attributes, so
the vehicle detail and category detail pages were dark in our Faro user-action
funnels even though they sit on the critical post-decode path.

Added attributes for:
- Vehicle page back button (`vehicle-back`)
- Category page back button (`category-back`)
- Category error retry button (`category-retry`)
- Breadcrumb root vehicle link (`breadcrumb-vehicle`)
- Breadcrumb intermediate category links (`breadcrumb-category`)
- View toggle buttons (`category-view-grid|tree|columns`) — emitted from the
  shared component so any future consumer inherits the tagging.

No behavioral change; pure observability.

Phase 8/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:55:13 +03:00
b110b917b6 fix(vehicle-page): proper dl semantics, layout-matched skeleton, empty state
- VehicleAttributes wrapped in <dl> (was emitting <dt>/<dd> with no list
  parent). Both the rich vinfoBasic branch and the DB-fallback branch now
  share one layout instead of diverging into span-based and dt/dd-based
  trees — easier to style and screen-reader friendly.
- New explicit empty state ("Bu araç için ayrıntı bilgisi bulunamadı") when
  both branches resolve to nothing. Previously rendered an empty grid.
- Loading skeleton now mirrors the actual page structure (header chip + logo
  + 2-line title, info card with 6 attribute slots, categories card with 8
  rows). The previous three plain rectangles caused a layout jump on resolve.
- Attribute rows use small-caps muted labels and tabular-nums values for
  scannable data (engine codes, mileages, years).

Phase 7/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:54:10 +03:00
a33deef040 fix(error-states): retry CTA on category error, drop duplicate retry on search
Category page: the previous error UI was a single flat red block with no
recovery path — users had to refresh the browser. Now it uses the standard
alert pattern (heading + detail + action), surfaces the actual error message
when one is available, and offers a Tekrar dene button wired to refetch().

Search page: removed the dedicated "Tekrar Dene" button inside the error
banner. The main Şase Çöz submit button sits immediately above and remains
enabled after an error — having two near-identical CTAs stacked on top of
each other was just noise. The error banner is now informational only.

Also drops the now-unused handleRetry function and RotateCcw import in the
search route.

Phase 6/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:52:54 +03:00
cfe090b45c fix(search): left-aligned header, drop the hero card, debounce auto-fix toast
Two unrelated polish items in the VIN search page bundled into one commit
because they touch the same file.

1. Hero card removed. The entire form lived inside a generic
   `rounded-2xl border bg-background p-6 sm:p-8` container on a page that had
   no other content competing for attention — the card added no hierarchy,
   only a frame. The header is also no longer centered: icon sits to the left
   of a left-aligned h2 + subtitle, breaking the AI-default centered hero.

2. I/O/Q auto-correction toast is now debounced. Each keystroke that produced
   a correction fired its own toast, so a user holding the I key or pasting
   "IIO" stacked three toasts on top of each other. Corrections now collect
   in a ref-backed Set and a single consolidated toast fires 400ms after the
   last edit, with cleanup on unmount.

Phase 5/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:51:19 +03:00
25300a339a refactor(category-view): extract shared toggle component with a11y
Both the vehicle detail page and the category detail page shipped their own
copy of a three-button view-mode toggle (Grid/Tree/Columns). They diverged
subtly and shared the same accessibility gaps: 28px tap targets, only `title`
attributes for screen readers, no `aria-pressed`, no focus ring.

- New `CategoryViewToggle` (apps/web/src/components/categories/) renders a
  proper `role="group"` segmented control with `aria-pressed`, `aria-label`
  per option, 36px tap targets, and a focus-visible ring.
- Active state is conveyed by an elevated background pill rather than just a
  hover-grey, so the selected mode is legible without color contrast guessing.
- Both pages collapse to a single line: `<CategoryViewToggle … />`.

Phase 4/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:49:38 +03:00
34ad77b2a1 feat(category-page): add breadcrumb trail + safer back navigation
Addresses long-term fix from insight cmpifhxfs000lvze9cy6ne0tx: users in
deep category trees had no orientation cue beyond a single back button.

- Breadcrumb derived from the cached /categories/tree/<vehicleId> response.
  When the cache is warm (user came from the vehicle page), the full ancestry
  renders; otherwise it falls back gracefully to "Vehicle → Current".
- Vehicle label fetched via the same query key already used by the vehicle
  page, so the request is deduped.
- Back button now prefers the breadcrumb parent (resolved synchronously from
  cache) over `data.parentId` (which is async). Clicking back before the
  category payload loads no longer dumps the user to the vehicle root.
- H1 placeholder during load swapped from a stale "Kategori Detayı" string
  to an inline pulse — prevents the title from briefly displaying wrong text
  before the real name resolves. Uses a span-based pulse (Skeleton is a div
  and would be invalid HTML inside an h1).

Phase 3/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:48:15 +03:00
3478144c87 fix(dashboard): restore Turkish diacritics in vehicle + category pages
Strings displayed to users were missing Turkish characters (ö, ç, ğ, ş, ı, ü).
Affects vehicle detail header/info card, category detail page, view-mode tooltips,
and the "category not found" empty state in the tree component.

Phase 1/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:44:28 +03:00
b4f1b2bd10 feat(ads): wire Meta Pixel for ad attribution
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Pixel ID 1904240520247944 (sase.tr) created on ad account
act_1227112768351770. Lazy-injected like PostHog so it stays off
when VITE_META_PIXEL_ID is unset. Tracks PageView on every route
change, CompleteRegistration on email/Google signup, and
InitiateCheckout on Stripe button click (value in TRY).

Coolify env: set VITE_META_PIXEL_ID=1904240520247944 before next
deploy so it gets baked into the Vite build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:28:40 +03:00
Fusion
5118c8e5e9 feat(FN-415): add locator-stability + hit-target verification tests
Commits merged:
- feat(FN-415): add locator-stability + hit-target verification tests

Files changed:
apps/api/src/telemetry/__tests__/telemetry.spec.ts |  8 +++
 .../src/routes/__tests__/dashboard-search.test.tsx | 67 ++++++++++++++++++++++
 2 files changed, 75 insertions(+)

Fusion-Task-Id: FN-415

Fusion-Task-Lineage: 01e6f3d2-cd2e-4023-b538-ea54926b4f87
2026-05-17 06:17:51 +00:00
Fusion
65daf99b2e chore(payments): remove EFT/bank-transfer method, keep Stripe only
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Removes the EFT (havale) payment surface across API, web, i18n, and docs.
Card payment via Stripe is now the only checkout path.

API (apps/api/src):
- Delete payments/bank-accounts/ module (controller + service + module)
- payments.service: drop EFT methods (createEftPayment, uploadEftReceipt,
  approveEft, rejectEft, getActiveBankInfo, getPendingEftPayments) and the
  BankAccountsService dependency
- payments.controller: drop /payments/eft, /payments/eft/:id/{receipt,approve,reject},
  /payments/bank-info, /payments/pending; keep /payments/me
- payments.module: drop BankAccountsModule import
- admin.service: drop pending EFT counter from getDashboardStats; drop
  getPendingPayments (admin EFT approval list)
- admin.controller: drop /admin/payments/pending endpoint
- admin.service.spec: drop the getPendingPayments + pendingPayments assertions

Web (apps/web/src):
- Delete components/payment/bank-transfer-card.tsx
- Delete routes/dashboard/admin/payments.tsx (EFT approval page); regenerate
  routeTree.gen.ts
- subscription/index.tsx: drop EFT tab from PaymentMethodSection, drop the
  paymentMethod state + handleEftCompleted, simplify ConfirmationCard to the
  Stripe-only path, drop unused imports (Tabs, BankTransferCard, Building2)
- billing.tsx: drop "eft" from method filter chips; keep methodLabels.eft and
  the legacy receipt download so historical EFT records still display
- dashboard.tsx + admin/index.tsx: drop /dashboard/admin/payments from the
  sidebar + admin quick-link grid; trim unused icon imports; KEYS_6 → KEYS_5
  for the now-5-card admin stats skeleton
- messages/{tr,en}.json: strip every EFT-only key under payment.* (eftTransfer,
  eftConfirmationDescription, eftPaid, eftStatus, bank.*, uploadReceipt* etc.)

Schema (apps/api/src/database/schema):
- bankAccounts table + payments.bankAccountId column are kept as-is: legacy
  EFT payments remain visible on the billing page and the schema preserves
  historical records.

Docs:
- README.md: "Stripe (kart) + EFT" → "Stripe (kart)"
- CLAUDE.md: stack table + PaymentsModule row + payments table note + route
  list updated; admin EFT approval route removed

Verification:
- pnpm typecheck: green (api + web + shared + config + ui)
- pnpm lint: green
- pnpm test: 20 web tests pass, 173 api tests pass
2026-05-16 11:03:30 +03:00
Fusion
e274e02c23 feat(FN-367): add inline retry affordance after VIN decode failure (FN-367, gitea #11)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Commits merged:
- fix(web): add inline retry affordance after VIN decode failure (FN-367, gitea #11)

Files changed:
apps/api/src/telemetry/__tests__/telemetry.spec.ts |   2 +-
 .../src/routes/__tests__/dashboard-search.test.tsx | 172 +++++++++++++++++++++
 apps/web/src/routes/dashboard/search.tsx           | 145 ++++++++++-------
 3 files changed, 266 insertions(+), 53 deletions(-)

Fusion-Task-Id: FN-367

Fusion-Task-Lineage: 5dccf49f-5c77-4fe4-ae2d-0273016d517d
2026-05-14 13:55:36 +00:00
Fusion
a0deefd4b2 chore(remotion): acknowledge Remotion license on Player instances
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Suppresses the 'license required' console warning on the homepage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:40:57 +00:00
Fusion
5bd5325367 feat(FN-356): add checkout_started PostHog event to subscription checkout flow (+1 more)
Commits merged:
- chore(FN-356): verify checkout_started event is in place
- feat(FN-356): add checkout_started PostHog event to subscription checkout flow

Files changed:
apps/web/src/routes/dashboard/subscription/index.tsx | 4 ++++
 1 file changed, 4 insertions(+)

Fusion-Task-Id: FN-356
2026-05-14 10:00:40 +00:00
Fusion
906eef148d feat(FN-346): add CLS regression test for skeleton plan grid (+1 more)
Commits merged:
- feat(FN-346): add grid-parity integration test for skeleton CLS regression
- feat(FN-346): add CLS regression test for skeleton plan grid

Files changed:
.../subscription/__tests__/cls-regression.test.tsx | 128 +++++++++++++++++++
 .../subscription/__tests__/grid-parity.test.tsx    | 141 +++++++++++++++++++++
 .../src/routes/dashboard/subscription/index.tsx    |   4 +-
 3 files changed, 271 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-346
2026-05-14 05:09:47 +00:00
Fusion
aaf2e763cb feat(FN-345): fix skeleton layout shift, dialog overflow, and safe-area padding (+1 more)
Commits merged:
- fix(FN-345): add mobile-safe max-width to cancel confirm dialog
- fix(FN-345): fix skeleton layout shift, dialog overflow, and safe-area padding

Files changed:
apps/web/src/routes/dashboard/subscription/index.tsx | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-345
2026-05-14 03:05:45 +00:00
Fusion
561b02b3d2 feat(FN-343): remove lingering iyzico references after Stripe migration (+1 more)
Commits merged:
- chore(FN-343): remove lingering iyzico references from docs, config, and scripts
- feat(FN-343): remove lingering iyzico references after Stripe migration

Files changed:
CLAUDE.md                                    | 10 +++++-----
 README.md                                    |  2 +-
 apps/api/src/database/schema/core.ts         |  1 +
 apps/web/src/messages/en.json                |  1 -
 apps/web/src/messages/tr.json                |  1 -
 apps/web/src/routes/dashboard/billing.tsx    |  6 +++---
 docker-compose.coolify.yml                   |  5 ++---
 docs/INDEX.md                                | 27 ++++++++++++-------------
 knowledge.md                                 | 30 ++++++++++++++--------------
 packages/shared/src/constants/error-codes.ts |  1 -
 packages/shared/src/index.ts                 |  1 -
 packages/shared/src/types/payment.ts         | 12 +----------
 scripts/fn342-pw-verify.mjs                  |  2 +-
 scripts/validate-env.sh                      |  3 +--
 14 files changed, 43 insertions(+), 59 deletions(-)

Fusion-Task-Id: FN-343
2026-05-14 02:31:42 +00:00
Sase Dev
02bf7342a1 fix(subscription): recover stepper when a pending checkout is abandoned
Some checks failed
Deploy / Deploy to Production (push) Has been cancelled
A user who picked a plan + brands, clicked the checkout button to
create the pending subscription, then closed the tab without paying
came back to a frozen "Mevcut Plan · Bekliyor" card with no payment
button and no way to edit. Status was "pending", which my conditional
treated as an active subscription and routed away from the stepper.

Frontend
- hasActiveSub narrowed to active|cancelled only. Pending users now
  see the stepper again.
- New effect pre-fills selectedPlanKey, selectedBrandIds,
  billingPeriod from the pending row on first render and jumps
  straight to the payment step.
- New PendingPaymentBanner at the top of the stepper with
  "Odemeye devam et" (scrolls + sets step=payment) and "Vazgec"
  (calls cancel-pending then resets selection).

Backend
- subscriptions.service.create() auto-expires existing pending
  rows for the user before inserting a new one — abandoned checkouts
  no longer accumulate and a fresh attempt with a different plan
  doesn't 409.
- New PATCH /subscriptions/cancel-pending + service method for
  the banner's explicit cancel action.

i18n: subscription.pendingBanner.* (tr/en).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:57:13 +00:00
Sase Dev
6a0b36a7a0 feat: replace Iyzico with Stripe + single-page subscription stepper
Subscription/checkout flow rewritten end-to-end. The plan-card "Devam Et"
button silently wiped brand selection on re-click and the actual proceed
button lived offscreen — fixed by collapsing plan/brands/payment/confirm
into a single vertical stepper with one sticky CTA.

Backend
- Stripe Hosted Checkout (`/payments/stripe/checkout`) + webhook
  (`/payments/stripe/webhook`, raw body) replacing the stubbed Iyzico
  module. Webhook activates subscription on `checkout.session.completed`,
  expires the pending subscription on cancel/expire so users can retry.
- New `bank_accounts` table — multiple rows, single `is_active` enforced
  by a partial unique index. Admin CRUD under `/admin/bank-accounts`
  with multipart QR upload to MinIO; atomic `/activate` swap in a
  transaction; `GET /payments/bank-info` returns the active row.
- `payments` gains `stripe_session_id`, `stripe_payment_intent_id`,
  `bank_account_id`. EFT flow now reads the active bank account at
  payment time and stores the FK for reconciliation.
- Env: `IYZICO_*` removed, `STRIPE_*` added (validated by zod schema).
- `main.ts` `rawBody: true` for Stripe signature verification.
- Drizzle 0003 snapshot id collision fixed (VIEW-only migration shared
  prevId with 0002, blocking new generates).

Frontend
- `/dashboard/subscription` rewritten as a 4-step vertical stepper with
  step-aware sticky bottom CTA; plan re-selection is idempotent and
  preserves brand state. `/dashboard/subscription/pay` deleted; Stripe
  returns to the same page via `?stripe=success|cancelled` and the UI
  polls `/subscriptions/me` until the webhook activates the row.
- New components: `bank-transfer-card.tsx` (DB-driven IBAN + Kolay Adres
  + uploaded QR image + receipt upload) and `stripe-checkout-button.tsx`.
- Active subscription view, trial onboarding/urgency banner, downgrade
  and cancel dialogs preserved.
- TR/EN i18n: new `subscription.steps.*`, `subscription.stickyCta.*`,
  `payment.stripe.*`, `payment.bank.*`; provider label updated.

PostHog: `method: "iyzico"` → `"stripe"`; new events `iban_copied`,
`kolay_adres_copied`, `qr_viewed`, `eft_initiated`,
`stripe_redirect_returned`.

Deploy runs `db:migrate` which applies 0004_hot_quicksilver
(additive: new table + nullable columns; safe to apply on prod).
Operator must add `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`,
`STRIPE_WEBHOOK_SECRET` to env and create the first
`bank_accounts` row via the admin endpoint before the bank tab works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:42:28 +00:00