Add il bazlı filtreleme ve Türkiye harita bileşeni
Sonuç sayfasına bölge haritası ile il seçimi eklendi; searchByRank artık il filtresini destekliyor. Paket adı tercihai olarak güncellendi. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "ddd",
|
"name": "tercihai",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ddd",
|
"name": "tercihai",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.11.1",
|
"better-sqlite3": "^12.11.1",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"turkey-map-react": "^2.0.5",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -11877,6 +11878,16 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/turkey-map-react": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/turkey-map-react/-/turkey-map-react-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-0EwmyQcbVXrSThj88vEUPcVUKV9FItrOrWPbATCRLQb/0JRUUnSEziUv32JrPSQV2rhHIgO/Hxb7s5mV0og5fQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tw-animate-css": {
|
"node_modules/tw-animate-css": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "ddd",
|
"name": "tercihai",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"turkey-map-react": "^2.0.5",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
MapPin,
|
||||||
Minus,
|
Minus,
|
||||||
Rocket,
|
Rocket,
|
||||||
Scale,
|
Scale,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
PUAN_TURLERI,
|
PUAN_TURLERI,
|
||||||
@@ -16,6 +18,7 @@ import {
|
|||||||
type Program,
|
type Program,
|
||||||
type PuanTuruKey,
|
type PuanTuruKey,
|
||||||
} from "@/lib/db";
|
} from "@/lib/db";
|
||||||
|
import { BolgeHaritasi, type IlOzeti } from "@/components/bolge-haritasi";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -135,7 +138,7 @@ function ProgramTable({ programs }: { programs: Program[] }) {
|
|||||||
export default async function SonucPage({
|
export default async function SonucPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ sira?: string; tur?: string }>;
|
searchParams: Promise<{ sira?: string; tur?: string; il?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const sira = Number.parseInt(params.sira ?? "", 10);
|
const sira = Number.parseInt(params.sira ?? "", 10);
|
||||||
@@ -143,6 +146,7 @@ export default async function SonucPage({
|
|||||||
params.tur && params.tur in PUAN_TURLERI
|
params.tur && params.tur in PUAN_TURLERI
|
||||||
? (params.tur as PuanTuruKey)
|
? (params.tur as PuanTuruKey)
|
||||||
: "say";
|
: "say";
|
||||||
|
const seciliIl = params.il?.trim() || null;
|
||||||
|
|
||||||
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
||||||
return (
|
return (
|
||||||
@@ -163,7 +167,26 @@ export default async function SonucPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = searchByRank(sira, turKey);
|
const results = searchByRank(sira, turKey, {
|
||||||
|
il: seciliIl ?? undefined,
|
||||||
|
});
|
||||||
|
// Harita her zaman filtresiz dağılımı gösterir ki il değiştirilebilsin
|
||||||
|
const haritaSonuclari = seciliIl ? searchByRank(sira, turKey) : results;
|
||||||
|
|
||||||
|
// Harita için il bazında özet (81 il haritada; KKTC/yurtdışı ayrıca listelenir)
|
||||||
|
const iller: Record<string, IlOzeti> = {};
|
||||||
|
const dilimler = [
|
||||||
|
["hayal", haritaSonuclari.hayal],
|
||||||
|
["dengeli", haritaSonuclari.dengeli],
|
||||||
|
["garanti", haritaSonuclari.garanti],
|
||||||
|
] as const;
|
||||||
|
for (const [dilim, programs] of dilimler) {
|
||||||
|
for (const p of programs) {
|
||||||
|
if (!p.il) continue;
|
||||||
|
iller[p.il] ??= { hayal: 0, dengeli: 0, garanti: 0 };
|
||||||
|
iller[p.il][dilim]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{
|
||||||
@@ -238,13 +261,58 @@ export default async function SonucPage({
|
|||||||
variant={key === turKey ? "default" : "outline"}
|
variant={key === turKey ? "default" : "outline"}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<Link href={`/sonuc?sira=${sira}&tur=${key}`}>
|
<Link
|
||||||
|
href={`/sonuc?sira=${sira}&tur=${key}${
|
||||||
|
seciliIl ? `&il=${encodeURIComponent(seciliIl)}` : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{TUR_LABELS[key]}
|
{TUR_LABELS[key]}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{seciliIl ? (
|
||||||
|
<div className="mt-4 flex flex-wrap items-center gap-3 rounded-xl border border-primary/30 bg-primary/5 px-4 py-3">
|
||||||
|
<MapPin className="size-4 text-primary" aria-hidden />
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Yalnızca <span className="font-bold">{seciliIl}</span>{" "}
|
||||||
|
programları gösteriliyor
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Link href={`/sonuc?sira=${sira}&tur=${turKey}`}>
|
||||||
|
<X className="size-3.5" aria-hidden />
|
||||||
|
Filtreyi kaldır
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Bölge haritası */}
|
||||||
|
<section aria-labelledby="baslik-harita" className="mt-10">
|
||||||
|
<h2 id="baslik-harita" className="font-heading text-2xl font-bold">
|
||||||
|
Nerede okuyacaksın?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-slate-600">
|
||||||
|
Aşağıdaki önerilerin illere dağılımı. Koyu renk, o ilde daha fazla
|
||||||
|
öneri olduğunu gösterir. Bir ile tıklayarak listeyi o ile
|
||||||
|
daraltabilir, tekrar tıklayarak filtreyi kaldırabilirsin.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 rounded-2xl border border-slate-200 bg-white p-4 sm:p-6">
|
||||||
|
<BolgeHaritasi
|
||||||
|
iller={iller}
|
||||||
|
sira={sira}
|
||||||
|
turKey={turKey}
|
||||||
|
seciliIl={seciliIl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="mt-10 space-y-12">
|
<div className="mt-10 space-y-12">
|
||||||
{sections.map((s) => (
|
{sections.map((s) => (
|
||||||
<section key={s.key} aria-labelledby={`baslik-${s.key}`}>
|
<section key={s.key} aria-labelledby={`baslik-${s.key}`}>
|
||||||
|
|||||||
182
src/components/bolge-haritasi.tsx
Normal file
182
src/components/bolge-haritasi.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cities } from "turkey-map-react/lib/data";
|
||||||
|
|
||||||
|
export type IlOzeti = {
|
||||||
|
hayal: number;
|
||||||
|
dengeli: number;
|
||||||
|
garanti: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/** DB'deki il adıyla (büyük harf Türkçe) program sayıları */
|
||||||
|
iller: Record<string, IlOzeti>;
|
||||||
|
sira: number;
|
||||||
|
turKey: string;
|
||||||
|
seciliIl: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Harita adını DB formatına çevirir: "Hakkâri" → "HAKKARİ"
|
||||||
|
function normalizeIl(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLocaleUpperCase("tr-TR")
|
||||||
|
.replace(/Â/g, "A")
|
||||||
|
.replace(/Î/g, "İ")
|
||||||
|
.replace(/Û/g, "U");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toplam(o: IlOzeti): number {
|
||||||
|
return o.hayal + o.dengeli + o.garanti;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Secim = {
|
||||||
|
name: string;
|
||||||
|
ozet: IlOzeti;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BolgeHaritasi({ iller, sira, turKey, seciliIl }: Props) {
|
||||||
|
const [secim, setSecim] = useState<Secim | null>(null);
|
||||||
|
const kapsayici = useRef<HTMLDivElement>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function filtrele(ilKey: string) {
|
||||||
|
const url =
|
||||||
|
ilKey === seciliIl
|
||||||
|
? `/sonuc?sira=${sira}&tur=${turKey}`
|
||||||
|
: `/sonuc?sira=${sira}&tur=${turKey}&il=${encodeURIComponent(ilKey)}`;
|
||||||
|
router.push(url, { scroll: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxToplam = Math.max(1, ...Object.values(iller).map((o) => toplam(o)));
|
||||||
|
|
||||||
|
// Haritada karşılığı olmayan yerler (KKTC, yurtdışı kampüsler)
|
||||||
|
const haritaIlleri = new Set(cities.map((c) => normalizeIl(c.name)));
|
||||||
|
const haritaDisi = Object.entries(iller).filter(
|
||||||
|
([il]) => !haritaIlleri.has(il)
|
||||||
|
);
|
||||||
|
|
||||||
|
function sec(el: SVGPathElement, name: string, ozet: IlOzeti) {
|
||||||
|
const kutu = kapsayici.current?.getBoundingClientRect();
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
if (!kutu) return;
|
||||||
|
setSecim({
|
||||||
|
name,
|
||||||
|
ozet,
|
||||||
|
x: r.x - kutu.x + r.width / 2,
|
||||||
|
y: r.y - kutu.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={kapsayici} className="relative">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1050 585"
|
||||||
|
className="h-auto w-full"
|
||||||
|
role="img"
|
||||||
|
aria-label="Önerilen programların illere dağılımı haritası"
|
||||||
|
onClick={(e) => {
|
||||||
|
if ((e.target as Element).tagName !== "path") setSecim(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Seçili il en son çizilir ki turuncu konturu komşu iller örtmesin */}
|
||||||
|
{[...cities]
|
||||||
|
.sort((a, b) =>
|
||||||
|
(normalizeIl(a.name) === seciliIl ? 1 : 0) -
|
||||||
|
(normalizeIl(b.name) === seciliIl ? 1 : 0)
|
||||||
|
)
|
||||||
|
.map((city) => {
|
||||||
|
const key = normalizeIl(city.name);
|
||||||
|
const ozet = iller[key];
|
||||||
|
const yogunluk = ozet ? toplam(ozet) / maxToplam : 0;
|
||||||
|
const secili = key === seciliIl;
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
key={city.id}
|
||||||
|
d={city.path}
|
||||||
|
className={
|
||||||
|
ozet
|
||||||
|
? "cursor-pointer transition-opacity duration-200 hover:opacity-80"
|
||||||
|
: "fill-slate-200 stroke-white"
|
||||||
|
}
|
||||||
|
style={
|
||||||
|
ozet
|
||||||
|
? {
|
||||||
|
fill: `oklch(0.623 0.188 259.8 / ${0.3 + 0.7 * yogunluk})`,
|
||||||
|
stroke: secili ? "oklch(0.705 0.213 47.6)" : "white",
|
||||||
|
strokeWidth: secili ? 2.5 : 1,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
strokeWidth={1}
|
||||||
|
onClick={
|
||||||
|
ozet ? () => filtrele(key) : () => setSecim(null)
|
||||||
|
}
|
||||||
|
onMouseEnter={
|
||||||
|
ozet
|
||||||
|
? (e) => sec(e.currentTarget, city.name, ozet)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onMouseLeave={ozet ? () => setSecim(null) : undefined}
|
||||||
|
>
|
||||||
|
<title>
|
||||||
|
{ozet
|
||||||
|
? `${city.name}: ${toplam(ozet)} program — filtrelemek için tıkla`
|
||||||
|
: city.name}
|
||||||
|
</title>
|
||||||
|
</path>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{secim ? (
|
||||||
|
<div
|
||||||
|
data-testid="harita-tooltip"
|
||||||
|
className="pointer-events-none absolute z-10 -translate-x-1/2 -translate-y-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm shadow-md"
|
||||||
|
style={{ left: secim.x, top: secim.y - 6 }}
|
||||||
|
>
|
||||||
|
<p className="font-heading font-semibold">{secim.name}</p>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
{toplam(secim.ozet)} program
|
||||||
|
<span className="block text-xs">
|
||||||
|
{secim.ozet.hayal} hayal · {secim.ozet.dengeli} dengeli ·{" "}
|
||||||
|
{secim.ozet.garanti} garanti
|
||||||
|
</span>
|
||||||
|
<span className="block text-xs font-medium text-primary">
|
||||||
|
{normalizeIl(secim.name) === seciliIl
|
||||||
|
? "Filtreyi kaldırmak için tıkla"
|
||||||
|
: "Filtrelemek için tıkla"}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-end gap-2 text-xs text-slate-500">
|
||||||
|
<span>az</span>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="h-2 w-24 rounded-full"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(to right, oklch(0.623 0.188 259.8 / 0.3), oklch(0.623 0.188 259.8))",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>çok program</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{haritaDisi.length > 0 ? (
|
||||||
|
<p className="mt-3 text-xs text-slate-500">
|
||||||
|
Harita dışı:{" "}
|
||||||
|
{haritaDisi
|
||||||
|
.map(
|
||||||
|
([il, ozet]) => `${il.toLocaleLowerCase("tr-TR")} (${toplam(ozet)})`
|
||||||
|
)
|
||||||
|
.join(", ")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -65,14 +65,17 @@ const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)";
|
|||||||
export function searchByRank(
|
export function searchByRank(
|
||||||
sira: number,
|
sira: number,
|
||||||
turKey: PuanTuruKey,
|
turKey: PuanTuruKey,
|
||||||
limitPerBucket = 12
|
opts: { il?: string; limitPerBucket?: number } = {}
|
||||||
): RankResults {
|
): RankResults {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const tur = PUAN_TURLERI[turKey];
|
const tur = PUAN_TURLERI[turKey];
|
||||||
const onlisans = turKey === "tyt" ? 1 : 0;
|
const onlisans = turKey === "tyt" ? 1 : 0;
|
||||||
|
const limitPerBucket = opts.limitPerBucket ?? 12;
|
||||||
|
const ilFiltre = opts.il ? "AND il = ?" : "";
|
||||||
|
const ilArgs = opts.il ? [opts.il] : [];
|
||||||
|
|
||||||
const base = `FROM programs
|
const base = `FROM programs
|
||||||
WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL
|
WHERE tur = ? AND onlisans = ? ${ilFiltre} AND ${EFEKTIF_SIRA} IS NOT NULL
|
||||||
AND ${EFEKTIF_SIRA} BETWEEN ? AND ?`;
|
AND ${EFEKTIF_SIRA} BETWEEN ? AND ?`;
|
||||||
|
|
||||||
// hayal: kullanıcıdan daha iyi taban sıralaması (sınıra en yakın olanlar önce)
|
// hayal: kullanıcıdan daha iyi taban sıralaması (sınıra en yakın olanlar önce)
|
||||||
@@ -80,12 +83,26 @@ export function searchByRank(
|
|||||||
.prepare(
|
.prepare(
|
||||||
`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} DESC LIMIT ?`
|
`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} DESC LIMIT ?`
|
||||||
)
|
)
|
||||||
.all(tur, onlisans, Math.round(sira * 0.5), sira - 1, limitPerBucket);
|
.all(
|
||||||
|
tur,
|
||||||
|
onlisans,
|
||||||
|
...ilArgs,
|
||||||
|
Math.round(sira * 0.5),
|
||||||
|
sira - 1,
|
||||||
|
limitPerBucket
|
||||||
|
);
|
||||||
|
|
||||||
// dengeli: tabanı kullanıcının sıralaması civarında
|
// dengeli: tabanı kullanıcının sıralaması civarında
|
||||||
const dengeli = db
|
const dengeli = db
|
||||||
.prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`)
|
.prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`)
|
||||||
.all(tur, onlisans, sira, Math.round(sira * 1.4), limitPerBucket);
|
.all(
|
||||||
|
tur,
|
||||||
|
onlisans,
|
||||||
|
...ilArgs,
|
||||||
|
sira,
|
||||||
|
Math.round(sira * 1.4),
|
||||||
|
limitPerBucket
|
||||||
|
);
|
||||||
|
|
||||||
// garanti: tabanı belirgin şekilde altında
|
// garanti: tabanı belirgin şekilde altında
|
||||||
const garanti = db
|
const garanti = db
|
||||||
@@ -93,6 +110,7 @@ export function searchByRank(
|
|||||||
.all(
|
.all(
|
||||||
tur,
|
tur,
|
||||||
onlisans,
|
onlisans,
|
||||||
|
...ilArgs,
|
||||||
Math.round(sira * 1.4) + 1,
|
Math.round(sira * 1.4) + 1,
|
||||||
Math.round(sira * 3),
|
Math.round(sira * 3),
|
||||||
limitPerBucket
|
limitPerBucket
|
||||||
|
|||||||
Reference in New Issue
Block a user