From dc6d894a371e5025418e4da4a9f585142ef9011e Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 30 Jul 2026 03:17:12 +0300 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- .claude/launch.json | 6 + data/app.db | Bin 135168 -> 135168 bytes package.json | 2 + pnpm-lock.yaml | 29 ++ src/app/giris/giris-form.tsx | 18 +- src/app/gizlilik/page.tsx | 119 ++--- src/app/globals.css | 20 +- src/app/iletisim/page.tsx | 44 +- src/app/kosullar/page.tsx | 134 +++--- src/app/layout.tsx | 28 +- src/app/listem/revizyon-kutusu.tsx | 8 +- src/app/listem/sohbet-client.tsx | 17 +- src/app/loading.tsx | 31 +- src/app/page.tsx | 645 ++++++++++++++++---------- src/app/paket/page.tsx | 257 +++++----- src/app/paket/satin-al-form.tsx | 4 - src/app/sonuc/liste-paneli.tsx | 39 +- src/components/dev/dev-panel.tsx | 6 +- src/components/hero-form.tsx | 15 +- src/components/landing/reveal.tsx | 38 ++ src/components/parallax.tsx | 64 +++ src/components/pixel-decor.tsx | 256 ++++++++++ src/components/scroll-flow.tsx | 74 +++ src/components/site-header.tsx | 10 +- src/components/turkey-pixel-map.tsx | 166 +++++++ src/components/ui/loader.tsx | 82 +--- src/components/ui/sonner.tsx | 4 +- src/components/use-pixel-heat.ts | 109 +++++ src/fonts/ClarityCity-Bold.woff2 | Bin 0 -> 17168 bytes src/fonts/ClarityCity-ExtraBold.woff2 | Bin 0 -> 17200 bytes src/fonts/ClarityCity-Medium.woff2 | Bin 0 -> 17092 bytes src/fonts/ClarityCity-Regular.woff2 | Bin 0 -> 16856 bytes src/fonts/ClarityCity-SemiBold.woff2 | Bin 0 -> 17252 bytes 33 files changed, 1574 insertions(+), 651 deletions(-) create mode 100644 src/components/landing/reveal.tsx create mode 100644 src/components/parallax.tsx create mode 100644 src/components/pixel-decor.tsx create mode 100644 src/components/scroll-flow.tsx create mode 100644 src/components/turkey-pixel-map.tsx create mode 100644 src/components/use-pixel-heat.ts create mode 100644 src/fonts/ClarityCity-Bold.woff2 create mode 100644 src/fonts/ClarityCity-ExtraBold.woff2 create mode 100644 src/fonts/ClarityCity-Medium.woff2 create mode 100644 src/fonts/ClarityCity-Regular.woff2 create mode 100644 src/fonts/ClarityCity-SemiBold.woff2 diff --git a/.claude/launch.json b/.claude/launch.json index cced7b3..91dff23 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -12,6 +12,12 @@ "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev", "--", "-p", "3100"], "port": 3100 + }, + { + "name": "prod-3100", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["start", "-p", "3100"], + "port": 3100 } ] } diff --git a/data/app.db b/data/app.db index 997a8f54c3b8b317689167e59dd0b2dd0104591f..e0822323841b142b62752e5411aaacb052b296d5 100644 GIT binary patch delta 89 zcmZozz|pXPV}dl}&WSS4j5`|>wk9wx;b&&yPTI~cz*xe?T)ZV?dYu&GL*}f?7u%Dh p85gneu<&~@@aOVR;@{5iv0Xrc@e4mh$O9<2oqy7H0RzSl_W|ZS8w&sc delta 89 zcmZozz|pXPV}dl}_K7mijN2O%wk9wx;b&&$^4-oZz*xe?oVQ}`^g1cVhs>#YvfGoS m85gneu<|Q2@aOVR;@{4%yj?(n@e4mh$O9+{7BXP`a327&!Wmxx diff --git a/package.json b/package.json index 2ad916d..9848947 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.112.4", + "@cds/city": "^1.1.0", "@libsql/client": "^0.17.4", "@react-pdf/renderer": "^4.5.1", "better-auth": "^1.6.23", @@ -20,6 +21,7 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.45.2", "iyzipay": "^2.0.69", + "lenis": "^1.3.25", "lucide-react": "^1.25.0", "marked": "^18.0.7", "motion": "^12.42.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26ed21b..e4313df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@anthropic-ai/sdk': specifier: ^0.112.4 version: 0.112.4(zod@4.4.3) + '@cds/city': + specifier: ^1.1.0 + version: 1.1.0 '@libsql/client': specifier: ^0.17.4 version: 0.17.4 @@ -35,6 +38,9 @@ importers: iyzipay: specifier: ^2.0.69 version: 2.0.69 + lenis: + specifier: ^1.3.25 + version: 1.3.25(react@19.2.4) lucide-react: specifier: ^1.25.0 version: 1.25.0(react@19.2.4) @@ -363,6 +369,9 @@ packages: '@better-fetch/fetch@1.3.1': resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@cds/city@1.1.0': + resolution: {integrity: sha512-S9K+Q39BGOghyLHmR0Wdcmu1i1noSUk8HcvMj+3IaohZw02WFd99aPTQDHJeseXrXZP3CNovaSlePI0R11NcFg==} + '@dotenvx/dotenvx@1.75.1': resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} hasBin: true @@ -3966,6 +3975,20 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} 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: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -5679,6 +5702,8 @@ snapshots: '@better-fetch/fetch@1.3.1': {} + '@cds/city@1.1.0': {} + '@dotenvx/dotenvx@1.75.1': dependencies: '@dotenvx/primitives': 0.8.0 @@ -9213,6 +9238,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lenis@1.3.25(react@19.2.4): + optionalDependencies: + react: 19.2.4 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 diff --git a/src/app/giris/giris-form.tsx b/src/app/giris/giris-form.tsx index 5ee6371..5714105 100644 --- a/src/app/giris/giris-form.tsx +++ b/src/app/giris/giris-form.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Loader2, Mail } from "lucide-react"; +import { Mail } from "lucide-react"; import { toast } from "sonner"; import { authClient } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; @@ -126,12 +126,8 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) { onClick={googleIleGiris} disabled={busy !== null} > - {busy === "google" ? ( - - ) : ( - - )} - Google ile devam et + + {busy === "google" ? "Yönlendiriliyorsun…" : "Google ile devam et"}
@@ -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" disabled={busy !== null} > - {busy === "email" ? ( - - ) : ( - - )} - Giriş bağlantısı gönder + + {busy === "email" ? "Gönderiliyor…" : "Giriş bağlantısı gönder"}
diff --git a/src/app/gizlilik/page.tsx b/src/app/gizlilik/page.tsx index e23dbc9..3326980 100644 --- a/src/app/gizlilik/page.tsx +++ b/src/app/gizlilik/page.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { Parallax } from "@/components/parallax"; +import { ScrollFlow } from "@/components/scroll-flow"; export const metadata: Metadata = { 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. export default function GizlilikPage() { return ( -
-

- Gizlilik ve Kişisel Verilerin Korunması -

-

Son güncelleme: 26 Temmuz 2026

+ +
+ +

+ Gizlilik ve Kişisel Verilerin Korunması +

+

+ Son güncelleme: 26 Temmuz 2026 +

+
-
-
-

- Hangi verileri işliyoruz? -

-

- KolayTercih; e-posta adresini (hesap oluşturma ve giriş için), YKS - başarı sıralamanı ve sihirbazda yaptığın seçimleri (tercih listeni - üretmek için), danışman sohbetine yazdığın mesajları (sorularını - yanıtlamak için) ve ödeme yaptığında sipariş kayıtlarını işler. - Kart bilgilerin bize hiç ulaşmaz; ödeme iyzico altyapısında - gerçekleşir. -

-
-
-

- Ne için kullanıyoruz? -

-

- Verilerin yalnızca tercih önerileri üretmek, hesabını ve kredi - bakiyeni yönetmek ve sana hizmetle ilgili e-posta (giriş bağlantısı, - ödeme bilgilendirmesi) göndermek için kullanılır. Verilerini - üçüncü taraflara satmayız, reklam amacıyla paylaşmayız. -

-
-
-

- Saklama ve silme -

-

- Hesabını sildirmek veya verilerinin bir kopyasını istemek için{" "} - - iletişim sayfasındaki - {" "} - adresten bize yazabilirsin; talepler 30 gün içinde sonuçlandırılır. -

-
-
-

- Çerezler -

-

- Yalnızca oturumunu açık tutmak için zorunlu çerezler kullanılır; - reklam veya izleme çerezi kullanılmaz. -

-
-
-
+ +
+

+ Hangi verileri işliyoruz? +

+

+ KolayTercih; e-posta adresini (hesap oluşturma ve giriş için), YKS + başarı sıralamanı ve sihirbazda yaptığın seçimleri (tercih listeni + üretmek için), danışman sohbetine yazdığın mesajları (sorularını + yanıtlamak için) ve ödeme yaptığında sipariş kayıtlarını işler. + Kart bilgilerin bize hiç ulaşmaz; ödeme iyzico altyapısında + gerçekleşir. +

+
+
+

+ Ne için kullanıyoruz? +

+

+ Verilerin yalnızca tercih önerileri üretmek, hesabını ve kredi + bakiyeni yönetmek ve sana hizmetle ilgili e-posta (giriş + bağlantısı, ödeme bilgilendirmesi) göndermek için kullanılır. + Verilerini üçüncü taraflara satmayız, reklam amacıyla paylaşmayız. +

+
+
+

+ Saklama ve silme +

+

+ Hesabını sildirmek veya verilerinin bir kopyasını istemek için{" "} + + iletişim sayfasındaki + {" "} + adresten bize yazabilirsin; talepler 30 gün içinde + sonuçlandırılır. +

+
+
+

+ Çerezler +

+

+ Yalnızca oturumunu açık tutmak için zorunlu çerezler kullanılır; + reklam veya izleme çerezi kullanılmaz. +

+
+
+
+ ); } diff --git a/src/app/globals.css b/src/app/globals.css index d569ac0..2b7197c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -7,9 +7,10 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-work-sans); + --font-sans: var(--font-clarity-city); --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-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -168,7 +169,22 @@ body { @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 { @apply font-sans; + scroll-behavior: smooth; + } + @media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } } } \ No newline at end of file diff --git a/src/app/iletisim/page.tsx b/src/app/iletisim/page.tsx index e4bb320..02c380a 100644 --- a/src/app/iletisim/page.tsx +++ b/src/app/iletisim/page.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import { Mail } from "lucide-react"; +import { Parallax } from "@/components/parallax"; +import { ScrollFlow } from "@/components/scroll-flow"; export const metadata: Metadata = { title: "İletişim — KolayTercih", @@ -7,24 +9,28 @@ export const metadata: Metadata = { export default function IletisimPage() { return ( -
-

İletişim

-

- Ödeme, iade, hesap silme ya da başka bir konuda yardıma mı ihtiyacın - var? Tercih döneminde mesajlara aynı gün dönmeye çalışıyoruz. -

- - - destek@kolaytercih.com - -

- Ödemenle ilgili yazıyorsan e-postana sipariş tarihini ve kullandığın - hesap adresini eklersen daha hızlı yardımcı olabiliriz. Kart bilgini - asla e-postayla paylaşma. -

-
+ +
+ +

İletişim

+
+

+ Ödeme, iade, hesap silme ya da başka bir konuda yardıma mı ihtiyacın + var? Tercih döneminde mesajlara aynı gün dönmeye çalışıyoruz. +

+ + + destek@kolaytercih.com + +

+ Ödemenle ilgili yazıyorsan e-postana sipariş tarihini ve kullandığın + hesap adresini eklersen daha hızlı yardımcı olabiliriz. Kart bilgini + asla e-postayla paylaşma. +

+
+
); } diff --git a/src/app/kosullar/page.tsx b/src/app/kosullar/page.tsx index 9cd3cfa..1d182bd 100644 --- a/src/app/kosullar/page.tsx +++ b/src/app/kosullar/page.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { Parallax } from "@/components/parallax"; +import { ScrollFlow } from "@/components/scroll-flow"; export const metadata: Metadata = { 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. export default function KosullarPage() { return ( -
-

- Kullanım ve Satış Koşulları -

-

Son güncelleme: 26 Temmuz 2026

+ +
+ +

+ Kullanım ve Satış Koşulları +

+

+ Son güncelleme: 26 Temmuz 2026 +

+
-
-
-

- Hizmetin kapsamı -

-

- KolayTercih, resmî YÖK Atlas verilerine dayanan bir karar destek - aracıdır. Öneriler bilgilendirme amaçlıdır; KolayTercih yerleşme - garantisi vermez ve ÖSYM ya da YÖK ile bağlantılı değildir. Tercih - listenin son hali ve başvuru sorumluluğu sana aittir. -

-
-
-

- Ücretlendirme -

-

- Temel program arama ücretsizdir. Tercih Dönemi Paketi tek seferlik - bir satın almadır; abonelik değildir, otomatik yenilenmez. Paket - içeriği ve güncel fiyat{" "} - - paket sayfasında - {" "} - yazar. -

-
-
-

- İade ve cayma -

-

- Paket, satın alır almaz ifasına başlanan dijital bir içeriktir. - Buna rağmen kişisel listen henüz üretilmemişse (paket hiç - kullanılmamışsa) satın almadan itibaren 14 gün içinde koşulsuz - iade talep edebilirsin. Teknik bir sorun nedeniyle hizmeti - alamadıysan, kullanımdan bağımsız olarak ücretin iade edilir. - İade talepleri için{" "} - - bize yaz - - ; talepler 14 gün içinde sonuçlandırılır ve iade, ödeme yaptığın - karta yapılır. -

-
-
-

- Hesap ve adil kullanım -

-

- Krediler hesabına tanımlıdır, kişiseldir ve üçüncü kişilere - devredilemez. Hizmeti kötüye kullanan (otomatik istek, içerik - kazıma vb.) hesaplar kapatılabilir. -

-
-
-
+ +
+

+ Hizmetin kapsamı +

+

+ KolayTercih, resmî YÖK Atlas verilerine dayanan bir karar destek + aracıdır. Öneriler bilgilendirme amaçlıdır; KolayTercih yerleşme + garantisi vermez ve ÖSYM ya da YÖK ile bağlantılı değildir. Tercih + listenin son hali ve başvuru sorumluluğu sana aittir. +

+
+
+

+ Ücretlendirme +

+

+ Temel program arama ücretsizdir. Tercih Dönemi Paketi tek seferlik + bir satın almadır; abonelik değildir, otomatik yenilenmez. Paket + içeriği ve güncel fiyat{" "} + + paket sayfasında + {" "} + yazar. +

+
+
+

+ İade ve cayma +

+

+ Paket, satın alır almaz ifasına başlanan dijital bir içeriktir. + Buna rağmen kişisel listen henüz üretilmemişse (paket hiç + kullanılmamışsa) satın almadan itibaren 14 gün içinde koşulsuz + iade talep edebilirsin. Teknik bir sorun nedeniyle hizmeti + alamadıysan, kullanımdan bağımsız olarak ücretin iade edilir. İade + talepleri için{" "} + + bize yaz + + ; talepler 14 gün içinde sonuçlandırılır ve iade, ödeme yaptığın + karta yapılır. +

+
+
+

+ Hesap ve adil kullanım +

+

+ Krediler hesabına tanımlıdır, kişiseldir ve üçüncü kişilere + devredilemez. Hizmeti kötüye kullanan (otomatik istek, içerik + kazıma vb.) hesaplar kapatılabilir. +

+
+
+
+ ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index be37141..6376c8f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ 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 { SiteHeader } from "@/components/site-header"; 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 "./globals.css"; -const outfit = Outfit({ - variable: "--font-outfit", +const calSans = Cal_Sans({ + variable: "--font-cal-sans", + weight: "400", subsets: ["latin", "latin-ext"], }); -const workSans = Work_Sans({ - variable: "--font-work-sans", - subsets: ["latin", "latin-ext"], +const clarityCity = localFont({ + variable: "--font-clarity-city", + 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({ @@ -23,6 +31,12 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); +const bricolage = Bricolage_Grotesque({ + variable: "--font-bricolage-grotesque", + subsets: ["latin", "latin-ext"], + axes: ["opsz"], +}); + export const metadata: Metadata = { title: "KolayTercih — Tercih Danışmanı", description: @@ -37,7 +51,7 @@ export default function RootLayout({ return ( {/* Site geneli progressive blur — üst ve alt */} diff --git a/src/app/listem/revizyon-kutusu.tsx b/src/app/listem/revizyon-kutusu.tsx index e76f789..f46ea23 100644 --- a/src/app/listem/revizyon-kutusu.tsx +++ b/src/app/listem/revizyon-kutusu.tsx @@ -6,7 +6,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; -import { Loader2, RefreshCcw } from "lucide-react"; +import { RefreshCcw } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { listeRevize } from "@/app/sonuc/actions"; @@ -80,11 +80,7 @@ export function RevizyonKutusu({ kalanHak }: { kalanHak: number }) { onClick={revizeEt} className="cursor-pointer" > - {pending ? ( - - ) : ( - - )} + {pending ? "Yeniden kuruluyor…" : "Revize et (3 kredi)"} diff --git a/src/app/listem/sohbet-client.tsx b/src/app/listem/sohbet-client.tsx index 3e10e13..e26193f 100644 --- a/src/app/listem/sohbet-client.tsx +++ b/src/app/listem/sohbet-client.tsx @@ -21,6 +21,7 @@ import { import { PromptSuggestion } from "@/components/ui/prompt-suggestion"; import { ScrollButton } from "@/components/ui/scroll-button"; import { DotsLoader } from "@/components/ui/loader"; +import { Skeleton } from "@/components/ui/skeleton"; interface Mesaj { id: string; @@ -174,7 +175,15 @@ export function SohbetClient({ ))} ) : ( - +
+ {[112, 148, 96, 132].map((w, i) => ( + + ))} +
)} ) : ( @@ -195,8 +204,10 @@ export function SohbetClient({ {m.content} ) : ( -
- +
+ + +
)} diff --git a/src/app/loading.tsx b/src/app/loading.tsx index 4e8ae6e..2c4f3d1 100644 --- a/src/app/loading.tsx +++ b/src/app/loading.tsx @@ -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() { return ( -
- -

Yükleniyor…

+
+ {/* Rozet + başlık + alt metin */} + + + + + + + {/* Form / aksiyon şeridi */} +
+ + +
+ + {/* Kart bloklar */} +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 80f7e5d..e7b1f86 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,6 +12,16 @@ import { import Image from "next/image"; import Link from "next/link"; 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 { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -156,261 +166,412 @@ function ComparisonCell({ value }: { value: boolean | string }) { export default function Home() { return ( -
-
- {/* Hero */} -
- - 2026 YKS tercih dönemi açık - -

- Dört yıllık geleceğini{" "} - 48 saatlik panikle seçme -

-

+

+ +
+ {/* Hero */} +
+
+ {/* Arka katman: harita scroll'un gerisinde kalır, derinlik verir */} + + {/* Harita hiçbir boyutta taşmaz: her zaman ekran genişliği + 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. */} + + +
+
+ + + 2026 YKS tercih dönemi açık + +

+ Dört yıllık geleceğini
+ + 48 saatlik panikle + {" "} + seçme +

+ {/*

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 liste, risk analizi, soru-cevap — onda bir fiyatına yapar. -

-
- -
-

- Temel arama her zaman ücretsiz · Kredi kartı gerekmez · Tam analiz - paketi tek seferlik {PAKET_FIYATI} TL -

-
- Yükseköğretim Kurulu - - - - YÖK Atlas'ın gerçek verileriyle - {" "} - güvenilir sonuçlar - -
-
- - {/* Problem */} -
-
-

- Tercih dönemi neden bu kadar stresli? -

-
- {problems.map((p) => ( - - -
- -
- - {p.title} - - - {p.description} - -
-
- ))} -
-
-
- - {/* How it works */} -
-
-

- Üç adımda listen hazır -

-
- {steps.map((s) => ( -
- - {s.step} - - -

- {s.title} -

-

- {s.description} -

-
- ))} -
- - {/* Veriye dayalı güven + garanti vaadine karşı dürüst konumlandırma */} -
-
- {[ - { 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) => ( -
-
{stat.label}
-
- {stat.value} -
-
- ))} -
- -
-
- -
-
-

- Dürüstlük notu -

-

- Yerleşme garantisi vermiyoruz — çünkü kimse veremez. -

-

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

-
+

*/} +
+
+

+ Temel arama her zaman ücretsiz · Kredi kartı gerekmez · Tam + analiz paketi tek seferlik {PAKET_FIYATI} TL +

+
+ Yükseköğretim Kurulu + + + + YÖK Atlas'ın gerçek verileriyle + {" "} + güvenilir sonuçlar + +
+ +
+ + {/* Hero → Problem geçişi: border yerine piksel ayraç */} +
+ + + +
+ + {/* Problem */} +
+
+ {/* Başlık gövdeden hafif hızlı akar — katman derinliği */} + + + Sorun +

+ Tercih dönemi neden bu kadar stresli? +

+

+ Her yıl iki milyondan fazla aday aynı üç duvara çarpıyor. +

+
+
+ + {problems.map((p, i) => ( + + + + +
+ +
+ + {p.title} + + + {p.description} + +
+
+
+ ))} +
-
-
+ - {/* Comparison */} -
-
-

- Robot mu, danışman mı, KolayTercih mi? -

-
- - - - Özellik - - Ücretsiz robotlar - - - İnsan danışman - - - KolayTercih - - - - - {comparison.map((row) => ( - - - {row.feature} - - - - - - - - - - - - ))} - -
+ {/* Problem → Nasıl çalışır geçişi */} +
+ + + +
+ + {/* How it works */} +
+
+ + + Nasıl çalışır? +

+ Üç adımda listen hazır +

+

+ Sıralamandan dengeli 24'lük listeye giden yol üç adım + sürer. +

+
+
+ + {steps.map((s, i) => ( + +
+ + {s.step} + + + +

+ {s.title} +

+

+ {s.description} +

+
+
+ ))} +
+ + {/* Veriye dayalı güven + garanti vaadine karşı dürüst konumlandırma */} + + +
+ {[ + { 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) => ( +
+
{stat.label}
+
+ {stat.value} +
+
+ ))} +
+ +
+
+ +
+
+

+ Dürüstlük notu +

+

+ Yerleşme garantisi vermiyoruz — çünkü kimse veremez. +

+

+ 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. +

+
+
+
+
-
-
+ - {/* FAQ */} -
-
-

- Sık sorulan sorular -

- - {faqs.map((faq) => ( - - - {faq.question} - - - {faq.answer} - - - ))} - + {/* Nasıl çalışır → Karşılaştırma geçişi */} +
+ + +
-
- {/* Final CTA */} -
-
- -

- Tercih listen, pişmanlık listesi olmasın -

-

- Sıralamanı gir, yapay zekâ danışmanın veriyle konuşsun. Tercih - dönemi bitmeden yerini al. + {/* Comparison */} +

+
+ + + Karşılaştır +

+ Robot mu, danışman mı, KolayTercih mi? +

+

+ Aynı işi kim, neyle, kaça yapıyor — yan yana bak. +

+
+
+ + + + + + Özellik + + Ücretsiz robotlar + + + İnsan danışman + + + KolayTercih + + + + + {comparison.map((row) => ( + + + {row.feature} + + + + + + + + + + + + ))} + +
+
+
+
+
+ + {/* Karşılaştırma → SSS geçişi */} +
+ + + +
+ + {/* FAQ */} +
+
+ + + SSS +

+ Sık sorulan sorular +

+
+
+ + + + {faqs.map((faq) => ( + + + {faq.question} + + + {faq.answer} + + + ))} + + + +
+
+ + {/* Final CTA — tam genişlik bant yerine yüzen, köşeleri yumuşak panel */} +
+ + +
+ {/* 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 */} +
+ + + +
+
+ +

+ Tercih listen, pişmanlık listesi olmasın +

+

+ Sıralamanı gir, yapay zekâ danışmanın veriyle konuşsun. + Tercih dönemi bitmeden yerini al. +

+ +
+
+
+
+
+
+ +
+
+

+ © 2026 KolayTercih. Veriler resmî YÖK Atlas kaynağından derlenir; + KolayTercih, ÖSYM veya YÖK ile bağlantılı değildir.

- + + Gizlilik & KVKK + + + Kullanım & iade koşulları + + + İletişim + +
- -
- -
-
-

- © 2026 KolayTercih. Veriler resmî YÖK Atlas kaynağından derlenir; - KolayTercih, ÖSYM veya YÖK ile bağlantılı değildir. -

-

- KolayTercih bir karar destek aracıdır, yerleşme garantisi vermez. - Tercih listenizin son hali ve başvuru sorumluluğu size aittir. -

- -
-
+ +
); } diff --git a/src/app/paket/page.tsx b/src/app/paket/page.tsx index 8519190..690113c 100644 --- a/src/app/paket/page.tsx +++ b/src/app/paket/page.tsx @@ -13,6 +13,8 @@ import { getCurrentUser } from "@/lib/session"; import { URUNLER, DENEME_KREDISI } from "@/lib/credits"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Parallax } from "@/components/parallax"; +import { ScrollFlow } from "@/components/scroll-flow"; import { SatinAlForm } from "./satin-al-form"; export const metadata: Metadata = { @@ -51,134 +53,143 @@ export default async function PaketPage({ const odemeHatasi = hata === "token" || hata === "siparis"; return ( -
-
- {odemeHatasi ? ( -
+ +
+ {odemeHatasi ? ( +
+ Ö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. +
+ ) : null} + + + Tek seferlik ödeme · Abonelik yok + +

+ İnsan danışmanın işini,{" "} + onda bir fiyatına +

+

+ 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. +

+
+ + - Ö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. -
- ) : null} -
- - Tek seferlik ödeme · Abonelik yok - -

- İnsan danışmanın işini,{" "} - onda bir fiyatına -

-

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

-
- -
- {/* Ana paket */} -
-
-

- Tercih Dönemi Paketi -

-

- {tl(URUNLER.paket.amountKurus)} TL -

-
-
    - {OZELLIKLER.map((o) => ( -
  • - - {o.text} -
  • - ))} -
-
- {u ? ( - u.hasPaket ? ( -
- - Paketin aktif —{" "} - - listeni oluştur - -
+ {/* Ana paket */} +
+
+

+ Tercih Dönemi Paketi +

+

+ {tl(URUNLER.paket.amountKurus)} TL +

+
+
    + {OZELLIKLER.map((o) => ( +
  • + + {o.text} +
  • + ))} +
+
+ {u ? ( + u.hasPaket ? ( +
+ + Paketin aktif —{" "} + + listeni oluştur + +
+ ) : ( + + ) ) : ( + + )} +
+

+ iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "} + + İade koşulları + +

+
+ + {/* Top-up */} +
+
+ +

Kredi bitti mi?

+
+

+ +{URUNLER.topup.credits} soru hakkı,{" "} + {tl(URUNLER.topup.amountKurus)} TL. +

+
+ {u ? ( - ) - ) : ( - - )} -
-

- iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "} - - İade koşulları - -

-
+ ) : ( + + )} +
+
+ - {/* Top-up */} -
-
- -

Kredi bitti mi?

-
-

- +{URUNLER.topup.credits} soru hakkı,{" "} - {tl(URUNLER.topup.amountKurus)} TL. -

-
- {u ? ( - - ) : ( - - )} -
-
-
- -

- 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?{" "} - - Bize yaz - - . -

-
+

+ 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?{" "} + + Bize yaz + + . +

+ +
); } diff --git a/src/app/paket/satin-al-form.tsx b/src/app/paket/satin-al-form.tsx index 5e490d6..141fa7d 100644 --- a/src/app/paket/satin-al-form.tsx +++ b/src/app/paket/satin-al-form.tsx @@ -1,7 +1,6 @@ "use client"; import { useActionState, useEffect } from "react"; -import { Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { baslatOdeme, type OdemeBaslatDurum } from "./actions"; @@ -37,9 +36,6 @@ export function SatinAlForm({ } variant={vurgulu ? "default" : "outline"} > - {pending ? ( - - ) : null} {pending ? "Ödeme sayfasına yönlendiriliyorsun…" : label} diff --git a/src/app/sonuc/liste-paneli.tsx b/src/app/sonuc/liste-paneli.tsx index 6881216..08d1d54 100644 --- a/src/app/sonuc/liste-paneli.tsx +++ b/src/app/sonuc/liste-paneli.tsx @@ -2,8 +2,9 @@ import { useMemo, useEffect, useRef, useState } from "react"; 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 { Skeleton } from "@/components/ui/skeleton"; import { SohbetPopup } from "./sohbet-popup"; import { RaporListesi } from "@/components/rapor-listesi"; import { TercihHaritasi, type HaritaPin } from "@/components/tercih-haritasi"; @@ -305,16 +306,28 @@ function UretimGovdesi() { ref={govdeRef} tabIndex={-1} 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" > - -

Listen hazırlanıyor

-

- {URETIM_MESAJLARI[mesajIdx]} -

-

- Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma. -

+
+

Listen hazırlanıyor

+

+ {URETIM_MESAJLARI[mesajIdx]} +

+

+ Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma. +

+
+ + {/* Gelmekte olan tercih listesinin iskeleti */} +
+
+ + +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
); } @@ -370,11 +383,7 @@ function RevizyonKutusu({ }} className="cursor-pointer" > - {pending ? ( - - ) : ( - - )} + {pending ? "Yeniden kuruluyor…" : "Revize et (3 kredi)"} diff --git a/src/components/dev/dev-panel.tsx b/src/components/dev/dev-panel.tsx index 0908530..9be480b 100644 --- a/src/components/dev/dev-panel.tsx +++ b/src/components/dev/dev-panel.tsx @@ -12,7 +12,7 @@ import { useCallback, useEffect, useState } from "react"; 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 { toast } from "sonner"; import { authClient } from "@/lib/auth-client"; @@ -184,8 +184,8 @@ export function DevPanel() { Dev süperadmin {mesgul ? ( - ) : null} diff --git a/src/components/hero-form.tsx b/src/components/hero-form.tsx index 370bfdf..da467e8 100644 --- a/src/components/hero-form.tsx +++ b/src/components/hero-form.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; -import { ArrowRight, Loader2 } from "lucide-react"; +import { ArrowRight } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -123,12 +123,16 @@ export function HeroForm() { siralamaDegisti(e.target.value)} aria-invalid={hata ? true : 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" /> diff --git a/src/components/landing/reveal.tsx b/src/components/landing/reveal.tsx new file mode 100644 index 0000000..a5ace21 --- /dev/null +++ b/src/components/landing/reveal.tsx @@ -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 ( + + {children} + + ); +} diff --git a/src/components/parallax.tsx b/src/components/parallax.tsx new file mode 100644 index 0000000..a40da1b --- /dev/null +++ b/src/components/parallax.tsx @@ -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(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 ( + + {children} + + ); +} diff --git a/src/components/pixel-decor.tsx b/src/components/pixel-decor.tsx new file mode 100644 index 0000000..72d3852 --- /dev/null +++ b/src/components/pixel-decor.tsx @@ -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 ( + + ); + } + const op = opacityFor(x, y); + return ( + + ); +} + +/** + * 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(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 ( + + ); +} + +/** + * 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(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 ( + + ); +} + +/** Küçük sabit süslerde (mark/korner) data-px öznitelikli rect. */ +function fixedPixel(x: number, y: number, opacity: number) { + return ( + + ); +} + +/** Eyebrow rozetlerinde kullanılan 2×2'lik minik piksel imzası. */ +export function PixelMark({ className }: { className?: string }) { + const svgRef = useRef(null); + usePixelHeat(svgRef, { radius: 24 }); + + return ( + + ); +} + +/** Kart köşelerine iliştirilen sessiz piksel kümesi. */ +export function PixelCorner({ className }: { className?: string }) { + const svgRef = useRef(null); + usePixelHeat(svgRef, { radius: 28 }); + + return ( + + ); +} + +/** Bölüm başlıklarının üstünde duran piksel imzalı eyebrow rozeti. */ +export function SectionEyebrow({ children }: { children: ReactNode }) { + return ( + + + {children} + + ); +} diff --git a/src/components/scroll-flow.tsx b/src/components/scroll-flow.tsx new file mode 100644 index 0000000..56630da --- /dev/null +++ b/src/components/scroll-flow.tsx @@ -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
{children}
; + } + + return ( + + {/* willChange: bükülme 0'a döndüğünde dev katman GPU'dan düşüp + bir sonraki tekerlekte yeniden rasterize edilmesin */} + + {children} + + + ); +} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index e21e39d..890198d 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -14,12 +14,14 @@ export function SiteHeader() {
- - + + Kolay + + Tercih + - KolayTercih