style: apply biome safe auto-fixes

Run 'biome check --fix' on apps/api/src and apps/web/src to clear
the safe-fixable lint backlog (151 files: parseInt → Number.parseInt,
isNaN → Number.isNaN, organize imports, etc.). 769 errors remain
that require manual changes (mostly noExplicitAny).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-05-09 16:13:56 +00:00
parent ca2798b5d1
commit 3184e4c619
181 changed files with 3739 additions and 3713 deletions

View File

@@ -15,9 +15,7 @@ interface DailyChartProps {
}
export function DailyChart({ dailyStats, isLoading }: DailyChartProps) {
const maxCount = dailyStats
? Math.max(...dailyStats.map((d) => d.count), 1)
: 1;
const maxCount = dailyStats ? Math.max(...dailyStats.map((d) => d.count), 1) : 1;
return (
<Card>
@@ -31,21 +29,16 @@ export function DailyChart({ dailyStats, isLoading }: DailyChartProps) {
{isLoading ? (
<Skeleton className="h-64 w-full" />
) : !dailyStats || dailyStats.length === 0 ? (
<p className="py-12 text-center text-muted-foreground">
Henuz veri bulunmuyor
</p>
<p className="py-12 text-center text-muted-foreground">Henuz veri bulunmuyor</p>
) : (
<div className="flex items-end gap-1 overflow-x-auto pb-2" style={{ height: 256 }}>
{dailyStats.map((day) => {
const heightPercent = (day.count / maxCount) * 100;
const successPercent =
day.count > 0
? (day.successCount / day.count) * 100
: 0;
const dateLabel = new Date(day.date).toLocaleDateString(
"tr-TR",
{ day: "2-digit", month: "2-digit" },
);
const successPercent = day.count > 0 ? (day.successCount / day.count) * 100 : 0;
const dateLabel = new Date(day.date).toLocaleDateString("tr-TR", {
day: "2-digit",
month: "2-digit",
});
return (
<div
key={day.date}
@@ -75,9 +68,7 @@ export function DailyChart({ dailyStats, isLoading }: DailyChartProps) {
/>
</div>
{/* Date label */}
<span className="mt-1 text-[9px] text-muted-foreground">
{dateLabel}
</span>
<span className="mt-1 text-[9px] text-muted-foreground">{dateLabel}</span>
</div>
);
})}

View File

@@ -1,9 +1,9 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Button, Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { useState } from "react";
interface VariantItem {
code: string;
@@ -159,11 +159,7 @@ export function FordVariantSelector({ vehicleId, onSelect }: FordVariantSelector
{/* Proceed button — only enabled when all required dimensions are selected */}
<div className="flex gap-2">
<Button
onClick={handleProceed}
disabled={!canProceed}
className="w-full sm:w-auto"
>
<Button onClick={handleProceed} disabled={!canProceed} className="w-full sm:w-auto">
{t("catalog.fordVariant.proceed")}
</Button>
</div>

View File

@@ -1,9 +1,9 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { Link, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Car, ChevronRight, Loader2 } from "lucide-react";
import { cn } from "@sase/ui";
import { api } from "@/lib/api-client";
import { getCategoryIcon } from "@/lib/category-icons";
import { cn } from "@sase/ui";
import { Link, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Car, ChevronRight, Loader2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
/* ── Types ── */
@@ -56,9 +56,7 @@ export function ModelListColumns({
vehicle: m,
}));
const [columns, setColumns] = useState<Column[]>([
{ type: "models", items: modelItems },
]);
const [columns, setColumns] = useState<Column[]>([{ type: "models", items: modelItems }]);
// Reset when models change
useEffect(() => {
@@ -132,10 +130,7 @@ export function ModelListColumns({
path: o.path,
isFinal: data.isFinal,
}));
setColumns((prev) => [
...prev.slice(0, colIdx + 1),
{ type: "restrictions", items },
]);
setColumns((prev) => [...prev.slice(0, colIdx + 1), { type: "restrictions", items }]);
} else {
// Fetch categories directly
await fetchCategories(vehicle.id, colIdx, undefined);
@@ -174,10 +169,7 @@ export function ModelListColumns({
path: o.path,
isFinal: data.isFinal,
}));
setColumns((prev) => [
...prev.slice(0, colIdx + 1),
{ type: "restrictions", items },
]);
setColumns((prev) => [...prev.slice(0, colIdx + 1), { type: "restrictions", items }]);
}
} catch {
// On error, do nothing
@@ -235,10 +227,7 @@ export function ModelListColumns({
isLeaf: c.children !== undefined && c.children.length === 0,
children: c.children,
}));
setColumns((prev) => [
...prev.slice(0, colIdx + 1),
{ type: "categories", items },
]);
setColumns((prev) => [...prev.slice(0, colIdx + 1), { type: "categories", items }]);
return;
}
@@ -251,7 +240,7 @@ export function ModelListColumns({
navigate({
to: "/dashboard/catalog/$brandName/$modelId/categories/$categoryId",
params: { brandName, modelId: vehicleId, categoryId: item.id },
search: { body: undefined, engine: undefined, gearbox: undefined, mgp: undefined },
search: { body: undefined, engine: undefined, gearbox: undefined, mgp: undefined },
});
return;
}
@@ -262,10 +251,7 @@ export function ModelListColumns({
isLeaf: c.children !== undefined && c.children.length === 0,
children: c.children,
}));
setColumns((prev) => [
...prev.slice(0, colIdx + 1),
{ type: "categories", items },
]);
setColumns((prev) => [...prev.slice(0, colIdx + 1), { type: "categories", items }]);
} catch {
// On error, do nothing
} finally {
@@ -301,56 +287,54 @@ export function ModelListColumns({
className="flex border rounded-lg overflow-x-auto"
style={{ minHeight: 320 }}
>
{columns.map((col, colIdx) => (
<div
key={colIdx}
className={cn(
"w-[220px] shrink-0 overflow-y-auto",
colIdx < columns.length - 1 && "border-r",
)}
style={{ maxHeight: 480 }}
>
{col.items.length === 0 ? (
<div className="flex h-full items-center justify-center p-4 text-xs text-muted-foreground">
Sonuç yok
</div>
) : (
col.items.map((item) => {
const itemId = item.kind === "restriction" ? item.code : item.id;
const isSelected = col.selectedId === itemId;
const isLoading = loadingCol === colIdx && isSelected;
{columns.map((col, colIdx) => (
<div
key={colIdx}
className={cn(
"w-[220px] shrink-0 overflow-y-auto",
colIdx < columns.length - 1 && "border-r",
)}
style={{ maxHeight: 480 }}
>
{col.items.length === 0 ? (
<div className="flex h-full items-center justify-center p-4 text-xs text-muted-foreground">
Sonuç yok
</div>
) : (
col.items.map((item) => {
const itemId = item.kind === "restriction" ? item.code : item.id;
const isSelected = col.selectedId === itemId;
const isLoading = loadingCol === colIdx && isSelected;
return (
<button
key={itemId}
type="button"
onClick={() => handleItemClick(item, colIdx)}
className={cn(
"flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-accent text-accent-foreground font-medium",
)}
>
<ItemIcon item={item} />
<div className="flex-1 min-w-0">
<p className="truncate">
{item.kind === "model" ? item.label : item.name}
</p>
{item.kind === "model" && item.sublabel && (
<p className="text-xs text-muted-foreground truncate">{item.sublabel}</p>
return (
<button
key={itemId}
type="button"
onClick={() => handleItemClick(item, colIdx)}
className={cn(
"flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-accent text-accent-foreground font-medium",
)}
</div>
{isLoading ? (
<Loader2 className="size-3.5 shrink-0 animate-spin" />
) : item.kind === "category" && item.isLeaf ? null : (
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
)}
</button>
);
})
)}
</div>
))}
>
<ItemIcon item={item} />
<div className="flex-1 min-w-0">
<p className="truncate">{item.kind === "model" ? item.label : item.name}</p>
{item.kind === "model" && item.sublabel && (
<p className="text-xs text-muted-foreground truncate">{item.sublabel}</p>
)}
</div>
{isLoading ? (
<Loader2 className="size-3.5 shrink-0 animate-spin" />
) : item.kind === "category" && item.isLeaf ? null : (
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
)}
</button>
);
})
)}
</div>
))}
</div>
</div>
);

View File

@@ -1,8 +1,8 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { ArrowLeft, Loader2 } from "lucide-react";
import { useState } from "react";
interface RestrictionOption {
code: string;
@@ -29,9 +29,7 @@ export function P5RestrictionSelector({ vehicleId, onComplete }: P5RestrictionSe
const { data, isLoading } = useQuery<P5RestrictionsResponse>({
queryKey: ["p5-restrictions", vehicleId, currentPath ?? "initial"],
queryFn: () => {
const pathParam = currentPath
? `?path=${encodeURIComponent(currentPath)}`
: "";
const pathParam = currentPath ? `?path=${encodeURIComponent(currentPath)}` : "";
return api.get<P5RestrictionsResponse>(
`/catalog/vehicles/${vehicleId}/p5-restrictions${pathParam}`,
);

View File

@@ -1,9 +1,9 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Button, Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { useState } from "react";
interface VariantItem {
code: string;

View File

@@ -1,10 +1,10 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useQueryClient } from "@tanstack/react-query";
import { ChevronRight, Loader2 } from "lucide-react";
import { cn } from "@sase/ui";
import { api } from "@/lib/api-client";
import { getCategoryIcon } from "@/lib/category-icons";
import { cn } from "@sase/ui";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { ChevronRight, Loader2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
interface Category {
id: string;
@@ -102,9 +102,7 @@ export function CategoryColumns({
);
if (!categories || categories.length === 0) {
return (
<p className="py-4 text-center text-sm text-muted-foreground">Kategori bulunamadi.</p>
);
return <p className="py-4 text-center text-sm text-muted-foreground">Kategori bulunamadi.</p>;
}
return (
@@ -212,10 +210,7 @@ function ColumnPanel({
return (
<div
className={cn(
"w-[220px] shrink-0 overflow-y-auto",
!isLast && "border-r",
)}
className={cn("w-[220px] shrink-0 overflow-y-auto", !isLast && "border-r")}
style={{ maxHeight: 420 }}
>
{categories.map((category) => {

View File

@@ -1,9 +1,9 @@
import { useState, useEffect, useRef } from "react";
import { Link } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import { Card, CardContent } from "@sase/ui";
import { api } from "@/lib/api-client";
import { getCategoryIcon } from "@/lib/category-icons";
import { Card, CardContent } from "@sase/ui";
import { Link } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import { useEffect, useRef, useState } from "react";
interface Category {
id: string;
@@ -25,7 +25,14 @@ interface CategoryGridProps {
variantSearch?: { body?: string; engine?: string; gearbox?: string };
}
export function CategoryGrid({ categories, vehicleId, catalogMode, brandName, parentId, variantSearch }: CategoryGridProps) {
export function CategoryGrid({
categories,
vehicleId,
catalogMode,
brandName,
parentId,
variantSearch,
}: CategoryGridProps) {
const [prefetchingIds, setPrefetchingIds] = useState<Set<string>>(new Set());
const [imageOverrides, setImageOverrides] = useState<Map<string, string>>(new Map());
const prefetchedRef = useRef<Set<string>>(new Set());
@@ -66,9 +73,7 @@ export function CategoryGrid({ categories, vehicleId, catalogMode, brandName, pa
if (!didCancel && parentId) {
try {
const refreshed = await api.get<Category[]>(
`/categories/${parentId}/children`,
);
const refreshed = await api.get<Category[]>(`/categories/${parentId}/children`);
if (!didCancel && refreshed?.length) {
setImageOverrides((prev) => {
const next = new Map(prev);
@@ -90,30 +95,29 @@ export function CategoryGrid({ categories, vehicleId, catalogMode, brandName, pa
}, [categories, vehicleId]);
if (!categories || categories.length === 0) {
return (
<p className="py-4 text-center text-sm text-muted-foreground">
Kategori bulunamadi.
</p>
);
return <p className="py-4 text-center text-sm text-muted-foreground">Kategori bulunamadi.</p>;
}
return (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{categories.map((category) => {
const Icon = getCategoryIcon(category.name);
const isLeaf =
category.children !== undefined && category.children.length === 0;
const isLeaf = category.children !== undefined && category.children.length === 0;
const schemaImageUrl = imageOverrides.get(category.id) || category.schemaImageUrl;
return (
<Link
key={category.id}
to={catalogMode
? "/dashboard/catalog/$brandName/$modelId/categories/$categoryId"
: "/dashboard/vehicles/$id/categories/$categoryId"}
params={catalogMode
? { brandName: brandName ?? vehicleId, modelId: vehicleId, categoryId: category.id }
: { id: vehicleId, categoryId: category.id }}
to={
catalogMode
? "/dashboard/catalog/$brandName/$modelId/categories/$categoryId"
: "/dashboard/vehicles/$id/categories/$categoryId"
}
params={
catalogMode
? { brandName: brandName ?? vehicleId, modelId: vehicleId, categoryId: category.id }
: { id: vehicleId, categoryId: category.id }
}
search={catalogMode && variantSearch ? variantSearch : undefined}
className={category.unavailable ? "opacity-40" : undefined}
>
@@ -171,9 +175,7 @@ function CategoryCard({
<div className="p-3 flex items-center justify-between gap-2">
<span className="font-medium text-sm line-clamp-2">{name}</span>
{partCount != null && partCount > 0 && (
<span className="text-xs text-muted-foreground flex-shrink-0">
{partCount} parça
</span>
<span className="text-xs text-muted-foreground flex-shrink-0">{partCount} parça</span>
)}
</div>
</CardContent>

View File

@@ -1,10 +1,10 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { Link } from "@tanstack/react-router";
import { useQueryClient } from "@tanstack/react-query";
import { ChevronRight, ChevronDown, Loader2 } from "lucide-react";
import { cn } from "@sase/ui";
import { api } from "@/lib/api-client";
import { getCategoryIcon } from "@/lib/category-icons";
import { cn } from "@sase/ui";
import { useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
interface Category {
id: string;
@@ -51,9 +51,22 @@ export function CategoryTree({
);
}
function CategoryNode({ category, vehicleId, level, parentPrefetching, catalogMode, brandName, variantSearch }: {
category: Category; vehicleId: string; level: number; parentPrefetching: boolean;
catalogMode?: boolean; brandName?: string; variantSearch?: { body?: string; engine?: string; gearbox?: string };
function CategoryNode({
category,
vehicleId,
level,
parentPrefetching,
catalogMode,
brandName,
variantSearch,
}: {
category: Category;
vehicleId: string;
level: number;
parentPrefetching: boolean;
catalogMode?: boolean;
brandName?: string;
variantSearch?: { body?: string; engine?: string; gearbox?: string };
}) {
const queryClient = useQueryClient();
const [expanded, setExpanded] = useState(false);
@@ -67,8 +80,14 @@ function CategoryNode({ category, vehicleId, level, parentPrefetching, catalogMo
const isLeaf = fetched && children.length === 0;
const handleExpand = useCallback(async () => {
if (expanded) { setExpanded(false); return; }
if (fetched) { setExpanded(true); return; }
if (expanded) {
setExpanded(false);
return;
}
if (fetched) {
setExpanded(true);
return;
}
setLoading(true);
try {
const data = await queryClient.fetchQuery({
@@ -91,7 +110,11 @@ function CategoryNode({ category, vehicleId, level, parentPrefetching, catalogMo
useEffect(() => {
if (!expanded || prefetchedRef.current) return;
const leafs = children.filter(
(c) => c.children !== undefined && c.children.length === 0 && !c.schemaImageUrl && c.source !== "parts-catalogs",
(c) =>
c.children !== undefined &&
c.children.length === 0 &&
!c.schemaImageUrl &&
c.source !== "parts-catalogs",
);
if (leafs.length === 0) return;
prefetchedRef.current = true;
@@ -113,17 +136,21 @@ function CategoryNode({ category, vehicleId, level, parentPrefetching, catalogMo
try {
const refreshed = await api.get<Category[]>(`/categories/${parentId}/children`);
if (!cancelled && refreshed?.length) {
setChildren((prev) => prev.map((c) => {
const u = refreshed.find((r) => r.id === c.id);
return u?.schemaImageUrl ? { ...c, schemaImageUrl: u.schemaImageUrl } : c;
}));
setChildren((prev) =>
prev.map((c) => {
const u = refreshed.find((r) => r.id === c.id);
return u?.schemaImageUrl ? { ...c, schemaImageUrl: u.schemaImageUrl } : c;
}),
);
}
} catch {}
}
}
if (!cancelled) setPrefetching(false);
})();
return () => { cancelled = true; };
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [expanded, vehicleId]);
@@ -146,27 +173,47 @@ function CategoryNode({ category, vehicleId, level, parentPrefetching, catalogMo
) : isLeaf ? (
<span className="h-5 w-5" />
) : (
<button type="button" onClick={handleExpand}
className="flex h-5 w-5 items-center justify-center rounded hover:bg-muted">
{expanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<button
type="button"
onClick={handleExpand}
className="flex h-5 w-5 items-center justify-center rounded hover:bg-muted"
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</button>
)}
<SchemaIcon Icon={Icon} schemaImageUrl={category.schemaImageUrl} name={category.name} shimmer={isShimmering} />
<SchemaIcon
Icon={Icon}
schemaImageUrl={category.schemaImageUrl}
name={category.name}
shimmer={isShimmering}
/>
{isLeaf ? (
<Link
to={catalogMode
? "/dashboard/catalog/$brandName/$modelId/categories/$categoryId"
: "/dashboard/vehicles/$id/categories/$categoryId"}
params={catalogMode
? { brandName: brandName ?? vehicleId, modelId: vehicleId, categoryId: category.id }
: { id: vehicleId, categoryId: category.id }}
to={
catalogMode
? "/dashboard/catalog/$brandName/$modelId/categories/$categoryId"
: "/dashboard/vehicles/$id/categories/$categoryId"
}
params={
catalogMode
? { brandName: brandName ?? vehicleId, modelId: vehicleId, categoryId: category.id }
: { id: vehicleId, categoryId: category.id }
}
search={catalogMode && variantSearch ? variantSearch : undefined}
className="flex-1 truncate hover:underline">
className="flex-1 truncate hover:underline"
>
{category.name}
</Link>
) : (
<button type="button" onClick={handleExpand}
className="flex-1 truncate text-left hover:underline">
<button
type="button"
onClick={handleExpand}
className="flex-1 truncate text-left hover:underline"
>
{category.name}
</button>
)}
@@ -177,9 +224,16 @@ function CategoryNode({ category, vehicleId, level, parentPrefetching, catalogMo
{hasChildren && expanded && (
<div>
{children.map((child) => (
<CategoryNode key={child.id} category={child} vehicleId={vehicleId}
level={level + 1} parentPrefetching={prefetching}
catalogMode={catalogMode} brandName={brandName} variantSearch={variantSearch} />
<CategoryNode
key={child.id}
category={child}
vehicleId={vehicleId}
level={level + 1}
parentPrefetching={prefetching}
catalogMode={catalogMode}
brandName={brandName}
variantSearch={variantSearch}
/>
))}
</div>
)}
@@ -187,7 +241,12 @@ function CategoryNode({ category, vehicleId, level, parentPrefetching, catalogMo
);
}
function SchemaIcon({ Icon, schemaImageUrl, name, shimmer }: {
function SchemaIcon({
Icon,
schemaImageUrl,
name,
shimmer,
}: {
Icon: React.ComponentType<{ className?: string }>;
schemaImageUrl?: string | null;
name: string;
@@ -208,8 +267,11 @@ function SchemaIcon({ Icon, schemaImageUrl, name, shimmer }: {
}
return (
<div className="relative flex-shrink-0"
onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
<div
className="relative flex-shrink-0"
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
>
<Icon className="h-4 w-4 text-primary cursor-pointer" />
{show && (
<div className="absolute left-0 bottom-full mb-2 z-50 w-56 rounded-lg border bg-card shadow-lg overflow-hidden pointer-events-none">

View File

@@ -1,7 +1,8 @@
import { api } from "@/lib/api-client";
import { startAction } from "@/lib/faro";
import { capture } from "@/lib/posthog";
import { useTranslation } from "@/lib/i18n";
import { capture } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Button } from "@sase/ui";
@@ -22,7 +23,6 @@ import {
Upload,
} from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { toast } from "@/lib/toast";
interface Brand {
id: string;
@@ -342,11 +342,19 @@ export function PaymentContent({ planKey, period, brandIds }: PaymentContentProp
onValueChange={(v) => setPaymentMethod(v as "iyzico" | "eft")}
>
<TabsList className="w-full">
<TabsTrigger value="iyzico" data-faro-user-action-name="payment-tab-card" className="flex-1">
<TabsTrigger
value="iyzico"
data-faro-user-action-name="payment-tab-card"
className="flex-1"
>
<CreditCard className="mr-2 h-4 w-4" />
{t("payment.creditCard")}
</TabsTrigger>
<TabsTrigger value="eft" data-faro-user-action-name="payment-tab-eft" className="flex-1">
<TabsTrigger
value="eft"
data-faro-user-action-name="payment-tab-eft"
className="flex-1"
>
<Building2 className="mr-2 h-4 w-4" />
{t("payment.eftTransfer")}
</TabsTrigger>

View File

@@ -1,6 +1,6 @@
import { useSyncExternalStore } from "react";
import { useSchemaStore } from "@/stores/schema.store";
import type { Hotspot } from "@/hooks/use-parts";
import { useSchemaStore } from "@/stores/schema.store";
import { useSyncExternalStore } from "react";
function useIsDark() {
return useSyncExternalStore(
@@ -41,12 +41,16 @@ function HotspotShape({
? "#ef4444"
: isHighlighted
? "#60a5fa"
: isDark ? "#a5b4fc" : "#6b7280";
: isDark
? "#a5b4fc"
: "#6b7280";
const fillColor = isSelected
? "#ef4444"
: isHighlighted
? "#60a5fa"
: isDark ? "#818cf8" : "#9ca3af";
: isDark
? "#818cf8"
: "#9ca3af";
const strokeWidth = isSelected || isHighlighted ? 2.5 : isDark ? 2 : 1.5;
const commonProps = {
@@ -85,11 +89,7 @@ function HotspotShape({
return null;
}
export function HotspotOverlay({
hotspots,
imageWidth,
imageHeight,
}: HotspotOverlayProps) {
export function HotspotOverlay({ hotspots, imageWidth, imageHeight }: HotspotOverlayProps) {
const { highlightedGroup, selectedGroup, setHighlightedGroup, setSelectedGroup } =
useSchemaStore();
const isDark = useIsDark();
@@ -119,7 +119,11 @@ export function HotspotOverlay({
: hotspot.coordinates[1] - 4;
return (
<g key={hotspot.id} style={{ pointerEvents: "auto" }} data-faro-user-action-name="hotspot-click">
<g
key={hotspot.id}
style={{ pointerEvents: "auto" }}
data-faro-user-action-name="hotspot-click"
>
<HotspotShape
hotspot={hotspot}
isHighlighted={isHighlighted}
@@ -128,9 +132,7 @@ export function HotspotOverlay({
onMouseEnter={() => setHighlightedGroup(hotspot.group)}
onMouseLeave={() => setHighlightedGroup(null)}
onClick={() =>
setSelectedGroup(
selectedGroup === hotspot.group ? null : hotspot.group,
)
setSelectedGroup(selectedGroup === hotspot.group ? null : hotspot.group)
}
/>
{(isHighlighted || isSelected) && hotspot.label && (
@@ -140,7 +142,13 @@ export function HotspotOverlay({
y={labelY}
textAnchor="middle"
className="pointer-events-none select-none text-xs font-medium"
style={{ fontSize: 12, stroke: isDark ? "#000" : "#fff", strokeWidth: 3, strokeLinejoin: "round", fill: isDark ? "#000" : "#fff" }}
style={{
fontSize: 12,
stroke: isDark ? "#000" : "#fff",
strokeWidth: 3,
strokeLinejoin: "round",
fill: isDark ? "#000" : "#fff",
}}
>
{hotspot.label}
</text>

View File

@@ -1,10 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Check, Copy } from "lucide-react";
import { useSchemaStore } from "@/stores/schema.store";
import { cn } from "@sase/ui";
import type { Part } from "@/hooks/use-parts";
import { api } from "@/lib/api-client";
import { capture } from "@/lib/posthog";
import type { Part } from "@/hooks/use-parts";
import { useSchemaStore } from "@/stores/schema.store";
import { cn } from "@sase/ui";
import { Check, Copy } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
interface PartsPanelProps {
parts: Part[];
@@ -33,28 +33,31 @@ export function PartsPanel({ parts, vehicleId, categoryId }: PartsPanelProps) {
return map;
}, [parts]);
const copyOemCode = useCallback((e: React.MouseEvent, partId: string, code: string) => {
e.stopPropagation();
navigator.clipboard.writeText(code);
setCopiedId(partId);
setTimeout(() => setCopiedId(null), 1500);
const copyOemCode = useCallback(
(e: React.MouseEvent, partId: string, code: string) => {
e.stopPropagation();
navigator.clipboard.writeText(code);
setCopiedId(partId);
setTimeout(() => setCopiedId(null), 1500);
api
.post("/analytics/oem-copy", {
oemCode: code,
partId,
vehicleId,
categoryId,
})
.catch(() => {});
api
.post("/analytics/oem-copy", {
oemCode: code,
partId,
vehicleId,
categoryId,
})
.catch(() => {});
capture("oem_code_copied", {
oem_code: code,
part_id: partId,
vehicle_id: vehicleId,
category_id: categoryId,
});
}, [vehicleId, categoryId]);
capture("oem_code_copied", {
oem_code: code,
part_id: partId,
vehicle_id: vehicleId,
category_id: categoryId,
});
},
[vehicleId, categoryId],
);
useEffect(() => {
if (selectedGroup != null) {
@@ -69,9 +72,7 @@ export function PartsPanel({ parts, vehicleId, categoryId }: PartsPanelProps) {
<div className="flex h-full flex-col overflow-hidden">
<div className="border-b border-border px-4 py-3">
<h3 className="text-sm font-semibold">Parcalar</h3>
<p className="text-xs text-muted-foreground">
{parts.length} parca listeleniyor
</p>
<p className="text-xs text-muted-foreground">{parts.length} parca listeleniyor</p>
</div>
<div className="flex-1 overflow-y-auto">
@@ -112,22 +113,15 @@ export function PartsPanel({ parts, vehicleId, categoryId }: PartsPanelProps) {
className={cn(
"cursor-pointer border-b border-border/50 transition-colors duration-150",
part.unavailable && "opacity-40",
isSelected &&
"bg-primary/10 ring-1 ring-inset ring-primary/20",
isSelected && "bg-primary/10 ring-1 ring-inset ring-primary/20",
isHighlighted && !isSelected && "bg-accent",
!isSelected && !isHighlighted && "hover:bg-accent/50",
)}
onMouseEnter={() => setHighlightedGroup(group)}
onMouseLeave={() => setHighlightedGroup(null)}
onClick={() =>
setSelectedGroup(
selectedGroup === group ? null : group,
)
}
onClick={() => setSelectedGroup(selectedGroup === group ? null : group)}
>
<td className="px-3 py-2 text-muted-foreground">
{part.hotspotIndex}
</td>
<td className="px-3 py-2 text-muted-foreground">{part.hotspotIndex}</td>
<td className="px-3 py-2">
<span className="font-medium">{part.name}</span>
{(part.remark || part.modelCodes) && (
@@ -155,9 +149,7 @@ export function PartsPanel({ parts, vehicleId, categoryId }: PartsPanelProps) {
</span>
</td>
<td className="px-3 py-2 text-center">{part.quantity}</td>
<td className="px-3 py-2 text-muted-foreground">
{part.position}
</td>
<td className="px-3 py-2 text-muted-foreground">{part.position}</td>
{hasPrices && (
<td className="px-3 py-2 text-right text-xs">
{part.price != null

View File

@@ -1,10 +1,9 @@
import { Button } from "@sase/ui";
import { ZoomIn, ZoomOut, RotateCcw, Maximize, Minimize } from "lucide-react";
import { useSchemaStore } from "@/stores/schema.store";
import { Button } from "@sase/ui";
import { Maximize, Minimize, RotateCcw, ZoomIn, ZoomOut } from "lucide-react";
export function SchemaToolbar() {
const { zoom, isFullscreen, setZoom, resetView, toggleFullscreen } =
useSchemaStore();
const { zoom, isFullscreen, setZoom, resetView, toggleFullscreen } = useSchemaStore();
return (
<div className="flex items-center gap-1 rounded-lg border border-border bg-background/95 p-1 shadow-sm backdrop-blur-sm">
@@ -53,11 +52,7 @@ export function SchemaToolbar() {
onClick={toggleFullscreen}
title={isFullscreen ? "Tam ekrandan çık" : "Tam ekran"}
>
{isFullscreen ? (
<Minimize className="h-4 w-4" />
) : (
<Maximize className="h-4 w-4" />
)}
{isFullscreen ? <Minimize className="h-4 w-4" /> : <Maximize className="h-4 w-4" />}
</Button>
</div>
);

View File

@@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef } from "react";
import { useSchemaStore } from "@/stores/schema.store";
import type { Hotspot, Part, SchemaPic } from "@/hooks/use-parts";
import { useSchemaInteraction } from "@/hooks/use-schema-interaction";
import { SchemaToolbar } from "./schema-toolbar";
import { HotspotOverlay } from "./hotspot-overlay";
import { PartsPanel } from "./parts-panel";
import { useSchemaStore } from "@/stores/schema.store";
import { Skeleton } from "@sase/ui";
import { cn } from "@sase/ui";
import type { Part, Hotspot, SchemaPic } from "@/hooks/use-parts";
import { useCallback, useEffect, useRef } from "react";
import { HotspotOverlay } from "./hotspot-overlay";
import { PartsPanel } from "./parts-panel";
import { SchemaToolbar } from "./schema-toolbar";
interface SchemaViewerProps {
schemaPic: SchemaPic | null;

View File

@@ -2,6 +2,8 @@ import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { signIn } from "@/lib/auth-client";
import { useTranslation } from "@/lib/i18n";
import { capture } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
@@ -21,8 +23,6 @@ import {
import { useQuery } from "@tanstack/react-query";
import { AlertTriangle, Copy, Gift, Link2, Share2, Shield, Trash2, User } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "@/lib/toast";
import { capture } from "@/lib/posthog";
export function SettingsContent() {
const { t } = useTranslation();

View File

@@ -1,3 +1,4 @@
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Card, CardContent } from "@sase/ui";
@@ -7,7 +8,6 @@ import { cn } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Check } from "lucide-react";
import { useEffect, useState } from "react";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
interface Brand {
id: string;
@@ -112,7 +112,12 @@ export function BrandSelector({
>
<CardContent className="flex flex-col items-center justify-center p-4">
<div className="relative">
<CarBrandLogo brandName={brand.name} logoUrl={brand.logoUrl} size={48} className="mb-2" />
<CarBrandLogo
brandName={brand.name}
logoUrl={brand.logoUrl}
size={48}
className="mb-2"
/>
{isSelected && (
<div className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check className="h-3 w-3" />

View File

@@ -1,13 +1,12 @@
import type { SVGProps } from "react";
import {
AlfaRomeoLogo,
AlfaRomeoLogoDark,
AudiLogo,
AudiLogoDark,
BentleyLogo,
BentleyLogoDark,
BMWLogo,
BMWLogoDark,
BentleyLogo,
BentleyLogoDark,
FiatLogo,
FiatLogoDark,
FordLogo,
@@ -28,10 +27,10 @@ import {
LandroverLogoDark,
LexusLogo,
LexusLogoDark,
MazdaLogo,
MazdaLogoDark,
MBLogo,
MBLogoDark,
MazdaLogo,
MazdaLogoDark,
MiniLogo,
MiniLogoDark,
MitsubishiLogo,
@@ -52,6 +51,7 @@ import {
VolvoLogoDark,
} from "@cardog-icons/react";
import { cn } from "@sase/ui";
import type { SVGProps } from "react";
// Static SVG asset imports (brands not in @cardog-icons/react)
import alpineUrl from "@/assets/brand-logos/alpine.svg";
@@ -122,10 +122,10 @@ const STATIC_LOGO_MAP: Record<string, StaticEntry> = {
dacia: { url: daciaUrl, invertDark: true },
peugeot: { url: peugeotUrl, invertDark: true },
citroen: { url: citroenUrl, invertDark: true },
"citroën": { url: citroenUrl, invertDark: true },
citroën: { url: citroenUrl, invertDark: true },
man: { url: manUrl, invertDark: true },
skoda: { url: skodaUrl, invertDark: false },
koda": { url: skodaUrl, invertDark: false },
škoda: { url: skodaUrl, invertDark: false },
seat: { url: seatUrl, invertDark: false },
suzuki: { url: suzukiUrl, invertDark: false },
};

View File

@@ -1,6 +1,6 @@
import { Link } from "@tanstack/react-router";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Link } from "@tanstack/react-router";
interface VehicleCardProps {
id: string;

View File

@@ -1,15 +1,15 @@
import { useState } from "react";
import {
Badge,
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
Button,
Badge,
Separator,
} from "@sase/ui";
import { Car, Loader2, ChevronRight } from "lucide-react";
import { Car, ChevronRight, Loader2 } from "lucide-react";
import { useState } from "react";
interface PcatCandidate {
id: string;
@@ -32,7 +32,9 @@ function getDifferingKeys(candidates: PcatCandidate[]): Set<string> {
const diffKeys = new Set<string>();
const allKeys = new Set(candidates.flatMap((c) => (c.parameters ?? []).map((p) => p.key)));
for (const key of allKeys) {
const values = new Set(candidates.map((c) => c.parameters?.find((p) => p.key === key)?.value ?? null));
const values = new Set(
candidates.map((c) => c.parameters?.find((p) => p.key === key)?.value ?? null),
);
if (values.size > 1) diffKeys.add(key);
}
return diffKeys;
@@ -53,12 +55,10 @@ export function VehicleSelectModal({
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle className="font-[family-name:var(--font-display)]">
Araç Seçimi
</DialogTitle>
<DialogTitle className="font-[family-name:var(--font-display)]">Araç Seçimi</DialogTitle>
<DialogDescription>
<span className="font-mono text-xs">{vin}</span> için birden fazla
araç bulundu. Lütfen aracınızı seçin.
<span className="font-mono text-xs">{vin}</span> için birden fazla araç bulundu. Lütfen
aracınızı seçin.
</DialogDescription>
</DialogHeader>
@@ -86,9 +86,7 @@ export function VehicleSelectModal({
>
<div
className={`flex size-10 shrink-0 items-center justify-center rounded-xl ${
isSelected
? "bg-primary/10 text-primary"
: "bg-muted text-muted-foreground"
isSelected ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"
}`}
>
<Car className="size-5" />
@@ -96,9 +94,7 @@ export function VehicleSelectModal({
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{car.name}</p>
{car.description && (
<p className="truncate text-xs text-muted-foreground">
{car.description}
</p>
<p className="truncate text-xs text-muted-foreground">{car.description}</p>
)}
<div className="mt-1.5 flex flex-wrap gap-1.5">
{sortedParams.map((p) => {
@@ -138,9 +134,7 @@ export function VehicleSelectModal({
onClick={() => selectedId && onSelect(selectedId)}
className="rounded-xl"
>
{loading ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : null}
{loading ? <Loader2 className="mr-2 size-4 animate-spin" /> : null}
Seç ve Devam Et
</Button>
</div>

View File

@@ -1,7 +1,7 @@
import { useState } from "react";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Search } from "lucide-react";
import { useState } from "react";
const VIN_REGEX = /^[A-HJ-NPR-Z0-9]{17}$/;
function isValidVin(vin: string): boolean {
@@ -39,11 +39,7 @@ export function VinInput({ onSubmit, loading, error }: VinInputProps) {
className="font-mono text-lg tracking-wider"
/>
<Button type="submit" disabled={loading || vin.length !== 17 || isInvalid}>
{loading ? (
<span className="animate-spin">...</span>
) : (
<Search className="h-4 w-4" />
)}
{loading ? <span className="animate-spin">...</span> : <Search className="h-4 w-4" />}
Ara
</Button>
</form>

View File

@@ -64,11 +64,15 @@
font-feature-settings: "cv11", "ss01", "ss03";
}
/* Headlines: tighter tracking, balanced wraps (no orphans) */
h1, h2, h3 {
h1,
h2,
h3 {
text-wrap: balance;
letter-spacing: -0.025em;
}
h4, h5, p {
h4,
h5,
p {
text-wrap: pretty;
}
/* Numbers in data contexts use tabular alignment */
@@ -98,45 +102,83 @@
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes scroll-left {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-scroll-left { animation: scroll-left 30s linear infinite; }
.animate-float { animation: float 3s ease-in-out infinite; }
.animate-fade-in-up { animation: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1); }
.animate-fade-in { animation: fade-in 0.4s ease-out; }
.carousel-track:hover .animate-scroll-left { animation-play-state: paused; }
.scrollbar-none::-webkit-scrollbar { display: none; }
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
.animate-scroll-left {
animation: scroll-left 30s linear infinite;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-fade-in-up {
animation: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-fade-in {
animation: fade-in 0.4s ease-out;
}
.carousel-track:hover .animate-scroll-left {
animation-play-state: paused;
}
.scrollbar-none::-webkit-scrollbar {
display: none;
}
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Sileo toast: deeper state colors for light mode */
:root {
--sileo-state-success: oklch(0.56 0.13 158);
--sileo-state-error: oklch(0.48 0.26 25);
--sileo-state-warning: oklch(0.58 0.2 70);
--sileo-state-info: oklch(0.50 0.2 237);
--sileo-state-loading: oklch(0.40 0 0);
--sileo-state-info: oklch(0.5 0.2 237);
--sileo-state-loading: oklch(0.4 0 0);
}
[data-sileo-description] {
@@ -158,7 +200,7 @@
--color-secondary-foreground: oklch(97% 0.004 80);
--color-accent: oklch(20% 0.008 250);
--color-accent-foreground: oklch(97% 0.004 80);
--color-destructive: oklch(54% 0.20 28);
--color-destructive: oklch(54% 0.2 28);
--color-destructive-foreground: oklch(97% 0.004 80);
--color-surface: oklch(17% 0.008 250);
--color-surface-foreground: oklch(97% 0.004 80);

View File

@@ -1,4 +1,4 @@
import { useSession, signIn, signUp, signOut } from "@/lib/auth-client";
import { signIn, signOut, signUp, useSession } from "@/lib/auth-client";
import { useAuthStore } from "@/stores/auth.store";
import { useEffect } from "react";

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { useQuery } from "@tanstack/react-query";
export interface Part {
id: string;
@@ -59,11 +59,7 @@ export interface CategorySchema {
export function useCategoryParts(vehicleId: string, categoryId: string) {
return useQuery<CategorySchema>({
queryKey: ["category-parts", vehicleId, categoryId],
queryFn: () =>
api.get<CategorySchema>(
`/vehicles/${vehicleId}/categories/${categoryId}`,
),
queryFn: () => api.get<CategorySchema>(`/vehicles/${vehicleId}/categories/${categoryId}`),
enabled: !!vehicleId && !!categoryId,
});
}

View File

@@ -1,5 +1,5 @@
import { useCallback, useRef } from "react";
import { useSchemaStore } from "@/stores/schema.store";
import { useCallback, useRef } from "react";
function getDistance(t1: React.Touch, t2: React.Touch): number {
const dx = t1.clientX - t2.clientX;
@@ -24,14 +24,11 @@ export function useSchemaInteraction() {
[zoom, setZoom],
);
const onMouseDown = useCallback(
(e: React.MouseEvent) => {
if (e.button !== 0) return;
isDragging.current = true;
lastMousePos.current = { x: e.clientX, y: e.clientY };
},
[],
);
const onMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return;
isDragging.current = true;
lastMousePos.current = { x: e.clientX, y: e.clientY };
}, []);
const onMouseMove = useCallback(
(e: React.MouseEvent) => {
@@ -48,25 +45,22 @@ export function useSchemaInteraction() {
isDragging.current = false;
}, []);
const onTouchStart = useCallback(
(e: React.TouchEvent) => {
if (e.touches.length === 2) {
const dist = getDistance(e.touches[0], e.touches[1]);
lastTouchDistance.current = dist;
lastTouchCenter.current = {
x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
y: (e.touches[0].clientY + e.touches[1].clientY) / 2,
};
} else if (e.touches.length === 1) {
isDragging.current = true;
lastMousePos.current = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
}
},
[],
);
const onTouchStart = useCallback((e: React.TouchEvent) => {
if (e.touches.length === 2) {
const dist = getDistance(e.touches[0], e.touches[1]);
lastTouchDistance.current = dist;
lastTouchCenter.current = {
x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
y: (e.touches[0].clientY + e.touches[1].clientY) / 2,
};
} else if (e.touches.length === 1) {
isDragging.current = true;
lastMousePos.current = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
}
}, []);
const onTouchMove = useCallback(
(e: React.TouchEvent) => {

View File

@@ -1,18 +1,18 @@
import {
Cog,
Settings,
CircleDot,
Shield,
Zap,
Droplets,
Wind,
Car,
Footprints,
Wrench,
Package,
FlaskConical,
CircleDot,
Cog,
Droplets,
FileText,
FlaskConical,
Footprints,
type LucideIcon,
Package,
Settings,
Shield,
Wind,
Wrench,
Zap,
} from "lucide-react";
const iconRules: [RegExp, LucideIcon][] = [

View File

@@ -43,17 +43,11 @@ export function getFaro() {
}
/** Start a programmatic user action (auto-completes 100ms after last linked event) */
export function startAction(
name: string,
attributes?: Record<string, string>,
) {
export function startAction(name: string, attributes?: Record<string, string>) {
faro?.api.startUserAction(name, attributes);
}
/** Push a Faro event (for non-action tracking like page-level events) */
export function pushEvent(
name: string,
attributes?: Record<string, string>,
) {
export function pushEvent(name: string, attributes?: Record<string, string>) {
faro?.api.pushEvent(name, attributes);
}

View File

@@ -61,9 +61,7 @@ export function capture(event: string, properties?: Record<string, unknown>): vo
}
export function capturePageView(path: string): void {
load().then((ph) =>
ph.capture("$pageview", { $current_url: window.location.origin + path }),
);
load().then((ph) => ph.capture("$pageview", { $current_url: window.location.origin + path }));
}
export function setPeopleProperties(properties: Record<string, unknown>): void {

View File

@@ -14,12 +14,8 @@ interface ToastOptions {
}
export const toast = {
success: (title: string, opts?: ToastOptions) =>
sileo.success({ title, ...opts }),
error: (title: string, opts?: ToastOptions) =>
sileo.error({ title, ...opts }),
info: (title: string, opts?: ToastOptions) =>
sileo.info({ title, ...opts }),
warning: (title: string, opts?: ToastOptions) =>
sileo.warning({ title, ...opts }),
success: (title: string, opts?: ToastOptions) => sileo.success({ title, ...opts }),
error: (title: string, opts?: ToastOptions) => sileo.error({ title, ...opts }),
info: (title: string, opts?: ToastOptions) => sileo.info({ title, ...opts }),
warning: (title: string, opts?: ToastOptions) => sileo.warning({ title, ...opts }),
};

View File

@@ -22,10 +22,7 @@ export function getUserSettings(): UserSettings {
}
}
export function setUserSetting<K extends keyof UserSettings>(
key: K,
value: UserSettings[K],
) {
export function setUserSetting<K extends keyof UserSettings>(key: K, value: UserSettings[K]) {
const settings = getUserSettings();
settings[key] = value;
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));

View File

@@ -1,9 +1,9 @@
import { initFaro } from "./lib/faro";
import { initPostHog } from "./lib/posthog";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { initFaro } from "./lib/faro";
import { initPostHog } from "./lib/posthog";
import { routeTree } from "./routeTree.gen";
import "./globals.css";

View File

@@ -1,10 +1,10 @@
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
Sequence,
interpolate,
spring,
Sequence,
useCurrentFrame,
useVideoConfig,
} from "remotion";
function getColors(isDark: boolean) {
@@ -21,13 +21,7 @@ function getColors(isDark: boolean) {
}
const VIN_TEXT = "WVWZZZ1JZ3W...";
const SIDEBAR_ITEMS = [
"Dashboard",
"Şase Arama",
"Katalog",
"Şemalar",
"Ayarlar",
];
const SIDEBAR_ITEMS = ["Dashboard", "Şase Arama", "Katalog", "Şemalar", "Ayarlar"];
const CATEGORIES = ["Motor", "Şasi", "Elektrik", "Karoseri", "Fren"];
const PARTS = [
{ code: "1J0 820 803F", name: "Klima Kompresörü" },
@@ -52,10 +46,7 @@ const VinInputScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
// Typewriter effect starts at frame 30
const typeStart = 30;
const charsToShow = Math.min(
Math.max(0, Math.floor((frame - typeStart) / 3)),
VIN_TEXT.length,
);
const charsToShow = Math.min(Math.max(0, Math.floor((frame - typeStart) / 3)), VIN_TEXT.length);
const typedText = VIN_TEXT.slice(0, charsToShow);
return (
@@ -212,11 +203,7 @@ const VehicleInfoScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<span
style={{ fontFamily: "monospace", fontSize: 11, color: c.fg }}
>
{VIN_TEXT}
</span>
<span style={{ fontFamily: "monospace", fontSize: 11, color: c.fg }}>{VIN_TEXT}</span>
</div>
{/* Vehicle card */}
@@ -353,14 +340,9 @@ const CategoryScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
borderRadius: 8,
fontSize: 11,
fontFamily: "system-ui, sans-serif",
color:
highlightProgress > 0.5 ? c.fg : c.mutedFg,
color: highlightProgress > 0.5 ? c.fg : c.mutedFg,
backgroundColor:
highlightProgress > 0.5
? isDark
? "#064e3b"
: "#d1fae5"
: c.muted,
highlightProgress > 0.5 ? (isDark ? "#064e3b" : "#d1fae5") : c.muted,
border: `1px solid ${highlightProgress > 0.5 ? c.emerald : c.border}`,
opacity: itemOpacity,
transform: `translateX(${translateX}px)`,

View File

@@ -1,10 +1,10 @@
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
Sequence,
interpolate,
spring,
Sequence,
useCurrentFrame,
useVideoConfig,
} from "remotion";
function getColors(isDark: boolean) {
@@ -50,9 +50,7 @@ const StorefrontScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
const badgeScale = interpolate(badgeSpring, [0, 1], [0, 1]);
return (
<AbsoluteFill
style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}
>
<AbsoluteFill style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}>
{/* Header */}
<div
style={{
@@ -284,9 +282,7 @@ const WidgetIntegrationScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
});
return (
<AbsoluteFill
style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}
>
<AbsoluteFill style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}>
{/* Header */}
<div
style={{
@@ -518,10 +514,7 @@ const VinFilterScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
const c = getColors(isDark);
// Typewriter (1 char / 3 frames)
const charsToShow = Math.min(
Math.max(0, Math.floor(frame / 3)),
VIN_DISPLAY.length,
);
const charsToShow = Math.min(Math.max(0, Math.floor(frame / 3)), VIN_DISPLAY.length);
const typedVin = VIN_DISPLAY.slice(0, charsToShow);
// Filter starts at ~frame 45
@@ -533,9 +526,7 @@ const VinFilterScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
const incompatibleIndices = [2, 4, 5];
return (
<AbsoluteFill
style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}
>
<AbsoluteFill style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}>
{/* Header */}
<div
style={{
@@ -721,13 +712,8 @@ const VinFilterScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
>
{PRODUCTS.map((product, i) => {
const isIncompatible = incompatibleIndices.includes(i);
const cardOpacity = isIncompatible
? interpolate(filterProgress, [0, 1], [1, 0.15])
: 1;
const borderColor =
!isIncompatible && filterProgress > 0.5
? c.emerald
: c.border;
const cardOpacity = isIncompatible ? interpolate(filterProgress, [0, 1], [1, 0.15]) : 1;
const borderColor = !isIncompatible && filterProgress > 0.5 ? c.emerald : c.border;
return (
<div
@@ -826,9 +812,7 @@ const AddToCartScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
// Click animation at frame 30
const clickScale =
frame >= 30 && frame <= 38
? interpolate(frame, [30, 34, 38], [1, 0.95, 1])
: 1;
frame >= 30 && frame <= 38 ? interpolate(frame, [30, 34, 38], [1, 0.95, 1]) : 1;
// Notification slides down at frame 40
const notifSpring = spring({
@@ -857,9 +841,7 @@ const AddToCartScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
const incompatibleIndices = [2, 4, 5];
return (
<AbsoluteFill
style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}
>
<AbsoluteFill style={{ backgroundColor: c.bg, flexDirection: "column", padding: 0 }}>
{/* Header */}
<div
style={{
@@ -954,9 +936,7 @@ const AddToCartScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
display: "flex",
alignItems: "center",
justifyContent: "center",
transform: showCartBadge
? `scale(${cartScale})`
: "scale(1)",
transform: showCartBadge ? `scale(${cartScale})` : "scale(1)",
}}
>
{showCartBadge ? "1" : "0"}

View File

@@ -1,10 +1,4 @@
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
interpolate,
spring,
} from "remotion";
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
function getColors(isDark: boolean) {
return {

View File

@@ -1,10 +1,10 @@
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
Sequence,
interpolate,
spring,
Sequence,
useCurrentFrame,
useVideoConfig,
} from "remotion";
const PARTS = ["P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9"];

View File

@@ -1,12 +1,12 @@
import { createRootRouteWithContext, Link, Outlet, useLocation } from "@tanstack/react-router";
import { Toaster } from "@/lib/toast";
import type { QueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { getUserSettings } from "@/lib/user-settings";
import { capturePageView, identifyUser, resetUser } from "@/lib/posthog";
import { useAuth } from "@/hooks/use-auth";
import { capturePageView, identifyUser, resetUser } from "@/lib/posthog";
import { Toaster } from "@/lib/toast";
import { getUserSettings } from "@/lib/user-settings";
import { Button } from "@sase/ui";
import type { QueryClient } from "@tanstack/react-query";
import { Link, Outlet, createRootRouteWithContext, useLocation } from "@tanstack/react-router";
import { ArrowLeft, Home, Search } from "lucide-react";
import { useEffect } from "react";
interface RouterContext {
queryClient: QueryClient;
@@ -34,8 +34,8 @@ function NotFoundComponent() {
<span className="text-muted-foreground">yanlış adres.</span>
</h1>
<p className="mx-auto mt-6 max-w-md text-base text-muted-foreground">
Aradığın sayfa silinmiş ya da hiç olmamış olabilir. Aşağıdan ana sayfaya
dönebilir veya doğrudan şase aramaya gidebilirsin.
Aradığın sayfa silinmiş ya da hiç olmamış olabilir. Aşağıdan ana sayfaya dönebilir veya
doğrudan şase aramaya gidebilirsin.
</p>
<div className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row">
@@ -82,8 +82,7 @@ function NotFoundComponent() {
function applyTheme(theme: "light" | "dark" | "system") {
const isDark =
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
(theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", isDark);
}

View File

@@ -1,4 +1,4 @@
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
import { Link, Outlet, createFileRoute } from "@tanstack/react-router";
import { Database, ShieldCheck, Zap } from "lucide-react";
export const Route = createFileRoute("/_auth")({
@@ -55,9 +55,8 @@ function AuthLayout() {
<span className="text-foreground/60">ilk seferde bulun.</span>
</h2>
<p className="text-base leading-relaxed text-muted-foreground">
Birden fazla katalogda çapraz sorgulama ile her zaman en güncel OEM
kodları. Şase numarasını girin, doğru parçayı saniyeler içinde
bulun.
Birden fazla katalogda çapraz sorgulama ile her zaman en güncel OEM kodları. Şase
numarasını girin, doğru parçayı saniyeler içinde bulun.
</p>
</div>
@@ -95,16 +94,15 @@ function AuthLayout() {
{/* Brand row */}
<p className="text-sm text-muted-foreground">
BMW · Mercedes-Benz · Audi · VW · Fiat · Renault · Toyota · Honda ·
Hyundai · Ford · Opel · Skoda
BMW · Mercedes-Benz · Audi · VW · Fiat · Renault · Toyota · Honda · Hyundai · Ford ·
Opel · Skoda
</p>
{/* Testimonial */}
<div className="rounded-2xl border border-border bg-surface/40 p-6 backdrop-blur-sm">
<p className="text-sm leading-relaxed text-foreground/85">
&ldquo;Sase.tr&apos;ye geçtiğimizden beri yanlış parça
siparişlerimiz neredeyse sıfıra indi. Aylık 40 saatin üzerinde
zaman tasarrufu sağlıyoruz.&rdquo;
&ldquo;Sase.tr&apos;ye geçtiğimizden beri yanlış parça siparişlerimiz neredeyse sıfıra
indi. Aylık 40 saatin üzerinde zaman tasarrufu sağlıyoruz.&rdquo;
</p>
<div className="mt-4 flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-full bg-muted text-sm font-medium">
@@ -112,9 +110,7 @@ function AuthLayout() {
</div>
<div>
<p className="text-sm font-medium">Mehmet K.</p>
<p className="text-xs text-muted-foreground">
Yedek Parça İşletme Sahibi
</p>
<p className="text-xs text-muted-foreground">Yedek Parça İşletme Sahibi</p>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
import { useState } from "react";
import { Link, createFileRoute } from "@tanstack/react-router";
import { toast } from "@/lib/toast";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Label } from "@sase/ui";
import { toast } from "@/lib/toast";
import { Link, createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/_auth/forgot-password")({
component: ForgotPasswordPage,
@@ -44,12 +44,9 @@ function ForgotPasswordPage() {
return (
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
E-posta Gönderildi
</h1>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">E-posta Gönderildi</h1>
<p className="mt-2 text-sm text-muted-foreground">
Şifre sıfırlama bağlantısı {email} adresine gönderildi. Lütfen
e-postanızı kontrol edin.
Şifre sıfırlama bağlantısı {email} adresine gönderildi. Lütfen e-postanızı kontrol edin.
</p>
</div>
<Link to="/login">
@@ -65,9 +62,7 @@ function ForgotPasswordPage() {
<div className="space-y-8">
{/* Heading */}
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
Şifremi Unuttum
</h1>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">Şifremi Unuttum</h1>
<p className="mt-2 text-sm text-muted-foreground">
E-posta adresinize şifre sıfırlama bağlantısı göndereceğiz
</p>
@@ -92,10 +87,7 @@ function ForgotPasswordPage() {
</form>
<p className="text-center text-sm">
<Link
to="/login"
className="text-muted-foreground hover:underline"
>
<Link to="/login" className="text-muted-foreground hover:underline">
Giriş Sayfasına Dön
</Link>
</p>

View File

@@ -1,13 +1,13 @@
import { useState } from "react";
import { Link, createFileRoute } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Label } from "@sase/ui";
import { api } from "@/lib/api-client";
import { signIn } from "@/lib/auth-client";
import { startAction } from "@/lib/faro";
import { capture } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Label } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/_auth/login")({
component: LoginPage,
@@ -50,9 +50,7 @@ function LoginPage() {
<div className="space-y-8">
{/* Heading */}
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
Hesabınıza Giriş Yapın
</h1>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">Hesabınıza Giriş Yapın</h1>
<p className="mt-2 text-sm text-muted-foreground">
Şase çözme ve parça kataloğuna erişmek için giriş yapın
</p>
@@ -69,10 +67,22 @@ function LoginPage() {
}}
>
<svg className="mr-2 h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
Google ile Giriş Yap
</Button>
@@ -83,9 +93,7 @@ function LoginPage() {
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
veya
</span>
<span className="bg-background px-2 text-muted-foreground">veya</span>
</div>
</div>
@@ -116,16 +124,10 @@ function LoginPage() {
{/* Remember me + Forgot password */}
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
className="size-4 rounded border-input accent-primary"
/>
<input type="checkbox" className="size-4 rounded border-input accent-primary" />
Beni hatırla
</label>
<Link
to="/forgot-password"
className="text-sm text-muted-foreground hover:underline"
>
<Link to="/forgot-password" className="text-sm text-muted-foreground hover:underline">
Şifremi Unuttum
</Link>
</div>

View File

@@ -1,14 +1,14 @@
import { useState } from "react";
import { Link, createFileRoute } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Label } from "@sase/ui";
import { api } from "@/lib/api-client";
import { signIn, signUp } from "@/lib/auth-client";
import { startAction } from "@/lib/faro";
import { capture } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { api } from "@/lib/api-client";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Label } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ShieldCheck } from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/_auth/register")({
component: RegisterPage,
@@ -59,12 +59,8 @@ function RegisterPage() {
<div className="space-y-8">
{/* Heading */}
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
Kayıt Ol
</h1>
<p className="mt-2 text-sm text-muted-foreground">
Yeni bir Sase.tr hesabı oluşturun
</p>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">Kayıt Ol</h1>
<p className="mt-2 text-sm text-muted-foreground">Yeni bir Sase.tr hesabı oluşturun</p>
{/* Trial messaging */}
<div className="mt-3 flex items-center gap-2 rounded-lg border border-brand/20 bg-brand/10 px-3 py-2 text-sm text-foreground">
@@ -84,10 +80,22 @@ function RegisterPage() {
}}
>
<svg className="mr-2 h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
Google ile Kayıt Ol
</Button>
@@ -98,9 +106,7 @@ function RegisterPage() {
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
veya
</span>
<span className="bg-background px-2 text-muted-foreground">veya</span>
</div>
</div>

View File

@@ -1,9 +1,9 @@
import { useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { toast } from "@/lib/toast";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Label } from "@sase/ui";
import { toast } from "@/lib/toast";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/_auth/reset-password")({
validateSearch: (search: Record<string, unknown>) => ({
@@ -31,9 +31,7 @@ function ResetPasswordPage() {
toast.success("Şifreniz başarıyla güncellendi.");
navigate({ to: "/login" });
} catch {
toast.error(
"Şifre sıfırlama başarısız. Bağlantı süresi dolmuş olabilir.",
);
toast.error("Şifre sıfırlama başarısız. Bağlantı süresi dolmuş olabilir.");
} finally {
setLoading(false);
}
@@ -43,12 +41,8 @@ function ResetPasswordPage() {
<div className="space-y-8">
{/* Heading */}
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
Şifre Sıfırla
</h1>
<p className="mt-2 text-sm text-muted-foreground">
Yeni şifrenizi belirleyin
</p>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">Şifre Sıfırla</h1>
<p className="mt-2 text-sm text-muted-foreground">Yeni şifrenizi belirleyin</p>
</div>
{/* Form */}

View File

@@ -1,6 +1,6 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Button } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/about")({
component: AboutPage,
@@ -37,47 +37,44 @@ function AboutPage() {
<div className="mt-8 space-y-6 text-muted-foreground leading-relaxed">
<p>
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.
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.
</p>
<h2 className="text-2xl font-semibold text-foreground">Misyonumuz</h2>
<p>
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.
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.
</p>
<h2 className="text-2xl font-semibold text-foreground">Ne Yapıyoruz?</h2>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Şase Çözme:</strong> VIN numarası ile aracın marka,
model, yıl, motor tipi ve donanım bilgilerine anında erişim.
<strong>Şase Çözme:</strong> VIN numarası ile aracın marka, model, yıl, motor tipi ve
donanım bilgilerine anında erişim.
</li>
<li>
<strong>Orijinal Parça Kataloğu:</strong> OEM bazlı parça
numaraları, ıklamalar ve çapraz referanslar.
<strong>Orijinal Parça Kataloğu:</strong> OEM bazlı parça numaraları, ıklamalar ve
çapraz referanslar.
</li>
<li>
<strong>İnteraktif Şema:</strong> Araç şemaları üzerinden
rsel parça seçimi ve detay rüntüleme.
<strong>İnteraktif Şema:</strong> Araç şemaları üzerinden görsel parça seçimi ve detay
görüntüleme.
</li>
<li>
<strong>Çoklu Marka Desteği:</strong> Volkswagen, Audi, BMW,
Mercedes-Benz, Ford ve daha fazlası.
<strong>Çoklu Marka Desteği:</strong> Volkswagen, Audi, BMW, Mercedes-Benz, Ford ve
daha fazlası.
</li>
</ul>
<h2 className="text-2xl font-semibold text-foreground">Neden Sase.tr?</h2>
<p>
Geleneksel yöntemlerle saatler süren parça arama işlemini
dakikalara indiriyoruz. Güncel ve doğrulanmış verilerle yanlış
parça siparişinin önüne geçiyoruz. Kullanıcı dostu arayüzümüz
sayesinde teknik bilgi seviyesinden bağımsız olarak herkes
kolayca kullanabilir.
Geleneksel yöntemlerle saatler süren parça arama işlemini dakikalara indiriyoruz. Güncel
ve doğrulanmış verilerle yanlış parça siparişinin önüne geçiyoruz. Kullanıcı dostu
arayüzümüz sayesinde teknik bilgi seviyesinden bağımsız olarak herkes kolayca
kullanabilir.
</p>
<h2 className="text-2xl font-semibold text-foreground">İletişim</h2>

View File

@@ -1,7 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@sase/ui";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Button } from "@sase/ui";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/blog")({
component: BlogPage,
@@ -78,9 +78,7 @@ function BlogPage() {
<CardTitle className="text-lg">{post.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{post.description}
</p>
<p className="text-sm text-muted-foreground">{post.description}</p>
</CardContent>
</Card>
</Link>

View File

@@ -1,7 +1,7 @@
import { createFileRoute, Link, notFound } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { ChevronRight } from "lucide-react";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Button } from "@sase/ui";
import { Link, createFileRoute, notFound } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
// ─── BLOG POST DATA ───────────────────────────────────────────────────────────
@@ -23,33 +23,40 @@ const POSTS: Record<string, BlogPost> = {
content: (
<div className="space-y-6 leading-relaxed text-muted-foreground">
<p>
VIN (Vehicle Identification Number), yani Araç Tanımlama Numarası, her motorlu taşıta üretim
aşamasında atanan 17 karakterlik benzersiz bir koddur. Türkiye'de "şase numarası" olarak da
bilinen bu kod, aracın üretiminden imhasına kadar tüm yaşam döngüsünü takip etmeye yarar.
VIN (Vehicle Identification Number), yani Araç Tanımlama Numarası, her motorlu taşıta
üretim aşamasında atanan 17 karakterlik benzersiz bir koddur. Türkiye'de "şase numarası"
olarak da bilinen bu kod, aracın üretiminden imhasına kadar tüm yaşam döngüsünü takip
etmeye yarar.
</p>
<h2 className="text-xl font-semibold text-foreground">VIN Nereden Okunur?</h2>
<p>
VIN numarasına birçok yerden ulaşabilirsiniz: ön camın sol alt köşesindeki plaka, sürücü
kapısının iç kısmındaki etiket, motor bölmesi veya araç ruhsatı ve fatura bunların
başında gelir. Bazı araçlarda bagaj kapısı iç kısmında da bulunur.
kapısının iç kısmındaki etiket, motor bölmesi veya araç ruhsatı ve fatura bunların başında
gelir. Bazı araçlarda bagaj kapısı iç kısmında da bulunur.
</p>
<h2 className="text-xl font-semibold text-foreground">17 Karakterin Anlamı</h2>
<p>VIN üç ana bölüme ayrılır:</p>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong className="text-foreground">WMI (1-3. karakterler) — Dünya Üretici Kodu:</strong>{" "}
<strong className="text-foreground">
WMI (1-3. karakterler) — Dünya Üretici Kodu:
</strong>{" "}
Aracın hangi ülkede ve hangi fabrikada üretildiğini gösterir. Örneğin "WVW" Volkswagen
Almanya, "ZFA" Fiat İtalya demektir.
</li>
<li>
<strong className="text-foreground">VDS (4-9. karakterler) — Araç Tanımlayıcı Bölüm:</strong>{" "}
<strong className="text-foreground">
VDS (4-9. karakterler) — Araç Tanımlayıcı Bölüm:
</strong>{" "}
Model, kasa tipi, motor hacmi, yakıt türü ve güvenlik donanımları hakkında bilgi içerir.
9. karakter her zaman kontrol karakteridir ve matematiksel bir doğrulama amacı taşır.
</li>
<li>
<strong className="text-foreground">VIS (10-17. karakterler) — Araç Tanımlama Bölümü:</strong>{" "}
<strong className="text-foreground">
VIS (10-17. karakterler) — Araç Tanımlama Bölümü:
</strong>{" "}
Model yılı (10. karakter), üretim fabrikası (11. karakter) ve üretim sıra numarası
(12-17. karakterler) bilgilerini içerir.
</li>
@@ -57,9 +64,9 @@ const POSTS: Record<string, BlogPost> = {
<h2 className="text-xl font-semibold text-foreground">Örnek: WVWZZZ1JZ3W597935</h2>
<p>
Bu VIN'i inceleyelim: <strong className="font-mono text-foreground">WVW</strong> Volkswagen
Almanya, <strong className="font-mono text-foreground">ZZZ</strong> pazar tanımlayıcı,{" "}
<strong className="font-mono text-foreground">1J</strong> Golf modeli,{" "}
Bu VIN'i inceleyelim: <strong className="font-mono text-foreground">WVW</strong>
Volkswagen Almanya, <strong className="font-mono text-foreground">ZZZ</strong> pazar
tanımlayıcı, <strong className="font-mono text-foreground">1J</strong> Golf modeli,{" "}
<strong className="font-mono text-foreground">Z</strong> motor tipi,{" "}
<strong className="font-mono text-foreground">3</strong> model yılı 2003,{" "}
<strong className="font-mono text-foreground">W</strong> Wolfsburg fabrikası,{" "}
@@ -109,10 +116,10 @@ const POSTS: Record<string, BlogPost> = {
<h2 className="text-xl font-semibold text-foreground">OEM Parça Nedir?</h2>
<p>
OEM (Original Equipment Manufacturer), aracın üretiminde kullanılan ya da araç üreticisinin
onayladığı orijinal parçalardır. Bu parçalar araç fabrikasında kullanılan parçalarla aynı
spesifikasyonlara sahiptir ve genellikle aynı tedarikçilerden gelir. Üzerinde araç markasının
logosu bulunabilir ya da yalnızca parça numarasıyla satılabilir.
OEM (Original Equipment Manufacturer), aracın üretiminde kullanılan ya da araç
üreticisinin onayladığı orijinal parçalardır. Bu parçalar araç fabrikasında kullanılan
parçalarla aynı spesifikasyonlara sahiptir ve genellikle aynı tedarikçilerden gelir.
Üzerinde araç markasının logosu bulunabilir ya da yalnızca parça numarasıyla satılabilir.
</p>
<h2 className="text-xl font-semibold text-foreground">Aftermarket (Muadil) Parça Nedir?</h2>
@@ -154,7 +161,9 @@ const POSTS: Record<string, BlogPost> = {
<li>Güvenlik sistemlerini olumsuz etkileyebilir</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Hangi Durumlarda Orijinal, Hangisinde Muadil?</h2>
<h2 className="text-xl font-semibold text-foreground">
Hangi Durumlarda Orijinal, Hangisinde Muadil?
</h2>
<p>
Fren sistemi, hava yastığı, direksiyon ve motor parçaları gibi güvenlik kritik
bileşenlerde kesinlikle OEM tercih edilmelidir. Kaporta, döşeme veya aksesuar
@@ -190,12 +199,12 @@ const POSTS: Record<string, BlogPost> = {
<h2 className="text-xl font-semibold text-foreground">Geleneksel Yöntemlerin Sorunları</h2>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong className="text-foreground">Zaman kaybı:</strong> Tek bir parça için birden fazla
katalog taramak ortalama 15-20 dakika sürebilir.
<strong className="text-foreground">Zaman kaybı:</strong> Tek bir parça için birden
fazla katalog taramak ortalama 15-20 dakika sürebilir.
</li>
<li>
<strong className="text-foreground">Hata oranı:</strong> Elle yapılan arama ve karşılaştırma
işlemlerinde yanlış parça sipariş riski yüksektir.
<strong className="text-foreground">Hata oranı:</strong> Elle yapılan arama ve
karşılaştırma işlemlerinde yanlış parça sipariş riski yüksektir.
</li>
<li>
<strong className="text-foreground">Güncellik sorunu:</strong> Basılı kataloglar yeni
@@ -208,9 +217,7 @@ const POSTS: Record<string, BlogPost> = {
</ul>
<h2 className="text-xl font-semibold text-foreground">Dijital Dönüşümün Faydaları</h2>
<p>
Dijital platformlar, yedek parça arama sürecini kökten değiştirmektedir:
</p>
<p>Dijital platformlar, yedek parça arama sürecini kökten değiştirmektedir:</p>
<ul className="list-disc space-y-2 pl-6">
<li>VIN bazlı anlık araç tanımlama saniyeler içinde doğru araç tespiti</li>
<li>Çoklu katalog çapraz sorgulama tek arayüzde birden fazla kaynak</li>
@@ -220,9 +227,7 @@ const POSTS: Record<string, BlogPost> = {
</ul>
<h2 className="text-xl font-semibold text-foreground">Sektörde Sayısal Dönüşüm</h2>
<p>
Dijital platforma geçiş yapan işletmelerin deneyimlerine göre:
</p>
<p>Dijital platforma geçiş yapan işletmelerin deneyimlerine göre:</p>
<ul className="list-disc space-y-2 pl-6">
<li>Parça arama süresi ortalama %85 kısalmaktadır</li>
<li>Yanlış parça iade oranları %60-80 düşmektedir</li>
@@ -230,20 +235,22 @@ const POSTS: Record<string, BlogPost> = {
<li>Personel kapasitesi daha katma değerli işlere yönlendirilebilmektedir</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Küçük ve Orta Ölçekli İşletmeler İçin Fırsatlar</h2>
<h2 className="text-xl font-semibold text-foreground">
Küçük ve Orta Ölçekli İşletmeler İçin Fırsatlar
</h2>
<p>
Dijital dönüşüm artık yalnızca büyük zincirlerin ayrıcalığı değildir. Aylık sabit maliyetli
abonelik modelleri sayesinde küçük oto yedek parçacılar ve servisler de kurumsal araçlara
erişebilmektedir. Bu durum, rekabet eşitliğini kısmen sağlamaktadır.
Dijital dönüşüm artık yalnızca büyük zincirlerin ayrıcalığı değildir. Aylık sabit
maliyetli abonelik modelleri sayesinde küçük oto yedek parçacılar ve servisler de kurumsal
araçlara erişebilmektedir. Bu durum, rekabet eşitliğini kısmen sağlamaktadır.
</p>
<h2 className="text-xl font-semibold text-foreground">Sase.tr'nin Rolü</h2>
<p>
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
görüntüleme özellikleriyle geleneksel yapış biçimlerini dönüştürmektedir.
Platform, 27+ marka ve 243.000'den fazla OEM parça numarasıyla sektörün en kapsamlı
dijital kataloğunu sunmaktadır.
görüntüleme özellikleriyle geleneksel yapış biçimlerini dönüştürmektedir. Platform, 27+
marka ve 243.000'den fazla OEM parça numarasıyla sektörün en kapsamlı dijital kataloğunu
sunmaktadır.
</p>
</div>
),
@@ -258,8 +265,8 @@ const POSTS: Record<string, BlogPost> = {
content: (
<div className="space-y-6 leading-relaxed text-muted-foreground">
<p>
Yanlış parça sipariş etmek hem zaman hem para kaybettirdiği gibi, müşteri memnuniyetini
de olumsuz etkiler. Sase.tr, bu sorunu kökten çözmek için tasarlanmıştır. Bu rehberde
Yanlış parça sipariş etmek hem zaman hem para kaybettirdiği gibi, müşteri memnuniyetini de
olumsuz etkiler. Sase.tr, bu sorunu kökten çözmek için tasarlanmıştır. Bu rehberde
platformu nasıl en verimli şekilde kullanacağınızı adım adım anlatıyoruz.
</p>
@@ -276,32 +283,34 @@ const POSTS: Record<string, BlogPost> = {
<h2 className="text-xl font-semibold text-foreground">Adım 2: Kategori Seçin</h2>
<p>
Araç tanımlandıktan sonra karşınıza o araca özgü parça kategorileri çıkar. Motor,
Şasi & Süspansiyon, Elektrik, Karoseri, Klima & Isıtma gibi ana kategorilerden
ihtiyacınız olan bölümü seçin.
Araç tanımlandıktan sonra karşınıza o araca özgü parça kategorileri çıkar. Motor, Şasi &
Süspansiyon, Elektrik, Karoseri, Klima & Isıtma gibi ana kategorilerden ihtiyacınız olan
bölümü seçin.
</p>
<p>
Her ana kategorinin altında detaylı alt kategoriler bulunur. Örneğin "Motor" altında
Silindir Kapağı, Krank Mili, Piston, Yağ Pompası gibi bölümler yer alır.
</p>
<h2 className="text-xl font-semibold text-foreground">Adım 3: İnteraktif Şemayı Kullanın</h2>
<h2 className="text-xl font-semibold text-foreground">
Adım 3: İnteraktif Şemayı Kullanın
</h2>
<p>
Kategori seçildikten sonra o bölgеnin teknik çizimi interaktif şema ekranda ılır.
Şema üzerindeki parçalara tıklayarak OEM kodunu, ıklamasını ve gerekli miktarını
görebilirsiniz.
</p>
<p>
Şemalar zoom ve pan desteğiyle büyütülebilir, yatay veya dikey kaydırılabilir.
Karmaşık motor veya şasi bölgelerinde parça konumunu görsel olarak tespit etmek
iade oranlarını ciddi ölçüde düşürmektedir.
Şemalar zoom ve pan desteğiyle büyütülebilir, yatay veya dikey kaydırılabilir. Karmaşık
motor veya şasi bölgelerinde parça konumunu görsel olarak tespit etmek iade oranlarını
ciddi ölçüde düşürmektedir.
</p>
<h2 className="text-xl font-semibold text-foreground">Adım 4: OEM Kodunu Kopyalayın</h2>
<p>
Doğru parçayı bulduktan sonra OEM kodunu panoya kopyalayın. Bu kodu tedarikçinize,
servis faturanıza veya online sipariş formunuza yapıştırarak yanlış parça riskini
tamamen ortadan kaldırın.
Doğru parçayı bulduktan sonra OEM kodunu panoya kopyalayın. Bu kodu tedarikçinize, servis
faturanıza veya online sipariş formunuza yapıştırarak yanlış parça riskini tamamen ortadan
kaldırın.
</p>
<h2 className="text-xl font-semibold text-foreground">İpuçları</h2>
@@ -315,19 +324,17 @@ const POSTS: Record<string, BlogPost> = {
karşılaştırma özelliğini kullanın.
</li>
<li>
Parça bulamadığınızda kategori ağacında bir seviye yukarı çıkarak daha geniş
bir arama yapabilirsiniz.
</li>
<li>
27+ markanın tamamına erişmek için Full Paket aboneliği en avantajlı seçenektir.
Parça bulamadığınızda kategori ağacında bir seviye yukarı çıkarak daha geniş bir arama
yapabilirsiniz.
</li>
<li>27+ markanın tamamına erişmek için Full Paket aboneliği en avantajlı seçenektir.</li>
</ul>
<h2 className="text-xl font-semibold text-foreground">Sonuç</h2>
<p>
Sase.tr ile tek bir yanlış parça iadesinden tasarruf ettiğiniz para, aylık abonelik
ücretini karşılar. 30 günlük ücretsiz deneme süresiyle platformu bugün deneyin
kredi kartı gerektirmez.
ücretini karşılar. 30 günlük ücretsiz deneme süresiyle platformu bugün deneyin kredi
kartı gerektirmez.
</p>
</div>
),

View File

@@ -1,7 +1,7 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Button } from "@sase/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/contact")({
component: ContactPage,
@@ -35,8 +35,7 @@ function ContactPage() {
<main className="container mx-auto max-w-3xl px-4 py-16">
<h1 className="text-4xl font-bold">İletişim</h1>
<p className="mt-4 text-lg text-muted-foreground">
Sorularınız, önerileriniz veya birliği talepleriniz için bize
ulaşın.
Sorularınız, önerileriniz veya birliği talepleriniz için bize ulaşın.
</p>
<div className="mt-12 grid gap-6 sm:grid-cols-2">
@@ -45,10 +44,7 @@ function ContactPage() {
<CardTitle className="text-lg">E-posta</CardTitle>
</CardHeader>
<CardContent>
<a
href="mailto:info@sase.tr"
className="text-primary underline"
>
<a href="mailto:info@sase.tr" className="text-primary underline">
info@sase.tr
</a>
<p className="mt-2 text-sm text-muted-foreground">
@@ -62,10 +58,7 @@ function ContactPage() {
<CardTitle className="text-lg">Destek</CardTitle>
</CardHeader>
<CardContent>
<a
href="mailto:destek@sase.tr"
className="text-primary underline"
>
<a href="mailto:destek@sase.tr" className="text-primary underline">
destek@sase.tr
</a>
<p className="mt-2 text-sm text-muted-foreground">
@@ -79,9 +72,7 @@ function ContactPage() {
<CardTitle className="text-lg">Adres</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
İstanbul, Türkiye
</p>
<p className="text-muted-foreground">İstanbul, Türkiye</p>
<p className="mt-2 text-sm text-muted-foreground">
Çalışma saatleri: Pazartesi Cuma, 09:00 18:00
</p>

View File

@@ -1,36 +1,36 @@
import { createFileRoute, Outlet, Link, useNavigate } from "@tanstack/react-router";
import { useAuth } from "@/hooks/use-auth";
import { useTranslation } from "@/lib/i18n";
import { capture, resetUser } from "@/lib/posthog";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Separator, Skeleton } from "@sase/ui";
import { Link, Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
import {
Search,
History,
CreditCard,
Receipt,
Settings,
Shield,
Users,
ArrowRight,
BarChart3,
Bell,
BookOpen,
Copy,
CreditCard,
DollarSign,
Share2,
Menu,
X,
History,
LayoutDashboard,
Library,
LogOut,
Mail,
Menu,
Moon,
PanelLeftClose,
PanelLeftOpen,
LayoutDashboard,
Bell,
ArrowRight,
Mail,
BookOpen,
Receipt,
Search,
Settings,
Share2,
Shield,
Sun,
Moon,
Copy,
Library,
Users,
X,
} from "lucide-react";
import { useState } from "react";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { capture, resetUser } from "@/lib/posthog";
export const Route = createFileRoute("/dashboard")({
component: DashboardLayout,
@@ -46,7 +46,12 @@ const mainMenuItems = [
] as const;
const accountItems = [
{ to: "/dashboard/subscription", label: "nav.subscription", translatable: true, icon: CreditCard },
{
to: "/dashboard/subscription",
label: "nav.subscription",
translatable: true,
icon: CreditCard,
},
{ to: "/dashboard/billing", label: "nav.billing", translatable: true, icon: Receipt },
{ to: "/dashboard/settings", label: "nav.settings", translatable: true, icon: Settings },
] as const;
@@ -126,9 +131,7 @@ function DashboardLayout() {
const { user, isLoading, signOut, isAdmin } = useAuth();
const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = useState(false);
const [collapsed, setCollapsed] = useState(
() => getUserSettings().sidebarCollapsed ?? false,
);
const [collapsed, setCollapsed] = useState(() => getUserSettings().sidebarCollapsed ?? false);
const [isDark, setIsDark] = useState(() => {
const theme = getUserSettings().theme ?? "dark";
if (theme === "system") {
@@ -284,9 +287,7 @@ function DashboardLayout() {
</div>
{/* Navigation */}
<nav
className={`flex-1 space-y-0.5 overflow-y-auto ${collapsed ? "p-2" : "px-3 py-2"}`}
>
<nav className={`flex-1 space-y-0.5 overflow-y-auto ${collapsed ? "p-2" : "px-3 py-2"}`}>
<SidebarNav />
</nav>
@@ -295,7 +296,7 @@ function DashboardLayout() {
<button
type="button"
onClick={handleSignOut}
title={collapsed ? user.name ?? ıkış" : undefined}
title={collapsed ? (user.name ?? ıkış") : undefined}
className={`flex w-full items-center rounded-lg text-left transition-colors hover:bg-accent ${collapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2.5"}`}
>
<div className="flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
@@ -305,9 +306,7 @@ function DashboardLayout() {
<>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{user.name}</p>
<p className="truncate text-xs text-muted-foreground">
{user.email}
</p>
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
</div>
<LogOut className="size-4 shrink-0 text-muted-foreground" />
</>
@@ -337,9 +336,7 @@ function DashboardLayout() {
</div>
<div className="hidden sm:block">
<p className="text-sm font-semibold">{user.name}</p>
<p className="text-xs text-muted-foreground">
Sase.tr'ye hoş geldiniz 👋
</p>
<p className="text-xs text-muted-foreground">Sase.tr'ye hoş geldiniz 👋</p>
</div>
</div>
</div>
@@ -388,8 +385,7 @@ function DashboardLayout() {
{/* Footer */}
<div className="border-t border-border px-6 py-3">
<p className="text-center text-xs text-muted-foreground/60">
&copy; {new Date().getFullYear()} Sase.tr | Gizlilik Politikası,
Kullanım Koşulları
&copy; {new Date().getFullYear()} Sase.tr | Gizlilik Politikası, Kullanım Koşulları
</p>
</div>
</div>
@@ -407,11 +403,7 @@ function DashboardLayout() {
<aside className="absolute left-0 top-0 flex h-full w-64 flex-col bg-background shadow-lg">
<div className="flex h-16 items-center justify-between border-b border-border px-4">
<span className="text-xl font-bold">Sase.tr</span>
<Button
variant="ghost"
size="icon"
onClick={() => setMobileOpen(false)}
>
<Button variant="ghost" size="icon" onClick={() => setMobileOpen(false)}>
<X className="size-4" />
</Button>
</div>
@@ -433,9 +425,7 @@ function DashboardLayout() {
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{user.name}</p>
<p className="truncate text-xs text-muted-foreground">
{user.email}
</p>
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
</div>
<LogOut className="size-4 shrink-0 text-muted-foreground" />
</button>

View File

@@ -1,22 +1,14 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { useAuth } from "@/hooks/use-auth";
import { Card, CardContent } from "@sase/ui";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import {
ChevronLeft,
ChevronRight,
Search,
X,
CheckCircle,
XCircle,
Activity,
} from "lucide-react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Activity, CheckCircle, ChevronLeft, ChevronRight, Search, X, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/dashboard/admin/analytics")({
@@ -75,9 +67,7 @@ function AdminAnalyticsPage() {
params.set("page", String(page));
params.set("limit", String(limit));
if (debouncedUserId) params.set("userId", debouncedUserId);
return api.get<QueryLogResponse>(
`/admin/query-logs?${params.toString()}`,
);
return api.get<QueryLogResponse>(`/admin/query-logs?${params.toString()}`);
},
enabled: user?.role === "admin",
});
@@ -176,19 +166,13 @@ function AdminAnalyticsPage() {
>
<div className="truncate">
<p className="truncate font-medium">{log.userName}</p>
<p className="truncate text-xs text-muted-foreground">
{log.userEmail}
</p>
<p className="truncate text-xs text-muted-foreground">{log.userEmail}</p>
</div>
<div>
<code className="rounded bg-muted px-1 py-0.5 text-xs">
{log.vin}
</code>
<code className="rounded bg-muted px-1 py-0.5 text-xs">{log.vin}</code>
</div>
<div className="truncate">
<span className="text-sm">
{log.brandName || "-"}
</span>
<span className="text-sm">{log.brandName || "-"}</span>
</div>
<div className="text-center">
{log.success ? (
@@ -214,9 +198,7 @@ function AdminAnalyticsPage() {
<span className="text-muted-foreground">-</span>
)}
</div>
<div className="text-xs text-muted-foreground">
{formatDate(log.createdAt)}
</div>
<div className="text-xs text-muted-foreground">{formatDate(log.createdAt)}</div>
<div>
{log.errorMessage && (
<span

View File

@@ -1,6 +1,5 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { api } from "@/lib/api-client";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { Card, CardContent } from "@sase/ui";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
@@ -8,14 +7,8 @@ import { Badge } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { Tabs, TabsList, TabsTrigger } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import {
ChevronLeft,
ChevronRight,
Search,
X,
Copy,
TrendingUp,
} from "lucide-react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { ChevronLeft, ChevronRight, Copy, Search, TrendingUp, X } from "lucide-react";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/dashboard/admin/copy-logs")({
@@ -78,9 +71,7 @@ function AdminCopyLogsPage() {
params.set("page", String(page));
params.set("limit", String(limit));
if (debouncedUserId) params.set("userId", debouncedUserId);
return api.get<CopyLogResponse>(
`/admin/copy-logs?${params.toString()}`,
);
return api.get<CopyLogResponse>(`/admin/copy-logs?${params.toString()}`);
},
enabled: user?.role === "admin" && tab === "logs",
});
@@ -195,9 +186,7 @@ function AdminCopyLogsPage() {
>
<div className="truncate">
<p className="truncate font-medium">{log.userName}</p>
<p className="truncate text-xs text-muted-foreground">
{log.userEmail}
</p>
<p className="truncate text-xs text-muted-foreground">{log.userEmail}</p>
</div>
<div>
<code className="rounded bg-muted px-1.5 py-0.5 text-xs font-semibold">
@@ -298,12 +287,8 @@ function AdminCopyLogsPage() {
{item.oemCode}
</code>
</div>
<div className="text-center font-medium">
{item.copyCount}
</div>
<div className="text-center text-muted-foreground">
{item.uniqueUsers}
</div>
<div className="text-center font-medium">{item.copyCount}</div>
<div className="text-center text-muted-foreground">{item.uniqueUsers}</div>
</div>
))}
</div>

View File

@@ -1,24 +1,24 @@
import { lazy, Suspense } from "react";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { useAuth } from "@/hooks/use-auth";
import { Card, CardContent } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { Badge } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import {
Users,
CreditCard,
TrendingUp,
Search,
UserPlus,
Clock,
UserCog,
Receipt,
Activity,
Clock,
Copy,
CreditCard,
Receipt,
Search,
TrendingUp,
UserCog,
UserPlus,
Users,
} from "lucide-react";
import { Suspense, lazy } from "react";
import { useEffect } from "react";
const DailyChart = lazy(() =>
@@ -205,9 +205,7 @@ function AdminDashboardPage() {
<Icon className={`h-6 w-6 ${card.color}`} />
</div>
<div>
<p className="text-sm text-muted-foreground">
{card.label}
</p>
<p className="text-sm text-muted-foreground">{card.label}</p>
<p className="text-2xl font-bold">{card.value}</p>
</div>
</CardContent>

View File

@@ -1,19 +1,13 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { useAuth } from "@/hooks/use-auth";
import { Card, CardContent } from "@sase/ui";
import { Button } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
CheckCircle,
XCircle,
ExternalLink,
Receipt,
AlertTriangle,
} from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { AlertTriangle, CheckCircle, ExternalLink, Receipt, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/dashboard/admin/payments")({
@@ -57,8 +51,7 @@ function AdminPaymentsPage() {
});
const approveMutation = useMutation({
mutationFn: (paymentId: string) =>
api.patch(`/payments/eft/${paymentId}/approve`, {}),
mutationFn: (paymentId: string) => api.patch(`/payments/eft/${paymentId}/approve`, {}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["admin", "payments"] });
queryClient.invalidateQueries({ queryKey: ["admin", "dashboard"] });
@@ -67,8 +60,7 @@ function AdminPaymentsPage() {
});
const rejectMutation = useMutation({
mutationFn: (paymentId: string) =>
api.patch(`/payments/eft/${paymentId}/reject`, {}),
mutationFn: (paymentId: string) => api.patch(`/payments/eft/${paymentId}/reject`, {}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["admin", "payments"] });
queryClient.invalidateQueries({ queryKey: ["admin", "dashboard"] });
@@ -109,9 +101,7 @@ function AdminPaymentsPage() {
<div className="mx-auto max-w-5xl space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">EFT Odeme Onaylari</h2>
<Badge variant="secondary">
{payments?.length ?? 0} bekleyen
</Badge>
<Badge variant="secondary">{payments?.length ?? 0} bekleyen</Badge>
</div>
{isLoading ? (
@@ -124,12 +114,8 @@ function AdminPaymentsPage() {
<Card>
<CardContent className="flex flex-col items-center gap-3 py-16 text-center">
<CheckCircle className="h-12 w-12 text-green-500" />
<p className="text-lg font-medium">
Bekleyen odeme bulunmuyor
</p>
<p className="text-sm text-muted-foreground">
Tum EFT odemeleri islenmis durumda
</p>
<p className="text-lg font-medium">Bekleyen odeme bulunmuyor</p>
<p className="text-sm text-muted-foreground">Tum EFT odemeleri islenmis durumda</p>
</CardContent>
</Card>
) : (
@@ -141,9 +127,7 @@ function AdminPaymentsPage() {
{/* User Info */}
<div className="space-y-1">
<p className="font-medium">{payment.userName}</p>
<p className="text-sm text-muted-foreground">
{payment.userEmail}
</p>
<p className="text-sm text-muted-foreground">{payment.userEmail}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{formatDate(payment.createdAt)}</span>
<span>|</span>
@@ -153,9 +137,7 @@ function AdminPaymentsPage() {
{/* Amount & Receipt */}
<div className="flex flex-col items-end gap-2">
<p className="text-xl font-bold">
{formatCurrency(payment.amount)}
</p>
<p className="text-xl font-bold">{formatCurrency(payment.amount)}</p>
{payment.eftReceiptUrl ? (
<a
href={payment.eftReceiptUrl}
@@ -194,15 +176,8 @@ function AdminPaymentsPage() {
<div className="mt-3 flex items-center gap-2">
<Button
size="sm"
variant={
confirmAction.type === "approve"
? "default"
: "destructive"
}
disabled={
approveMutation.isPending ||
rejectMutation.isPending
}
variant={confirmAction.type === "approve" ? "default" : "destructive"}
disabled={approveMutation.isPending || rejectMutation.isPending}
onClick={() => {
if (confirmAction.type === "approve") {
approveMutation.mutate(payment.id);
@@ -211,8 +186,7 @@ function AdminPaymentsPage() {
}
}}
>
{approveMutation.isPending ||
rejectMutation.isPending
{approveMutation.isPending || rejectMutation.isPending
? "Isleniyor..."
: "Evet, onayla"}
</Button>

View File

@@ -1,19 +1,19 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { api } from "@/lib/api-client";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { Card, CardContent } from "@sase/ui";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import {
Search,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronDown,
ChevronUp,
Gift,
Search,
Users,
X,
} from "lucide-react";
@@ -207,9 +207,7 @@ function AdminReferralsPage() {
<button
type="button"
className="flex w-full items-center justify-between p-6 text-left transition-colors hover:bg-muted/50"
onClick={() =>
setExpandedReferrer(isExpanded ? null : referrer.referrerId)
}
onClick={() => setExpandedReferrer(isExpanded ? null : referrer.referrerId)}
>
<div className="flex items-center gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary font-bold">
@@ -217,9 +215,7 @@ function AdminReferralsPage() {
</div>
<div>
<p className="font-medium">{referrer.referrerName}</p>
<p className="text-sm text-muted-foreground">
{referrer.referrerEmail}
</p>
<p className="text-sm text-muted-foreground">{referrer.referrerEmail}</p>
</div>
</div>
@@ -252,9 +248,7 @@ function AdminReferralsPage() {
>
<div>
<p className="text-sm font-medium">{ref.referredName}</p>
<p className="text-xs text-muted-foreground">
{ref.referredEmail}
</p>
<p className="text-xs text-muted-foreground">{ref.referredEmail}</p>
</div>
<div className="flex items-center gap-2">
{ref.rewardApplied && (

View File

@@ -1,7 +1,7 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { useAuth } from "@/hooks/use-auth";
import { toast } from "@/lib/toast";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
@@ -16,17 +16,9 @@ import {
DialogHeader,
DialogTitle,
} from "@sase/ui";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "@/lib/toast";
import {
Search,
ChevronLeft,
ChevronRight,
Eye,
ArrowLeft,
X,
UserPlus,
} from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, ChevronLeft, ChevronRight, Eye, Search, UserPlus, X } from "lucide-react";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/dashboard/admin/users")({
@@ -185,11 +177,7 @@ function AdminUsersPage() {
return (
<div className="mx-auto max-w-4xl space-y-6">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedUserId(null)}
>
<Button variant="ghost" size="sm" onClick={() => setSelectedUserId(null)}>
<ArrowLeft className="mr-1 h-4 w-4" />
Geri
</Button>
@@ -245,15 +233,11 @@ function AdminUsersPage() {
{/* Subscriptions */}
<Card>
<CardHeader>
<CardTitle>
Abonelikler ({userDetail.subscriptions.length})
</CardTitle>
<CardTitle>Abonelikler ({userDetail.subscriptions.length})</CardTitle>
</CardHeader>
<CardContent>
{userDetail.subscriptions.length === 0 ? (
<p className="py-4 text-center text-muted-foreground">
Abonelik bulunmuyor
</p>
<p className="py-4 text-center text-muted-foreground">Abonelik bulunmuyor</p>
) : (
<div className="space-y-3">
{userDetail.subscriptions.map((sub) => (
@@ -279,13 +263,11 @@ function AdminUsersPage() {
</span>
</div>
<p className="text-xs text-muted-foreground">
{sub.startDate ? formatDate(sub.startDate) : "-"}{" "}
- {sub.endDate ? formatDate(sub.endDate) : "-"}
{sub.startDate ? formatDate(sub.startDate) : "-"} -{" "}
{sub.endDate ? formatDate(sub.endDate) : "-"}
</p>
</div>
<p className="text-xs text-muted-foreground">
{formatDate(sub.createdAt)}
</p>
<p className="text-xs text-muted-foreground">{formatDate(sub.createdAt)}</p>
</div>
))}
</div>
@@ -296,15 +278,11 @@ function AdminUsersPage() {
{/* Payments */}
<Card>
<CardHeader>
<CardTitle>
Odemeler ({userDetail.payments.length})
</CardTitle>
<CardTitle>Odemeler ({userDetail.payments.length})</CardTitle>
</CardHeader>
<CardContent>
{userDetail.payments.length === 0 ? (
<p className="py-4 text-center text-muted-foreground">
Odeme bulunmuyor
</p>
<p className="py-4 text-center text-muted-foreground">Odeme bulunmuyor</p>
) : (
<div className="space-y-3">
{userDetail.payments.map((payment) => (
@@ -324,14 +302,10 @@ function AdminUsersPage() {
>
{payment.status}
</Badge>
<span className="text-sm">
{payment.method.toUpperCase()}
</span>
<span className="text-sm">{payment.method.toUpperCase()}</span>
</div>
<div className="text-right">
<p className="font-medium">
{formatCurrency(payment.amount)}
</p>
<p className="font-medium">{formatCurrency(payment.amount)}</p>
<p className="text-xs text-muted-foreground">
{formatDate(payment.createdAt)}
</p>
@@ -369,9 +343,7 @@ function AdminUsersPage() {
<DialogContent>
<DialogHeader>
<DialogTitle>Yeni Kullanici Olustur</DialogTitle>
<DialogDescription>
Sisteme yeni bir kullanici ekleyin.
</DialogDescription>
<DialogDescription>Sisteme yeni bir kullanici ekleyin.</DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
@@ -426,10 +398,7 @@ function AdminUsersPage() {
</select>
</div>
<DialogFooter>
<Button
type="submit"
disabled={createUserMutation.isPending}
>
<Button type="submit" disabled={createUserMutation.isPending}>
{createUserMutation.isPending ? "Olusturuluyor..." : "Olustur"}
</Button>
</DialogFooter>
@@ -500,9 +469,7 @@ function AdminUsersPage() {
<p className="text-sm text-muted-foreground">{u.email}</p>
</div>
<div>
<Badge variant={roleVariants[u.role] || "secondary"}>
{u.role}
</Badge>
<Badge variant={roleVariants[u.role] || "secondary"}>{u.role}</Badge>
</div>
<div>
<Badge variant={subStatusVariants[u.subscriptionStatus] || "outline"}>
@@ -510,9 +477,7 @@ function AdminUsersPage() {
</Badge>
</div>
<div>
<p className="text-sm text-muted-foreground">
{formatDate(u.createdAt)}
</p>
<p className="text-sm text-muted-foreground">{formatDate(u.createdAt)}</p>
</div>
<div className="text-right">
<Button

View File

@@ -1,4 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
@@ -7,6 +6,7 @@ import { Button } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { Separator } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { Download, Filter } from "lucide-react";
import { useState } from "react";

View File

@@ -1,13 +1,13 @@
import { useState } from "react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Skeleton, cn } from "@sase/ui";
import { Button } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ChevronRight, Columns2, LayoutGrid, Library, List, Lock } from "lucide-react";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { useState } from "react";
export const Route = createFileRoute("/dashboard/catalog/")({
component: CatalogBrandsPage,
@@ -50,13 +50,11 @@ function CatalogBrandsPage() {
aria-label="Görünüm modu"
className="inline-flex items-center gap-0.5 rounded-lg border border-border bg-muted/40 p-0.5"
>
{(
[
{ mode: "grid" as const, Icon: LayoutGrid, label: "Izgara" },
{ mode: "tree" as const, Icon: List, label: "Liste" },
{ mode: "columns" as const, Icon: Columns2, label: "Sütun" },
]
).map(({ mode, Icon, label }) => (
{[
{ mode: "grid" as const, Icon: LayoutGrid, label: "Izgara" },
{ mode: "tree" as const, Icon: List, label: "Liste" },
{ mode: "columns" as const, Icon: Columns2, label: "Sütun" },
].map(({ mode, Icon, label }) => (
<button
key={mode}
type="button"
@@ -174,10 +172,7 @@ function BrandListTree({ brands }: { brands: CatalogBrand[] }) {
{brands.map((brand) => {
if (!brand.hasAccess) {
return (
<div
key={brand.brandName}
className="flex items-center gap-3 px-4 py-3 opacity-50"
>
<div key={brand.brandName} className="flex items-center gap-3 px-4 py-3 opacity-50">
<CarBrandLogo brandName={brand.brandName} logoUrl={brand.logoUrl} size={24} />
<span className="flex-1 truncate text-sm font-medium">{brand.brandName}</span>
<Lock className="size-3.5 shrink-0 text-muted-foreground" />

View File

@@ -1,13 +1,22 @@
import { useState } from "react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { ModelListColumns } from "@/components/catalog/model-list-columns";
import { ModelListTree } from "@/components/catalog/model-list-tree";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Button, Skeleton, cn } from "@sase/ui";
import { ArrowLeft, BookOpen, Car, ChevronRight, Columns2, LayoutGrid, List, Loader2 } from "lucide-react";
import { ModelListTree } from "@/components/catalog/model-list-tree";
import { ModelListColumns } from "@/components/catalog/model-list-columns";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Skeleton, cn } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import {
ArrowLeft,
BookOpen,
Car,
ChevronRight,
Columns2,
LayoutGrid,
List,
Loader2,
} from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/dashboard/catalog_/$brandName/")({
validateSearch: (search: Record<string, unknown>) => ({
@@ -102,11 +111,7 @@ function CatalogModelsPage() {
{" / "}
{isMultiCatalog && activeCatalog ? (
<>
<Link
to="."
search={{ catalog: undefined }}
className="hover:underline"
>
<Link to="." search={{ catalog: undefined }} className="hover:underline">
{decodedBrandName}
</Link>
{" / "}
@@ -130,11 +135,7 @@ function CatalogModelsPage() {
</div>
) : isMultiCatalog && !activeCatalog ? (
// Sub-catalog selector
<CatalogSelector
catalogs={catalogs!}
brandName={brandName}
brandLabel={decodedBrandName}
/>
<CatalogSelector catalogs={catalogs!} brandName={brandName} brandLabel={decodedBrandName} />
) : modelsLoading ? (
<div>
<div className="mb-4 flex items-center gap-2 text-sm text-muted-foreground">
@@ -159,7 +160,10 @@ function CatalogModelsPage() {
<button
type="button"
onClick={() => changeViewMode("grid")}
className={cn("rounded p-1.5", viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Izgara"
>
<LayoutGrid className="size-4" />
@@ -167,7 +171,10 @@ function CatalogModelsPage() {
<button
type="button"
onClick={() => changeViewMode("tree")}
className={cn("rounded p-1.5", viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Liste"
>
<List className="size-4" />
@@ -175,7 +182,12 @@ function CatalogModelsPage() {
<button
type="button"
onClick={() => changeViewMode("columns")}
className={cn("rounded p-1.5", viewMode === "columns" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "columns"
? "bg-accent"
: "text-muted-foreground hover:text-foreground",
)}
title="Sutun"
>
<Columns2 className="size-4" />

View File

@@ -1,14 +1,14 @@
import { lazy, Suspense, useState } from "react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Button, Skeleton, cn } from "@sase/ui";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { CategoryColumns } from "@/components/categories/category-columns";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import { CategoryColumns } from "@/components/categories/category-columns";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Skeleton, cn } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { Suspense, lazy, useState } from "react";
const SchemaViewer = lazy(() =>
import("@/components/schema/schema-viewer").then((mod) => ({
@@ -64,7 +64,8 @@ function CatalogCategoryPage() {
const gearbox = search.gearbox;
const mgp = search.mgp;
const variantSearch = body || engine || gearbox || mgp ? { body, engine, gearbox, mgp } : undefined;
const variantSearch =
body || engine || gearbox || mgp ? { body, engine, gearbox, mgp } : undefined;
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
() => getUserSettings().categoryViewMode ?? "grid",
@@ -91,13 +92,23 @@ function CatalogCategoryPage() {
navigate({
to: "/dashboard/catalog/$brandName/$modelId/categories/$categoryId",
params: { brandName, modelId, categoryId: data.parentId },
search: variantSearch ?? { body: undefined, engine: undefined, gearbox: undefined, mgp: undefined },
search: variantSearch ?? {
body: undefined,
engine: undefined,
gearbox: undefined,
mgp: undefined,
},
});
} else {
navigate({
to: "/dashboard/catalog/$brandName/$modelId",
params: { brandName, modelId },
search: variantSearch ?? { body: undefined, engine: undefined, gearbox: undefined, mgp: undefined },
search: variantSearch ?? {
body: undefined,
engine: undefined,
gearbox: undefined,
mgp: undefined,
},
});
}
};
@@ -144,7 +155,10 @@ function CatalogCategoryPage() {
<button
type="button"
onClick={() => changeViewMode("grid")}
className={cn("rounded p-1.5", viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Izgara"
>
<LayoutGrid className="size-4" />
@@ -152,7 +166,10 @@ function CatalogCategoryPage() {
<button
type="button"
onClick={() => changeViewMode("tree")}
className={cn("rounded p-1.5", viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Agac"
>
<List className="size-4" />
@@ -160,7 +177,12 @@ function CatalogCategoryPage() {
<button
type="button"
onClick={() => changeViewMode("columns")}
className={cn("rounded p-1.5", viewMode === "columns" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "columns"
? "bg-accent"
: "text-muted-foreground hover:text-foreground",
)}
title="Sutun"
>
<Columns2 className="size-4" />

View File

@@ -1,17 +1,17 @@
import { useState } from "react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Button, Skeleton, Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import { CategoryColumns } from "@/components/categories/category-columns";
import { PsaVariantSelector } from "@/components/catalog/psa-variant-selector";
import { FordVariantSelector } from "@/components/catalog/ford-variant-selector";
import { P5RestrictionSelector } from "@/components/catalog/p5-restriction-selector";
import { PsaVariantSelector } from "@/components/catalog/psa-variant-selector";
import { CategoryColumns } from "@/components/categories/category-columns";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/dashboard/catalog_/$brandName_/$modelId/")({
validateSearch: (search) => ({
@@ -62,10 +62,7 @@ function CatalogVehiclePage() {
});
const isPsa = vehicle?.architecture === "LEGACY_PSA";
const isP4Legacy = [
"LEGACY_FORD",
"LEGACY_VOLVO",
].includes(vehicle?.architecture);
const isP4Legacy = ["LEGACY_FORD", "LEGACY_VOLVO"].includes(vehicle?.architecture);
const isP5WithRestrictions =
vehicle?.architecture === "P5_MODERN" &&
!!vehicle?.catalogPath &&
@@ -73,7 +70,8 @@ function CatalogVehiclePage() {
const showPsaVariantSelector = isPsa && !hasVariant;
const showFordVariantSelector = isP4Legacy && !hasVariant;
const showP5RestrictionSelector = isP5WithRestrictions && !hasVariant;
const showVariantSelector = showPsaVariantSelector || showFordVariantSelector || showP5RestrictionSelector;
const showVariantSelector =
showPsaVariantSelector || showFordVariantSelector || showP5RestrictionSelector;
const variantSearch = hasVariant ? { body, engine, gearbox, mgp } : undefined;
@@ -99,7 +97,11 @@ function CatalogVehiclePage() {
});
};
const handleVariantSelect = (selectedBody: string, selectedEngine: string, selectedGearbox: string) => {
const handleVariantSelect = (
selectedBody: string,
selectedEngine: string,
selectedGearbox: string,
) => {
const norm = (v: string) => (v && v !== "_all_" && v !== "_nor_" ? v : undefined);
navigate({
to: "/dashboard/catalog/$brandName/$modelId",
@@ -144,41 +146,52 @@ function CatalogVehiclePage() {
<ArrowLeft className="size-4" />
</Button>
<div>
<div className="text-xs text-muted-foreground">
<Link to="/dashboard/catalog" className="hover:underline">
{t("catalog.title")}
</Link>
{" / "}
<Link
to="/dashboard/catalog/$brandName"
params={{ brandName }}
search={{ catalog: undefined }}
className="hover:underline"
>
{decodedBrandName}
</Link>
{" / "}
<span className="font-medium text-foreground">
<div className="text-xs text-muted-foreground">
<Link to="/dashboard/catalog" className="hover:underline">
{t("catalog.title")}
</Link>
{" / "}
<Link
to="/dashboard/catalog/$brandName"
params={{ brandName }}
search={{ catalog: undefined }}
className="hover:underline"
>
{decodedBrandName}
</Link>
{" / "}
<span className="font-medium text-foreground">{vehicle?.model}</span>
{hasVariant && (
<>
{body && body !== "_all_" && (
<>
<span className="mx-1">/</span>
<span className="font-medium text-foreground">{body}</span>
</>
)}
{engine && engine !== "_all_" && (
<>
<span className="mx-1">/</span>
<span className="font-medium text-foreground">{engine}</span>
</>
)}
{gearbox && gearbox !== "_all_" && (
<>
<span className="mx-1">/</span>
<span className="font-medium text-foreground">{gearbox}</span>
</>
)}
</>
)}
</div>
<h1 className="text-xl font-bold">
{vehicle?.model}
</span>
{hasVariant && (
<>
{body && body !== "_all_" && (
<><span className="mx-1">/</span><span className="font-medium text-foreground">{body}</span></>
)}
{engine && engine !== "_all_" && (
<><span className="mx-1">/</span><span className="font-medium text-foreground">{engine}</span></>
)}
{gearbox && gearbox !== "_all_" && (
<><span className="mx-1">/</span><span className="font-medium text-foreground">{gearbox}</span></>
)}
</>
)}
</div>
<h1 className="text-xl font-bold">
{vehicle?.model}
{vehicle?.year && <span className="ml-2 text-base font-normal text-muted-foreground">({vehicle.year})</span>}
</h1>
{vehicle?.year && (
<span className="ml-2 text-base font-normal text-muted-foreground">
({vehicle.year})
</span>
)}
</h1>
</div>
</div>
{/* View toggle — always visible */}
@@ -259,7 +272,9 @@ function CatalogVehiclePage() {
<CardHeader>
<CardTitle className="text-base">{t("catalog.categories")}</CardTitle>
</CardHeader>
<CardContent className={viewMode === "columns" ? "p-0 overflow-hidden rounded-b-lg" : undefined}>
<CardContent
className={viewMode === "columns" ? "p-0 overflow-hidden rounded-b-lg" : undefined}
>
{categoriesLoading ? (
<div className="space-y-2">
{Array.from({ length: 8 }).map((_, i) => (

View File

@@ -1,9 +1,9 @@
import { Link, createFileRoute } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard/history")({
component: HistoryPage,
@@ -41,11 +41,7 @@ function HistoryPage() {
) : (
<div className="grid gap-4 md:grid-cols-2">
{data.map((vehicle: any) => (
<Link
key={vehicle.id}
to="/dashboard/vehicles/$id"
params={{ id: vehicle.id }}
>
<Link key={vehicle.id} to="/dashboard/vehicles/$id" params={{ id: vehicle.id }}>
<Card className="transition-shadow hover:shadow-md cursor-pointer">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">

View File

@@ -1,18 +1,18 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { Button, Badge, Skeleton, Separator } from "@sase/ui";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { Badge, Button, Separator, Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
import {
Search,
Car,
Database,
User,
ArrowRight,
Crown,
Calendar,
Car,
CheckCircle2,
Crown,
Database,
Search,
User,
} from "lucide-react";
export const Route = createFileRoute("/dashboard/")({
@@ -74,9 +74,7 @@ function StatCard({
<p className="font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight">
{value}
</p>
<p className="mt-0.5 text-sm font-medium text-muted-foreground">
{label}
</p>
<p className="mt-0.5 text-sm font-medium text-muted-foreground">{label}</p>
</div>
{detail && (
<div className="flex items-center justify-between text-sm">
@@ -179,7 +177,9 @@ function DashboardHome() {
queryKey: ["subscription", "me"],
queryFn: async () => {
try {
return await api.get<{ subscription: Subscription | null; eligibleForTrial: boolean }>("/subscriptions/me");
return await api.get<{ subscription: Subscription | null; eligibleForTrial: boolean }>(
"/subscriptions/me",
);
} catch {
return null;
}
@@ -266,19 +266,13 @@ function DashboardHome() {
label="Aktif Marka"
detail="Erişilebilir Marka"
detailValue={maxBrands > 0 ? String(maxBrands) : "—"}
progress={
maxBrands > 0 ? Math.round((brandCount / maxBrands) * 100) : undefined
}
progress={maxBrands > 0 ? Math.round((brandCount / maxBrands) * 100) : undefined}
buttonLabel={maxBrands === 0 ? "Plan Seçin" : undefined}
buttonTo={maxBrands === 0 ? "/dashboard/subscription" : undefined}
/>
{/* 4. Profil */}
<ProfileCard
name={user?.name ?? "—"}
email={user?.email ?? "—"}
initials={initials}
/>
<ProfileCard name={user?.name ?? "—"} email={user?.email ?? "—"} initials={initials} />
</div>
)}
@@ -295,7 +289,9 @@ function DashboardHome() {
{subLoading ? (
<Skeleton className="h-48 w-full rounded-2xl" />
) : subscription && (subscription.status === "active" || (subscription.status === "trial" && !subData?.eligibleForTrial)) ? (
) : subscription &&
(subscription.status === "active" ||
(subscription.status === "trial" && !subData?.eligibleForTrial)) ? (
<div className="rounded-2xl border border-border bg-background p-5 sm:p-6">
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
{/* Plan Info */}
@@ -309,15 +305,15 @@ function DashboardHome() {
<h3 className="text-lg font-bold">
{subscription.plan?.name ?? "Aktif Plan"}
</h3>
<Badge variant="default" className="bg-brand text-xs text-brand-foreground hover:bg-brand/90">
<Badge
variant="default"
className="bg-brand text-xs text-brand-foreground hover:bg-brand/90"
>
{subscription.status === "trial" ? "Deneme" : "Aktif"}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{subscription.billingPeriod === "yearly"
? "Yıllık"
: "Aylık"}{" "}
abonelik
{subscription.billingPeriod === "yearly" ? "Yıllık" : "Aylık"} abonelik
</p>
</div>
</div>
@@ -326,7 +322,11 @@ function DashboardHome() {
{subscription.brands && subscription.brands.length > 0 && (
<div className="flex flex-wrap gap-2">
{subscription.brands.map((b) => (
<Badge key={b.brandId} variant="outline" className="flex items-center gap-1.5">
<Badge
key={b.brandId}
variant="outline"
className="flex items-center gap-1.5"
>
<CarBrandLogo brandName={b.brandName} size={16} className="shrink-0" />
{b.brandName}
</Badge>
@@ -341,9 +341,7 @@ function DashboardHome() {
<Calendar className="size-3.5" />
Başlangıç:{" "}
<span className="font-medium text-foreground">
{new Date(subscription.startDate).toLocaleDateString(
"tr-TR",
)}
{new Date(subscription.startDate).toLocaleDateString("tr-TR")}
</span>
</div>
)}
@@ -352,9 +350,7 @@ function DashboardHome() {
<Calendar className="size-3.5" />
Bitiş:{" "}
<span className="font-medium text-foreground">
{new Date(subscription.endDate).toLocaleDateString(
"tr-TR",
)}
{new Date(subscription.endDate).toLocaleDateString("tr-TR")}
</span>
</div>
)}
@@ -362,11 +358,7 @@ function DashboardHome() {
{/* Features */}
<div className="flex flex-wrap gap-x-4 gap-y-1">
{[
"Sınırsız şase arama",
"Parça kataloğu",
"İnteraktif şema",
].map((f) => (
{["Sınırsız şase arama", "Parça kataloğu", "İnteraktif şema"].map((f) => (
<span
key={f}
className="flex items-center gap-1.5 text-sm text-muted-foreground"
@@ -434,11 +426,7 @@ function DashboardHome() {
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{history.slice(0, 3).map((v: any) => (
<Link
key={v.id}
to="/dashboard/vehicles/$id"
params={{ id: v.id }}
>
<Link key={v.id} to="/dashboard/vehicles/$id" params={{ id: v.id }}>
<div className="group flex items-center gap-4 rounded-2xl border border-border bg-background p-4 transition-colors hover:bg-accent">
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted">
<Car className="size-4 text-muted-foreground" />
@@ -447,9 +435,7 @@ function DashboardHome() {
<p className="truncate text-sm font-medium">
{v.brandName} {v.model}
</p>
<p className="truncate font-mono text-xs text-muted-foreground">
{v.vin}
</p>
<p className="truncate font-mono text-xs text-muted-foreground">{v.vin}</p>
</div>
<Badge variant="secondary" className="shrink-0">
{v.year}

View File

@@ -1,20 +1,13 @@
import { useState, useEffect, useRef } from "react";
import { createFileRoute, useNavigate, Link } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { Button, Input, Badge, Separator } from "@sase/ui";
import {
Search,
Car,
Loader2,
Clock,
AlertCircle,
Send,
} from "lucide-react";
import { api, ApiError } from "@/lib/api-client";
import { VehicleSelectModal } from "@/components/vehicles/vehicle-select-modal";
import { ApiError, api } from "@/lib/api-client";
import { startAction } from "@/lib/faro";
import { capture } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { VehicleSelectModal } from "@/components/vehicles/vehicle-select-modal";
import { Badge, Button, Input, Separator } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { AlertCircle, Car, Clock, Loader2, Search, Send } from "lucide-react";
import { useEffect, useRef, useState } from "react";
// ─── HELPERS ──────────────────────────────────────────────────────────────────
@@ -30,9 +23,16 @@ function sanitizeVin(raw: string): { cleaned: string; corrections: string[] } {
const corrections: string[] = [];
const cleaned = raw.replace(/[IOQioq]/g, (ch) => {
const upper = ch.toUpperCase();
if (upper === "I") { corrections.push("I→1"); return "1"; }
if (upper === "O") { corrections.push("O→0"); return "0"; }
corrections.push("Q→9"); return "9";
if (upper === "I") {
corrections.push("I→1");
return "1";
}
if (upper === "O") {
corrections.push("O→0");
return "0";
}
corrections.push("Q→9");
return "9";
});
return { cleaned, corrections };
}
@@ -140,9 +140,7 @@ function SearchPage() {
startAction("vin-decode", { vin: cleanVin });
capture("vin_decoded", { vin: cleanVin });
if (!isValidVin(cleanVin)) {
setError(
"Geçersiz şase. 17 karakter olmalı, I, O, Q harfleri kullanılamaz.",
);
setError("Geçersiz şase. 17 karakter olmalı, I, O, Q harfleri kullanılamaz.");
return;
}
@@ -170,7 +168,8 @@ function SearchPage() {
params: { id: data.id },
});
} catch (err) {
const message = err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin.";
const message =
err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin.";
capture("vin_decode_error", { vin: cleanVin, error: message });
if (err instanceof ApiError) {
setError(err.message);
@@ -217,7 +216,7 @@ function SearchPage() {
try {
const payload: Record<string, unknown> = { vin: candidateVin };
if (candidateSource === "emex") {
payload.emexCarIndex = parseInt(carId, 10);
payload.emexCarIndex = Number.parseInt(carId, 10);
} else {
payload.pcatCarId = carId;
}
@@ -236,9 +235,7 @@ function SearchPage() {
});
} catch (err) {
const message =
err instanceof ApiError
? err.message
: "Bir hata oluştu. Lütfen tekrar deneyin.";
err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin.";
setError(message);
setCandidates(null);
setCandidateSource(null);
@@ -312,9 +309,7 @@ function SearchPage() {
{/* Counter + Example VIN */}
<div className="flex items-center justify-between text-sm">
<span className="tabular-nums text-muted-foreground">
{vin.length}/17 karakter
</span>
<span className="tabular-nums text-muted-foreground">{vin.length}/17 karakter</span>
<button
type="button"
onClick={fillExampleVin}
@@ -391,9 +386,7 @@ function SearchPage() {
{previewLoading && (
<div className="flex items-center justify-center gap-3 rounded-2xl border border-border bg-background p-6">
<Loader2 className="size-5 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">
Araç bilgileri alınıyor...
</span>
<span className="text-sm text-muted-foreground">Araç bilgileri alınıyor...</span>
</div>
)}
@@ -428,7 +421,6 @@ function SearchPage() {
</div>
)}
{/* ─── SECTION 4: Son Aramalar ────────────────────────────────────── */}
{history && history.length > 0 && (
<div className="space-y-4">
@@ -466,9 +458,7 @@ function SearchPage() {
<p className="truncate text-sm font-medium">
{v.brandName} {v.model}
</p>
<p className="truncate font-mono text-xs text-muted-foreground">
{v.vin}
</p>
<p className="truncate font-mono text-xs text-muted-foreground">{v.vin}</p>
</div>
<Badge variant="secondary" className="shrink-0">
{v.year}

View File

@@ -1,6 +1,6 @@
import { lazy, Suspense } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { Skeleton } from "@sase/ui";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense, lazy } from "react";
const SettingsContent = lazy(() =>
import("@/components/settings/settings-content").then((mod) => ({

View File

@@ -1,10 +1,10 @@
import { lazy, Suspense, useEffect, useRef, useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { api } from "@/lib/api-client";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { api } from "@/lib/api-client";
import { startAction } from "@/lib/faro";
import { capture, setPeopleProperties } from "@/lib/posthog";
import { useTranslation } from "@/lib/i18n";
import { capture, setPeopleProperties } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { getUserSettings } from "@/lib/user-settings";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Button } from "@sase/ui";
@@ -20,10 +20,18 @@ import {
DialogTrigger,
} from "@sase/ui";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, Crown, Sparkles, ShieldCheck, Loader2, CheckCircle2, ArrowRight } from "lucide-react";
import { toast } from "@/lib/toast";
import { getUserSettings } from "@/lib/user-settings";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import confetti from "canvas-confetti";
import {
ArrowRight,
Check,
CheckCircle2,
Crown,
Loader2,
ShieldCheck,
Sparkles,
} from "lucide-react";
import { Suspense, lazy, useEffect, useRef, useState } from "react";
const BrandSelector = lazy(() =>
import("@/components/subscription/brand-selector").then((mod) => ({
@@ -31,9 +39,7 @@ const BrandSelector = lazy(() =>
})),
);
const LazyPlayer = lazy(() =>
import("@remotion/player").then((mod) => ({ default: mod.Player })),
);
const LazyPlayer = lazy(() => import("@remotion/player").then((mod) => ({ default: mod.Player })));
const LazyOnboardingProgress = lazy(() =>
import("@/remotion/OnboardingProgress").then((mod) => ({
@@ -149,7 +155,10 @@ function SubscriptionPage() {
const { data: subData, isLoading } = useQuery({
queryKey: ["subscription", "me"],
queryFn: () => api.get<{ subscription: Subscription | null; eligibleForTrial: boolean }>("/subscriptions/me"),
queryFn: () =>
api.get<{ subscription: Subscription | null; eligibleForTrial: boolean }>(
"/subscriptions/me",
),
});
const subscription = subData?.subscription;
@@ -338,10 +347,7 @@ function SubscriptionPage() {
<p className="text-sm text-red-600 dark:text-red-400">
{t("subscription.onboarding.error")}
</p>
<Button
variant="outline"
onClick={() => trialMutation.mutate()}
>
<Button variant="outline" onClick={() => trialMutation.mutate()}>
{t("subscription.onboarding.retry")}
</Button>
</div>
@@ -369,12 +375,18 @@ function SubscriptionPage() {
{/* Subscription info box */}
<div className="w-full max-w-md space-y-4 rounded-xl border border-brand/20 bg-background/60 p-5 backdrop-blur-sm dark:bg-background/30">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">{t("subscription.currentPlan")}</span>
<span className="text-sm text-muted-foreground">
{t("subscription.currentPlan")}
</span>
<Badge className="bg-brand text-brand-foreground">Full Paket</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">{t("subscription.billingPeriod")}</span>
<span className="text-sm font-medium">{t("subscription.onboarding.trialDuration")}</span>
<span className="text-sm text-muted-foreground">
{t("subscription.billingPeriod")}
</span>
<span className="text-sm font-medium">
{t("subscription.onboarding.trialDuration")}
</span>
</div>
{freshSub?.endDate && (
<div className="flex items-center justify-between">
@@ -474,7 +486,13 @@ function SubscriptionPage() {
<div className="flex items-center gap-2 rounded-lg bg-brand/10 px-3 py-2 text-sm text-brand">
<Sparkles className="h-4 w-4" />
{(() => {
const days = Math.max(0, Math.ceil((new Date(subscription.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)));
const days = Math.max(
0,
Math.ceil(
(new Date(subscription.endDate).getTime() - Date.now()) /
(1000 * 60 * 60 * 24),
),
);
return `${days} gün kaldı`;
})()}
</div>
@@ -515,7 +533,13 @@ function SubscriptionPage() {
</Dialog>
)}
{subscription.status === "cancelled" && (
<Button onClick={() => { capture("subscription_resumed"); resumeMutation.mutate(); }} disabled={resumeMutation.isPending}>
<Button
onClick={() => {
capture("subscription_resumed");
resumeMutation.mutate();
}}
disabled={resumeMutation.isPending}
>
{resumeMutation.isPending
? t("subscription.resuming")
: t("subscription.resumeSubscription")}
@@ -527,44 +551,43 @@ function SubscriptionPage() {
)}
{/* Trial CTA Card */}
{eligibleForTrial && (!subscription || subscription.status === "expired" || subscription.status === "trial") && (
<Card className="relative overflow-hidden border-brand/25 bg-brand/5">
<div className="pointer-events-none absolute -right-24 top-0 h-[300px] w-[300px] rounded-full bg-brand/15 blur-[100px]" />
<CardHeader className="relative">
<div className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-brand" />
<CardTitle className="text-foreground">
{t("subscription.trialTitle")}
</CardTitle>
</div>
<CardDescription className="text-muted-foreground">
{t("subscription.trialDescription")}
</CardDescription>
</CardHeader>
<CardContent className="relative space-y-4">
<ul className="space-y-2 text-sm">
{["allBrands", "vinSearch", "partsCatalog", "schemaViewer"].map((f) => (
<li key={f} className="flex items-center gap-2 text-foreground/85">
<Check className="h-4 w-4 text-brand" />
{t(`subscription.features.${f}`)}
</li>
))}
</ul>
<Button
className="bg-brand text-brand-foreground hover:bg-brand/90"
onClick={() => {
startAction("trial-start");
capture("trial_started");
trialMutation.mutate();
}}
disabled={trialMutation.isPending}
>
<ShieldCheck className="mr-2 h-4 w-4" />
{trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")}
</Button>
</CardContent>
</Card>
)}
{eligibleForTrial &&
(!subscription || subscription.status === "expired" || subscription.status === "trial") && (
<Card className="relative overflow-hidden border-brand/25 bg-brand/5">
<div className="pointer-events-none absolute -right-24 top-0 h-[300px] w-[300px] rounded-full bg-brand/15 blur-[100px]" />
<CardHeader className="relative">
<div className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-brand" />
<CardTitle className="text-foreground">{t("subscription.trialTitle")}</CardTitle>
</div>
<CardDescription className="text-muted-foreground">
{t("subscription.trialDescription")}
</CardDescription>
</CardHeader>
<CardContent className="relative space-y-4">
<ul className="space-y-2 text-sm">
{["allBrands", "vinSearch", "partsCatalog", "schemaViewer"].map((f) => (
<li key={f} className="flex items-center gap-2 text-foreground/85">
<Check className="h-4 w-4 text-brand" />
{t(`subscription.features.${f}`)}
</li>
))}
</ul>
<Button
className="bg-brand text-brand-foreground hover:bg-brand/90"
onClick={() => {
startAction("trial-start");
capture("trial_started");
trialMutation.mutate();
}}
disabled={trialMutation.isPending}
>
<ShieldCheck className="mr-2 h-4 w-4" />
{trialMutation.isPending ? t("common.loading") : t("subscription.startTrial")}
</Button>
</CardContent>
</Card>
)}
{/* No Subscription Banner */}
{!eligibleForTrial && !subscription && (

View File

@@ -1,6 +1,6 @@
import { lazy, Suspense } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { Skeleton } from "@sase/ui";
import { createFileRoute } from "@tanstack/react-router";
import { Suspense, lazy } from "react";
const PaymentContent = lazy(() =>
import("@/components/payment/payment-content").then((mod) => ({

View File

@@ -1,12 +1,12 @@
import { lazy, Suspense, useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useCategoryParts } from "@/hooks/use-parts";
import { CategoryColumns } from "@/components/categories/category-columns";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import { CategoryColumns } from "@/components/categories/category-columns";
import { Button, Skeleton, cn } from "@sase/ui";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { useCategoryParts } from "@/hooks/use-parts";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Skeleton, cn } from "@sase/ui";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { Suspense, lazy, useState } from "react";
const SchemaViewer = lazy(() =>
import("@/components/schema/schema-viewer").then((mod) => ({
@@ -79,22 +79,13 @@ function VehicleCategoryPage() {
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={handleBack}
title="Geri don"
>
<Button variant="ghost" size="icon" onClick={handleBack} title="Geri don">
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-bold">
{data?.name || "Kategori Detayi"}
</h1>
<h1 className="text-xl font-bold">{data?.name || "Kategori Detayi"}</h1>
{data?.description && (
<p className="text-sm text-muted-foreground">
{data.description}
</p>
<p className="text-sm text-muted-foreground">{data.description}</p>
)}
</div>
</div>
@@ -103,7 +94,10 @@ function VehicleCategoryPage() {
<button
type="button"
onClick={() => changeViewMode("grid")}
className={cn("rounded p-1.5", viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Izgara"
>
<LayoutGrid className="h-4 w-4" />
@@ -111,7 +105,10 @@ function VehicleCategoryPage() {
<button
type="button"
onClick={() => changeViewMode("tree")}
className={cn("rounded p-1.5", viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Agac"
>
<List className="h-4 w-4" />
@@ -119,7 +116,12 @@ function VehicleCategoryPage() {
<button
type="button"
onClick={() => changeViewMode("columns")}
className={cn("rounded p-1.5", viewMode === "columns" ? "bg-accent" : "text-muted-foreground hover:text-foreground")}
className={cn(
"rounded p-1.5",
viewMode === "columns"
? "bg-accent"
: "text-muted-foreground hover:text-foreground",
)}
title="Sutun"
>
<Columns2 className="h-4 w-4" />
@@ -136,29 +138,17 @@ function VehicleCategoryPage() {
)}
{/* Loading state */}
{isLoading && !data && (
<CategoryGridFallback />
)}
{isLoading && !data && <CategoryGridFallback />}
{/* Parent category — show children */}
{hasChildren && (
viewMode === "grid" ? (
<CategoryGrid
categories={data.children!}
vehicleId={id}
/>
{hasChildren &&
(viewMode === "grid" ? (
<CategoryGrid categories={data.children!} vehicleId={id} />
) : viewMode === "tree" ? (
<CategoryTree
categories={data.children!}
vehicleId={id}
/>
<CategoryTree categories={data.children!} vehicleId={id} />
) : (
<CategoryColumns
categories={data.children!}
vehicleId={id}
/>
)
)}
<CategoryColumns categories={data.children!} vehicleId={id} />
))}
{/* Leaf category — show schema viewer */}
{data && !hasChildren && (

View File

@@ -1,16 +1,16 @@
import { useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api-client";
import { CategoryTree } from "@/components/categories/category-tree";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryColumns } from "@/components/categories/category-columns";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { api } from "@/lib/api-client";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Card, CardContent, CardHeader, CardTitle, cn } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { Button } from "@sase/ui";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/dashboard/vehicles_/$id/")({
component: VehicleDetailPage,
@@ -53,12 +53,7 @@ function VehicleDetailPage() {
<div className="mx-auto max-w-4xl space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={() => window.history.back()}
title="Geri don"
>
<Button variant="ghost" size="icon" onClick={() => window.history.back()} title="Geri don">
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-3">
@@ -94,9 +89,7 @@ function VehicleDetailPage() {
onClick={() => changeViewMode("grid")}
className={cn(
"p-1.5 rounded",
viewMode === "grid"
? "bg-accent"
: "text-muted-foreground hover:text-foreground",
viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Izgara"
>
@@ -107,9 +100,7 @@ function VehicleDetailPage() {
onClick={() => changeViewMode("tree")}
className={cn(
"p-1.5 rounded",
viewMode === "tree"
? "bg-accent"
: "text-muted-foreground hover:text-foreground",
viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground",
)}
title="Agac"
>
@@ -130,7 +121,9 @@ function VehicleDetailPage() {
</button>
</div>
</CardHeader>
<CardContent className={viewMode === "columns" ? "p-0 overflow-hidden rounded-b-lg" : undefined}>
<CardContent
className={viewMode === "columns" ? "p-0 overflow-hidden rounded-b-lg" : undefined}
>
{categoriesLoading ? (
<div className="space-y-2">
{Array.from({ length: 8 }).map((_, i) => (
@@ -138,20 +131,11 @@ function VehicleDetailPage() {
))}
</div>
) : viewMode === "grid" ? (
<CategoryGrid
categories={categoryTree || []}
vehicleId={id}
/>
<CategoryGrid categories={categoryTree || []} vehicleId={id} />
) : viewMode === "tree" ? (
<CategoryTree
categories={categoryTree || []}
vehicleId={id}
/>
<CategoryTree categories={categoryTree || []} vehicleId={id} />
) : (
<CategoryColumns
categories={categoryTree || []}
vehicleId={id}
/>
<CategoryColumns categories={categoryTree || []} vehicleId={id} />
)}
</CardContent>
</Card>

View File

@@ -1,19 +1,19 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Button, Input } from "@sase/ui";
import { usePageMeta } from "@/hooks/use-page-meta";
import {
Search,
Car,
ArrowRight,
Lock,
Loader2,
FolderTree,
MousePointerClick,
Sun,
Moon,
} from "lucide-react";
import { useState, useEffect } from "react";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Input } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
import {
ArrowRight,
Car,
FolderTree,
Loader2,
Lock,
Moon,
MousePointerClick,
Search,
Sun,
} from "lucide-react";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/demo")({
component: DemoPage,
@@ -129,7 +129,10 @@ function DemoPage() {
Demo
</span>
<Link to="/register">
<Button size="sm" className="rounded-full bg-foreground text-background hover:bg-foreground/90">
<Button
size="sm"
className="rounded-full bg-foreground text-background hover:bg-foreground/90"
>
Tam Erişim
<ArrowRight className="ml-1.5 size-3.5" />
</Button>
@@ -142,7 +145,10 @@ function DemoPage() {
{/* Step indicator */}
<div className="mb-8 flex items-center justify-center gap-2 text-sm text-muted-foreground">
<button
onClick={() => { setStep("vin"); setSelectedCategory(null); }}
onClick={() => {
setStep("vin");
setSelectedCategory(null);
}}
className={`rounded-full px-3 py-1 transition ${step === "vin" ? "bg-foreground text-background" : "bg-muted"}`}
>
1. VIN Girin
@@ -263,7 +269,10 @@ function DemoPage() {
)}
</div>
<button
onClick={() => { setStep("vin"); setSelectedCategory(null); }}
onClick={() => {
setStep("vin");
setSelectedCategory(null);
}}
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
>
Farklı VIN dene
@@ -334,10 +343,14 @@ function DemoPage() {
<div
key={i}
className={`flex items-center justify-center rounded-lg border border-border text-xs text-muted-foreground ${
[2, 5, 9, 13].includes(i) ? "border-brand/50 bg-brand/10 text-brand" : "bg-muted/50"
[2, 5, 9, 13].includes(i)
? "border-brand/50 bg-brand/10 text-brand"
: "bg-muted/50"
}`}
>
{[2, 5, 9, 13].includes(i) ? EXAMPLE_SCHEMA_PARTS[[2, 5, 9, 13].indexOf(i)]?.position : ""}
{[2, 5, 9, 13].includes(i)
? EXAMPLE_SCHEMA_PARTS[[2, 5, 9, 13].indexOf(i)]?.position
: ""}
</div>
))}
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/kvkk")({
component: KvkkPage,
@@ -25,49 +25,37 @@ function KvkkPage() {
</header>
<main className="container mx-auto max-w-3xl px-4 py-16">
<h1 className="text-4xl font-bold">
KVKK Aydınlatma Metni
</h1>
<h1 className="text-4xl font-bold">KVKK Aydınlatma Metni</h1>
<p className="mt-2 text-sm text-muted-foreground">
6698 Sayılı Kişisel Verilerin Korunması Kanunu Kapsamında
Aydınlatma Metni
6698 Sayılı Kişisel Verilerin Korunması Kanunu Kapsamında Aydınlatma Metni
</p>
<div className="mt-8 space-y-8 text-muted-foreground leading-relaxed">
<section>
<h2 className="text-xl font-semibold text-foreground">
1. Veri Sorumlusu
</h2>
<h2 className="text-xl font-semibold text-foreground">1. Veri Sorumlusu</h2>
<p className="mt-3">
6698 sayılı Kişisel Verilerin Korunması Kanunu ("KVKK")
uyarınca, kişisel verileriniz veri sorumlusu sıfatıyla
Sase.tr tarafından aşağıda ıklanan kapsamda işlenmektedir.
6698 sayılı Kişisel Verilerin Korunması Kanunu ("KVKK") uyarınca, kişisel verileriniz
veri sorumlusu sıfatıyla Sase.tr tarafından aşağıda ıklanan kapsamda işlenmektedir.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
2. İşlenen Kişisel Veriler
</h2>
<h2 className="text-xl font-semibold text-foreground">2. İşlenen Kişisel Veriler</h2>
<ul className="mt-3 list-disc space-y-2 pl-6">
<li>
<strong>Kimlik Bilgileri:</strong> Ad, soyad
</li>
<li>
<strong>İletişim Bilgileri:</strong> E-posta adresi,
telefon numarası (isteğe bağlı)
<strong>İletişim Bilgileri:</strong> E-posta adresi, telefon numarası (isteğe bağlı)
</li>
<li>
<strong>İşlem Güvenliği:</strong> IP adresi, oturum
bilgileri, log kayıtları
<strong>İşlem Güvenliği:</strong> IP adresi, oturum bilgileri, log kayıtları
</li>
<li>
<strong>Kullanım Verileri:</strong> Arama geçmişi,
platform kullanım istatistikleri
<strong>Kullanım Verileri:</strong> Arama geçmişi, platform kullanım istatistikleri
</li>
<li>
<strong>Finansal Bilgiler:</strong> Fatura bilgileri,
abonelik durumu
<strong>Finansal Bilgiler:</strong> Fatura bilgileri, abonelik durumu
</li>
</ul>
</section>
@@ -77,35 +65,23 @@ function KvkkPage() {
3. Kişisel Verilerin İşlenme Amaçları
</h2>
<ul className="mt-3 list-disc space-y-2 pl-6">
<li>Üyelik işlemlerinin gerçekleştirilmesi ve hesap yönetimi</li>
<li>Platform hizmetlerinin sunulması ve iyileştirilmesi</li>
<li>Ödeme ve faturalama işlemlerinin yürütülmesi</li>
<li>Müşteri destek taleplerinin karşılanması</li>
<li>
Üyelik işlemlerinin gerçekleştirilmesi ve hesap yönetimi
</li>
<li>
Platform hizmetlerinin sunulması ve iyileştirilmesi
</li>
<li>
Ödeme ve faturalama işlemlerinin yürütülmesi
</li>
<li>
Müşteri destek taleplerinin karşılanması
</li>
<li>
Yasal yükümlülüklerin yerine getirilmesi (5651 sayılı
Kanun kapsamında log tutma yükümlülüğü dahil)
</li>
<li>
İstatistiksel analiz ve hizmet geliştirme
Yasal yükümlülüklerin yerine getirilmesi (5651 sayılı Kanun kapsamında log tutma
yükümlülüğü dahil)
</li>
<li>İstatistiksel analiz ve hizmet geliştirme</li>
</ul>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
4. Hukuki Sebepler
</h2>
<h2 className="text-xl font-semibold text-foreground">4. Hukuki Sebepler</h2>
<p className="mt-3">
Kişisel verileriniz, KVKK'nın 5. maddesinde belirtilen
aşağıdaki hukuki sebeplere dayanılarak işlenmektedir:
Kişisel verileriniz, KVKK'nın 5. maddesinde belirtilen aşağıdaki hukuki sebeplere
dayanılarak işlenmektedir:
</p>
<ul className="mt-2 list-disc space-y-2 pl-6">
<li>Sözleşmenin kurulması ve ifası</li>
@@ -116,84 +92,56 @@ function KvkkPage() {
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
5. Verilerin Aktarımı
</h2>
<h2 className="text-xl font-semibold text-foreground">5. Verilerin Aktarımı</h2>
<p className="mt-3">
Kişisel verileriniz, hizmetin sunulması amacıyla yurt
içindeki iş ortaklarımız (hosting, ödeme altyapısı) ve
yasal zorunluluk halinde yetkili kamu kurum ve kuruluşlarıyla
paylaşılabilir. Yurt dışına veri aktarımı, KVKK'nın 9.
maddesi kapsamındaki güvencelere uygun olarak
gerçekleştirilir.
Kişisel verileriniz, hizmetin sunulması amacıyla yurt içindeki iş ortaklarımız
(hosting, ödeme altyapısı) ve yasal zorunluluk halinde yetkili kamu kurum ve
kuruluşlarıyla paylaşılabilir. Yurt dışına veri aktarımı, KVKK'nın 9. maddesi
kapsamındaki güvencelere uygun olarak gerçekleştirilir.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
6. Veri Saklama Süresi
</h2>
<h2 className="text-xl font-semibold text-foreground">6. Veri Saklama Süresi</h2>
<p className="mt-3">
Kişisel verileriniz, işlenme amaçlarının gerektirdiği süre
boyunca ve yasal saklama yükümlülükleri kapsamında muhafaza
edilir. Süre sona erdiğinde veriler silinir, yok edilir
veya anonim hale getirilir.
Kişisel verileriniz, işlenme amaçlarının gerektirdiği süre boyunca ve yasal saklama
yükümlülükleri kapsamında muhafaza edilir. Süre sona erdiğinde veriler silinir, yok
edilir veya anonim hale getirilir.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
7. Haklarınız
</h2>
<p className="mt-3">
KVKK'nın 11. maddesi uyarınca aşağıdaki haklara
sahipsiniz:
</p>
<h2 className="text-xl font-semibold text-foreground">7. Haklarınız</h2>
<p className="mt-3">KVKK'nın 11. maddesi uyarınca aşağıdaki haklara sahipsiniz:</p>
<ul className="mt-2 list-disc space-y-2 pl-6">
<li>Kişisel verilerinizin işlenip işlenmediğini öğrenme</li>
<li>İşlenmişse buna ilişkin bilgi talep etme</li>
<li>İşlenme amacını ve amacına uygun kullanılıp kullanılmadığını öğrenme</li>
<li>Yurt içinde veya yurt dışında aktarıldığı üçüncü kişileri bilme</li>
<li>Eksik veya yanlış işlenmişse düzeltilmesini isteme</li>
<li>
İşlenme amacını ve amacına uygun kullanılıp
kullanılmadığını öğrenme
KVKK'nın 7. maddesindeki şartlar çerçevesinde silinmesini veya yok edilmesini isteme
</li>
<li>
Yurt içinde veya yurt dışında aktarıldığı üçüncü
kişileri bilme
İşlenen verilerin münhasıran otomatik sistemler vasıtasıyla analiz edilmesi
suretiyle aleyhinize bir sonucun ortaya çıkmasına itiraz etme
</li>
<li>
Eksik veya yanlış işlenmişse düzeltilmesini isteme
</li>
<li>
KVKK'nın 7. maddesindeki şartlar çerçevesinde silinmesini
veya yok edilmesini isteme
</li>
<li>
İşlenen verilerin münhasıran otomatik sistemler
vasıtasıyla analiz edilmesi suretiyle aleyhinize bir
sonucun ortaya çıkmasına itiraz etme
</li>
<li>
Kanuna aykırı işlenmesi sebebiyle zarara uğramanız
halinde zararın giderilmesini talep etme
Kanuna aykırı işlenmesi sebebiyle zarara uğramanız halinde zararın giderilmesini
talep etme
</li>
</ul>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
8. Başvuru Yöntemi
</h2>
<h2 className="text-xl font-semibold text-foreground">8. Başvuru Yöntemi</h2>
<p className="mt-3">
Yukarıda belirtilen haklarınızı kullanmak için{" "}
<a
href="mailto:info@sase.tr"
className="text-primary underline"
>
<a href="mailto:info@sase.tr" className="text-primary underline">
info@sase.tr
</a>{" "}
adresine kimliğinizi tespit edici belgelerle birlikte
yazılı olarak başvurabilirsiniz. Başvurular en geç 30 gün
içinde sonuçlandırılır.
adresine kimliğinizi tespit edici belgelerle birlikte yazılı olarak başvurabilirsiniz.
Başvurular en geç 30 gün içinde sonuçlandırılır.
</p>
</section>
</div>

View File

@@ -1,8 +1,8 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@sase/ui";
import { Badge } from "@sase/ui";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Button } from "@sase/ui";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@sase/ui";
import { Badge } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/pricing")({
component: PricingPage,
@@ -88,9 +88,7 @@ function PricingPage() {
<main id="main-content" className="container mx-auto px-4 py-24">
<div className="text-center">
<h1 className="text-4xl font-bold">Fiyatlandırma</h1>
<p className="mt-4 text-lg text-muted-foreground">
İhtiyacınıza uygun planı seçin.
</p>
<p className="mt-4 text-lg text-muted-foreground">İhtiyacınıza uygun planı seçin.</p>
</div>
<div className="mt-12 grid gap-6 md:grid-cols-2 lg:grid-cols-4">

View File

@@ -1,5 +1,5 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/privacy")({
component: PrivacyPage,
@@ -26,51 +26,42 @@ function PrivacyPage() {
<main className="container mx-auto max-w-3xl px-4 py-16">
<h1 className="text-4xl font-bold">Gizlilik Politikası</h1>
<p className="mt-2 text-sm text-muted-foreground">
Son güncelleme: 15 Şubat 2026
</p>
<p className="mt-2 text-sm text-muted-foreground">Son güncelleme: 15 Şubat 2026</p>
<div className="mt-8 space-y-8 text-muted-foreground leading-relaxed">
<section>
<h2 className="text-xl font-semibold text-foreground">
1. Genel Bakış
</h2>
<h2 className="text-xl font-semibold text-foreground">1. Genel Bakış</h2>
<p className="mt-3">
Sase.tr ("Platform") olarak kullanıcılarımızın gizliliğine
önem veriyoruz. Bu politika, kişisel verilerinizin nasıl
toplandığını, işlendiğini ve korunduğunu ıklar.
Sase.tr ("Platform") olarak kullanıcılarımızın gizliliğine önem veriyoruz. Bu
politika, kişisel verilerinizin nasıl toplandığını, işlendiğini ve korunduğunu
ıklar.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
2. Toplanan Veriler
</h2>
<h2 className="text-xl font-semibold text-foreground">2. Toplanan Veriler</h2>
<ul className="mt-3 list-disc space-y-2 pl-6">
<li>
<strong>Hesap Bilgileri:</strong> Ad, e-posta adresi,
telefon numarası (isteğe bağlı).
<strong>Hesap Bilgileri:</strong> Ad, e-posta adresi, telefon numarası (isteğe
bağlı).
</li>
<li>
<strong>Kullanım Verileri:</strong> Arama geçmişi, VIN
sorguları, sayfa görüntüleme istatistikleri.
<strong>Kullanım Verileri:</strong> Arama geçmişi, VIN sorguları, sayfa görüntüleme
istatistikleri.
</li>
<li>
<strong>Teknik Veriler:</strong> IP adresi, tarayıcı türü,
cihaz bilgisi, çerez verileri.
<strong>Teknik Veriler:</strong> IP adresi, tarayıcı türü, cihaz bilgisi, çerez
verileri.
</li>
<li>
<strong>Ödeme Bilgileri:</strong> Ödeme işlemleri üçüncü
parti ödeme sağlayıcıları aracılığıyla gerçekleştirilir.
Kredi kartı bilgileri tarafımızca saklanmaz.
<strong>Ödeme Bilgileri:</strong> Ödeme işlemleri üçüncü parti ödeme sağlayıcıları
aracılığıyla gerçekleştirilir. Kredi kartı bilgileri tarafımızca saklanmaz.
</li>
</ul>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
3. Verilerin Kullanım Amacı
</h2>
<h2 className="text-xl font-semibold text-foreground">3. Verilerin Kullanım Amacı</h2>
<ul className="mt-3 list-disc space-y-2 pl-6">
<li>Hizmetin sunulması ve iyileştirilmesi</li>
<li>Kullanıcı hesaplarının yönetimi</li>
@@ -81,52 +72,37 @@ function PrivacyPage() {
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
4. Veri Paylaşımı
</h2>
<h2 className="text-xl font-semibold text-foreground">4. Veri Paylaşımı</h2>
<p className="mt-3">
Kişisel verileriniz, yasal zorunluluklar dışında üçüncü
taraflarla paylaşılmaz. Hizmet sağlayıcılarımız (hosting,
ödeme altyapısı) yalnızca hizmetin işletilmesi için gerekli
Kişisel verileriniz, yasal zorunluluklar dışında üçüncü taraflarla paylaşılmaz. Hizmet
sağlayıcılarımız (hosting, ödeme altyapısı) yalnızca hizmetin işletilmesi için gerekli
olan verilere erişir ve gizlilik sözleşmeleri ile bağlıdır.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
5. Çerezler
</h2>
<h2 className="text-xl font-semibold text-foreground">5. Çerezler</h2>
<p className="mt-3">
Platform, oturum yönetimi ve kullanıcı deneyimini
iyileştirmek amacıyla çerezler kullanır. Zorunlu çerezler
hizmetin çalışması için gereklidir. Analitik çerezler
Platform, oturum yönetimi ve kullanıcı deneyimini iyileştirmek amacıyla çerezler
kullanır. Zorunlu çerezler hizmetin çalışması için gereklidir. Analitik çerezler
kullanıcı tercihine bağlıdır.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
6. Veri Güvenliği
</h2>
<h2 className="text-xl font-semibold text-foreground">6. Veri Güvenliği</h2>
<p className="mt-3">
Verileriniz SSL/TLS şifrelemesi ile korunur. Sunucularımız
güvenli veri merkezlerinde barındırılır ve düzenli güvenlik
denetimleri yapılır.
Verileriniz SSL/TLS şifrelemesi ile korunur. Sunucularımız güvenli veri merkezlerinde
barındırılır ve düzenli güvenlik denetimleri yapılır.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
7. Haklarınız
</h2>
<h2 className="text-xl font-semibold text-foreground">7. Haklarınız</h2>
<p className="mt-3">
KVKK kapsamında kişisel verilerinize erişim, düzeltme,
silme ve işlemeye itiraz etme haklarına sahipsiniz.
Talepleriniz için{" "}
<a
href="mailto:info@sase.tr"
className="text-primary underline"
>
KVKK kapsamında kişisel verilerinize erişim, düzeltme, silme ve işlemeye itiraz etme
haklarına sahipsiniz. Talepleriniz için{" "}
<a href="mailto:info@sase.tr" className="text-primary underline">
info@sase.tr
</a>{" "}
adresine başvurabilirsiniz.
@@ -134,13 +110,10 @@ function PrivacyPage() {
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
8. Değişiklikler
</h2>
<h2 className="text-xl font-semibold text-foreground">8. Değişiklikler</h2>
<p className="mt-3">
Bu politika zaman zaman güncellenebilir. Önemli
değişiklikler e-posta veya platform içi bildirim yoluyla
duyurulur.
Bu politika zaman zaman güncellenebilir. Önemli değişiklikler e-posta veya platform
içi bildirim yoluyla duyurulur.
</p>
</section>
</div>

View File

@@ -1,5 +1,5 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Button } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/terms")({
component: TermsPage,
@@ -26,37 +26,28 @@ function TermsPage() {
<main className="container mx-auto max-w-3xl px-4 py-16">
<h1 className="text-4xl font-bold">Kullanım Koşulları</h1>
<p className="mt-2 text-sm text-muted-foreground">
Son güncelleme: 15 Şubat 2026
</p>
<p className="mt-2 text-sm text-muted-foreground">Son güncelleme: 15 Şubat 2026</p>
<div className="mt-8 space-y-8 text-muted-foreground leading-relaxed">
<section>
<h2 className="text-xl font-semibold text-foreground">
1. Kabul ve Onay
</h2>
<h2 className="text-xl font-semibold text-foreground">1. Kabul ve Onay</h2>
<p className="mt-3">
Sase.tr platformunu ("Platform") kullanarak bu kullanım
koşullarını kabul etmiş sayılırsınız. Koşulları kabul
etmiyorsanız platformu kullanmayınız.
Sase.tr platformunu ("Platform") kullanarak bu kullanım koşullarını kabul etmiş
sayılırsınız. Koşulları kabul etmiyorsanız platformu kullanmayınız.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
2. Hizmet Tanımı
</h2>
<h2 className="text-xl font-semibold text-foreground">2. Hizmet Tanımı</h2>
<p className="mt-3">
Platform, şase numarası (VIN) ile araç tanımlama, orijinal
yedek parça kataloğuna erişim ve interaktif şema görüntüleme
hizmetleri sunar. Hizmetler abonelik modeli ile sunulur.
Platform, şase numarası (VIN) ile araç tanımlama, orijinal yedek parça kataloğuna
erişim ve interaktif şema görüntüleme hizmetleri sunar. Hizmetler abonelik modeli ile
sunulur.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
3. Hesap Oluşturma
</h2>
<h2 className="text-xl font-semibold text-foreground">3. Hesap Oluşturma</h2>
<ul className="mt-3 list-disc space-y-2 pl-6">
<li>Kayıt sırasında doğru ve güncel bilgi vermekle yükümlüsünüz.</li>
<li>Hesap bilgilerinizin güvenliğinden siz sorumlusunuz.</li>
@@ -66,30 +57,19 @@ function TermsPage() {
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
4. Abonelik ve Ödeme
</h2>
<h2 className="text-xl font-semibold text-foreground">4. Abonelik ve Ödeme</h2>
<ul className="mt-3 list-disc space-y-2 pl-6">
<li>Abonelikler aylık veya yıllık olarak faturalandırılır.</li>
<li>Yıllık aboneliklerde indirimli fiyat uygulanır.</li>
<li>Abonelik, dönem sonunda otomatik olarak yenilenir.</li>
<li>
Abonelikler aylık veya yıllık olarak faturalandırılır.
</li>
<li>
Yıllık aboneliklerde indirimli fiyat uygulanır.
</li>
<li>
Abonelik, dönem sonunda otomatik olarak yenilenir.
</li>
<li>
İptal işlemi mevcut dönemin sonunda geçerli olur;
kalan süre için iade yapılmaz.
İptal işlemi mevcut dönemin sonunda geçerli olur; kalan süre için iade yapılmaz.
</li>
</ul>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
5. Kullanım Kuralları
</h2>
<h2 className="text-xl font-semibold text-foreground">5. Kullanım Kuralları</h2>
<p className="mt-3">Aşağıdaki eylemler yasaktır:</p>
<ul className="mt-2 list-disc space-y-2 pl-6">
<li>Platformdaki verilerin toplu olarak çekilmesi (scraping)</li>
@@ -100,47 +80,36 @@ function TermsPage() {
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
6. Fikri Mülkiyet
</h2>
<h2 className="text-xl font-semibold text-foreground">6. Fikri Mülkiyet</h2>
<p className="mt-3">
Platform üzerindeki tüm içerik, tasarım, yazılım ve
veritabanı Sase.tr'ye aittir. Kullanıcılar yalnızca
kişisel kullanım amacıyla erişim hakkına sahiptir.
Platform üzerindeki tüm içerik, tasarım, yazılım ve veritabanı Sase.tr'ye aittir.
Kullanıcılar yalnızca kişisel kullanım amacıyla erişim hakkına sahiptir.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
7. Sorumluluk Sınırı
</h2>
<h2 className="text-xl font-semibold text-foreground">7. Sorumluluk Sınırı</h2>
<p className="mt-3">
Platform, sunulan bilgilerin doğruluğu için azami özeni
gösterir ancak verilerin eksiksiz veya hatasız olduğunu
garanti etmez. Yedek parça alım kararlarında son sorumluluk
kullanıcıya aittir.
Platform, sunulan bilgilerin doğruluğu için azami özeni gösterir ancak verilerin
eksiksiz veya hatasız olduğunu garanti etmez. Yedek parça alım kararlarında son
sorumluluk kullanıcıya aittir.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
8. Hizmet Değişiklikleri
</h2>
<h2 className="text-xl font-semibold text-foreground">8. Hizmet Değişiklikleri</h2>
<p className="mt-3">
Sase.tr, hizmet içeriğini, fiyatlandırmayı ve bu koşulları
önceden bildirimde bulunarak değiştirme hakkını saklı tutar.
Önemli değişiklikler en az 30 gün önce duyurulur.
Sase.tr, hizmet içeriğini, fiyatlandırmayı ve bu koşulları önceden bildirimde
bulunarak değiştirme hakkını saklı tutar. Önemli değişiklikler en az 30 gün önce
duyurulur.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-foreground">
9. Uyuşmazlık Çözümü
</h2>
<h2 className="text-xl font-semibold text-foreground">9. Uyuşmazlık Çözümü</h2>
<p className="mt-3">
Bu koşullar Türkiye Cumhuriyeti kanunlarına tabidir.
Uyuşmazlıklarda İstanbul mahkemeleri ve icra daireleri
yetkilidir.
Bu koşullar Türkiye Cumhuriyeti kanunlarına tabidir. Uyuşmazlıklarda İstanbul
mahkemeleri ve icra daireleri yetkilidir.
</p>
</section>
</div>