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:
Semih
2026-01-22 11:15:06 +01:00
parent b9532a61ee
commit 2358b38b4d
155 changed files with 31575 additions and 1109 deletions

2516
apps/web/CLAUDE.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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&apos;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>

View File

@@ -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&apos;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&apos;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&apos;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">&ldquo;{testimonial.quote}&rdquo;</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&apos;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&apos;de tasarlandi</span>
<span className="h-1 w-1 rounded-full bg-muted-foreground" />
<span>Istanbul</span>
</div>
</div>
</div>
</footer>

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>
));
}
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View 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');
}

View 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`);
}

View File

@@ -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} />
);
}

View File

@@ -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';

View File

@@ -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,
};

View File

@@ -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 };

View File

@@ -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 };

View 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>
);
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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))',
},
},
},