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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
- 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>
- 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>
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>
- 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>
- Kayan şeritte metin yerine CarBrandLogo (cardog-icons + statik SVG'ler)
- Logoya tıklayınca seçilen marka adı SmartAnimateText ile karakter karakter yazılır
- globals.css: char-in keyframe + animate-char-in utility
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks in the catalog regression check after CategoryGrid switched from
in-place stack drilling to per-level route navigation: catalog-mode parent
clicks route to the catalog category page preserving variant + mgp search
params (no vehicles-mode cache seeding), and vehicles-mode parent clicks
route to the category page and seed the query cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vehicle category flow had two competing navigation models: the grid
drilled in place (no URL change, own breadcrumb) while the category route
had a separate breadcrumb reconstructed from the cached tree plus a back
button with different semantics. The route breadcrumb collapsed on
refresh/deep-link and for lazily-fetched deep nodes, and browser-back from
an in-grid drill ejected the user out of the whole flow.
Make the URL + a server-provided ancestor trail the single source of truth:
- API: getCategoryWithParts now returns `ancestors[]` (root→parent) via a
recursive CTE, so breadcrumbs are complete regardless of client cache.
- New shared <CategoryBreadcrumb> (Arama → vehicle → ancestors → current)
used by both the vehicle and category pages.
- Category page back button derives the parent from `ancestors` (no more
cache-path race); cache reconstruction (findCategoryPath) removed.
- CategoryGrid drills via route navigation per level (each level a URL +
history entry), seeding the query cache for instant render. Browser-back
now goes up one level and deep levels are shareable/refreshable.
- Tree & Columns intentionally keep their in-place paradigm.
- a11y: aria-labels on back buttons and the breadcrumb nav.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracted CredibilityStrip out of the subscription route module into
@/components/credibility-strip so the public pricing page can reuse it
without pulling in the heavy dashboard/Stripe/Remotion bundle. Subscription
now imports it (dropping the now-unused Boxes/Hash/Layers/Zap icons); pricing
renders it below the plan cards to justify price with real product facts
(50+ marka, 1M+ parça, OEM, saniyede sonuç, KVKK·SSL). Test import updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deep category levels are hard to scan — Elektrik has 62 children, Kaporta 58.
Add a name filter input that appears when a level has more than 8 items and
filters the current drill level live (Turkish-aware lowercasing for İ/ı), with a
clear button and a "no match" message. Resets automatically when navigating
between levels. Benefits the vehicle, catalog, and category sub-page flows since
they all share CategoryGrid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root-level category grid cards clipped their names to one line ("Yakıt, egzoz
gaz…", "Ön aks, direksiy…"). Switch the icon card title from truncate to
line-clamp-2 so two-line names stay legible (matching the image card). Also add
a focus-visible ring to the grid category buttons so keyboard users can see
where they are — the tree view already had one, the grid did not.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
Addresses insight cmpifhxfs000lvze9cy6ne0tx (P1 ux_friction, rage click on
/dashboard/vehicles/<id>/categories/<id>).
Before: each row had two competing click targets — a 20px chevron button and a
separate text button — both calling the same expand handler. Row height was
~28px (py-1.5), well below the 44px mobile guideline. No active/press feedback
made it hard to tell whether a tap registered, triggering rage clicks.
After:
- Whole row is a single button (non-leaf) or Link (leaf). One hit area, no
ambiguity about what gets the click.
- min-h-[44px], gap-3, px-3 py-2 — meets mobile guideline with breathing room.
- transition-colors + hover + active:bg-accent/80 give immediate tap feedback.
- focus-visible ring for keyboard nav, aria-expanded for screen readers.
- Chevron becomes a decorative span that rotates 90deg on expand instead of
swapping icons (no extra button), preserving the loading spinner in place.
Phase 2/8 of the UX audit follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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
PL24's bomOverviewlist widget (PR-code-filtered overview tables like
"Alternatöre genel bakış · Start-Stop modu bulunan araçlar için
[PR:7L6,7L8]") returns parts but no illustration — upstream genuinely
has no schema for these. Previously the 60% left panel rendered the
text "Sema goruntusu bulunamadi", which reads like a failure and
wastes most of the layout for what is actually a complete result.
When schemaPic is null, skip the schema panel entirely and let the
parts list take the full container width. The parts panel already
handles its own header/empty state, so no other adjustment needed.
useTranslation returns a fresh `t` each render, so including it in the
useEffect deps caused the effect to re-fire continuously, resetting the
drill-down stack immediately after handleSelect pushed a child level.
Result: clicking a parent category in grid view did nothing — no API
call, no UI change, no navigation. Reproduced via Playwright on
dev.sase.tr.
Drop `t` from the dep array (key is static; locale changes mid-session
are rare and acceptable to render stale until the next prop change).
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>
useEffect and useCallback were declared after three `return null` guards,
so when a user's subscription transitioned into the trial+≤7-days window
React saw more hooks than the previous render and crashed with #310.
Hoisted all hooks above the single guard return; render and dismiss
behavior unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>