Commit Graph

176 Commits

Author SHA1 Message Date
229666e333 i18n(web): full landing page TR↔EN — language switcher actually works now
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Root cause: landing was almost entirely hard-coded Turkish (5 t() calls in
1889 lines), and initLocale() — which reads the persisted locale from
localStorage on boot — was never called from main.tsx, so a reload always
reverted to the TR default.

This commit fixes both.

apps/web/src/main.tsx
- Call initLocale() before first render so the persisted choice survives reload.

apps/web/src/routes/__root.tsx
- Translate the skip-to-content link via a new `a11y.skipToContent` key.

apps/web/src/routes/index.tsx
- Internationalize ~145 user-visible strings across hero, brand strip, features,
  comparison table, bento grid, use cases, stats, dashboard showcase, ecommerce
  block, testimonials, pricing tiers, FAQ, final CTA, referral banner and ARIA
  labels. Module-scope data arrays moved inside the component so re-renders
  pick up the active locale.
- Wrap the three Remotion players (Schema / Dashboard / Ecommerce) so they read
  the current locale and pass translated labels into their inputProps.

Remotion demos (DashboardDemo, EcommerceDemo, SchemaDemo)
- Each now accepts a `labels` prop (DashboardDemoLabels / EcommerceDemoLabels /
  partLabel). All hard-coded TR sidebar items, placeholders, category names,
  product names, "Add to Cart" buttons, the cart-added toast, overlays and
  stat labels are now driven by labels with a TR fallback (keeps Remotion
  Studio preview working standalone).

messages/{tr,en}.json
- New blocks: `a11y`, `landing.*` (huge tree covering every section above),
  `landing.dashboardDemo`, `landing.ecommerceDemo`.

Intentionally preserved as data, not translated: brand names (Volkswagen,
Mitsubishi …), VIN literals, OEM codes (1J0 820 803F …), numeric stat values
(1.2sn, 27, 1M+, %99.9 …), prices (200/350/500/999), and the testimonial
person name "Özge D." (proper noun).

Verified: typecheck clean, 17/17 test files / 57/57 tests pass, biome clean,
Playwright TR↔EN round-trip succeeds, only the testimonial name remains as
Turkish on the EN page.
2026-05-31 23:50:52 +03:00
931c0cfd85 feat(web): pricing — by-the-numbers stat strip, verifiable facts only
Adds a four-stat band below the hero CTAs — 27 brand catalogs, 1M+ OEM/alt
parts, 30-day free trial, 7-day no-questions refund. Big tabular-nums numerals
with small labels; divides on sm+. Every number is product or policy fact we
can defend — no fabricated customer counts, no testimonial copy.

Side effect: FAQPage JSON-LD now injected into <head> via useEffect (was an
inline <script dangerouslySetInnerHTML>). Same SEO payload, less attack
surface, tagged with data-sase-faq so it's diff-friendly.
2026-05-31 23:18:14 +03:00
b8bbdbda67 polish(web): pricing zigzag timeline, FAQ JSON-LD, trust icon variety, feature matrix sticky thead
Pricing page
- "Nasıl çalışır" now renders as a vertical timeline with alternating sides on
  md+ and a connecting rail — no more 3-equal-cards AI tell
- Hero trust strip swaps four identical Check icons for distinct ones
  (CalendarClock / CreditCard / RotateCcw / Check) so each promise reads on
  its own
- FAQ section emits FAQPage JSON-LD for rich SERP results (free SEO upside;
  no UI cost)

Subscription page
- FeatureMatrix gets sticky thead + a right-fade gradient on narrow viewports
  so users can tell more columns lie offscreen
2026-05-31 23:12:21 +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
611b41c793 feat(web): subscription — radiogroup plan picker, trial stuck guard, stale-brand cleanup, scoped LS
- PlanGrid now wraps cards as a real radiogroup with role=radio + aria-checked +
  focus-visible ring (was aria-pressed on plain buttons)
- Adds ProvisioningStuckHint: at 30s in trial provisioning we surface a
  contact-support + retry escape so users aren't trapped on a spinner
