Some checks failed
Deploy / deploy (push) Has been cancelled
- pixel-decor: PixelField/PixelDivider piksel üretimi useMemo([seed]) — 504 iterasyon + ~150 element çekmece reorder'ının her karesinde yeniden üretiliyordu - rapor-listesi: acikSira effect'i önceki-prop desenine döndü (çift render ve eslint-disable kalktı), Set lazy init, tekIl useMemo - program-tablosu: 3 dilimin spread'i useMemo - typing-animation: displayedText state yerine türetme; kaynak reset'i render sırasında — tick başına state yazımı ve effect bağımlılığı azaldı - site-top-banner: saniyelik geri sayım tiki startTransition'da - program-liste-verileri: sparkline 6 dizi geçişi tek döngüde - secimlerim-paneli: SecimSatiri profili prop'tan alır + memo (24 ayrı store aboneliği kalktı) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
"use client";
|
||
|
||
import { startTransition, useEffect, useState } from "react";
|
||
import NumberFlow, { NumberFlowGroup } from "@number-flow/react";
|
||
import { CalendarDays, Clock3 } from "lucide-react";
|
||
|
||
const TERCIH_BASLANGICI = new Date("2026-07-29T00:00:00+03:00").getTime();
|
||
const TERCIH_BITISI = new Date("2026-08-10T23:59:59+03:00").getTime();
|
||
|
||
type GeriSayim = {
|
||
asama: "baslamadi" | "devam-ediyor" | "sona-erdi";
|
||
gun: number;
|
||
saat: number;
|
||
dakika: number;
|
||
saniye: number;
|
||
};
|
||
|
||
function geriSayimiHesapla(simdi: number): GeriSayim {
|
||
const asama =
|
||
simdi < TERCIH_BASLANGICI
|
||
? "baslamadi"
|
||
: simdi <= TERCIH_BITISI
|
||
? "devam-ediyor"
|
||
: "sona-erdi";
|
||
|
||
const hedef =
|
||
asama === "baslamadi" ? TERCIH_BASLANGICI : TERCIH_BITISI;
|
||
const kalan = Math.max(0, hedef - simdi);
|
||
|
||
return {
|
||
asama,
|
||
gun: Math.floor(kalan / 86_400_000),
|
||
saat: Math.floor((kalan % 86_400_000) / 3_600_000),
|
||
dakika: Math.floor((kalan % 3_600_000) / 60_000),
|
||
saniye: Math.floor((kalan % 60_000) / 1_000),
|
||
};
|
||
}
|
||
|
||
const BIRIMLER = [
|
||
{ anahtar: "gun", etiket: "gün" },
|
||
{ anahtar: "saat", etiket: "sa" },
|
||
{ anahtar: "dakika", etiket: "dk" },
|
||
{ anahtar: "saniye", etiket: "sn" },
|
||
] as const;
|
||
|
||
export function SiteTopBanner() {
|
||
const [geriSayim, setGeriSayim] = useState<GeriSayim | null>(null);
|
||
|
||
useEffect(() => {
|
||
// Saniyelik tik acil değil — layout'ta her sayfada monte; urgent şeritten çık
|
||
const guncelle = () =>
|
||
startTransition(() => setGeriSayim(geriSayimiHesapla(Date.now())));
|
||
|
||
guncelle();
|
||
const zamanlayici = window.setInterval(guncelle, 1_000);
|
||
|
||
return () => window.clearInterval(zamanlayici);
|
||
}, []);
|
||
|
||
const asamaMetni =
|
||
geriSayim?.asama === "baslamadi"
|
||
? "Başlamasına"
|
||
: geriSayim?.asama === "sona-erdi"
|
||
? "Tercih dönemi sona erdi"
|
||
: "Sona ermesine";
|
||
|
||
return (
|
||
<section
|
||
aria-label="2026 YKS tercih dönemi"
|
||
className="relative z-50 border-b border-primary-foreground/15 bg-primary text-primary-foreground print:hidden"
|
||
>
|
||
<div className="mx-auto flex min-h-12 max-w-6xl flex-wrap items-center justify-center gap-x-5 gap-y-1.5 px-4 py-2 sm:justify-between">
|
||
<div className="flex min-w-0 items-center gap-2.5">
|
||
<CalendarDays className="size-4 shrink-0 opacity-80" aria-hidden />
|
||
<p className="truncate text-sm font-semibold tracking-[-0.01em]">
|
||
<span className="hidden sm:inline">2026 YKS tercih dönemi · </span>
|
||
<time dateTime="2026-07-29">29 Temmuz</time>
|
||
<span aria-hidden> – </span>
|
||
<time dateTime="2026-08-10">10 Ağustos 2026</time>
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2.5" role="timer">
|
||
<div className="flex items-center gap-1.5 text-xs font-medium opacity-85">
|
||
<Clock3 className="size-3.5" aria-hidden />
|
||
<span>{asamaMetni}</span>
|
||
</div>
|
||
|
||
{geriSayim?.asama !== "sona-erdi" ? (
|
||
<dl
|
||
className="flex items-baseline gap-1 font-mono text-xs tabular-nums"
|
||
aria-label={
|
||
geriSayim
|
||
? `${geriSayim.gun} gün, ${geriSayim.saat} saat, ${geriSayim.dakika} dakika, ${geriSayim.saniye} saniye`
|
||
: "Geri sayım yükleniyor"
|
||
}
|
||
>
|
||
<NumberFlowGroup>
|
||
{BIRIMLER.map(({ anahtar, etiket }) => (
|
||
<div className="flex items-baseline gap-0.5" key={anahtar}>
|
||
<dt className="sr-only">{etiket}</dt>
|
||
<dd className="font-semibold">
|
||
{geriSayim ? (
|
||
<NumberFlow
|
||
value={geriSayim[anahtar]}
|
||
trend={-1}
|
||
format={{
|
||
minimumIntegerDigits: 2,
|
||
useGrouping: false,
|
||
}}
|
||
transformTiming={{
|
||
duration: 280,
|
||
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
|
||
}}
|
||
spinTiming={{
|
||
duration: 280,
|
||
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
|
||
}}
|
||
opacityTiming={{
|
||
duration: 180,
|
||
easing: "ease-out",
|
||
}}
|
||
willChange={anahtar === "saniye"}
|
||
className="inline-block min-w-[2ch] text-right"
|
||
aria-hidden
|
||
/>
|
||
) : (
|
||
"--"
|
||
)}
|
||
<span className="ml-0.5 text-[10px] font-medium opacity-70">
|
||
{etiket}
|
||
</span>
|
||
</dd>
|
||
</div>
|
||
))}
|
||
</NumberFlowGroup>
|
||
</dl>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|