perf: deterministik dekor memo'ları ve küçük render süpürmeleri (Faz 7)
Some checks failed
Deploy / deploy (push) Has been cancelled
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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, type ReactNode } from "react";
|
import { useMemo, useRef, type ReactNode } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { usePixelHeat } from "@/components/use-pixel-heat";
|
import { usePixelHeat } from "@/components/use-pixel-heat";
|
||||||
|
|
||||||
@@ -88,35 +88,41 @@ export function PixelField({
|
|||||||
|
|
||||||
const COLS = 42;
|
const COLS = 42;
|
||||||
const ROWS = 12;
|
const ROWS = 12;
|
||||||
const cx = (COLS - 1) / 2;
|
|
||||||
const cy = (ROWS - 1) / 2;
|
|
||||||
const maxD = Math.sqrt(cx * cx + cy * cy);
|
|
||||||
|
|
||||||
const pixels: ReactNode[] = [];
|
// Çıktı seed'e göre deterministik; 504 iterasyon + ~150 element üretimini
|
||||||
let accentIdx = 0;
|
// her render'da (ör. çekmece reorder'ında) tekrarlama (rendering-hoist-jsx)
|
||||||
|
const pixels = useMemo(() => {
|
||||||
|
const cx = (COLS - 1) / 2;
|
||||||
|
const cy = (ROWS - 1) / 2;
|
||||||
|
const maxD = Math.sqrt(cx * cx + cy * cy);
|
||||||
|
|
||||||
// Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon
|
const sonuc: ReactNode[] = [];
|
||||||
// (genişliğin %22–78 bandı) tüm yükseklik boyunca tamamen boş kalır
|
let accentIdx = 0;
|
||||||
const BAND_MIN = COLS * 0.22;
|
|
||||||
const BAND_MAX = COLS * 0.78;
|
|
||||||
|
|
||||||
for (let y = 0; y < ROWS; y++) {
|
// Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon
|
||||||
for (let x = 0; x < COLS; x++) {
|
// (genişliğin %22–78 bandı) tüm yükseklik boyunca tamamen boş kalır
|
||||||
if (x > BAND_MIN && x < BAND_MAX) continue;
|
const BAND_MIN = COLS * 0.22;
|
||||||
// Kenarlara doğru artan doluluk
|
const BAND_MAX = COLS * 0.78;
|
||||||
const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) / maxD;
|
|
||||||
const density = 0.1 + 0.42 * d * d;
|
for (let y = 0; y < ROWS; y++) {
|
||||||
if (hash(x, y, seed) > density) continue;
|
for (let x = 0; x < COLS; x++) {
|
||||||
const accent = hash(x, y, seed + 99) > 0.96;
|
if (x > BAND_MIN && x < BAND_MAX) continue;
|
||||||
pixels.push(
|
// Kenarlara doğru artan doluluk
|
||||||
pixelRect(x, y, {
|
const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) / maxD;
|
||||||
key: `${x}-${y}`,
|
const density = 0.1 + 0.42 * d * d;
|
||||||
accent,
|
if (hash(x, y, seed) > density) continue;
|
||||||
accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0,
|
const accent = hash(x, y, seed + 99) > 0.96;
|
||||||
}),
|
sonuc.push(
|
||||||
);
|
pixelRect(x, y, {
|
||||||
|
key: `${x}-${y}`,
|
||||||
|
accent,
|
||||||
|
accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return sonuc;
|
||||||
|
}, [seed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -149,28 +155,32 @@ export function PixelDivider({
|
|||||||
|
|
||||||
const COLS = 12;
|
const COLS = 12;
|
||||||
const ROWS = 2;
|
const ROWS = 2;
|
||||||
const cx = (COLS - 1) / 2;
|
|
||||||
|
|
||||||
const pixels: ReactNode[] = [];
|
// Deterministik çıktı — render başına yeniden üretme (PixelField ile aynı)
|
||||||
let accentPlaced = false;
|
const pixels = useMemo(() => {
|
||||||
|
const cx = (COLS - 1) / 2;
|
||||||
|
const sonuc: ReactNode[] = [];
|
||||||
|
let accentPlaced = false;
|
||||||
|
|
||||||
for (let y = 0; y < ROWS; y++) {
|
for (let y = 0; y < ROWS; y++) {
|
||||||
for (let x = 0; x < COLS; x++) {
|
for (let x = 0; x < COLS; x++) {
|
||||||
// Az sayıda, iri piksel: ortada yoğun, kenara doğru seyrelen küçük küme
|
// Az sayıda, iri piksel: ortada yoğun, kenara doğru seyrelen küçük küme
|
||||||
const dx = Math.abs(x - cx) / cx;
|
const dx = Math.abs(x - cx) / cx;
|
||||||
const rowFalloff = y === 0 ? 0.9 : 0.35;
|
const rowFalloff = y === 0 ? 0.9 : 0.35;
|
||||||
const density = (1 - dx) ** 1.1 * rowFalloff;
|
const density = (1 - dx) ** 1.1 * rowFalloff;
|
||||||
if (hash(x, y, seed) > density) continue;
|
if (hash(x, y, seed) > density) continue;
|
||||||
const accent = !accentPlaced && hash(x, y, seed + 99) > 0.8;
|
const accent = !accentPlaced && hash(x, y, seed + 99) > 0.8;
|
||||||
if (accent) accentPlaced = true;
|
if (accent) accentPlaced = true;
|
||||||
pixels.push(
|
sonuc.push(
|
||||||
pixelRect(x, y, {
|
pixelRect(x, y, {
|
||||||
key: `${x}-${y}`,
|
key: `${x}-${y}`,
|
||||||
accent,
|
accent,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return sonuc;
|
||||||
|
}, [seed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -26,22 +26,32 @@ export function programEtkinSira(program: SiraSerisi): number | null {
|
|||||||
|
|
||||||
/** 2021–2025 taban sıralamasını sonuç listesindeki kompakt çizgiyle gösterir. */
|
/** 2021–2025 taban sıralamasını sonuç listesindeki kompakt çizgiyle gösterir. */
|
||||||
export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) {
|
export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) {
|
||||||
const yillar = [
|
// Satır başına render edilen bileşen: filter/map/min/max/map/map'in 6 ayrı
|
||||||
|
// geçişi tek döngüde toplanır (js-combine-iterations, js-min-max-loop)
|
||||||
|
const yillar: [number, number][] = [];
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
const ozetParcalari: string[] = [];
|
||||||
|
for (const [yil, sira] of [
|
||||||
[2021, program.sira2021],
|
[2021, program.sira2021],
|
||||||
[2022, program.sira2022],
|
[2022, program.sira2022],
|
||||||
[2023, program.sira2023],
|
[2023, program.sira2023],
|
||||||
[2024, program.sira2024],
|
[2024, program.sira2024],
|
||||||
[2025, program.sira2025],
|
[2025, program.sira2025],
|
||||||
].filter((yil): yil is [number, number] => yil[1] != null);
|
] as const) {
|
||||||
|
if (sira == null) continue;
|
||||||
|
yillar.push([yil, sira]);
|
||||||
|
if (sira < min) min = sira;
|
||||||
|
if (sira > max) max = sira;
|
||||||
|
ozetParcalari.push(`${yil}: ~${sira.toLocaleString("tr-TR")}.`);
|
||||||
|
}
|
||||||
|
|
||||||
if (yillar.length < 2) return <span className="text-slate-400">—</span>;
|
if (yillar.length < 2) return <span className="text-slate-400">—</span>;
|
||||||
|
|
||||||
const siralar = yillar.map(([, sira]) => sira);
|
|
||||||
const min = Math.min(...siralar);
|
|
||||||
const max = Math.max(...siralar);
|
|
||||||
const genislik = 56;
|
const genislik = 56;
|
||||||
const yukseklik = 18;
|
const yukseklik = 18;
|
||||||
const bosluk = 2;
|
const bosluk = 2;
|
||||||
|
// min/max'a bağımlı olduğu için ikinci kısa döngü
|
||||||
const noktalar = yillar.map(([yil, sira], index) => {
|
const noktalar = yillar.map(([yil, sira], index) => {
|
||||||
const x =
|
const x =
|
||||||
bosluk +
|
bosluk +
|
||||||
@@ -54,9 +64,7 @@ export function ProgramSiraGecmisi({ program }: { program: SiraSerisi }) {
|
|||||||
return { x, y, yil, sira };
|
return { x, y, yil, sira };
|
||||||
});
|
});
|
||||||
const son = noktalar[noktalar.length - 1];
|
const son = noktalar[noktalar.length - 1];
|
||||||
const ozet = yillar
|
const ozet = ozetParcalari.join("\n");
|
||||||
.map(([yil, sira]) => `${yil}: ~${sira.toLocaleString("tr-TR")}.`)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { startTransition, useEffect, useState } from "react";
|
||||||
import NumberFlow, { NumberFlowGroup } from "@number-flow/react";
|
import NumberFlow, { NumberFlowGroup } from "@number-flow/react";
|
||||||
import { CalendarDays, Clock3 } from "lucide-react";
|
import { CalendarDays, Clock3 } from "lucide-react";
|
||||||
|
|
||||||
@@ -47,7 +47,9 @@ export function SiteTopBanner() {
|
|||||||
const [geriSayim, setGeriSayim] = useState<GeriSayim | null>(null);
|
const [geriSayim, setGeriSayim] = useState<GeriSayim | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const guncelle = () => setGeriSayim(geriSayimiHesapla(Date.now()));
|
// Saniyelik tik acil değil — layout'ta her sayfada monte; urgent şeritten çık
|
||||||
|
const guncelle = () =>
|
||||||
|
startTransition(() => setGeriSayim(geriSayimiHesapla(Date.now())));
|
||||||
|
|
||||||
guncelle();
|
guncelle();
|
||||||
const zamanlayici = window.setInterval(guncelle, 1_000);
|
const zamanlayici = window.setInterval(guncelle, 1_000);
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ export function TypingAnimation({
|
|||||||
Component
|
Component
|
||||||
] as TypingAnimationMotionComponent
|
] as TypingAnimationMotionComponent
|
||||||
|
|
||||||
const [displayedText, setDisplayedText] = useState<string>("")
|
|
||||||
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
||||||
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
||||||
const [phase, setPhase] = useState<"typing" | "pause" | "deleting">("typing")
|
const [phase, setPhase] = useState<"typing" | "pause" | "deleting">("typing")
|
||||||
@@ -109,21 +108,32 @@ export function TypingAnimation({
|
|||||||
[words, children]
|
[words, children]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Kaynak metin değişince animasyon baştan başlar — bilinçli reset
|
// Kaynak metin değişince animasyon baştan başlar — önceki-prop deseniyle
|
||||||
useEffect(() => {
|
// render sırasında (effect'te setState çift render yaratıyordu)
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
const [oncekiKaynak, setOncekiKaynak] = useState(animationSourceKey)
|
||||||
setDisplayedText("")
|
if (animationSourceKey !== oncekiKaynak) {
|
||||||
|
setOncekiKaynak(animationSourceKey)
|
||||||
setCurrentWordIndex(0)
|
setCurrentWordIndex(0)
|
||||||
setCurrentCharIndex(0)
|
setCurrentCharIndex(0)
|
||||||
setPhase("typing")
|
setPhase("typing")
|
||||||
}, [animationSourceKey])
|
}
|
||||||
|
|
||||||
|
// Görünen metin state değil türetme: tick başına ekstra state yazımı ve
|
||||||
|
// grapheme dizisi allocation'ı kalkar (rerender-derived-state-no-effect)
|
||||||
|
const displayedText = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from(wordsToAnimate[currentWordIndex] || "")
|
||||||
|
.slice(0, currentCharIndex)
|
||||||
|
.join(""),
|
||||||
|
[wordsToAnimate, currentWordIndex, currentCharIndex]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
if (!azMotion && shouldStart && wordsToAnimate.length > 0) {
|
if (!azMotion && shouldStart && wordsToAnimate.length > 0) {
|
||||||
const timeoutDelay =
|
const timeoutDelay =
|
||||||
delay > 0 && displayedText === ""
|
delay > 0 && currentCharIndex === 0
|
||||||
? delay
|
? delay
|
||||||
: phase === "typing"
|
: phase === "typing"
|
||||||
? typingSpeed
|
? typingSpeed
|
||||||
@@ -138,9 +148,6 @@ export function TypingAnimation({
|
|||||||
switch (phase) {
|
switch (phase) {
|
||||||
case "typing":
|
case "typing":
|
||||||
if (currentCharIndex < graphemes.length) {
|
if (currentCharIndex < graphemes.length) {
|
||||||
setDisplayedText(
|
|
||||||
graphemes.slice(0, currentCharIndex + 1).join("")
|
|
||||||
)
|
|
||||||
setCurrentCharIndex(currentCharIndex + 1)
|
setCurrentCharIndex(currentCharIndex + 1)
|
||||||
} else {
|
} else {
|
||||||
if (hasMultipleWords || loop) {
|
if (hasMultipleWords || loop) {
|
||||||
@@ -159,9 +166,6 @@ export function TypingAnimation({
|
|||||||
|
|
||||||
case "deleting":
|
case "deleting":
|
||||||
if (currentCharIndex > 0) {
|
if (currentCharIndex > 0) {
|
||||||
setDisplayedText(
|
|
||||||
graphemes.slice(0, currentCharIndex - 1).join("")
|
|
||||||
)
|
|
||||||
setCurrentCharIndex(currentCharIndex - 1)
|
setCurrentCharIndex(currentCharIndex - 1)
|
||||||
} else {
|
} else {
|
||||||
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
|
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
|
||||||
@@ -184,7 +188,6 @@ export function TypingAnimation({
|
|||||||
phase,
|
phase,
|
||||||
currentCharIndex,
|
currentCharIndex,
|
||||||
currentWordIndex,
|
currentWordIndex,
|
||||||
displayedText,
|
|
||||||
wordsToAnimate,
|
wordsToAnimate,
|
||||||
hasMultipleWords,
|
hasMultipleWords,
|
||||||
loop,
|
loop,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// sayfalanır ("daha fazla göster" /api/programlar'dan akıtır). Her satırın "+"
|
// sayfalanır ("daha fazla göster" /api/programlar'dan akıtır). Her satırın "+"
|
||||||
// butonu programı manuel 24'lük listeye ekler (bkz. manuel-liste/store).
|
// butonu programı manuel 24'lük listeye ekler (bkz. manuel-liste/store).
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -119,6 +119,13 @@ export function ProgramTablosu({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Görünmeyen sekmeler dahil 3 dilimin spread'i her render'da tekrarlanmasın
|
||||||
|
const tumSatirlar = useMemo(() => {
|
||||||
|
const r = {} as Record<DilimKey, ProgramListeSatiri[]>;
|
||||||
|
for (const d of DILIMLER) r[d.key] = [...sonuclar[d.key], ...ekstra[d.key]];
|
||||||
|
return r;
|
||||||
|
}, [sonuclar, ekstra]);
|
||||||
|
|
||||||
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
|
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
|
||||||
setYuklenen(dilim);
|
setYuklenen(dilim);
|
||||||
try {
|
try {
|
||||||
@@ -174,7 +181,7 @@ export function ProgramTablosu({
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{DILIMLER.map((d) => {
|
{DILIMLER.map((d) => {
|
||||||
const satirlar = [...sonuclar[d.key], ...ekstra[d.key]];
|
const satirlar = tumSatirlar[d.key];
|
||||||
const toplam = sonuclar.toplam[d.key];
|
const toplam = sonuclar.toplam[d.key];
|
||||||
return (
|
return (
|
||||||
<TabsContent key={d.key} value={d.key}>
|
<TabsContent key={d.key} value={d.key}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
sonucaGitIste,
|
sonucaGitIste,
|
||||||
useTercihProfili,
|
useTercihProfili,
|
||||||
} from "../hooks/use-tercih-profili";
|
} from "../hooks/use-tercih-profili";
|
||||||
|
import type { TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
|
|
||||||
const RISK_STIL: Record<RiskSeviyesi, { nokta: string; kenar: string }> = {
|
const RISK_STIL: Record<RiskSeviyesi, { nokta: string; kenar: string }> = {
|
||||||
guvenli: { nokta: "bg-emerald-500", kenar: "border-l-emerald-500" },
|
guvenli: { nokta: "bg-emerald-500", kenar: "border-l-emerald-500" },
|
||||||
@@ -176,7 +178,7 @@ export function SecimlerimPaneli() {
|
|||||||
|
|
||||||
<ol className="flex flex-col gap-3">
|
<ol className="flex flex-col gap-3">
|
||||||
{liste.map((t, i) => (
|
{liste.map((t, i) => (
|
||||||
<SecimSatiri key={t.id} tercih={t} sira={i + 1} />
|
<SecimSatiri key={t.id} tercih={t} sira={i + 1} profil={profil} />
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
@@ -196,14 +198,17 @@ function Istatistik({ etiket, deger }: { etiket: string; deger: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SecimSatiri({
|
// Profil parent'tan prop olarak gelir (parent zaten abone) + memo: 24 satırın
|
||||||
|
// her biri ayrı store aboneliği kurup her profil olayında uyanıyordu.
|
||||||
|
const SecimSatiri = memo(function SecimSatiri({
|
||||||
tercih,
|
tercih,
|
||||||
sira,
|
sira,
|
||||||
|
profil,
|
||||||
}: {
|
}: {
|
||||||
tercih: ManuelTercih;
|
tercih: ManuelTercih;
|
||||||
sira: number;
|
sira: number;
|
||||||
|
profil: TercihProfili | null;
|
||||||
}) {
|
}) {
|
||||||
const profil = useTercihProfili();
|
|
||||||
const degerlendirme = profil
|
const degerlendirme = profil
|
||||||
? tercihDegerlendir(
|
? tercihDegerlendir(
|
||||||
{
|
{
|
||||||
@@ -302,4 +307,4 @@ function SecimSatiri({
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// açık; Yapay Zeka katmanı kilitliBaslangic sonrasında ücretlidir (metin
|
// açık; Yapay Zeka katmanı kilitliBaslangic sonrasında ücretlidir (metin
|
||||||
// sunucuda zaten maskelidir).
|
// sunucuda zaten maskelidir).
|
||||||
|
|
||||||
import { Fragment, useEffect, useRef, useState } from "react";
|
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ChevronDown, Lock, MapPin, Rocket, Scale, ShieldCheck } from "lucide-react";
|
import { ChevronDown, Lock, MapPin, Rocket, Scale, ShieldCheck } from "lucide-react";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
@@ -121,14 +121,20 @@ export function RaporListesi({
|
|||||||
/** Detaydaki "Haritada gör" — il DB biçiminde */
|
/** Detaydaki "Haritada gör" — il DB biçiminde */
|
||||||
onHaritadaGor?: (il: string | null, sira: number) => void;
|
onHaritadaGor?: (il: string | null, sira: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const [acik, setAcik] = useState<Set<number>>(new Set());
|
// Lazy init: boş Set her render'da boşuna allocate edilmesin
|
||||||
|
const [acik, setAcik] = useState<Set<number>>(() => new Set());
|
||||||
const satirRefs = useRef(new Map<number, HTMLTableRowElement>());
|
const satirRefs = useRef(new Map<number, HTMLTableRowElement>());
|
||||||
|
|
||||||
// Haritadan gelen seçim: satırı aç ve görünür alana kaydır
|
// Haritadan gelen seçim: satırın açılması render sırasındaki önceki-prop
|
||||||
|
// deseniyle (effect'te setState çift render yaratıyordu); yalnızca DOM işi
|
||||||
|
// olan scrollIntoView effect'te kalır.
|
||||||
|
const [oncekiAcikSira, setOncekiAcikSira] = useState(acikSira);
|
||||||
|
if (acikSira !== oncekiAcikSira) {
|
||||||
|
setOncekiAcikSira(acikSira);
|
||||||
|
if (acikSira != null) setAcik((mevcut) => new Set(mevcut).add(acikSira));
|
||||||
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (acikSira == null) return;
|
if (acikSira == null) return;
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setAcik((mevcut) => new Set(mevcut).add(acikSira));
|
|
||||||
satirRefs.current
|
satirRefs.current
|
||||||
.get(acikSira)
|
.get(acikSira)
|
||||||
?.scrollIntoView({ behavior: "smooth", block: "center" });
|
?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
@@ -145,12 +151,14 @@ export function RaporListesi({
|
|||||||
|
|
||||||
const kilitIdx = kilitliBaslangic ?? rapor.tercihler.length;
|
const kilitIdx = kilitliBaslangic ?? rapor.tercihler.length;
|
||||||
|
|
||||||
const iller = new Set(
|
const tekIl = useMemo(() => {
|
||||||
rapor.tercihler
|
const iller = new Set<string>();
|
||||||
.map((t) => rapor.programlar[t.programId]?.il)
|
for (const t of rapor.tercihler) {
|
||||||
.filter((il): il is string => Boolean(il)),
|
const il = rapor.programlar[t.programId]?.il;
|
||||||
);
|
if (il) iller.add(il);
|
||||||
const tekIl = iller.size === 1 ? [...iller][0] : null;
|
}
|
||||||
|
return iller.size === 1 ? [...iller][0] : null;
|
||||||
|
}, [rapor]);
|
||||||
|
|
||||||
const satir = (t: RaporSonuc["tercihler"][number], index: number) => {
|
const satir = (t: RaporSonuc["tercihler"][number], index: number) => {
|
||||||
const p = rapor.programlar[t.programId];
|
const p = rapor.programlar[t.programId];
|
||||||
|
|||||||
Reference in New Issue
Block a user