- PaymentMethodSection strips stale brand IDs against the live /brands query
  before Stripe checkout (would have 4xx'd silently)
- trial-banner dismissal LS key now scoped to userId + endDate (was endDate
  only — two users on the same browser could leak state)
- Sticky CTA disabled state gains a hint title + aria-disabled + opacity
- Brand-selector Suspense gets an 8s slow-load fallback with reload affordance
- OnboardingCompletedView shows the actual subscription plan name (was hard-
  coded "Full Paket")
- Picks up sase-pending-period stashed by the public pricing toggle
2026-05-31 23:04:20 +03:00
eb8d8c2dee feat(web): pricing — hero CTA, monthly/yearly toggle, numeric savings, popular elevation
- Hero gets a primary "start trial" CTA + secondary "see plans" scroll-to anchor
- Adds monthly/yearly billing toggle that switches displayed amounts and surfaces
  effective per-month yearly cost
- Yearly savings shown as both percent and TL/year (no more vague "save")
- Full Paket card visually elevated (ring + tinted bg + slight vertical offset on lg)
- Plan CTA carries plan=key into /register; chosen period stashed in localStorage
- New PostHog events: pricing_hero_cta_clicked, pricing_period_toggled,
  pricing_plan_cta_clicked
2026-05-31 22:58:18 +03:00
209e98cb41 fix(web): stop leaking /vehicles/undefined and the raw uuid parse error
When the landing decodes a VIN for a logged-in visitor and the API
returns a multi-candidate response (no single id), the code was
navigating to /dashboard/vehicles/$id with id=undefined. The catalog
page then asked the API for /vehicles/undefined and the backend
"invalid input syntax for type uuid: 'undefined'" message landed in
front of the user.

Two-sided fix:
- On the landing, if the decode reply has no id, hand the VIN off to
  /dashboard/search?vin=… instead — that page already renders the
  candidate-picker and the unrecognised-VIN flow.
- On the vehicle detail page, gate both queries on a UUID check and
  render a friendly "Geçersiz araç bağlantısı / Aramaya git" panel for
  any malformed id. Also stop showing raw backend error messages in the
  generic error state — surface the localised hint instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 22:04:21 +03:00
a4597544b4 feat(web): vehicle detail polish — analytics, chatwoot cleanup, skeleton, heading
- Capture category_view_changed and catalog_search_opened so the funnel
  picks up category-view and cross-tree search activation (previously
  uninstrumented).
- Expose clearChatwootVehicle() and call it from the route's effect
  cleanup so a later support chat doesn't carry the previously-viewed
  car's VIN/brand.
- Match the loading skeleton shape to the active viewMode — render a
  card grid for grid mode (was always 8 row-bars).
- Switch the vehicle title to the display font (font-display, tracking-tight)
  for typography parity with the rest of the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:45:04 +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
73fe3e33c1 refactor(web): type the vehicle detail page — drop the any-soup
Replace `vehicle: any` in the route + every helper (getVehicleAttributes,
fromPcat, fromEmex, fromVinApi, getFallbackAttributes, getDisplayAttributes,
getEngineCode, getEquipment, VehicleSummary, VehicleAttributes,
VehicleEquipment) with the shared Vehicle type, and type the two queries
as Vehicle / CategoryNode[]. The opaque rawData blob now goes through a
small RawData interface that captures the four source-specific shapes
(pl24, parts-catalogs, emex, vin-api) instead of `any` index access, so
the per-source helpers stay readable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:35:37 +03:00
74f32b82e5 feat(web): vehicle detail — empty-categories state, equipment show-more, mobile header
- Render a composed "no categories found" panel when the category tree
  resolves to an empty array, so vehicles without a catalog stop showing
  a silent void under "Yedek Parça Kategorileri".
- Collapse the equipment list to the first 8 codes by default with a
  "Tümünü göster (N)" toggle instead of a sub-scrollarea — friendlier
  on mobile, no card-in-card scroll.
- Loosen the header flex layout: the title block gets `min-w-0 flex-1`
  with break-words and a truncated VIN line so long brand+model strings
  no longer crush the layout on narrow screens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 21:29:49 +03:00
a6ce215c47 i18n(web): translate the search page
Move every hard-coded Turkish string on /dashboard/search into a
search.* message block (tr + en) and resolve them via t() — title,
subtitle, input label/placeholder, counter, example button, submit, the
error banner with the subscription nudge, the report-VIN flow + toasts,
preview loading + identified badge + hint, recent-searches header /
see-all / open hint / row aria-label, validation/generic/toast errors,
auto-correction toast, decoding skeleton sr-only text, and the candidate
selection failure toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 17:28:45 +03:00
29178c0eaa feat(web): post-signup goes to the catalog only for typed VINs
Until now any VIN carried from the landing — including the example VIN
filled by "Örnek aramayı deneyin" — auto-decoded after signup and jumped
straight to the vehicle catalog, skipping the search page entirely. That
is the right behaviour for a visitor who typed their own VIN, but the
example button is meant for exploration; users who clicked it should
land on the search page so they can look around.

Track an `isExampleVin` flag on the landing (set by fillExampleVin,
cleared on any keystroke), carry it through to /register and onward to
/dashboard/search as `example=1`, and skip the auto-decode there when
the flag is present (the VIN is still prefilled). User-typed VINs keep
the existing land-on-catalog behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 17:22:00 +03:00
81d1086fef a11y(web): label the search VIN input and announce history rows
Add a visually hidden Label for the VIN input (the previous version
relied on placeholder text only) and an aria-label on each history row
so screen readers announce the vehicle and VIN before activation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 17:19:30 +03:00
1fd21bd04b feat(web): search history opens the vehicle in one click
Clicking a recent search now navigates straight to the vehicle's catalog
page instead of just refilling the VIN input — the row already carries
the vehicle id, so the re-decode round trip is wasted. Hint copy updated
to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 17:18:46 +03:00
6cff3dea24 refactor(web): type the search history list, fetch only what we render
Replace the `any[]` history query with a small VehicleHistoryItem
interface covering the fields the row renders, and lower the API limit
from 20 to 6 since the UI slices to 6 anyway (saved bandwidth + correct
types on .map).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 17:18:15 +03:00
1787607be5 feat: embed Chatwoot live-chat widget (destek.sase.tr)
Site-wide live-chat widget served from the self-hosted Chatwoot at
destek.sase.tr, with verified user identity and vehicle context.

- apps/web: lib/chatwoot.ts loads the SDK lazily (mirrors the PostHog
  init pattern), init in main.tsx, identify logged-in users in __root
  via a server-computed HMAC, and attach the viewed vehicle (VIN/brand/
  model) as contact custom attributes on the vehicle detail page.
- apps/api: GET /api/chatwoot/identity (AuthGuard-protected) returns
  HMAC-SHA256(user.id) so the widget can use verified identity.
- env: VITE_CHATWOOT_BASE_URL + VITE_CHATWOOT_WEBSITE_TOKEN (build-time,
  wired through docker-compose.coolify.yml build args + Dockerfile ARG)
  and CHATWOOT_HMAC_TOKEN (api runtime). All optional — widget and
  endpoint no-op when unset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:28:50 +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
9753d00015 fix(web): link "Bize Ulaşın" custom-needs button to the contact page
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The custom-integration / bulk-query CTA was a dead button. Wrap it in a
Link to /contact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:49:16 +03:00
efd42ade4a fix(web): align remaining "50+ marka" claims to the real count (27)
Follow-up to the landing fix: the credibility strip (subscription/checkout),
meta description, auth-layout badge and blog copy still claimed "50+
marka" while the catalog supports 27. Align all to 27 for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:59:46 +03:00
c76ab7a280 feat(web): plan preselection from landing pricing (CRO #7)
Carry the plan a visitor picks on the landing pricing through to checkout:
- Pricing CTAs now pass ?plan=<key>; logged-in users go straight to
  /dashboard/subscription, others to /register.
- Subscription page accepts ?plan= (and a localStorage carrier that
  survives the value-first register→search onboarding detour) and
  preselects the plan — advancing to brand selection (or payment for
  Full) — unless the sub is active/pending or onboarding is in progress.
- Register validates ?plan and persists it on signup so the choice isn't
  lost through the trial onboarding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 03:01:26 +03:00
0dc298631d fix(web): landing — align pricing tier cards for easy comparison
Drop the staggered sm:translate-y offsets on the three brand tiers so
their titles, prices and feature lists share a baseline, making the
plans scannable side by side instead of cascading down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:53:33 +03:00
e21683bce6 feat(web): landing — add FAQ section and trust signals near final CTA
Add an objection-handling FAQ (brands, KVKK/security, no card, cancel
anytime, integration, no-VIN path) before the closing CTA, and a
KVKK/SSL trust line under the final CTA button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:52:51 +03:00
076c61d599 feat(web): landing — stronger final CTA, move referral after the close
The closing CTA was an outline (ghost) button — the weakest style at the
most important conversion point. Make it a filled brand button, size lg,
with clearer copy ("30 Gün Ücretsiz Başla — Kart Gerekmez"). Move the
referral banner (a retention loop) to after the final CTA so it no longer
sits between the pricing and the primary close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:51:11 +03:00
eaf6d47f05 fix(web): landing — consistent brand count (27) and wrong-part cost figure
The page claimed "50+ marka" in the hero, carousel, plan and meta while
the stats block and the actual brand carousel show 27 — a credibility
contradiction. Align every brand-count mention to 27. Also align the
wrong-part cost: hero said "450 TL", final CTA "200-500 TL" — use the
200-500 TL range in both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:50:12 +03:00
18576a2818 chore(web): contact — E-posta kartını kaldır, form placeholder + meta güncelle
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
- info@sase.tr E-posta bilgi kartı kaldırıldı (Destek + Adres kaldı)
- mesaj placeholder "Bize..." → "Size nasıl yardımcı olabiliriz?"
- meta description info@sase.tr referansından arındırıldı

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:37:21 +03:00
7eb2e85bbc feat(auth): "beni 30 gün hatırla" + login Turnstile koruması
- session.expiresIn = 30 gün; login'de rememberMe checkbox signIn.email'e bağlandı
  (işaretli=30 gün kalıcı çerez, değilse oturum çerezi)
- better-auth captcha plugin'i (cloudflare-turnstile, TURNSTILE_SECRET_KEY varsa)
  sign-in/sign-up'ı korur; login formuna Turnstile widget'ı + x-captcha-response

Not: auth.ts ve login.tsx her iki özelliği birlikte içerdiğinden tek commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:14:18 +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
cf1fee68a9 fix(web): responsive billing table + filters on mobile
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The payments table forced min-w-[640px] inside overflow-x-auto, so mobile
got an awkward horizontal scroll, and the 5-option status filter could
overflow the viewport. Make the table a responsive layout: on mobile each
payment renders as a labelled stacked card (column label via ::before
from data-label, header row hidden), and on sm+ it stays a normal table.
Let the segmented filters wrap instead of overflowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:36:03 +03:00
c633e846e2 fix(payments): curate /payments/me response — stop leaking internal columns, join plan name
getMyPayments returned the raw payment row, exposing internal fields
(adminNote, iyzicoPaymentId, bankAccountId, session/intent ids) to the
end user. Replace with an explicit projection that returns only what the
billing UI needs, joins planName from the subscription's plan (was always
"-"), and surfaces Stripe receipt availability as a hasStripeReceipt
boolean instead of the raw payment intent id. Frontend reads the boolean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:31:33 +03:00
d77face831 fix(web): billing — correct cancelled status label and gate receipt button
- Subscription status is "cancelled" (and "pending") in the API; the i18n
  keys/variant map used "canceled", so the badge showed the raw
  billing.subStatus.cancelled key. Fix the spelling and add pending.
- Only show "View receipt" for Stripe payments that actually have a
  stripePaymentIntentId. Seeded/legacy completed payments have none, so
  the button used to appear and then toast "no receipt available".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:28:39 +03:00
7a8fe5e98e feat(payments): Stripe receipt link on billing rows
Add GET /payments/:id/receipt — resolves the Stripe-hosted receipt URL
from the payment intent's latest charge (ownership-scoped; returns EFT
receipt directly when present, null otherwise). Wire a "View receipt"
action on completed Stripe rows that fetches the URL on demand and opens
it, with a toast when none is available.

