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:
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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][] = [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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)`,
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
“Sase.tr'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.”
|
||||
“Sase.tr'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.”
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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ı, açıklamalar ve çapraz referanslar.
|
||||
<strong>Orijinal Parça Kataloğu:</strong> OEM bazlı parça numaraları, açıklamalar ve
|
||||
çapraz referanslar.
|
||||
</li>
|
||||
<li>
|
||||
<strong>İnteraktif Şema:</strong> Araç şemaları üzerinden
|
||||
görsel parça seçimi ve detay gö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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, iç 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 iş 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 iş 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 açılır.
|
||||
Şema üzerindeki parçalara tıklayarak OEM kodunu, açı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>
|
||||
),
|
||||
|
||||
@@ -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 iş birliği talepleriniz için bize
|
||||
ulaşın.
|
||||
Sorularınız, önerileriniz veya iş 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>
|
||||
|
||||
@@ -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">
|
||||
© {new Date().getFullYear()} Sase.tr | Gizlilik Politikası,
|
||||
Kullanım Koşulları
|
||||
© {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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 açı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 açı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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 açı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
|
||||
açı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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user