From d6c88ede73f5d0da6fbc839f97849f7d6bf685ff Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 19:07:49 +0300 Subject: [PATCH 01/35] feat(observability): session-replay + in-app feedback on catalog UX failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two things only the browser can add on top of the backend catalog-degradation reporting: - Replay-on-failure: when the catalog UI renders empty-tree (decoded vehicle, no categories) or a drill loadError, capture a browser Sentry warning and flush the Session Replay → you can WATCH the user hit the dead-end (serkan's session, reproducible). Per-session deduped (one replay/session covers the whole journey). - In-app feedback: a "Çalışmadı mı? Bildir" button on the empty-parts, empty-tree and loadError states opens the Sentry feedback dialog pre-tagged with the vehicle/category (+ session replay) — turns a parts shop's complaint into a structured, triageable report instead of an email. Browser events are fingerprinted source="browser" so they form their own "what users actually saw" issues (carrying replays) next to the server-side detections. tsc + biome + web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../catalog/report-catalog-issue.tsx | 63 +++++++++++++ .../web/src/components/schema/parts-panel.tsx | 42 +++++---- apps/web/src/lib/sentry.ts | 93 +++++++++++++++++++ apps/web/src/messages/en.json | 1 + apps/web/src/messages/tr.json | 1 + .../vehicles_/$id/categories_/$categoryId.tsx | 51 +++++++--- .../routes/dashboard/vehicles_/$id/index.tsx | 24 +++++ 7 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 apps/web/src/components/catalog/report-catalog-issue.tsx diff --git a/apps/web/src/components/catalog/report-catalog-issue.tsx b/apps/web/src/components/catalog/report-catalog-issue.tsx new file mode 100644 index 0000000..56bee8d --- /dev/null +++ b/apps/web/src/components/catalog/report-catalog-issue.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from "@/lib/i18n"; +import { + type CatalogIssueContext, + type CatalogIssueKind, + openCatalogFeedback, + reportCatalogDegradation, +} from "@/lib/sentry"; +import { Button } from "@sase/ui"; +import { Flag } from "lucide-react"; +import { useEffect } from "react"; + +/** + * Fire-and-forget: when `active` becomes true, report the catalog degradation the + * user is looking at to Sentry (with a flushed session replay so it's watchable). + * Deduped per session in the lib, so re-renders are safe. + */ +export function useReportCatalogDegradation( + kind: CatalogIssueKind, + active: boolean, + ctx: CatalogIssueContext, +): void { + const { vehicleId, categoryId, vehicleLabel, categoryName, source, vin } = ctx; + useEffect(() => { + if (!active) return; + void reportCatalogDegradation(kind, { + vehicleId, + categoryId, + vehicleLabel, + categoryName, + source, + vin, + }); + }, [active, kind, vehicleId, categoryId, vehicleLabel, categoryName, source, vin]); +} + +/** + * "Çalışmadı mı? Bildir" — opens the Sentry feedback dialog pre-tagged with this + * vehicle/category. Shown on empty/error catalog states so a parts shop can + * report a missing/wrong catalog in one click (with the session replay attached) + * instead of emailing. + */ +export function ReportCatalogIssueButton({ + ctx, + className, +}: { + ctx: CatalogIssueContext; + className?: string; +}) { + const { t } = useTranslation(); + return ( + + ); +} diff --git a/apps/web/src/components/schema/parts-panel.tsx b/apps/web/src/components/schema/parts-panel.tsx index 8ec2b8c..368a65b 100644 --- a/apps/web/src/components/schema/parts-panel.tsx +++ b/apps/web/src/components/schema/parts-panel.tsx @@ -1,3 +1,4 @@ +import { ReportCatalogIssueButton } from "@/components/catalog/report-catalog-issue"; import type { Part } from "@/hooks/use-parts"; import { api } from "@/lib/api-client"; import { capture } from "@/lib/posthog"; @@ -204,23 +205,30 @@ export function PartsPanel({ {parts.length === 0 ? (

Bu kategori için parça bulunamadı.

- +
+ + {/* User-driven report: a parts shop knows if this category SHOULD + have parts. One click → structured Sentry feedback + replay. */} + +
) : ( diff --git a/apps/web/src/lib/sentry.ts b/apps/web/src/lib/sentry.ts index 3bb2968..52e5a2a 100644 --- a/apps/web/src/lib/sentry.ts +++ b/apps/web/src/lib/sentry.ts @@ -10,6 +10,9 @@ import type { init as SentryInit } from "@sentry/react"; let initialized = false; +// Set once init succeeds so the catalog-degradation helpers below can use the +// already-loaded SDK without re-awaiting the dynamic import on every call. +let sentryApi: typeof import("@sentry/react") | null = null; export async function initSentry() { if (initialized) return; @@ -46,6 +49,9 @@ export async function initSentry() { integrations: [ Sentry.browserTracingIntegration(), Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }), + // Contextual in-app feedback (no global floating button — opened from the + // empty/error catalog states via openCatalogFeedback). + Sentry.feedbackIntegration({ autoInject: false, showBranding: false }), ], // Performance: 10% trace sample rate (low volume site, can raise later). tracesSampleRate: 0.1, @@ -75,8 +81,95 @@ export async function initSentry() { denyUrls: [/iabjs:\/\//], }); initialized = true; + sentryApi = Sentry; console.log("[sentry] browser SDK initialized"); } catch (err) { console.warn("[sentry] init failed:", (err as Error).message); } } + +// ─── Catalog UX-degradation reporting (browser) ─── +// Two things only the browser can add on top of the backend reporter +// (apps/api/src/common/catalog-degradation.ts): a Session Replay of the user +// hitting the dead-end, and a one-click structured feedback path. Browser events +// are fingerprinted with source "browser" so they form their own "what users +// actually saw" issues (carrying replays), distinct from the server-side +// detections — same kinds, complementary views. + +export type CatalogIssueKind = "empty-tree" | "empty-parts" | "drill-load-error"; + +export interface CatalogIssueContext { + vehicleId?: string; + vehicleLabel?: string; + categoryId?: string; + categoryName?: string; + source?: string; + vin?: string; +} + +// Per-session dedup: re-renders or browsing many empty categories shouldn't spam +// the issue stream, and one replay upload already covers the whole journey. +const reportedThisSession = new Set(); +let replayFlushedThisSession = false; + +const brandOf = (label?: string) => (label?.trim().split(/\s+/)[0] || "unknown").toLowerCase(); + +/** + * Report the catalog failure the user is currently looking at and flush the + * Session Replay so it's watchable in Sentry. Best-effort; never throws. + */ +export async function reportCatalogDegradation( + kind: CatalogIssueKind, + ctx: CatalogIssueContext, +): Promise { + try { + const Sentry = sentryApi; + if (!Sentry) return; + const dedupKey = `${kind}:${ctx.categoryId ?? ctx.vehicleId ?? ""}`; + if (reportedThisSession.has(dedupKey)) return; + reportedThisSession.add(dedupKey); + + const brand = brandOf(ctx.vehicleLabel); + Sentry.captureMessage(`catalog degraded: ${kind} (browser/${brand})`, { + level: "warning", + tags: { catalog_degradation: kind, catalog_source: ctx.source ?? "unknown" }, + fingerprint: ["catalog-degradation", "browser", kind, brand], + extra: { ...ctx }, + }); + + if (!replayFlushedThisSession) { + replayFlushedThisSession = true; + await Sentry.getReplay()?.flush(); + } + } catch { + // telemetry must never break the UI + } +} + +/** + * Open the Sentry feedback dialog pre-tagged with the vehicle/category the user + * is reporting from, so "bu araç çalışmadı" becomes a structured report with the + * session replay attached — instead of an email. + */ +export async function openCatalogFeedback(ctx: CatalogIssueContext): Promise { + try { + const Sentry = sentryApi; + if (!Sentry) return; + Sentry.setTag("catalog_source", ctx.source ?? "unknown"); + Sentry.setContext("catalog_issue", { ...ctx }); + const feedback = Sentry.getFeedback(); + if (!feedback) return; + const form = await feedback.createForm({ + formTitle: "Bu araç / parça çalışmadı mı?", + messagePlaceholder: + "Hangi araç ve parça eksik veya yanlış? (şase ve kategori bilgisi otomatik eklenir)", + submitButtonLabel: "Gönder", + cancelButtonLabel: "Vazgeç", + // The session replay + the catalog_issue context above ride along with this. + }); + form.appendToDom(); + form.open(); + } catch { + // feedback is best-effort + } +} diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index ceaf602..f4b2995 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -738,6 +738,7 @@ "noCategoriesTitle": "No categories for this vehicle", "noCategoriesHint": "The vehicle was decoded but the catalog may not be ready yet. Reach out to support to expedite it.", "drillHint": "Parts live inside the subcategories — open a category to drill down to its part lists.", + "reportIssue": "Not working? Report it", "noAttrs": "No detailed info available for this vehicle.", "labelModel": "Model", "labelYear": "Model year", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index 5ceb583..44fb52c 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -738,6 +738,7 @@ "noCategoriesTitle": "Bu araç için kategori bulunamadı", "noCategoriesHint": "Araç decode edildi ama parça kataloğu henüz hazırlanmamış olabilir. Destek ekibimize bildirirseniz katalog hızlandırılır.", "drillHint": "Parçalar alt kategorilerin içinde yer alır. Bir kategoriye girerek ilerleyin — her grup, içindeki parça listesine kadar açılır.", + "reportIssue": "Çalışmadı mı? Bildir", "noAttrs": "Bu araç için ayrıntı bilgisi bulunamadı.", "labelModel": "Model", "labelYear": "Model yılı", diff --git a/apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx b/apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx index a4b2eb7..4c019a8 100644 --- a/apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx +++ b/apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx @@ -1,3 +1,7 @@ +import { + ReportCatalogIssueButton, + useReportCatalogDegradation, +} from "@/components/catalog/report-catalog-issue"; import { CategoryBreadcrumb } from "@/components/categories/category-breadcrumb"; import { CategoryColumns } from "@/components/categories/category-columns"; import { CategoryGrid } from "@/components/categories/category-grid"; @@ -61,7 +65,9 @@ function VehicleCategoryPage() { const { data: vehicle } = useQuery({ queryKey: ["vehicle", id], queryFn: () => - api.get<{ brandName?: string; model?: string; year?: number }>(`/vehicles/${id}`), + api.get<{ brandName?: string; model?: string; year?: number; source?: string; vin?: string }>( + `/vehicles/${id}`, + ), enabled: !!id, }); @@ -97,6 +103,17 @@ function VehicleCategoryPage() { ? `${vehicle.brandName}${cleanModel ? ` ${cleanModel}` : ""}` : t("vehicle.title"); + // A loadError = the drill/parts fetch failed and the user sees a "couldn't load" + // panel instead of parts — report it (with a session replay) so it surfaces. + useReportCatalogDegradation("drill-load-error", !!data?.loadError, { + vehicleId: id, + categoryId, + categoryName: data?.name, + vehicleLabel, + source: vehicle?.source, + vin: vehicle?.vin, + }); + return (
{t("vehicle.catalogUnavailableTitle")}

{t("vehicle.catalogUnavailableHint")}

- +
+ + +
) : ( }> diff --git a/apps/web/src/routes/dashboard/vehicles_/$id/index.tsx b/apps/web/src/routes/dashboard/vehicles_/$id/index.tsx index ededccc..9ec4162 100644 --- a/apps/web/src/routes/dashboard/vehicles_/$id/index.tsx +++ b/apps/web/src/routes/dashboard/vehicles_/$id/index.tsx @@ -1,3 +1,7 @@ +import { + ReportCatalogIssueButton, + useReportCatalogDegradation, +} from "@/components/catalog/report-catalog-issue"; import { CatalogSearch } from "@/components/categories/catalog-search"; import { CategoryBreadcrumb } from "@/components/categories/category-breadcrumb"; import { CategoryColumns } from "@/components/categories/category-columns"; @@ -105,6 +109,16 @@ function VehicleDetailPage() { ? `${vehicle.brandName}${cleanModelName(vehicle.model) ? ` ${cleanModelName(vehicle.model)}` : ""}` : t("vehicle.title"); + // An empty category tree on a decoded vehicle is a silent failure ("model var, + // kategori yok") — report it to Sentry with a session replay so it surfaces. + const treeEmpty = !categoriesLoading && Array.isArray(categoryTree) && categoryTree.length === 0; + useReportCatalogDegradation("empty-tree", treeEmpty, { + vehicleId: id, + vehicleLabel, + source: vehicle?.source, + vin: vehicle?.vin, + }); + // Surface the viewed vehicle to the support chat widget (VIN/brand/model) // so agents have the car context for part-compatibility questions. useEffect(() => { @@ -344,6 +358,16 @@ function VehicleDetailPage() {

{t("vehicle.noCategoriesHint")}

+
+ +
) : viewMode === "grid" ? ( From 11975d8b5a145e40177f0233568b03969c0dfb13 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 19:17:10 +0300 Subject: [PATCH 02/35] feat(payments): remove EFT/havale, Stripe-only checkout + Turkish locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EFT/Havale was retired; Stripe is now the sole payment method. Remove the EFT code surface (shared PaymentMethod "eft" + EftPaymentInput + eftReceiptUrl, EFT_RECEIPT_REQUIRED error code, billing UI receipt/filter/label paths, payments.service eft read paths). DB columns (eft_receipt_url, bank_account_id, bank_accounts) are kept and marked @deprecated to preserve historical records and avoid a destructive migration — same pattern as the retired iyzico column. Faz 3 conversion lever: set locale "tr" on the Stripe Checkout session. The audience is Turkish B2B and ~60% of sessions reached the foreign-language hosted page but never started a payment intent (pure abandonment, not decline). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/database/schema/core.ts | 6 ++++- apps/api/src/payments/payments.service.ts | 18 +++++-------- .../api/src/payments/stripe/stripe.service.ts | 4 +++ apps/web/src/messages/en.json | 3 +-- apps/web/src/messages/tr.json | 3 +-- apps/web/src/routes/dashboard/billing.tsx | 27 +++++++------------ packages/shared/src/constants/error-codes.ts | 1 - packages/shared/src/index.spec.ts | 1 - packages/shared/src/index.ts | 1 - packages/shared/src/types/payment.ts | 9 +++---- 10 files changed, 29 insertions(+), 44 deletions(-) diff --git a/apps/api/src/database/schema/core.ts b/apps/api/src/database/schema/core.ts index a0c3717..3b63ec9 100644 --- a/apps/api/src/database/schema/core.ts +++ b/apps/api/src/database/schema/core.ts @@ -184,7 +184,9 @@ export const userBrands = pgTable( ], ); -// ─── Bank Accounts (EFT/Havale destination accounts, one active at a time) ──── +// ─── Bank Accounts ─────────────────────────────────── +// @deprecated EFT/Havale was retired (Stripe is the sole payment method). Table +// kept only to preserve historical FK integrity; no rows are created anymore. export const bankAccounts = pgTable( "bank_accounts", { @@ -225,7 +227,9 @@ export const payments = pgTable( iyzicoPaymentId: text("iyzico_payment_id"), stripeSessionId: text("stripe_session_id"), stripePaymentIntentId: text("stripe_payment_intent_id"), + /** @deprecated EFT/Havale retired (Stripe-only). Kept for historical data. */ bankAccountId: uuid("bank_account_id").references(() => bankAccounts.id), + /** @deprecated EFT/Havale retired (Stripe-only). Kept for historical data. */ eftReceiptUrl: text("eft_receipt_url"), adminNote: text("admin_note"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), diff --git a/apps/api/src/payments/payments.service.ts b/apps/api/src/payments/payments.service.ts index 629e65d..678f827 100644 --- a/apps/api/src/payments/payments.service.ts +++ b/apps/api/src/payments/payments.service.ts @@ -16,9 +16,9 @@ export class PaymentsService { /** * Payment history for the billing page. Returns a curated projection — never * the raw row — so internal columns (adminNote, iyzicoPaymentId, - * bankAccountId, session/intent ids) are not leaked to the client. planName - * is joined from the subscription's plan; Stripe receipt availability is - * surfaced as a boolean rather than exposing the payment intent id. + * session/intent ids) are not leaked to the client. planName is joined from + * the subscription's plan; Stripe receipt availability is surfaced as a + * boolean rather than exposing the payment intent id. */ async getMyPayments(userId: string) { const rows = await this.db @@ -29,7 +29,6 @@ export class PaymentsService { status: payments.status, createdAt: payments.createdAt, planName: plans.name, - eftReceiptUrl: payments.eftReceiptUrl, stripePaymentIntentId: payments.stripePaymentIntentId, }) .from(payments) @@ -45,16 +44,15 @@ export class PaymentsService { status: row.status, createdAt: row.createdAt, planName: row.planName ?? null, - eftReceiptUrl: row.eftReceiptUrl ?? null, hasStripeReceipt: row.method === "stripe" && row.status === "completed" && !!row.stripePaymentIntentId, })); } /** - * Resolve a receipt URL for one of the current user's payments. - * EFT receipts are stored locally; Stripe receipts are fetched live from the - * payment intent's latest charge. Returns { url: null } when none exists. + * Resolve a receipt URL for one of the current user's payments. Stripe is the + * sole payment method; receipts are fetched live from the payment intent's + * latest charge. Returns { url: null } when none exists. */ async getReceiptUrl(userId: string, paymentId: string): Promise<{ url: string | null }> { const [payment] = await this.db @@ -67,10 +65,6 @@ export class PaymentsService { throw new NotFoundException("Payment not found"); } - if (payment.eftReceiptUrl) { - return { url: payment.eftReceiptUrl }; - } - if (payment.method === "stripe" && payment.stripePaymentIntentId) { return { url: await this.stripeService.getReceiptUrl(payment.stripePaymentIntentId) }; } diff --git a/apps/api/src/payments/stripe/stripe.service.ts b/apps/api/src/payments/stripe/stripe.service.ts index 0519e56..74c32fc 100644 --- a/apps/api/src/payments/stripe/stripe.service.ts +++ b/apps/api/src/payments/stripe/stripe.service.ts @@ -143,6 +143,10 @@ export class StripeService { const session = await this.stripe.checkout.sessions.create({ mode: "payment", + // Render Stripe's hosted page in Turkish. The audience is Turkish B2B; a + // foreign-language checkout is a known abandonment driver (~60% of sessions + // reached the page but never started a payment intent). + locale: "tr", payment_method_types: ["card"], customer_email: userEmail, line_items: [ diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index f4b2995..1b3259b 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -489,8 +489,7 @@ "refunded": "Refunded" }, "methodLabels": { - "stripe": "Credit Card", - "eft": "EFT/Wire" + "stripe": "Credit Card" }, "summary": { "title": "Current Plan", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index 44fb52c..a9a0b57 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -489,8 +489,7 @@ "refunded": "İade" }, "methodLabels": { - "stripe": "Kredi Kartı", - "eft": "EFT/Havale" + "stripe": "Kredi Kartı" }, "summary": { "title": "Mevcut Plan", diff --git a/apps/web/src/routes/dashboard/billing.tsx b/apps/web/src/routes/dashboard/billing.tsx index 01d1f16..70e6767 100644 --- a/apps/web/src/routes/dashboard/billing.tsx +++ b/apps/web/src/routes/dashboard/billing.tsx @@ -8,7 +8,7 @@ import { Button } from "@sase/ui"; import { Skeleton } from "@sase/ui"; import { useQuery } from "@tanstack/react-query"; import { Link, createFileRoute } from "@tanstack/react-router"; -import { ArrowRight, Download, Filter, HelpCircle, Receipt, ReceiptText } from "lucide-react"; +import { ArrowRight, Filter, HelpCircle, Receipt, ReceiptText } from "lucide-react"; import { useState } from "react"; export const Route = createFileRoute("/dashboard/billing")({ @@ -26,10 +26,11 @@ interface Subscription { interface Payment { id: string; amount: number; - method: "stripe" | "eft"; + // Stripe is the sole method; kept as string so retired historical values + // (iyzico/eft) still render in the history table. + method: string; status: "completed" | "pending" | "failed" | "refunded"; planName?: string | null; - eftReceiptUrl?: string | null; hasStripeReceipt?: boolean; createdAt: string; } @@ -213,7 +214,7 @@ function BillingPage() { onChange={setMethodFilter} options={[ { value: "all", label: t("common.all") }, - ...["stripe", "eft"].map((m) => ({ + ...["stripe"].map((m) => ({ value: m, label: t(`billing.methodLabels.${m}`), })), @@ -309,7 +310,9 @@ function BillingPage() {
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index f60aea4..b13daae 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -8,558 +8,570 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from "./routes/__root" -import { Route as TermsRouteImport } from "./routes/terms" -import { Route as PrivacyRouteImport } from "./routes/privacy" -import { Route as PricingRouteImport } from "./routes/pricing" -import { Route as KvkkRouteImport } from "./routes/kvkk" -import { Route as DemoRouteImport } from "./routes/demo" -import { Route as DashboardRouteImport } from "./routes/dashboard" -import { Route as ContactRouteImport } from "./routes/contact" -import { Route as BlogRouteImport } from "./routes/blog" -import { Route as AboutRouteImport } from "./routes/about" -import { Route as AuthRouteImport } from "./routes/_auth" -import { Route as IndexRouteImport } from "./routes/index" -import { Route as DashboardIndexRouteImport } from "./routes/dashboard/index" -import { Route as DashboardSettingsRouteImport } from "./routes/dashboard/settings" -import { Route as DashboardServiceTestRouteImport } from "./routes/dashboard/service-test" -import { Route as DashboardSearchRouteImport } from "./routes/dashboard/search" -import { Route as DashboardHistoryRouteImport } from "./routes/dashboard/history" -import { Route as DashboardChangelogRouteImport } from "./routes/dashboard/changelog" -import { Route as DashboardBillingRouteImport } from "./routes/dashboard/billing" -import { Route as BlogSlugRouteImport } from "./routes/blog_/$slug" -import { Route as AuthResetPasswordRouteImport } from "./routes/_auth/reset-password" -import { Route as AuthRegisterRouteImport } from "./routes/_auth/register" -import { Route as AuthLoginRouteImport } from "./routes/_auth/login" -import { Route as AuthForgotPasswordRouteImport } from "./routes/_auth/forgot-password" -import { Route as AuthEmailVerifiedRouteImport } from "./routes/_auth/email-verified" -import { Route as DashboardSubscriptionIndexRouteImport } from "./routes/dashboard/subscription/index" -import { Route as DashboardCatalogIndexRouteImport } from "./routes/dashboard/catalog/index" -import { Route as DashboardAdminIndexRouteImport } from "./routes/dashboard/admin/index" -import { Route as DemoCategoriesCategoryIdRouteImport } from "./routes/demo_/categories_/$categoryId" -import { Route as DashboardAdminUsersRouteImport } from "./routes/dashboard/admin/users" -import { Route as DashboardAdminReferralsRouteImport } from "./routes/dashboard/admin/referrals" -import { Route as DashboardAdminCopyLogsRouteImport } from "./routes/dashboard/admin/copy-logs" -import { Route as DashboardAdminAnalyticsRouteImport } from "./routes/dashboard/admin/analytics" -import { Route as DashboardVehiclesIdIndexRouteImport } from "./routes/dashboard/vehicles_/$id/index" -import { Route as DashboardCatalogBrandNameIndexRouteImport } from "./routes/dashboard/catalog_/$brandName/index" -import { Route as DashboardCatalogPcatCatalogIdIndexRouteImport } from "./routes/dashboard/catalog_/pcat/$catalogId/index" -import { Route as DashboardCatalogEmexCatalogCodeIndexRouteImport } from "./routes/dashboard/catalog_/emex/$catalogCode/index" -import { Route as DashboardCatalogBrandNameModelIdIndexRouteImport } from "./routes/dashboard/catalog_/$brandName_/$modelId/index" -import { Route as DashboardVehiclesIdCategoriesCategoryIdRouteImport } from "./routes/dashboard/vehicles_/$id/categories_/$categoryId" -import { Route as DashboardCatalogPcatCatalogIdModelIdIndexRouteImport } from "./routes/dashboard/catalog_/pcat/$catalogId_/$modelId/index" -import { Route as DashboardCatalogEmexCatalogCodeVehicleIdIndexRouteImport } from "./routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId/index" -import { Route as DashboardCatalogBrandNameModelIdCategoriesCategoryIdRouteImport } from "./routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId" -import { Route as DashboardCatalogPcatCatalogIdModelIdCarIdIndexRouteImport } from "./routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/index" -import { Route as DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRouteImport } from "./routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId" -import { Route as DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRouteImport } from "./routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId" +import { Route as rootRouteImport } from './routes/__root' +import { Route as TermsRouteImport } from './routes/terms' +import { Route as PrivacyRouteImport } from './routes/privacy' +import { Route as PricingRouteImport } from './routes/pricing' +import { Route as KvkkRouteImport } from './routes/kvkk' +import { Route as DemoRouteImport } from './routes/demo' +import { Route as DashboardRouteImport } from './routes/dashboard' +import { Route as ContactRouteImport } from './routes/contact' +import { Route as BlogRouteImport } from './routes/blog' +import { Route as AboutRouteImport } from './routes/about' +import { Route as AuthRouteImport } from './routes/_auth' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DashboardIndexRouteImport } from './routes/dashboard/index' +import { Route as DashboardSettingsRouteImport } from './routes/dashboard/settings' +import { Route as DashboardServiceTestRouteImport } from './routes/dashboard/service-test' +import { Route as DashboardSearchRouteImport } from './routes/dashboard/search' +import { Route as DashboardHistoryRouteImport } from './routes/dashboard/history' +import { Route as DashboardChangelogRouteImport } from './routes/dashboard/changelog' +import { Route as DashboardBillingRouteImport } from './routes/dashboard/billing' +import { Route as BlogSlugRouteImport } from './routes/blog_/$slug' +import { Route as AuthResetPasswordRouteImport } from './routes/_auth/reset-password' +import { Route as AuthRegisterRouteImport } from './routes/_auth/register' +import { Route as AuthLoginRouteImport } from './routes/_auth/login' +import { Route as AuthForgotPasswordRouteImport } from './routes/_auth/forgot-password' +import { Route as AuthEmailVerifiedRouteImport } from './routes/_auth/email-verified' +import { Route as DashboardSubscriptionIndexRouteImport } from './routes/dashboard/subscription/index' +import { Route as DashboardCatalogIndexRouteImport } from './routes/dashboard/catalog/index' +import { Route as DashboardAdminIndexRouteImport } from './routes/dashboard/admin/index' +import { Route as DemoCategoriesCategoryIdRouteImport } from './routes/demo_/categories_/$categoryId' +import { Route as DashboardOemCodeRouteImport } from './routes/dashboard/oem.$code' +import { Route as DashboardAdminUsersRouteImport } from './routes/dashboard/admin/users' +import { Route as DashboardAdminReferralsRouteImport } from './routes/dashboard/admin/referrals' +import { Route as DashboardAdminCopyLogsRouteImport } from './routes/dashboard/admin/copy-logs' +import { Route as DashboardAdminAnalyticsRouteImport } from './routes/dashboard/admin/analytics' +import { Route as DashboardVehiclesIdIndexRouteImport } from './routes/dashboard/vehicles_/$id/index' +import { Route as DashboardCatalogBrandNameIndexRouteImport } from './routes/dashboard/catalog_/$brandName/index' +import { Route as DashboardCatalogPcatCatalogIdIndexRouteImport } from './routes/dashboard/catalog_/pcat/$catalogId/index' +import { Route as DashboardCatalogEmexCatalogCodeIndexRouteImport } from './routes/dashboard/catalog_/emex/$catalogCode/index' +import { Route as DashboardCatalogBrandNameModelIdIndexRouteImport } from './routes/dashboard/catalog_/$brandName_/$modelId/index' +import { Route as DashboardVehiclesIdCategoriesCategoryIdRouteImport } from './routes/dashboard/vehicles_/$id/categories_/$categoryId' +import { Route as DashboardCatalogPcatCatalogIdModelIdIndexRouteImport } from './routes/dashboard/catalog_/pcat/$catalogId_/$modelId/index' +import { Route as DashboardCatalogEmexCatalogCodeVehicleIdIndexRouteImport } from './routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId/index' +import { Route as DashboardCatalogBrandNameModelIdCategoriesCategoryIdRouteImport } from './routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId' +import { Route as DashboardCatalogPcatCatalogIdModelIdCarIdIndexRouteImport } from './routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/index' +import { Route as DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRouteImport } from './routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId' +import { Route as DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRouteImport } from './routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId' const TermsRoute = TermsRouteImport.update({ - id: "/terms", - path: "/terms", + id: '/terms', + path: '/terms', getParentRoute: () => rootRouteImport, } as any) const PrivacyRoute = PrivacyRouteImport.update({ - id: "/privacy", - path: "/privacy", + id: '/privacy', + path: '/privacy', getParentRoute: () => rootRouteImport, } as any) const PricingRoute = PricingRouteImport.update({ - id: "/pricing", - path: "/pricing", + id: '/pricing', + path: '/pricing', getParentRoute: () => rootRouteImport, } as any) const KvkkRoute = KvkkRouteImport.update({ - id: "/kvkk", - path: "/kvkk", + id: '/kvkk', + path: '/kvkk', getParentRoute: () => rootRouteImport, } as any) const DemoRoute = DemoRouteImport.update({ - id: "/demo", - path: "/demo", + id: '/demo', + path: '/demo', getParentRoute: () => rootRouteImport, } as any) const DashboardRoute = DashboardRouteImport.update({ - id: "/dashboard", - path: "/dashboard", + id: '/dashboard', + path: '/dashboard', getParentRoute: () => rootRouteImport, } as any) const ContactRoute = ContactRouteImport.update({ - id: "/contact", - path: "/contact", + id: '/contact', + path: '/contact', getParentRoute: () => rootRouteImport, } as any) const BlogRoute = BlogRouteImport.update({ - id: "/blog", - path: "/blog", + id: '/blog', + path: '/blog', getParentRoute: () => rootRouteImport, } as any) const AboutRoute = AboutRouteImport.update({ - id: "/about", - path: "/about", + id: '/about', + path: '/about', getParentRoute: () => rootRouteImport, } as any) const AuthRoute = AuthRouteImport.update({ - id: "/_auth", + id: '/_auth', getParentRoute: () => rootRouteImport, } as any) const IndexRoute = IndexRouteImport.update({ - id: "/", - path: "/", + id: '/', + path: '/', getParentRoute: () => rootRouteImport, } as any) const DashboardIndexRoute = DashboardIndexRouteImport.update({ - id: "/", - path: "/", + id: '/', + path: '/', getParentRoute: () => DashboardRoute, } as any) const DashboardSettingsRoute = DashboardSettingsRouteImport.update({ - id: "/settings", - path: "/settings", + id: '/settings', + path: '/settings', getParentRoute: () => DashboardRoute, } as any) const DashboardServiceTestRoute = DashboardServiceTestRouteImport.update({ - id: "/service-test", - path: "/service-test", + id: '/service-test', + path: '/service-test', getParentRoute: () => DashboardRoute, } as any) const DashboardSearchRoute = DashboardSearchRouteImport.update({ - id: "/search", - path: "/search", + id: '/search', + path: '/search', getParentRoute: () => DashboardRoute, } as any) const DashboardHistoryRoute = DashboardHistoryRouteImport.update({ - id: "/history", - path: "/history", + id: '/history', + path: '/history', getParentRoute: () => DashboardRoute, } as any) const DashboardChangelogRoute = DashboardChangelogRouteImport.update({ - id: "/changelog", - path: "/changelog", + id: '/changelog', + path: '/changelog', getParentRoute: () => DashboardRoute, } as any) const DashboardBillingRoute = DashboardBillingRouteImport.update({ - id: "/billing", - path: "/billing", + id: '/billing', + path: '/billing', getParentRoute: () => DashboardRoute, } as any) const BlogSlugRoute = BlogSlugRouteImport.update({ - id: "/blog_/$slug", - path: "/blog/$slug", + id: '/blog_/$slug', + path: '/blog/$slug', getParentRoute: () => rootRouteImport, } as any) const AuthResetPasswordRoute = AuthResetPasswordRouteImport.update({ - id: "/reset-password", - path: "/reset-password", + id: '/reset-password', + path: '/reset-password', getParentRoute: () => AuthRoute, } as any) const AuthRegisterRoute = AuthRegisterRouteImport.update({ - id: "/register", - path: "/register", + id: '/register', + path: '/register', getParentRoute: () => AuthRoute, } as any) const AuthLoginRoute = AuthLoginRouteImport.update({ - id: "/login", - path: "/login", + id: '/login', + path: '/login', getParentRoute: () => AuthRoute, } as any) const AuthForgotPasswordRoute = AuthForgotPasswordRouteImport.update({ - id: "/forgot-password", - path: "/forgot-password", + id: '/forgot-password', + path: '/forgot-password', getParentRoute: () => AuthRoute, } as any) const AuthEmailVerifiedRoute = AuthEmailVerifiedRouteImport.update({ - id: "/email-verified", - path: "/email-verified", + id: '/email-verified', + path: '/email-verified', getParentRoute: () => AuthRoute, } as any) const DashboardSubscriptionIndexRoute = DashboardSubscriptionIndexRouteImport.update({ - id: "/subscription/", - path: "/subscription/", + id: '/subscription/', + path: '/subscription/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogIndexRoute = DashboardCatalogIndexRouteImport.update({ - id: "/catalog/", - path: "/catalog/", + id: '/catalog/', + path: '/catalog/', getParentRoute: () => DashboardRoute, } as any) const DashboardAdminIndexRoute = DashboardAdminIndexRouteImport.update({ - id: "/admin/", - path: "/admin/", + id: '/admin/', + path: '/admin/', getParentRoute: () => DashboardRoute, } as any) const DemoCategoriesCategoryIdRoute = DemoCategoriesCategoryIdRouteImport.update({ - id: "/demo_/categories_/$categoryId", - path: "/demo/categories/$categoryId", + id: '/demo_/categories_/$categoryId', + path: '/demo/categories/$categoryId', getParentRoute: () => rootRouteImport, } as any) +const DashboardOemCodeRoute = DashboardOemCodeRouteImport.update({ + id: '/oem/$code', + path: '/oem/$code', + getParentRoute: () => DashboardRoute, +} as any) const DashboardAdminUsersRoute = DashboardAdminUsersRouteImport.update({ - id: "/admin/users", - path: "/admin/users", + id: '/admin/users', + path: '/admin/users', getParentRoute: () => DashboardRoute, } as any) const DashboardAdminReferralsRoute = DashboardAdminReferralsRouteImport.update({ - id: "/admin/referrals", - path: "/admin/referrals", + id: '/admin/referrals', + path: '/admin/referrals', getParentRoute: () => DashboardRoute, } as any) const DashboardAdminCopyLogsRoute = DashboardAdminCopyLogsRouteImport.update({ - id: "/admin/copy-logs", - path: "/admin/copy-logs", + id: '/admin/copy-logs', + path: '/admin/copy-logs', getParentRoute: () => DashboardRoute, } as any) const DashboardAdminAnalyticsRoute = DashboardAdminAnalyticsRouteImport.update({ - id: "/admin/analytics", - path: "/admin/analytics", + id: '/admin/analytics', + path: '/admin/analytics', getParentRoute: () => DashboardRoute, } as any) const DashboardVehiclesIdIndexRoute = DashboardVehiclesIdIndexRouteImport.update({ - id: "/vehicles_/$id/", - path: "/vehicles/$id/", + id: '/vehicles_/$id/', + path: '/vehicles/$id/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogBrandNameIndexRoute = DashboardCatalogBrandNameIndexRouteImport.update({ - id: "/catalog_/$brandName/", - path: "/catalog/$brandName/", + id: '/catalog_/$brandName/', + path: '/catalog/$brandName/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogPcatCatalogIdIndexRoute = DashboardCatalogPcatCatalogIdIndexRouteImport.update({ - id: "/catalog_/pcat/$catalogId/", - path: "/catalog/pcat/$catalogId/", + id: '/catalog_/pcat/$catalogId/', + path: '/catalog/pcat/$catalogId/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogEmexCatalogCodeIndexRoute = DashboardCatalogEmexCatalogCodeIndexRouteImport.update({ - id: "/catalog_/emex/$catalogCode/", - path: "/catalog/emex/$catalogCode/", + id: '/catalog_/emex/$catalogCode/', + path: '/catalog/emex/$catalogCode/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogBrandNameModelIdIndexRoute = DashboardCatalogBrandNameModelIdIndexRouteImport.update({ - id: "/catalog_/$brandName_/$modelId/", - path: "/catalog/$brandName/$modelId/", + id: '/catalog_/$brandName_/$modelId/', + path: '/catalog/$brandName/$modelId/', getParentRoute: () => DashboardRoute, } as any) const DashboardVehiclesIdCategoriesCategoryIdRoute = DashboardVehiclesIdCategoriesCategoryIdRouteImport.update({ - id: "/vehicles_/$id/categories_/$categoryId", - path: "/vehicles/$id/categories/$categoryId", + id: '/vehicles_/$id/categories_/$categoryId', + path: '/vehicles/$id/categories/$categoryId', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogPcatCatalogIdModelIdIndexRoute = DashboardCatalogPcatCatalogIdModelIdIndexRouteImport.update({ - id: "/catalog_/pcat/$catalogId_/$modelId/", - path: "/catalog/pcat/$catalogId/$modelId/", + id: '/catalog_/pcat/$catalogId_/$modelId/', + path: '/catalog/pcat/$catalogId/$modelId/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogEmexCatalogCodeVehicleIdIndexRoute = DashboardCatalogEmexCatalogCodeVehicleIdIndexRouteImport.update({ - id: "/catalog_/emex/$catalogCode_/$vehicleId/", - path: "/catalog/emex/$catalogCode/$vehicleId/", + id: '/catalog_/emex/$catalogCode_/$vehicleId/', + path: '/catalog/emex/$catalogCode/$vehicleId/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogBrandNameModelIdCategoriesCategoryIdRoute = DashboardCatalogBrandNameModelIdCategoriesCategoryIdRouteImport.update({ - id: "/catalog_/$brandName_/$modelId/categories_/$categoryId", - path: "/catalog/$brandName/$modelId/categories/$categoryId", + id: '/catalog_/$brandName_/$modelId/categories_/$categoryId', + path: '/catalog/$brandName/$modelId/categories/$categoryId', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogPcatCatalogIdModelIdCarIdIndexRoute = DashboardCatalogPcatCatalogIdModelIdCarIdIndexRouteImport.update({ - id: "/catalog_/pcat/$catalogId_/$modelId_/$carId/", - path: "/catalog/pcat/$catalogId/$modelId/$carId/", + id: '/catalog_/pcat/$catalogId_/$modelId_/$carId/', + path: '/catalog/pcat/$catalogId/$modelId/$carId/', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRoute = DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRouteImport.update({ - id: "/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId", - path: "/catalog/emex/$catalogCode/$vehicleId/groups/$groupId", + id: '/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId', + path: '/catalog/emex/$catalogCode/$vehicleId/groups/$groupId', getParentRoute: () => DashboardRoute, } as any) const DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRoute = DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRouteImport.update({ - id: "/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId", - path: "/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId", + id: '/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId', + path: '/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId', getParentRoute: () => DashboardRoute, } as any) export interface FileRoutesByFullPath { - "/": typeof IndexRoute - "/about": typeof AboutRoute - "/blog": typeof BlogRoute - "/contact": typeof ContactRoute - "/dashboard": typeof DashboardRouteWithChildren - "/demo": typeof DemoRoute - "/kvkk": typeof KvkkRoute - "/pricing": typeof PricingRoute - "/privacy": typeof PrivacyRoute - "/terms": typeof TermsRoute - "/email-verified": typeof AuthEmailVerifiedRoute - "/forgot-password": typeof AuthForgotPasswordRoute - "/login": typeof AuthLoginRoute - "/register": typeof AuthRegisterRoute - "/reset-password": typeof AuthResetPasswordRoute - "/blog/$slug": typeof BlogSlugRoute - "/dashboard/billing": typeof DashboardBillingRoute - "/dashboard/changelog": typeof DashboardChangelogRoute - "/dashboard/history": typeof DashboardHistoryRoute - "/dashboard/search": typeof DashboardSearchRoute - "/dashboard/service-test": typeof DashboardServiceTestRoute - "/dashboard/settings": typeof DashboardSettingsRoute - "/dashboard/": typeof DashboardIndexRoute - "/dashboard/admin/analytics": typeof DashboardAdminAnalyticsRoute - "/dashboard/admin/copy-logs": typeof DashboardAdminCopyLogsRoute - "/dashboard/admin/referrals": typeof DashboardAdminReferralsRoute - "/dashboard/admin/users": typeof DashboardAdminUsersRoute - "/demo/categories/$categoryId": typeof DemoCategoriesCategoryIdRoute - "/dashboard/admin/": typeof DashboardAdminIndexRoute - "/dashboard/catalog/": typeof DashboardCatalogIndexRoute - "/dashboard/subscription/": typeof DashboardSubscriptionIndexRoute - "/dashboard/catalog/$brandName/": typeof DashboardCatalogBrandNameIndexRoute - "/dashboard/vehicles/$id/": typeof DashboardVehiclesIdIndexRoute - "/dashboard/vehicles/$id/categories/$categoryId": typeof DashboardVehiclesIdCategoriesCategoryIdRoute - "/dashboard/catalog/$brandName/$modelId/": typeof DashboardCatalogBrandNameModelIdIndexRoute - "/dashboard/catalog/emex/$catalogCode/": typeof DashboardCatalogEmexCatalogCodeIndexRoute - "/dashboard/catalog/pcat/$catalogId/": typeof DashboardCatalogPcatCatalogIdIndexRoute - "/dashboard/catalog/$brandName/$modelId/categories/$categoryId": typeof DashboardCatalogBrandNameModelIdCategoriesCategoryIdRoute - "/dashboard/catalog/emex/$catalogCode/$vehicleId/": typeof DashboardCatalogEmexCatalogCodeVehicleIdIndexRoute - "/dashboard/catalog/pcat/$catalogId/$modelId/": typeof DashboardCatalogPcatCatalogIdModelIdIndexRoute - "/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId": typeof DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRoute - "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/": typeof DashboardCatalogPcatCatalogIdModelIdCarIdIndexRoute - "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId": typeof DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRoute + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRoute + '/contact': typeof ContactRoute + '/dashboard': typeof DashboardRouteWithChildren + '/demo': typeof DemoRoute + '/kvkk': typeof KvkkRoute + '/pricing': typeof PricingRoute + '/privacy': typeof PrivacyRoute + '/terms': typeof TermsRoute + '/email-verified': typeof AuthEmailVerifiedRoute + '/forgot-password': typeof AuthForgotPasswordRoute + '/login': typeof AuthLoginRoute + '/register': typeof AuthRegisterRoute + '/reset-password': typeof AuthResetPasswordRoute + '/blog/$slug': typeof BlogSlugRoute + '/dashboard/billing': typeof DashboardBillingRoute + '/dashboard/changelog': typeof DashboardChangelogRoute + '/dashboard/history': typeof DashboardHistoryRoute + '/dashboard/search': typeof DashboardSearchRoute + '/dashboard/service-test': typeof DashboardServiceTestRoute + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/': typeof DashboardIndexRoute + '/dashboard/admin/analytics': typeof DashboardAdminAnalyticsRoute + '/dashboard/admin/copy-logs': typeof DashboardAdminCopyLogsRoute + '/dashboard/admin/referrals': typeof DashboardAdminReferralsRoute + '/dashboard/admin/users': typeof DashboardAdminUsersRoute + '/dashboard/oem/$code': typeof DashboardOemCodeRoute + '/demo/categories/$categoryId': typeof DemoCategoriesCategoryIdRoute + '/dashboard/admin/': typeof DashboardAdminIndexRoute + '/dashboard/catalog/': typeof DashboardCatalogIndexRoute + '/dashboard/subscription/': typeof DashboardSubscriptionIndexRoute + '/dashboard/catalog/$brandName/': typeof DashboardCatalogBrandNameIndexRoute + '/dashboard/vehicles/$id/': typeof DashboardVehiclesIdIndexRoute + '/dashboard/vehicles/$id/categories/$categoryId': typeof DashboardVehiclesIdCategoriesCategoryIdRoute + '/dashboard/catalog/$brandName/$modelId/': typeof DashboardCatalogBrandNameModelIdIndexRoute + '/dashboard/catalog/emex/$catalogCode/': typeof DashboardCatalogEmexCatalogCodeIndexRoute + '/dashboard/catalog/pcat/$catalogId/': typeof DashboardCatalogPcatCatalogIdIndexRoute + '/dashboard/catalog/$brandName/$modelId/categories/$categoryId': typeof DashboardCatalogBrandNameModelIdCategoriesCategoryIdRoute + '/dashboard/catalog/emex/$catalogCode/$vehicleId/': typeof DashboardCatalogEmexCatalogCodeVehicleIdIndexRoute + '/dashboard/catalog/pcat/$catalogId/$modelId/': typeof DashboardCatalogPcatCatalogIdModelIdIndexRoute + '/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId': typeof DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRoute + '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/': typeof DashboardCatalogPcatCatalogIdModelIdCarIdIndexRoute + '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId': typeof DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRoute } export interface FileRoutesByTo { - "/": typeof IndexRoute - "/about": typeof AboutRoute - "/blog": typeof BlogRoute - "/contact": typeof ContactRoute - "/demo": typeof DemoRoute - "/kvkk": typeof KvkkRoute - "/pricing": typeof PricingRoute - "/privacy": typeof PrivacyRoute - "/terms": typeof TermsRoute - "/email-verified": typeof AuthEmailVerifiedRoute - "/forgot-password": typeof AuthForgotPasswordRoute - "/login": typeof AuthLoginRoute - "/register": typeof AuthRegisterRoute - "/reset-password": typeof AuthResetPasswordRoute - "/blog/$slug": typeof BlogSlugRoute - "/dashboard/billing": typeof DashboardBillingRoute - "/dashboard/changelog": typeof DashboardChangelogRoute - "/dashboard/history": typeof DashboardHistoryRoute - "/dashboard/search": typeof DashboardSearchRoute - "/dashboard/service-test": typeof DashboardServiceTestRoute - "/dashboard/settings": typeof DashboardSettingsRoute - "/dashboard": typeof DashboardIndexRoute - "/dashboard/admin/analytics": typeof DashboardAdminAnalyticsRoute - "/dashboard/admin/copy-logs": typeof DashboardAdminCopyLogsRoute - "/dashboard/admin/referrals": typeof DashboardAdminReferralsRoute - "/dashboard/admin/users": typeof DashboardAdminUsersRoute - "/demo/categories/$categoryId": typeof DemoCategoriesCategoryIdRoute - "/dashboard/admin": typeof DashboardAdminIndexRoute - "/dashboard/catalog": typeof DashboardCatalogIndexRoute - "/dashboard/subscription": typeof DashboardSubscriptionIndexRoute - "/dashboard/catalog/$brandName": typeof DashboardCatalogBrandNameIndexRoute - "/dashboard/vehicles/$id": typeof DashboardVehiclesIdIndexRoute - "/dashboard/vehicles/$id/categories/$categoryId": typeof DashboardVehiclesIdCategoriesCategoryIdRoute - "/dashboard/catalog/$brandName/$modelId": typeof DashboardCatalogBrandNameModelIdIndexRoute - "/dashboard/catalog/emex/$catalogCode": typeof DashboardCatalogEmexCatalogCodeIndexRoute - "/dashboard/catalog/pcat/$catalogId": typeof DashboardCatalogPcatCatalogIdIndexRoute - "/dashboard/catalog/$brandName/$modelId/categories/$categoryId": typeof DashboardCatalogBrandNameModelIdCategoriesCategoryIdRoute - "/dashboard/catalog/emex/$catalogCode/$vehicleId": typeof DashboardCatalogEmexCatalogCodeVehicleIdIndexRoute - "/dashboard/catalog/pcat/$catalogId/$modelId": typeof DashboardCatalogPcatCatalogIdModelIdIndexRoute - "/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId": typeof DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRoute - "/dashboard/catalog/pcat/$catalogId/$modelId/$carId": typeof DashboardCatalogPcatCatalogIdModelIdCarIdIndexRoute - "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId": typeof DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRoute + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/blog': typeof BlogRoute + '/contact': typeof ContactRoute + '/demo': typeof DemoRoute + '/kvkk': typeof KvkkRoute + '/pricing': typeof PricingRoute + '/privacy': typeof PrivacyRoute + '/terms': typeof TermsRoute + '/email-verified': typeof AuthEmailVerifiedRoute + '/forgot-password': typeof AuthForgotPasswordRoute + '/login': typeof AuthLoginRoute + '/register': typeof AuthRegisterRoute + '/reset-password': typeof AuthResetPasswordRoute + '/blog/$slug': typeof BlogSlugRoute + '/dashboard/billing': typeof DashboardBillingRoute + '/dashboard/changelog': typeof DashboardChangelogRoute + '/dashboard/history': typeof DashboardHistoryRoute + '/dashboard/search': typeof DashboardSearchRoute + '/dashboard/service-test': typeof DashboardServiceTestRoute + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard': typeof DashboardIndexRoute + '/dashboard/admin/analytics': typeof DashboardAdminAnalyticsRoute + '/dashboard/admin/copy-logs': typeof DashboardAdminCopyLogsRoute + '/dashboard/admin/referrals': typeof DashboardAdminReferralsRoute + '/dashboard/admin/users': typeof DashboardAdminUsersRoute + '/dashboard/oem/$code': typeof DashboardOemCodeRoute + '/demo/categories/$categoryId': typeof DemoCategoriesCategoryIdRoute + '/dashboard/admin': typeof DashboardAdminIndexRoute + '/dashboard/catalog': typeof DashboardCatalogIndexRoute + '/dashboard/subscription': typeof DashboardSubscriptionIndexRoute + '/dashboard/catalog/$brandName': typeof DashboardCatalogBrandNameIndexRoute + '/dashboard/vehicles/$id': typeof DashboardVehiclesIdIndexRoute + '/dashboard/vehicles/$id/categories/$categoryId': typeof DashboardVehiclesIdCategoriesCategoryIdRoute + '/dashboard/catalog/$brandName/$modelId': typeof DashboardCatalogBrandNameModelIdIndexRoute + '/dashboard/catalog/emex/$catalogCode': typeof DashboardCatalogEmexCatalogCodeIndexRoute + '/dashboard/catalog/pcat/$catalogId': typeof DashboardCatalogPcatCatalogIdIndexRoute + '/dashboard/catalog/$brandName/$modelId/categories/$categoryId': typeof DashboardCatalogBrandNameModelIdCategoriesCategoryIdRoute + '/dashboard/catalog/emex/$catalogCode/$vehicleId': typeof DashboardCatalogEmexCatalogCodeVehicleIdIndexRoute + '/dashboard/catalog/pcat/$catalogId/$modelId': typeof DashboardCatalogPcatCatalogIdModelIdIndexRoute + '/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId': typeof DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRoute + '/dashboard/catalog/pcat/$catalogId/$modelId/$carId': typeof DashboardCatalogPcatCatalogIdModelIdCarIdIndexRoute + '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId': typeof DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport - "/": typeof IndexRoute - "/_auth": typeof AuthRouteWithChildren - "/about": typeof AboutRoute - "/blog": typeof BlogRoute - "/contact": typeof ContactRoute - "/dashboard": typeof DashboardRouteWithChildren - "/demo": typeof DemoRoute - "/kvkk": typeof KvkkRoute - "/pricing": typeof PricingRoute - "/privacy": typeof PrivacyRoute - "/terms": typeof TermsRoute - "/_auth/email-verified": typeof AuthEmailVerifiedRoute - "/_auth/forgot-password": typeof AuthForgotPasswordRoute - "/_auth/login": typeof AuthLoginRoute - "/_auth/register": typeof AuthRegisterRoute - "/_auth/reset-password": typeof AuthResetPasswordRoute - "/blog_/$slug": typeof BlogSlugRoute - "/dashboard/billing": typeof DashboardBillingRoute - "/dashboard/changelog": typeof DashboardChangelogRoute - "/dashboard/history": typeof DashboardHistoryRoute - "/dashboard/search": typeof DashboardSearchRoute - "/dashboard/service-test": typeof DashboardServiceTestRoute - "/dashboard/settings": typeof DashboardSettingsRoute - "/dashboard/": typeof DashboardIndexRoute - "/dashboard/admin/analytics": typeof DashboardAdminAnalyticsRoute - "/dashboard/admin/copy-logs": typeof DashboardAdminCopyLogsRoute - "/dashboard/admin/referrals": typeof DashboardAdminReferralsRoute - "/dashboard/admin/users": typeof DashboardAdminUsersRoute - "/demo_/categories_/$categoryId": typeof DemoCategoriesCategoryIdRoute - "/dashboard/admin/": typeof DashboardAdminIndexRoute - "/dashboard/catalog/": typeof DashboardCatalogIndexRoute - "/dashboard/subscription/": typeof DashboardSubscriptionIndexRoute - "/dashboard/catalog_/$brandName/": typeof DashboardCatalogBrandNameIndexRoute - "/dashboard/vehicles_/$id/": typeof DashboardVehiclesIdIndexRoute - "/dashboard/vehicles_/$id/categories_/$categoryId": typeof DashboardVehiclesIdCategoriesCategoryIdRoute - "/dashboard/catalog_/$brandName_/$modelId/": typeof DashboardCatalogBrandNameModelIdIndexRoute - "/dashboard/catalog_/emex/$catalogCode/": typeof DashboardCatalogEmexCatalogCodeIndexRoute - "/dashboard/catalog_/pcat/$catalogId/": typeof DashboardCatalogPcatCatalogIdIndexRoute - "/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId": typeof DashboardCatalogBrandNameModelIdCategoriesCategoryIdRoute - "/dashboard/catalog_/emex/$catalogCode_/$vehicleId/": typeof DashboardCatalogEmexCatalogCodeVehicleIdIndexRoute - "/dashboard/catalog_/pcat/$catalogId_/$modelId/": typeof DashboardCatalogPcatCatalogIdModelIdIndexRoute - "/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId": typeof DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRoute - "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/": typeof DashboardCatalogPcatCatalogIdModelIdCarIdIndexRoute - "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId": typeof DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRoute + '/': typeof IndexRoute + '/_auth': typeof AuthRouteWithChildren + '/about': typeof AboutRoute + '/blog': typeof BlogRoute + '/contact': typeof ContactRoute + '/dashboard': typeof DashboardRouteWithChildren + '/demo': typeof DemoRoute + '/kvkk': typeof KvkkRoute + '/pricing': typeof PricingRoute + '/privacy': typeof PrivacyRoute + '/terms': typeof TermsRoute + '/_auth/email-verified': typeof AuthEmailVerifiedRoute + '/_auth/forgot-password': typeof AuthForgotPasswordRoute + '/_auth/login': typeof AuthLoginRoute + '/_auth/register': typeof AuthRegisterRoute + '/_auth/reset-password': typeof AuthResetPasswordRoute + '/blog_/$slug': typeof BlogSlugRoute + '/dashboard/billing': typeof DashboardBillingRoute + '/dashboard/changelog': typeof DashboardChangelogRoute + '/dashboard/history': typeof DashboardHistoryRoute + '/dashboard/search': typeof DashboardSearchRoute + '/dashboard/service-test': typeof DashboardServiceTestRoute + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/': typeof DashboardIndexRoute + '/dashboard/admin/analytics': typeof DashboardAdminAnalyticsRoute + '/dashboard/admin/copy-logs': typeof DashboardAdminCopyLogsRoute + '/dashboard/admin/referrals': typeof DashboardAdminReferralsRoute + '/dashboard/admin/users': typeof DashboardAdminUsersRoute + '/dashboard/oem/$code': typeof DashboardOemCodeRoute + '/demo_/categories_/$categoryId': typeof DemoCategoriesCategoryIdRoute + '/dashboard/admin/': typeof DashboardAdminIndexRoute + '/dashboard/catalog/': typeof DashboardCatalogIndexRoute + '/dashboard/subscription/': typeof DashboardSubscriptionIndexRoute + '/dashboard/catalog_/$brandName/': typeof DashboardCatalogBrandNameIndexRoute + '/dashboard/vehicles_/$id/': typeof DashboardVehiclesIdIndexRoute + '/dashboard/vehicles_/$id/categories_/$categoryId': typeof DashboardVehiclesIdCategoriesCategoryIdRoute + '/dashboard/catalog_/$brandName_/$modelId/': typeof DashboardCatalogBrandNameModelIdIndexRoute + '/dashboard/catalog_/emex/$catalogCode/': typeof DashboardCatalogEmexCatalogCodeIndexRoute + '/dashboard/catalog_/pcat/$catalogId/': typeof DashboardCatalogPcatCatalogIdIndexRoute + '/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId': typeof DashboardCatalogBrandNameModelIdCategoriesCategoryIdRoute + '/dashboard/catalog_/emex/$catalogCode_/$vehicleId/': typeof DashboardCatalogEmexCatalogCodeVehicleIdIndexRoute + '/dashboard/catalog_/pcat/$catalogId_/$modelId/': typeof DashboardCatalogPcatCatalogIdModelIdIndexRoute + '/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId': typeof DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRoute + '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/': typeof DashboardCatalogPcatCatalogIdModelIdCarIdIndexRoute + '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId': typeof DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: - | "/" - | "/about" - | "/blog" - | "/contact" - | "/dashboard" - | "/demo" - | "/kvkk" - | "/pricing" - | "/privacy" - | "/terms" - | "/email-verified" - | "/forgot-password" - | "/login" - | "/register" - | "/reset-password" - | "/blog/$slug" - | "/dashboard/billing" - | "/dashboard/changelog" - | "/dashboard/history" - | "/dashboard/search" - | "/dashboard/service-test" - | "/dashboard/settings" - | "/dashboard/" - | "/dashboard/admin/analytics" - | "/dashboard/admin/copy-logs" - | "/dashboard/admin/referrals" - | "/dashboard/admin/users" - | "/demo/categories/$categoryId" - | "/dashboard/admin/" - | "/dashboard/catalog/" - | "/dashboard/subscription/" - | "/dashboard/catalog/$brandName/" - | "/dashboard/vehicles/$id/" - | "/dashboard/vehicles/$id/categories/$categoryId" - | "/dashboard/catalog/$brandName/$modelId/" - | "/dashboard/catalog/emex/$catalogCode/" - | "/dashboard/catalog/pcat/$catalogId/" - | "/dashboard/catalog/$brandName/$modelId/categories/$categoryId" - | "/dashboard/catalog/emex/$catalogCode/$vehicleId/" - | "/dashboard/catalog/pcat/$catalogId/$modelId/" - | "/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId" - | "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/" - | "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId" + | '/' + | '/about' + | '/blog' + | '/contact' + | '/dashboard' + | '/demo' + | '/kvkk' + | '/pricing' + | '/privacy' + | '/terms' + | '/email-verified' + | '/forgot-password' + | '/login' + | '/register' + | '/reset-password' + | '/blog/$slug' + | '/dashboard/billing' + | '/dashboard/changelog' + | '/dashboard/history' + | '/dashboard/search' + | '/dashboard/service-test' + | '/dashboard/settings' + | '/dashboard/' + | '/dashboard/admin/analytics' + | '/dashboard/admin/copy-logs' + | '/dashboard/admin/referrals' + | '/dashboard/admin/users' + | '/dashboard/oem/$code' + | '/demo/categories/$categoryId' + | '/dashboard/admin/' + | '/dashboard/catalog/' + | '/dashboard/subscription/' + | '/dashboard/catalog/$brandName/' + | '/dashboard/vehicles/$id/' + | '/dashboard/vehicles/$id/categories/$categoryId' + | '/dashboard/catalog/$brandName/$modelId/' + | '/dashboard/catalog/emex/$catalogCode/' + | '/dashboard/catalog/pcat/$catalogId/' + | '/dashboard/catalog/$brandName/$modelId/categories/$categoryId' + | '/dashboard/catalog/emex/$catalogCode/$vehicleId/' + | '/dashboard/catalog/pcat/$catalogId/$modelId/' + | '/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId' + | '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/' + | '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId' fileRoutesByTo: FileRoutesByTo to: - | "/" - | "/about" - | "/blog" - | "/contact" - | "/demo" - | "/kvkk" - | "/pricing" - | "/privacy" - | "/terms" - | "/email-verified" - | "/forgot-password" - | "/login" - | "/register" - | "/reset-password" - | "/blog/$slug" - | "/dashboard/billing" - | "/dashboard/changelog" - | "/dashboard/history" - | "/dashboard/search" - | "/dashboard/service-test" - | "/dashboard/settings" - | "/dashboard" - | "/dashboard/admin/analytics" - | "/dashboard/admin/copy-logs" - | "/dashboard/admin/referrals" - | "/dashboard/admin/users" - | "/demo/categories/$categoryId" - | "/dashboard/admin" - | "/dashboard/catalog" - | "/dashboard/subscription" - | "/dashboard/catalog/$brandName" - | "/dashboard/vehicles/$id" - | "/dashboard/vehicles/$id/categories/$categoryId" - | "/dashboard/catalog/$brandName/$modelId" - | "/dashboard/catalog/emex/$catalogCode" - | "/dashboard/catalog/pcat/$catalogId" - | "/dashboard/catalog/$brandName/$modelId/categories/$categoryId" - | "/dashboard/catalog/emex/$catalogCode/$vehicleId" - | "/dashboard/catalog/pcat/$catalogId/$modelId" - | "/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId" - | "/dashboard/catalog/pcat/$catalogId/$modelId/$carId" - | "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId" + | '/' + | '/about' + | '/blog' + | '/contact' + | '/demo' + | '/kvkk' + | '/pricing' + | '/privacy' + | '/terms' + | '/email-verified' + | '/forgot-password' + | '/login' + | '/register' + | '/reset-password' + | '/blog/$slug' + | '/dashboard/billing' + | '/dashboard/changelog' + | '/dashboard/history' + | '/dashboard/search' + | '/dashboard/service-test' + | '/dashboard/settings' + | '/dashboard' + | '/dashboard/admin/analytics' + | '/dashboard/admin/copy-logs' + | '/dashboard/admin/referrals' + | '/dashboard/admin/users' + | '/dashboard/oem/$code' + | '/demo/categories/$categoryId' + | '/dashboard/admin' + | '/dashboard/catalog' + | '/dashboard/subscription' + | '/dashboard/catalog/$brandName' + | '/dashboard/vehicles/$id' + | '/dashboard/vehicles/$id/categories/$categoryId' + | '/dashboard/catalog/$brandName/$modelId' + | '/dashboard/catalog/emex/$catalogCode' + | '/dashboard/catalog/pcat/$catalogId' + | '/dashboard/catalog/$brandName/$modelId/categories/$categoryId' + | '/dashboard/catalog/emex/$catalogCode/$vehicleId' + | '/dashboard/catalog/pcat/$catalogId/$modelId' + | '/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId' + | '/dashboard/catalog/pcat/$catalogId/$modelId/$carId' + | '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId' id: - | "__root__" - | "/" - | "/_auth" - | "/about" - | "/blog" - | "/contact" - | "/dashboard" - | "/demo" - | "/kvkk" - | "/pricing" - | "/privacy" - | "/terms" - | "/_auth/email-verified" - | "/_auth/forgot-password" - | "/_auth/login" - | "/_auth/register" - | "/_auth/reset-password" - | "/blog_/$slug" - | "/dashboard/billing" - | "/dashboard/changelog" - | "/dashboard/history" - | "/dashboard/search" - | "/dashboard/service-test" - | "/dashboard/settings" - | "/dashboard/" - | "/dashboard/admin/analytics" - | "/dashboard/admin/copy-logs" - | "/dashboard/admin/referrals" - | "/dashboard/admin/users" - | "/demo_/categories_/$categoryId" - | "/dashboard/admin/" - | "/dashboard/catalog/" - | "/dashboard/subscription/" - | "/dashboard/catalog_/$brandName/" - | "/dashboard/vehicles_/$id/" - | "/dashboard/vehicles_/$id/categories_/$categoryId" - | "/dashboard/catalog_/$brandName_/$modelId/" - | "/dashboard/catalog_/emex/$catalogCode/" - | "/dashboard/catalog_/pcat/$catalogId/" - | "/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId" - | "/dashboard/catalog_/emex/$catalogCode_/$vehicleId/" - | "/dashboard/catalog_/pcat/$catalogId_/$modelId/" - | "/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId" - | "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/" - | "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId" + | '__root__' + | '/' + | '/_auth' + | '/about' + | '/blog' + | '/contact' + | '/dashboard' + | '/demo' + | '/kvkk' + | '/pricing' + | '/privacy' + | '/terms' + | '/_auth/email-verified' + | '/_auth/forgot-password' + | '/_auth/login' + | '/_auth/register' + | '/_auth/reset-password' + | '/blog_/$slug' + | '/dashboard/billing' + | '/dashboard/changelog' + | '/dashboard/history' + | '/dashboard/search' + | '/dashboard/service-test' + | '/dashboard/settings' + | '/dashboard/' + | '/dashboard/admin/analytics' + | '/dashboard/admin/copy-logs' + | '/dashboard/admin/referrals' + | '/dashboard/admin/users' + | '/dashboard/oem/$code' + | '/demo_/categories_/$categoryId' + | '/dashboard/admin/' + | '/dashboard/catalog/' + | '/dashboard/subscription/' + | '/dashboard/catalog_/$brandName/' + | '/dashboard/vehicles_/$id/' + | '/dashboard/vehicles_/$id/categories_/$categoryId' + | '/dashboard/catalog_/$brandName_/$modelId/' + | '/dashboard/catalog_/emex/$catalogCode/' + | '/dashboard/catalog_/pcat/$catalogId/' + | '/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId' + | '/dashboard/catalog_/emex/$catalogCode_/$vehicleId/' + | '/dashboard/catalog_/pcat/$catalogId_/$modelId/' + | '/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId' + | '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/' + | '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -578,313 +590,320 @@ export interface RootRouteChildren { DemoCategoriesCategoryIdRoute: typeof DemoCategoriesCategoryIdRoute } -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface FileRoutesByPath { - "/terms": { - id: "/terms" - path: "/terms" - fullPath: "/terms" + '/terms': { + id: '/terms' + path: '/terms' + fullPath: '/terms' preLoaderRoute: typeof TermsRouteImport parentRoute: typeof rootRouteImport } - "/privacy": { - id: "/privacy" - path: "/privacy" - fullPath: "/privacy" + '/privacy': { + id: '/privacy' + path: '/privacy' + fullPath: '/privacy' preLoaderRoute: typeof PrivacyRouteImport parentRoute: typeof rootRouteImport } - "/pricing": { - id: "/pricing" - path: "/pricing" - fullPath: "/pricing" + '/pricing': { + id: '/pricing' + path: '/pricing' + fullPath: '/pricing' preLoaderRoute: typeof PricingRouteImport parentRoute: typeof rootRouteImport } - "/kvkk": { - id: "/kvkk" - path: "/kvkk" - fullPath: "/kvkk" + '/kvkk': { + id: '/kvkk' + path: '/kvkk' + fullPath: '/kvkk' preLoaderRoute: typeof KvkkRouteImport parentRoute: typeof rootRouteImport } - "/demo": { - id: "/demo" - path: "/demo" - fullPath: "/demo" + '/demo': { + id: '/demo' + path: '/demo' + fullPath: '/demo' preLoaderRoute: typeof DemoRouteImport parentRoute: typeof rootRouteImport } - "/dashboard": { - id: "/dashboard" - path: "/dashboard" - fullPath: "/dashboard" + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' preLoaderRoute: typeof DashboardRouteImport parentRoute: typeof rootRouteImport } - "/contact": { - id: "/contact" - path: "/contact" - fullPath: "/contact" + '/contact': { + id: '/contact' + path: '/contact' + fullPath: '/contact' preLoaderRoute: typeof ContactRouteImport parentRoute: typeof rootRouteImport } - "/blog": { - id: "/blog" - path: "/blog" - fullPath: "/blog" + '/blog': { + id: '/blog' + path: '/blog' + fullPath: '/blog' preLoaderRoute: typeof BlogRouteImport parentRoute: typeof rootRouteImport } - "/about": { - id: "/about" - path: "/about" - fullPath: "/about" + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' preLoaderRoute: typeof AboutRouteImport parentRoute: typeof rootRouteImport } - "/_auth": { - id: "/_auth" - path: "" - fullPath: "/" + '/_auth': { + id: '/_auth' + path: '' + fullPath: '/' preLoaderRoute: typeof AuthRouteImport parentRoute: typeof rootRouteImport } - "/": { - id: "/" - path: "/" - fullPath: "/" + '/': { + id: '/' + path: '/' + fullPath: '/' preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - "/dashboard/": { - id: "/dashboard/" - path: "/" - fullPath: "/dashboard/" + '/dashboard/': { + id: '/dashboard/' + path: '/' + fullPath: '/dashboard/' preLoaderRoute: typeof DashboardIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/settings": { - id: "/dashboard/settings" - path: "/settings" - fullPath: "/dashboard/settings" + '/dashboard/settings': { + id: '/dashboard/settings' + path: '/settings' + fullPath: '/dashboard/settings' preLoaderRoute: typeof DashboardSettingsRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/service-test": { - id: "/dashboard/service-test" - path: "/service-test" - fullPath: "/dashboard/service-test" + '/dashboard/service-test': { + id: '/dashboard/service-test' + path: '/service-test' + fullPath: '/dashboard/service-test' preLoaderRoute: typeof DashboardServiceTestRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/search": { - id: "/dashboard/search" - path: "/search" - fullPath: "/dashboard/search" + '/dashboard/search': { + id: '/dashboard/search' + path: '/search' + fullPath: '/dashboard/search' preLoaderRoute: typeof DashboardSearchRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/history": { - id: "/dashboard/history" - path: "/history" - fullPath: "/dashboard/history" + '/dashboard/history': { + id: '/dashboard/history' + path: '/history' + fullPath: '/dashboard/history' preLoaderRoute: typeof DashboardHistoryRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/changelog": { - id: "/dashboard/changelog" - path: "/changelog" - fullPath: "/dashboard/changelog" + '/dashboard/changelog': { + id: '/dashboard/changelog' + path: '/changelog' + fullPath: '/dashboard/changelog' preLoaderRoute: typeof DashboardChangelogRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/billing": { - id: "/dashboard/billing" - path: "/billing" - fullPath: "/dashboard/billing" + '/dashboard/billing': { + id: '/dashboard/billing' + path: '/billing' + fullPath: '/dashboard/billing' preLoaderRoute: typeof DashboardBillingRouteImport parentRoute: typeof DashboardRoute } - "/blog_/$slug": { - id: "/blog_/$slug" - path: "/blog/$slug" - fullPath: "/blog/$slug" + '/blog_/$slug': { + id: '/blog_/$slug' + path: '/blog/$slug' + fullPath: '/blog/$slug' preLoaderRoute: typeof BlogSlugRouteImport parentRoute: typeof rootRouteImport } - "/_auth/reset-password": { - id: "/_auth/reset-password" - path: "/reset-password" - fullPath: "/reset-password" + '/_auth/reset-password': { + id: '/_auth/reset-password' + path: '/reset-password' + fullPath: '/reset-password' preLoaderRoute: typeof AuthResetPasswordRouteImport parentRoute: typeof AuthRoute } - "/_auth/register": { - id: "/_auth/register" - path: "/register" - fullPath: "/register" + '/_auth/register': { + id: '/_auth/register' + path: '/register' + fullPath: '/register' preLoaderRoute: typeof AuthRegisterRouteImport parentRoute: typeof AuthRoute } - "/_auth/login": { - id: "/_auth/login" - path: "/login" - fullPath: "/login" + '/_auth/login': { + id: '/_auth/login' + path: '/login' + fullPath: '/login' preLoaderRoute: typeof AuthLoginRouteImport parentRoute: typeof AuthRoute } - "/_auth/forgot-password": { - id: "/_auth/forgot-password" - path: "/forgot-password" - fullPath: "/forgot-password" + '/_auth/forgot-password': { + id: '/_auth/forgot-password' + path: '/forgot-password' + fullPath: '/forgot-password' preLoaderRoute: typeof AuthForgotPasswordRouteImport parentRoute: typeof AuthRoute } - "/_auth/email-verified": { - id: "/_auth/email-verified" - path: "/email-verified" - fullPath: "/email-verified" + '/_auth/email-verified': { + id: '/_auth/email-verified' + path: '/email-verified' + fullPath: '/email-verified' preLoaderRoute: typeof AuthEmailVerifiedRouteImport parentRoute: typeof AuthRoute } - "/dashboard/subscription/": { - id: "/dashboard/subscription/" - path: "/subscription" - fullPath: "/dashboard/subscription/" + '/dashboard/subscription/': { + id: '/dashboard/subscription/' + path: '/subscription' + fullPath: '/dashboard/subscription/' preLoaderRoute: typeof DashboardSubscriptionIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog/": { - id: "/dashboard/catalog/" - path: "/catalog" - fullPath: "/dashboard/catalog/" + '/dashboard/catalog/': { + id: '/dashboard/catalog/' + path: '/catalog' + fullPath: '/dashboard/catalog/' preLoaderRoute: typeof DashboardCatalogIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/admin/": { - id: "/dashboard/admin/" - path: "/admin" - fullPath: "/dashboard/admin/" + '/dashboard/admin/': { + id: '/dashboard/admin/' + path: '/admin' + fullPath: '/dashboard/admin/' preLoaderRoute: typeof DashboardAdminIndexRouteImport parentRoute: typeof DashboardRoute } - "/demo_/categories_/$categoryId": { - id: "/demo_/categories_/$categoryId" - path: "/demo/categories/$categoryId" - fullPath: "/demo/categories/$categoryId" + '/demo_/categories_/$categoryId': { + id: '/demo_/categories_/$categoryId' + path: '/demo/categories/$categoryId' + fullPath: '/demo/categories/$categoryId' preLoaderRoute: typeof DemoCategoriesCategoryIdRouteImport parentRoute: typeof rootRouteImport } - "/dashboard/admin/users": { - id: "/dashboard/admin/users" - path: "/admin/users" - fullPath: "/dashboard/admin/users" + '/dashboard/oem/$code': { + id: '/dashboard/oem/$code' + path: '/oem/$code' + fullPath: '/dashboard/oem/$code' + preLoaderRoute: typeof DashboardOemCodeRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/admin/users': { + id: '/dashboard/admin/users' + path: '/admin/users' + fullPath: '/dashboard/admin/users' preLoaderRoute: typeof DashboardAdminUsersRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/admin/referrals": { - id: "/dashboard/admin/referrals" - path: "/admin/referrals" - fullPath: "/dashboard/admin/referrals" + '/dashboard/admin/referrals': { + id: '/dashboard/admin/referrals' + path: '/admin/referrals' + fullPath: '/dashboard/admin/referrals' preLoaderRoute: typeof DashboardAdminReferralsRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/admin/copy-logs": { - id: "/dashboard/admin/copy-logs" - path: "/admin/copy-logs" - fullPath: "/dashboard/admin/copy-logs" + '/dashboard/admin/copy-logs': { + id: '/dashboard/admin/copy-logs' + path: '/admin/copy-logs' + fullPath: '/dashboard/admin/copy-logs' preLoaderRoute: typeof DashboardAdminCopyLogsRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/admin/analytics": { - id: "/dashboard/admin/analytics" - path: "/admin/analytics" - fullPath: "/dashboard/admin/analytics" + '/dashboard/admin/analytics': { + id: '/dashboard/admin/analytics' + path: '/admin/analytics' + fullPath: '/dashboard/admin/analytics' preLoaderRoute: typeof DashboardAdminAnalyticsRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/vehicles_/$id/": { - id: "/dashboard/vehicles_/$id/" - path: "/vehicles/$id" - fullPath: "/dashboard/vehicles/$id/" + '/dashboard/vehicles_/$id/': { + id: '/dashboard/vehicles_/$id/' + path: '/vehicles/$id' + fullPath: '/dashboard/vehicles/$id/' preLoaderRoute: typeof DashboardVehiclesIdIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/$brandName/": { - id: "/dashboard/catalog_/$brandName/" - path: "/catalog/$brandName" - fullPath: "/dashboard/catalog/$brandName/" + '/dashboard/catalog_/$brandName/': { + id: '/dashboard/catalog_/$brandName/' + path: '/catalog/$brandName' + fullPath: '/dashboard/catalog/$brandName/' preLoaderRoute: typeof DashboardCatalogBrandNameIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/pcat/$catalogId/": { - id: "/dashboard/catalog_/pcat/$catalogId/" - path: "/catalog/pcat/$catalogId" - fullPath: "/dashboard/catalog/pcat/$catalogId/" + '/dashboard/catalog_/pcat/$catalogId/': { + id: '/dashboard/catalog_/pcat/$catalogId/' + path: '/catalog/pcat/$catalogId' + fullPath: '/dashboard/catalog/pcat/$catalogId/' preLoaderRoute: typeof DashboardCatalogPcatCatalogIdIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/emex/$catalogCode/": { - id: "/dashboard/catalog_/emex/$catalogCode/" - path: "/catalog/emex/$catalogCode" - fullPath: "/dashboard/catalog/emex/$catalogCode/" + '/dashboard/catalog_/emex/$catalogCode/': { + id: '/dashboard/catalog_/emex/$catalogCode/' + path: '/catalog/emex/$catalogCode' + fullPath: '/dashboard/catalog/emex/$catalogCode/' preLoaderRoute: typeof DashboardCatalogEmexCatalogCodeIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/$brandName_/$modelId/": { - id: "/dashboard/catalog_/$brandName_/$modelId/" - path: "/catalog/$brandName/$modelId" - fullPath: "/dashboard/catalog/$brandName/$modelId/" + '/dashboard/catalog_/$brandName_/$modelId/': { + id: '/dashboard/catalog_/$brandName_/$modelId/' + path: '/catalog/$brandName/$modelId' + fullPath: '/dashboard/catalog/$brandName/$modelId/' preLoaderRoute: typeof DashboardCatalogBrandNameModelIdIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/vehicles_/$id/categories_/$categoryId": { - id: "/dashboard/vehicles_/$id/categories_/$categoryId" - path: "/vehicles/$id/categories/$categoryId" - fullPath: "/dashboard/vehicles/$id/categories/$categoryId" + '/dashboard/vehicles_/$id/categories_/$categoryId': { + id: '/dashboard/vehicles_/$id/categories_/$categoryId' + path: '/vehicles/$id/categories/$categoryId' + fullPath: '/dashboard/vehicles/$id/categories/$categoryId' preLoaderRoute: typeof DashboardVehiclesIdCategoriesCategoryIdRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/pcat/$catalogId_/$modelId/": { - id: "/dashboard/catalog_/pcat/$catalogId_/$modelId/" - path: "/catalog/pcat/$catalogId/$modelId" - fullPath: "/dashboard/catalog/pcat/$catalogId/$modelId/" + '/dashboard/catalog_/pcat/$catalogId_/$modelId/': { + id: '/dashboard/catalog_/pcat/$catalogId_/$modelId/' + path: '/catalog/pcat/$catalogId/$modelId' + fullPath: '/dashboard/catalog/pcat/$catalogId/$modelId/' preLoaderRoute: typeof DashboardCatalogPcatCatalogIdModelIdIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/emex/$catalogCode_/$vehicleId/": { - id: "/dashboard/catalog_/emex/$catalogCode_/$vehicleId/" - path: "/catalog/emex/$catalogCode/$vehicleId" - fullPath: "/dashboard/catalog/emex/$catalogCode/$vehicleId/" + '/dashboard/catalog_/emex/$catalogCode_/$vehicleId/': { + id: '/dashboard/catalog_/emex/$catalogCode_/$vehicleId/' + path: '/catalog/emex/$catalogCode/$vehicleId' + fullPath: '/dashboard/catalog/emex/$catalogCode/$vehicleId/' preLoaderRoute: typeof DashboardCatalogEmexCatalogCodeVehicleIdIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId": { - id: "/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId" - path: "/catalog/$brandName/$modelId/categories/$categoryId" - fullPath: "/dashboard/catalog/$brandName/$modelId/categories/$categoryId" + '/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId': { + id: '/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId' + path: '/catalog/$brandName/$modelId/categories/$categoryId' + fullPath: '/dashboard/catalog/$brandName/$modelId/categories/$categoryId' preLoaderRoute: typeof DashboardCatalogBrandNameModelIdCategoriesCategoryIdRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/": { - id: "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/" - path: "/catalog/pcat/$catalogId/$modelId/$carId" - fullPath: "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/" + '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/': { + id: '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId/' + path: '/catalog/pcat/$catalogId/$modelId/$carId' + fullPath: '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/' preLoaderRoute: typeof DashboardCatalogPcatCatalogIdModelIdCarIdIndexRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId": { - id: "/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId" - path: "/catalog/emex/$catalogCode/$vehicleId/groups/$groupId" - fullPath: "/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId" + '/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId': { + id: '/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId' + path: '/catalog/emex/$catalogCode/$vehicleId/groups/$groupId' + fullPath: '/dashboard/catalog/emex/$catalogCode/$vehicleId/groups/$groupId' preLoaderRoute: typeof DashboardCatalogEmexCatalogCodeVehicleIdGroupsGroupIdRouteImport parentRoute: typeof DashboardRoute } - "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId": { - id: "/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId" - path: "/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId" - fullPath: "/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId" + '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId': { + id: '/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId' + path: '/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId' + fullPath: '/dashboard/catalog/pcat/$catalogId/$modelId/$carId/groups/$groupId' preLoaderRoute: typeof DashboardCatalogPcatCatalogIdModelIdCarIdGroupsGroupIdRouteImport parentRoute: typeof DashboardRoute } @@ -921,6 +940,7 @@ interface DashboardRouteChildren { DashboardAdminCopyLogsRoute: typeof DashboardAdminCopyLogsRoute DashboardAdminReferralsRoute: typeof DashboardAdminReferralsRoute DashboardAdminUsersRoute: typeof DashboardAdminUsersRoute + DashboardOemCodeRoute: typeof DashboardOemCodeRoute DashboardAdminIndexRoute: typeof DashboardAdminIndexRoute DashboardCatalogIndexRoute: typeof DashboardCatalogIndexRoute DashboardSubscriptionIndexRoute: typeof DashboardSubscriptionIndexRoute @@ -950,6 +970,7 @@ const DashboardRouteChildren: DashboardRouteChildren = { DashboardAdminCopyLogsRoute: DashboardAdminCopyLogsRoute, DashboardAdminReferralsRoute: DashboardAdminReferralsRoute, DashboardAdminUsersRoute: DashboardAdminUsersRoute, + DashboardOemCodeRoute: DashboardOemCodeRoute, DashboardAdminIndexRoute: DashboardAdminIndexRoute, DashboardCatalogIndexRoute: DashboardCatalogIndexRoute, DashboardSubscriptionIndexRoute: DashboardSubscriptionIndexRoute, diff --git a/apps/web/src/routes/dashboard/oem.$code.tsx b/apps/web/src/routes/dashboard/oem.$code.tsx new file mode 100644 index 0000000..de537cf --- /dev/null +++ b/apps/web/src/routes/dashboard/oem.$code.tsx @@ -0,0 +1,261 @@ +import { api } from "@/lib/api-client"; +import { capture } from "@/lib/posthog"; +import { Badge, Button, Skeleton } from "@sase/ui"; +import { useQuery } from "@tanstack/react-query"; +import { Link, createFileRoute } from "@tanstack/react-router"; +import { AlertCircle, ArrowLeft, Check, Copy, ImageOff, PackageSearch } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +// ─── Types (mirror apps/api TecdocOemResult) ──────────────────────────────── + +interface OeNumber { + brand: string; + code: string; +} +interface TecdocArticle { + id: string; + brand: string; + articleNumber: string; + name: string | null; + spareInfo: string | null; + images: Array<{ url: string; thumb: string | null }>; + eans: string[]; + oeNumbers: OeNumber[]; + compatible: Array<{ brand: string; article: string }>; +} +interface TecdocOemResult { + query: string; + queryNorm: string; + matched: boolean; + articles: TecdocArticle[]; + aftermarketParts: Array<{ brand: string; articleNumber: string; thumb: string | null }>; + oeCrossReferences: OeNumber[]; + truncated: boolean; +} + +// ─── Reusable copy-to-clipboard chip ──────────────────────────────────────── + +function CopyCode({ code, className }: { code: string; className?: string }) { + const [copied, setCopied] = useState(false); + const timer = useRef(null); + useEffect( + () => () => { + if (timer.current) window.clearTimeout(timer.current); + }, + [], + ); + return ( + + ); +} + +// ─── Image with graceful fallback ─────────────────────────────────────────── + +function PartThumb({ src, alt }: { src: string | null; alt: string }) { + const [failed, setFailed] = useState(false); + if (!src || failed) { + return ( +
+ +
+ ); + } + return ( + {alt} setFailed(true)} + className="size-16 shrink-0 rounded-lg border border-border bg-white object-contain" + /> + ); +} + +// ─── Route ─────────────────────────────────────────────────────────────────── + +export const Route = createFileRoute("/dashboard/oem/$code")({ + component: OemDetailPage, +}); + +function OemDetailPage() { + const { code } = Route.useParams(); + + const { data, isLoading, error } = useQuery({ + queryKey: ["tecdoc-oem", code], + queryFn: () => api.get(`/tecdoc/oem?code=${encodeURIComponent(code)}`), + }); + + useEffect(() => { + if (data) { + capture("oem_detail_viewed", { + oem_code: code, + matched: data.matched, + article_count: data.articles.length, + aftermarket_count: data.aftermarketParts.length, + oe_xref_count: data.oeCrossReferences.length, + }); + } + }, [data, code]); + + return ( +
+ {/* ─── Header ─────────────────────────────────────────────────────── */} +
+ +
+

+ OEM kodu +

+
+

{code}

+ +
+

+ TecDoc kataloğundan uyumlu parça kodları ve muadiller +

+
+
+ + {/* ─── Loading ────────────────────────────────────────────────────── */} + {isLoading && ( +
+ + + +
+ )} + + {/* ─── Error ──────────────────────────────────────────────────────── */} + {error && ( +
+ +

+ Uyumlu parça bilgisi yüklenemedi. Lütfen tekrar deneyin. +

+
+ )} + + {/* ─── Empty state (no TecDoc match) ──────────────────────────────── */} + {data && !data.matched && ( +
+
+ +
+

Bu OEM kodu için TecDoc karşılığı bulunamadı

+

+ Bağlantı parçaları, klipsler ve bazı orijinal kodlar TecDoc kapsamında olmayabilir. + Katalog büyüdükçe eşleşme oranı artar. +

+
+ )} + + {/* ─── Results ────────────────────────────────────────────────────── */} + {data?.matched && ( +
+ {/* Summary */} +
+ {data.articles.length} eşleşen parça + {data.aftermarketParts.length} yan sanayi numarası + {data.oeCrossReferences.length} muadil OE kodu + {data.truncated && ( + + liste kısaltıldı + + )} +
+ + {/* Matched TecDoc articles (highest-confidence: carry this OEM directly) */} +
+

Bu OEM'i taşıyan parçalar

+
+ {data.articles.map((a) => ( +
+ +
+

{a.brand}

+ + {a.name && a.name !== a.articleNumber && ( +

{a.name}

+ )} + {a.eans.length > 0 && ( +

+ EAN: {a.eans[0]} +

+ )} +
+
+ ))} +
+
+ + {/* Aftermarket equivalents (buyable substitutes across supplier brands) */} + {data.aftermarketParts.length > 0 && ( +
+

Yan sanayi muadilleri

+
+
- {t(`billing.methodLabels.${payment.method}`)} + {payment.method === "stripe" + ? t("billing.methodLabels.stripe") + : payment.method.toUpperCase()} @@ -329,19 +332,7 @@ function BillingPage() { - {payment.eftReceiptUrl ? ( - - ) : payment.hasStripeReceipt ? ( + {payment.hasStripeReceipt ? ( + + + + + + ); +} diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 1b3259b..04ff442 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -413,6 +413,11 @@ "cta": "Upgrade Plan", "progressLabel": "{percent}% of trial used" }, + "valueUpsell": { + "title": "Ready to subscribe?", + "description": "You're actively using the platform. Subscribe to keep querying customer vehicles and accessing OEM parts without interruption.", + "cta": "Subscribe" + }, "trialProgress": { "title": "{days} days left in your Full Package trial", "description": "Pick a plan now to keep your access without interruption.", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index a9a0b57..29e9b82 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -413,6 +413,11 @@ "cta": "Plana Yükselt", "progressLabel": "Denemenin %{percent}'i kullanıldı" }, + "valueUpsell": { + "title": "Aboneliğe geçmeye hazır mısın?", + "description": "Platformu aktif kullanıyorsun. Müşteri araçlarını sınırsız sorgulamaya ve OEM parça erişimine kesintisiz devam etmek için aboneliğe geç.", + "cta": "Aboneliğe geç" + }, "trialProgress": { "title": "Full Paket denemende {days} gün kaldı", "description": "Kesintisiz devam etmek için planını şimdi seç.", diff --git a/apps/web/src/routes/dashboard.tsx b/apps/web/src/routes/dashboard.tsx index 31335f8..eab65e0 100644 --- a/apps/web/src/routes/dashboard.tsx +++ b/apps/web/src/routes/dashboard.tsx @@ -1,6 +1,7 @@ import { LanguageSwitcher } from "@/components/language-switcher"; import { SiteFooter } from "@/components/site-footer"; import { TrialUrgencyBanner } from "@/components/trial-urgency-banner"; +import { TrialValueUpsell } from "@/components/trial-value-upsell"; import { useAuth } from "@/hooks/use-auth"; import { api } from "@/lib/api-client"; import { useTranslation } from "@/lib/i18n"; @@ -509,6 +510,7 @@ function DashboardLayout() { {/* Page Content */}
+
From 5e1675f95a0bab9c81260298b4b8664342120ae1 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 20:44:58 +0300 Subject: [PATCH 06/35] fix(web): replace unbuilt feature claims with real capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The landing OEM card, the manual-vs-Sase comparison table, and a blog post advertised "price comparison" / "order history & part tracking" — features that aren't built. Replaced with real capabilities (exploded-diagram matching, interactive part diagrams, brand catalogs) in both tr + en. Mirrors the same accuracy fix already shipped to the Novu lifecycle emails. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/messages/en.json | 8 ++++---- apps/web/src/messages/tr.json | 8 ++++---- apps/web/src/routes/blog_/$slug.tsx | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 04ff442..92edc54 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -873,7 +873,7 @@ "description": "Cross-references multiple catalogs so OEM codes are always current and correct — no more wrong parts, no more returns.", "bullet1": "Multi-catalog cross-referencing", "bullet2": "Always-current OEM codes", - "bullet3": "Price comparison", + "bullet3": "Matched via exploded diagrams", "mockupTitle": "OEM Catalog — Parts List", "part1": "AC Compressor", "part2": "Heater Blower Motor", @@ -902,9 +902,9 @@ "sase": "Multi-catalog cross-query" }, "3": { - "label": "Price comparison", - "manual": "Impossible", - "sase": "On a single screen" + "label": "Interactive part diagrams", + "manual": "Paper catalog", + "sase": "Clickable diagrams" }, "4": { "label": "Monthly time saved", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index 29e9b82..e3adf52 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -873,7 +873,7 @@ "description": "Birden fazla katalogda çapraz sorgulama yaparak her zaman en güncel ve doğru OEM kodlarını sunar — yanlış parça, iade derdi tarih olur.", "bullet1": "Çoklu katalog çapraz sorgulama", "bullet2": "Her zaman güncel OEM kodları", - "bullet3": "Fiyat karşılaştırma", + "bullet3": "Patlamış parça şemasıyla eşleştirme", "mockupTitle": "OEM Katalog — Parça Listesi", "part1": "Klima Kompresörü", "part2": "Kalorifer Motoru", @@ -902,9 +902,9 @@ "sase": "Çoklu katalog çapraz sorgu" }, "3": { - "label": "Fiyat karşılaştırma", - "manual": "İmkansız", - "sase": "Tek ekranda" + "label": "İnteraktif parça şeması", + "manual": "Kağıt katalog", + "sase": "Tıklanabilir diyagram" }, "4": { "label": "Aylık zaman tasarrufu", diff --git a/apps/web/src/routes/blog_/$slug.tsx b/apps/web/src/routes/blog_/$slug.tsx index 4f43e4a..dad9088 100644 --- a/apps/web/src/routes/blog_/$slug.tsx +++ b/apps/web/src/routes/blog_/$slug.tsx @@ -226,8 +226,8 @@ const POSTS: Record = {
  • VIN bazlı anlık araç tanımlama — saniyeler içinde doğru araç tespiti
  • Çoklu katalog çapraz sorgulama — tek arayüzde birden fazla kaynak
  • Otomatik parça eşleştirme — insan hatasını minimuma indirir
  • -
  • Gerçek zamanlı fiyat karşılaştırma
  • -
  • Sipariş geçmişi ve parça takibi
  • +
  • İnteraktif teknik şemalar — parçayı görsel bul
  • +
  • 27 marka orijinal parça kataloğu — tek çatı altında
  • Sektörde Sayısal Dönüşüm

    From fb2c28faf165b8a0f42132c5d9a5f9f5137e4c82 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 20:52:51 +0300 Subject: [PATCH 07/35] feat(tecdoc): OEM detail page with TecDoc cross-references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve a catalog OEM code to its TecDoc equivalents on a new /dashboard/oem/$code page: the aftermarket parts that carry it (brand + article number + image + EAN), buyable supplier substitutes, and OE cross-references (same part under other makes). - API: TecdocModule (read-only postgres-js client to the imported `td` snapshot), GET /tecdoc/oem?code=. Normalisation-based match (TecDoc stores `1J0 973 702`, catalog gives `1J0973702`); exact match recovers ~1/10 vs normalised ~5/10 on real codes. Self- disables without TECDOC_DB_* env → { matched: false }. - Web: OEM code in the parts panel is now a link (new tab) to the detail page; "N/A" stays plain text. - Mirrors CatalogSourceDbModule (raw queries, no Drizzle modelling). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/app.module.ts | 2 + apps/api/src/config/configuration.ts | 8 + .../tecdoc/tecdoc-source-db.service.ts | 220 ++++ .../integrations/tecdoc/tecdoc.controller.ts | 17 + .../src/integrations/tecdoc/tecdoc.module.ts | 15 + .../src/integrations/tecdoc/tecdoc.types.ts | 50 + .../web/src/components/schema/parts-panel.tsx | 29 +- apps/web/src/routeTree.gen.ts | 1157 +++++++++-------- apps/web/src/routes/dashboard/oem.$code.tsx | 261 ++++ docker-compose.coolify.yml | 4 + 10 files changed, 1193 insertions(+), 570 deletions(-) create mode 100644 apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts create mode 100644 apps/api/src/integrations/tecdoc/tecdoc.controller.ts create mode 100644 apps/api/src/integrations/tecdoc/tecdoc.module.ts create mode 100644 apps/api/src/integrations/tecdoc/tecdoc.types.ts create mode 100644 apps/web/src/routes/dashboard/oem.$code.tsx diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index c3e0289..78bdb68 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -29,6 +29,7 @@ import { DemoModule } from "./demo/demo.module"; import { EmailModule } from "./email/email.module"; import { HealthController } from "./health.controller"; import { EmexModule } from "./integrations/emex/emex.module"; +import { TecdocModule } from "./integrations/tecdoc/tecdoc.module"; import { InternalAdminModule } from "./internal-admin/internal-admin.module"; import { JobsModule } from "./jobs/jobs.module"; import { MetaCapiModule } from "./meta-capi/meta-capi.module"; @@ -86,6 +87,7 @@ import { VehiclesModule } from "./vehicles/vehicles.module"; CategoriesModule, DemoModule, PartsModule, + TecdocModule, JobsModule, EmexModule, TranslationsModule, diff --git a/apps/api/src/config/configuration.ts b/apps/api/src/config/configuration.ts index 534616f..2e9b377 100644 --- a/apps/api/src/config/configuration.ts +++ b/apps/api/src/config/configuration.ts @@ -88,6 +88,14 @@ export default () => ({ .map((s) => s.trim()) .filter(Boolean), }, + tecdoc: { + // Read-only lookup against the imported TecDoc snapshot (db `td`). When + // enabled + url set, the OEM detail page resolves a part's OEM code to + // TecDoc aftermarket equivalents + OE cross-references. Disabled → endpoint + // returns { matched: false } and the UI shows an empty state. + enabled: process.env.TECDOC_DB_ENABLED === "true", + url: process.env.TECDOC_DB_URL, + }, otel: { enabled: process.env.OTEL_ENABLED === "true", endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, diff --git a/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts b/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts new file mode 100644 index 0000000..e183e72 --- /dev/null +++ b/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts @@ -0,0 +1,220 @@ +import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import postgres, { type Sql } from "postgres"; +import type { + TecdocArticle, + TecdocCompatible, + TecdocOeNumber, + TecdocOemResult, +} from "./tecdoc.types"; + +/** + * Read-only lookup against the imported TecDoc snapshot (db `td` — a selective + * copy of articles + OE numbers + aftermarket compatibility + images/eans; the + * 29 GB vehicle-fitment table is intentionally excluded). Given an OEM code from + * the sase catalog, returns the TecDoc articles that carry it as an OE number, + * with their aftermarket equivalents and OE cross-references. + * + * Matching is normalisation-based, not exact: TecDoc stores OE codes with + * spaces/dashes (`1J0 973 702`) while the catalog gives `1J0973702`, so both + * sides are reduced to `[A-Z0-9]` uppercase before comparison (a precomputed + * `code_norm` column, indexed, holds the TecDoc side). Exact matching recovers + * almost nothing — verified ~1/10 vs normalised ~5/10 on real catalog codes. + * + * Never throws: disabled feature, too-short code, connection blip or no match + * all collapse to `matched: false` so the UI has a single empty-state path. + */ +@Injectable() +export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(TecdocSourceDbService.name); + private sql: Sql | null = null; + private enabled = false; + + // Short normalised codes (e.g. "NA" from "N/A", single digits) collide across + // unrelated parts — refuse to match below this length. Real OE numbers are 5+. + private static readonly MIN_NORM_LEN = 5; + private static readonly MAX_ARTICLES = 60; + private static readonly MAX_AGG = 300; + + constructor(private readonly config: ConfigService) {} + + onModuleInit() { + const enabled = this.config.get("tecdoc.enabled"); + const url = this.config.get("tecdoc.url"); + if (!enabled || !url) { + this.logger.log(`[tecdoc] disabled (enabled=${enabled}, urlSet=${Boolean(url)})`); + return; + } + this.sql = postgres(url, { + max: 5, + idle_timeout: 30, + connect_timeout: 10, + prepare: false, + }); + this.enabled = true; + this.logger.log("[tecdoc] connected, OEM cross-reference lookup enabled"); + } + + async onModuleDestroy() { + if (this.sql) { + await this.sql.end({ timeout: 5 }); + this.sql = null; + } + } + + /** `1J0 973 702` / `1j0-973-702` → `1J0973702`. Used for both the input code + * and JS-side dedupe; the TecDoc side is matched against the stored + * `code_norm` (built with the identical rule at import time). */ + private static norm(code: string): string { + return code.toUpperCase().replace(/[^A-Z0-9]/g, ""); + } + + async lookupByOem(rawCode: string): Promise { + const query = (rawCode ?? "").trim(); + const queryNorm = TecdocSourceDbService.norm(query); + const miss: TecdocOemResult = { + query, + queryNorm, + matched: false, + articles: [], + aftermarketParts: [], + oeCrossReferences: [], + truncated: false, + }; + + if (!this.enabled || !this.sql) return miss; + if (queryNorm.length < TecdocSourceDbService.MIN_NORM_LEN) return miss; + + try { + const rows = await this.sql< + Array<{ + id: string; + brand: string; + article_number: string; + name: string | null; + spare_info: string | null; + oe_numbers: TecdocOeNumber[]; + compatible: TecdocCompatible[]; + images: Array<{ url: string; thumb: string | null }>; + eans: string[]; + }> + >` + WITH hit AS ( + SELECT DISTINCT article_id + FROM article_oe_numbers + WHERE code_norm = ${queryNorm} + LIMIT ${TecdocSourceDbService.MAX_ARTICLES} + ) + SELECT + a.id::text AS id, + b.name AS brand, + a.article_number AS article_number, + a.name AS name, + a.spare_info AS spare_info, + COALESCE(( + SELECT json_agg(json_build_object('brand', o.brand, 'code', o.code)) + FROM ( + SELECT DISTINCT brand, code FROM article_oe_numbers + WHERE article_id = a.id ORDER BY brand LIMIT 200 + ) o + ), '[]') AS oe_numbers, + COALESCE(( + SELECT json_agg(json_build_object('brand', c.compatible_brand, 'article', c.compatible_article)) + FROM ( + SELECT DISTINCT compatible_brand, compatible_article FROM article_compatibility + WHERE article_id = a.id ORDER BY compatible_brand LIMIT 200 + ) c + ), '[]') AS compatible, + COALESCE(( + SELECT json_agg(json_build_object('url', i.image_url, 'thumb', i.thumb_url) ORDER BY i.sort_order) + FROM ( + SELECT image_url, thumb_url, sort_order FROM article_images + WHERE article_id = a.id ORDER BY sort_order LIMIT 8 + ) i + ), '[]') AS images, + COALESCE(( + SELECT json_agg(e.ean) FROM ( + SELECT DISTINCT ean FROM article_ean_numbers WHERE article_id = a.id LIMIT 20 + ) e + ), '[]') AS eans + FROM hit + JOIN articles a ON a.id = hit.article_id + JOIN article_brands b ON b.id = a.brand_id + ORDER BY b.name, a.article_number + `; + + if (rows.length === 0) return miss; + + let truncated = rows.length >= TecdocSourceDbService.MAX_ARTICLES; + + const articles: TecdocArticle[] = rows.map((r) => { + if (r.oe_numbers.length >= 200 || r.compatible.length >= 200) truncated = true; + return { + id: r.id, + brand: r.brand, + articleNumber: r.article_number, + name: r.name, + spareInfo: r.spare_info, + images: r.images, + eans: r.eans, + oeNumbers: r.oe_numbers, + compatible: r.compatible, + }; + }); + + // ── Aggregate: buyable aftermarket part numbers ────────────────────── + // The matched articles are themselves aftermarket parts; their + // compatibility rows add equivalent numbers from other supplier brands. + const afterSeen = new Set(); + const aftermarketParts: TecdocOemResult["aftermarketParts"] = []; + const pushAfter = (brand: string, articleNumber: string, thumb: string | null) => { + const key = `${brand.toUpperCase().trim()}␟${TecdocSourceDbService.norm(articleNumber)}`; + if (afterSeen.has(key) || !articleNumber.trim()) return; + afterSeen.add(key); + if (aftermarketParts.length < TecdocSourceDbService.MAX_AGG) { + aftermarketParts.push({ brand, articleNumber, thumb }); + } else { + truncated = true; + } + }; + for (const a of articles) { + pushAfter(a.brand, a.articleNumber, a.images[0]?.thumb ?? a.images[0]?.url ?? null); + } + for (const a of articles) { + for (const c of a.compatible) pushAfter(c.brand, c.article, null); + } + + // ── Aggregate: OE cross-references (same part, other makes) ─────────── + // Exclude restatements of the queried code itself (same normalised code). + const oeSeen = new Set(); + const oeCrossReferences: TecdocOeNumber[] = []; + for (const a of articles) { + for (const oe of a.oeNumbers) { + const codeNorm = TecdocSourceDbService.norm(oe.code); + if (codeNorm === queryNorm) continue; + const key = `${oe.brand.toUpperCase().trim()}␟${codeNorm}`; + if (oeSeen.has(key)) continue; + oeSeen.add(key); + if (oeCrossReferences.length < TecdocSourceDbService.MAX_AGG) { + oeCrossReferences.push(oe); + } else { + truncated = true; + } + } + } + + return { + query, + queryNorm, + matched: true, + articles, + aftermarketParts, + oeCrossReferences, + truncated, + }; + } catch (err) { + this.logger.warn(`[tecdoc] lookup failed (oem=${query}): ${(err as Error).message}`); + return miss; + } + } +} diff --git a/apps/api/src/integrations/tecdoc/tecdoc.controller.ts b/apps/api/src/integrations/tecdoc/tecdoc.controller.ts new file mode 100644 index 0000000..077f6a7 --- /dev/null +++ b/apps/api/src/integrations/tecdoc/tecdoc.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, Query } from "@nestjs/common"; +import { TecdocSourceDbService } from "./tecdoc-source-db.service"; + +@Controller("tecdoc") +export class TecdocController { + constructor(private readonly tecdoc: TecdocSourceDbService) {} + + /** + * Resolve an OEM code from the catalog to its TecDoc equivalents. + * `GET /tecdoc/oem?code=1J0973702` → { matched, articles, aftermarketParts, + * oeCrossReferences }. Always 200 with `matched: false` on any miss. + */ + @Get("oem") + async oem(@Query("code") code: string) { + return this.tecdoc.lookupByOem(code ?? ""); + } +} diff --git a/apps/api/src/integrations/tecdoc/tecdoc.module.ts b/apps/api/src/integrations/tecdoc/tecdoc.module.ts new file mode 100644 index 0000000..11ff55f --- /dev/null +++ b/apps/api/src/integrations/tecdoc/tecdoc.module.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common"; +import { TecdocSourceDbService } from "./tecdoc-source-db.service"; +import { TecdocController } from "./tecdoc.controller"; + +/** + * OEM cross-reference lookup against the imported TecDoc snapshot (db `td`). + * Raw read-only queries — intentionally no Drizzle schema modelling, mirroring + * CatalogSourceDbModule. Self-disables when TECDOC_DB_* env is unset. + */ +@Module({ + controllers: [TecdocController], + providers: [TecdocSourceDbService], + exports: [TecdocSourceDbService], +}) +export class TecdocModule {} diff --git a/apps/api/src/integrations/tecdoc/tecdoc.types.ts b/apps/api/src/integrations/tecdoc/tecdoc.types.ts new file mode 100644 index 0000000..72177ed --- /dev/null +++ b/apps/api/src/integrations/tecdoc/tecdoc.types.ts @@ -0,0 +1,50 @@ +/** An OE (original-equipment) number cross-reference: the same physical part as + * catalogued by a vehicle manufacturer (e.g. VAG `1J0 973 702`). */ +export interface TecdocOeNumber { + brand: string; + code: string; +} + +/** An aftermarket equivalent: a buyable part number from a supplier brand + * (e.g. FEBI BILSTEIN `171903`). */ +export interface TecdocCompatible { + brand: string; + article: string; +} + +export interface TecdocImage { + url: string; + thumb: string | null; +} + +/** One TecDoc article whose OE number list contains the queried OEM code. */ +export interface TecdocArticle { + id: string; + brand: string; + articleNumber: string; + name: string | null; + spareInfo: string | null; + images: TecdocImage[]; + eans: string[]; + oeNumbers: TecdocOeNumber[]; + compatible: TecdocCompatible[]; +} + +/** Response of the OEM detail lookup. `matched: false` covers every miss — + * feature disabled, code too short, or no TecDoc article carries that OE + * number — so the UI has a single empty-state path. */ +export interface TecdocOemResult { + query: string; + queryNorm: string; + matched: boolean; + /** Distinct articles whose OE list contains the queried code. */ + articles: TecdocArticle[]; + /** Deduped buyable aftermarket part numbers across all matched articles + * (the matched articles themselves + their compatibility entries). */ + aftermarketParts: Array<{ brand: string; articleNumber: string; thumb: string | null }>; + /** Deduped OE cross-references across all matched articles, excluding the + * queried code itself — i.e. the same part's numbers under other makes. */ + oeCrossReferences: TecdocOeNumber[]; + /** True when any per-article list or the article set hit its cap. */ + truncated: boolean; +} diff --git a/apps/web/src/components/schema/parts-panel.tsx b/apps/web/src/components/schema/parts-panel.tsx index 368a65b..cd663a1 100644 --- a/apps/web/src/components/schema/parts-panel.tsx +++ b/apps/web/src/components/schema/parts-panel.tsx @@ -352,7 +352,7 @@ export function PartsPanel({
    - {part.oemCode && ( + {part.oemCode && part.oemCode !== "N/A" && ( {part.quantity}
    + + {data.aftermarketParts.map((p) => ( + + + + + ))} + +
    {p.brand} + +
    + + + )} + + {/* OE cross-references (same physical part, other vehicle makes) */} + {data.oeCrossReferences.length > 0 && ( +
    +

    Muadil orijinal (OE) kodları

    +
    + {data.oeCrossReferences.map((oe) => ( + + {oe.brand} + + + ))} +
    +
    + )} + + )} + + ); +} diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index a5417c1..6cf5742 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -100,6 +100,10 @@ services: # memory: catalog-wide bridge had 7-114x noise; only add a catalog once # its per-vehicle bridge is wired & OEM-verified. - EMEX_SOURCE_DB_ALLOWED_CATALOGS=${EMEX_SOURCE_DB_ALLOWED_CATALOGS:-} + # TecDoc snapshot lookup (db `td`) backing the OEM detail page. Off until + # both vars are set in Coolify; see sase-prod-db-api-access memory. + - TECDOC_DB_ENABLED=${TECDOC_DB_ENABLED:-false} + - TECDOC_DB_URL=${TECDOC_DB_URL:-} depends_on: sase-redis: condition: service_healthy From eae373c553774b8898985ceadd01431b80edc857 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 21:00:08 +0300 Subject: [PATCH 08/35] =?UTF-8?q?fix(web):=20accuracy=20sweep=20=E2=80=94?= =?UTF-8?q?=20drop=20unbuilt-feature=20claims,=20VIN=E2=86=92=C5=9Fase,=20?= =?UTF-8?q?B2B=20framing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full marketing-copy audit. Removes price-comparison claims (pricing step, hero variant B, blog tip) that aren't built; normalises VIN→şase across TR copy (plan features, e-commerce section, FAQ, blog, about) — keeping VIN only in the educational "VIN nedir" post and the EN file; de-consumerises framing (FAQ "aracınız", about "bireysel kullanıcılar"). Fabricated metrics/testimonials left untouched pending review. Co-Authored-By: Claude Opus 4.8 --- apps/web/src/messages/en.json | 6 +++--- apps/web/src/messages/tr.json | 28 ++++++++++++++-------------- apps/web/src/routes/about.tsx | 14 +++++++------- apps/web/src/routes/blog_/$slug.tsx | 26 +++++++++++++------------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 92edc54..0fca206 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -667,7 +667,7 @@ "s2t": "See the parts", "s2d": "OEM codes, diagrams and compatible parts appear in seconds.", "s3t": "Pick the right part", - "s3d": "Compare prices and find the right part in one go — no wrong orders." + "s3d": "Copy the right OEM code and order the correct part from your supplier in one go — no wrong orders." }, "faq": { "title": "Frequently asked questions", @@ -803,7 +803,7 @@ "subtitleA": "Enter the customer vehicle's VIN and find the correct OEM part the first time with cross-catalog verification. No returns, no losses — trusted by 500+ businesses. Try it free.", "lossPillB": "Start looking up VINs free — no card required", "titleB": "Find the Customer's Correct Part in Seconds", - "subtitleB": "Enter the 17-digit VIN and reach the right OEM codes, part diagrams and prices in seconds. 27 brands, unlimited lookups. Start free — no credit card.", + "subtitleB": "Enter the 17-digit VIN and reach the right OEM codes, part diagrams in seconds. 27 brands, unlimited lookups. Start free — no credit card.", "vinPlaceholder": "Example: WVWZZZ1JZ3W597935", "searchCta": "Search", "decoding": "Decoding...", @@ -1165,7 +1165,7 @@ "items": { "0": { "q": "Which brands do you support?", - "a": "We currently support 27 automotive brands and the list keeps growing. Enter your VIN in the search box to see right away whether your vehicle is covered." + "a": "We currently support 27 automotive brands and the list keeps growing. Enter a VIN in the search box to see right away whether a vehicle is covered." }, "1": { "q": "Is my data safe?", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index e3adf52..eafe55f 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -324,7 +324,7 @@ "brandSearchPlaceholder": "Marka ara — ör. Volkswagen", "brandSearchNoMatch": "\"{query}\" ile eşleşen marka yok.", "features": { - "vinSearch": "Sınırsız VIN arama", + "vinSearch": "Sınırsız şase arama", "partsCatalog": "Parça kataloğu", "schemaViewer": "Şema görüntüleyici", "prioritySupport": "Öncelikli destek", @@ -663,11 +663,11 @@ "how": { "title": "Nasıl çalışır", "s1t": "Şase numarasını gir", - "s1d": "17 haneli VIN/şase numarasını yapıştır.", + "s1d": "17 haneli şase numarasını yapıştır.", "s2t": "Parçaları gör", "s2d": "Saniyeler içinde OEM kodları, şemalar ve uyumlu parçalar listelensin.", "s3t": "Doğru parçayı seç", - "s3d": "Fiyatları karşılaştır, doğru parçayı tek seferde bul — yanlış sipariş yok." + "s3d": "Doğru OEM kodunu kopyala, tedarikçine tek seferde doğru parçayı sipariş et — yanlış sipariş yok." }, "faq": { "title": "Sık sorulan sorular", @@ -775,7 +775,7 @@ "meta": { "title": "Şase Numarası Sorgulama & OEM Parça Kataloğu | Sase.tr", "description": "Araç şase numarasını (VIN) girin, saniyeler içinde OEM parça kodlarına ulaşın. 27 marka, 1M+ parça. Yanlış sipariş yapmadan doğru parçayı bulun.", - "schemaOrgDescription": "Türkiye'nin VIN/şase numarası sorgulama ve OEM yedek parça kataloğu platformu.", + "schemaOrgDescription": "Türkiye'nin şase numarası sorgulama ve OEM yedek parça kataloğu platformu.", "schemaSoftwareDescription": "Araç şase numarası ile OEM yedek parça sorgulama platformu." }, "nav": { @@ -803,7 +803,7 @@ "subtitleA": "Müşteri aracının şasesini girin; çapraz katalog doğrulamasıyla doğru OEM parçasını ilk seferde bulun. İade yok, kayıp yok — 500+ işletme güveniyor. Ücretsiz deneyin.", "lossPillB": "Şase sorgulamaya ücretsiz başlayın — kart gerekmez", "titleB": "Müşteri Aracının Doğru Parçasını Saniyede Bulun", - "subtitleB": "17 haneli şaseyi girin; doğru OEM kodlarına, parça şemalarına ve fiyatlara saniyeler içinde ulaşın. 27 marka, sınırsız sorgu. Ücretsiz başlayın, kredi kartı istemiyoruz.", + "subtitleB": "17 haneli şaseyi girin; doğru OEM kodlarına, parça şemalarına saniyeler içinde ulaşın. 27 marka, sınırsız sorgu. Ücretsiz başlayın, kredi kartı istemiyoruz.", "vinPlaceholder": "Örnek: WVWZZZ1JZ3W597935", "searchCta": "Ara", "decoding": "Çözülüyor...", @@ -1052,14 +1052,14 @@ "ecommerce": { "badge": "E-Ticaret Entegrasyonu", "title": "Kendi Sitenize Şase Çözme Gücü Katın", - "subtitle": "Müşterileriniz yanlış parça sipariş edip iade mi açıyor? Sase.tr entegrasyonu ile VIN bazlı filtreleme ekleyin — iadeler düşsün, dönüşüm artsın.", + "subtitle": "Müşterileriniz yanlış parça sipariş edip iade mi açıyor? Sase.tr entegrasyonu ile şase bazlı filtreleme ekleyin — iadeler düşsün, dönüşüm artsın.", "frameTitle": "otoyedekparca.co — Sase.tr Entegrasyonu", "loading": "Yükleniyor…", "easyBadge": "Kolay Entegrasyon", "cardTitle": "Birkaç Satır Kod, Büyük Fark", - "cardBody": "E-ticaret sitenize Sase.tr'nin VIN çözme motorunu entegre edin. Müşterileriniz şase numarasını girsin — sadece araçlarına uygun parçalar listelensin.", + "cardBody": "E-ticaret sitenize Sase.tr'nin şase çözme motorunu entegre edin. Müşterileriniz şase numarasını girsin — sadece araçlarına uygun parçalar listelensin.", "bullet1": "API & Widget — Birkaç satır kodla sitenize entegre edin", - "bullet2": "VIN Bazlı Filtreleme — Sadece uyumlu parçalar görünsün", + "bullet2": "Şase Bazlı Filtreleme — Sadece uyumlu parçalar görünsün", "bullet3": "İade Oranını Düşürün — Doğru parça, ilk seferde", "bullet4": "White-Label — Widget sitenizin tasarımına uyum sağlar", "cta": "İletişime Geçin", @@ -1095,7 +1095,7 @@ "2": { "name": "Özge D.", "role": "E-Ticaret Yöneticisi", - "text": "Online mağazamıza VIN entegrasyonu ekledik. Müşteriler şase numarasını giriyor, sadece uyumlu parçalar listeleniyor. İade oranımız %42 düştü." + "text": "Online mağazamıza şase entegrasyonu ekledik. Müşteriler şase numarasını giriyor, sadece uyumlu parçalar listeleniyor. İade oranımız %42 düştü." }, "3": { "name": "Burak T.", @@ -1118,7 +1118,7 @@ "name": "1 Marka", "description": "Tek marka için erişim", "feature1": "1 marka seçimi", - "feature2": "Sınırsız VIN arama", + "feature2": "Sınırsız şase arama", "feature3": "Parça kataloğu", "feature4": "Şema görüntüleyici" }, @@ -1126,7 +1126,7 @@ "name": "2 Marka", "description": "İki farklı marka", "feature1": "2 marka seçimi", - "feature2": "Sınırsız VIN arama", + "feature2": "Sınırsız şase arama", "feature3": "Parça kataloğu", "feature4": "Öncelikli destek" }, @@ -1134,7 +1134,7 @@ "name": "3 Marka", "description": "Üç marka kapsamlı", "feature1": "3 marka seçimi", - "feature2": "Sınırsız VIN arama", + "feature2": "Sınırsız şase arama", "feature3": "Parça kataloğu", "feature4": "Öncelikli destek" } @@ -1149,7 +1149,7 @@ "ctaFineprint": "Kart bilgisi gerekmez. İstediğin zaman iptal et.", "included": "Pakete dahil", "feature1": "Tüm 27 markaya erişim", - "feature2": "Sınırsız VIN arama", + "feature2": "Sınırsız şase arama", "feature3": "OEM parça kataloğu", "feature4": "İnteraktif şema görüntüleyici", "feature5": "Geçmiş sorgular & favoriler", @@ -1165,7 +1165,7 @@ "items": { "0": { "q": "Hangi markaları destekliyorsunuz?", - "a": "Şu an 27 otomobil markası destekleniyor ve liste sürekli genişliyor. Şase numaranızı arama kutusuna girerek aracınızın desteklenip desteklenmediğini hemen görebilirsiniz." + "a": "Şu an 27 otomobil markası destekleniyor ve liste sürekli genişliyor. Şase numarasını arama kutusuna girerek bir aracın desteklenip desteklenmediğini hemen görebilirsiniz." }, "1": { "q": "Verilerim güvende mi?", diff --git a/apps/web/src/routes/about.tsx b/apps/web/src/routes/about.tsx index ed51358..5ee42c6 100644 --- a/apps/web/src/routes/about.tsx +++ b/apps/web/src/routes/about.tsx @@ -1,5 +1,5 @@ -import { usePageMeta } from "@/hooks/use-page-meta"; import { SiteHeader } from "@/components/site-header"; +import { usePageMeta } from "@/hooks/use-page-meta"; import { Button } from "@sase/ui"; import { Link, createFileRoute } from "@tanstack/react-router"; @@ -11,7 +11,7 @@ function AboutPage() { usePageMeta({ title: "Hakkımızda — Sase.tr | Türkiye'nin Şase Sorgulama Platformu", description: - "Sase.tr, Türkiye yedek parça sektörüne yönelik VIN/şase numarası sorgulama ve OEM parça kataloğu platformudur.", + "Sase.tr, Türkiye yedek parça sektörüne yönelik şase numarası sorgulama ve OEM parça kataloğu platformudur.", canonical: "https://sase.tr/about", }); @@ -25,21 +25,21 @@ function AboutPage() {

    Sase.tr, Türkiye'nin yedek parça sektörüne yönelik geliştirilen dijital bir platformdur. - Şase numarası (VIN) ile araç tanımlama, orijinal parça kataloğuna erişim ve interaktif - şema görüntüleme hizmetlerini tek bir çatı altında sunar. + Şase numarası ile araç tanımlama, orijinal parça kataloğuna erişim ve interaktif şema + görüntüleme hizmetlerini tek bir çatı altında sunar.

    Misyonumuz

    Yedek parça arama sürecini hızlandırmak, doğru parçaya ilk seferde ulaşmayı sağlamak ve - sektördeki bilgi asimetrisini ortadan kaldırmak. Oto yedek parçacılar, servisler ve - bireysel kullanıcılar için güvenilir bir referans noktası olmayı hedefliyoruz. + sektördeki bilgi asimetrisini ortadan kaldırmak. Oto yedek parçacılar ve servisler için + güvenilir bir referans noktası olmayı hedefliyoruz.

    Ne Yapıyoruz?

    • - Şase Çözme: VIN numarası ile aracın marka, model, yıl, motor tipi ve + Şase Çözme: Şase numarası ile aracın marka, model, yıl, motor tipi ve donanım bilgilerine anında erişim.
    • diff --git a/apps/web/src/routes/blog_/$slug.tsx b/apps/web/src/routes/blog_/$slug.tsx index dad9088..182feac 100644 --- a/apps/web/src/routes/blog_/$slug.tsx +++ b/apps/web/src/routes/blog_/$slug.tsx @@ -91,12 +91,12 @@ const POSTS: Record = {

      VIN'in en kritik kullanım alanlarından biri yedek parça aramadır. Aynı model arabada bile üretim yılı, motor tipi veya donanım paketine göre farklı parçalar kullanılmış olabilir. - VIN numarasıyla sorgulama yaparak yalnızca aracınıza tam uyumlu OEM parçalara ulaşabilir, + Şase numarasıyla sorgulama yaparak yalnızca araca tam uyumlu OEM parçalara ulaşabilir, yanlış parça sipariş etme riskini sıfıra indirebilirsiniz.

      - Sase.tr platformunda VIN numaranızı girerek saniyeler içinde aracınızın tam teknik + Sase.tr platformunda şase numarasını girerek saniyeler içinde aracın tam teknik bilgilerine ve orijinal parça kataloğuna erişebilirsiniz. 50'den fazla markayı destekleyen platformumuzla yanlış parça siparişlerine son verin.

      @@ -177,7 +177,7 @@ const POSTS: Record = {

      Doğru Parçayı Nasıl Bulursunuz?

      İster OEM ister muadil tercih edin, en önemli adım doğru OEM parça numarasını bilmektir. - Sase.tr ile araç VIN numaranızdan yola çıkarak orijinal parça kodlarına ulaşın. Bu kodu + Sase.tr ile araç şase numarasından yola çıkarak orijinal parça kodlarına ulaşın. Bu kodu elinizde bulundurmak, hem servisle hem de tedarikçiyle iletişimi kolaylaştırır, yanlış parça siparişini önler.

      @@ -223,7 +223,7 @@ const POSTS: Record = {

      Dijital Dönüşümün Faydaları

      Dijital platformlar, yedek parça arama sürecini kökten değiştirmektedir:

        -
      • VIN bazlı anlık araç tanımlama — saniyeler içinde doğru araç tespiti
      • +
      • Şase bazlı anlık araç tanımlama — saniyeler içinde doğru araç tespiti
      • Çoklu katalog çapraz sorgulama — tek arayüzde birden fazla kaynak
      • Otomatik parça eşleştirme — insan hatasını minimuma indirir
      • İnteraktif teknik şemalar — parçayı görsel bul
      • @@ -251,7 +251,7 @@ const POSTS: Record = {

        Sase.tr'nin Rolü

        Sase.tr, Türkiye'nin yedek parça sektörüne özgü geliştirilen bu dijital dönüşümün - öncüsüdür. VIN/şase numarası sorgulama, çoklu katalog entegrasyonu ve interaktif şema + öncüsüdür. Şase numarası sorgulama, çoklu katalog entegrasyonu ve interaktif şema görüntüleme özellikleriyle geleneksel iş yapış biçimlerini dönüştürmektedir. Platform, 27 marka ve 1 milyondan fazla OEM parça numarasıyla sektörün en kapsamlı dijital kataloğunu sunmaktadır. @@ -264,7 +264,7 @@ const POSTS: Record = { slug: "dogru-parcayi-bulun", title: "Sase.tr ile Doğru Parçayı İlk Seferde Bulun", description: - "Platform özelliklerini kullanarak VIN bazlı arama, şema görüntüleme ve parça eşleştirme rehberi.", + "Platform özelliklerini kullanarak şase bazlı arama, şema görüntüleme ve parça eşleştirme rehberi.", date: "2026-01-05", content: (

        @@ -274,11 +274,11 @@ const POSTS: Record = { platformu nasıl en verimli şekilde kullanacağınızı adım adım anlatıyoruz.

        -

        Adım 1: VIN Numarasını Girin

        +

        Adım 1: Şase Numarasını Girin

        - Ana sayfadaki arama kutusuna aracın 17 haneli şase numarasını (VIN) girin. Numara - girilirken anlık doğrulama çubuğu dolmaya başlar — 17. karaktere ulaştığınızda sistem - otomatik olarak araç bilgilerini çeker. + Ana sayfadaki arama kutusuna aracın 17 haneli şase numarasını girin. Numara girilirken + anlık doğrulama çubuğu dolmaya başlar — 17. karaktere ulaştığınızda sistem otomatik olarak + araç bilgilerini çeker.

        Sistem saniyeler içinde araç markasını, modelini, yılını ve motor bilgilerini gösterir. @@ -320,12 +320,12 @@ const POSTS: Record = {

        İpuçları

        • - Birden fazla araç için sorgu yapıyorsanız, her birini VIN ile kaydedin; geçmiş + Birden fazla araç için sorgu yapıyorsanız, her birini şase ile kaydedin; geçmiş aramalarınıza hızla dönebilirsiniz.
        • - Aynı OEM kodu birden fazla katalogda farklı fiyatlarla listelenebilir — fiyat - karşılaştırma özelliğini kullanın. + Aynı parça farklı kataloglarda listelenebilir — çapraz sorgulama her zaman en güncel OEM + kodunu getirir.
        • Parça bulamadığınızda kategori ağacında bir seviye yukarı çıkarak daha geniş bir arama From 24e44a49f83f50d8c2a2bc5fb8252cdd44a08bb0 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 21:00:12 +0300 Subject: [PATCH 09/35] fix(tecdoc): only surface publicly-resolvable part images The snapshot stores scrape-local '/_debug/...' image paths that 404 off-host. Filter the OEM lookup to http(s) image URLs and render the thumbnail only when one exists, so the detail page shows clean code-focused cards instead of a grid of broken-image placeholders. Real CDN URLs will surface automatically once present. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/integrations/tecdoc/tecdoc-source-db.service.ts | 7 ++++++- apps/web/src/routes/dashboard/oem.$code.tsx | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts b/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts index e183e72..4ff2396 100644 --- a/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts +++ b/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts @@ -128,8 +128,13 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { COALESCE(( SELECT json_agg(json_build_object('url', i.image_url, 'thumb', i.thumb_url) ORDER BY i.sort_order) FROM ( + -- Only publicly-resolvable URLs. The current snapshot stores + -- scrape-local '/_debug/...' paths (404 off-host) — filtering them + -- here keeps the API contract honest so the UI shows no broken + -- thumbnails; real CDN URLs surface automatically once present. SELECT image_url, thumb_url, sort_order FROM article_images - WHERE article_id = a.id ORDER BY sort_order LIMIT 8 + WHERE article_id = a.id AND image_url LIKE 'http%' + ORDER BY sort_order LIMIT 8 ) i ), '[]') AS images, COALESCE(( diff --git a/apps/web/src/routes/dashboard/oem.$code.tsx b/apps/web/src/routes/dashboard/oem.$code.tsx index de537cf..1a55acc 100644 --- a/apps/web/src/routes/dashboard/oem.$code.tsx +++ b/apps/web/src/routes/dashboard/oem.$code.tsx @@ -198,7 +198,9 @@ function OemDetailPage() { key={a.id} className="flex gap-3 rounded-xl border border-border bg-background p-3" > - + {a.images[0] && ( + + )}

          {a.brand}

          From aeb438e4429e7300ddec5132576318eec0d98ed1 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 21:13:03 +0300 Subject: [PATCH 10/35] feat(blog): back blog module with central Directus CMS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blog posts now live in the shared Directus instance (Coolify / Süper Panel project) instead of the per-env blog_posts table, so prod and staging serve identical content. API response shape is unchanged; Redis list cache and the n8n automation endpoint keep working as before. Co-Authored-By: Claude Fable 5 --- apps/api/src/blog/blog.service.ts | 185 +++++++++++++++++++++--------- docker-compose.coolify.yml | 8 ++ 2 files changed, 137 insertions(+), 56 deletions(-) diff --git a/apps/api/src/blog/blog.service.ts b/apps/api/src/blog/blog.service.ts index 7f0a9c4..9c9af66 100644 --- a/apps/api/src/blog/blog.service.ts +++ b/apps/api/src/blog/blog.service.ts @@ -1,7 +1,9 @@ -import { Inject, Injectable, NotFoundException } from "@nestjs/common"; -import { desc, eq } from "drizzle-orm"; -import { DATABASE, type Database } from "../database/database.provider"; -import { blogPosts } from "../database/schema/core"; +import { + Injectable, + Logger, + NotFoundException, + ServiceUnavailableException, +} from "@nestjs/common"; import { RedisService } from "../redis/redis.service"; import type { BlogPost, BlogPostListItem, CreateBlogPost } from "./blog.dto"; @@ -10,6 +12,28 @@ const CACHE_TTL = 1800; // 30 minutes — matches the web staleTime const PUBLIC_WEB_URL = process.env.PUBLIC_WEB_URL ?? "https://sase.tr"; +// Central Directus CMS (Coolify, Süper Panel project). Prod and staging share +// the same instance so blog content is identical across environments. +const DIRECTUS_URL = process.env.DIRECTUS_URL?.replace(/\/+$/, ""); +const DIRECTUS_TOKEN = process.env.DIRECTUS_TOKEN; +const DIRECTUS_PROJECT = process.env.DIRECTUS_PROJECT ?? "sase"; + +// Row shape of the shared `posts` collection in Directus. +interface DirectusPost { + id: string; + slug: string; + title: string; + meta_description: string | null; + body_markdown: string; + tags: string[] | null; + cover_image: string | null; + status: string; + source: string; + published_at: string; + date_created: string | null; + date_updated: string | null; +} + // URL-safe slug, Turkish-aware. The content_blog prompt already emits a clean // slug, but we slugify defensively (and to derive one if it's missing). function slugify(input: string): string { @@ -31,18 +55,48 @@ function slugify(input: string): string { @Injectable() export class BlogService { - constructor( - @Inject(DATABASE) private db: Database, - private readonly redis: RedisService, - ) {} + private readonly logger = new Logger(BlogService.name); - private toPost(row: typeof blogPosts.$inferSelect): BlogPost { + constructor(private readonly redis: RedisService) {} + + private async directus( + path: string, + init: { method?: string; body?: unknown } = {}, + ): Promise { + if (!DIRECTUS_URL || !DIRECTUS_TOKEN) { + throw new ServiceUnavailableException("Blog CMS is not configured"); + } + const res = await fetch(`${DIRECTUS_URL}${path}`, { + method: init.method ?? "GET", + headers: { + Authorization: `Bearer ${DIRECTUS_TOKEN}`, + "Content-Type": "application/json", + }, + body: init.body !== undefined ? JSON.stringify(init.body) : undefined, + signal: AbortSignal.timeout(10_000), + }); + if (!res.ok) { + const detail = await res.text().catch(() => ""); + throw new Error(`Directus ${init.method ?? "GET"} ${path} -> ${res.status}: ${detail.slice(0, 300)}`); + } + const json = (await res.json()) as { data: T }; + return json.data; + } + + private toPost(row: DirectusPost): BlogPost { return { - ...row, + id: row.id, + slug: row.slug, + title: row.title, + metaDescription: row.meta_description, + bodyMarkdown: row.body_markdown, + tags: row.tags ?? [], + coverImage: row.cover_image, status: row.status as BlogPost["status"], - publishedAt: row.publishedAt.toISOString(), - createdAt: row.createdAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), + source: row.source, + publishedAt: row.published_at, + createdAt: row.date_created ?? row.published_at, + updatedAt: row.date_updated ?? row.date_created ?? row.published_at, }; } @@ -50,55 +104,72 @@ export class BlogService { const cached = await this.redis.getJson(LIST_CACHE_KEY); if (cached) return cached; - const rows = await this.db - .select({ - slug: blogPosts.slug, - title: blogPosts.title, - metaDescription: blogPosts.metaDescription, - tags: blogPosts.tags, - coverImage: blogPosts.coverImage, - publishedAt: blogPosts.publishedAt, - status: blogPosts.status, - }) - .from(blogPosts) - .orderBy(desc(blogPosts.publishedAt)); + let rows: DirectusPost[]; + try { + rows = await this.directus( + "/items/posts?" + + new URLSearchParams({ + "filter[project][_eq]": DIRECTUS_PROJECT, + "filter[status][_eq]": "published", + sort: "-published_at", + fields: "slug,title,meta_description,tags,cover_image,published_at", + limit: "-1", + }), + ); + } catch (err) { + // Degrade gracefully — the blog page still renders its static posts. + this.logger.error(`Blog list fetch failed: ${(err as Error).message}`); + return []; + } - const items: BlogPostListItem[] = rows - .filter((r) => r.status === "published") - .map((r) => ({ - slug: r.slug, - title: r.title, - metaDescription: r.metaDescription, - tags: r.tags, - coverImage: r.coverImage, - publishedAt: r.publishedAt.toISOString(), - })); + const items: BlogPostListItem[] = rows.map((r) => ({ + slug: r.slug, + title: r.title, + metaDescription: r.meta_description, + tags: r.tags ?? [], + coverImage: r.cover_image, + publishedAt: r.published_at, + })); await this.redis.setJson(LIST_CACHE_KEY, items, CACHE_TTL); return items; } async findBySlug(slug: string): Promise { - const result = await this.db - .select() - .from(blogPosts) - .where(eq(blogPosts.slug, slug)) - .limit(1); - if (result.length === 0 || result[0].status !== "published") { + let rows: DirectusPost[]; + try { + rows = await this.directus( + "/items/posts?" + + new URLSearchParams({ + "filter[project][_eq]": DIRECTUS_PROJECT, + "filter[slug][_eq]": slug, + "filter[status][_eq]": "published", + limit: "1", + }), + ); + } catch (err) { + this.logger.error(`Blog post fetch failed (${slug}): ${(err as Error).message}`); throw new NotFoundException("Blog post not found"); } - return this.toPost(result[0]); + if (rows.length === 0) { + throw new NotFoundException("Blog post not found"); + } + return this.toPost(rows[0]); } - // Ensures a unique slug by appending a numeric suffix on collision. + // Ensures a unique slug (per project) by appending a numeric suffix on collision. private async uniqueSlug(base: string): Promise { let slug = base; for (let i = 2; i < 50; i++) { - const existing = await this.db - .select({ id: blogPosts.id }) - .from(blogPosts) - .where(eq(blogPosts.slug, slug)) - .limit(1); + const existing = await this.directus[]>( + "/items/posts?" + + new URLSearchParams({ + "filter[project][_eq]": DIRECTUS_PROJECT, + "filter[slug][_eq]": slug, + fields: "id", + limit: "1", + }), + ); if (existing.length === 0) return slug; slug = `${base}-${i}`; } @@ -112,19 +183,21 @@ export class BlogService { let body = dto.body_markdown ?? ""; if (dto.cta && !body.includes(dto.cta)) body = `${body}\n\n${dto.cta}`; - const [row] = await this.db - .insert(blogPosts) - .values({ + const row = await this.directus("/items/posts", { + method: "POST", + body: { + project: DIRECTUS_PROJECT, slug, title: dto.title, - metaDescription: dto.meta_description ?? null, - bodyMarkdown: body, + meta_description: dto.meta_description ?? null, + body_markdown: body, tags: Array.isArray(dto.tags) ? dto.tags.slice(0, 16) : [], - coverImage: dto.coverImage ?? null, + cover_image: dto.coverImage ?? null, status: dto.status ?? "published", source: "panel", - }) - .returning(); + published_at: new Date().toISOString(), + }, + }); await this.redis.del(LIST_CACHE_KEY); diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index 6cf5742..7207b85 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -104,6 +104,14 @@ services: # both vars are set in Coolify; see sase-prod-db-api-access memory. - TECDOC_DB_ENABLED=${TECDOC_DB_ENABLED:-false} - TECDOC_DB_URL=${TECDOC_DB_URL:-} + # Central Directus CMS (Süper Panel project on Coolify). Prod and staging + # point at the SAME instance so blog content is shared across envs. + # Internal docker-network URL — both stacks join the `coolify` network. + - DIRECTUS_URL=${DIRECTUS_URL:-} + - DIRECTUS_TOKEN=${DIRECTUS_TOKEN:-} + - DIRECTUS_PROJECT=${DIRECTUS_PROJECT:-sase} + # Bearer token for POST /blog/posts/internal (Süper Panel n8n pipeline) + - BLOG_AUTOMATION_TOKEN=${BLOG_AUTOMATION_TOKEN:-} depends_on: sase-redis: condition: service_healthy From 3b144313b371bc0bf9e3200b59f22839441592dc Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 22:29:40 +0300 Subject: [PATCH 11/35] =?UTF-8?q?refactor(p):=20rename=20TecDoc=20?= =?UTF-8?q?=E2=86=92=20P=20(parts)=20across=20the=20cross-reference=20feat?= =?UTF-8?q?ure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the third-party brand name from the product surface and code. The OEM detail feature is now "P" (short for parts) everywhere: endpoint /p/oem, PModule/PController/PSourceDbService, config key `p`, env P_DB_ENABLED/P_DB_URL, UI copy ("P kataloğundan…"), and the unused tab label. Physical snapshot DB stays `td` (server-only). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tecdoc-source-db.service.ts => p/p-source-db.service.ts} | 0 .../{tecdoc/tecdoc.controller.ts => p/p.controller.ts} | 0 .../src/integrations/{tecdoc/tecdoc.module.ts => p/p.module.ts} | 0 .../api/src/integrations/{tecdoc/tecdoc.types.ts => p/p.types.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename apps/api/src/integrations/{tecdoc/tecdoc-source-db.service.ts => p/p-source-db.service.ts} (100%) rename apps/api/src/integrations/{tecdoc/tecdoc.controller.ts => p/p.controller.ts} (100%) rename apps/api/src/integrations/{tecdoc/tecdoc.module.ts => p/p.module.ts} (100%) rename apps/api/src/integrations/{tecdoc/tecdoc.types.ts => p/p.types.ts} (100%) diff --git a/apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts b/apps/api/src/integrations/p/p-source-db.service.ts similarity index 100% rename from apps/api/src/integrations/tecdoc/tecdoc-source-db.service.ts rename to apps/api/src/integrations/p/p-source-db.service.ts diff --git a/apps/api/src/integrations/tecdoc/tecdoc.controller.ts b/apps/api/src/integrations/p/p.controller.ts similarity index 100% rename from apps/api/src/integrations/tecdoc/tecdoc.controller.ts rename to apps/api/src/integrations/p/p.controller.ts diff --git a/apps/api/src/integrations/tecdoc/tecdoc.module.ts b/apps/api/src/integrations/p/p.module.ts similarity index 100% rename from apps/api/src/integrations/tecdoc/tecdoc.module.ts rename to apps/api/src/integrations/p/p.module.ts diff --git a/apps/api/src/integrations/tecdoc/tecdoc.types.ts b/apps/api/src/integrations/p/p.types.ts similarity index 100% rename from apps/api/src/integrations/tecdoc/tecdoc.types.ts rename to apps/api/src/integrations/p/p.types.ts From e91af4b953f79c23c6720f163afda61e6a7484f6 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 22:30:46 +0300 Subject: [PATCH 12/35] =?UTF-8?q?refactor(p):=20complete=20TecDoc=20?= =?UTF-8?q?=E2=86=92=20P=20content=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finishes 3b14431 (which only captured the file renames): apply the identifier/endpoint/env/UI changes so the code matches the new paths — PModule/PController/PSourceDbService, @Controller("p"), /p/oem, config key `p`, P_DB_ENABLED/P_DB_URL, "P kataloğundan…" copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/src/app.module.ts | 4 +- apps/api/src/config/configuration.ts | 10 +-- .../src/integrations/p/p-source-db.service.ts | 61 +++++++++---------- apps/api/src/integrations/p/p.controller.ts | 14 ++--- apps/api/src/integrations/p/p.module.ts | 16 ++--- apps/api/src/integrations/p/p.types.ts | 24 ++++---- .../web/src/components/schema/parts-panel.tsx | 2 +- apps/web/src/messages/en.json | 2 +- apps/web/src/messages/tr.json | 2 +- apps/web/src/routes/dashboard/oem.$code.tsx | 24 ++++---- docker-compose.coolify.yml | 6 +- 11 files changed, 80 insertions(+), 85 deletions(-) diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 78bdb68..e25ba89 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -29,7 +29,7 @@ import { DemoModule } from "./demo/demo.module"; import { EmailModule } from "./email/email.module"; import { HealthController } from "./health.controller"; import { EmexModule } from "./integrations/emex/emex.module"; -import { TecdocModule } from "./integrations/tecdoc/tecdoc.module"; +import { PModule } from "./integrations/p/p.module"; import { InternalAdminModule } from "./internal-admin/internal-admin.module"; import { JobsModule } from "./jobs/jobs.module"; import { MetaCapiModule } from "./meta-capi/meta-capi.module"; @@ -87,7 +87,7 @@ import { VehiclesModule } from "./vehicles/vehicles.module"; CategoriesModule, DemoModule, PartsModule, - TecdocModule, + PModule, JobsModule, EmexModule, TranslationsModule, diff --git a/apps/api/src/config/configuration.ts b/apps/api/src/config/configuration.ts index 2e9b377..8316ad4 100644 --- a/apps/api/src/config/configuration.ts +++ b/apps/api/src/config/configuration.ts @@ -88,13 +88,13 @@ export default () => ({ .map((s) => s.trim()) .filter(Boolean), }, - tecdoc: { - // Read-only lookup against the imported TecDoc snapshot (db `td`). When + p: { + // Read-only lookup against the imported P snapshot (db `td`). When // enabled + url set, the OEM detail page resolves a part's OEM code to - // TecDoc aftermarket equivalents + OE cross-references. Disabled → endpoint + // P aftermarket equivalents + OE cross-references. Disabled → endpoint // returns { matched: false } and the UI shows an empty state. - enabled: process.env.TECDOC_DB_ENABLED === "true", - url: process.env.TECDOC_DB_URL, + enabled: process.env.P_DB_ENABLED === "true", + url: process.env.P_DB_URL, }, otel: { enabled: process.env.OTEL_ENABLED === "true", diff --git a/apps/api/src/integrations/p/p-source-db.service.ts b/apps/api/src/integrations/p/p-source-db.service.ts index 4ff2396..8b4ff49 100644 --- a/apps/api/src/integrations/p/p-source-db.service.ts +++ b/apps/api/src/integrations/p/p-source-db.service.ts @@ -1,32 +1,27 @@ import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import postgres, { type Sql } from "postgres"; -import type { - TecdocArticle, - TecdocCompatible, - TecdocOeNumber, - TecdocOemResult, -} from "./tecdoc.types"; +import type { PArticle, PCompatible, POeNumber, POemResult } from "./p.types"; /** - * Read-only lookup against the imported TecDoc snapshot (db `td` — a selective + * Read-only lookup against the imported P snapshot (db `td` — a selective * copy of articles + OE numbers + aftermarket compatibility + images/eans; the * 29 GB vehicle-fitment table is intentionally excluded). Given an OEM code from - * the sase catalog, returns the TecDoc articles that carry it as an OE number, + * the sase catalog, returns the P articles that carry it as an OE number, * with their aftermarket equivalents and OE cross-references. * - * Matching is normalisation-based, not exact: TecDoc stores OE codes with + * Matching is normalisation-based, not exact: P stores OE codes with * spaces/dashes (`1J0 973 702`) while the catalog gives `1J0973702`, so both * sides are reduced to `[A-Z0-9]` uppercase before comparison (a precomputed - * `code_norm` column, indexed, holds the TecDoc side). Exact matching recovers + * `code_norm` column, indexed, holds the P side). Exact matching recovers * almost nothing — verified ~1/10 vs normalised ~5/10 on real catalog codes. * * Never throws: disabled feature, too-short code, connection blip or no match * all collapse to `matched: false` so the UI has a single empty-state path. */ @Injectable() -export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { - private readonly logger = new Logger(TecdocSourceDbService.name); +export class PSourceDbService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(PSourceDbService.name); private sql: Sql | null = null; private enabled = false; @@ -39,10 +34,10 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { constructor(private readonly config: ConfigService) {} onModuleInit() { - const enabled = this.config.get("tecdoc.enabled"); - const url = this.config.get("tecdoc.url"); + const enabled = this.config.get("p.enabled"); + const url = this.config.get("p.url"); if (!enabled || !url) { - this.logger.log(`[tecdoc] disabled (enabled=${enabled}, urlSet=${Boolean(url)})`); + this.logger.log(`[p] disabled (enabled=${enabled}, urlSet=${Boolean(url)})`); return; } this.sql = postgres(url, { @@ -52,7 +47,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { prepare: false, }); this.enabled = true; - this.logger.log("[tecdoc] connected, OEM cross-reference lookup enabled"); + this.logger.log("[p] connected, OEM cross-reference lookup enabled"); } async onModuleDestroy() { @@ -63,16 +58,16 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { } /** `1J0 973 702` / `1j0-973-702` → `1J0973702`. Used for both the input code - * and JS-side dedupe; the TecDoc side is matched against the stored + * and JS-side dedupe; the P side is matched against the stored * `code_norm` (built with the identical rule at import time). */ private static norm(code: string): string { return code.toUpperCase().replace(/[^A-Z0-9]/g, ""); } - async lookupByOem(rawCode: string): Promise { + async lookupByOem(rawCode: string): Promise { const query = (rawCode ?? "").trim(); - const queryNorm = TecdocSourceDbService.norm(query); - const miss: TecdocOemResult = { + const queryNorm = PSourceDbService.norm(query); + const miss: POemResult = { query, queryNorm, matched: false, @@ -83,7 +78,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { }; if (!this.enabled || !this.sql) return miss; - if (queryNorm.length < TecdocSourceDbService.MIN_NORM_LEN) return miss; + if (queryNorm.length < PSourceDbService.MIN_NORM_LEN) return miss; try { const rows = await this.sql< @@ -93,8 +88,8 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { article_number: string; name: string | null; spare_info: string | null; - oe_numbers: TecdocOeNumber[]; - compatible: TecdocCompatible[]; + oe_numbers: POeNumber[]; + compatible: PCompatible[]; images: Array<{ url: string; thumb: string | null }>; eans: string[]; }> @@ -103,7 +98,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { SELECT DISTINCT article_id FROM article_oe_numbers WHERE code_norm = ${queryNorm} - LIMIT ${TecdocSourceDbService.MAX_ARTICLES} + LIMIT ${PSourceDbService.MAX_ARTICLES} ) SELECT a.id::text AS id, @@ -150,9 +145,9 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { if (rows.length === 0) return miss; - let truncated = rows.length >= TecdocSourceDbService.MAX_ARTICLES; + let truncated = rows.length >= PSourceDbService.MAX_ARTICLES; - const articles: TecdocArticle[] = rows.map((r) => { + const articles: PArticle[] = rows.map((r) => { if (r.oe_numbers.length >= 200 || r.compatible.length >= 200) truncated = true; return { id: r.id, @@ -171,12 +166,12 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { // The matched articles are themselves aftermarket parts; their // compatibility rows add equivalent numbers from other supplier brands. const afterSeen = new Set(); - const aftermarketParts: TecdocOemResult["aftermarketParts"] = []; + const aftermarketParts: POemResult["aftermarketParts"] = []; const pushAfter = (brand: string, articleNumber: string, thumb: string | null) => { - const key = `${brand.toUpperCase().trim()}␟${TecdocSourceDbService.norm(articleNumber)}`; + const key = `${brand.toUpperCase().trim()}␟${PSourceDbService.norm(articleNumber)}`; if (afterSeen.has(key) || !articleNumber.trim()) return; afterSeen.add(key); - if (aftermarketParts.length < TecdocSourceDbService.MAX_AGG) { + if (aftermarketParts.length < PSourceDbService.MAX_AGG) { aftermarketParts.push({ brand, articleNumber, thumb }); } else { truncated = true; @@ -192,15 +187,15 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { // ── Aggregate: OE cross-references (same part, other makes) ─────────── // Exclude restatements of the queried code itself (same normalised code). const oeSeen = new Set(); - const oeCrossReferences: TecdocOeNumber[] = []; + const oeCrossReferences: POeNumber[] = []; for (const a of articles) { for (const oe of a.oeNumbers) { - const codeNorm = TecdocSourceDbService.norm(oe.code); + const codeNorm = PSourceDbService.norm(oe.code); if (codeNorm === queryNorm) continue; const key = `${oe.brand.toUpperCase().trim()}␟${codeNorm}`; if (oeSeen.has(key)) continue; oeSeen.add(key); - if (oeCrossReferences.length < TecdocSourceDbService.MAX_AGG) { + if (oeCrossReferences.length < PSourceDbService.MAX_AGG) { oeCrossReferences.push(oe); } else { truncated = true; @@ -218,7 +213,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy { truncated, }; } catch (err) { - this.logger.warn(`[tecdoc] lookup failed (oem=${query}): ${(err as Error).message}`); + this.logger.warn(`[p] lookup failed (oem=${query}): ${(err as Error).message}`); return miss; } } diff --git a/apps/api/src/integrations/p/p.controller.ts b/apps/api/src/integrations/p/p.controller.ts index 077f6a7..8237cf0 100644 --- a/apps/api/src/integrations/p/p.controller.ts +++ b/apps/api/src/integrations/p/p.controller.ts @@ -1,17 +1,17 @@ import { Controller, Get, Query } from "@nestjs/common"; -import { TecdocSourceDbService } from "./tecdoc-source-db.service"; +import { PSourceDbService } from "./p-source-db.service"; -@Controller("tecdoc") -export class TecdocController { - constructor(private readonly tecdoc: TecdocSourceDbService) {} +@Controller("p") +export class PController { + constructor(private readonly p: PSourceDbService) {} /** - * Resolve an OEM code from the catalog to its TecDoc equivalents. - * `GET /tecdoc/oem?code=1J0973702` → { matched, articles, aftermarketParts, + * Resolve an OEM code from the catalog to its P equivalents. + * `GET /p/oem?code=1J0973702` → { matched, articles, aftermarketParts, * oeCrossReferences }. Always 200 with `matched: false` on any miss. */ @Get("oem") async oem(@Query("code") code: string) { - return this.tecdoc.lookupByOem(code ?? ""); + return this.p.lookupByOem(code ?? ""); } } diff --git a/apps/api/src/integrations/p/p.module.ts b/apps/api/src/integrations/p/p.module.ts index 11ff55f..3f2d695 100644 --- a/apps/api/src/integrations/p/p.module.ts +++ b/apps/api/src/integrations/p/p.module.ts @@ -1,15 +1,15 @@ import { Module } from "@nestjs/common"; -import { TecdocSourceDbService } from "./tecdoc-source-db.service"; -import { TecdocController } from "./tecdoc.controller"; +import { PSourceDbService } from "./p-source-db.service"; +import { PController } from "./p.controller"; /** - * OEM cross-reference lookup against the imported TecDoc snapshot (db `td`). + * OEM cross-reference lookup against the imported P snapshot (db `td`). * Raw read-only queries — intentionally no Drizzle schema modelling, mirroring - * CatalogSourceDbModule. Self-disables when TECDOC_DB_* env is unset. + * CatalogSourceDbModule. Self-disables when P_DB_* env is unset. */ @Module({ - controllers: [TecdocController], - providers: [TecdocSourceDbService], - exports: [TecdocSourceDbService], + controllers: [PController], + providers: [PSourceDbService], + exports: [PSourceDbService], }) -export class TecdocModule {} +export class PModule {} diff --git a/apps/api/src/integrations/p/p.types.ts b/apps/api/src/integrations/p/p.types.ts index 72177ed..85b84ff 100644 --- a/apps/api/src/integrations/p/p.types.ts +++ b/apps/api/src/integrations/p/p.types.ts @@ -1,50 +1,50 @@ /** An OE (original-equipment) number cross-reference: the same physical part as * catalogued by a vehicle manufacturer (e.g. VAG `1J0 973 702`). */ -export interface TecdocOeNumber { +export interface POeNumber { brand: string; code: string; } /** An aftermarket equivalent: a buyable part number from a supplier brand * (e.g. FEBI BILSTEIN `171903`). */ -export interface TecdocCompatible { +export interface PCompatible { brand: string; article: string; } -export interface TecdocImage { +export interface PImage { url: string; thumb: string | null; } -/** One TecDoc article whose OE number list contains the queried OEM code. */ -export interface TecdocArticle { +/** One P article whose OE number list contains the queried OEM code. */ +export interface PArticle { id: string; brand: string; articleNumber: string; name: string | null; spareInfo: string | null; - images: TecdocImage[]; + images: PImage[]; eans: string[]; - oeNumbers: TecdocOeNumber[]; - compatible: TecdocCompatible[]; + oeNumbers: POeNumber[]; + compatible: PCompatible[]; } /** Response of the OEM detail lookup. `matched: false` covers every miss — - * feature disabled, code too short, or no TecDoc article carries that OE + * feature disabled, code too short, or no P article carries that OE * number — so the UI has a single empty-state path. */ -export interface TecdocOemResult { +export interface POemResult { query: string; queryNorm: string; matched: boolean; /** Distinct articles whose OE list contains the queried code. */ - articles: TecdocArticle[]; + articles: PArticle[]; /** Deduped buyable aftermarket part numbers across all matched articles * (the matched articles themselves + their compatibility entries). */ aftermarketParts: Array<{ brand: string; articleNumber: string; thumb: string | null }>; /** Deduped OE cross-references across all matched articles, excluding the * queried code itself — i.e. the same part's numbers under other makes. */ - oeCrossReferences: TecdocOeNumber[]; + oeCrossReferences: POeNumber[]; /** True when any per-article list or the article set hit its cap. */ truncated: boolean; } diff --git a/apps/web/src/components/schema/parts-panel.tsx b/apps/web/src/components/schema/parts-panel.tsx index cd663a1..760a6ea 100644 --- a/apps/web/src/components/schema/parts-panel.tsx +++ b/apps/web/src/components/schema/parts-panel.tsx @@ -366,7 +366,7 @@ export function PartsPanel({ )} {part.oemCode && part.oemCode !== "N/A" ? ( - // Click → OEM detail page (TecDoc cross-reference) in a + // Click → OEM detail page (P cross-reference) in a // new tab so the catalog/schema context stays put. Plain // anchor (not router Link) — a fresh load resolves the // route and keeps this cell router-context-free. diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 0fca206..51ea47e 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -185,7 +185,7 @@ "tabPl24": "Pl24", "tabPcat": "Pcat", "tabEmex": "Emex", - "tabTecdoc": "Tecdoc", + "tabTecdoc": "P", "comingSoon": "Coming Soon", "categories": "Categories", "noCategories": "No categories found", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index eafe55f..06659a0 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -185,7 +185,7 @@ "tabPl24": "Pl24", "tabPcat": "Pcat", "tabEmex": "Emex", - "tabTecdoc": "Tecdoc", + "tabTecdoc": "P", "comingSoon": "Yakında", "categories": "Kategoriler", "noCategories": "Kategori bulunamadı", diff --git a/apps/web/src/routes/dashboard/oem.$code.tsx b/apps/web/src/routes/dashboard/oem.$code.tsx index 1a55acc..a1c21d4 100644 --- a/apps/web/src/routes/dashboard/oem.$code.tsx +++ b/apps/web/src/routes/dashboard/oem.$code.tsx @@ -6,13 +6,13 @@ import { Link, createFileRoute } from "@tanstack/react-router"; import { AlertCircle, ArrowLeft, Check, Copy, ImageOff, PackageSearch } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -// ─── Types (mirror apps/api TecdocOemResult) ──────────────────────────────── +// ─── Types (mirror apps/api POemResult) ──────────────────────────────── interface OeNumber { brand: string; code: string; } -interface TecdocArticle { +interface PArticle { id: string; brand: string; articleNumber: string; @@ -23,11 +23,11 @@ interface TecdocArticle { oeNumbers: OeNumber[]; compatible: Array<{ brand: string; article: string }>; } -interface TecdocOemResult { +interface POemResult { query: string; queryNorm: string; matched: boolean; - articles: TecdocArticle[]; + articles: PArticle[]; aftermarketParts: Array<{ brand: string; articleNumber: string; thumb: string | null }>; oeCrossReferences: OeNumber[]; truncated: boolean; @@ -99,8 +99,8 @@ function OemDetailPage() { const { code } = Route.useParams(); const { data, isLoading, error } = useQuery({ - queryKey: ["tecdoc-oem", code], - queryFn: () => api.get(`/tecdoc/oem?code=${encodeURIComponent(code)}`), + queryKey: ["p-oem", code], + queryFn: () => api.get(`/p/oem?code=${encodeURIComponent(code)}`), }); useEffect(() => { @@ -136,7 +136,7 @@ function OemDetailPage() { />

          - TecDoc kataloğundan uyumlu parça kodları ve muadiller + P kataloğundan uyumlu parça kodları ve muadiller

        @@ -160,16 +160,16 @@ function OemDetailPage() {
    )} - {/* ─── Empty state (no TecDoc match) ──────────────────────────────── */} + {/* ─── Empty state (no P match) ──────────────────────────────── */} {data && !data.matched && (
    -

    Bu OEM kodu için TecDoc karşılığı bulunamadı

    +

    Bu OEM kodu için P karşılığı bulunamadı

    - Bağlantı parçaları, klipsler ve bazı orijinal kodlar TecDoc kapsamında olmayabilir. - Katalog büyüdükçe eşleşme oranı artar. + Bağlantı parçaları, klipsler ve bazı orijinal kodlar P kapsamında olmayabilir. Katalog + büyüdükçe eşleşme oranı artar.

    )} @@ -189,7 +189,7 @@ function OemDetailPage() { )} - {/* Matched TecDoc articles (highest-confidence: carry this OEM directly) */} + {/* Matched P articles (highest-confidence: carry this OEM directly) */}

    Bu OEM'i taşıyan parçalar

    diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index 7207b85..e7a3c6b 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -100,10 +100,10 @@ services: # memory: catalog-wide bridge had 7-114x noise; only add a catalog once # its per-vehicle bridge is wired & OEM-verified. - EMEX_SOURCE_DB_ALLOWED_CATALOGS=${EMEX_SOURCE_DB_ALLOWED_CATALOGS:-} - # TecDoc snapshot lookup (db `td`) backing the OEM detail page. Off until + # P snapshot lookup (db `td`) backing the OEM detail page. Off until # both vars are set in Coolify; see sase-prod-db-api-access memory. - - TECDOC_DB_ENABLED=${TECDOC_DB_ENABLED:-false} - - TECDOC_DB_URL=${TECDOC_DB_URL:-} + - P_DB_ENABLED=${P_DB_ENABLED:-false} + - P_DB_URL=${P_DB_URL:-} # Central Directus CMS (Süper Panel project on Coolify). Prod and staging # point at the SAME instance so blog content is shared across envs. # Internal docker-network URL — both stacks join the `coolify` network. From 4716232398fd22d5bb363ac2c99b4e240403236b Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 22:37:15 +0300 Subject: [PATCH 13/35] fix(p): natural empty-state copy (drop literal "P" from user text) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "P karşılığı bulunamadı" / "P kapsamında" read as nonsense — "P" is the internal name, not a user-facing word. Use plain Turkish: "Bu OEM kodu için uyumlu parça bulunamadı" and describe the catalog gap without a brand or single-letter label. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/routes/dashboard/oem.$code.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/routes/dashboard/oem.$code.tsx b/apps/web/src/routes/dashboard/oem.$code.tsx index a1c21d4..3b2e06f 100644 --- a/apps/web/src/routes/dashboard/oem.$code.tsx +++ b/apps/web/src/routes/dashboard/oem.$code.tsx @@ -136,7 +136,7 @@ function OemDetailPage() { />

    - P kataloğundan uyumlu parça kodları ve muadiller + Uyumlu parça kodları ve muadil numaralar

    @@ -166,10 +166,10 @@ function OemDetailPage() {
    -

    Bu OEM kodu için P karşılığı bulunamadı

    +

    Bu OEM kodu için uyumlu parça bulunamadı

    - Bağlantı parçaları, klipsler ve bazı orijinal kodlar P kapsamında olmayabilir. Katalog - büyüdükçe eşleşme oranı artar. + Bağlantı parçaları, klipsler ve bazı orijinal kodların muadili henüz kataloğumuzda + olmayabilir. Katalog büyüdükçe eşleşme oranı artar.

    )} From 597391bbdf7300fdfe56016fbe6b26cb242a68cc Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 23:44:45 +0300 Subject: [PATCH 14/35] feat(surveys): Formbricks in-app survey bridge over PostHog events Self-hosted Formbricks (anket.sase.tr) replaces PostHog surveys (free-tier branding). PostHog stays the single instrumentation source: capture() forwards allowlisted trigger events (trial_urgency_banner_viewed, subscription_cancelled, onboarding_completed, vin_decode_error, empty_catalog_cta_clicked) to the Formbricks SDK, identify/reset/people-properties mirror into Formbricks attributes, and $pageview registers SPA route changes for no-code triggers. - apps/web/src/lib/formbricks.ts: lazy fire-and-forget wrapper (inert without VITE_FORMBRICKS_APP_URL + VITE_FORMBRICKS_ENV_ID) - CSP: allow anket.sase.tr in connect-src/img-src - Dockerfile + compose: bake the two VITE_ build args Co-Authored-By: Claude Fable 5 --- Dockerfile | 2 + apps/api/src/main.ts | 2 + apps/web/package.json | 1 + apps/web/src/lib/formbricks.ts | 79 ++++++++++++++++++++++++++++++++++ apps/web/src/lib/posthog.ts | 23 ++++++++++ apps/web/src/main.tsx | 4 ++ docker-compose.coolify.yml | 2 + pnpm-lock.yaml | 74 +++++-------------------------- 8 files changed, 124 insertions(+), 63 deletions(-) create mode 100644 apps/web/src/lib/formbricks.ts diff --git a/Dockerfile b/Dockerfile index 58557c8..8317cb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,8 @@ ARG VITE_POSTHOG_KEY= ARG VITE_META_PIXEL_ID= ARG VITE_CHATWOOT_BASE_URL= ARG VITE_CHATWOOT_WEBSITE_TOKEN= +ARG VITE_FORMBRICKS_APP_URL= +ARG VITE_FORMBRICKS_ENV_ID= RUN pnpm build diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 5150010..54c107e 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -39,6 +39,7 @@ async function bootstrap() { "https://storage.sase.tr", "https://www.facebook.com", "https://destek.sase.tr", + "https://anket.sase.tr", ], fontSrc: ["'self'", "https:", "data:"], mediaSrc: ["'self'", "data:", "https://destek.sase.tr"], @@ -51,6 +52,7 @@ async function bootstrap() { "https://challenges.cloudflare.com", "https://destek.sase.tr", "wss://destek.sase.tr", + "https://anket.sase.tr", // Sentry browser SDK envelope POSTs (otolog org, de region). // Without this CSP silently blocks every error/replay upload. "https://*.ingest.de.sentry.io", diff --git a/apps/web/package.json b/apps/web/package.json index 8a64c90..b038bd5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@cardog-icons/react": "^1.1.1", + "@formbricks/js": "4.4.0", "@grafana/faro-web-sdk": "^2.2.4", "@grafana/faro-web-tracing": "^2.2.4", "@remotion/player": "^4.0.422", diff --git a/apps/web/src/lib/formbricks.ts b/apps/web/src/lib/formbricks.ts new file mode 100644 index 0000000..5a5e533 --- /dev/null +++ b/apps/web/src/lib/formbricks.ts @@ -0,0 +1,79 @@ +// Formbricks in-app surveys (self-hosted at anket.sase.tr) — lazily loaded so +// it stays out of the initial bundle. All exported functions are fire-and-forget; +// when the env vars are unset (e.g. local dev) every call is a no-op. +// +// PostHog stays the single instrumentation source: lib/posthog.ts forwards +// allowlisted capture() events here (FORMBRICKS_TRIGGER_EVENTS) so surveys +// trigger on the same event names that exist in PostHog. + +type Formbricks = typeof import("@formbricks/js")["default"]; + +const APP_URL = import.meta.env.VITE_FORMBRICKS_APP_URL; +const ENV_ID = import.meta.env.VITE_FORMBRICKS_ENV_ID; + +let _setupPromise: Promise | null = null; + +function load(): Promise { + if (!APP_URL || !ENV_ID) return Promise.resolve(null); + if (_setupPromise) return _setupPromise; + _setupPromise = import("@formbricks/js") + .then(async (m) => { + await m.default.setup({ environmentId: ENV_ID, appUrl: APP_URL }); + return m.default; + }) + .catch(() => null); + return _setupPromise; +} + +export function initFormbricks(): void { + void load(); +} + +/** Forwarded from posthog.ts capture() for allowlisted events — shows any + * survey whose trigger action matches the event code. */ +export function trackFormbricks(event: string): void { + load() + .then((fb) => fb?.track(event)) + .catch(() => {}); +} + +export function identifyFormbricksUser(user: { + id: string; + email: string; + role?: string; +}): void { + load() + .then(async (fb) => { + if (!fb) return; + await fb.setUserId(user.id); + await fb.setEmail(user.email); + if (user.role) await fb.setAttribute("role", user.role); + }) + .catch(() => {}); +} + +/** Person properties → Formbricks attributes (string-only API), so surveys can + * target e.g. subscription_status=trial the same way PostHog cohorts do. */ +export function setFormbricksAttributes(properties: Record): void { + const attrs: Record = {}; + for (const [key, value] of Object.entries(properties)) { + if (value !== null && value !== undefined) attrs[key] = String(value); + } + if (Object.keys(attrs).length === 0) return; + load() + .then((fb) => fb?.setAttributes(attrs)) + .catch(() => {}); +} + +export function resetFormbricksUser(): void { + load() + .then((fb) => fb?.logout()) + .catch(() => {}); +} + +/** SPA navigation hook — lets URL-based (no-code) survey triggers fire. */ +export function formbricksRouteChange(): void { + load() + .then((fb) => fb?.registerRouteChange()) + .catch(() => {}); +} diff --git a/apps/web/src/lib/posthog.ts b/apps/web/src/lib/posthog.ts index 26c9702..9f65c3b 100644 --- a/apps/web/src/lib/posthog.ts +++ b/apps/web/src/lib/posthog.ts @@ -1,8 +1,26 @@ // PostHog — lazily loaded so it doesn't land in the initial bundle. // All exported functions are fire-and-forget; analytics loss on failure is acceptable. +import { + formbricksRouteChange, + identifyFormbricksUser, + resetFormbricksUser, + setFormbricksAttributes, + trackFormbricks, +} from "./formbricks"; + type PostHog = import("posthog-js").PostHog; +// Events that double as Formbricks survey triggers (mirrored as code actions +// at anket.sase.tr). Keep in sync when adding a survey with an event trigger. +const FORMBRICKS_TRIGGER_EVENTS = new Set([ + "trial_urgency_banner_viewed", + "subscription_cancelled", + "onboarding_completed", + "vin_decode_error", + "empty_catalog_cta_clicked", +]); + let _ph: PostHog | null = null; let _loadPromise: Promise | null = null; let _initialized = false; @@ -60,14 +78,17 @@ export function identifyUser(user: { role: user.role, }); }); + identifyFormbricksUser({ id: user.id, email: user.email, role: user.role }); } export function resetUser(): void { load().then((ph) => ph.reset()); + resetFormbricksUser(); } export function capture(event: string, properties?: Record): void { load().then((ph) => ph.capture(event, properties)); + if (FORMBRICKS_TRIGGER_EVENTS.has(event)) trackFormbricks(event); } export function capturePageView(path: string): void { @@ -79,10 +100,12 @@ export function capturePageView(path: string): void { load().then((ph) => ph.capture("$pageview", { $current_url: window.location.href, $pathname: path }), ); + formbricksRouteChange(); } export function setPeopleProperties(properties: Record): void { load().then((ph) => ph.people?.set(properties)); + setFormbricksAttributes(properties); } /** diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 968f0b2..70877d1 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -4,6 +4,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { initChatwoot } from "./lib/chatwoot"; import { initFaro } from "./lib/faro"; +import { initFormbricks } from "./lib/formbricks"; import { initLocale } from "./lib/i18n"; import { initMetaPixel } from "./lib/meta-pixel"; import { initPostHog } from "./lib/posthog"; @@ -30,6 +31,9 @@ initMetaPixel(); // Initialize Chatwoot live-chat widget initChatwoot(); +// Initialize Formbricks in-app surveys +initFormbricks(); + const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index e7a3c6b..6757145 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -10,6 +10,8 @@ services: - VITE_META_PIXEL_ID=${VITE_META_PIXEL_ID:-} - VITE_CHATWOOT_BASE_URL=${VITE_CHATWOOT_BASE_URL:-} - VITE_CHATWOOT_WEBSITE_TOKEN=${VITE_CHATWOOT_WEBSITE_TOKEN:-} + - VITE_FORMBRICKS_APP_URL=${VITE_FORMBRICKS_APP_URL:-} + - VITE_FORMBRICKS_ENV_ID=${VITE_FORMBRICKS_ENV_ID:-} # Sentry browser SDK — gated on DSN presence (unset = skip init). # VITE_* args must be build-time; runtime env won't reach the bundle. - VITE_SENTRY_DSN=${VITE_SENTRY_DSN:-} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5715e7..6b26821 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: '@cardog-icons/react': specifier: ^1.1.1 version: 1.1.1(react@19.2.4) + '@formbricks/js': + specifier: 4.4.0 + version: 4.4.0 '@grafana/faro-web-sdk': specifier: ^2.2.4 version: 2.2.4 @@ -225,7 +228,7 @@ importers: version: 1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) better-auth: specifier: ^1.2.0 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8))(mysql2@3.22.4(@types/node@22.19.11))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8))(mysql2@3.22.4(@types/node@22.19.11))(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) canvas-confetti: specifier: ^1.9.4 version: 1.9.4 @@ -1328,6 +1331,9 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@formbricks/js@4.4.0': + resolution: {integrity: sha512-97nu0lOhsDXmymE2bwg+PJqCXD7gIl5YbUaXWnvEGdRwyKmPZ3hFPEmw05G3y5xqjjd2XkmfuIYNP8x6WVYwMw==} + '@grafana/faro-core@2.2.4': resolution: {integrity: sha512-VUmvJgHUzgB7zGROpQfOHEW6xumwvcHJNwTSFLkAVfl7A5aNq+fXnMawn+W7qMJuUFkmUG6lMZ2N+WCvd0GPTA==} @@ -7471,6 +7477,8 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@formbricks/js@4.4.0': {} + '@grafana/faro-core@2.2.4': dependencies: '@opentelemetry/api': 1.9.0 @@ -10176,30 +10184,7 @@ snapshots: drizzle-kit: 0.31.9 drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8) mysql2: 3.22.4(@types/node@22.19.11) - next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - vitest: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8))(mysql2@3.22.4(@types/node@22.19.11))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): - dependencies: - '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.21 - '@noble/ciphers': 2.1.1 - '@noble/hashes': 2.0.1 - better-call: 1.1.8(zod@4.3.6) - defu: 6.1.4 - jose: 6.1.3 - kysely: 0.28.11 - nanostores: 1.1.0 - zod: 4.3.6 - optionalDependencies: - drizzle-kit: 0.31.9 - drizzle-orm: 0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8) - mysql2: 3.22.4(@types/node@22.19.11) - next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) vitest: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) @@ -10632,16 +10617,6 @@ snapshots: mysql2: 3.22.4(@types/node@22.19.11) postgres: 3.4.8 - drizzle-orm@0.41.0(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(mysql2@3.22.4(@types/node@22.19.11))(postgres@3.4.8): - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/pg': 8.15.6 - gel: 2.2.0 - kysely: 0.28.11 - mysql2: 3.22.4(@types/node@22.19.11) - postgres: 3.4.8 - optional: true - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -11893,34 +11868,7 @@ snapshots: neo-async@2.6.2: {} - next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@next/env': 16.1.6 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 - postcss: 8.4.31 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) - optionalDependencies: - '@next/swc-darwin-arm64': 16.1.6 - '@next/swc-darwin-x64': 16.1.6 - '@next/swc-linux-arm64-gnu': 16.1.6 - '@next/swc-linux-arm64-musl': 16.1.6 - '@next/swc-linux-x64-gnu': 16.1.6 - '@next/swc-linux-x64-musl': 16.1.6 - '@next/swc-win32-arm64-msvc': 16.1.6 - '@next/swc-win32-x64-msvc': 16.1.6 - '@opentelemetry/api': 1.9.1 - '@playwright/test': 1.58.2 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - optional: true - - next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 From f955bb7ef896f3526ff2a8258f1cea40282f7fda Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 23:47:33 +0300 Subject: [PATCH 15/35] fix(vehicles): canonicalise brand casing (case-insensitive brand match) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decode persist looked brands up with a case-sensitive eq(), so an uppercase decode string ("FORD") missed canonical "Ford" → brand_id NULL + raw uppercase stored as brand_name, splitting one brand across casing variants in analytics/catalog. Now matches brands case-insensitively and stores the canonical name. Migration 0013_fix_brand_casing backfills existing rows (60 on prod, 1 on dev). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/drizzle/0013_fix_brand_casing.sql | 28 ++++++++++++++++++++++ apps/api/drizzle/meta/_journal.json | 7 ++++++ apps/api/src/vehicles/vehicles.service.ts | 10 ++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 apps/api/drizzle/0013_fix_brand_casing.sql diff --git a/apps/api/drizzle/0013_fix_brand_casing.sql b/apps/api/drizzle/0013_fix_brand_casing.sql new file mode 100644 index 0000000..6ccc514 --- /dev/null +++ b/apps/api/drizzle/0013_fix_brand_casing.sql @@ -0,0 +1,28 @@ +-- Canonicalise vehicle brand casing + backfill missing brand_id. +-- +-- Root cause: the decode persist (vehicles.service.ts) looked brands up with a +-- case-SENSITIVE `eq(brands.name, brandName)`. A decode source that yields an +-- uppercase brand string (e.g. "FORD", "MERCEDES-BENZ") missed the canonical +-- "Ford" / "Mercedes-Benz" row → brand_id stayed NULL and the raw uppercase +-- string was stored as brand_name. That split the same brand across casing +-- variants in analytics + the catalog ("Ford" vs "FORD"). The companion code +-- fix makes the lookup case-insensitive and stores the canonical name; this +-- migration repairs the rows already written. +-- +-- Backfill every vehicle whose brand_name matches a canonical brand +-- case-insensitively: set the FK and the canonical-cased name. (catalog_vehicles +-- is intentionally untouched — its brand_name namespace does not match the +-- brands table, so 0 rows would qualify.) +UPDATE "vehicles" v +SET brand_id = b.id, brand_name = b.name +FROM "brands" b +WHERE v.brand_id IS NULL AND lower(v.brand_name) = lower(b.name); +--> statement-breakpoint + +-- Defensive: realign any vehicle whose brand_id is set but whose denormalised +-- brand_name has drifted from the canonical brands.name (none today, but keeps +-- the FK and the denormalised name consistent going forward). +UPDATE "vehicles" v +SET brand_name = b.name +FROM "brands" b +WHERE v.brand_id = b.id AND v.brand_name <> b.name; diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 4352627..d8222f7 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1780581755559, "tag": "0012_lifecycle_email_sent", "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1780600000000, + "tag": "0013_fix_brand_casing", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/vehicles/vehicles.service.ts b/apps/api/src/vehicles/vehicles.service.ts index fd2cd63..7e5e3ee 100644 --- a/apps/api/src/vehicles/vehicles.service.ts +++ b/apps/api/src/vehicles/vehicles.service.ts @@ -193,16 +193,22 @@ export class VehiclesService { // 3. Brand access check let brandId: string | null = null; - const brandName = resolved.brandName; + // Match the brands table case-insensitively and adopt the CANONICAL name: + // decode sources emit the brand with inconsistent casing ("FORD" vs "Ford"), + // and a case-sensitive lookup left brand_id NULL + stored the raw uppercase + // string, splitting one brand across casing variants. See migration + // 0013_fix_brand_casing for the backfill of pre-existing rows. + let brandName = resolved.brandName; if (brandName) { const [brand] = await this.db .select() .from(brands) - .where(eq(brands.name, brandName)) + .where(sql`lower(${brands.name}) = lower(${brandName})`) .limit(1); if (brand) { brandId = brand.id; + brandName = brand.name; await this.checkBrandAccess(userId, brandId); } } From 07cf710bc357b9a922e821041593019268297b68 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Wed, 10 Jun 2026 00:03:05 +0300 Subject: [PATCH 16/35] fix(csp): allow Formbricks SDK in script-src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @formbricks/js is a loader shim that injects the real SDK as an external script from anket.sase.tr — connect-src alone wasn't enough; the browser blocked /js/formbricks.umd.cjs and no survey could render. Co-Authored-By: Claude Fable 5 --- apps/api/src/main.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 54c107e..5605074 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -30,6 +30,9 @@ async function bootstrap() { "https://connect.facebook.net", "https://challenges.cloudflare.com", "https://destek.sase.tr", + // @formbricks/js is only a loader shim — the real SDK is injected as + // an external