Refactor code structure for improved readability and maintainability
All checks were successful
Deploy / deploy (push) Successful in 10m26s
All checks were successful
Deploy / deploy (push) Successful in 10m26s
This commit is contained in:
@@ -12,6 +12,12 @@
|
|||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "dev", "--", "-p", "3100"],
|
"runtimeArgs": ["run", "dev", "--", "-p", "3100"],
|
||||||
"port": 3100
|
"port": 3100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prod-3100",
|
||||||
|
"runtimeExecutable": "pnpm",
|
||||||
|
"runtimeArgs": ["start", "-p", "3100"],
|
||||||
|
"port": 3100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.112.4",
|
"@anthropic-ai/sdk": "^0.112.4",
|
||||||
|
"@cds/city": "^1.1.0",
|
||||||
"@libsql/client": "^0.17.4",
|
"@libsql/client": "^0.17.4",
|
||||||
"@react-pdf/renderer": "^4.5.1",
|
"@react-pdf/renderer": "^4.5.1",
|
||||||
"better-auth": "^1.6.23",
|
"better-auth": "^1.6.23",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"iyzipay": "^2.0.69",
|
"iyzipay": "^2.0.69",
|
||||||
|
"lenis": "^1.3.25",
|
||||||
"lucide-react": "^1.25.0",
|
"lucide-react": "^1.25.0",
|
||||||
"marked": "^18.0.7",
|
"marked": "^18.0.7",
|
||||||
"motion": "^12.42.2",
|
"motion": "^12.42.2",
|
||||||
|
|||||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@anthropic-ai/sdk':
|
'@anthropic-ai/sdk':
|
||||||
specifier: ^0.112.4
|
specifier: ^0.112.4
|
||||||
version: 0.112.4(zod@4.4.3)
|
version: 0.112.4(zod@4.4.3)
|
||||||
|
'@cds/city':
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
'@libsql/client':
|
'@libsql/client':
|
||||||
specifier: ^0.17.4
|
specifier: ^0.17.4
|
||||||
version: 0.17.4
|
version: 0.17.4
|
||||||
@@ -35,6 +38,9 @@ importers:
|
|||||||
iyzipay:
|
iyzipay:
|
||||||
specifier: ^2.0.69
|
specifier: ^2.0.69
|
||||||
version: 2.0.69
|
version: 2.0.69
|
||||||
|
lenis:
|
||||||
|
specifier: ^1.3.25
|
||||||
|
version: 1.3.25(react@19.2.4)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.25.0
|
specifier: ^1.25.0
|
||||||
version: 1.25.0(react@19.2.4)
|
version: 1.25.0(react@19.2.4)
|
||||||
@@ -363,6 +369,9 @@ packages:
|
|||||||
'@better-fetch/fetch@1.3.1':
|
'@better-fetch/fetch@1.3.1':
|
||||||
resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==}
|
resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==}
|
||||||
|
|
||||||
|
'@cds/city@1.1.0':
|
||||||
|
resolution: {integrity: sha512-S9K+Q39BGOghyLHmR0Wdcmu1i1noSUk8HcvMj+3IaohZw02WFd99aPTQDHJeseXrXZP3CNovaSlePI0R11NcFg==}
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.75.1':
|
'@dotenvx/dotenvx@1.75.1':
|
||||||
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
|
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3966,6 +3975,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
|
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
|
lenis@1.3.25:
|
||||||
|
resolution: {integrity: sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nuxt/kit': '>=3.0.0'
|
||||||
|
react: '>=17.0.0'
|
||||||
|
vue: '>=3.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@nuxt/kit':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
vue:
|
||||||
|
optional: true
|
||||||
|
|
||||||
levn@0.4.1:
|
levn@0.4.1:
|
||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -5679,6 +5702,8 @@ snapshots:
|
|||||||
|
|
||||||
'@better-fetch/fetch@1.3.1': {}
|
'@better-fetch/fetch@1.3.1': {}
|
||||||
|
|
||||||
|
'@cds/city@1.1.0': {}
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.75.1':
|
'@dotenvx/dotenvx@1.75.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@dotenvx/primitives': 0.8.0
|
'@dotenvx/primitives': 0.8.0
|
||||||
@@ -9213,6 +9238,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
language-subtag-registry: 0.3.23
|
language-subtag-registry: 0.3.23
|
||||||
|
|
||||||
|
lenis@1.3.25(react@19.2.4):
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
levn@0.4.1:
|
levn@0.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Loader2, Mail } from "lucide-react";
|
import { Mail } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -126,12 +126,8 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
|||||||
onClick={googleIleGiris}
|
onClick={googleIleGiris}
|
||||||
disabled={busy !== null}
|
disabled={busy !== null}
|
||||||
>
|
>
|
||||||
{busy === "google" ? (
|
<GoogleIcon />
|
||||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
{busy === "google" ? "Yönlendiriliyorsun…" : "Google ile devam et"}
|
||||||
) : (
|
|
||||||
<GoogleIcon />
|
|
||||||
)}
|
|
||||||
Google ile devam et
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-xs text-slate-400">
|
<div className="flex items-center gap-3 text-xs text-slate-400">
|
||||||
@@ -156,12 +152,8 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
|||||||
className="h-11 w-full cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-11 w-full cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
disabled={busy !== null}
|
disabled={busy !== null}
|
||||||
>
|
>
|
||||||
{busy === "email" ? (
|
<Mail className="size-4" aria-hidden />
|
||||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
{busy === "email" ? "Gönderiliyor…" : "Giriş bağlantısı gönder"}
|
||||||
) : (
|
|
||||||
<Mail className="size-4" aria-hidden />
|
|
||||||
)}
|
|
||||||
Giriş bağlantısı gönder
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Parallax } from "@/components/parallax";
|
||||||
|
import { ScrollFlow } from "@/components/scroll-flow";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Gizlilik ve KVKK — KolayTercih",
|
title: "Gizlilik ve KVKK — KolayTercih",
|
||||||
@@ -8,59 +10,70 @@ export const metadata: Metadata = {
|
|||||||
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
|
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
|
||||||
export default function GizlilikPage() {
|
export default function GizlilikPage() {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
<ScrollFlow>
|
||||||
<h1 className="font-heading text-3xl font-bold">
|
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||||
Gizlilik ve Kişisel Verilerin Korunması
|
<Parallax fromStart strength={10}>
|
||||||
</h1>
|
<h1 className="font-heading text-3xl font-bold">
|
||||||
<p className="mt-2 text-sm text-slate-500">Son güncelleme: 26 Temmuz 2026</p>
|
Gizlilik ve Kişisel Verilerin Korunması
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-sm text-slate-500">
|
||||||
|
Son güncelleme: 26 Temmuz 2026
|
||||||
|
</p>
|
||||||
|
</Parallax>
|
||||||
|
|
||||||
<div className="mt-8 space-y-6 text-sm leading-7 text-slate-700">
|
<Parallax
|
||||||
<section>
|
fromStart
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
strength={4}
|
||||||
Hangi verileri işliyoruz?
|
className="mt-8 space-y-6 text-sm leading-7 text-slate-700"
|
||||||
</h2>
|
>
|
||||||
<p className="mt-2">
|
<section>
|
||||||
KolayTercih; e-posta adresini (hesap oluşturma ve giriş için), YKS
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
başarı sıralamanı ve sihirbazda yaptığın seçimleri (tercih listeni
|
Hangi verileri işliyoruz?
|
||||||
üretmek için), danışman sohbetine yazdığın mesajları (sorularını
|
</h2>
|
||||||
yanıtlamak için) ve ödeme yaptığında sipariş kayıtlarını işler.
|
<p className="mt-2">
|
||||||
Kart bilgilerin bize hiç ulaşmaz; ödeme iyzico altyapısında
|
KolayTercih; e-posta adresini (hesap oluşturma ve giriş için), YKS
|
||||||
gerçekleşir.
|
başarı sıralamanı ve sihirbazda yaptığın seçimleri (tercih listeni
|
||||||
</p>
|
üretmek için), danışman sohbetine yazdığın mesajları (sorularını
|
||||||
</section>
|
yanıtlamak için) ve ödeme yaptığında sipariş kayıtlarını işler.
|
||||||
<section>
|
Kart bilgilerin bize hiç ulaşmaz; ödeme iyzico altyapısında
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
gerçekleşir.
|
||||||
Ne için kullanıyoruz?
|
</p>
|
||||||
</h2>
|
</section>
|
||||||
<p className="mt-2">
|
<section>
|
||||||
Verilerin yalnızca tercih önerileri üretmek, hesabını ve kredi
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
bakiyeni yönetmek ve sana hizmetle ilgili e-posta (giriş bağlantısı,
|
Ne için kullanıyoruz?
|
||||||
ödeme bilgilendirmesi) göndermek için kullanılır. Verilerini
|
</h2>
|
||||||
üçüncü taraflara satmayız, reklam amacıyla paylaşmayız.
|
<p className="mt-2">
|
||||||
</p>
|
Verilerin yalnızca tercih önerileri üretmek, hesabını ve kredi
|
||||||
</section>
|
bakiyeni yönetmek ve sana hizmetle ilgili e-posta (giriş
|
||||||
<section>
|
bağlantısı, ödeme bilgilendirmesi) göndermek için kullanılır.
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
Verilerini üçüncü taraflara satmayız, reklam amacıyla paylaşmayız.
|
||||||
Saklama ve silme
|
</p>
|
||||||
</h2>
|
</section>
|
||||||
<p className="mt-2">
|
<section>
|
||||||
Hesabını sildirmek veya verilerinin bir kopyasını istemek için{" "}
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
<Link href="/iletisim" className="text-primary underline">
|
Saklama ve silme
|
||||||
iletişim sayfasındaki
|
</h2>
|
||||||
</Link>{" "}
|
<p className="mt-2">
|
||||||
adresten bize yazabilirsin; talepler 30 gün içinde sonuçlandırılır.
|
Hesabını sildirmek veya verilerinin bir kopyasını istemek için{" "}
|
||||||
</p>
|
<Link href="/iletisim" className="text-primary underline">
|
||||||
</section>
|
iletişim sayfasındaki
|
||||||
<section>
|
</Link>{" "}
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
adresten bize yazabilirsin; talepler 30 gün içinde
|
||||||
Çerezler
|
sonuçlandırılır.
|
||||||
</h2>
|
</p>
|
||||||
<p className="mt-2">
|
</section>
|
||||||
Yalnızca oturumunu açık tutmak için zorunlu çerezler kullanılır;
|
<section>
|
||||||
reklam veya izleme çerezi kullanılmaz.
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
</p>
|
Çerezler
|
||||||
</section>
|
</h2>
|
||||||
</div>
|
<p className="mt-2">
|
||||||
</main>
|
Yalnızca oturumunu açık tutmak için zorunlu çerezler kullanılır;
|
||||||
|
reklam veya izleme çerezi kullanılmaz.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</Parallax>
|
||||||
|
</main>
|
||||||
|
</ScrollFlow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-work-sans);
|
--font-sans: var(--font-clarity-city);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
--font-heading: var(--font-outfit);
|
--font-heading: var(--font-cal-sans);
|
||||||
|
--font-bricolage: var(--font-bricolage-grotesque);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -168,7 +169,22 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
/* Chrome autofill'in soluk mavi dolgusu inputun kendi arka planını
|
||||||
|
ezmesin; dolgu metne kırpılır, renkler temadan gelir. */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus {
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: var(--foreground);
|
||||||
|
caret-color: var(--foreground);
|
||||||
|
}
|
||||||
html {
|
html {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Mail } from "lucide-react";
|
import { Mail } from "lucide-react";
|
||||||
|
import { Parallax } from "@/components/parallax";
|
||||||
|
import { ScrollFlow } from "@/components/scroll-flow";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "İletişim — KolayTercih",
|
title: "İletişim — KolayTercih",
|
||||||
@@ -7,24 +9,28 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function IletisimPage() {
|
export default function IletisimPage() {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
<ScrollFlow>
|
||||||
<h1 className="font-heading text-3xl font-bold">İletişim</h1>
|
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||||
<p className="mt-4 text-sm leading-7 text-slate-700">
|
<Parallax fromStart strength={10}>
|
||||||
Ödeme, iade, hesap silme ya da başka bir konuda yardıma mı ihtiyacın
|
<h1 className="font-heading text-3xl font-bold">İletişim</h1>
|
||||||
var? Tercih döneminde mesajlara aynı gün dönmeye çalışıyoruz.
|
</Parallax>
|
||||||
</p>
|
<p className="mt-4 text-sm leading-7 text-slate-700">
|
||||||
<a
|
Ödeme, iade, hesap silme ya da başka bir konuda yardıma mı ihtiyacın
|
||||||
href="mailto:destek@kolaytercih.com"
|
var? Tercih döneminde mesajlara aynı gün dönmeye çalışıyoruz.
|
||||||
className="mt-6 inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-900 shadow-xs transition-colors hover:border-slate-300 hover:bg-slate-50"
|
</p>
|
||||||
>
|
<a
|
||||||
<Mail className="size-4 text-primary" aria-hidden />
|
href="mailto:destek@kolaytercih.com"
|
||||||
destek@kolaytercih.com
|
className="mt-6 inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-900 shadow-xs transition-colors hover:border-slate-300 hover:bg-slate-50"
|
||||||
</a>
|
>
|
||||||
<p className="mt-6 text-xs text-slate-500">
|
<Mail className="size-4 text-primary" aria-hidden />
|
||||||
Ödemenle ilgili yazıyorsan e-postana sipariş tarihini ve kullandığın
|
destek@kolaytercih.com
|
||||||
hesap adresini eklersen daha hızlı yardımcı olabiliriz. Kart bilgini
|
</a>
|
||||||
asla e-postayla paylaşma.
|
<p className="mt-6 text-xs text-slate-500">
|
||||||
</p>
|
Ödemenle ilgili yazıyorsan e-postana sipariş tarihini ve kullandığın
|
||||||
</main>
|
hesap adresini eklersen daha hızlı yardımcı olabiliriz. Kart bilgini
|
||||||
|
asla e-postayla paylaşma.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</ScrollFlow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Parallax } from "@/components/parallax";
|
||||||
|
import { ScrollFlow } from "@/components/scroll-flow";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Kullanım ve Satış Koşulları — KolayTercih",
|
title: "Kullanım ve Satış Koşulları — KolayTercih",
|
||||||
@@ -8,67 +10,77 @@ export const metadata: Metadata = {
|
|||||||
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
|
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
|
||||||
export default function KosullarPage() {
|
export default function KosullarPage() {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
<ScrollFlow>
|
||||||
<h1 className="font-heading text-3xl font-bold">
|
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||||
Kullanım ve Satış Koşulları
|
<Parallax fromStart strength={10}>
|
||||||
</h1>
|
<h1 className="font-heading text-3xl font-bold">
|
||||||
<p className="mt-2 text-sm text-slate-500">Son güncelleme: 26 Temmuz 2026</p>
|
Kullanım ve Satış Koşulları
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-sm text-slate-500">
|
||||||
|
Son güncelleme: 26 Temmuz 2026
|
||||||
|
</p>
|
||||||
|
</Parallax>
|
||||||
|
|
||||||
<div className="mt-8 space-y-6 text-sm leading-7 text-slate-700">
|
<Parallax
|
||||||
<section>
|
fromStart
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
strength={4}
|
||||||
Hizmetin kapsamı
|
className="mt-8 space-y-6 text-sm leading-7 text-slate-700"
|
||||||
</h2>
|
>
|
||||||
<p className="mt-2">
|
<section>
|
||||||
KolayTercih, resmî YÖK Atlas verilerine dayanan bir karar destek
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
aracıdır. Öneriler bilgilendirme amaçlıdır; KolayTercih yerleşme
|
Hizmetin kapsamı
|
||||||
garantisi vermez ve ÖSYM ya da YÖK ile bağlantılı değildir. Tercih
|
</h2>
|
||||||
listenin son hali ve başvuru sorumluluğu sana aittir.
|
<p className="mt-2">
|
||||||
</p>
|
KolayTercih, resmî YÖK Atlas verilerine dayanan bir karar destek
|
||||||
</section>
|
aracıdır. Öneriler bilgilendirme amaçlıdır; KolayTercih yerleşme
|
||||||
<section>
|
garantisi vermez ve ÖSYM ya da YÖK ile bağlantılı değildir. Tercih
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
listenin son hali ve başvuru sorumluluğu sana aittir.
|
||||||
Ücretlendirme
|
</p>
|
||||||
</h2>
|
</section>
|
||||||
<p className="mt-2">
|
<section>
|
||||||
Temel program arama ücretsizdir. Tercih Dönemi Paketi tek seferlik
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
bir satın almadır; abonelik değildir, otomatik yenilenmez. Paket
|
Ücretlendirme
|
||||||
içeriği ve güncel fiyat{" "}
|
</h2>
|
||||||
<Link href="/paket" className="text-primary underline">
|
<p className="mt-2">
|
||||||
paket sayfasında
|
Temel program arama ücretsizdir. Tercih Dönemi Paketi tek seferlik
|
||||||
</Link>{" "}
|
bir satın almadır; abonelik değildir, otomatik yenilenmez. Paket
|
||||||
yazar.
|
içeriği ve güncel fiyat{" "}
|
||||||
</p>
|
<Link href="/paket" className="text-primary underline">
|
||||||
</section>
|
paket sayfasında
|
||||||
<section id="iade" className="scroll-mt-24">
|
</Link>{" "}
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
yazar.
|
||||||
İade ve cayma
|
</p>
|
||||||
</h2>
|
</section>
|
||||||
<p className="mt-2">
|
<section id="iade" className="scroll-mt-24">
|
||||||
Paket, satın alır almaz ifasına başlanan dijital bir içeriktir.
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
Buna rağmen kişisel listen henüz üretilmemişse (paket hiç
|
İade ve cayma
|
||||||
kullanılmamışsa) satın almadan itibaren 14 gün içinde koşulsuz
|
</h2>
|
||||||
iade talep edebilirsin. Teknik bir sorun nedeniyle hizmeti
|
<p className="mt-2">
|
||||||
alamadıysan, kullanımdan bağımsız olarak ücretin iade edilir.
|
Paket, satın alır almaz ifasına başlanan dijital bir içeriktir.
|
||||||
İade talepleri için{" "}
|
Buna rağmen kişisel listen henüz üretilmemişse (paket hiç
|
||||||
<Link href="/iletisim" className="text-primary underline">
|
kullanılmamışsa) satın almadan itibaren 14 gün içinde koşulsuz
|
||||||
bize yaz
|
iade talep edebilirsin. Teknik bir sorun nedeniyle hizmeti
|
||||||
</Link>
|
alamadıysan, kullanımdan bağımsız olarak ücretin iade edilir. İade
|
||||||
; talepler 14 gün içinde sonuçlandırılır ve iade, ödeme yaptığın
|
talepleri için{" "}
|
||||||
karta yapılır.
|
<Link href="/iletisim" className="text-primary underline">
|
||||||
</p>
|
bize yaz
|
||||||
</section>
|
</Link>
|
||||||
<section>
|
; talepler 14 gün içinde sonuçlandırılır ve iade, ödeme yaptığın
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
karta yapılır.
|
||||||
Hesap ve adil kullanım
|
</p>
|
||||||
</h2>
|
</section>
|
||||||
<p className="mt-2">
|
<section>
|
||||||
Krediler hesabına tanımlıdır, kişiseldir ve üçüncü kişilere
|
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||||
devredilemez. Hizmeti kötüye kullanan (otomatik istek, içerik
|
Hesap ve adil kullanım
|
||||||
kazıma vb.) hesaplar kapatılabilir.
|
</h2>
|
||||||
</p>
|
<p className="mt-2">
|
||||||
</section>
|
Krediler hesabına tanımlıdır, kişiseldir ve üçüncü kişilere
|
||||||
</div>
|
devredilemez. Hizmeti kötüye kullanan (otomatik istek, içerik
|
||||||
</main>
|
kazıma vb.) hesaplar kapatılabilir.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</Parallax>
|
||||||
|
</main>
|
||||||
|
</ScrollFlow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Outfit, Work_Sans, Geist_Mono } from "next/font/google";
|
import { Cal_Sans, Geist_Mono, Bricolage_Grotesque } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { SiteHeader } from "@/components/site-header";
|
import { SiteHeader } from "@/components/site-header";
|
||||||
import { ListeCekmecesi } from "@/components/manuel-liste/liste-cekmecesi";
|
import { ListeCekmecesi } from "@/components/manuel-liste/liste-cekmecesi";
|
||||||
@@ -8,14 +9,21 @@ import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
|
|||||||
import { DevPanel } from "@/components/dev/dev-panel";
|
import { DevPanel } from "@/components/dev/dev-panel";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const outfit = Outfit({
|
const calSans = Cal_Sans({
|
||||||
variable: "--font-outfit",
|
variable: "--font-cal-sans",
|
||||||
|
weight: "400",
|
||||||
subsets: ["latin", "latin-ext"],
|
subsets: ["latin", "latin-ext"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const workSans = Work_Sans({
|
const clarityCity = localFont({
|
||||||
variable: "--font-work-sans",
|
variable: "--font-clarity-city",
|
||||||
subsets: ["latin", "latin-ext"],
|
src: [
|
||||||
|
{ path: "../fonts/ClarityCity-Regular.woff2", weight: "400" },
|
||||||
|
{ path: "../fonts/ClarityCity-Medium.woff2", weight: "500" },
|
||||||
|
{ path: "../fonts/ClarityCity-SemiBold.woff2", weight: "600" },
|
||||||
|
{ path: "../fonts/ClarityCity-Bold.woff2", weight: "700" },
|
||||||
|
{ path: "../fonts/ClarityCity-ExtraBold.woff2", weight: "800" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
@@ -23,6 +31,12 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bricolage = Bricolage_Grotesque({
|
||||||
|
variable: "--font-bricolage-grotesque",
|
||||||
|
subsets: ["latin", "latin-ext"],
|
||||||
|
axes: ["opsz"],
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "KolayTercih — Tercih Danışmanı",
|
title: "KolayTercih — Tercih Danışmanı",
|
||||||
description:
|
description:
|
||||||
@@ -37,7 +51,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="tr"
|
lang="tr"
|
||||||
className={`${outfit.variable} ${workSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col bg-slate-50">
|
<body className="min-h-full flex flex-col bg-slate-50">
|
||||||
{/* Site geneli progressive blur — üst ve alt */}
|
{/* Site geneli progressive blur — üst ve alt */}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Loader2, RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { listeRevize } from "@/app/sonuc/actions";
|
import { listeRevize } from "@/app/sonuc/actions";
|
||||||
@@ -80,11 +80,7 @@ export function RevizyonKutusu({ kalanHak }: { kalanHak: number }) {
|
|||||||
onClick={revizeEt}
|
onClick={revizeEt}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{pending ? (
|
<RefreshCcw className="size-4" aria-hidden />
|
||||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
||||||
) : (
|
|
||||||
<RefreshCcw className="size-4" aria-hidden />
|
|
||||||
)}
|
|
||||||
{pending ? "Yeniden kuruluyor…" : "Revize et (3 kredi)"}
|
{pending ? "Yeniden kuruluyor…" : "Revize et (3 kredi)"}
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-xs text-slate-500">
|
<span className="text-xs text-slate-500">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { PromptSuggestion } from "@/components/ui/prompt-suggestion";
|
import { PromptSuggestion } from "@/components/ui/prompt-suggestion";
|
||||||
import { ScrollButton } from "@/components/ui/scroll-button";
|
import { ScrollButton } from "@/components/ui/scroll-button";
|
||||||
import { DotsLoader } from "@/components/ui/loader";
|
import { DotsLoader } from "@/components/ui/loader";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
interface Mesaj {
|
interface Mesaj {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -174,7 +175,15 @@ export function SohbetClient({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<DotsLoader />
|
<div className="flex max-w-lg flex-wrap justify-center gap-2">
|
||||||
|
{[112, 148, 96, 132].map((w, i) => (
|
||||||
|
<Skeleton
|
||||||
|
key={i}
|
||||||
|
className="h-7 rounded-full"
|
||||||
|
style={{ width: w }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -195,8 +204,10 @@ export function SohbetClient({
|
|||||||
{m.content}
|
{m.content}
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg bg-slate-100 px-4 py-3">
|
<div className="w-full max-w-[85%] space-y-2 rounded-lg bg-slate-100 px-4 py-3">
|
||||||
<DotsLoader size="sm" />
|
<Skeleton className="h-3.5 w-full bg-slate-200" />
|
||||||
|
<Skeleton className="h-3.5 w-4/5 bg-slate-200" />
|
||||||
|
<Skeleton className="h-3.5 w-3/5 bg-slate-200" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Message>
|
</Message>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
// Kendi loading dosyası olmayan tüm rotalar için genel sayfa iskeleti:
|
||||||
|
// spinner yerine başlık, metin ve kart bloklarının yer tutucuları —
|
||||||
|
// içerik gelince zıplama olmaz.
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<main
|
<main aria-live="polite" className="mx-auto w-full max-w-6xl px-4 py-12">
|
||||||
aria-live="polite"
|
{/* Rozet + başlık + alt metin */}
|
||||||
className="flex min-h-[60vh] flex-col items-center justify-center gap-3"
|
<Skeleton className="h-6 w-44 rounded-full" />
|
||||||
>
|
<Skeleton className="mt-5 h-10 w-3/4 max-w-2xl" />
|
||||||
<Loader2 className="size-8 animate-spin text-primary" aria-hidden />
|
<Skeleton className="mt-3 h-10 w-1/2 max-w-md" />
|
||||||
<p className="text-sm text-slate-500">Yükleniyor…</p>
|
<Skeleton className="mt-5 h-5 w-2/3 max-w-lg" />
|
||||||
|
<Skeleton className="mt-2 h-5 w-1/2 max-w-sm" />
|
||||||
|
|
||||||
|
{/* Form / aksiyon şeridi */}
|
||||||
|
<div className="mt-8 flex max-w-xl flex-col gap-3 sm:flex-row">
|
||||||
|
<Skeleton className="h-12 flex-1 rounded-lg" />
|
||||||
|
<Skeleton className="h-12 w-full rounded-lg sm:w-44" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kart bloklar */}
|
||||||
|
<div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-40 w-full rounded-2xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
645
src/app/page.tsx
645
src/app/page.tsx
@@ -12,6 +12,16 @@ import {
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { HeroForm } from "@/components/hero-form";
|
import { HeroForm } from "@/components/hero-form";
|
||||||
|
import { Parallax } from "@/components/parallax";
|
||||||
|
import { ScrollFlow } from "@/components/scroll-flow";
|
||||||
|
import { Reveal } from "@/components/landing/reveal";
|
||||||
|
import { TurkeyPixelMap } from "@/components/turkey-pixel-map";
|
||||||
|
import {
|
||||||
|
PixelCorner,
|
||||||
|
PixelDivider,
|
||||||
|
PixelField,
|
||||||
|
SectionEyebrow,
|
||||||
|
} from "@/components/pixel-decor";
|
||||||
import { URUNLER } from "@/lib/credits";
|
import { URUNLER } from "@/lib/credits";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -156,261 +166,412 @@ function ComparisonCell({ value }: { value: boolean | string }) {
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
|
<div className="bg-slate-50 text-slate-900">
|
||||||
<main className="flex-1">
|
<ScrollFlow className="flex min-h-screen flex-col">
|
||||||
{/* Hero */}
|
<main className="flex-1">
|
||||||
<section className="mx-auto flex max-w-6xl flex-col items-center px-4 pb-20 pt-16 text-center sm:pt-24">
|
{/* Hero */}
|
||||||
<Badge
|
<section className="relative isolate overflow-hidden">
|
||||||
variant="secondary"
|
<div
|
||||||
className="mb-6 bg-primary/10 text-primary"
|
aria-hidden
|
||||||
>
|
// SVG altta 9 hücrelik serpinti payı taşıdığı için silüet merkezi
|
||||||
2026 YKS tercih dönemi açık
|
// yüksekliğin %37,1'inde; -1/2 yerine bu oranla hizalanır
|
||||||
</Badge>
|
className="pointer-events-none absolute left-1/2 top-[43%] -z-10 hidden -translate-x-1/2 -translate-y-[37.14%] sm:block"
|
||||||
<h1 className="max-w-3xl font-heading text-4xl font-bold tracking-tight sm:text-6xl">
|
>
|
||||||
Dört yıllık geleceğini{" "}
|
{/* Arka katman: harita scroll'un gerisinde kalır, derinlik verir */}
|
||||||
<span className="text-primary">48 saatlik panikle</span> seçme
|
<Parallax fromStart strength={-52}>
|
||||||
</h1>
|
{/* Harita hiçbir boyutta taşmaz: her zaman ekran genişliği
|
||||||
<p className="mt-6 max-w-2xl text-lg leading-relaxed text-slate-600">
|
eksi kenar payı kadar; sayfa küçüldükçe onunla birlikte
|
||||||
|
küçülür. Çok geniş ekranda 1665px tavanına oturur;
|
||||||
|
mobilde tümüyle gizli. */}
|
||||||
|
<TurkeyPixelMap className="w-[min(1665px,100vw_-_2.5rem)] max-w-none text-primary" />
|
||||||
|
</Parallax>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_58%_52%_at_50%_42%,rgba(248,250,252,0.9)_25%,rgba(248,250,252,0)_75%)]"
|
||||||
|
/>
|
||||||
|
<Parallax
|
||||||
|
fromStart
|
||||||
|
strength={14}
|
||||||
|
className="mx-auto flex min-h-[calc(100svh-5.5rem)] max-w-6xl flex-col items-center justify-center px-4 pb-44 pt-4 text-center"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="mb-6 bg-primary/10 text-primary"
|
||||||
|
>
|
||||||
|
2026 YKS tercih dönemi açık
|
||||||
|
</Badge>
|
||||||
|
<h1 className="max-w-3xl font-heading text-4xl font-bold tracking-[0.015em] sm:text-6xl">
|
||||||
|
Dört yıllık geleceğini <br className="hidden sm:block" />
|
||||||
|
<span className="font-bricolage italic tracking-tighter font-bold text-primary">
|
||||||
|
48 saatlik panikle
|
||||||
|
</span>{" "}
|
||||||
|
seçme
|
||||||
|
</h1>
|
||||||
|
{/* <p className="mt-6 max-w-2xl text-lg leading-relaxed text-slate-600">
|
||||||
KolayTercih, gerçek YÖK Atlas verisiyle çalışan yapay zekâ tercih
|
KolayTercih, gerçek YÖK Atlas verisiyle çalışan yapay zekâ tercih
|
||||||
danışmanın. İnsan danışmanın binlerce lira aldığı işi — dengeli
|
danışmanın. İnsan danışmanın binlerce lira aldığı işi — dengeli
|
||||||
liste, risk analizi, soru-cevap — onda bir fiyatına yapar.
|
liste, risk analizi, soru-cevap — onda bir fiyatına yapar.
|
||||||
</p>
|
</p> */}
|
||||||
<div
|
<div
|
||||||
id="hero-form"
|
id="hero-form"
|
||||||
className="mt-10 flex w-full scroll-mt-28 justify-center"
|
className="mt-6 flex w-full scroll-mt-28 justify-center"
|
||||||
>
|
>
|
||||||
<HeroForm />
|
<HeroForm />
|
||||||
</div>
|
|
||||||
<p className="mt-4 text-sm text-slate-500">
|
|
||||||
Temel arama her zaman ücretsiz · Kredi kartı gerekmez · Tam analiz
|
|
||||||
paketi tek seferlik {PAKET_FIYATI} TL
|
|
||||||
</p>
|
|
||||||
<div className="mt-3 inline-flex items-center gap-2.5 rounded-lg border border-slate-200 bg-white px-3 py-2 text-left text-xs text-slate-700 shadow-xs">
|
|
||||||
<Image
|
|
||||||
src="/yok-logo.svg"
|
|
||||||
alt="Yükseköğretim Kurulu"
|
|
||||||
width={68}
|
|
||||||
height={33}
|
|
||||||
className="h-7 w-auto shrink-0"
|
|
||||||
/>
|
|
||||||
<span className="h-5 w-px shrink-0 bg-slate-200" aria-hidden />
|
|
||||||
<span>
|
|
||||||
<strong className="font-semibold text-slate-900">
|
|
||||||
YÖK Atlas'ın gerçek verileriyle
|
|
||||||
</strong>{" "}
|
|
||||||
güvenilir sonuçlar
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Problem */}
|
|
||||||
<section className="border-y border-slate-200 bg-white py-20">
|
|
||||||
<div className="mx-auto max-w-6xl px-4">
|
|
||||||
<h2 className="text-center font-heading text-3xl font-bold sm:text-4xl">
|
|
||||||
Tercih dönemi neden bu kadar stresli?
|
|
||||||
</h2>
|
|
||||||
<div className="mt-12 grid gap-6 md:grid-cols-3">
|
|
||||||
{problems.map((p) => (
|
|
||||||
<Card key={p.title} className="border-slate-200">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10">
|
|
||||||
<p.icon className="size-5 text-primary" aria-hidden />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="font-heading text-xl">
|
|
||||||
{p.title}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-base leading-relaxed">
|
|
||||||
{p.description}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* How it works */}
|
|
||||||
<section id="nasil-calisir" className="scroll-mt-24 py-20">
|
|
||||||
<div className="mx-auto max-w-6xl px-4">
|
|
||||||
<h2 className="text-center font-heading text-3xl font-bold sm:text-4xl">
|
|
||||||
Üç adımda listen hazır
|
|
||||||
</h2>
|
|
||||||
<div className="mt-12 grid gap-6 md:grid-cols-3">
|
|
||||||
{steps.map((s) => (
|
|
||||||
<div
|
|
||||||
key={s.step}
|
|
||||||
className="relative rounded-2xl border border-slate-200 bg-white p-8"
|
|
||||||
>
|
|
||||||
<span className="absolute -top-4 left-8 flex size-8 items-center justify-center rounded-full bg-orange-500 font-heading text-sm font-bold text-white">
|
|
||||||
{s.step}
|
|
||||||
</span>
|
|
||||||
<s.icon className="size-6 text-primary" aria-hidden />
|
|
||||||
<h3 className="mt-4 font-heading text-xl font-semibold">
|
|
||||||
{s.title}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-2 leading-relaxed text-slate-600">
|
|
||||||
{s.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Veriye dayalı güven + garanti vaadine karşı dürüst konumlandırma */}
|
|
||||||
<div className="mx-auto mt-12 w-full max-w-3xl overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
||||||
<dl className="grid grid-cols-1 divide-y divide-slate-200 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
|
|
||||||
{[
|
|
||||||
{ value: "23.000+", label: "Taranan lisans programı" },
|
|
||||||
{ value: "4 yıl", label: "Taban sıralaması trend verisi" },
|
|
||||||
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
|
|
||||||
].map((stat) => (
|
|
||||||
<div key={stat.label} className="p-6 text-center">
|
|
||||||
<dt className="text-sm text-slate-500">{stat.label}</dt>
|
|
||||||
<dd className="mt-1 font-heading text-3xl font-bold text-primary">
|
|
||||||
{stat.value}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<div className="flex gap-4 border-t border-slate-200 bg-primary/5 p-6 text-left sm:items-center sm:p-7">
|
|
||||||
<div className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-primary text-white shadow-sm">
|
|
||||||
<ShieldCheck className="size-5" aria-hidden />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-primary">
|
|
||||||
Dürüstlük notu
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 font-heading text-lg font-bold text-slate-900 sm:text-xl">
|
|
||||||
Yerleşme garantisi vermiyoruz — çünkü kimse veremez.
|
|
||||||
</p>
|
|
||||||
<p className="mt-1.5 text-sm leading-6 text-slate-600">
|
|
||||||
Taban sıralamaları her yıl değişir. Biz sonuç vaadi satmak
|
|
||||||
yerine riski satır satır gösterir, daha güvenli bir liste
|
|
||||||
kurmana yardımcı oluruz.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-slate-500">
|
||||||
|
Temel arama her zaman ücretsiz · Kredi kartı gerekmez · Tam
|
||||||
|
analiz paketi tek seferlik {PAKET_FIYATI} TL
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 inline-flex items-center gap-2.5 rounded-lg border border-slate-200 bg-white px-3 py-2 text-left text-xs text-slate-700 shadow-xs">
|
||||||
|
<Image
|
||||||
|
src="/yok-logo.svg"
|
||||||
|
alt="Yükseköğretim Kurulu"
|
||||||
|
width={68}
|
||||||
|
height={33}
|
||||||
|
className="h-7 w-auto shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="h-5 w-px shrink-0 bg-slate-200" aria-hidden />
|
||||||
|
<span>
|
||||||
|
<strong className="font-semibold text-slate-900">
|
||||||
|
YÖK Atlas'ın gerçek verileriyle
|
||||||
|
</strong>{" "}
|
||||||
|
güvenilir sonuçlar
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Parallax>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Hero → Problem geçişi: border yerine piksel ayraç */}
|
||||||
|
<div aria-hidden className="mx-auto w-56">
|
||||||
|
<Parallax strength={26}>
|
||||||
|
<PixelDivider seed={11} />
|
||||||
|
</Parallax>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Problem */}
|
||||||
|
<section className="py-20 sm:py-24">
|
||||||
|
<div className="mx-auto max-w-6xl px-4">
|
||||||
|
{/* Başlık gövdeden hafif hızlı akar — katman derinliği */}
|
||||||
|
<Parallax strength={16}>
|
||||||
|
<Reveal className="flex flex-col items-center gap-4 text-center">
|
||||||
|
<SectionEyebrow>Sorun</SectionEyebrow>
|
||||||
|
<h2 className="font-heading text-3xl font-bold sm:text-4xl">
|
||||||
|
Tercih dönemi neden bu kadar stresli?
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-xl text-lg leading-relaxed text-slate-600">
|
||||||
|
Her yıl iki milyondan fazla aday aynı üç duvara çarpıyor.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
</Parallax>
|
||||||
|
<Parallax
|
||||||
|
strength={7}
|
||||||
|
className="mt-12 grid gap-6 md:grid-cols-3"
|
||||||
|
>
|
||||||
|
{problems.map((p, i) => (
|
||||||
|
<Reveal key={p.title} delay={i * 0.08}>
|
||||||
|
<Card className="relative h-full overflow-hidden border-slate-200/70 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg hover:shadow-primary/5">
|
||||||
|
<PixelCorner className="absolute right-4 top-4" />
|
||||||
|
<CardHeader>
|
||||||
|
<div className="mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10">
|
||||||
|
<p.icon className="size-5 text-primary" aria-hidden />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="font-heading text-xl">
|
||||||
|
{p.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-base leading-relaxed">
|
||||||
|
{p.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</Parallax>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Comparison */}
|
{/* Problem → Nasıl çalışır geçişi */}
|
||||||
<section
|
<div aria-hidden className="mx-auto w-56">
|
||||||
id="karsilastirma"
|
<Parallax strength={26}>
|
||||||
className="scroll-mt-24 border-y border-slate-200 bg-white py-20"
|
<PixelDivider seed={17} />
|
||||||
>
|
</Parallax>
|
||||||
<div className="mx-auto max-w-4xl px-4">
|
</div>
|
||||||
<h2 className="text-center font-heading text-3xl font-bold sm:text-4xl">
|
|
||||||
Robot mu, danışman mı, KolayTercih mi?
|
{/* How it works */}
|
||||||
</h2>
|
<section id="nasil-calisir" className="scroll-mt-24 py-20 sm:py-24">
|
||||||
<div className="mt-12 overflow-x-auto rounded-2xl border border-slate-200 bg-white">
|
<div className="mx-auto max-w-6xl px-4">
|
||||||
<Table>
|
<Parallax strength={16}>
|
||||||
<TableHeader>
|
<Reveal className="flex flex-col items-center gap-4 text-center">
|
||||||
<TableRow className="bg-slate-50">
|
<SectionEyebrow>Nasıl çalışır?</SectionEyebrow>
|
||||||
<TableHead className="w-[40%]">Özellik</TableHead>
|
<h2 className="font-heading text-3xl font-bold sm:text-4xl">
|
||||||
<TableHead className="text-center">
|
Üç adımda listen hazır
|
||||||
Ücretsiz robotlar
|
</h2>
|
||||||
</TableHead>
|
<p className="max-w-xl text-lg leading-relaxed text-slate-600">
|
||||||
<TableHead className="text-center">
|
Sıralamandan dengeli 24'lük listeye giden yol üç adım
|
||||||
İnsan danışman
|
sürer.
|
||||||
</TableHead>
|
</p>
|
||||||
<TableHead className="text-center font-heading font-bold text-primary">
|
</Reveal>
|
||||||
KolayTercih
|
</Parallax>
|
||||||
</TableHead>
|
<Parallax
|
||||||
</TableRow>
|
strength={7}
|
||||||
</TableHeader>
|
className="mt-12 grid gap-6 md:grid-cols-3"
|
||||||
<TableBody>
|
>
|
||||||
{comparison.map((row) => (
|
{steps.map((s, i) => (
|
||||||
<TableRow key={row.feature}>
|
<Reveal key={s.step} delay={i * 0.08} className="h-full">
|
||||||
<TableCell className="font-medium">
|
<div className="relative h-full rounded-2xl border border-slate-200/70 bg-white p-8 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg hover:shadow-primary/5">
|
||||||
{row.feature}
|
<span className="absolute -top-4 left-8 flex size-8 items-center justify-center rounded-full bg-orange-500 font-heading text-sm font-bold text-white">
|
||||||
</TableCell>
|
{s.step}
|
||||||
<TableCell className="text-center">
|
</span>
|
||||||
<ComparisonCell value={row.robot} />
|
<PixelCorner className="absolute right-4 top-4" />
|
||||||
</TableCell>
|
<s.icon className="size-6 text-primary" aria-hidden />
|
||||||
<TableCell className="text-center">
|
<h3 className="mt-4 font-heading text-xl font-semibold">
|
||||||
<ComparisonCell value={row.danisman} />
|
{s.title}
|
||||||
</TableCell>
|
</h3>
|
||||||
<TableCell className="bg-primary/5 text-center">
|
<p className="mt-2 leading-relaxed text-slate-600">
|
||||||
<ComparisonCell value={row.tercihai} />
|
{s.description}
|
||||||
</TableCell>
|
</p>
|
||||||
</TableRow>
|
</div>
|
||||||
))}
|
</Reveal>
|
||||||
</TableBody>
|
))}
|
||||||
</Table>
|
</Parallax>
|
||||||
|
|
||||||
|
{/* Veriye dayalı güven + garanti vaadine karşı dürüst konumlandırma */}
|
||||||
|
<Parallax
|
||||||
|
strength={10}
|
||||||
|
className="mx-auto mt-12 w-full max-w-3xl"
|
||||||
|
>
|
||||||
|
<Reveal
|
||||||
|
delay={0.1}
|
||||||
|
className="overflow-hidden rounded-2xl border border-slate-200/70 bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<dl className="grid grid-cols-1 divide-y divide-slate-200 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
|
||||||
|
{[
|
||||||
|
{ value: "23.000+", label: "Taranan lisans programı" },
|
||||||
|
{
|
||||||
|
value: "4 yıl",
|
||||||
|
label: "Taban sıralaması trend verisi",
|
||||||
|
},
|
||||||
|
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
|
||||||
|
].map((stat) => (
|
||||||
|
<div key={stat.label} className="p-6 text-center">
|
||||||
|
<dt className="text-sm text-slate-500">{stat.label}</dt>
|
||||||
|
<dd className="mt-1 font-heading text-3xl font-bold text-primary">
|
||||||
|
{stat.value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="flex gap-4 border-t border-slate-200 bg-primary/5 p-6 text-left sm:items-center sm:p-7">
|
||||||
|
<div className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-primary text-white shadow-sm">
|
||||||
|
<ShieldCheck className="size-5" aria-hidden />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-primary">
|
||||||
|
Dürüstlük notu
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 font-heading text-lg font-bold text-slate-900 sm:text-xl">
|
||||||
|
Yerleşme garantisi vermiyoruz — çünkü kimse veremez.
|
||||||
|
</p>
|
||||||
|
<p className="mt-1.5 text-sm leading-6 text-slate-600">
|
||||||
|
Taban sıralamaları her yıl değişir. Biz sonuç vaadi
|
||||||
|
satmak yerine riski satır satır gösterir, daha güvenli
|
||||||
|
bir liste kurmana yardımcı oluruz.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</Parallax>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* FAQ */}
|
{/* Nasıl çalışır → Karşılaştırma geçişi */}
|
||||||
<section id="sss" className="scroll-mt-24 py-20">
|
<div aria-hidden className="mx-auto w-56">
|
||||||
<div className="mx-auto max-w-3xl px-4">
|
<Parallax strength={26}>
|
||||||
<h2 className="text-center font-heading text-3xl font-bold sm:text-4xl">
|
<PixelDivider seed={23} />
|
||||||
Sık sorulan sorular
|
</Parallax>
|
||||||
</h2>
|
|
||||||
<Accordion type="single" collapsible className="mt-10">
|
|
||||||
{faqs.map((faq) => (
|
|
||||||
<AccordionItem key={faq.question} value={faq.question}>
|
|
||||||
<AccordionTrigger className="cursor-pointer text-left font-heading text-base font-semibold">
|
|
||||||
{faq.question}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="text-base leading-relaxed text-slate-600">
|
|
||||||
{faq.answer}
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Final CTA */}
|
{/* Comparison */}
|
||||||
<section className="bg-primary py-20 text-white">
|
<section id="karsilastirma" className="scroll-mt-24 py-20 sm:py-24">
|
||||||
<div className="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
|
<div className="mx-auto max-w-4xl px-4">
|
||||||
<MessageCircleQuestion className="size-10 opacity-80" aria-hidden />
|
<Parallax strength={16}>
|
||||||
<h2 className="mt-6 font-heading text-3xl font-bold sm:text-4xl">
|
<Reveal className="flex flex-col items-center gap-4 text-center">
|
||||||
Tercih listen, pişmanlık listesi olmasın
|
<SectionEyebrow>Karşılaştır</SectionEyebrow>
|
||||||
</h2>
|
<h2 className="font-heading text-3xl font-bold sm:text-4xl">
|
||||||
<p className="mt-4 max-w-xl text-lg leading-relaxed text-white/90">
|
Robot mu, danışman mı, KolayTercih mi?
|
||||||
Sıralamanı gir, yapay zekâ danışmanın veriyle konuşsun. Tercih
|
</h2>
|
||||||
dönemi bitmeden yerini al.
|
<p className="max-w-xl text-lg leading-relaxed text-slate-600">
|
||||||
|
Aynı işi kim, neyle, kaça yapıyor — yan yana bak.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
</Parallax>
|
||||||
|
<Parallax strength={7} className="mt-12">
|
||||||
|
<Reveal
|
||||||
|
delay={0.08}
|
||||||
|
className="overflow-x-auto rounded-2xl border border-slate-200/70 bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-slate-50">
|
||||||
|
<TableHead className="w-[40%]">Özellik</TableHead>
|
||||||
|
<TableHead className="text-center">
|
||||||
|
Ücretsiz robotlar
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center">
|
||||||
|
İnsan danışman
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center font-heading font-bold text-primary">
|
||||||
|
KolayTercih
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{comparison.map((row) => (
|
||||||
|
<TableRow key={row.feature}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{row.feature}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<ComparisonCell value={row.robot} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<ComparisonCell value={row.danisman} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="bg-primary/5 text-center">
|
||||||
|
<ComparisonCell value={row.tercihai} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Reveal>
|
||||||
|
</Parallax>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Karşılaştırma → SSS geçişi */}
|
||||||
|
<div aria-hidden className="mx-auto w-56">
|
||||||
|
<Parallax strength={26}>
|
||||||
|
<PixelDivider seed={31} />
|
||||||
|
</Parallax>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ */}
|
||||||
|
<section id="sss" className="scroll-mt-24 py-20 sm:py-24">
|
||||||
|
<div className="mx-auto max-w-3xl px-4">
|
||||||
|
<Parallax strength={16}>
|
||||||
|
<Reveal className="flex flex-col items-center gap-4 text-center">
|
||||||
|
<SectionEyebrow>SSS</SectionEyebrow>
|
||||||
|
<h2 className="font-heading text-3xl font-bold sm:text-4xl">
|
||||||
|
Sık sorulan sorular
|
||||||
|
</h2>
|
||||||
|
</Reveal>
|
||||||
|
</Parallax>
|
||||||
|
<Parallax strength={7}>
|
||||||
|
<Reveal delay={0.08}>
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
collapsible
|
||||||
|
className="mt-10 rounded-2xl border border-slate-200/70 bg-white px-6 shadow-sm"
|
||||||
|
>
|
||||||
|
{faqs.map((faq) => (
|
||||||
|
<AccordionItem
|
||||||
|
key={faq.question}
|
||||||
|
value={faq.question}
|
||||||
|
className="last:border-b-0"
|
||||||
|
>
|
||||||
|
<AccordionTrigger className="cursor-pointer text-left font-heading text-base font-semibold">
|
||||||
|
{faq.question}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="text-base leading-relaxed text-slate-600">
|
||||||
|
{faq.answer}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</Reveal>
|
||||||
|
</Parallax>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Final CTA — tam genişlik bant yerine yüzen, köşeleri yumuşak panel */}
|
||||||
|
<section className="px-4 pb-24 pt-6 sm:px-6">
|
||||||
|
<Parallax strength={12} className="mx-auto max-w-6xl">
|
||||||
|
<Reveal>
|
||||||
|
<div className="relative isolate overflow-hidden rounded-3xl bg-primary px-4 py-20 text-white shadow-xl shadow-primary/20 sm:py-24">
|
||||||
|
{/* Hero'nun piksel dokusu koyu zeminde: ortası boş, kenarlara doğru yoğun */}
|
||||||
|
{/* Piksel zemin panel içinde geride kalır; taşma payı için dikeyde büyütülür */}
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
|
||||||
|
>
|
||||||
|
<Parallax strength={-22} className="h-full">
|
||||||
|
<PixelField seed={8} />
|
||||||
|
</Parallax>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
|
||||||
|
<MessageCircleQuestion
|
||||||
|
className="size-10 opacity-80"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<h2 className="mt-6 font-heading text-3xl font-bold sm:text-4xl">
|
||||||
|
Tercih listen, pişmanlık listesi olmasın
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 max-w-xl text-lg leading-relaxed text-white/90">
|
||||||
|
Sıralamanı gir, yapay zekâ danışmanın veriyle konuşsun.
|
||||||
|
Tercih dönemi bitmeden yerini al.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
className="mt-8 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
<a href="#hero-form">Ücretsiz dene</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</Parallax>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="pb-32 pt-4">
|
||||||
|
<div className="mx-auto max-w-6xl px-4 text-center text-sm leading-relaxed text-slate-500">
|
||||||
|
<p>
|
||||||
|
© 2026 KolayTercih. Veriler resmî YÖK Atlas kaynağından derlenir;
|
||||||
|
KolayTercih, ÖSYM veya YÖK ile bağlantılı değildir.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<p className="mt-2">
|
||||||
asChild
|
KolayTercih bir karar destek aracıdır, yerleşme garantisi vermez.
|
||||||
size="lg"
|
Tercih listenizin son hali ve başvuru sorumluluğu size aittir.
|
||||||
className="mt-8 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
</p>
|
||||||
|
<nav
|
||||||
|
aria-label="Yasal sayfalar"
|
||||||
|
className="mt-4 flex items-center justify-center gap-4"
|
||||||
>
|
>
|
||||||
<a href="#hero-form">Ücretsiz dene</a>
|
<Link
|
||||||
</Button>
|
href="/gizlilik"
|
||||||
|
className="hover:text-slate-700 hover:underline"
|
||||||
|
>
|
||||||
|
Gizlilik & KVKK
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/kosullar"
|
||||||
|
className="hover:text-slate-700 hover:underline"
|
||||||
|
>
|
||||||
|
Kullanım & iade koşulları
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/iletisim"
|
||||||
|
className="hover:text-slate-700 hover:underline"
|
||||||
|
>
|
||||||
|
İletişim
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</footer>
|
||||||
</main>
|
</ScrollFlow>
|
||||||
|
|
||||||
<footer className="border-t border-slate-200 bg-white pb-32 pt-10">
|
|
||||||
<div className="mx-auto max-w-6xl px-4 text-center text-sm leading-relaxed text-slate-500">
|
|
||||||
<p>
|
|
||||||
© 2026 KolayTercih. Veriler resmî YÖK Atlas kaynağından derlenir;
|
|
||||||
KolayTercih, ÖSYM veya YÖK ile bağlantılı değildir.
|
|
||||||
</p>
|
|
||||||
<p className="mt-2">
|
|
||||||
KolayTercih bir karar destek aracıdır, yerleşme garantisi vermez.
|
|
||||||
Tercih listenizin son hali ve başvuru sorumluluğu size aittir.
|
|
||||||
</p>
|
|
||||||
<nav
|
|
||||||
aria-label="Yasal sayfalar"
|
|
||||||
className="mt-4 flex items-center justify-center gap-4"
|
|
||||||
>
|
|
||||||
<Link href="/gizlilik" className="hover:text-slate-700 hover:underline">
|
|
||||||
Gizlilik & KVKK
|
|
||||||
</Link>
|
|
||||||
<Link href="/kosullar" className="hover:text-slate-700 hover:underline">
|
|
||||||
Kullanım & iade koşulları
|
|
||||||
</Link>
|
|
||||||
<Link href="/iletisim" className="hover:text-slate-700 hover:underline">
|
|
||||||
İletişim
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { getCurrentUser } from "@/lib/session";
|
|||||||
import { URUNLER, DENEME_KREDISI } from "@/lib/credits";
|
import { URUNLER, DENEME_KREDISI } from "@/lib/credits";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Parallax } from "@/components/parallax";
|
||||||
|
import { ScrollFlow } from "@/components/scroll-flow";
|
||||||
import { SatinAlForm } from "./satin-al-form";
|
import { SatinAlForm } from "./satin-al-form";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -51,134 +53,143 @@ export default async function PaketPage({
|
|||||||
const odemeHatasi = hata === "token" || hata === "siparis";
|
const odemeHatasi = hata === "token" || hata === "siparis";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
|
<div className="bg-slate-50 text-slate-900">
|
||||||
<main className="mx-auto w-full max-w-3xl flex-1 px-4 py-16">
|
<ScrollFlow className="flex min-h-screen flex-col">
|
||||||
{odemeHatasi ? (
|
<main className="mx-auto w-full max-w-3xl flex-1 px-4 py-16">
|
||||||
<div
|
{odemeHatasi ? (
|
||||||
role="alert"
|
<div
|
||||||
className="mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800"
|
role="alert"
|
||||||
|
className="mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800"
|
||||||
|
>
|
||||||
|
Ödemen doğrulanamadı. Kartından çekim yapıldıysa kredilerin birkaç
|
||||||
|
dakika içinde otomatik tanımlanır; tanımlanmazsa aşağıdan tekrar
|
||||||
|
deneyebilirsin — çekim yapılmadıysa hiçbir ücret ödemedin.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<Parallax fromStart strength={12} className="text-center">
|
||||||
|
<Badge variant="secondary" className="bg-primary/10 text-primary">
|
||||||
|
Tek seferlik ödeme · Abonelik yok
|
||||||
|
</Badge>
|
||||||
|
<h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl">
|
||||||
|
İnsan danışmanın işini,{" "}
|
||||||
|
<span className="text-primary">onda bir fiyatına</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto mt-3 max-w-xl text-slate-600">
|
||||||
|
Tercih danışmanları bu dönemde 2.000–10.000 TL alıyor. KolayTercih
|
||||||
|
aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar.
|
||||||
|
</p>
|
||||||
|
</Parallax>
|
||||||
|
|
||||||
|
<Parallax
|
||||||
|
strength={6}
|
||||||
|
className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]"
|
||||||
>
|
>
|
||||||
Ödemen doğrulanamadı. Kartından çekim yapıldıysa kredilerin birkaç
|
{/* Ana paket */}
|
||||||
dakika içinde otomatik tanımlanır; tanımlanmazsa aşağıdan tekrar
|
<section className="rounded-2xl border-2 border-orange-500 bg-white p-8 shadow-sm">
|
||||||
deneyebilirsin — çekim yapılmadıysa hiçbir ücret ödemedin.
|
<div className="flex items-baseline justify-between">
|
||||||
</div>
|
<h2 className="font-heading text-xl font-bold">
|
||||||
) : null}
|
Tercih Dönemi Paketi
|
||||||
<div className="text-center">
|
</h2>
|
||||||
<Badge variant="secondary" className="bg-primary/10 text-primary">
|
<p className="font-heading text-3xl font-bold">
|
||||||
Tek seferlik ödeme · Abonelik yok
|
{tl(URUNLER.paket.amountKurus)} TL
|
||||||
</Badge>
|
</p>
|
||||||
<h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl">
|
</div>
|
||||||
İnsan danışmanın işini,{" "}
|
<ul className="mt-6 space-y-3">
|
||||||
<span className="text-primary">onda bir fiyatına</span>
|
{OZELLIKLER.map((o) => (
|
||||||
</h1>
|
<li key={o.text} className="flex items-start gap-3 text-sm">
|
||||||
<p className="mx-auto mt-3 max-w-xl text-slate-600">
|
<o.icon
|
||||||
Tercih danışmanları bu dönemde 2.000–10.000 TL alıyor. KolayTercih
|
className="mt-0.5 size-4 shrink-0 text-emerald-600"
|
||||||
aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar.
|
aria-hidden
|
||||||
</p>
|
/>
|
||||||
</div>
|
{o.text}
|
||||||
|
</li>
|
||||||
<div className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]">
|
))}
|
||||||
{/* Ana paket */}
|
</ul>
|
||||||
<section className="rounded-2xl border-2 border-orange-500 bg-white p-8 shadow-sm">
|
<div className="mt-8">
|
||||||
<div className="flex items-baseline justify-between">
|
{u ? (
|
||||||
<h2 className="font-heading text-xl font-bold">
|
u.hasPaket ? (
|
||||||
Tercih Dönemi Paketi
|
<div className="flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">
|
||||||
</h2>
|
<CheckCircle2 className="size-4" aria-hidden />
|
||||||
<p className="font-heading text-3xl font-bold">
|
Paketin aktif —{" "}
|
||||||
{tl(URUNLER.paket.amountKurus)} TL
|
<Link href="/" className="underline">
|
||||||
</p>
|
listeni oluştur
|
||||||
</div>
|
</Link>
|
||||||
<ul className="mt-6 space-y-3">
|
</div>
|
||||||
{OZELLIKLER.map((o) => (
|
) : (
|
||||||
<li key={o.text} className="flex items-start gap-3 text-sm">
|
<SatinAlForm
|
||||||
<o.icon
|
urun="paket"
|
||||||
className="mt-0.5 size-4 shrink-0 text-emerald-600"
|
label={`${tl(URUNLER.paket.amountKurus)} TL — Paketi al`}
|
||||||
aria-hidden
|
vurgulu
|
||||||
/>
|
/>
|
||||||
{o.text}
|
)
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div className="mt-8">
|
|
||||||
{u ? (
|
|
||||||
u.hasPaket ? (
|
|
||||||
<div className="flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">
|
|
||||||
<CheckCircle2 className="size-4" aria-hidden />
|
|
||||||
Paketin aktif —{" "}
|
|
||||||
<Link href="/" className="underline">
|
|
||||||
listeni oluştur
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/giris?callback=${encodeURIComponent("/paket")}`}
|
||||||
|
>
|
||||||
|
Giriş yap ve {DENEME_KREDISI} deneme kredisi kazan
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-center text-xs text-slate-500">
|
||||||
|
iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "}
|
||||||
|
<Link
|
||||||
|
href="/kosullar#iade"
|
||||||
|
className="underline hover:text-slate-700"
|
||||||
|
>
|
||||||
|
İade koşulları
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Top-up */}
|
||||||
|
<section className="flex h-fit flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:w-56">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Coins className="size-4 text-amber-500" aria-hidden />
|
||||||
|
<h2 className="font-heading font-bold">Kredi bitti mi?</h2>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 flex-1 text-sm text-slate-600">
|
||||||
|
+{URUNLER.topup.credits} soru hakkı,{" "}
|
||||||
|
{tl(URUNLER.topup.amountKurus)} TL.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
{u ? (
|
||||||
<SatinAlForm
|
<SatinAlForm
|
||||||
urun="paket"
|
urun="topup"
|
||||||
label={`${tl(URUNLER.paket.amountKurus)} TL — Paketi al`}
|
label={`+${URUNLER.topup.credits} kredi al`}
|
||||||
vurgulu
|
|
||||||
/>
|
/>
|
||||||
)
|
) : (
|
||||||
) : (
|
<Button
|
||||||
<Button
|
asChild
|
||||||
asChild
|
variant="outline"
|
||||||
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-11 w-full cursor-pointer"
|
||||||
>
|
>
|
||||||
<Link href={`/giris?callback=${encodeURIComponent("/paket")}`}>
|
<Link
|
||||||
Giriş yap ve {DENEME_KREDISI} deneme kredisi kazan
|
href={`/giris?callback=${encodeURIComponent("/paket")}`}
|
||||||
</Link>
|
>
|
||||||
</Button>
|
Önce giriş yap
|
||||||
)}
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
<p className="mt-4 text-center text-xs text-slate-500">
|
)}
|
||||||
iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "}
|
</div>
|
||||||
<Link
|
</section>
|
||||||
href="/kosullar#iade"
|
</Parallax>
|
||||||
className="underline hover:text-slate-700"
|
|
||||||
>
|
|
||||||
İade koşulları
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Top-up */}
|
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
|
||||||
<section className="flex h-fit flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm sm:w-56">
|
KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas
|
||||||
<div className="flex items-center gap-2">
|
verisine dayanır ancak yerleşme garantisi verilmez. Tercih
|
||||||
<Coins className="size-4 text-amber-500" aria-hidden />
|
listesinin nihai sorumluluğu adaya aittir. Sorun mu var?{" "}
|
||||||
<h2 className="font-heading font-bold">Kredi bitti mi?</h2>
|
<Link href="/iletisim" className="underline hover:text-slate-700">
|
||||||
</div>
|
Bize yaz
|
||||||
<p className="mt-2 flex-1 text-sm text-slate-600">
|
</Link>
|
||||||
+{URUNLER.topup.credits} soru hakkı,{" "}
|
.
|
||||||
{tl(URUNLER.topup.amountKurus)} TL.
|
</p>
|
||||||
</p>
|
</main>
|
||||||
<div className="mt-4">
|
</ScrollFlow>
|
||||||
{u ? (
|
|
||||||
<SatinAlForm
|
|
||||||
urun="topup"
|
|
||||||
label={`+${URUNLER.topup.credits} kredi al`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
variant="outline"
|
|
||||||
className="h-11 w-full cursor-pointer"
|
|
||||||
>
|
|
||||||
<Link href={`/giris?callback=${encodeURIComponent("/paket")}`}>
|
|
||||||
Önce giriş yap
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
|
|
||||||
KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas verisine
|
|
||||||
dayanır ancak yerleşme garantisi verilmez. Tercih listesinin nihai
|
|
||||||
sorumluluğu adaya aittir. Sorun mu var?{" "}
|
|
||||||
<Link href="/iletisim" className="underline hover:text-slate-700">
|
|
||||||
Bize yaz
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useActionState, useEffect } from "react";
|
import { useActionState, useEffect } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { baslatOdeme, type OdemeBaslatDurum } from "./actions";
|
import { baslatOdeme, type OdemeBaslatDurum } from "./actions";
|
||||||
@@ -37,9 +36,6 @@ export function SatinAlForm({
|
|||||||
}
|
}
|
||||||
variant={vurgulu ? "default" : "outline"}
|
variant={vurgulu ? "default" : "outline"}
|
||||||
>
|
>
|
||||||
{pending ? (
|
|
||||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
||||||
) : null}
|
|
||||||
{pending ? "Ödeme sayfasına yönlendiriliyorsun…" : label}
|
{pending ? "Ödeme sayfasına yönlendiriliyorsun…" : label}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import { useMemo, useEffect, useRef, useState } from "react";
|
import { useMemo, useEffect, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FileText, Loader2, Lock, RefreshCcw } from "lucide-react";
|
import { FileText, Lock, RefreshCcw } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { SohbetPopup } from "./sohbet-popup";
|
import { SohbetPopup } from "./sohbet-popup";
|
||||||
import { RaporListesi } from "@/components/rapor-listesi";
|
import { RaporListesi } from "@/components/rapor-listesi";
|
||||||
import { TercihHaritasi, type HaritaPin } from "@/components/tercih-haritasi";
|
import { TercihHaritasi, type HaritaPin } from "@/components/tercih-haritasi";
|
||||||
@@ -305,16 +306,28 @@ function UretimGovdesi() {
|
|||||||
ref={govdeRef}
|
ref={govdeRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
className="mt-16 flex flex-col items-center justify-center gap-4 rounded-2xl border border-slate-200 bg-white py-16 text-center outline-none"
|
className="mt-16 rounded-2xl border border-slate-200 bg-white p-6 outline-none sm:p-8"
|
||||||
>
|
>
|
||||||
<Loader2 className="size-10 animate-spin text-primary" aria-hidden />
|
<div className="flex flex-col items-center gap-1.5 text-center">
|
||||||
<p className="font-heading text-lg font-bold">Listen hazırlanıyor</p>
|
<p className="font-heading text-lg font-bold">Listen hazırlanıyor</p>
|
||||||
<p className="max-w-sm text-sm text-slate-500">
|
<p className="max-w-sm text-sm text-slate-500">
|
||||||
{URETIM_MESAJLARI[mesajIdx]}
|
{URETIM_MESAJLARI[mesajIdx]}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-slate-400">
|
||||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gelmekte olan tercih listesinin iskeleti */}
|
||||||
|
<div className="mt-8 flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Skeleton className="h-6 w-44" />
|
||||||
|
<Skeleton className="h-6 w-24 rounded-full" />
|
||||||
|
</div>
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-14 w-full rounded-xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -370,11 +383,7 @@ function RevizyonKutusu({
|
|||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{pending ? (
|
<RefreshCcw className="size-4" aria-hidden />
|
||||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
||||||
) : (
|
|
||||||
<RefreshCcw className="size-4" aria-hidden />
|
|
||||||
)}
|
|
||||||
{pending ? "Yeniden kuruluyor…" : "Revize et (3 kredi)"}
|
{pending ? "Yeniden kuruluyor…" : "Revize et (3 kredi)"}
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-xs text-slate-500">
|
<span className="text-xs text-slate-500">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Loader2, Wrench, X } from "lucide-react";
|
import { Wrench, X } from "lucide-react";
|
||||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
@@ -184,8 +184,8 @@ export function DevPanel() {
|
|||||||
</span>
|
</span>
|
||||||
Dev süperadmin
|
Dev süperadmin
|
||||||
{mesgul ? (
|
{mesgul ? (
|
||||||
<Loader2
|
<span
|
||||||
className="size-3.5 animate-spin text-slate-400"
|
className="size-2 animate-pulse rounded-full bg-slate-400"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ArrowRight, Loader2 } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -123,12 +123,16 @@ export function HeroForm() {
|
|||||||
<Input
|
<Input
|
||||||
id="siralama"
|
id="siralama"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
|
autoComplete="off"
|
||||||
placeholder="YKS başarı sıralaman (ör. 85.000)"
|
placeholder="YKS başarı sıralaman (ör. 85.000)"
|
||||||
value={siralama}
|
value={siralama}
|
||||||
onChange={(e) => siralamaDegisti(e.target.value)}
|
onChange={(e) => siralamaDegisti(e.target.value)}
|
||||||
aria-invalid={hata ? true : undefined}
|
aria-invalid={hata ? true : undefined}
|
||||||
aria-describedby={hata ? "siralama-hata" : undefined}
|
aria-describedby={hata ? "siralama-hata" : undefined}
|
||||||
className="h-12 flex-1 bg-white text-base"
|
// flex-1 yalnızca yatay dizilimde (sm+): mobilde form flex-col olduğu
|
||||||
|
// için flex-1 inputun yüksekliğini eziyordu. Mobilde geniş padding
|
||||||
|
// + büyük yazı; sm ve üzeri eski görünüm.
|
||||||
|
className="h-12 bg-background px-5 text-lg sm:flex-1 sm:px-2.5 sm:text-base"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -136,11 +140,8 @@ export function HeroForm() {
|
|||||||
disabled={yukleniyor}
|
disabled={yukleniyor}
|
||||||
className="h-12 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-12 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
{yukleniyor ? (
|
{yukleniyor ? "Hazırlanıyor…" : "Listemi oluştur"}
|
||||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
) : null}
|
|
||||||
Listemi oluştur
|
|
||||||
{yukleniyor ? null : <ArrowRight className="size-4" aria-hidden />}
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
38
src/components/landing/reveal.tsx
Normal file
38
src/components/landing/reveal.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing bölümlerinin görünüme girerken yumuşakça belirmesi.
|
||||||
|
* Yalnızca transform/opacity animasyonu yapar; hareket azaltılmışsa
|
||||||
|
* kaydırma yerine sadece kısa bir belirme kalır.
|
||||||
|
*/
|
||||||
|
export function Reveal({
|
||||||
|
children,
|
||||||
|
delay = 0,
|
||||||
|
y = 28,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
delay?: number;
|
||||||
|
y?: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const reduceMotion = useReducedMotion();
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
initial={{ opacity: 0, y: reduceMotion ? 0 : y }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: "0px 0px -80px 0px" }}
|
||||||
|
transition={{
|
||||||
|
duration: reduceMotion ? 0.25 : 0.55,
|
||||||
|
delay,
|
||||||
|
ease: [0.21, 0.47, 0.32, 0.98],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/components/parallax.tsx
Normal file
64
src/components/parallax.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, type ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
motion,
|
||||||
|
useReducedMotion,
|
||||||
|
useScroll,
|
||||||
|
useTransform,
|
||||||
|
} from "motion/react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site geneli hafif scroll parallax'ı. Element görünümden geçerken
|
||||||
|
* scroll'a bağlı küçük bir dikey sürüklenme uygular (yalnızca transform,
|
||||||
|
* scroll ile 1:1 — gecikme/easing yok).
|
||||||
|
*
|
||||||
|
* strength > 0: içerik scroll'dan biraz hızlı akar (ön katman).
|
||||||
|
* strength < 0: içerik scroll'un gerisinde kalır (arka katman, derinlik).
|
||||||
|
*
|
||||||
|
* `fromStart`, sayfanın tepesinde yüklenen (hero gibi) elemanlar içindir:
|
||||||
|
* sayfa en üstteyken kayma tam 0'dır, aşağı indikçe birikir — böylece
|
||||||
|
* ilk boyamada içerik kaymış görünmez.
|
||||||
|
*
|
||||||
|
* Hareket azaltılmışsa (prefers-reduced-motion) hiç kayma uygulanmaz.
|
||||||
|
*/
|
||||||
|
export function Parallax({
|
||||||
|
children,
|
||||||
|
strength = 20,
|
||||||
|
fromStart = false,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
/** Piksel cinsinden sürüklenme genliği; işaret katman yönünü belirler. */
|
||||||
|
strength?: number;
|
||||||
|
fromStart?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const reduceMotion = useReducedMotion();
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: ref,
|
||||||
|
offset: fromStart
|
||||||
|
? ["start start", "end start"]
|
||||||
|
: ["start end", "end start"],
|
||||||
|
});
|
||||||
|
const y = useTransform(
|
||||||
|
scrollYProgress,
|
||||||
|
[0, 1],
|
||||||
|
fromStart ? [0, -2 * strength] : [strength, -strength],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
className={className}
|
||||||
|
// willChange: kayma 0'dan geçerken katmanın compositor'dan düşüp
|
||||||
|
// tekrar oluşturulmasını (raster churn) engeller
|
||||||
|
style={
|
||||||
|
reduceMotion ? { y: 0 } : { y, willChange: "transform" }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
256
src/components/pixel-decor.tsx
Normal file
256
src/components/pixel-decor.tsx
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, type ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { usePixelHeat } from "@/components/use-pixel-heat";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero'daki dot-matrix dilini (TurkeyPixelMap) bölüm ayraçlarına ve
|
||||||
|
* dekorlara taşıyan piksel süsleri. Rastgelelik yerine deterministik
|
||||||
|
* hash kullanılır ki sunucu ve istemci aynı deseni üretsin.
|
||||||
|
*
|
||||||
|
* Hepsi hero'daki imleç ısı fırçasına katılır (usePixelHeat): imleç
|
||||||
|
* yaklaşınca pikseller turuncuya ısınır, uzaklaşınca yavaşça soğur.
|
||||||
|
* Turuncu vurgu (accent) pikselleri hero'daki şehirler gibi fırçaya
|
||||||
|
* katılmaz.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Hero ile aynı ızgara ölçüleri
|
||||||
|
const CELL = 10;
|
||||||
|
const DOT = 7.4;
|
||||||
|
const INSET = (CELL - DOT) / 2;
|
||||||
|
|
||||||
|
// Hero ile aynı doku paleti
|
||||||
|
const OPACITIES = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
|
||||||
|
const opacityFor = (x: number, y: number) =>
|
||||||
|
OPACITIES[(x * 7 + y * 11) % OPACITIES.length];
|
||||||
|
|
||||||
|
// Deterministik sözde-rastgele: aynı girdi her ortamda aynı deseni verir
|
||||||
|
const hash = (x: number, y: number, seed: number) => {
|
||||||
|
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||||||
|
return n - Math.floor(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
function pixelRect(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
extra: { key: string; accent?: boolean; accentDelay?: number },
|
||||||
|
) {
|
||||||
|
const shared = {
|
||||||
|
x: x * CELL + INSET,
|
||||||
|
y: y * CELL + INSET,
|
||||||
|
width: DOT,
|
||||||
|
height: DOT,
|
||||||
|
rx: 2,
|
||||||
|
};
|
||||||
|
if (extra.accent) {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={extra.key}
|
||||||
|
{...shared}
|
||||||
|
className="animate-pulse fill-orange-500/70 motion-reduce:animate-none"
|
||||||
|
style={{
|
||||||
|
animationDuration: "3.6s",
|
||||||
|
animationDelay: `${extra.accentDelay ?? 0}s`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const op = opacityFor(x, y);
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={extra.key}
|
||||||
|
{...shared}
|
||||||
|
data-px=""
|
||||||
|
data-cx={x * CELL + CELL / 2}
|
||||||
|
data-cy={y * CELL + CELL / 2}
|
||||||
|
data-op={op}
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity={op}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geniş alanlar için seyrek piksel zemini (ör. koyu CTA bandı).
|
||||||
|
* İçeriğin oturduğu orta bölge tamamen boş bırakılır; pikseller
|
||||||
|
* kenarlara doğru yoğunlaşır.
|
||||||
|
*/
|
||||||
|
export function PixelField({
|
||||||
|
seed = 7,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
seed?: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
usePixelHeat(svgRef);
|
||||||
|
|
||||||
|
const COLS = 42;
|
||||||
|
const ROWS = 12;
|
||||||
|
const cx = (COLS - 1) / 2;
|
||||||
|
const cy = (ROWS - 1) / 2;
|
||||||
|
const maxD = Math.sqrt(cx * cx + cy * cy);
|
||||||
|
|
||||||
|
const pixels: ReactNode[] = [];
|
||||||
|
let accentIdx = 0;
|
||||||
|
|
||||||
|
// Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon
|
||||||
|
// (genişliğin %22–78 bandı) tüm yükseklik boyunca tamamen boş kalır
|
||||||
|
const BAND_MIN = COLS * 0.22;
|
||||||
|
const BAND_MAX = COLS * 0.78;
|
||||||
|
|
||||||
|
for (let y = 0; y < ROWS; y++) {
|
||||||
|
for (let x = 0; x < COLS; x++) {
|
||||||
|
if (x > BAND_MIN && x < BAND_MAX) continue;
|
||||||
|
// Kenarlara doğru artan doluluk
|
||||||
|
const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) / maxD;
|
||||||
|
const density = 0.1 + 0.42 * d * d;
|
||||||
|
if (hash(x, y, seed) > density) continue;
|
||||||
|
const accent = hash(x, y, seed + 99) > 0.96;
|
||||||
|
pixels.push(
|
||||||
|
pixelRect(x, y, {
|
||||||
|
key: `${x}-${y}`,
|
||||||
|
accent,
|
||||||
|
accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox={`0 0 ${COLS * CELL} ${ROWS * CELL}`}
|
||||||
|
preserveAspectRatio="xMidYMid slice"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
className={cn("h-full w-full", className)}
|
||||||
|
>
|
||||||
|
{pixels}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bölümler arasında sert border çizgileri yerine kullanılan, ortada
|
||||||
|
* yoğunlaşıp kenarlara doğru dağılan piksel ayraç. Hero'nun dot-matrix
|
||||||
|
* dilini bölüm geçişlerine taşır.
|
||||||
|
*/
|
||||||
|
export function PixelDivider({
|
||||||
|
seed = 5,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
seed?: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
usePixelHeat(svgRef);
|
||||||
|
|
||||||
|
const COLS = 12;
|
||||||
|
const ROWS = 2;
|
||||||
|
const cx = (COLS - 1) / 2;
|
||||||
|
|
||||||
|
const pixels: ReactNode[] = [];
|
||||||
|
let accentPlaced = false;
|
||||||
|
|
||||||
|
for (let y = 0; y < ROWS; y++) {
|
||||||
|
for (let x = 0; x < COLS; x++) {
|
||||||
|
// Az sayıda, iri piksel: ortada yoğun, kenara doğru seyrelen küçük küme
|
||||||
|
const dx = Math.abs(x - cx) / cx;
|
||||||
|
const rowFalloff = y === 0 ? 0.9 : 0.35;
|
||||||
|
const density = (1 - dx) ** 1.1 * rowFalloff;
|
||||||
|
if (hash(x, y, seed) > density) continue;
|
||||||
|
const accent = !accentPlaced && hash(x, y, seed + 99) > 0.8;
|
||||||
|
if (accent) accentPlaced = true;
|
||||||
|
pixels.push(
|
||||||
|
pixelRect(x, y, {
|
||||||
|
key: `${x}-${y}`,
|
||||||
|
accent,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox={`0 0 ${COLS * CELL} ${ROWS * CELL}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
className={cn("h-auto w-full text-primary", className)}
|
||||||
|
>
|
||||||
|
{pixels}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Küçük sabit süslerde (mark/korner) data-px öznitelikli rect. */
|
||||||
|
function fixedPixel(x: number, y: number, opacity: number) {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
x={x + INSET}
|
||||||
|
y={y + INSET}
|
||||||
|
width={DOT}
|
||||||
|
height={DOT}
|
||||||
|
rx={2}
|
||||||
|
data-px=""
|
||||||
|
data-cx={x + CELL / 2}
|
||||||
|
data-cy={y + CELL / 2}
|
||||||
|
data-op={opacity}
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity={opacity}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Eyebrow rozetlerinde kullanılan 2×2'lik minik piksel imzası. */
|
||||||
|
export function PixelMark({ className }: { className?: string }) {
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
usePixelHeat(svgRef, { radius: 24 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
className={cn("size-3.5 shrink-0", className)}
|
||||||
|
>
|
||||||
|
{fixedPixel(0, 0, 0.35)}
|
||||||
|
<rect x={11.3} y={1.3} width={7.4} height={7.4} rx={2} className="fill-orange-500" fillOpacity={0.85} />
|
||||||
|
{fixedPixel(0, 10, 0.6)}
|
||||||
|
{fixedPixel(10, 10, 0.18)}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kart köşelerine iliştirilen sessiz piksel kümesi. */
|
||||||
|
export function PixelCorner({ className }: { className?: string }) {
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
usePixelHeat(svgRef, { radius: 28 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox="0 0 30 30"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
className={cn("size-7 text-primary", className)}
|
||||||
|
>
|
||||||
|
{fixedPixel(20, 0, 0.5)}
|
||||||
|
{fixedPixel(10, 0, 0.14)}
|
||||||
|
{fixedPixel(20, 10, 0.26)}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bölüm başlıklarının üstünde duran piksel imzalı eyebrow rozeti. */
|
||||||
|
export function SectionEyebrow({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold uppercase tracking-[0.12em] text-primary">
|
||||||
|
<PixelMark />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/components/scroll-flow.tsx
Normal file
74
src/components/scroll-flow.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type ReactNode } from "react";
|
||||||
|
import { ReactLenis } from "lenis/react";
|
||||||
|
import {
|
||||||
|
motion,
|
||||||
|
useReducedMotion,
|
||||||
|
useScroll,
|
||||||
|
useSpring,
|
||||||
|
useTransform,
|
||||||
|
useVelocity,
|
||||||
|
} from "motion/react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitrin sayfalarının akışkan scroll efekti: Lenis ile lerp'li (yavaşlatılmış,
|
||||||
|
* momentumlu) kaydırma + scroll hızına bağlı içerik bükülmesi (skew).
|
||||||
|
* Hızlı kaydırdıkça sayfa hafifçe yana yatar, durunca yayla yerine oturur.
|
||||||
|
*
|
||||||
|
* Sohbet ve liste gibi iç scroll alanı olan uygulama sayfalarında KULLANMA —
|
||||||
|
* Lenis tekerlek olayını sayfaya yönlendirdiği için iç scroll'u bozar,
|
||||||
|
* transform da `fixed` konumlu çocukları koparır.
|
||||||
|
*
|
||||||
|
* Hareket azaltılmışsa hem yumuşatma hem bükülme tamamen kapalıdır.
|
||||||
|
*/
|
||||||
|
export function ScrollFlow({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
maxSkew = 3,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
/** Derece cinsinden en fazla bükülme açısı. */
|
||||||
|
maxSkew?: number;
|
||||||
|
}) {
|
||||||
|
const reduceMotion = useReducedMotion();
|
||||||
|
const { scrollY } = useScroll();
|
||||||
|
const velocity = useVelocity(scrollY);
|
||||||
|
// Ham hız titrek olur; yay ile yumuşatınca bükülme salınıp yerine oturur
|
||||||
|
const smoothVelocity = useSpring(velocity, {
|
||||||
|
damping: 50,
|
||||||
|
stiffness: 420,
|
||||||
|
mass: 0.6,
|
||||||
|
});
|
||||||
|
// Ölü bölge: yavaş/ufak kaydırmalarda bükülme tamamen 0 kalır — transform
|
||||||
|
// hiç değişmediği için compositor'a kare başı iş çıkmaz. Eşik üstünde
|
||||||
|
// kareyle (ease-in) yükselir, 0,01°'lik adımlara yuvarlanır ki yayın
|
||||||
|
// mikro-salınımları her karede katmanı kirletmesin.
|
||||||
|
const skewY = useTransform(smoothVelocity, (v) => {
|
||||||
|
const DEADZONE = 500; // px/sn — bunun altı "normal gezinme" sayılır
|
||||||
|
const FULL = 2600; // px/sn — bu hızda maxSkew'e ulaşılır
|
||||||
|
const mag = Math.abs(v);
|
||||||
|
if (mag < DEADZONE) return 0;
|
||||||
|
const t = Math.min(1, (mag - DEADZONE) / (FULL - DEADZONE));
|
||||||
|
const s = t * t * maxSkew * Math.sign(v);
|
||||||
|
return Math.round(s * 100) / 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reduceMotion) {
|
||||||
|
return <div className={className}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactLenis root options={{ lerp: 0.09, anchors: true }}>
|
||||||
|
{/* willChange: bükülme 0'a döndüğünde dev katman GPU'dan düşüp
|
||||||
|
bir sonraki tekerlekte yeniden rasterize edilmesin */}
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
style={{ skewY, willChange: "transform" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</ReactLenis>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,12 +14,14 @@ export function SiteHeader() {
|
|||||||
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between gap-3 px-4">
|
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between gap-3 px-4">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="group flex items-center gap-3 font-heading text-xl font-bold"
|
className="group flex items-center gap-3 font-bricolage text-xl"
|
||||||
>
|
>
|
||||||
<span className="flex size-11 items-center justify-center rounded-full bg-primary text-white transition-transform duration-200 group-hover:-rotate-6">
|
<span>
|
||||||
<GraduationCap className="size-6" aria-hidden />
|
<span className="font-light text-3xl tracking-tighter">Kolay</span>
|
||||||
|
<span className="font-semibold text-3xl tracking-tighter">
|
||||||
|
Tercih
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
KolayTercih
|
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="hidden items-center gap-1 rounded-full border border-slate-200 bg-white p-1.5 text-base font-medium text-slate-600 sm:flex">
|
<nav className="hidden items-center gap-1 rounded-full border border-slate-200 bg-white p-1.5 text-base font-medium text-slate-600 sm:flex">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
|
|||||||
166
src/components/turkey-pixel-map.tsx
Normal file
166
src/components/turkey-pixel-map.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dot-matrix Türkiye silüeti — hero arka planı için dekoratif SVG.
|
||||||
|
* Izgara, gerçek sınır poligonlarından 64×28 hücreye rasterize edildi;
|
||||||
|
* Marmara Denizi boşluğu bilinçli olarak açık.
|
||||||
|
*
|
||||||
|
* Performans kurgusu: ~1000 piksel, React elemanı olarak DEĞİL, modül
|
||||||
|
* yüklenirken tek sefer üretilen statik bir SVG string'i olarak basılır
|
||||||
|
* (dangerouslySetInnerHTML). Böylece her sayfa yenilemesinde React bu
|
||||||
|
* elemanları yeniden kurmaz/hydrate etmez; sunucudan gelen HTML olduğu
|
||||||
|
* gibi kullanılır. Deterministik üretim sayesinde server ve client aynı
|
||||||
|
* çıktıyı verir (hydration uyumsuzluğu olmaz).
|
||||||
|
*
|
||||||
|
* Cursor etkileşimi: imleç yaklaşınca pikseller anında turuncuya ısınır,
|
||||||
|
* imleç uzaklaşınca yavaşça kendi mavi tonuna soğur (asimetrik zamanlama).
|
||||||
|
* Fırça mantığı usePixelHeat hook'unda ortaklaştırıldı; DOM'a
|
||||||
|
* querySelectorAll ile bağlandığı için statik markup'la sorunsuz çalışır.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { usePixelHeat } from "@/components/use-pixel-heat";
|
||||||
|
|
||||||
|
const GRID = [
|
||||||
|
"..######..............##########................................",
|
||||||
|
"..#######...........##############..............................",
|
||||||
|
"..#######.........#################................#####........",
|
||||||
|
".########...###########################...........########......",
|
||||||
|
".######.....###############################################.....",
|
||||||
|
".###........###############################################.....",
|
||||||
|
".##.......##################################################....",
|
||||||
|
".###########################################################....",
|
||||||
|
".#############################################################..",
|
||||||
|
"..#############################################################.",
|
||||||
|
"...############################################################.",
|
||||||
|
"...##########################################################...",
|
||||||
|
"...##########################################################...",
|
||||||
|
"...###########################################################..",
|
||||||
|
"..############################################################..",
|
||||||
|
"..############################################################..",
|
||||||
|
"...###########################################################..",
|
||||||
|
"....##########################################################..",
|
||||||
|
"....##########################################################..",
|
||||||
|
".....#########################################################..",
|
||||||
|
".....###################################################..##....",
|
||||||
|
".....############################################...............",
|
||||||
|
"......##########..######################...###..................",
|
||||||
|
"...........#####....#########....#####..........................",
|
||||||
|
"............###.......#####......####...........................",
|
||||||
|
".................................###............................",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Vurgu pikselleri: öğrenci yoğunluğu yüksek şehirler (col, row)
|
||||||
|
const CITIES = [
|
||||||
|
{ name: "İstanbul", x: 8, y: 4 },
|
||||||
|
{ name: "Ankara", x: 23, y: 9 },
|
||||||
|
{ name: "İzmir", x: 4, y: 16 },
|
||||||
|
{ name: "Antalya", x: 16, y: 22 },
|
||||||
|
{ name: "Kayseri", x: 32, y: 14 },
|
||||||
|
{ name: "Trabzon", x: 46, y: 5 },
|
||||||
|
{ name: "Diyarbakır", x: 47, y: 18 },
|
||||||
|
{ name: "Van", x: 58, y: 15 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CELL = 10;
|
||||||
|
const DOT = 7.4;
|
||||||
|
const INSET = (CELL - DOT) / 2;
|
||||||
|
const COLS = GRID[0].length;
|
||||||
|
const ROWS = GRID.length;
|
||||||
|
// Güney kıyının altına doğru seyrelerek dağılan serpinti derinliği (hücre)
|
||||||
|
const SCATTER_DEPTH = 9;
|
||||||
|
const VIEW_W = COLS * CELL;
|
||||||
|
const VIEW_H = (ROWS + SCATTER_DEPTH) * CELL;
|
||||||
|
|
||||||
|
// Deterministik doku: hücreye göre sabit opaklık (hydration güvenli)
|
||||||
|
const OPACITIES = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
|
||||||
|
const opacityFor = (x: number, y: number) =>
|
||||||
|
OPACITIES[(x * 7 + y * 11) % OPACITIES.length];
|
||||||
|
|
||||||
|
const cityCells = new Set(CITIES.map((c) => `${c.x},${c.y}`));
|
||||||
|
|
||||||
|
// Deterministik sözde-rastgele (pixel-decor ile aynı): server ve client
|
||||||
|
// aynı serpinti desenini üretir
|
||||||
|
const hash = (x: number, y: number, seed = 0) => {
|
||||||
|
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||||||
|
return n - Math.floor(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Statik markup: modül yüklenirken TEK SEFER üretilir */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const rectAttrs = (x: number, y: number) =>
|
||||||
|
`x="${x * CELL + INSET}" y="${y * CELL + INSET}" width="${DOT}" height="${DOT}" rx="2"`;
|
||||||
|
|
||||||
|
const MAP_MARKUP = (() => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
GRID.forEach((row, y) => {
|
||||||
|
for (let x = 0; x < row.length; x++) {
|
||||||
|
if (row[x] !== "#" || cityCells.has(`${x},${y}`)) continue;
|
||||||
|
const op = opacityFor(x, y);
|
||||||
|
parts.push(
|
||||||
|
`<rect data-px data-cx="${x * CELL + CELL / 2}" data-cy="${y * CELL + CELL / 2}" data-op="${op}" ${rectAttrs(x, y)} fill="currentColor" fill-opacity="${op}"/>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Silüetin altına serpinti: her kolonun en alttaki kara hücresinden
|
||||||
|
// aşağı doğru, kıyı hattını izleyerek seyrelen pikseller. Hover
|
||||||
|
// fırçasına da katılırlar (data-px).
|
||||||
|
const lastLand: number[] = Array(COLS).fill(-1);
|
||||||
|
GRID.forEach((row, y) => {
|
||||||
|
for (let x = 0; x < row.length; x++) {
|
||||||
|
if (row[x] === "#") lastLand[x] = y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let scatterAccent = 0;
|
||||||
|
for (let x = 0; x < COLS; x++) {
|
||||||
|
if (lastLand[x] < 0) continue;
|
||||||
|
for (let d = 1; d <= SCATTER_DEPTH; d++) {
|
||||||
|
const y = lastLand[x] + d;
|
||||||
|
if (y >= ROWS + SCATTER_DEPTH) break;
|
||||||
|
// Kıyıdan uzaklaştıkça seyrelen doluluk; küçük taban sayesinde
|
||||||
|
// en derin sıralarda da tek tük piksel kalır
|
||||||
|
const density = 0.05 + 0.4 * (1 - (d - 1) / SCATTER_DEPTH) ** 2;
|
||||||
|
if (hash(x, y) > density) continue;
|
||||||
|
if (hash(x, y, 99) > 0.96) {
|
||||||
|
// Nadir turuncu vurgu: şehir pikselleriyle aynı nabız dili
|
||||||
|
parts.push(
|
||||||
|
`<rect ${rectAttrs(x, y)} class="animate-pulse fill-orange-500/70 motion-reduce:animate-none" style="animation-duration:3.6s;animation-delay:${(scatterAccent++ % 8) * 0.45}s"/>`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Derinlikle sönen opaklık
|
||||||
|
const op =
|
||||||
|
Math.round(opacityFor(x, y) * (1 - d / (SCATTER_DEPTH + 1)) * 100) /
|
||||||
|
100;
|
||||||
|
parts.push(
|
||||||
|
`<rect data-px data-cx="${x * CELL + CELL / 2}" data-cy="${y * CELL + CELL / 2}" data-op="${op}" ${rectAttrs(x, y)} fill="currentColor" fill-opacity="${op}"/>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Şehir pikselleri: turuncu, hafif nabız; hover fırçasına katılmazlar
|
||||||
|
CITIES.forEach((city, i) => {
|
||||||
|
parts.push(
|
||||||
|
`<rect ${rectAttrs(city.x, city.y)} class="animate-pulse fill-orange-500/70 motion-reduce:animate-none" style="animation-duration:3.6s;animation-delay:${i * 0.45}s"/>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return parts.join("");
|
||||||
|
})();
|
||||||
|
|
||||||
|
export function TurkeyPixelMap({ className }: { className?: string }) {
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
usePixelHeat(svgRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
||||||
|
dangerouslySetInnerHTML={{ __html: MAP_MARKUP }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ import React from "react"
|
|||||||
|
|
||||||
export interface LoaderProps {
|
export interface LoaderProps {
|
||||||
variant?:
|
variant?:
|
||||||
| "circular"
|
|
||||||
| "classic"
|
|
||||||
| "pulse"
|
| "pulse"
|
||||||
| "pulse-dot"
|
| "pulse-dot"
|
||||||
| "dots"
|
| "dots"
|
||||||
@@ -22,78 +20,6 @@ export interface LoaderProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CircularLoader({
|
|
||||||
className,
|
|
||||||
size = "md",
|
|
||||||
}: {
|
|
||||||
className?: string
|
|
||||||
size?: "sm" | "md" | "lg"
|
|
||||||
}) {
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: "size-4",
|
|
||||||
md: "size-5",
|
|
||||||
lg: "size-6",
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"border-primary animate-spin rounded-full border-2 border-t-transparent",
|
|
||||||
sizeClasses[size],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Loading</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ClassicLoader({
|
|
||||||
className,
|
|
||||||
size = "md",
|
|
||||||
}: {
|
|
||||||
className?: string
|
|
||||||
size?: "sm" | "md" | "lg"
|
|
||||||
}) {
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: "size-4",
|
|
||||||
md: "size-5",
|
|
||||||
lg: "size-6",
|
|
||||||
}
|
|
||||||
|
|
||||||
const barSizes = {
|
|
||||||
sm: { height: "6px", width: "1.5px" },
|
|
||||||
md: { height: "8px", width: "2px" },
|
|
||||||
lg: { height: "10px", width: "2.5px" },
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("relative", sizeClasses[size], className)}>
|
|
||||||
<div className="absolute h-full w-full">
|
|
||||||
{[...Array(12)].map((_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="bg-primary absolute animate-[spinner-fade_1.2s_linear_infinite] rounded-full"
|
|
||||||
style={{
|
|
||||||
top: "0",
|
|
||||||
left: "50%",
|
|
||||||
marginLeft:
|
|
||||||
size === "sm" ? "-0.75px" : size === "lg" ? "-1.25px" : "-1px",
|
|
||||||
transformOrigin: `${size === "sm" ? "0.75px" : size === "lg" ? "1.25px" : "1px"} ${size === "sm" ? "10px" : size === "lg" ? "14px" : "12px"}`,
|
|
||||||
transform: `rotate(${i * 30}deg)`,
|
|
||||||
opacity: 0,
|
|
||||||
animationDelay: `${i * 0.1}s`,
|
|
||||||
height: barSizes[size].height,
|
|
||||||
width: barSizes[size].width,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<span className="sr-only">Loading</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PulseLoader({
|
export function PulseLoader({
|
||||||
className,
|
className,
|
||||||
size = "md",
|
size = "md",
|
||||||
@@ -461,16 +387,12 @@ export function TextDotsLoader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Loader({
|
function Loader({
|
||||||
variant = "circular",
|
variant = "dots",
|
||||||
size = "md",
|
size = "md",
|
||||||
text,
|
text,
|
||||||
className,
|
className,
|
||||||
}: LoaderProps) {
|
}: LoaderProps) {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case "circular":
|
|
||||||
return <CircularLoader size={size} className={className} />
|
|
||||||
case "classic":
|
|
||||||
return <ClassicLoader size={size} className={className} />
|
|
||||||
case "pulse":
|
case "pulse":
|
||||||
return <PulseLoader size={size} className={className} />
|
return <PulseLoader size={size} className={className} />
|
||||||
case "pulse-dot":
|
case "pulse-dot":
|
||||||
@@ -492,7 +414,7 @@ function Loader({
|
|||||||
case "loading-dots":
|
case "loading-dots":
|
||||||
return <TextDotsLoader text={text} size={size} className={className} />
|
return <TextDotsLoader text={text} size={size} className={className} />
|
||||||
default:
|
default:
|
||||||
return <CircularLoader size={size} className={className} />
|
return <DotsLoader size={size} className={className} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon } from "lucide-react"
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme()
|
||||||
@@ -25,7 +25,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
<OctagonXIcon className="size-4" />
|
<OctagonXIcon className="size-4" />
|
||||||
),
|
),
|
||||||
loading: (
|
loading: (
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
<span className="block size-2.5 animate-pulse rounded-full bg-current" />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
|
|||||||
109
src/components/use-pixel-heat.ts
Normal file
109
src/components/use-pixel-heat.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hero'daki (TurkeyPixelMap) imleç ısı fırçasının ortak hook hali.
|
||||||
|
* `data-px` işaretli rect'ler imleç yaklaşınca anında turuncuya ısınır,
|
||||||
|
* uzaklaşınca yavaşça kendi rengine soğur (asimetrik zamanlama).
|
||||||
|
*
|
||||||
|
* SVG katmanları çoğunlukla pointer-events-none olduğundan imleç window
|
||||||
|
* üzerinden izlenir ve viewBox koordinatına çevrilir. Beklenen veri
|
||||||
|
* öznitelikleri: data-px, data-cx, data-cy, data-op (taban opaklık).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, type RefObject } from "react";
|
||||||
|
import { animate, type AnimationPlaybackControls } from "motion";
|
||||||
|
|
||||||
|
const FINE_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
|
||||||
|
const HOT_COLOR = "#f97316"; // orange-500
|
||||||
|
const HOT_OPACITY = 0.85;
|
||||||
|
const HEAT_DURATION = 0.15; // ısınma: anlık geri bildirim
|
||||||
|
const COOL_DURATION = 1.4; // soğuma: iz gibi yavaşça söner
|
||||||
|
|
||||||
|
type HeatTarget = {
|
||||||
|
el: SVGRectElement;
|
||||||
|
cx: number;
|
||||||
|
cy: number;
|
||||||
|
baseOpacity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function usePixelHeat(
|
||||||
|
svgRef: RefObject<SVGSVGElement | null>,
|
||||||
|
{
|
||||||
|
/** Fırça yarıçapı, SVG (viewBox) biriminde */
|
||||||
|
radius = 34,
|
||||||
|
}: { radius?: number } = {},
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const svg = svgRef.current;
|
||||||
|
if (!svg) return;
|
||||||
|
// Dokunmatik cihazlarda hover yanlış tetiklenir; efekti imleçli cihazlara sakla
|
||||||
|
if (!window.matchMedia(FINE_POINTER_QUERY).matches) return;
|
||||||
|
|
||||||
|
const viewW = svg.viewBox.baseVal.width;
|
||||||
|
if (!viewW) return;
|
||||||
|
|
||||||
|
const baseColor = getComputedStyle(svg).color;
|
||||||
|
const pixels: HeatTarget[] = Array.from(
|
||||||
|
svg.querySelectorAll<SVGRectElement>("[data-px]"),
|
||||||
|
).map((el) => ({
|
||||||
|
el,
|
||||||
|
cx: Number(el.dataset.cx),
|
||||||
|
cy: Number(el.dataset.cy),
|
||||||
|
baseOpacity: Number(el.dataset.op),
|
||||||
|
}));
|
||||||
|
if (pixels.length === 0) return;
|
||||||
|
|
||||||
|
const hot = new Set<SVGRectElement>();
|
||||||
|
const controls = new Map<SVGRectElement, AnimationPlaybackControls>();
|
||||||
|
|
||||||
|
const run = (
|
||||||
|
el: SVGRectElement,
|
||||||
|
to: { fill: string; fillOpacity: number },
|
||||||
|
duration: number,
|
||||||
|
) => {
|
||||||
|
controls.get(el)?.stop();
|
||||||
|
controls.set(el, animate(el, to, { duration, ease: "easeOut" }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMove = (e: PointerEvent) => {
|
||||||
|
const rect = svg.getBoundingClientRect();
|
||||||
|
if (rect.width === 0) return;
|
||||||
|
const scale = rect.width / viewW;
|
||||||
|
const pad = radius * scale;
|
||||||
|
// İmleç fırça menzilinin tamamen dışındaysa ve sıcak piksel yoksa
|
||||||
|
// hiç hesap yapma (sayfada birden çok örnek dinlediği için önemli)
|
||||||
|
if (
|
||||||
|
hot.size === 0 &&
|
||||||
|
(e.clientX < rect.left - pad ||
|
||||||
|
e.clientX > rect.right + pad ||
|
||||||
|
e.clientY < rect.top - pad ||
|
||||||
|
e.clientY > rect.bottom + pad)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const x = (e.clientX - rect.left) / scale;
|
||||||
|
const y = (e.clientY - rect.top) / scale;
|
||||||
|
for (const p of pixels) {
|
||||||
|
const d2 = (p.cx - x) ** 2 + (p.cy - y) ** 2;
|
||||||
|
const isHot = d2 <= radius * radius;
|
||||||
|
if (isHot && !hot.has(p.el)) {
|
||||||
|
hot.add(p.el);
|
||||||
|
run(p.el, { fill: HOT_COLOR, fillOpacity: HOT_OPACITY }, HEAT_DURATION);
|
||||||
|
} else if (!isHot && hot.has(p.el)) {
|
||||||
|
hot.delete(p.el);
|
||||||
|
run(
|
||||||
|
p.el,
|
||||||
|
{ fill: baseColor, fillOpacity: p.baseOpacity },
|
||||||
|
COOL_DURATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", onMove, { passive: true });
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointermove", onMove);
|
||||||
|
controls.forEach((c) => c.stop());
|
||||||
|
};
|
||||||
|
}, [svgRef, radius]);
|
||||||
|
}
|
||||||
BIN
src/fonts/ClarityCity-Bold.woff2
Normal file
BIN
src/fonts/ClarityCity-Bold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/ClarityCity-ExtraBold.woff2
Normal file
BIN
src/fonts/ClarityCity-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
src/fonts/ClarityCity-Medium.woff2
Normal file
BIN
src/fonts/ClarityCity-Medium.woff2
Normal file
Binary file not shown.
BIN
src/fonts/ClarityCity-Regular.woff2
Normal file
BIN
src/fonts/ClarityCity-Regular.woff2
Normal file
Binary file not shown.
BIN
src/fonts/ClarityCity-SemiBold.woff2
Normal file
BIN
src/fonts/ClarityCity-SemiBold.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user