All checks were successful
Deploy / deploy (push) Successful in 9m29s
- Modified redirects in next.config.ts to change the destination for "/sohbet" to "/listem". - Added new dependencies in package.json: marked, motion, react-markdown, remark-breaks, remark-gfm, shiki, and use-stick-to-bottom. - Updated pnpm-lock.yaml to reflect the new package versions and dependencies. - Removed obsolete stack files for Flutter, Nuxt, Nuxt.js, React Native, and Svelte from the UI/UX Pro Max skill data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
283 lines
9.3 KiB
TypeScript
283 lines
9.3 KiB
TypeScript
"use client";
|
||
|
||
// Sihirbazın 3 adımlık seçim gövdesi. Hem ana sayfadaki hero modalında hem de
|
||
// /sonuc'taki sihirbaz modalında kullanılır; seçim state'ini kendi tutar,
|
||
// tamamlanınca onTamamla(secimler) çağırır.
|
||
|
||
import { useState } from "react";
|
||
import { ArrowLeft, ArrowRight, Sparkles, X } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
import { cities } from "turkey-map-react/lib/data";
|
||
import { Button } from "@/components/ui/button";
|
||
import { IlSecimHaritasi } from "@/components/il-secim-haritasi";
|
||
import { normalizeIlAdi } from "@/lib/harita";
|
||
import type { SihirbazFacetleri } from "@/lib/db";
|
||
import {
|
||
ONCELIKLER,
|
||
UNIVERSITE_TIPI_ETIKET,
|
||
type SihirbazSecimleri,
|
||
type UniversiteTipi,
|
||
} from "@/lib/sihirbaz";
|
||
|
||
export function Cip({
|
||
secili,
|
||
onClick,
|
||
children,
|
||
}: {
|
||
secili: boolean;
|
||
onClick: () => void;
|
||
children: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onClick}
|
||
aria-pressed={secili}
|
||
className={
|
||
secili
|
||
? "cursor-pointer rounded-full border border-primary bg-primary px-3.5 py-2 text-sm font-medium text-white transition-colors duration-200"
|
||
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3.5 py-2 text-sm text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
||
}
|
||
>
|
||
{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
// Haritada karşılığı olan iller (KKTC/yurtdışı kampüsler çip olarak kalır)
|
||
const HARITA_ILLERI = new Set(cities.map((c) => normalizeIlAdi(c.name)));
|
||
|
||
export function SihirbazAdimlar({
|
||
facetler,
|
||
baslangicSecimler,
|
||
sonButonEtiketi,
|
||
onTamamla,
|
||
}: {
|
||
facetler: SihirbazFacetleri;
|
||
baslangicSecimler?: SihirbazSecimleri | null;
|
||
sonButonEtiketi: string;
|
||
onTamamla: (secimler: SihirbazSecimleri) => void;
|
||
}) {
|
||
const [adim, setAdim] = useState(0);
|
||
const [kategoriler, setKategoriler] = useState<string[]>(
|
||
baslangicSecimler?.kategoriler ?? [],
|
||
);
|
||
const [iller, setIller] = useState<string[]>(baslangicSecimler?.iller ?? []);
|
||
const [universiteTipi, setUniversiteTipi] = useState<UniversiteTipi>(
|
||
baslangicSecimler?.universiteTipi ?? "farketmez",
|
||
);
|
||
const [oncelikler, setOncelikler] = useState<string[]>(
|
||
baslangicSecimler?.oncelikler ?? [],
|
||
);
|
||
|
||
function listeDegistir(
|
||
liste: string[],
|
||
setListe: (v: string[]) => void,
|
||
deger: string,
|
||
maks?: number,
|
||
) {
|
||
if (liste.includes(deger)) {
|
||
setListe(liste.filter((x) => x !== deger));
|
||
} else {
|
||
if (maks && liste.length >= maks) {
|
||
toast.info(`En fazla ${maks} seçebilirsin.`);
|
||
return;
|
||
}
|
||
setListe([...liste, deger]);
|
||
}
|
||
}
|
||
|
||
const adimGecerli = adim === 0 ? kategoriler.length > 0 : true;
|
||
|
||
return (
|
||
<div className="flex flex-col gap-5">
|
||
<div className="flex items-center gap-1.5">
|
||
{[0, 1, 2].map((i) => (
|
||
<span
|
||
key={i}
|
||
className={
|
||
i === adim
|
||
? "h-1.5 w-6 rounded-full bg-primary transition-all"
|
||
: "h-1.5 w-1.5 rounded-full bg-slate-200 transition-all"
|
||
}
|
||
/>
|
||
))}
|
||
<span className="ml-2 text-xs font-medium text-slate-400">
|
||
Adım {adim + 1}/3
|
||
</span>
|
||
</div>
|
||
|
||
{adim === 0 ? (
|
||
<div>
|
||
<h3 className="font-heading text-lg font-bold">
|
||
Hangi alanlara ilgi duyuyorsun?
|
||
</h3>
|
||
<p className="mt-1 text-sm text-slate-500">
|
||
Sıralamanla ulaşabileceğin alanlar. En az bir tane seç.
|
||
</p>
|
||
<div className="mt-4 flex flex-wrap gap-2">
|
||
{facetler.kategoriler.map((k) => (
|
||
<Cip
|
||
key={k.ad}
|
||
secili={kategoriler.includes(k.ad)}
|
||
onClick={() => listeDegistir(kategoriler, setKategoriler, k.ad)}
|
||
>
|
||
{k.ad}
|
||
<span className="ml-1.5 text-xs opacity-70">{k.adet}</span>
|
||
</Cip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : adim === 1 ? (
|
||
<div>
|
||
<h3 className="font-heading text-lg font-bold">
|
||
Nerede okumak istersin?
|
||
</h3>
|
||
<p className="mt-1 text-sm text-slate-500">
|
||
Haritadan dokunarak seç (en fazla 5 il). İstersen boş bırak, fark
|
||
etmez.
|
||
</p>
|
||
<div className="mt-4">
|
||
<IlSecimHaritasi
|
||
iller={facetler.iller.filter((i) => HARITA_ILLERI.has(i.il))}
|
||
secili={iller}
|
||
onToggle={(il) => listeDegistir(iller, setIller, il, 5)}
|
||
/>
|
||
</div>
|
||
{iller.length > 0 ? (
|
||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||
<span className="text-xs font-semibold text-slate-400">
|
||
Seçtiklerin:
|
||
</span>
|
||
{iller.map((il) => (
|
||
<button
|
||
key={il}
|
||
type="button"
|
||
onClick={() => listeDegistir(iller, setIller, il)}
|
||
className="inline-flex cursor-pointer items-center gap-1 rounded-full bg-orange-500 px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200 hover:bg-orange-600"
|
||
>
|
||
{il.toLocaleLowerCase("tr-TR")}
|
||
<X className="size-3" aria-hidden />
|
||
</button>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
{facetler.iller.some((i) => !HARITA_ILLERI.has(i.il)) ? (
|
||
<div className="mt-3">
|
||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
||
Harita dışı (KKTC vb.)
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{facetler.iller
|
||
.filter((i) => !HARITA_ILLERI.has(i.il))
|
||
.map((i) => (
|
||
<Cip
|
||
key={i.il}
|
||
secili={iller.includes(i.il)}
|
||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||
>
|
||
{i.il.toLocaleLowerCase("tr-TR")}
|
||
<span className="ml-1.5 text-xs opacity-70">
|
||
{i.adet}
|
||
</span>
|
||
</Cip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-5">
|
||
<div>
|
||
<h3 className="font-heading text-lg font-bold">
|
||
Üniversite tipi & öncelikler
|
||
</h3>
|
||
<p className="mt-1 text-sm text-slate-500">
|
||
Bunlar listenin dengesini belirler.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
||
Üniversite tipi
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{facetler.uniturler.map((u) => (
|
||
<Cip
|
||
key={u.grup}
|
||
secili={universiteTipi === u.grup}
|
||
onClick={() =>
|
||
setUniversiteTipi(
|
||
universiteTipi === u.grup ? "farketmez" : u.grup,
|
||
)
|
||
}
|
||
>
|
||
{UNIVERSITE_TIPI_ETIKET[u.grup]}
|
||
<span className="ml-1.5 text-xs opacity-70">{u.adet}</span>
|
||
</Cip>
|
||
))}
|
||
<Cip
|
||
secili={universiteTipi === "farketmez"}
|
||
onClick={() => setUniversiteTipi("farketmez")}
|
||
>
|
||
Fark etmez
|
||
</Cip>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
||
Senin için önemli olan
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{ONCELIKLER.map((o) => (
|
||
<Cip
|
||
key={o}
|
||
secili={oncelikler.includes(o)}
|
||
onClick={() => listeDegistir(oncelikler, setOncelikler, o)}
|
||
>
|
||
{o}
|
||
</Cip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex items-center justify-between border-t border-slate-100 pt-4">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
onClick={() => setAdim((a) => a - 1)}
|
||
disabled={adim === 0}
|
||
className="cursor-pointer"
|
||
>
|
||
<ArrowLeft className="size-4" aria-hidden />
|
||
Geri
|
||
</Button>
|
||
{adim < 2 ? (
|
||
<Button
|
||
type="button"
|
||
onClick={() => setAdim((a) => a + 1)}
|
||
disabled={!adimGecerli}
|
||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||
>
|
||
Devam
|
||
<ArrowRight className="size-4" aria-hidden />
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
type="button"
|
||
onClick={() =>
|
||
onTamamla({ kategoriler, iller, universiteTipi, oncelikler })
|
||
}
|
||
disabled={kategoriler.length === 0}
|
||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||
>
|
||
<Sparkles className="size-4" aria-hidden />
|
||
{sonButonEtiketi}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|