Closes the last billing-audit item (#8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:14:28 +03:00
0e61a2fcc7 feat(web): billing loading skeleton shape + show-more pagination
Make the payments loading state mirror the table card (header + rows)
instead of generic bars, and paginate the list client-side (10 at a time
with a "Show more" button) so long payment histories don't render all at
once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:07:50 +03:00
94d04d5339 refactor(web): semantic billing table + segmented filters
Replace the div-grid "table" with a real <table> (thead/th scope=col/
tbody/td) inside an overflow-x-auto wrapper, so screen readers announce
each cell under its column and every amount/status is labelled (drops the
hidden-header mobile hack). Convert the filter button rows into compact
segmented controls (fieldset/legend) and only show filters once payments
exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:06:36 +03:00
36bd4b500b feat(web): add subscription summary card to billing page
Billing was payment history only. Add a summary card at the top showing
the current plan, status badge, and next renewal / trial-end / end date
(from /subscriptions/me), with a "Manage subscription" link — or a
"Choose a plan" CTA when there's no active subscription.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:04:58 +03:00
acada22e71 fix(web): billing — distinguish empty vs filtered, localize date, tabular nums
Show "no payments match this filter" with a clear-filters button when
filters hide everything (was the misleading "no payments yet"). Follow
the active locale for the payment date instead of hard-coding tr-TR, and
use tabular figures for date and amount columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:03:26 +03:00
0749a55b41 fix(web): repair EFT receipt download and add EFT payment filter
The receipt link read payment.receiptUrl, but the API returns the column
as eftReceiptUrl — so the download button never rendered (dead feature).
Use eftReceiptUrl. Also add the missing "EFT" option to the method
filter; EFT is a live method with rows and receipts but couldn't be
filtered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:02:23 +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
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
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
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
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
fa4e08a0a7 feat(web): add "upgrade account" CTA above sidebar profile for free users
Shows a brand-tinted "Hesabını Yükselt" badge above the username in the
sidebar footer, linking to the subscription page. Hidden for users with
an active paid plan. Renders in desktop (expanded + collapsed icon
variant), and the mobile drawer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:09:12 +03:00
e1d929d3eb feat(web): replace register subtext with terms-acceptance notice
Swap the "start searching / no spam" line under the register button for a
terms-of-use acceptance notice linking to /terms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:54:37 +03:00
71242a72be feat(web): dashboard sidebar UX/UI + a11y overhaul
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Replace the click-to-logout profile trap with a proper DropdownMenu
(settings / subscription / logout), swap the hand-rolled mobile overlay
for a Radix-Dialog Sheet (focus-trap, Esc, scroll-lock), and fix
accessibility gaps: aria-labels on icon buttons, aria-current + labelled
nav landmarks, and effect-based auth redirect instead of navigating
during render.

Also: reorder nav (Main → Account → Support → Admin), full i18n of
labels/sections, conditional "upgrade plan" button, header page title,
collapsed-mode active indicator + tooltips, contrast/touch-target
tweaks, and a typed NavItem to drop the `as any` exact cast.

Adds DropdownMenu, Tooltip and Sheet primitives to @sase/ui.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:54:53 +03:00
57b3cb7ebe feat: contact sayfasına iletişim formu + POST /contact endpoint'i
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
- Yeni contact modülü: @Public POST /contact, zod validation, @Throttle 5/10dk
  spam koruması; EmailService ile admin@sase.tr'ye mail (reply-to = gönderen),
  kullanıcı girdileri HTML-escape
- EmailService: replyTo desteği eklendi
- contact.tsx: useState + zod + @sase/ui ile iletişim formu (mevcut form
  pattern'iyle tutarlı; yeni form kütüphanesi yok), toast + alan validasyonu

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:27:56 +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