feat: PL24 integration + EMEX on-demand parts loading
## Major Changes ### PL24 Integration (PartsLink24 API) - Add PL24 auth service with JWT token management - Add PL24 service for VIN decode and parts catalog - Support for VAG, Mercedes, BMW, Ford, Renault and more - Schema image download with base64 decoding - Image deduplication via SchemaPic table ### EMEX On-Demand Loading - Remove automatic parts scraping during VIN decode - Add fetchCategoryParts() for on-demand loading - Store category URLs in rawResponse for later fetching - Fix Chrome executable path for Puppeteer ### VIN Decode Flow - PL24 first, fallback to EMEX if not available - Both sources now use on-demand parts loading - Faster VIN lookup (no parts scraped upfront) ### Database Schema - Add SchemaPic model for image deduplication - Add schemaPicId to VehicleCategory - Add EMEX models for parallel scraping ### Frontend Improvements - Dark theme with Tailwind CSS - Improved UI components (card, button, input) - Better category and parts display - Schema image viewer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2516
apps/web/CLAUDE.md
Normal file
2516
apps/web/CLAUDE.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Loader2, Mail, Lock, ArrowRight, CheckCircle2, Sparkles } from 'lucide-react';
|
||||
import { Mail, Lock, ArrowRight, CheckCircle2, Sparkles, Eye, EyeOff } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -19,6 +19,7 @@ export default function LoginPage() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loginSuccess, setLoginSuccess] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleLogin = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -58,15 +59,15 @@ export default function LoginPage() {
|
||||
|
||||
if (loginSuccess) {
|
||||
return (
|
||||
<Card className="border-0 shadow-xl">
|
||||
<Card className="animate-scale-in">
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<div className="h-16 w-16 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-4">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-600" />
|
||||
<div className="h-16 w-16 rounded-full bg-success-50 ring ring-success/20 flex items-center justify-center mb-6 animate-bounce-soft">
|
||||
<CheckCircle2 className="h-8 w-8 text-success" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Giris Basarili!</h2>
|
||||
<h2 className="text-heading-md mb-2">Giris Basarili!</h2>
|
||||
<p className="text-muted-foreground mb-6">Yonlendiriliyorsunuz...</p>
|
||||
<Link href="/dashboard">
|
||||
<Button className="gradient-bg hover:opacity-90">
|
||||
<Button size="lg">
|
||||
Dashboard'a Git
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
@@ -77,19 +78,21 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-xl overflow-hidden">
|
||||
<div className="h-1 gradient-bg" />
|
||||
<CardHeader className="text-center pb-2">
|
||||
<div className="mx-auto mb-4 h-12 w-12 rounded-xl gradient-bg flex items-center justify-center lg:hidden">
|
||||
<Sparkles className="h-6 w-6 text-white" />
|
||||
<Card className="animate-fade-in">
|
||||
<CardHeader align="center" className="pb-4">
|
||||
<div className="mx-auto mb-4 h-14 w-14 rounded-xl gradient-primary flex items-center justify-center shadow-primary lg:hidden">
|
||||
<Sparkles className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Tekrar Hosgeldiniz</CardTitle>
|
||||
<CardTitle className="text-heading-md">Tekrar Hosgeldiniz</CardTitle>
|
||||
<CardDescription>Hesabiniza giris yapin</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleLogin}>
|
||||
<CardContent className="space-y-4 pt-4">
|
||||
<CardContent className="space-y-4">
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3 text-sm text-red-700 dark:text-red-300">
|
||||
<div className="flex items-center gap-3 rounded-xl bg-destructive-50 ring ring-destructive/20 p-3 text-body-sm text-destructive animate-fade-in">
|
||||
<div className="h-8 w-8 rounded-lg bg-destructive/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-lg font-semibold">!</span>
|
||||
</div>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -105,7 +108,7 @@ export default function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="h-12 border-2 focus:border-purple-500"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -115,38 +118,53 @@ export default function LoginPage() {
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="********"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="h-12 border-2 focus:border-purple-500"
|
||||
size="md"
|
||||
rightIcon={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="hover:text-primary transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Link href="/forgot-password" className="text-sm text-purple-600 hover:text-purple-700 hover:underline">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-caption-lg text-primary hover:text-primary/80 hover:underline transition-colors"
|
||||
>
|
||||
Sifremi unuttum
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4 pt-2">
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 gradient-bg hover:opacity-90 shadow-lg shadow-purple-500/25 text-base group"
|
||||
disabled={isLoading}
|
||||
size="lg"
|
||||
block
|
||||
className="group"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
{!isLoading && (
|
||||
<>
|
||||
Giris Yap
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-body-sm text-muted-foreground">
|
||||
Hesabiniz yok mu?{' '}
|
||||
<Link href="/register" className="text-purple-600 hover:text-purple-700 font-medium hover:underline">
|
||||
<Link
|
||||
href="/register"
|
||||
className="text-primary hover:text-primary/80 font-semibold hover:underline transition-colors"
|
||||
>
|
||||
Kayit ol
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -1,251 +1,558 @@
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Car, Search, Shield, Zap, CheckCircle2, ArrowRight, Sparkles } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Car,
|
||||
Search,
|
||||
Shield,
|
||||
Zap,
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
Database,
|
||||
Globe,
|
||||
Clock,
|
||||
Users,
|
||||
Star,
|
||||
ChevronRight,
|
||||
Play,
|
||||
BarChart3,
|
||||
Lock,
|
||||
Cpu
|
||||
} from 'lucide-react';
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="fixed top-0 left-0 right-0 z-50 glass">
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg gradient-bg flex items-center justify-center">
|
||||
<Car className="h-5 w-5 text-white" />
|
||||
<div className="flex min-h-screen flex-col overflow-hidden bg-background">
|
||||
{/* Header - Floating glass navbar */}
|
||||
<header className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-[calc(100%-2rem)] max-w-5xl">
|
||||
<div className="glass rounded-2xl px-4 py-2.5 ring ring-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2.5">
|
||||
<div className="h-9 w-9 rounded-xl gradient-primary flex items-center justify-center shadow-primary">
|
||||
<Car className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-lg font-bold gradient-text">Sase.tr</span>
|
||||
</Link>
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
<Link href="#features">
|
||||
<Button variant="ghost" size="sm">Ozellikler</Button>
|
||||
</Link>
|
||||
<Link href="#pricing">
|
||||
<Button variant="ghost" size="sm">Fiyatlar</Button>
|
||||
</Link>
|
||||
<Link href="#how-it-works">
|
||||
<Button variant="ghost" size="sm">Nasil Calisir</Button>
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeToggle variant="compact" />
|
||||
<Link href="/login" className="hidden sm:block">
|
||||
<Button variant="ghost" size="sm">Giris</Button>
|
||||
</Link>
|
||||
<Link href="/register">
|
||||
<Button size="sm" className="shadow-primary">
|
||||
Baslat
|
||||
<ArrowRight className="ml-1.5 h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="text-xl font-bold gradient-text">Sase.tr</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link href="/subscription/plans" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
Fiyatlar
|
||||
</Link>
|
||||
<Link href="/login">
|
||||
<Button variant="ghost" className="hover:bg-purple-100 dark:hover:bg-purple-900/20">
|
||||
Giris Yap
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/register">
|
||||
<Button className="gradient-bg hover:opacity-90 transition-opacity shadow-lg shadow-purple-500/25">
|
||||
Kayit Ol
|
||||
</Button>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero */}
|
||||
<section className="relative min-h-screen flex items-center justify-center pt-16">
|
||||
{/* Background decorations */}
|
||||
{/* Hero Section - Modern minimal */}
|
||||
<section className="relative min-h-screen flex items-center justify-center pt-24 pb-16">
|
||||
{/* Animated background */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-40 -right-40 h-80 w-80 rounded-full bg-purple-500/20 blur-3xl animate-pulse-slow" />
|
||||
<div className="absolute top-1/2 -left-40 h-80 w-80 rounded-full bg-indigo-500/20 blur-3xl animate-pulse-slow" style={{ animationDelay: '2s' }} />
|
||||
<div className="absolute -bottom-40 right-1/3 h-80 w-80 rounded-full bg-violet-500/20 blur-3xl animate-pulse-slow" style={{ animationDelay: '4s' }} />
|
||||
<div className="absolute top-1/4 left-1/4 h-[500px] w-[500px] rounded-full bg-primary/8 blur-[100px] animate-pulse-slow" />
|
||||
<div className="absolute bottom-1/4 right-1/4 h-[400px] w-[400px] rounded-full bg-accent/8 blur-[100px] animate-pulse-slow" style={{ animationDelay: '1s' }} />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[600px] w-[600px] rounded-full bg-primary/5 blur-[120px]" />
|
||||
</div>
|
||||
|
||||
{/* Grid pattern */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:4rem_4rem]" />
|
||||
{/* Subtle grid */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,hsl(var(--border)/0.05)_1px,transparent_1px),linear-gradient(to_bottom,hsl(var(--border)/0.05)_1px,transparent_1px)] bg-[size:4rem_4rem]" />
|
||||
|
||||
<div className="container relative z-10 text-center py-20">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-purple-200 dark:border-purple-800 bg-purple-50 dark:bg-purple-900/20 px-4 py-2 mb-8 animate-float">
|
||||
<Sparkles className="h-4 w-4 text-purple-600" />
|
||||
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">Turkiye'nin en kapsamli VIN sorgulama sistemi</span>
|
||||
<div className="container relative z-10 text-center">
|
||||
{/* Announcement badge */}
|
||||
<div className="inline-flex items-center gap-2 mb-8 animate-fade-in">
|
||||
<Badge variant="outline" size="lg" className="pl-1.5 pr-3 py-1.5 gap-2 bg-background/50 backdrop-blur-sm">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md gradient-primary">
|
||||
<Sparkles className="h-3.5 w-3.5 text-white" />
|
||||
</span>
|
||||
<span className="text-caption-lg">Turkiye'nin 1 numarali VIN sorgulama platformu</span>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<h1 className="mb-6 text-5xl md:text-7xl font-bold tracking-tight">
|
||||
Arac Sase Sorgulama
|
||||
{/* Main headline */}
|
||||
<h1 className="mb-6 text-display-md md:text-display-lg lg:text-[4.5rem] tracking-tight leading-[1.1] animate-fade-in" style={{ animationDelay: '0.1s' }}>
|
||||
Arac Bilgilerine
|
||||
<br />
|
||||
<span className="gradient-text">Platformu</span>
|
||||
<span className="gradient-text">Aninda Ulasin</span>
|
||||
</h1>
|
||||
|
||||
<p className="mx-auto mb-10 max-w-2xl text-xl text-muted-foreground leading-relaxed">
|
||||
VIN numarasi ile aracinizin tum bilgilerine, yedek parca kodlarina
|
||||
ve guncel fiyatlarina <span className="text-foreground font-medium">saniyeler icinde</span> ulasin.
|
||||
{/* Subheadline */}
|
||||
<p className="mx-auto mb-10 max-w-xl text-body-lg text-muted-foreground animate-fade-in" style={{ animationDelay: '0.2s' }}>
|
||||
VIN numarasi ile arac bilgileri, yedek parca kodlari ve guncel fiyatlara
|
||||
<span className="text-foreground font-medium"> saniyeler icinde</span> erisin.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4 mb-16">
|
||||
{/* CTA buttons */}
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-3 mb-16 animate-fade-in" style={{ animationDelay: '0.3s' }}>
|
||||
<Link href="/register">
|
||||
<Button size="lg" className="gradient-bg hover:opacity-90 transition-all shadow-xl shadow-purple-500/25 text-lg px-8 py-6 group">
|
||||
Ucretsiz Baslat
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
<Button size="lg" className="group h-12 px-6 text-base shadow-primary">
|
||||
Ucretsiz Deneyin
|
||||
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/subscription/plans">
|
||||
<Button size="lg" variant="outline" className="text-lg px-8 py-6 border-2 hover:bg-purple-50 dark:hover:bg-purple-900/20">
|
||||
Fiyatlari Gor
|
||||
<Link href="#how-it-works">
|
||||
<Button size="lg" variant="outline" className="h-12 px-6 text-base group">
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Nasil Calisir?
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 max-w-3xl mx-auto">
|
||||
{/* Stats row */}
|
||||
<div className="flex flex-wrap justify-center gap-8 md:gap-12 animate-fade-in" style={{ animationDelay: '0.4s' }}>
|
||||
{[
|
||||
{ value: '50+', label: 'Marka' },
|
||||
{ value: '1M+', label: 'Parca' },
|
||||
{ value: '10K+', label: 'Kullanici' },
|
||||
{ value: '99.9%', label: 'Uptime' },
|
||||
{ value: '50+', label: 'Desteklenen Marka', icon: Car },
|
||||
{ value: '1M+', label: 'Parca Verisi', icon: Database },
|
||||
{ value: '10K+', label: 'Aktif Kullanici', icon: Users },
|
||||
{ value: '99.9%', label: 'Sistem Uptime', icon: Zap },
|
||||
].map((stat) => (
|
||||
<div key={stat.label} className="text-center">
|
||||
<div className="text-3xl md:text-4xl font-bold gradient-text">{stat.value}</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">{stat.label}</div>
|
||||
<div key={stat.label} className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<stat.icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-heading-sm">{stat.value}</div>
|
||||
<div className="text-caption-md text-muted-foreground">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce-soft">
|
||||
<div className="h-10 w-6 rounded-full ring ring-border flex items-start justify-center p-2">
|
||||
<div className="h-2 w-1 rounded-full bg-muted-foreground animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Trusted by section */}
|
||||
<section className="py-12 border-y border-border bg-background-secondary/50">
|
||||
<div className="container">
|
||||
<p className="text-center text-caption-lg text-muted-foreground mb-8">
|
||||
Turkiye'nin onde gelen oto yedek parca firmalari tarafindan tercih ediliyor
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center items-center gap-8 md:gap-16 opacity-60">
|
||||
{['Bosch', 'Valeo', 'Delphi', 'Denso', 'Continental', 'Mahle'].map((brand) => (
|
||||
<span key={brand} className="text-xl font-bold text-muted-foreground/50">{brand}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How it works */}
|
||||
<section id="how-it-works" className="py-24 scroll-mt-20">
|
||||
<div className="container">
|
||||
<div className="text-center mb-16">
|
||||
<Badge variant="soft-primary" size="default" className="mb-4">
|
||||
<Clock className="h-3.5 w-3.5 mr-1.5" />
|
||||
3 Kolay Adim
|
||||
</Badge>
|
||||
<h2 className="text-heading-lg md:text-display-sm mb-4">
|
||||
Nasil <span className="gradient-text">Calisir</span>?
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-body-md max-w-2xl mx-auto">
|
||||
Sadece birkaç adimda arac bilgilerine ulasin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||
{[
|
||||
{
|
||||
step: '01',
|
||||
title: 'VIN Numarasi Girin',
|
||||
description: 'Aracinizin 17 haneli VIN/sase numarasini girin veya ruhsattan okutun.',
|
||||
icon: Search,
|
||||
gradient: 'gradient-primary'
|
||||
},
|
||||
{
|
||||
step: '02',
|
||||
title: 'Arac Bilgilerini Gorun',
|
||||
description: 'Marka, model, yil, motor tipi ve tum teknik detaylara aninda ulasin.',
|
||||
icon: Car,
|
||||
gradient: 'gradient-accent'
|
||||
},
|
||||
{
|
||||
step: '03',
|
||||
title: 'Yedek Parca Bulun',
|
||||
description: 'OEM kodlari, alternatif parcalar ve guncel fiyatlari karsilastirin.',
|
||||
icon: Database,
|
||||
gradient: 'gradient-success'
|
||||
},
|
||||
].map((item, index) => (
|
||||
<div key={item.step} className="relative group">
|
||||
{index < 2 && (
|
||||
<div className="hidden md:block absolute top-16 left-full w-full h-px bg-gradient-to-r from-border to-transparent z-0" />
|
||||
)}
|
||||
<Card variant="interactive" className="relative z-10 h-full">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className={`h-14 w-14 rounded-2xl ${item.gradient} flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform`}>
|
||||
<item.icon className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<span className="text-display-sm text-muted-foreground/20 font-bold">{item.step}</span>
|
||||
</div>
|
||||
<h3 className="text-heading-sm mb-2">{item.title}</h3>
|
||||
<p className="text-body-sm text-muted-foreground">{item.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features */}
|
||||
<section className="container py-24 relative">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Neden <span className="gradient-text">Sase.tr</span>?
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||
Modern altyapi, hizli sonuclar ve kapsamli veritabani ile aracinizi taniyin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card className="card-hover border-0 shadow-lg bg-gradient-to-br from-white to-purple-50/50 dark:from-gray-900 dark:to-purple-900/10">
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="h-14 w-14 rounded-2xl gradient-bg flex items-center justify-center shadow-lg shadow-purple-500/25">
|
||||
<Zap className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">Simsek Hizinda Sorgulama</CardTitle>
|
||||
<CardDescription className="text-base leading-relaxed">
|
||||
VIN numarasi girin, milisaniyeler icinde arac bilgilerine ulasin. API tabanli modern altyapi.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="card-hover border-0 shadow-lg bg-gradient-to-br from-white to-indigo-50/50 dark:from-gray-900 dark:to-indigo-900/10">
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-indigo-500 to-blue-600 flex items-center justify-center shadow-lg shadow-indigo-500/25">
|
||||
<Search className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">Detayli Parca Bilgisi</CardTitle>
|
||||
<CardDescription className="text-base leading-relaxed">
|
||||
OEM kodlari, alternatif markalar, stok durumu ve guncel fiyatlar tek platformda.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="card-hover border-0 shadow-lg bg-gradient-to-br from-white to-violet-50/50 dark:from-gray-900 dark:to-violet-900/10">
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center shadow-lg shadow-violet-500/25">
|
||||
<Shield className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">Guvenli Altyapi</CardTitle>
|
||||
<CardDescription className="text-base leading-relaxed">
|
||||
SSL sertifikasi, sifreli veri iletimi ve KVKK uyumlu guvenli depolama.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing */}
|
||||
<section className="py-24 bg-gradient-to-b from-transparent via-purple-50/50 to-transparent dark:via-purple-900/10">
|
||||
{/* Features Section */}
|
||||
<section id="features" className="py-24 bg-background-secondary scroll-mt-20">
|
||||
<div className="container">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Basit ve Seffaf <span className="gradient-text">Fiyatlandirma</span>
|
||||
<Badge variant="accent" size="default" className="mb-4">
|
||||
<Sparkles className="h-3.5 w-3.5 mr-1.5" />
|
||||
Ozellikler
|
||||
</Badge>
|
||||
<h2 className="text-heading-lg md:text-display-sm mb-4">
|
||||
Neden <span className="gradient-text">Sase.tr</span>?
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||
Isletmenize uygun plani secin, hemen baslayin.
|
||||
<p className="text-muted-foreground text-body-md max-w-2xl mx-auto">
|
||||
En gelismis arac sorgulama altyapisi ile profesyonel sonuclar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-4 max-w-6xl mx-auto">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ name: 'Baslangic', price: 299, brands: 1, features: ['1 marka erisimi', 'Sinirsiz sorgu', 'Email destek'] },
|
||||
{ name: 'Pro', price: 599, brands: 3, popular: true, features: ['3 marka erisimi', 'Sinirsiz sorgu', 'Oncelikli destek', 'API erisimi'] },
|
||||
{ name: 'Isletme', price: 999, brands: 10, features: ['10 marka erisimi', 'Sinirsiz sorgu', '7/24 destek', 'API erisimi', 'Ozel raporlar'] },
|
||||
{ name: 'Full', price: 1999, brands: 'Tum', features: ['Tum markalara erisim', 'Sinirsiz sorgu', 'VIP destek', 'API erisimi', 'Ozel raporlar', 'Beyaz etiket'] },
|
||||
{
|
||||
icon: Zap,
|
||||
title: 'Ultra Hizli Sorgulama',
|
||||
description: 'API tabanli modern altyapi ile milisaniyeler icinde sonuc alin.',
|
||||
gradient: 'gradient-primary'
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: 'Genis Veritabani',
|
||||
description: '50+ marka ve 1 milyonun uzerinde yedek parca verisi.',
|
||||
gradient: 'gradient-accent'
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: 'Guvenli Altyapi',
|
||||
description: 'SSL sertifikasi ve KVKK uyumlu veri koruma.',
|
||||
gradient: 'gradient-success'
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'Global Veri Kaynaklari',
|
||||
description: 'Dunya capinda OEM ve aftermarket veri entegrasyonu.',
|
||||
gradient: 'gradient-primary'
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Detayli Raporlama',
|
||||
description: 'Sorgu gecmisi, favori araclar ve ozel raporlar.',
|
||||
gradient: 'gradient-accent'
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: 'API Erisimi',
|
||||
description: 'REST API ile kendi sistemlerinize entegre edin.',
|
||||
gradient: 'gradient-success'
|
||||
},
|
||||
].map((feature) => (
|
||||
<Card key={feature.title} variant="interactive" className="group">
|
||||
<CardHeader className="pb-4">
|
||||
<div className={`h-12 w-12 rounded-xl ${feature.gradient} flex items-center justify-center shadow-lg mb-4 group-hover:scale-105 transition-transform`}>
|
||||
<feature.icon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-heading-sm">{feature.title}</CardTitle>
|
||||
<CardDescription className="text-body-sm">{feature.description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<section id="pricing" className="py-24 scroll-mt-20">
|
||||
<div className="container">
|
||||
<div className="text-center mb-16">
|
||||
<Badge variant="soft-success" size="default" className="mb-4">
|
||||
<Star className="h-3.5 w-3.5 mr-1.5" />
|
||||
Fiyatlandirma
|
||||
</Badge>
|
||||
<h2 className="text-heading-lg md:text-display-sm mb-4">
|
||||
Seffaf <span className="gradient-text">Fiyatlar</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-body-md max-w-2xl mx-auto">
|
||||
Isletmenize uygun plani secin, gizli maliyet yok
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 max-w-6xl mx-auto">
|
||||
{[
|
||||
{
|
||||
name: 'Baslangic',
|
||||
price: 299,
|
||||
description: 'Bireysel kullanici',
|
||||
features: ['1 marka erisimi', 'Aylik 100 sorgu', 'Email destek', 'Temel raporlar'],
|
||||
cta: 'Baslat'
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
price: 599,
|
||||
description: 'Kucuk isletmeler',
|
||||
popular: true,
|
||||
features: ['3 marka erisimi', 'Sinirsiz sorgu', 'Oncelikli destek', 'API erisimi', 'Gelismis raporlar'],
|
||||
cta: 'Populer Plan'
|
||||
},
|
||||
{
|
||||
name: 'Isletme',
|
||||
price: 999,
|
||||
description: 'Orta olcekli firmalar',
|
||||
features: ['10 marka erisimi', 'Sinirsiz sorgu', '7/24 destek', 'API erisimi', 'Ozel raporlar', 'Coklu kullanici'],
|
||||
cta: 'Baslat'
|
||||
},
|
||||
{
|
||||
name: 'Kurumsal',
|
||||
price: 1999,
|
||||
description: 'Buyuk isletmeler',
|
||||
features: ['Tum markalara erisim', 'Sinirsiz her sey', 'VIP destek', 'Ozel API limiti', 'Beyaz etiket', 'SLA garantisi'],
|
||||
cta: 'Iletisime Gec'
|
||||
},
|
||||
].map((plan) => (
|
||||
<Card
|
||||
key={plan.name}
|
||||
className={`card-hover relative overflow-hidden ${
|
||||
plan.popular
|
||||
? 'border-2 border-purple-500 shadow-xl shadow-purple-500/20 scale-105'
|
||||
: 'border-0 shadow-lg'
|
||||
}`}
|
||||
className={`relative ${plan.popular ? 'ring-2 ring-primary shadow-primary-lg scale-[1.02] z-10' : ''}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute top-0 right-0 gradient-bg px-4 py-1 text-xs font-semibold text-white rounded-bl-xl">
|
||||
Populer
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<Badge className="shadow-lg">En Populer</Badge>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-lg">{plan.name}</CardTitle>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-bold">{plan.price}</span>
|
||||
<span className="text-muted-foreground">TL/ay</span>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<CardTitle className="text-heading-sm">{plan.name}</CardTitle>
|
||||
{plan.popular && <Sparkles className="h-4 w-4 text-primary" />}
|
||||
</div>
|
||||
<CardDescription className="text-caption-lg">{plan.description}</CardDescription>
|
||||
<div className="flex items-baseline gap-1 mt-4">
|
||||
<span className="text-display-sm">{plan.price}</span>
|
||||
<span className="text-muted-foreground text-body-sm">TL/ay</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<ul className="space-y-3">
|
||||
<CardContent className="pt-0">
|
||||
<ul className="space-y-3 mb-6">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-purple-500 flex-shrink-0" />
|
||||
<li key={feature} className="flex items-center gap-2.5 text-body-sm">
|
||||
<div className="h-5 w-5 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-primary" />
|
||||
</div>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link href="/register" className="block">
|
||||
<Button
|
||||
className={`w-full ${plan.popular ? 'gradient-bg hover:opacity-90 shadow-lg shadow-purple-500/25' : ''}`}
|
||||
block
|
||||
variant={plan.popular ? 'default' : 'outline'}
|
||||
className={plan.popular ? 'shadow-primary' : ''}
|
||||
>
|
||||
Hemen Basla
|
||||
{plan.cta}
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Money back guarantee */}
|
||||
<div className="mt-12 text-center">
|
||||
<div className="inline-flex items-center gap-3 px-6 py-3 rounded-2xl bg-success/10 ring ring-success/20">
|
||||
<Lock className="h-5 w-5 text-success" />
|
||||
<span className="text-body-sm text-success">14 gun icerisinde kosulsuz iade garantisi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonials */}
|
||||
<section className="py-24 bg-background-secondary">
|
||||
<div className="container">
|
||||
<div className="text-center mb-16">
|
||||
<Badge variant="soft-primary" size="default" className="mb-4">
|
||||
<Users className="h-3.5 w-3.5 mr-1.5" />
|
||||
Kullanici Yorumlari
|
||||
</Badge>
|
||||
<h2 className="text-heading-lg md:text-display-sm mb-4">
|
||||
Musterilerimiz <span className="gradient-text">Ne Diyor</span>?
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-5xl mx-auto">
|
||||
{[
|
||||
{
|
||||
quote: 'Sase.tr ile arac sorgulama islerimiz inanilmaz hizlandi. Artik musterilere aninda bilgi verebiliyoruz.',
|
||||
author: 'Mehmet Y.',
|
||||
role: 'Oto Yedek Parca - Istanbul',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
quote: 'API entegrasyonu cok kolay oldu. Kendi sistemimize entegre ettik ve verimliligi 3 katina cikardik.',
|
||||
author: 'Ayse K.',
|
||||
role: 'Yazilim Gelistirici - Ankara',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
quote: 'Parca kodlari ve fiyat karsilastirma ozelligi muazzam. Rekabetci fiyat vermemizi sagliyor.',
|
||||
author: 'Ali R.',
|
||||
role: 'Oto Galeri Sahibi - Izmir',
|
||||
rating: 5
|
||||
},
|
||||
].map((testimonial, index) => (
|
||||
<Card key={index} variant="glass" className="h-full">
|
||||
<CardContent className="p-6 flex flex-col h-full">
|
||||
<div className="flex gap-1 mb-4">
|
||||
{Array.from({ length: testimonial.rating }).map((_, i) => (
|
||||
<Star key={i} className="h-4 w-4 fill-primary text-primary" />
|
||||
))}
|
||||
</div>
|
||||
<p className="text-body-sm text-foreground mb-6 flex-grow">“{testimonial.quote}”</p>
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-border">
|
||||
<div className="h-10 w-10 rounded-full gradient-primary flex items-center justify-center text-white font-semibold">
|
||||
{testimonial.author[0]}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-caption-lg font-medium">{testimonial.author}</div>
|
||||
<div className="text-caption-md text-muted-foreground">{testimonial.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="container py-24">
|
||||
<div className="relative overflow-hidden rounded-3xl gradient-bg p-12 md:p-20 text-center animate-gradient">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff0a_1px,transparent_1px),linear-gradient(to_bottom,#ffffff0a_1px,transparent_1px)] bg-[size:2rem_2rem]" />
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-white mb-6">
|
||||
Hemen Ucretsiz Deneyin
|
||||
</h2>
|
||||
<p className="text-white/80 text-lg max-w-2xl mx-auto mb-8">
|
||||
Kredi karti gerekmez. Hemen kayit olun ve arac sorgulama deneyiminizi kesfetmeye baslayin.
|
||||
</p>
|
||||
<Link href="/register">
|
||||
<Button size="lg" variant="secondary" className="text-lg px-8 py-6 bg-white text-purple-700 hover:bg-gray-100 shadow-xl group">
|
||||
Ucretsiz Hesap Olustur
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Final CTA */}
|
||||
<section className="py-24">
|
||||
<div className="container">
|
||||
<Card className="gradient-primary overflow-hidden">
|
||||
<CardContent className="p-10 md:p-16 relative">
|
||||
{/* Grid pattern */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff06_1px,transparent_1px),linear-gradient(to_bottom,#ffffff06_1px,transparent_1px)] bg-[size:2rem_2rem]" />
|
||||
|
||||
{/* Glow effects */}
|
||||
<div className="absolute -top-20 -right-20 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="absolute -bottom-20 -left-20 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
|
||||
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-8">
|
||||
<div className="text-center lg:text-left max-w-xl">
|
||||
<h2 className="text-heading-lg md:text-display-sm text-white mb-4">
|
||||
Hemen Ucretsiz Baslatin
|
||||
</h2>
|
||||
<p className="text-white/80 text-body-md">
|
||||
Kredi karti gerektirmez. 14 gun ucretsiz deneme ile tum ozellikleri kesfedin.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Link href="/register">
|
||||
<Button size="lg" variant="secondary" className="bg-white text-primary hover:bg-white/90 group h-12 px-8">
|
||||
Ucretsiz Kayit Ol
|
||||
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/contact">
|
||||
<Button size="lg" variant="outline" className="border-white/30 text-white hover:bg-white/10 h-12 px-8">
|
||||
Bize Ulasin
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t bg-gray-50/50 dark:bg-gray-900/50">
|
||||
<div className="container py-12">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg gradient-bg flex items-center justify-center">
|
||||
<Car className="h-5 w-5 text-white" />
|
||||
<footer className="border-t border-border bg-background-secondary">
|
||||
<div className="container py-16">
|
||||
<div className="grid md:grid-cols-4 gap-8 mb-12">
|
||||
{/* Brand */}
|
||||
<div className="md:col-span-1">
|
||||
<Link href="/" className="flex items-center gap-2.5 mb-4">
|
||||
<div className="h-10 w-10 rounded-xl gradient-primary flex items-center justify-center shadow-primary">
|
||||
<Car className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-bold gradient-text">Sase.tr</span>
|
||||
</Link>
|
||||
<p className="text-body-sm text-muted-foreground mb-4">
|
||||
Turkiye'nin en kapsamli arac sase sorgulama platformu.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline" size="sm">SSL Guvenli</Badge>
|
||||
<Badge variant="outline" size="sm">KVKK Uyumlu</Badge>
|
||||
</div>
|
||||
<span className="text-lg font-bold gradient-text">Sase.tr</span>
|
||||
</div>
|
||||
<nav className="flex items-center gap-8 text-sm text-muted-foreground">
|
||||
<Link href="/subscription/plans" className="hover:text-foreground transition-colors">Fiyatlar</Link>
|
||||
<Link href="/privacy" className="hover:text-foreground transition-colors">Gizlilik</Link>
|
||||
<Link href="/terms" className="hover:text-foreground transition-colors">Kullanim Sartlari</Link>
|
||||
<Link href="/contact" className="hover:text-foreground transition-colors">Iletisim</Link>
|
||||
</nav>
|
||||
|
||||
{/* Links */}
|
||||
<div>
|
||||
<h4 className="text-caption-lg font-semibold mb-4">Urun</h4>
|
||||
<ul className="space-y-3 text-body-sm text-muted-foreground">
|
||||
<li><Link href="#features" className="hover:text-foreground transition-colors">Ozellikler</Link></li>
|
||||
<li><Link href="#pricing" className="hover:text-foreground transition-colors">Fiyatlandirma</Link></li>
|
||||
<li><Link href="#how-it-works" className="hover:text-foreground transition-colors">Nasil Calisir</Link></li>
|
||||
<li><Link href="/api-docs" className="hover:text-foreground transition-colors">API Dokumantasyonu</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-caption-lg font-semibold mb-4">Sirket</h4>
|
||||
<ul className="space-y-3 text-body-sm text-muted-foreground">
|
||||
<li><Link href="/about" className="hover:text-foreground transition-colors">Hakkimizda</Link></li>
|
||||
<li><Link href="/contact" className="hover:text-foreground transition-colors">Iletisim</Link></li>
|
||||
<li><Link href="/blog" className="hover:text-foreground transition-colors">Blog</Link></li>
|
||||
<li><Link href="/careers" className="hover:text-foreground transition-colors">Kariyer</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-caption-lg font-semibold mb-4">Yasal</h4>
|
||||
<ul className="space-y-3 text-body-sm text-muted-foreground">
|
||||
<li><Link href="/privacy" className="hover:text-foreground transition-colors">Gizlilik Politikasi</Link></li>
|
||||
<li><Link href="/terms" className="hover:text-foreground transition-colors">Kullanim Kosullari</Link></li>
|
||||
<li><Link href="/kvkk" className="hover:text-foreground transition-colors">KVKK Aydinlatma</Link></li>
|
||||
<li><Link href="/cookies" className="hover:text-foreground transition-colors">Cerez Politikasi</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 pt-8 border-t text-center text-sm text-muted-foreground">
|
||||
<p>2025 Sase.tr - Tum haklar saklidir.</p>
|
||||
|
||||
<div className="pt-8 border-t border-border flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-caption-lg text-muted-foreground">
|
||||
2025 Sase.tr - Tum haklar saklidir.
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-caption-md text-muted-foreground">
|
||||
<span>Turkiye'de tasarlandi</span>
|
||||
<span className="h-1 w-1 rounded-full bg-muted-foreground" />
|
||||
<span>Istanbul</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter, usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ThemeToggleDropdown } from '@/components/ui/theme-toggle';
|
||||
import { Search, Car, CreditCard, User, LogOut, Menu, X, Sparkles } from 'lucide-react';
|
||||
|
||||
const navItems = [
|
||||
@@ -32,11 +33,11 @@ export default function DashboardLayout({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex h-screen items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<div className="relative">
|
||||
<div className="h-16 w-16 rounded-full border-4 border-purple-200 dark:border-purple-900" />
|
||||
<div className="absolute inset-0 h-16 w-16 animate-spin rounded-full border-4 border-transparent border-t-purple-600" />
|
||||
<div className="h-16 w-16 rounded-full border-4 border-primary/20" />
|
||||
<div className="absolute inset-0 h-16 w-16 animate-spin rounded-full border-4 border-transparent border-t-primary" />
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">Yukleniyor...</p>
|
||||
</div>
|
||||
@@ -49,26 +50,26 @@ export default function DashboardLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50/50 dark:bg-gray-950">
|
||||
<div className="flex min-h-screen bg-background">
|
||||
{/* Mobile sidebar backdrop */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
|
||||
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`fixed inset-y-0 left-0 z-50 w-72 transform bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:z-auto ${
|
||||
className={`fixed inset-y-0 left-0 z-50 w-72 transform bg-card border-r border-border transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:z-auto ${
|
||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Sidebar Header */}
|
||||
<div className="flex h-16 items-center justify-between border-b border-gray-200 dark:border-gray-800 px-6">
|
||||
<div className="flex h-16 items-center justify-between border-b border-border px-6">
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg gradient-bg flex items-center justify-center">
|
||||
<div className="h-8 w-8 rounded-lg gradient-primary flex items-center justify-center">
|
||||
<Car className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-bold gradient-text">Sase.tr</span>
|
||||
@@ -92,14 +93,14 @@ export default function DashboardLayout({
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-all duration-200 ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-purple-500/10 to-indigo-500/10 text-purple-700 dark:text-purple-300 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
? 'bg-primary/10 text-primary shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<item.icon className={`h-5 w-5 ${isActive ? 'text-purple-600' : ''}`} />
|
||||
<item.icon className={`h-5 w-5 ${isActive ? 'text-primary' : ''}`} />
|
||||
{item.label}
|
||||
{isActive && (
|
||||
<div className="ml-auto h-2 w-2 rounded-full bg-purple-600" />
|
||||
<div className="ml-auto h-2 w-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -108,9 +109,9 @@ export default function DashboardLayout({
|
||||
</nav>
|
||||
|
||||
{/* Sidebar Footer - User Info */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-800 p-4">
|
||||
<div className="flex items-center gap-3 rounded-xl bg-gray-100 dark:bg-gray-800 p-3">
|
||||
<div className="h-10 w-10 rounded-full gradient-bg flex items-center justify-center text-white font-semibold">
|
||||
<div className="border-t border-border p-4">
|
||||
<div className="flex items-center gap-3 rounded-xl bg-muted/50 p-3">
|
||||
<div className="h-10 w-10 rounded-full gradient-primary flex items-center justify-center text-white font-semibold">
|
||||
{(user.name || user.email || 'U').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -121,7 +122,7 @@ export default function DashboardLayout({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={logout}
|
||||
className="text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -133,7 +134,7 @@ export default function DashboardLayout({
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 flex-col min-w-0">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl px-6">
|
||||
<header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b border-border bg-background/80 backdrop-blur-xl px-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -144,15 +145,18 @@ export default function DashboardLayout({
|
||||
</Button>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-purple-600" />
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
VIN sorgulama platformuna hosgeldiniz
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggleDropdown />
|
||||
|
||||
<Link href="/subscription/plans">
|
||||
<Button variant="outline" size="sm" className="hidden sm:flex border-purple-200 dark:border-purple-800 text-purple-700 dark:text-purple-300 hover:bg-purple-50 dark:hover:bg-purple-900/20">
|
||||
<Button variant="outline" size="sm" className="hidden sm:flex border-primary/30 text-primary hover:bg-primary/10">
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
Yukselt
|
||||
</Button>
|
||||
@@ -169,7 +173,7 @@ export default function DashboardLayout({
|
||||
</header>
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1 p-6 lg:p-8">{children}</main>
|
||||
<main className="flex-1 p-6 lg:p-8 bg-background">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,8 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Search, Car, CreditCard, ArrowRight, Zap, TrendingUp, Clock } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Search, Car, CreditCard, ArrowRight, TrendingUp, Clock, Shield, Sparkles } from 'lucide-react';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
@@ -15,7 +16,7 @@ export default function DashboardPage() {
|
||||
description: 'Arac sase numarasi ile sorgulama yapin',
|
||||
icon: Search,
|
||||
href: '/dashboard/vehicles/search',
|
||||
gradient: 'from-purple-500 to-indigo-600',
|
||||
gradient: 'gradient-primary',
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
@@ -23,59 +24,62 @@ export default function DashboardPage() {
|
||||
description: 'Gecmis sorgularinizi goruntuleyin',
|
||||
icon: Car,
|
||||
href: '/dashboard/vehicles',
|
||||
gradient: 'from-indigo-500 to-blue-600',
|
||||
gradient: 'gradient-accent',
|
||||
},
|
||||
{
|
||||
title: 'Abonelik',
|
||||
description: 'Abonelik durumunuzu kontrol edin',
|
||||
icon: CreditCard,
|
||||
href: '/dashboard/subscription',
|
||||
gradient: 'from-violet-500 to-purple-600',
|
||||
gradient: 'gradient-success',
|
||||
},
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ label: 'Bu Ay Sorgu', value: '24', icon: Search, change: '+12%' },
|
||||
{ label: 'Kayitli Arac', value: '8', icon: Car, change: '+2' },
|
||||
{ label: 'Kalan Gun', value: '18', icon: Clock, change: '' },
|
||||
{ label: 'Bu Ay Sorgu', value: '24', icon: Search, change: '+12%', color: 'gradient-primary' },
|
||||
{ label: 'Kayitli Arac', value: '8', icon: Car, change: '+2', color: 'gradient-accent' },
|
||||
{ label: 'Kalan Gun', value: '18', icon: Clock, change: '', color: 'gradient-success' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="relative overflow-hidden rounded-2xl gradient-bg p-8 text-white">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff08_1px,transparent_1px),linear-gradient(to_bottom,#ffffff08_1px,transparent_1px)] bg-[size:2rem_2rem]" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-purple-200">Hosgeldiniz</span>
|
||||
<Zap className="h-4 w-4 text-yellow-300" />
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* Welcome Section - Selia style */}
|
||||
<Card className="gradient-primary overflow-hidden">
|
||||
<CardContent className="p-6 md:p-8 relative">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#ffffff06_1px,transparent_1px),linear-gradient(to_bottom,#ffffff06_1px,transparent_1px)] bg-[size:2rem_2rem]" />
|
||||
<div className="absolute -right-20 -top-20 h-48 w-48 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="relative z-10">
|
||||
<Badge variant="secondary" size="sm" className="bg-white/10 text-white ring-white/20 mb-3">
|
||||
<Sparkles className="h-3 w-3 mr-1" />
|
||||
Hosgeldiniz
|
||||
</Badge>
|
||||
<h1 className="text-heading-lg md:text-display-sm text-white mb-2">
|
||||
{user?.name || 'Kullanici'}
|
||||
</h1>
|
||||
<p className="text-body-sm text-white/80 max-w-xl">
|
||||
VIN sorgulama platformuna hosgeldiniz. Aracinizin tum bilgilerine saniyeler icinde ulasin.
|
||||
</p>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-2">
|
||||
{user?.name || 'Kullanici'}
|
||||
</h1>
|
||||
<p className="text-purple-100 max-w-xl">
|
||||
VIN sorgulama platformuna hosgeldiniz. Aracinizin tum bilgilerine saniyeler icinde ulasin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Stats */}
|
||||
{/* Stats - Selia clean cards */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{stats.map((stat) => (
|
||||
<Card key={stat.label} className="border-0 shadow-md bg-white dark:bg-gray-900">
|
||||
<CardContent className="p-6">
|
||||
<Card key={stat.label} className="group">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
||||
<p className="text-3xl font-bold mt-1">{stat.value}</p>
|
||||
<p className="text-caption-lg text-muted-foreground">{stat.label}</p>
|
||||
<p className="text-display-sm mt-1">{stat.value}</p>
|
||||
{stat.change && (
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-500" />
|
||||
<span className="text-xs text-green-600">{stat.change}</span>
|
||||
</div>
|
||||
<Badge variant="soft-success" size="sm" className="mt-2">
|
||||
<TrendingUp className="h-3 w-3 mr-1" />
|
||||
{stat.change}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-12 w-12 rounded-xl gradient-bg flex items-center justify-center">
|
||||
<div className={`h-12 w-12 rounded-xl ${stat.color} flex items-center justify-center shadow-primary group-hover:scale-105 transition-transform`}>
|
||||
<stat.icon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,36 +88,32 @@ export default function DashboardPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{/* Quick Actions - Selia interactive cards */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Hizli Islemler</h2>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<h2 className="text-heading-sm mb-4">Hizli Islemler</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{quickActions.map((action) => (
|
||||
<Card
|
||||
key={action.title}
|
||||
className={`card-hover border-0 shadow-lg overflow-hidden ${
|
||||
action.primary ? 'ring-2 ring-purple-500/20' : ''
|
||||
}`}
|
||||
variant="interactive"
|
||||
className="group"
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<div className={`h-12 w-12 rounded-xl bg-gradient-to-br ${action.gradient} flex items-center justify-center shadow-lg mb-3`}>
|
||||
<action.icon className="h-6 w-6 text-white" />
|
||||
<CardHeader className="pb-3">
|
||||
<div className={`h-11 w-11 rounded-xl ${action.gradient} flex items-center justify-center shadow-sm mb-3 group-hover:scale-105 transition-transform`}>
|
||||
<action.icon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">{action.title}</CardTitle>
|
||||
<CardDescription>{action.description}</CardDescription>
|
||||
<CardTitle className="text-heading-sm">{action.title}</CardTitle>
|
||||
<CardDescription className="text-body-sm">{action.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="pt-0">
|
||||
<Link href={action.href}>
|
||||
<Button
|
||||
className={`w-full group ${
|
||||
action.primary
|
||||
? 'gradient-bg hover:opacity-90 shadow-lg shadow-purple-500/25'
|
||||
: ''
|
||||
}`}
|
||||
variant={action.primary ? 'default' : 'outline'}
|
||||
block
|
||||
className="group/btn"
|
||||
>
|
||||
{action.primary ? 'Sorgula' : 'Goruntule'}
|
||||
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
||||
<ArrowRight className="ml-2 h-4 w-4 group-hover/btn:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
@@ -122,18 +122,18 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tips Section */}
|
||||
<Card className="border-0 shadow-md bg-gradient-to-br from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20">
|
||||
<CardContent className="p-6">
|
||||
{/* Tips Section - Selia glass style */}
|
||||
<Card variant="glass" className="ring-primary/20">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-10 w-10 rounded-xl bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center flex-shrink-0">
|
||||
<Zap className="h-5 w-5 text-purple-600" />
|
||||
<div className="h-10 w-10 rounded-xl gradient-primary flex items-center justify-center flex-shrink-0 shadow-primary">
|
||||
<Shield className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-purple-900 dark:text-purple-100 mb-1">
|
||||
<h3 className="text-heading-sm text-foreground mb-1">
|
||||
Ipucu: VIN Numarasi Nerede?
|
||||
</h3>
|
||||
<p className="text-sm text-purple-700 dark:text-purple-300">
|
||||
<p className="text-body-sm text-muted-foreground">
|
||||
VIN numaranizi aracinizin ruhsatinda, sol on kapi pervazinda veya on camin sol alt kosesinde bulabilirsiniz. 17 karakterden olusur ve I, O, Q harflerini icermez.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
@@ -10,10 +10,18 @@ import {
|
||||
Search,
|
||||
Package,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Grid3X3,
|
||||
List,
|
||||
Copy,
|
||||
Check
|
||||
Check,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Maximize2,
|
||||
Image as ImageIcon,
|
||||
Layers,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -26,11 +34,19 @@ import { useToast } from '@/hooks/use-toast';
|
||||
interface Part {
|
||||
id: string;
|
||||
oemCode: string;
|
||||
// Formatted part number with spaces (e.g., "WHT 002 437")
|
||||
formattedPartNo?: string;
|
||||
alternativeOems: string[];
|
||||
nameEn: string;
|
||||
nameTr: string;
|
||||
description: string | null;
|
||||
// Remark field (e.g., "Colour code: JG3")
|
||||
remark?: string | null;
|
||||
// Quantity (Unit column in PL24)
|
||||
quantity?: number | null;
|
||||
positionCode: string | null;
|
||||
// Model codes / PR codes for compatibility (e.g., "PR:1PD+F...FM4")
|
||||
modelCodes?: string | null;
|
||||
imageUrl: string | null;
|
||||
prices: Array<{
|
||||
brand: string;
|
||||
@@ -40,6 +56,14 @@ interface Part {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface SubGroup {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
schemaImageUrl: string | null;
|
||||
partCount: number;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
id: string;
|
||||
code: string;
|
||||
@@ -58,11 +82,19 @@ interface Vehicle {
|
||||
year: number;
|
||||
series: string | null;
|
||||
engineCode: string | null;
|
||||
// Additional vehicle details from PL24
|
||||
productionDate?: string | null;
|
||||
transmissionCode?: string | null;
|
||||
driveType?: string | null;
|
||||
colorCode?: string | null;
|
||||
salesType?: string | null;
|
||||
equipment?: string | null;
|
||||
}
|
||||
|
||||
interface CategoryPartsData {
|
||||
vehicle: Vehicle;
|
||||
category: Category;
|
||||
subGroups?: SubGroup[];
|
||||
parts: Part[];
|
||||
totalParts: number;
|
||||
}
|
||||
@@ -78,14 +110,26 @@ export default function CategoryPartsPage() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [viewMode, setViewMode] = useState<'split' | 'grid' | 'list'>('split');
|
||||
const [copiedOem, setCopiedOem] = useState<string | null>(null);
|
||||
const [selectedPosition, setSelectedPosition] = useState<string | null>(null);
|
||||
const [hoveredPart, setHoveredPart] = useState<string | null>(null);
|
||||
const [selectedSubGroup, setSelectedSubGroup] = useState<string | null>(null);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showVehicleInfo, setShowVehicleInfo] = useState(false);
|
||||
const partRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const diagramRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchCategoryParts() {
|
||||
try {
|
||||
const response = await apiClient.get(`/vehicles/${vin}/categories/${categoryId}/parts`);
|
||||
setData(response.data.data);
|
||||
// Select first subgroup by default if available
|
||||
if (response.data.data?.subGroups?.length > 0) {
|
||||
setSelectedSubGroup(response.data.data.subGroups[0].id);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error?.message || 'Parcalar yuklenemedi');
|
||||
} finally {
|
||||
@@ -98,7 +142,7 @@ export default function CategoryPartsPage() {
|
||||
}
|
||||
}, [vin, categoryId]);
|
||||
|
||||
const copyOemCode = async (oemCode: string) => {
|
||||
const copyOemCode = useCallback(async (oemCode: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(oemCode);
|
||||
setCopiedOem(oemCode);
|
||||
@@ -110,9 +154,35 @@ export default function CategoryPartsPage() {
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
// Parcalari filtrele
|
||||
// Scroll to part when position is clicked on diagram
|
||||
const handlePositionClick = useCallback((positionCode: string) => {
|
||||
setSelectedPosition(positionCode);
|
||||
const partRef = partRefs.current.get(positionCode);
|
||||
if (partRef) {
|
||||
partRef.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get unique position codes for diagram markers
|
||||
const getPositionCodes = useCallback(() => {
|
||||
if (!data?.parts) return [];
|
||||
const positions = new Set<string>();
|
||||
data.parts.forEach(part => {
|
||||
if (part.positionCode && part.positionCode !== '-') {
|
||||
positions.add(part.positionCode);
|
||||
}
|
||||
});
|
||||
return Array.from(positions).sort((a, b) => {
|
||||
const numA = parseInt(a);
|
||||
const numB = parseInt(b);
|
||||
if (!isNaN(numA) && !isNaN(numB)) return numA - numB;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}, [data?.parts]);
|
||||
|
||||
// Filter parts
|
||||
const filteredParts = data?.parts.filter((part) => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
@@ -120,10 +190,23 @@ export default function CategoryPartsPage() {
|
||||
part.oemCode.toLowerCase().includes(query) ||
|
||||
part.nameTr?.toLowerCase().includes(query) ||
|
||||
part.nameEn.toLowerCase().includes(query) ||
|
||||
part.alternativeOems?.some(oem => oem.toLowerCase().includes(query))
|
||||
part.alternativeOems?.some(oem => oem.toLowerCase().includes(query)) ||
|
||||
part.positionCode?.toLowerCase().includes(query)
|
||||
);
|
||||
}) || [];
|
||||
|
||||
// Get current schema image (from selected subgroup or category)
|
||||
const getCurrentSchemaImage = () => {
|
||||
if (selectedSubGroup && data?.subGroups) {
|
||||
const sg = data.subGroups.find(s => s.id === selectedSubGroup);
|
||||
if (sg?.schemaImageUrl) return sg.schemaImageUrl;
|
||||
}
|
||||
return data?.category?.schemaImageUrl;
|
||||
};
|
||||
|
||||
const schemaImageUrl = getCurrentSchemaImage();
|
||||
const positionCodes = getPositionCodes();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-[60vh] items-center justify-center">
|
||||
@@ -161,10 +244,10 @@ export default function CategoryPartsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const { vehicle, category, totalParts } = data;
|
||||
const { vehicle, category, subGroups = [], totalParts } = data;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link href="/dashboard/vehicles/search" className="hover:text-foreground transition-colors">
|
||||
@@ -207,11 +290,21 @@ export default function CategoryPartsPage() {
|
||||
{totalParts} parca
|
||||
</Badge>
|
||||
<div className="flex items-center border rounded-lg p-1">
|
||||
<Button
|
||||
variant={viewMode === 'split' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-8 px-2 gap-1"
|
||||
onClick={() => setViewMode('split')}
|
||||
title="Split View"
|
||||
>
|
||||
<Layers className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setViewMode('grid')}
|
||||
title="Grid View"
|
||||
>
|
||||
<Grid3X3 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -220,6 +313,7 @@ export default function CategoryPartsPage() {
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setViewMode('list')}
|
||||
title="List View"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -227,202 +321,329 @@ export default function CategoryPartsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vehicle Identification Panel (PL24 style) */}
|
||||
<Card className="border-0 shadow-md">
|
||||
<button
|
||||
onClick={() => setShowVehicleInfo(!showVehicleInfo)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg gradient-bg flex items-center justify-center flex-shrink-0">
|
||||
<Info className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h3 className="font-semibold">Arac Bilgileri</h3>
|
||||
<p className="text-sm text-muted-foreground font-mono">{formatVin(vehicle.vin)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{showVehicleInfo ? (
|
||||
<ChevronUp className="h-5 w-5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
{showVehicleInfo && (
|
||||
<CardContent className="pt-0 pb-4 px-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 pt-4 border-t">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Marka</p>
|
||||
<p className="font-medium">{vehicle.brand.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Model</p>
|
||||
<p className="font-medium">{vehicle.model}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Yil</p>
|
||||
<p className="font-medium">{vehicle.year}</p>
|
||||
</div>
|
||||
{vehicle.productionDate && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Uretim Tarihi</p>
|
||||
<p className="font-medium">{vehicle.productionDate}</p>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.series && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Seri</p>
|
||||
<p className="font-medium">{vehicle.series}</p>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.salesType && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Satis Tipi</p>
|
||||
<p className="font-medium">{vehicle.salesType}</p>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.engineCode && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Motor Kodu</p>
|
||||
<p className="font-medium">{vehicle.engineCode}</p>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.transmissionCode && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Sanziman Kodu</p>
|
||||
<p className="font-medium">{vehicle.transmissionCode}</p>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.driveType && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Cekis Tipi</p>
|
||||
<p className="font-medium">{vehicle.driveType}</p>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.colorCode && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Renk Kodu</p>
|
||||
<p className="font-medium">{vehicle.colorCode}</p>
|
||||
</div>
|
||||
)}
|
||||
{vehicle.equipment && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Donanim</p>
|
||||
<p className="font-medium">{vehicle.equipment}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Sub-groups tabs (if available) */}
|
||||
{subGroups.length > 0 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 md:mx-0 md:px-0">
|
||||
{subGroups.map((sg) => (
|
||||
<Button
|
||||
key={sg.id}
|
||||
variant={selectedSubGroup === sg.id ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={`flex-shrink-0 ${selectedSubGroup === sg.id ? 'gradient-bg' : ''}`}
|
||||
onClick={() => setSelectedSubGroup(sg.id)}
|
||||
>
|
||||
{sg.name}
|
||||
<Badge variant="secondary" className="ml-2 h-5 px-1.5">
|
||||
{sg.partCount}
|
||||
</Badge>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="OEM kodu veya parca adi ara..."
|
||||
placeholder="OEM kodu, parca adi veya pozisyon ara..."
|
||||
className="pl-12 h-12 text-base border-2 focus:border-purple-500"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Schema Image */}
|
||||
{category.schemaImageUrl && (
|
||||
<Card className="border-0 shadow-md overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative aspect-video bg-gray-100 dark:bg-gray-800">
|
||||
<Image
|
||||
src={category.schemaImageUrl}
|
||||
alt={category.nameTr}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Parts */}
|
||||
{filteredParts.length === 0 ? (
|
||||
<Card className="border-0 shadow-lg">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="h-16 w-16 rounded-2xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
|
||||
<Package className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
{searchQuery ? 'Parca bulunamadi' : 'Henuz parca yok'}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md">
|
||||
{searchQuery
|
||||
? `"${searchQuery}" ile eslesen parca bulunamadi. Farkli bir arama deneyin.`
|
||||
: 'Bu kategori icin henuz parca eklenmemis.'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : viewMode === 'grid' ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredParts.map((part) => (
|
||||
<Card key={part.id} className="border-2 border-transparent hover:border-purple-300 dark:hover:border-purple-700 hover:shadow-lg transition-all">
|
||||
<CardContent className="p-4">
|
||||
{/* Part Image */}
|
||||
{part.imageUrl && (
|
||||
<div className="relative aspect-square mb-4 bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={part.imageUrl}
|
||||
alt={part.nameTr || part.nameEn}
|
||||
fill
|
||||
className="object-contain p-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Part Info */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="font-semibold line-clamp-2">
|
||||
{part.nameTr || part.nameEn}
|
||||
</h3>
|
||||
{part.positionCode && (
|
||||
<Badge variant="outline" className="flex-shrink-0">
|
||||
{part.positionCode}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* OEM Code */}
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded flex-1 truncate">
|
||||
{part.oemCode}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
onClick={() => copyOemCode(part.oemCode)}
|
||||
>
|
||||
{copiedOem === part.oemCode ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Alternative OEMs */}
|
||||
{part.alternativeOems && part.alternativeOems.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{part.alternativeOems.slice(0, 3).map((oem) => (
|
||||
<Badge key={oem} variant="secondary" className="text-xs">
|
||||
{oem}
|
||||
</Badge>
|
||||
))}
|
||||
{part.alternativeOems.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
+{part.alternativeOems.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prices */}
|
||||
{part.prices && part.prices.length > 0 && (
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{part.prices[0].brand}
|
||||
</span>
|
||||
<span className="font-semibold text-purple-600">
|
||||
{part.prices[0].price.toLocaleString('tr-TR')} {part.prices[0].currency}
|
||||
</span>
|
||||
</div>
|
||||
{part.prices[0].inStock && (
|
||||
<Badge className="mt-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||
Stokta
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Main Content */}
|
||||
{viewMode === 'split' && schemaImageUrl ? (
|
||||
/* Split View - PL24 Style */
|
||||
<div className={`grid gap-4 ${isFullscreen ? 'fixed inset-0 z-50 bg-background p-4' : 'lg:grid-cols-2'}`}>
|
||||
{/* Diagram Panel */}
|
||||
<Card className={`border-0 shadow-md overflow-hidden ${isFullscreen ? 'h-full' : ''}`}>
|
||||
<CardContent className="p-0">
|
||||
{/* Diagram Header */}
|
||||
<div className="flex items-center justify-between p-3 border-b bg-gray-50 dark:bg-gray-800/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<ImageIcon className="h-4 w-4 text-purple-600" />
|
||||
<span className="text-sm font-medium">Sema Diyagrami</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setZoom(Math.max(0.5, zoom - 0.25))}
|
||||
>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-xs w-12 text-center">{Math.round(zoom * 100)}%</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setZoom(Math.min(3, zoom + 0.25))}
|
||||
>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 ml-2"
|
||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diagram Container */}
|
||||
<div
|
||||
ref={diagramRef}
|
||||
className={`relative overflow-auto bg-white dark:bg-gray-900 ${isFullscreen ? 'h-[calc(100%-48px)]' : 'aspect-square md:aspect-video'}`}
|
||||
>
|
||||
<div
|
||||
className="relative min-w-full min-h-full flex items-center justify-center p-4"
|
||||
style={{ transform: `scale(${zoom})`, transformOrigin: 'center' }}
|
||||
>
|
||||
<Image
|
||||
src={schemaImageUrl}
|
||||
alt={category.nameTr}
|
||||
width={800}
|
||||
height={600}
|
||||
className="max-w-full h-auto object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Position Markers (overlay on diagram) */}
|
||||
<div className="absolute bottom-4 left-4 right-4 flex flex-wrap gap-1.5 bg-white/90 dark:bg-gray-900/90 p-2 rounded-lg backdrop-blur-sm max-h-32 overflow-y-auto">
|
||||
{positionCodes.map((pos) => (
|
||||
<Button
|
||||
key={pos}
|
||||
variant={selectedPosition === pos ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={`h-7 w-7 p-0 text-xs font-bold rounded-full ${
|
||||
selectedPosition === pos
|
||||
? 'gradient-bg'
|
||||
: hoveredPart === pos
|
||||
? 'bg-purple-100 border-purple-500 dark:bg-purple-900/50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handlePositionClick(pos)}
|
||||
>
|
||||
{pos}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Parts List Panel */}
|
||||
<div className={`space-y-2 ${isFullscreen ? 'overflow-y-auto max-h-full' : 'lg:max-h-[calc(100vh-300px)] lg:overflow-y-auto'}`}>
|
||||
{filteredParts.length === 0 ? (
|
||||
<Card className="border-0 shadow-lg">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<Package className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground">
|
||||
{searchQuery ? `"${searchQuery}" ile eslesen parca bulunamadi` : 'Parca bulunamadi'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
filteredParts.map((part) => (
|
||||
<Card
|
||||
key={part.id}
|
||||
ref={(el) => {
|
||||
if (el && part.positionCode) {
|
||||
partRefs.current.set(part.positionCode, el);
|
||||
}
|
||||
}}
|
||||
className={`border-2 transition-all cursor-pointer ${
|
||||
selectedPosition === part.positionCode
|
||||
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20 shadow-lg'
|
||||
: 'border-transparent hover:border-purple-300 dark:hover:border-purple-700 hover:shadow-md'
|
||||
}`}
|
||||
onMouseEnter={() => setHoveredPart(part.positionCode)}
|
||||
onMouseLeave={() => setHoveredPart(null)}
|
||||
onClick={() => part.positionCode && setSelectedPosition(part.positionCode)}
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Position Badge */}
|
||||
{part.positionCode && part.positionCode !== '-' && (
|
||||
<div className="h-8 w-8 rounded-full gradient-bg flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xs font-bold text-white">{part.positionCode}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Part Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-sm line-clamp-2">
|
||||
{part.nameTr || part.nameEn}
|
||||
</h3>
|
||||
{/* Remark (color codes, etc.) */}
|
||||
{part.remark && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mt-0.5 line-clamp-1">
|
||||
{part.remark}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center flex-wrap gap-2 mt-2">
|
||||
{/* Formatted part number */}
|
||||
<code className="text-xs font-mono bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded">
|
||||
{part.formattedPartNo || part.oemCode}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyOemCode(part.oemCode);
|
||||
}}
|
||||
>
|
||||
{copiedOem === part.oemCode ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
{/* Quantity badge */}
|
||||
{part.quantity && (
|
||||
<Badge variant="outline" className="text-xs h-5">
|
||||
{part.quantity} adet
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{/* Model codes / PR codes */}
|
||||
{part.modelCodes && (
|
||||
<p className="text-xs text-muted-foreground mt-1 font-mono truncate" title={part.modelCodes}>
|
||||
{part.modelCodes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price (if available) */}
|
||||
{part.prices && part.prices.length > 0 && (
|
||||
<div className="text-right flex-shrink-0">
|
||||
<div className="font-semibold text-purple-600">
|
||||
{part.prices[0].price.toLocaleString('tr-TR')} {part.prices[0].currency}
|
||||
</div>
|
||||
{part.prices[0].inStock && (
|
||||
<Badge className="mt-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 text-xs">
|
||||
Stokta
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : viewMode === 'split' && !schemaImageUrl ? (
|
||||
/* Fallback to grid when no schema image */
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{renderGridView()}
|
||||
</div>
|
||||
) : viewMode === 'grid' ? (
|
||||
/* Grid View */
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{renderGridView()}
|
||||
</div>
|
||||
) : (
|
||||
/* List View */
|
||||
<div className="space-y-2">
|
||||
{filteredParts.map((part) => (
|
||||
<Card key={part.id} className="border-2 border-transparent hover:border-purple-300 dark:hover:border-purple-700 hover:shadow-md transition-all">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Part Image */}
|
||||
{part.imageUrl && (
|
||||
<div className="relative h-16 w-16 bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden flex-shrink-0">
|
||||
<Image
|
||||
src={part.imageUrl}
|
||||
alt={part.nameTr || part.nameEn}
|
||||
fill
|
||||
className="object-contain p-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Part Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold truncate">
|
||||
{part.nameTr || part.nameEn}
|
||||
</h3>
|
||||
{part.positionCode && (
|
||||
<Badge variant="outline" className="flex-shrink-0">
|
||||
{part.positionCode}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<code className="text-sm font-mono text-muted-foreground">
|
||||
{part.oemCode}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => copyOemCode(part.oemCode)}
|
||||
>
|
||||
{copiedOem === part.oemCode ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
{part.prices && part.prices.length > 0 && (
|
||||
<div className="text-right flex-shrink-0">
|
||||
<div className="font-semibold text-purple-600">
|
||||
{part.prices[0].price.toLocaleString('tr-TR')} {part.prices[0].currency}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{part.prices[0].brand}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{renderListView()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -454,4 +675,237 @@ export default function CategoryPartsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
function renderGridView() {
|
||||
if (filteredParts.length === 0) {
|
||||
return (
|
||||
<Card className="col-span-full border-0 shadow-lg">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="h-16 w-16 rounded-2xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
|
||||
<Package className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
{searchQuery ? 'Parca bulunamadi' : 'Henuz parca yok'}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md">
|
||||
{searchQuery
|
||||
? `"${searchQuery}" ile eslesen parca bulunamadi.`
|
||||
: 'Bu kategori icin henuz parca eklenmemis.'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return filteredParts.map((part) => (
|
||||
<Card key={part.id} className="border-2 border-transparent hover:border-purple-300 dark:hover:border-purple-700 hover:shadow-lg transition-all">
|
||||
<CardContent className="p-4">
|
||||
{/* Part Image */}
|
||||
{part.imageUrl && (
|
||||
<div className="relative aspect-square mb-4 bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={part.imageUrl}
|
||||
alt={part.nameTr || part.nameEn}
|
||||
fill
|
||||
className="object-contain p-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Part Info */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="font-semibold line-clamp-2">
|
||||
{part.nameTr || part.nameEn}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{part.positionCode && part.positionCode !== '-' && (
|
||||
<Badge variant="outline">
|
||||
{part.positionCode}
|
||||
</Badge>
|
||||
)}
|
||||
{part.quantity && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{part.quantity}x
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remark (color codes, etc.) */}
|
||||
{part.remark && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 line-clamp-1">
|
||||
{part.remark}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* OEM Code */}
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded flex-1 truncate">
|
||||
{part.formattedPartNo || part.oemCode}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
onClick={() => copyOemCode(part.oemCode)}
|
||||
>
|
||||
{copiedOem === part.oemCode ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Model codes / PR codes */}
|
||||
{part.modelCodes && (
|
||||
<p className="text-xs text-muted-foreground font-mono truncate" title={part.modelCodes}>
|
||||
{part.modelCodes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Alternative OEMs */}
|
||||
{part.alternativeOems && part.alternativeOems.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{part.alternativeOems.slice(0, 3).map((oem) => (
|
||||
<Badge key={oem} variant="secondary" className="text-xs">
|
||||
{oem}
|
||||
</Badge>
|
||||
))}
|
||||
{part.alternativeOems.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
+{part.alternativeOems.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prices */}
|
||||
{part.prices && part.prices.length > 0 && (
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{part.prices[0].brand}
|
||||
</span>
|
||||
<span className="font-semibold text-purple-600">
|
||||
{part.prices[0].price.toLocaleString('tr-TR')} {part.prices[0].currency}
|
||||
</span>
|
||||
</div>
|
||||
{part.prices[0].inStock && (
|
||||
<Badge className="mt-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||
Stokta
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
));
|
||||
}
|
||||
|
||||
function renderListView() {
|
||||
if (filteredParts.length === 0) {
|
||||
return (
|
||||
<Card className="border-0 shadow-lg">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="h-16 w-16 rounded-2xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
|
||||
<Package className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
{searchQuery ? 'Parca bulunamadi' : 'Henuz parca yok'}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-center max-w-md">
|
||||
{searchQuery
|
||||
? `"${searchQuery}" ile eslesen parca bulunamadi.`
|
||||
: 'Bu kategori icin henuz parca eklenmemis.'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return filteredParts.map((part) => (
|
||||
<Card key={part.id} className="border-2 border-transparent hover:border-purple-300 dark:hover:border-purple-700 hover:shadow-md transition-all">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Position Badge */}
|
||||
{part.positionCode && part.positionCode !== '-' && (
|
||||
<div className="h-10 w-10 rounded-full gradient-bg flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-sm font-bold text-white">{part.positionCode}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Part Image */}
|
||||
{part.imageUrl && (
|
||||
<div className="relative h-16 w-16 bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden flex-shrink-0">
|
||||
<Image
|
||||
src={part.imageUrl}
|
||||
alt={part.nameTr || part.nameEn}
|
||||
fill
|
||||
className="object-contain p-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Part Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold truncate">
|
||||
{part.nameTr || part.nameEn}
|
||||
</h3>
|
||||
{part.quantity && (
|
||||
<Badge variant="secondary" className="text-xs h-5 flex-shrink-0">
|
||||
{part.quantity}x
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{/* Remark */}
|
||||
{part.remark && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 truncate">
|
||||
{part.remark}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<code className="text-sm font-mono text-muted-foreground">
|
||||
{part.formattedPartNo || part.oemCode}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => copyOemCode(part.oemCode)}
|
||||
>
|
||||
{copiedOem === part.oemCode ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Model codes */}
|
||||
{part.modelCodes && (
|
||||
<p className="text-xs text-muted-foreground font-mono truncate mt-0.5" title={part.modelCodes}>
|
||||
{part.modelCodes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
{part.prices && part.prices.length > 0 && (
|
||||
<div className="text-right flex-shrink-0">
|
||||
<div className="font-semibold text-purple-600">
|
||||
{part.prices[0].price.toLocaleString('tr-TR')} {part.prices[0].currency}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{part.prices[0].brand}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ interface Vehicle {
|
||||
}>;
|
||||
}
|
||||
|
||||
// Kategori ikonları
|
||||
// Category icons
|
||||
const categoryIcons: Record<string, any> = {
|
||||
'motor': Cog,
|
||||
'engine': Cog,
|
||||
@@ -119,7 +119,7 @@ export default function VehicleCategoriesPage() {
|
||||
}
|
||||
}, [vin]);
|
||||
|
||||
// Kategorileri filtrele
|
||||
// Filter categories
|
||||
const filteredCategories = vehicle?.categories.filter((vc) => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
@@ -135,8 +135,8 @@ export default function VehicleCategoriesPage() {
|
||||
<div className="flex h-[60vh] items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="relative mx-auto w-fit">
|
||||
<div className="h-16 w-16 rounded-full border-4 border-purple-200 dark:border-purple-900" />
|
||||
<div className="absolute inset-0 h-16 w-16 animate-spin rounded-full border-4 border-transparent border-t-purple-600" />
|
||||
<div className="h-16 w-16 rounded-full border-4 border-primary/20" />
|
||||
<div className="absolute inset-0 h-16 w-16 animate-spin rounded-full border-4 border-transparent border-t-primary" />
|
||||
</div>
|
||||
<p className="mt-6 text-muted-foreground">Parca kategorileri yukleniyor...</p>
|
||||
</div>
|
||||
@@ -153,13 +153,13 @@ export default function VehicleCategoriesPage() {
|
||||
</Button>
|
||||
<Card className="border-0 shadow-lg">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="h-20 w-20 rounded-2xl bg-red-100 dark:bg-red-900/30 flex items-center justify-center mb-6">
|
||||
<Car className="h-10 w-10 text-red-600" />
|
||||
<div className="h-20 w-20 rounded-2xl bg-destructive/10 flex items-center justify-center mb-6">
|
||||
<Car className="h-10 w-10 text-destructive" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">Arac Bulunamadi</h3>
|
||||
<p className="text-muted-foreground mb-6">{error}</p>
|
||||
<Link href="/dashboard/vehicles/search">
|
||||
<Button className="gradient-bg">Yeni Sorgulama Yap</Button>
|
||||
<Button className="gradient-primary">Yeni Sorgulama Yap</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -170,18 +170,18 @@ export default function VehicleCategoriesPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Vehicle Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 border-b">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 border-b border-border">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-xl border-2 hover:bg-purple-50 dark:hover:bg-purple-900/20 hover:border-purple-300 flex-shrink-0"
|
||||
className="rounded-xl border-2 hover:bg-primary/10 hover:border-primary/30 flex-shrink-0"
|
||||
onClick={() => router.push('/dashboard/vehicles/search')}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-12 w-12 rounded-xl gradient-bg flex items-center justify-center flex-shrink-0">
|
||||
<div className="h-12 w-12 rounded-xl gradient-primary flex items-center justify-center flex-shrink-0">
|
||||
<Car className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -190,7 +190,7 @@ export default function VehicleCategoriesPage() {
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-mono">{formatVin(vehicle.vin)}</span>
|
||||
<span className="text-purple-600 font-medium">• {vehicle.year}</span>
|
||||
<span className="text-primary font-medium">• {vehicle.year}</span>
|
||||
{vehicle.engineCode && (
|
||||
<span className="hidden md:inline">• Motor: {vehicle.engineCode}</span>
|
||||
)}
|
||||
@@ -214,7 +214,7 @@ export default function VehicleCategoriesPage() {
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Kategori ara... (ornek: motor, fren, elektrik)"
|
||||
className="pl-12 h-12 text-base border-2 focus:border-purple-500"
|
||||
className="pl-12 h-12 text-base border-2 focus:border-primary"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -224,7 +224,7 @@ export default function VehicleCategoriesPage() {
|
||||
{filteredCategories.length === 0 ? (
|
||||
<Card className="border-0 shadow-lg">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className="h-16 w-16 rounded-2xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
|
||||
<div className="h-16 w-16 rounded-2xl bg-muted flex items-center justify-center mb-4">
|
||||
<Package className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
@@ -246,21 +246,21 @@ export default function VehicleCategoriesPage() {
|
||||
key={vc.id}
|
||||
href={`/dashboard/vehicles/${vehicle.vin}/categories/${vc.category.id}`}
|
||||
>
|
||||
<Card className="border-2 border-transparent hover:border-purple-300 dark:hover:border-purple-700 hover:shadow-lg transition-all cursor-pointer group h-full">
|
||||
<Card className="border-2 border-transparent hover:border-primary/30 hover:shadow-lg transition-all cursor-pointer group h-full">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-purple-100 to-indigo-100 dark:from-purple-900/30 dark:to-indigo-900/30 flex items-center justify-center flex-shrink-0 group-hover:from-purple-200 group-hover:to-indigo-200 dark:group-hover:from-purple-800/40 dark:group-hover:to-indigo-800/40 transition-colors">
|
||||
<Icon className="h-6 w-6 text-purple-600" />
|
||||
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-primary-100 to-accent-100 flex items-center justify-center flex-shrink-0 group-hover:from-primary-200 group-hover:to-accent-200 transition-colors">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold truncate group-hover:text-purple-600 transition-colors">
|
||||
<h3 className="font-semibold truncate group-hover:text-primary transition-colors">
|
||||
{vc.category.nameTr}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{vc.partCount} parca
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-purple-600 group-hover:translate-x-1 transition-all flex-shrink-0" />
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all flex-shrink-0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -271,20 +271,20 @@ export default function VehicleCategoriesPage() {
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card className="border-0 shadow-md bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20">
|
||||
<Card className="border-0 shadow-md bg-gradient-to-r from-primary-50 to-accent-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-white dark:bg-gray-800 shadow-sm flex items-center justify-center">
|
||||
<Settings className="h-5 w-5 text-purple-600" />
|
||||
<div className="h-10 w-10 rounded-lg bg-card shadow-sm flex items-center justify-center">
|
||||
<Settings className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-purple-900 dark:text-purple-100">Arac detaylarini gor</p>
|
||||
<p className="text-sm text-purple-600 dark:text-purple-300">Tum teknik ozellikler</p>
|
||||
<p className="font-medium text-foreground">Arac detaylarini gor</p>
|
||||
<p className="text-sm text-muted-foreground">Tum teknik ozellikler</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/dashboard/vehicles/${vehicle.vin}`}>
|
||||
<Button variant="outline" className="border-purple-300 text-purple-700 hover:bg-purple-100 dark:border-purple-700 dark:text-purple-300 dark:hover:bg-purple-900/30">
|
||||
<Button variant="outline" className="border-primary/30 text-primary hover:bg-primary/10">
|
||||
Detaylari Gor
|
||||
<ChevronRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function VehicleDetailPage() {
|
||||
{vehicle.categories.map((vc) => (
|
||||
<Link
|
||||
key={vc.id}
|
||||
href={`/parts/${vehicle.id}/${vc.category.id}`}
|
||||
href={`/dashboard/vehicles/${vehicle.vin}/categories/${vc.category.id}`}
|
||||
>
|
||||
<div className="flex items-center justify-between rounded-xl border-2 border-transparent bg-gray-50 dark:bg-gray-800/50 p-4 hover:border-purple-300 dark:hover:border-purple-700 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all group">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Loader2, Search, AlertCircle, Info, Sparkles, ArrowRight } from 'lucide-react';
|
||||
import { Search, AlertCircle, Info, Sparkles, ArrowRight, Car, FileSearch } from 'lucide-react';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -43,7 +43,6 @@ export default function VehicleSearchPage() {
|
||||
const response = await apiClient.post('/vehicles/decode', { vin: data.vin });
|
||||
const vehicle = response.data.data.vehicle;
|
||||
|
||||
// Direkt parça kategorileri sayfasına yönlendir
|
||||
router.push(`/dashboard/vehicles/${vehicle.vin}/categories`);
|
||||
} catch (err: any) {
|
||||
const message = err.response?.data?.error?.message || 'VIN sorgulama basarisiz';
|
||||
@@ -59,81 +58,83 @@ export default function VehicleSearchPage() {
|
||||
};
|
||||
|
||||
const progress = Math.min((vinValue.length / 17) * 100, 100);
|
||||
const isComplete = vinValue.length === 17;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<div className="mx-auto max-w-2xl space-y-8 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-purple-200 dark:border-purple-800 bg-purple-50 dark:bg-purple-900/20 px-4 py-2 mb-4">
|
||||
<Sparkles className="h-4 w-4 text-purple-600" />
|
||||
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">Hizli Sorgulama</span>
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary-50 px-4 py-2 mb-5">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<span className="text-caption-lg text-primary">Hizli Sorgulama</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold mb-2">VIN Sorgulama</h1>
|
||||
<p className="text-muted-foreground">
|
||||
<h1 className="text-display-sm mb-3">VIN Sorgulama</h1>
|
||||
<p className="text-body-md text-muted-foreground max-w-md mx-auto">
|
||||
Arac sase numarasini girerek detayli bilgilere ulasin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search Card */}
|
||||
<Card className="border-0 shadow-xl overflow-hidden">
|
||||
<div className="h-1 bg-gray-100 dark:bg-gray-800">
|
||||
<Card variant="elevated" className="overflow-hidden">
|
||||
<div className="h-1.5 bg-secondary">
|
||||
<div
|
||||
className="h-full gradient-bg transition-all duration-300"
|
||||
className={`h-full transition-all duration-300 ${isComplete ? 'bg-success' : 'gradient-primary'}`}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg gradient-bg flex items-center justify-center">
|
||||
<Search className="h-4 w-4 text-white" />
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-xl gradient-primary flex items-center justify-center shadow-primary">
|
||||
<FileSearch className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-heading-sm">Sase Numarasi (VIN)</span>
|
||||
<p className="text-caption-md text-muted-foreground font-normal mt-0.5">17 haneli arac kimlik numarasini girin</p>
|
||||
</div>
|
||||
Sase Numarasi (VIN)
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
17 haneli arac kimlik numarasini girin
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vin" className="text-sm font-medium">VIN Numarasi</Label>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="vin" className="text-caption-lg">VIN Numarasi</Label>
|
||||
<Input
|
||||
id="vin"
|
||||
placeholder="WVWZZZ3CZWE123456"
|
||||
maxLength={17}
|
||||
className="font-mono text-lg uppercase tracking-wider h-14 text-center border-2 focus:border-purple-500 focus:ring-purple-500"
|
||||
size="xl"
|
||||
className="font-mono text-lg uppercase tracking-widest text-center"
|
||||
{...register('vin')}
|
||||
/>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className={`transition-colors ${vinValue.length === 17 ? 'text-green-600 font-medium' : 'text-muted-foreground'}`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className={`text-caption-md transition-colors ${isComplete ? 'text-success font-medium' : 'text-muted-foreground'}`}>
|
||||
{vinValue.length}/17 karakter
|
||||
{isComplete && ' - Hazir'}
|
||||
</span>
|
||||
{errors.vin && (
|
||||
<span className="text-destructive">{errors.vin.message}</span>
|
||||
<span className="text-caption-md text-destructive">{errors.vin.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-3 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4 text-sm text-red-700 dark:text-red-300">
|
||||
<AlertCircle className="h-5 w-5 flex-shrink-0" />
|
||||
<div className="flex items-center gap-3 rounded-xl bg-destructive-50 border border-destructive/20 p-4 text-body-sm text-destructive animate-slide-in-from-top">
|
||||
<div className="h-10 w-10 rounded-xl bg-destructive/10 flex items-center justify-center flex-shrink-0">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
</div>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 gradient-bg hover:opacity-90 shadow-lg shadow-purple-500/25 text-base group"
|
||||
disabled={isLoading}
|
||||
variant="gradient"
|
||||
size="xl"
|
||||
className="w-full group"
|
||||
isLoading={isLoading}
|
||||
leftIcon={!isLoading ? <Search className="h-5 w-5" /> : undefined}
|
||||
>
|
||||
{isLoading ? (
|
||||
{!isLoading && (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Sorgulanıyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="mr-2 h-5 w-5" />
|
||||
Sorgula
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
@@ -144,40 +145,36 @@ export default function VehicleSearchPage() {
|
||||
</Card>
|
||||
|
||||
{/* Info Card */}
|
||||
<Card className="border-0 shadow-md bg-gradient-to-br from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20">
|
||||
<Card variant="glass" className="border-primary/10">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-purple-900 dark:text-purple-100">
|
||||
<Info className="h-5 w-5 text-purple-600" />
|
||||
<CardTitle className="flex items-center gap-3 text-heading-sm">
|
||||
<div className="h-9 w-9 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<Info className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
VIN Nedir?
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-purple-800 dark:text-purple-200 space-y-4">
|
||||
<p>
|
||||
<CardContent className="text-body-sm text-muted-foreground space-y-5">
|
||||
<p className="leading-relaxed">
|
||||
VIN (Vehicle Identification Number), her araca ozgu 17 karakterlik bir kimlik numarasidir.
|
||||
Bu numara aracinizin ruhsatinda, kapi pervazinda veya on camin altinda bulunabilir.
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-3 bg-white/50 dark:bg-gray-900/30 rounded-lg p-3">
|
||||
<div className="h-8 w-8 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center text-xs font-bold text-purple-600">1-3</div>
|
||||
<div>
|
||||
<p className="font-medium text-purple-900 dark:text-purple-100">Uretici Kodu (WMI)</p>
|
||||
<p className="text-xs text-purple-600 dark:text-purple-300">Ulke ve uretici bilgisi</p>
|
||||
<div className="grid gap-3">
|
||||
{[
|
||||
{ range: '1-3', title: 'Uretici Kodu (WMI)', desc: 'Ulke ve uretici bilgisi', color: 'from-cyan-500 to-blue-600' },
|
||||
{ range: '4-9', title: 'Arac Ozellikleri (VDS)', desc: 'Model, motor ve donanim', color: 'from-teal-500 to-cyan-600' },
|
||||
{ range: '10-17', title: 'Uretim Bilgileri (VIS)', desc: 'Yil ve seri numarasi', color: 'from-emerald-500 to-teal-600' },
|
||||
].map((item) => (
|
||||
<div key={item.range} className="flex items-center gap-4 bg-card rounded-xl p-4 border border-border/50">
|
||||
<div className={`h-10 w-10 rounded-xl bg-gradient-to-br ${item.color} flex items-center justify-center text-xs font-bold text-white shadow-sm`}>
|
||||
{item.range}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{item.title}</p>
|
||||
<p className="text-caption-md text-muted-foreground">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-white/50 dark:bg-gray-900/30 rounded-lg p-3">
|
||||
<div className="h-8 w-8 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center text-xs font-bold text-purple-600">4-9</div>
|
||||
<div>
|
||||
<p className="font-medium text-purple-900 dark:text-purple-100">Arac Ozellikleri (VDS)</p>
|
||||
<p className="text-xs text-purple-600 dark:text-purple-300">Model, motor ve donanim</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-white/50 dark:bg-gray-900/30 rounded-lg p-3">
|
||||
<div className="h-8 w-8 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center text-xs font-bold text-purple-600">10-17</div>
|
||||
<div>
|
||||
<p className="font-medium text-purple-900 dark:text-purple-100">Uretim Bilgileri (VIS)</p>
|
||||
<p className="text-xs text-purple-600 dark:text-purple-300">Yil ve seri numarasi</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
29
apps/web/app/parts/[vehicleId]/[categoryId]/page.tsx
Normal file
29
apps/web/app/parts/[vehicleId]/[categoryId]/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ vehicleId: string; categoryId: string }>;
|
||||
}
|
||||
|
||||
export default async function PartsRedirectPage({ params }: Props) {
|
||||
const { vehicleId, categoryId } = await params;
|
||||
|
||||
// Try to fetch vehicle from API (internal server-to-server call)
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';
|
||||
const response = await fetch(`${apiUrl}/vehicles/by-id/${vehicleId}`, {
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data?.data?.vin) {
|
||||
redirect(`/dashboard/vehicles/${data.data.vin}/categories/${categoryId}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// API call failed, try direct database lookup or fallback
|
||||
}
|
||||
|
||||
// Fallback: redirect to dashboard and let user search
|
||||
redirect('/dashboard/vehicles/search');
|
||||
}
|
||||
10
apps/web/app/vehicles/[vin]/page.tsx
Normal file
10
apps/web/app/vehicles/[vin]/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ vin: string }>;
|
||||
}
|
||||
|
||||
export default async function VehicleRedirectPage({ params }: Props) {
|
||||
const { vin } = await params;
|
||||
redirect(`/dashboard/vehicles/${vin}/categories`);
|
||||
}
|
||||
@@ -3,21 +3,50 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
'inline-flex items-center rounded-full text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
// Primary
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
'bg-primary text-primary-foreground ring ring-primary/20',
|
||||
// Secondary
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
'bg-secondary text-secondary-foreground ring ring-border',
|
||||
// Outline
|
||||
outline:
|
||||
'bg-transparent text-foreground ring ring-border',
|
||||
// Destructive/Danger
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
'bg-destructive text-destructive-foreground ring ring-destructive/20',
|
||||
// Success
|
||||
success:
|
||||
'bg-success text-success-foreground ring ring-success/20',
|
||||
// Warning
|
||||
warning:
|
||||
'bg-warning text-warning-foreground ring ring-warning/20',
|
||||
// Accent
|
||||
accent:
|
||||
'bg-accent text-accent-foreground ring ring-accent/20',
|
||||
// Soft variants
|
||||
'soft-primary':
|
||||
'bg-primary-50 text-primary ring ring-primary/10',
|
||||
'soft-destructive':
|
||||
'bg-destructive-50 text-destructive ring ring-destructive/10',
|
||||
'soft-success':
|
||||
'bg-success-50 text-success ring ring-success/10',
|
||||
'soft-warning':
|
||||
'bg-warning-50 text-warning ring ring-warning/10',
|
||||
},
|
||||
size: {
|
||||
sm: 'px-2 py-0.5 text-[10px]',
|
||||
default: 'px-2.5 py-0.5',
|
||||
lg: 'px-3 py-1 text-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -26,9 +55,9 @@ export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
function Badge({ className, variant, size, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
<div className={cn(badgeVariants({ variant, size }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,76 @@
|
||||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.98]',
|
||||
'relative inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium select-none cursor-pointer transition-all focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm hover:shadow-md',
|
||||
outline: 'border-2 border-input bg-background hover:bg-accent hover:text-accent-foreground hover:border-accent',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
// Primary - Selia style with inset shadow
|
||||
default:
|
||||
'bg-primary text-primary-foreground ring ring-primary/20 shadow-sm inset-shadow-white/15 hover:bg-primary/90 active:scale-[0.98]',
|
||||
// Secondary
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground ring ring-border hover:bg-secondary/80 active:scale-[0.98]',
|
||||
// Tertiary
|
||||
tertiary:
|
||||
'bg-tertiary text-tertiary-foreground ring ring-border hover:bg-tertiary/80 active:scale-[0.98]',
|
||||
// Danger/Destructive
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground ring ring-destructive/20 shadow-sm inset-shadow-white/15 hover:bg-destructive/90 active:scale-[0.98]',
|
||||
// Outline - Ring based border
|
||||
outline:
|
||||
'bg-transparent ring ring-border text-foreground hover:bg-muted hover:ring-border-secondary active:bg-muted/80',
|
||||
// Ghost
|
||||
ghost:
|
||||
'text-foreground hover:bg-muted active:bg-muted/80',
|
||||
// Plain/Link
|
||||
plain:
|
||||
'text-foreground hover:text-primary underline-offset-4 hover:underline',
|
||||
// Link
|
||||
link:
|
||||
'text-primary underline-offset-4 hover:underline',
|
||||
// Gradient - Selia premium style
|
||||
gradient:
|
||||
'gradient-primary text-white shadow-md ring ring-primary/20 inset-shadow-white/15 hover:opacity-95 active:scale-[0.98]',
|
||||
// Success
|
||||
success:
|
||||
'bg-success text-success-foreground ring ring-success/20 shadow-sm inset-shadow-white/15 hover:bg-success/90 active:scale-[0.98]',
|
||||
// Warning
|
||||
warning:
|
||||
'bg-warning text-warning-foreground ring ring-warning/20 shadow-sm hover:bg-warning/90 active:scale-[0.98]',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-5 py-2',
|
||||
sm: 'h-9 rounded-lg px-4',
|
||||
lg: 'h-12 rounded-xl px-8',
|
||||
icon: 'h-10 w-10',
|
||||
xs: 'h-7 px-2.5 text-xs rounded-md [&_svg]:size-3',
|
||||
sm: 'h-8 px-3 text-sm rounded-lg [&_svg]:size-3.5',
|
||||
default: 'h-9 px-4 text-sm rounded-lg [&_svg]:size-4',
|
||||
md: 'h-10 px-5 text-sm rounded-lg [&_svg]:size-4',
|
||||
lg: 'h-11 px-6 text-base rounded-xl [&_svg]:size-5',
|
||||
xl: 'h-12 px-8 text-base font-semibold rounded-xl [&_svg]:size-5',
|
||||
// Icon sizes
|
||||
'icon-xs': 'h-7 w-7 rounded-md [&_svg]:size-3.5',
|
||||
'icon-sm': 'h-8 w-8 rounded-lg [&_svg]:size-4',
|
||||
icon: 'h-9 w-9 rounded-lg [&_svg]:size-4',
|
||||
'icon-md': 'h-10 w-10 rounded-lg [&_svg]:size-5',
|
||||
'icon-lg': 'h-11 w-11 rounded-xl [&_svg]:size-5',
|
||||
},
|
||||
pill: {
|
||||
true: 'rounded-full',
|
||||
false: '',
|
||||
},
|
||||
block: {
|
||||
true: 'w-full',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
pill: false,
|
||||
block: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -33,12 +79,35 @@ export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
isLoading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
({ className, variant, size, pill, block, asChild = false, isLoading, leftIcon, rightIcon, children, disabled, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, pill, block, className }))}
|
||||
ref={ref}
|
||||
disabled={disabled || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin" />
|
||||
<span>{children}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{leftIcon && <span className="inline-flex">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="inline-flex">{rightIcon}</span>}
|
||||
</>
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
@@ -1,51 +1,134 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
const cardVariants = cva(
|
||||
'rounded-xl bg-card text-card-foreground ring ring-card-border shadow-card transition-all',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: '',
|
||||
elevated: 'shadow-md hover:shadow-lg',
|
||||
outline: 'bg-transparent shadow-none',
|
||||
ghost: 'ring-0 bg-transparent shadow-none',
|
||||
glass: 'glass ring-0',
|
||||
gradient: 'gradient-border ring-0 bg-card',
|
||||
interactive: 'cursor-pointer hover:ring-border-secondary hover:shadow-md',
|
||||
success: 'ring-success/30 bg-success-50',
|
||||
warning: 'ring-warning/30 bg-warning-50',
|
||||
destructive: 'ring-destructive/30 bg-destructive-50',
|
||||
},
|
||||
padding: {
|
||||
none: '',
|
||||
sm: 'p-4',
|
||||
default: 'p-6',
|
||||
lg: 'p-8',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
padding: 'none',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface CardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof cardVariants> {}
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant, padding, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-xl border bg-card text-card-foreground shadow-sm transition-all duration-300',
|
||||
className
|
||||
)}
|
||||
className={cn(cardVariants({ variant, padding }), className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
||||
),
|
||||
);
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
compact?: boolean;
|
||||
align?: 'default' | 'center' | 'right';
|
||||
}
|
||||
>(({ className, compact, align = 'default', ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-col border-b border-card-separator',
|
||||
compact ? 'gap-y-1 p-4' : 'gap-y-1.5 p-6',
|
||||
align === 'center' && 'items-center text-center',
|
||||
align === 'right' && 'items-end text-right',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
|
||||
),
|
||||
);
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { as?: 'h1' | 'h2' | 'h3' | 'h4' }
|
||||
>(({ className, as: Component = 'h3', ...props }, ref) => (
|
||||
<Component
|
||||
ref={ref as React.Ref<HTMLHeadingElement>}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight text-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
),
|
||||
);
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
|
||||
);
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { compact?: boolean }
|
||||
>(({ className, compact, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(compact ? 'p-4' : 'p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
||||
),
|
||||
);
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { compact?: boolean }
|
||||
>(({ className, compact, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 border-t border-card-separator bg-card-footer rounded-b-xl',
|
||||
compact ? 'p-4' : 'p-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
cardVariants,
|
||||
};
|
||||
|
||||
@@ -1,23 +1,108 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
const inputVariants = cva(
|
||||
'flex w-full rounded-lg text-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-dimmed focus:outline-0 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
// Default - Selia ring-based border
|
||||
default:
|
||||
'bg-input ring ring-input-border hover:not-disabled:not-focus:ring-input-accent-border focus:ring-2 focus:ring-primary',
|
||||
// Subtle - Transparent background
|
||||
subtle:
|
||||
'bg-input/60 ring ring-input-border hover:not-disabled:not-focus:ring-input-accent-border focus:ring-2 focus:ring-primary',
|
||||
// Filled
|
||||
filled:
|
||||
'bg-secondary ring-0 focus:ring-2 focus:ring-primary',
|
||||
// Ghost
|
||||
ghost:
|
||||
'bg-transparent ring-0 hover:bg-muted focus:ring-2 focus:ring-primary',
|
||||
// Error
|
||||
error:
|
||||
'bg-destructive-50 ring ring-destructive/50 text-destructive focus:ring-2 focus:ring-destructive',
|
||||
// Success
|
||||
success:
|
||||
'bg-success-50 ring ring-success/50 text-success focus:ring-2 focus:ring-success',
|
||||
},
|
||||
size: {
|
||||
xs: 'h-7 px-2.5 text-xs',
|
||||
sm: 'h-8 px-3 text-sm',
|
||||
default: 'h-9 px-3.5',
|
||||
md: 'h-10 px-4',
|
||||
lg: 'h-11 px-4 text-base',
|
||||
xl: 'h-12 px-5 text-base',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface InputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
|
||||
VariantProps<typeof inputVariants> {
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
error?: string;
|
||||
success?: string;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
type,
|
||||
variant,
|
||||
size,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
error,
|
||||
success,
|
||||
wrapperClassName,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const inputVariant = error ? 'error' : success ? 'success' : variant;
|
||||
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
<div className={cn('relative w-full', wrapperClassName)}>
|
||||
{leftIcon && (
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dimmed pointer-events-none">
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
inputVariants({ variant: inputVariant, size }),
|
||||
leftIcon && 'pl-10',
|
||||
rightIcon && 'pr-10',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{rightIcon && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-dimmed">
|
||||
{rightIcon}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<p className="mt-1.5 text-xs text-destructive animate-fade-in">{error}</p>
|
||||
)}
|
||||
{success && !error && (
|
||||
<p className="mt-1.5 text-xs text-success animate-fade-in">{success}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
export { Input, inputVariants };
|
||||
|
||||
@@ -6,15 +6,37 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
'text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: '',
|
||||
muted: 'text-muted-foreground font-normal',
|
||||
dimmed: 'text-dimmed font-normal',
|
||||
},
|
||||
size: {
|
||||
sm: 'text-xs',
|
||||
default: 'text-sm',
|
||||
lg: 'text-base',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
export { Label, labelVariants };
|
||||
|
||||
178
apps/web/components/ui/theme-toggle.tsx
Normal file
178
apps/web/components/ui/theme-toggle.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Sun, Moon, Monitor } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ThemeToggleProps {
|
||||
variant?: 'default' | 'compact' | 'dropdown';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ThemeToggle({ variant = 'default', className }: ThemeToggleProps) {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className={cn('h-9 w-9 rounded-lg bg-muted animate-pulse', className)} />
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'compact') {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
||||
className={cn(
|
||||
'relative h-9 w-9 rounded-lg transition-colors',
|
||||
'hover:bg-primary/10 hover:text-primary',
|
||||
className
|
||||
)}
|
||||
aria-label={`Switch to ${resolvedTheme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
>
|
||||
<Sun className={cn(
|
||||
'h-[1.125rem] w-[1.125rem] transition-all duration-300',
|
||||
resolvedTheme === 'dark' ? 'rotate-0 scale-100' : 'rotate-90 scale-0'
|
||||
)} />
|
||||
<Moon className={cn(
|
||||
'absolute h-[1.125rem] w-[1.125rem] transition-all duration-300',
|
||||
resolvedTheme === 'dark' ? 'rotate-90 scale-0' : 'rotate-0 scale-100'
|
||||
)} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: segmented toggle with all three options
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-lg p-1 gap-0.5',
|
||||
'bg-muted/50 border border-border',
|
||||
className
|
||||
)}
|
||||
role="radiogroup"
|
||||
aria-label="Theme selection"
|
||||
>
|
||||
{[
|
||||
{ value: 'light', icon: Sun, label: 'Aydinlik' },
|
||||
{ value: 'dark', icon: Moon, label: 'Karanlik' },
|
||||
{ value: 'system', icon: Monitor, label: 'Sistem' },
|
||||
].map(({ value, icon: Icon, label }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => setTheme(value)}
|
||||
className={cn(
|
||||
'relative flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
theme === value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
|
||||
)}
|
||||
role="radio"
|
||||
aria-checked={theme === value}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-4 w-4 mr-1.5" />
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Dropdown version for mobile
|
||||
export function ThemeToggleDropdown({ className }: { className?: string }) {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = () => setIsOpen(false);
|
||||
if (isOpen) {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className={cn('h-9 w-9 rounded-lg bg-muted animate-pulse', className)} />;
|
||||
}
|
||||
|
||||
const themes = [
|
||||
{ value: 'light', icon: Sun, label: 'Aydinlik Mod' },
|
||||
{ value: 'dark', icon: Moon, label: 'Karanlik Mod' },
|
||||
{ value: 'system', icon: Monitor, label: 'Sistem Ayari' },
|
||||
];
|
||||
|
||||
const currentTheme = themes.find(t => t.value === theme) || themes[2];
|
||||
const CurrentIcon = resolvedTheme === 'dark' ? Moon : Sun;
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className={cn(
|
||||
'h-9 w-9 rounded-lg transition-colors',
|
||||
'hover:bg-primary/10 hover:text-primary',
|
||||
isOpen && 'bg-primary/10 text-primary'
|
||||
)}
|
||||
aria-label="Tema sec"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<CurrentIcon className="h-[1.125rem] w-[1.125rem]" />
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-full mt-2 z-50',
|
||||
'min-w-[160px] rounded-lg border border-border bg-popover p-1 shadow-lg',
|
||||
'animate-scale-in origin-top-right'
|
||||
)}
|
||||
role="menu"
|
||||
>
|
||||
{themes.map(({ value, icon: Icon, label }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => {
|
||||
setTheme(value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
theme === value
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-foreground hover:bg-muted'
|
||||
)}
|
||||
role="menuitem"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
{theme === value && (
|
||||
<span className="ml-auto h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,12 +24,14 @@ const ToastViewport = React.forwardRef<
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-4 pr-8 ring ring-card-border shadow-card transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border bg-background text-foreground',
|
||||
destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
default: 'bg-card text-foreground',
|
||||
destructive: 'bg-destructive text-destructive-foreground ring-destructive/20',
|
||||
success: 'bg-success text-success-foreground ring-success/20',
|
||||
warning: 'bg-warning text-warning-foreground ring-warning/20',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -53,7 +55,7 @@ const ToastAction = React.forwardRef<
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
||||
'inline-flex h-8 shrink-0 items-center justify-center rounded-lg ring ring-border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:ring-destructive/30 group-[.destructive]:hover:bg-destructive/90 group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -68,7 +70,7 @@ const ToastClose = React.forwardRef<
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||
'absolute right-2 top-2 rounded-lg p-1.5 text-foreground/50 opacity-0 transition-all hover:text-foreground hover:bg-muted focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring group-hover:opacity-100 group-[.destructive]:text-destructive-foreground/70 group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:hover:bg-destructive/80',
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
@@ -91,7 +93,7 @@ const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description ref={ref} className={cn('text-sm opacity-90', className)} {...props} />
|
||||
<ToastPrimitives.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
|
||||
@@ -2,50 +2,218 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/*
|
||||
* SASE.TR Design System - Selia Inspired
|
||||
* Modern, minimal design with ring-based borders
|
||||
* Clean typography and subtle shadows
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* ===== LIGHT MODE - Selia Inspired ===== */
|
||||
|
||||
/* Core Backgrounds - Clean whites */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71% 4%;
|
||||
--background-secondary: 240 5% 96%;
|
||||
--background-tertiary: 240 5% 93%;
|
||||
--foreground: 240 10% 4%;
|
||||
--foreground-secondary: 240 5% 46%;
|
||||
|
||||
/* Surface Colors - Cards, Popovers */
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71% 4%;
|
||||
--card-foreground: 240 10% 4%;
|
||||
--card-hover: 240 5% 97%;
|
||||
--card-footer: 240 5% 97%;
|
||||
--card-separator: 240 6% 90%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71% 4%;
|
||||
--primary: 262 83% 58%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 220 14% 96%;
|
||||
--secondary-foreground: 220 9% 46%;
|
||||
--muted: 220 14% 96%;
|
||||
--muted-foreground: 220 9% 46%;
|
||||
--accent: 262 83% 58%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
|
||||
/* Primary - Modern Blue */
|
||||
--primary: 221 83% 53%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-50: 214 100% 97%;
|
||||
--primary-100: 214 95% 93%;
|
||||
--primary-200: 213 97% 87%;
|
||||
--primary-300: 212 96% 78%;
|
||||
--primary-400: 213 94% 68%;
|
||||
--primary-500: 217 91% 60%;
|
||||
--primary-600: 221 83% 53%;
|
||||
--primary-700: 224 76% 48%;
|
||||
--primary-800: 226 71% 40%;
|
||||
--primary-900: 224 64% 33%;
|
||||
|
||||
/* Secondary - Soft Gray */
|
||||
--secondary: 240 5% 96%;
|
||||
--secondary-foreground: 240 6% 10%;
|
||||
|
||||
/* Tertiary */
|
||||
--tertiary: 240 5% 93%;
|
||||
--tertiary-foreground: 240 6% 25%;
|
||||
|
||||
/* Muted - Subtle grays */
|
||||
--muted: 240 5% 96%;
|
||||
--muted-foreground: 240 4% 46%;
|
||||
|
||||
/* Dimmed text */
|
||||
--dimmed: 240 5% 65%;
|
||||
|
||||
/* Accent - Teal/Cyan */
|
||||
--accent: 173 80% 40%;
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--accent-50: 174 72% 95%;
|
||||
--accent-100: 175 77% 88%;
|
||||
--accent-500: 173 80% 40%;
|
||||
|
||||
/* Semantic Colors */
|
||||
--success: 142 71% 45%;
|
||||
--success-foreground: 0 0% 100%;
|
||||
--success-50: 143 64% 95%;
|
||||
--success-100: 141 79% 85%;
|
||||
--success-500: 142 71% 45%;
|
||||
|
||||
--warning: 38 92% 50%;
|
||||
--warning-foreground: 38 92% 10%;
|
||||
--warning-50: 48 96% 95%;
|
||||
--warning-100: 48 96% 85%;
|
||||
--warning-500: 38 92% 50%;
|
||||
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 262 83% 58%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--destructive-50: 0 86% 97%;
|
||||
--destructive-100: 0 93% 94%;
|
||||
--destructive-500: 0 84% 60%;
|
||||
|
||||
/* Danger alias */
|
||||
--danger: 0 84% 60%;
|
||||
--danger-foreground: 0 0% 100%;
|
||||
|
||||
/* Borders & Inputs - Ring based system */
|
||||
--border: 240 6% 90%;
|
||||
--border-secondary: 240 5% 85%;
|
||||
--input: 0 0% 100%;
|
||||
--input-border: 240 6% 90%;
|
||||
--input-accent-border: 240 5% 75%;
|
||||
--ring: 221 83% 53%;
|
||||
|
||||
/* Card specific */
|
||||
--card-border: 240 6% 90%;
|
||||
--card-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.04), 0 1px 2px -1px rgb(0 0 0 / 0.04);
|
||||
|
||||
/* Radius - Selia style rounded corners */
|
||||
--radius: 0.75rem;
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Shadows - Minimal, subtle */
|
||||
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.03);
|
||||
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.04), 0 1px 2px -1px rgb(0 0 0 / 0.04);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.05), 0 4px 6px -4px rgb(0 0 0 / 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.05), 0 8px 10px -6px rgb(0 0 0 / 0.05);
|
||||
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.04), 0 1px 2px -1px rgb(0 0 0 / 0.04);
|
||||
--shadow-primary: 0 4px 14px 0 hsl(var(--primary) / 0.15);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 224 71% 8%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 224 71% 8%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 263 70% 50%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 215 28% 17%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 215 28% 17%;
|
||||
--muted-foreground: 217 10% 65%;
|
||||
--accent: 263 70% 50%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62% 30%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 215 28% 17%;
|
||||
--input: 215 28% 17%;
|
||||
--ring: 263 70% 50%;
|
||||
/* ===== DARK MODE - Selia Inspired ===== */
|
||||
|
||||
/* Core Backgrounds - Deep grays */
|
||||
--background: 240 10% 4%;
|
||||
--background-secondary: 240 6% 10%;
|
||||
--background-tertiary: 240 5% 14%;
|
||||
--foreground: 0 0% 98%;
|
||||
--foreground-secondary: 240 5% 65%;
|
||||
|
||||
/* Surface Colors */
|
||||
--card: 240 6% 10%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--card-hover: 240 5% 14%;
|
||||
--card-footer: 240 5% 8%;
|
||||
--card-separator: 240 4% 18%;
|
||||
--popover: 240 6% 10%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
/* Primary - Brighter for dark mode */
|
||||
--primary: 217 91% 60%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--primary-50: 224 71% 12%;
|
||||
--primary-100: 224 70% 16%;
|
||||
--primary-200: 224 70% 22%;
|
||||
--primary-300: 217 85% 35%;
|
||||
--primary-400: 217 88% 48%;
|
||||
--primary-500: 217 91% 60%;
|
||||
--primary-600: 213 94% 68%;
|
||||
--primary-700: 212 96% 78%;
|
||||
--primary-800: 213 97% 87%;
|
||||
--primary-900: 214 95% 93%;
|
||||
|
||||
/* Secondary */
|
||||
--secondary: 240 4% 16%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
/* Tertiary */
|
||||
--tertiary: 240 5% 20%;
|
||||
--tertiary-foreground: 240 5% 80%;
|
||||
|
||||
/* Muted */
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 240 5% 55%;
|
||||
|
||||
/* Dimmed */
|
||||
--dimmed: 240 5% 45%;
|
||||
|
||||
/* Accent */
|
||||
--accent: 173 80% 50%;
|
||||
--accent-foreground: 240 10% 4%;
|
||||
--accent-50: 175 50% 12%;
|
||||
--accent-100: 175 55% 18%;
|
||||
--accent-500: 173 80% 50%;
|
||||
|
||||
/* Semantic Colors */
|
||||
--success: 142 71% 50%;
|
||||
--success-foreground: 142 80% 10%;
|
||||
--success-50: 142 50% 12%;
|
||||
--success-100: 142 55% 18%;
|
||||
--success-500: 142 71% 50%;
|
||||
|
||||
--warning: 38 92% 55%;
|
||||
--warning-foreground: 38 92% 10%;
|
||||
--warning-50: 38 50% 12%;
|
||||
--warning-100: 38 55% 18%;
|
||||
--warning-500: 38 92% 55%;
|
||||
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--destructive-50: 0 50% 12%;
|
||||
--destructive-100: 0 55% 18%;
|
||||
--destructive-500: 0 84% 60%;
|
||||
|
||||
/* Danger alias */
|
||||
--danger: 0 84% 60%;
|
||||
--danger-foreground: 0 0% 100%;
|
||||
|
||||
/* Borders & Inputs */
|
||||
--border: 240 4% 18%;
|
||||
--border-secondary: 240 4% 24%;
|
||||
--input: 240 6% 10%;
|
||||
--input-border: 240 4% 18%;
|
||||
--input-accent-border: 240 4% 30%;
|
||||
--ring: 217 91% 60%;
|
||||
|
||||
/* Card specific */
|
||||
--card-border: 240 4% 18%;
|
||||
--card-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.2), 0 1px 2px -1px rgb(0 0 0 / 0.2);
|
||||
|
||||
/* Shadows - Darker for dark mode */
|
||||
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.2);
|
||||
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.25), 0 1px 2px -1px rgb(0 0 0 / 0.25);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3);
|
||||
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.2), 0 1px 2px -1px rgb(0 0 0 / 0.2);
|
||||
--shadow-primary: 0 4px 20px 0 hsl(var(--primary) / 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,27 +221,237 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
}
|
||||
|
||||
/* Focus states for accessibility - Ring based */
|
||||
:focus-visible {
|
||||
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
@apply bg-primary/20 text-foreground;
|
||||
}
|
||||
|
||||
/* Disable transitions for reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* ===== SELIA GRADIENT UTILITIES ===== */
|
||||
|
||||
.gradient-primary {
|
||||
@apply bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-600;
|
||||
}
|
||||
|
||||
.dark .gradient-primary {
|
||||
@apply from-blue-400 via-blue-500 to-indigo-500;
|
||||
}
|
||||
|
||||
.gradient-primary-soft {
|
||||
@apply bg-gradient-to-br from-blue-500/10 via-blue-600/10 to-indigo-600/10;
|
||||
}
|
||||
|
||||
.gradient-accent {
|
||||
@apply bg-gradient-to-r from-teal-500 via-cyan-500 to-blue-500;
|
||||
}
|
||||
|
||||
.gradient-success {
|
||||
@apply bg-gradient-to-r from-emerald-500 to-teal-500;
|
||||
}
|
||||
|
||||
.gradient-warning {
|
||||
@apply bg-gradient-to-r from-amber-500 to-orange-500;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
@apply bg-gradient-to-r from-blue-600 via-indigo-600 to-violet-600 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.dark .gradient-text {
|
||||
@apply from-blue-400 via-indigo-400 to-violet-400;
|
||||
}
|
||||
|
||||
.gradient-text-accent {
|
||||
@apply bg-gradient-to-r from-teal-500 via-cyan-500 to-blue-500 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
/* Gradient border - Selia style */
|
||||
.gradient-border {
|
||||
position: relative;
|
||||
background: linear-gradient(hsl(var(--card)), hsl(var(--card))) padding-box,
|
||||
linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent))) border-box;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* Legacy support */
|
||||
.gradient-bg {
|
||||
@apply gradient-primary;
|
||||
}
|
||||
|
||||
/* ===== GLASS MORPHISM - Selia Style ===== */
|
||||
|
||||
.glass {
|
||||
@apply backdrop-blur-xl;
|
||||
background: hsl(var(--background) / 0.8);
|
||||
border: 1px solid hsl(var(--border) / 0.5);
|
||||
}
|
||||
|
||||
.glass-strong {
|
||||
@apply backdrop-blur-2xl;
|
||||
background: hsl(var(--background) / 0.9);
|
||||
border: 1px solid hsl(var(--border) / 0.6);
|
||||
}
|
||||
|
||||
/* ===== CARD EFFECTS - Selia Style ===== */
|
||||
|
||||
.card-hover {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-hover-subtle {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
.card-hover-subtle:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* ===== RING SYSTEM - Selia Core ===== */
|
||||
|
||||
.ring-card-border {
|
||||
--tw-ring-color: hsl(var(--card-border));
|
||||
}
|
||||
|
||||
.ring-input-border {
|
||||
--tw-ring-color: hsl(var(--input-border));
|
||||
}
|
||||
|
||||
.ring-input-accent-border {
|
||||
--tw-ring-color: hsl(var(--input-accent-border));
|
||||
}
|
||||
|
||||
/* ===== SHADOW SYSTEM ===== */
|
||||
|
||||
.shadow-card {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.shadow-primary {
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.shadow-primary-lg {
|
||||
box-shadow: 0 10px 25px -3px hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
/* ===== INTERACTIVE STATES - Selia Style ===== */
|
||||
|
||||
.interactive {
|
||||
@apply transition-colors duration-150 cursor-pointer;
|
||||
}
|
||||
|
||||
.interactive:hover {
|
||||
background: hsl(var(--primary) / 0.06);
|
||||
}
|
||||
|
||||
.interactive:active {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.interactive-scale {
|
||||
@apply transition-transform duration-150 cursor-pointer;
|
||||
}
|
||||
|
||||
.interactive-scale:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.interactive-scale:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* ===== SURFACE UTILITIES ===== */
|
||||
|
||||
.surface {
|
||||
background: hsl(var(--card));
|
||||
@apply ring ring-card-border;
|
||||
}
|
||||
|
||||
.surface-raised {
|
||||
background: hsl(var(--card));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.surface-elevated {
|
||||
background: hsl(var(--card));
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* ===== SKELETON LOADING ===== */
|
||||
|
||||
.skeleton {
|
||||
@apply animate-pulse rounded;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
/* ===== STATUS INDICATORS ===== */
|
||||
|
||||
.status-dot {
|
||||
@apply h-2 w-2 rounded-full;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@apply status-dot;
|
||||
background: hsl(var(--success));
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
@apply status-dot;
|
||||
background: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.status-busy {
|
||||
@apply status-dot;
|
||||
background: hsl(var(--warning));
|
||||
}
|
||||
|
||||
/* ===== INSET SHADOW - Selia Button Style ===== */
|
||||
|
||||
.inset-shadow-white {
|
||||
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.inset-shadow-white\/15 {
|
||||
box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.gradient-text {
|
||||
@apply bg-gradient-to-r from-violet-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
@apply bg-gradient-to-br from-violet-600 via-purple-600 to-indigo-600;
|
||||
}
|
||||
|
||||
.glass {
|
||||
@apply backdrop-blur-xl bg-white/70 dark:bg-gray-900/70 border border-white/20;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply transition-all duration-300 hover:shadow-xl hover:shadow-purple-500/10 hover:-translate-y-1;
|
||||
}
|
||||
/* ===== ANIMATIONS ===== */
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
@@ -87,8 +465,107 @@
|
||||
background-size: 200% 200%;
|
||||
animation: gradient 8s ease infinite;
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
animation: shimmer 2s linear infinite;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
hsl(var(--primary) / 0.1) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-down {
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.animate-bounce-soft {
|
||||
animation: bounceSoft 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ===== TEXT UTILITIES ===== */
|
||||
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.text-pretty {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.text-dimmed {
|
||||
color: hsl(var(--dimmed));
|
||||
}
|
||||
|
||||
/* ===== SCROLLBAR - Selia Style ===== */
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border-secondary)) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border-secondary));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
/* ===== THEME-AWARE HELPERS ===== */
|
||||
|
||||
.bg-surface {
|
||||
background: hsl(var(--background-secondary));
|
||||
}
|
||||
|
||||
.bg-surface-alt {
|
||||
background: hsl(var(--background-tertiary));
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: hsl(var(--foreground-secondary));
|
||||
}
|
||||
|
||||
.border-subtle {
|
||||
border-color: hsl(var(--border-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== KEYFRAMES ===== */
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
@@ -99,3 +576,64 @@
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceSoft {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
|
||||
/* ===== PRINT STYLES ===== */
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,29 +18,78 @@ const config: Config = {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
'border-secondary': 'hsl(var(--border-secondary))',
|
||||
input: 'hsl(var(--input))',
|
||||
'input-border': 'hsl(var(--input-border))',
|
||||
'input-accent-border': 'hsl(var(--input-accent-border))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
background: {
|
||||
DEFAULT: 'hsl(var(--background))',
|
||||
secondary: 'hsl(var(--background-secondary))',
|
||||
tertiary: 'hsl(var(--background-tertiary))',
|
||||
},
|
||||
foreground: {
|
||||
DEFAULT: 'hsl(var(--foreground))',
|
||||
secondary: 'hsl(var(--foreground-secondary))',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
50: 'hsl(var(--primary-50))',
|
||||
100: 'hsl(var(--primary-100))',
|
||||
200: 'hsl(var(--primary-200))',
|
||||
300: 'hsl(var(--primary-300))',
|
||||
400: 'hsl(var(--primary-400))',
|
||||
500: 'hsl(var(--primary-500))',
|
||||
600: 'hsl(var(--primary-600))',
|
||||
700: 'hsl(var(--primary-700))',
|
||||
800: 'hsl(var(--primary-800))',
|
||||
900: 'hsl(var(--primary-900))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
tertiary: {
|
||||
DEFAULT: 'hsl(var(--tertiary))',
|
||||
foreground: 'hsl(var(--tertiary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
50: 'hsl(var(--destructive-50))',
|
||||
100: 'hsl(var(--destructive-100))',
|
||||
500: 'hsl(var(--destructive-500))',
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: 'hsl(var(--danger))',
|
||||
foreground: 'hsl(var(--danger-foreground))',
|
||||
},
|
||||
success: {
|
||||
DEFAULT: 'hsl(var(--success))',
|
||||
foreground: 'hsl(var(--success-foreground))',
|
||||
50: 'hsl(var(--success-50))',
|
||||
100: 'hsl(var(--success-100))',
|
||||
500: 'hsl(var(--success-500))',
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: 'hsl(var(--warning))',
|
||||
foreground: 'hsl(var(--warning-foreground))',
|
||||
50: 'hsl(var(--warning-50))',
|
||||
100: 'hsl(var(--warning-100))',
|
||||
500: 'hsl(var(--warning-500))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
dimmed: 'hsl(var(--dimmed))',
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
50: 'hsl(var(--accent-50))',
|
||||
100: 'hsl(var(--accent-100))',
|
||||
500: 'hsl(var(--accent-500))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
@@ -49,12 +98,52 @@ const config: Config = {
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
hover: 'hsl(var(--card-hover))',
|
||||
footer: 'hsl(var(--card-footer))',
|
||||
separator: 'hsl(var(--card-separator))',
|
||||
border: 'hsl(var(--card-border))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
xl: 'var(--radius-xl)',
|
||||
full: 'var(--radius-full)',
|
||||
},
|
||||
fontSize: {
|
||||
// Display sizes
|
||||
'display-lg': ['3.5rem', { lineHeight: '1.1', letterSpacing: '-0.02em', fontWeight: '700' }],
|
||||
'display-md': ['2.875rem', { lineHeight: '1.15', letterSpacing: '-0.01em', fontWeight: '700' }],
|
||||
'display-sm': ['2.25rem', { lineHeight: '1.2', letterSpacing: '-0.005em', fontWeight: '700' }],
|
||||
// Heading sizes
|
||||
'heading-lg': ['2rem', { lineHeight: '1.3', letterSpacing: '-0.005em', fontWeight: '600' }],
|
||||
'heading-md': ['1.5rem', { lineHeight: '1.35', fontWeight: '600' }],
|
||||
'heading-sm': ['1.25rem', { lineHeight: '1.4', fontWeight: '600' }],
|
||||
// Body sizes
|
||||
'body-lg': ['1.125rem', { lineHeight: '1.6', fontWeight: '400' }],
|
||||
'body-md': ['1rem', { lineHeight: '1.6', fontWeight: '400' }],
|
||||
'body-sm': ['0.9375rem', { lineHeight: '1.5', fontWeight: '400' }],
|
||||
// Caption sizes
|
||||
'caption-lg': ['0.875rem', { lineHeight: '1.5', fontWeight: '500' }],
|
||||
'caption-md': ['0.8125rem', { lineHeight: '1.4', fontWeight: '500' }],
|
||||
'caption-sm': ['0.75rem', { lineHeight: '1.3', fontWeight: '500' }],
|
||||
},
|
||||
spacing: {
|
||||
'18': '4.5rem',
|
||||
'22': '5.5rem',
|
||||
'26': '6.5rem',
|
||||
'30': '7.5rem',
|
||||
},
|
||||
boxShadow: {
|
||||
'xs': 'var(--shadow-xs)',
|
||||
'sm': 'var(--shadow-sm)',
|
||||
'md': 'var(--shadow-md)',
|
||||
'lg': 'var(--shadow-lg)',
|
||||
'xl': 'var(--shadow-xl)',
|
||||
'card': 'var(--shadow-card)',
|
||||
'primary': 'var(--shadow-primary)',
|
||||
'primary-lg': '0 10px 25px -3px hsl(var(--primary) / 0.2)',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
@@ -65,10 +154,61 @@ const config: Config = {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
'fade-in': {
|
||||
from: { opacity: '0' },
|
||||
to: { opacity: '1' },
|
||||
},
|
||||
'fade-out': {
|
||||
from: { opacity: '1' },
|
||||
to: { opacity: '0' },
|
||||
},
|
||||
'slide-in-from-top': {
|
||||
from: { transform: 'translateY(-8px)', opacity: '0' },
|
||||
to: { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
'slide-in-from-bottom': {
|
||||
from: { transform: 'translateY(8px)', opacity: '0' },
|
||||
to: { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
'scale-in': {
|
||||
from: { transform: 'scale(0.96)', opacity: '0' },
|
||||
to: { transform: 'scale(1)', opacity: '1' },
|
||||
},
|
||||
'spin-slow': {
|
||||
from: { transform: 'rotate(0deg)' },
|
||||
to: { transform: 'rotate(360deg)' },
|
||||
},
|
||||
'pulse-soft': {
|
||||
'0%, 100%': { opacity: '1' },
|
||||
'50%': { opacity: '0.7' },
|
||||
},
|
||||
'bounce-soft': {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-4px)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'fade-in': 'fade-in 0.2s ease-out',
|
||||
'fade-out': 'fade-out 0.2s ease-out',
|
||||
'slide-in-from-top': 'slide-in-from-top 0.2s ease-out',
|
||||
'slide-in-from-bottom': 'slide-in-from-bottom 0.2s ease-out',
|
||||
'scale-in': 'scale-in 0.15s ease-out',
|
||||
'spin-slow': 'spin-slow 3s linear infinite',
|
||||
'pulse-soft': 'pulse-soft 2s ease-in-out infinite',
|
||||
'bounce-soft': 'bounce-soft 2s ease-in-out infinite',
|
||||
},
|
||||
transitionDuration: {
|
||||
'400': '400ms',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
'bounce-in': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
|
||||
'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user