v2 #10

Merged
bilal merged 34 commits from v2 into main 2026-10-11 01:38:50 +03:00
10 changed files with 428 additions and 9 deletions
Showing only changes of commit 8e2790ec6f - Show all commits

View File

@@ -133,6 +133,22 @@ Seçici `src/components/tema-secici.tsx` (footer'da), sağlayıcı `src/componen
(`next-themes`, `<html class="dark">`). Renklerin tek kaynağı `src/app/globals.css` içindeki (`next-themes`, `<html class="dark">`). Renklerin tek kaynağı `src/app/globals.css` içindeki
`.dark` bloğu. `.dark` bloğu.
**Geçiş ve kısayol (7 Eki 2026; son hâli 9 Eki):** tema değişimi anlık değildir; yeni
tema header'daki logodan **yumuşak bir halka** gibi açılır: kenarı keskin değil, geniş ve
tüylü (ışık yayılır gibi), kısa ve hafif: 600 ms. **Geçiş bitince** logonun kareleri
yeni temanın biçimine dönüşür: koyuya geçişte şeftali kare hilale, mavi kare hilalin iki ucunun
ortasındaki yıldıza (turuncu söner); açığa geçişte turuncu kare güneşe, şeftali kare halesine. Kısa bir an durur, yeniden
logoya toplanır (`animasyonlu-mark.tsx` `sekilOynat`, 1,7 sn; yalnız header'daki logo; hover döngüsünden
önceliklidir, biçim bitince imleç üzerindeyse döngü kaldığı yerden değil yerinden sürer). Tema
seçicinin ikonları aynı biçimlerin durağan hâlidir (ayrıntı ve kurallar §4.34) (`tema-ikonu.tsx`: güneş, ay, Sistem için
logonun üç karesi); burada lucide ikonu kullanılmaz. View Transitions + radyal degrade
maskesi: `src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket azaltılmışsa düz
değişir. Denenip bırakılanlar: logo biçimli maske ("logo içinde logo"), düz crossfade, piksel
ızgarası (sert bulundu). Kural: bu geçişte sert kenar ve ikinci bir şekil yok.
`D` tuşu açık ↔ koyu geçişi yapar (`src/components/tema-kisayolu.tsx`); ipucu seçicinin
altında `Kbd` ile durur, başka yere kopyalanmaz. Header'daki Beta rozeti `/surum-notlari`na
bağlantıdır.
**Nasıl çalışır:** bileşenlere `dark:` yazılmaz; palet token'ları çevrilir. **Nasıl çalışır:** bileşenlere `dark:` yazılmaz; palet token'ları çevrilir.
| Açık temadaki sınıf | Koyu temada ne olur | | Açık temadaki sınıf | Koyu temada ne olur |
@@ -1276,6 +1292,8 @@ Hareket kuralları (Apple, `motion/react`):
- Kaydırmaya bağlı tetik `IntersectionObserver` ile (kaydırma dinleyicisi değil). - Kaydırmaya bağlı tetik `IntersectionObserver` ile (kaydırma dinleyicisi değil).
- "Hareketi azalt": kayma/yay yok, yalnız opaklık (`useReducedMotion`, - "Hareketi azalt": kayma/yay yok, yalnız opaklık (`useReducedMotion`,
`motion-reduce:transition-opacity`). `motion-reduce:transition-opacity`).
- Logo kareleri bir durumu anlatmak için kısa bir an başka biçime dönüşebilir (§4.34); sayfa
geçişiyle üst üste binmez, sırayla oynar.
## 7. Yap / Yapma ## 7. Yap / Yapma
@@ -1293,6 +1311,7 @@ Hareket kuralları (Apple, `motion/react`):
| Gösterge kareleri masonry (kaymış sütun, farklı boy) | Hizalı eşit ızgara; 8 px'lik "neredeyse hizalı" kaymalar | | Gösterge kareleri masonry (kaymış sütun, farklı boy) | Hizalı eşit ızgara; 8 px'lik "neredeyse hizalı" kaymalar |
| Liste satırında ad sarar, sayı alt satıra iner | `truncate` ile yarım ad; 32 px satır butonu | | Liste satırında ad sarar, sayı alt satıra iner | `truncate` ile yarım ad; 32 px satır butonu |
| Hareket: `Reveal`, piksel ısısı, `kt-unblur`, sekmesiz yay; `animate-ping` yalnızca "şu anda canlı" noktasında | Her elemanda `fade-up`, `whileHover scale 1.05`, `animate-bounce` | | Hareket: `Reveal`, piksel ısısı, `kt-unblur`, sekmesiz yay; `animate-ping` yalnızca "şu anda canlı" noktasında | Her elemanda `fade-up`, `whileHover scale 1.05`, `animate-bounce` |
| Tema ikonu ve logo dönüşümü: logonun üç karesinden güneş / ay (§4.34) | Tema seçicide lucide `Sun`/`Moon`; logoyu döngüde sürekli dönüştürmek |
| Başlık `font-heading`, vurgu `font-bricolage` | Inter/Roboto/serif, tek kelimelik BÜYÜK HARF etiket, 10–11 px yazı | | Başlık `font-heading`, vurgu `font-bricolage` | Inter/Roboto/serif, tek kelimelik BÜYÜK HARF etiket, 10–11 px yazı |
| Risk rengi + metin etiketi; tür ayrımı ikonla | Renk tek başına, "yerleşirsin" gibi vaat cümlesi | | Risk rengi + metin etiketi; tür ayrımı ikonla | Renk tek başına, "yerleşirsin" gibi vaat cümlesi |
| Üç kart yalnızca üç gerçek öğe varsa | Hero + üç kart iskeleti refleksi | | Üç kart yalnızca üç gerçek öğe varsa | Hero + üç kart iskeleti refleksi |

View File

@@ -43,6 +43,7 @@ const staticEntries: MetadataRoute.Sitemap = [
{ url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" }, { url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" },
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" }, { url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
{ url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" }, { url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" },
{ url: `${SITE_URL}/surum-notlari`, priority: 0.3, changeFrequency: "monthly" },
{ url: `${SITE_URL}/gizlilik`, priority: 0.2, changeFrequency: "yearly" }, { url: `${SITE_URL}/gizlilik`, priority: 0.2, changeFrequency: "yearly" },
{ {
url: `${SITE_URL}/cerez-politikasi`, url: `${SITE_URL}/cerez-politikasi`,

View File

@@ -0,0 +1,114 @@
import type { Metadata } from "next";
import { ArayuzBilgisi } from "@/components/arayuz-bilgisi";
import { Parallax } from "@/components/parallax";
import { GeriBildirimFormu } from "@/features/geri-bildirim/components/geri-bildirim-formu";
import { SiteFooter } from "@/components/site-footer";
import { SURUM } from "@/lib/surum";
import { sayfaMetadata } from "@/lib/seo";
export const metadata: Metadata = sayfaMetadata({
title: "Sürüm notları",
description:
"KolayTercih beta sürüm notları: neler değişti ve geri bildirimini nasıl iletirsin.",
path: "/surum-notlari",
});
/**
* Sürüm günlüğü. En yeni en üstte; her sürüm package.json'daki numarayla
* aynı adı taşır (AGENTS.md "Sürümleme"). Yeni bir sürüm çıkarken buraya bir
* kayıt eklenir. Yalnız görünüşü etkileyen değişiklikler `arayuz` alanına
* yazılır: listede tek satır ("Arayüz iyileştirmeleri") olur, ayrıntısı
* yanındaki bilgi düğmesinin balonunda durur.
*/
const SURUMLER: {
surum: string;
tarih: string;
maddeler: string[];
arayuz?: string[];
}[] = [
{
surum: "1.1.0",
tarih: "7 Ekim 2026",
maddeler: [],
arayuz: [
"D tuşu açık / koyu temayı değiştirir; geçiş logodan yayılarak yumuşak oynar.",
"Bu sayfa: beta durumu, sürüm günlüğü ve geri bildirim kutusu. Header'daki Beta rozeti buraya getirir.",
],
},
{
surum: "1.0.0",
tarih: "7 Ekim 2026",
maddeler: [
"Sıralama, alan, il ve devlet/vakıf tercihine göre 24 tercihlik dengeli liste: hayal, dengeli ve güvenli dilimler.",
"Her program için YÖK Atlas verisinden taban sıralama, kontenjan ve yıllara göre eğilim.",
"Yapay zekâ destekli tercih raporu ve liste üzerinde soru sorabildiğin danışman sohbeti.",
"Üniversite ve bölüm kataloğu, üniversite sıralamaları, rehber yazıları.",
],
arayuz: ["Açık, koyu ve sistem teması."],
},
];
export default function SurumNotlariPage() {
return (
<>
<main className="mx-auto w-full max-w-2xl px-4 py-16">
<Parallax fromStart strength={10}>
<h1 className="font-heading text-3xl font-bold">Sürüm notları</h1>
<p className="mt-3 text-slate-600">
KolayTercih şu an beta sürümünde. Ürün çalışıyor ve gerçek YÖK
Atlas verisiyle liste üretiyor; ama hâlâ her gün bir şeyler
düzeltiyor, eksik gördüğümüz yerleri tamamlıyoruz. Her sürümde
neyin değiştiği aşağıda.
</p>
</Parallax>
{/* Zaman çizgisi: her sürümün piksel karesi solda durur, kareleri
ince bir çizgi birbirine bağlar (karenin ortasından bir sonraki
karenin ortasına). En eski sürümde çizgi biter. */}
<ol aria-label="Sürümler" className="mt-12">
{SURUMLER.map((s, i) => (
<li key={s.surum} className="relative pb-12 pl-7 last:pb-0">
{i < SURUMLER.length - 1 && (
<span
aria-hidden
className="absolute top-4 -bottom-4 left-[4.5px] w-px bg-slate-200 sm:top-[18px] sm:-bottom-[18px]"
/>
)}
<span
aria-hidden
className="absolute top-[11px] left-0 size-2.5 rounded-[2px] bg-primary sm:top-[13px]"
/>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<h2 className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl">
v{s.surum}
</h2>
<time className="text-sm text-slate-500">{s.tarih}</time>
{s.surum === SURUM && (
<span className="text-xs font-semibold text-primary">
Şu anki sürüm
</span>
)}
</div>
<ul className="mt-3 list-disc space-y-2 pl-5 text-base leading-relaxed text-slate-600 marker:text-slate-400">
{s.maddeler.map((m) => (
<li key={m}>{m}</li>
))}
{s.arayuz && (
<li>
<span className="inline-flex items-center gap-2 align-middle">
Arayüz iyileştirmeleri
<ArayuzBilgisi maddeler={s.arayuz} />
</span>
</li>
)}
</ul>
</li>
))}
</ol>
<GeriBildirimFormu className="mt-16" />
</main>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,57 @@
"use client";
import { useRef, useState } from "react";
import { Info } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
/**
* Sürüm notlarında "Arayüz iyileştirmeleri" satırının yanındaki bilgi düğmesi.
* Düğme ve balon, danışman sohbetindeki "Danışmanın bildikleri" düğmesiyle
* birebir aynıdır (`sohbet-client.tsx`). Fark: dokunmatikte üzerine gelme
* olmadığı için dokununca da açılıp kapanır.
*/
export function ArayuzBilgisi({ maddeler }: { maddeler: string[] }) {
const [acik, setAcik] = useState(false);
// Basış anındaki durum: Radix basışta balonu kapatır, tık ona göre çevirir.
const basistaAcikti = useRef(false);
return (
<TooltipProvider delayDuration={120}>
<Tooltip open={acik} onOpenChange={setAcik}>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Arayüz iyileştirmelerinin ayrıntısı"
onPointerDown={() => {
basistaAcikti.current = acik;
}}
onClick={(e) => {
e.preventDefault();
setAcik(!basistaAcikti.current);
}}
className="relative inline-flex size-7 cursor-help items-center justify-center rounded-full border border-slate-200 bg-card text-slate-600 transition-colors after:absolute after:-inset-2 hover:bg-slate-100 hover:text-slate-800"
>
<Info className="size-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent
side="bottom"
sideOffset={6}
className="max-w-xs flex-col items-start gap-1.5 p-3 text-left leading-relaxed"
>
<p className="font-semibold">Bu sürümdeki arayüz iyileştirmeleri:</p>
<ul className="flex list-disc flex-col gap-1 pl-4">
{maddeler.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@@ -201,6 +201,12 @@ export function SiteFooter() {
> >
İletişim İletişim
</Link> </Link>
<Link
href="/surum-notlari"
className="hover:text-slate-700 hover:underline"
>
Sürüm notları
</Link>
</nav> </nav>
<TemaSecici className="mt-6" /> <TemaSecici className="mt-6" />
{/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları {/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları

View File

@@ -33,10 +33,12 @@ export function SiteHeader() {
Tercih Tercih
</span> </span>
</span> </span>
{/* Beta rozeti + sürüm (surum-rozeti.tsx); yazı logosuyla aynı
fade'i taşır ki NavBaslik yuvası açılınca birlikte sönsün. */}
<SurumRozeti className="ml-2 transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity sm:ml-3" />
</Link> </Link>
{/* Beta rozeti + sürüm (surum-rozeti.tsx): /surum-notlari'na giden
ayrı bir Link — logo Link'inin içinde bağlantı içinde bağlantı
olmasın. Yazı logosuyla aynı fade'i taşır ki NavBaslik yuvası
açılınca birlikte sönsün. */}
<SurumRozeti className="ml-2 transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity sm:ml-3" />
<div <div
id="nav-baslik-yuvasi" id="nav-baslik-yuvasi"
className="pointer-events-none absolute inset-y-0 right-0 left-9 flex min-w-0 items-center sm:left-12 lg:right-auto lg:w-52 xl:w-72" className="pointer-events-none absolute inset-y-0 right-0 left-9 flex min-w-0 items-center sm:left-12 lg:right-auto lg:w-52 xl:w-72"

View File

@@ -1,3 +1,4 @@
import Link from "next/link";
import { SURUM } from "@/lib/surum"; import { SURUM } from "@/lib/surum";
/** /**
@@ -7,20 +8,25 @@ import { SURUM } from "@/lib/surum";
* (`bg-indigo-500`, §2.2 istisnası). Sürüm rozetin içinde değil, yanında * (`bg-indigo-500`, §2.2 istisnası). Sürüm rozetin içinde değil, yanında
* küçük puntoyla; dar ekranda yalnız rozet kalır. Logo fade'iyle birlikte * küçük puntoyla; dar ekranda yalnız rozet kalır. Logo fade'iyle birlikte
* söner (NavBaslik yuvası açılınca), o yüzden site-header'daki fade * söner (NavBaslik yuvası açılınca), o yüzden site-header'daki fade
* sınıflarını aynen taşır. * sınıflarını aynen taşır. Rozet /surum-notlari'na bağlantıdır (beta
* açıklaması, sürüm günlüğü, geri bildirim adresi).
*/ */
export function SurumRozeti({ className = "" }: { className?: string }) { export function SurumRozeti({ className = "" }: { className?: string }) {
return ( return (
<span className={`inline-flex shrink-0 items-center gap-1.5 ${className}`}> <Link
<span className="inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2 text-xs leading-none font-semibold tracking-[-0.01em] whitespace-nowrap text-white"> href="/surum-notlari"
aria-label={`Beta sürüm notları, sürüm ${SURUM}`}
className={`group/rozet inline-flex shrink-0 items-center gap-1.5 rounded-full outline-none focus-visible:ring-3 focus-visible:ring-ring/50 ${className}`}
>
<span className="inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2 text-xs leading-none font-semibold tracking-[-0.01em] whitespace-nowrap text-white transition-opacity duration-150 group-hover/rozet:opacity-85">
Beta Beta
</span> </span>
<span <span
aria-hidden
className="hidden text-xs leading-none font-medium tracking-[-0.01em] text-slate-500 tabular-nums sm:inline" className="hidden text-xs leading-none font-medium tracking-[-0.01em] text-slate-500 tabular-nums sm:inline"
aria-label={`Sürüm ${SURUM}`}
> >
v{SURUM} v{SURUM}
</span> </span>
</span> </Link>
); );
} }

View File

@@ -0,0 +1,96 @@
"use client";
// Sürüm notları sayfasının altındaki geri bildirim kutusu: başlık, metin
// alanı, buton. Mesaj geriBildirimGonder sunucu aksiyonuyla destek kutusuna
// e-posta olarak gider.
import { useState } from "react";
import { ArrowRight } from "lucide-react";
import { toast } from "@/components/ui/bildirim";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { TypingAnimation } from "@/components/ui/typing-animation";
import { geriBildirimGonder } from "@/features/geri-bildirim/geri-bildirim-actions";
const BASLIK = "Bir hata mı gördün?";
const DEVAM = "Yalın üretim metodunu benimsiyor ve fikirlerine önem veriyoruz.";
export function GeriBildirimFormu({ className = "" }: { className?: string }) {
const [mesaj, setMesaj] = useState("");
const [pending, setPending] = useState(false);
async function gonder(e: React.FormEvent) {
e.preventDefault();
setPending(true);
try {
const sonuc = await geriBildirimGonder(mesaj);
if (sonuc.ok) {
toast.success("Geri bildirimin ulaştı, teşekkürler.");
setMesaj("");
} else {
toast.error(sonuc.error);
}
} catch {
toast.error("Beklenmeyen bir hata oluştu, tekrar dener misin?");
} finally {
setPending(false);
}
}
// Dış kart yok: başlık sayfadaki sürüm başlıklarıyla aynı ölçekte durur,
// altında tek yüzey var — metin alanı kenarlıksız, buton aynı yüzeyin
// içinde (sohbet kutusuyla aynı kalıp, DESIGN.md §4.6).
return (
<section aria-labelledby="geri-bildirim-baslik" className={className}>
<form onSubmit={gonder}>
{/* Başlığın başı sabit; devamı bir kez daktilo gibi yazılır ve kalır
(döngü yok). Görünmez tam metin aynı hücrede yer tutar ki yazı
ilerlerken kutu aşağı kaymasın. Ekran okuyucu tam metni okur. */}
<h2
id="geri-bildirim-baslik"
className="grid font-heading text-2xl font-bold text-balance text-slate-900 sm:text-3xl"
>
<span className="sr-only">
{BASLIK} {DEVAM}
</span>
<span aria-hidden className="invisible col-start-1 row-start-1">
{BASLIK} {DEVAM}
</span>
<span aria-hidden className="col-start-1 row-start-1">
{BASLIK}{" "}
<TypingAnimation
typeSpeed={40}
delay={400}
className="inline leading-[inherit] tracking-normal text-slate-500"
>
{DEVAM}
</TypingAnimation>
</span>
</h2>
<div className="mt-5 rounded-2xl border border-slate-200 bg-card p-2 transition-colors duration-150 focus-within:border-primary focus-within:ring-3 focus-within:ring-ring/50">
<Textarea
id="geri-bildirim-mesaj"
aria-labelledby="geri-bildirim-baslik"
value={mesaj}
onChange={(e) => setMesaj(e.target.value)}
maxLength={2000}
placeholder="Çekinme, aklına geleni yaz…"
disabled={pending}
className="min-h-28 resize-none border-0 bg-transparent px-3 py-2 focus-visible:ring-0 disabled:bg-transparent"
/>
<div className="flex justify-end">
<Button
type="submit"
size="lg"
variant="cta"
disabled={pending || mesaj.trim().length < 10}
>
{pending ? "Gönderiliyor…" : "Gönder"}
<ArrowRight className="size-4" aria-hidden />
</Button>
</div>
</div>
</form>
</section>
);
}

View File

@@ -0,0 +1,82 @@
"use server";
import { headers } from "next/headers";
import { getSession } from "@/lib/session";
import { geriBildirimEpostasi } from "@/lib/eposta";
import { epostaGonder } from "@/lib/eposta-gonder";
import { SURUM } from "@/lib/surum";
const DESTEK_ADRESI = "destek@kolaytercih.com";
const MIN_UZUNLUK = 10;
const MAX_UZUNLUK = 2000;
// Uç oturumsuz da çalışır; kötüye kullanım yalnız kendi destek kutumuzu ve
// Resend kotasını doldurur. Tek süreçte bellek içi sayaç bunu sınırlamaya
// yeter: IP başına 10 dakikada 3 mesaj.
const PENCERE_MS = 10 * 60 * 1000;
const PENCERE_LIMIT = 3;
const sonGonderimler = new Map<string, number[]>();
function sinirAsildi(ip: string): boolean {
const simdi = Date.now();
const taze = (sonGonderimler.get(ip) ?? []).filter(
(t) => simdi - t < PENCERE_MS,
);
if (taze.length >= PENCERE_LIMIT) {
sonGonderimler.set(ip, taze);
return true;
}
taze.push(simdi);
sonGonderimler.set(ip, taze);
// Harita sınırsız büyümesin: eskimiş kayıtları ara sıra süpür.
if (sonGonderimler.size > 1000) {
for (const [anahtar, zamanlar] of sonGonderimler) {
if (zamanlar.every((t) => simdi - t >= PENCERE_MS)) {
sonGonderimler.delete(anahtar);
}
}
}
return false;
}
export async function geriBildirimGonder(
hamMesaj: string,
): Promise<{ ok: true } | { ok: false; error: string }> {
const mesaj = typeof hamMesaj === "string" ? hamMesaj.trim() : "";
if (mesaj.length < MIN_UZUNLUK) {
return { ok: false, error: "Biraz daha ayrıntı yazar mısın?" };
}
if (mesaj.length > MAX_UZUNLUK) {
return {
ok: false,
error: `Mesaj en fazla ${MAX_UZUNLUK} karakter olabilir.`,
};
}
const [session, h] = await Promise.all([getSession(), headers()]);
const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "bilinmiyor";
if (sinirAsildi(ip)) {
return {
ok: false,
error: "Kısa sürede çok mesaj gönderdin. Birkaç dakika sonra tekrar dene.",
};
}
try {
await epostaGonder(
DESTEK_ADRESI,
geriBildirimEpostasi({
mesaj,
gonderen: session?.user.email ?? null,
surum: SURUM,
}),
);
return { ok: true };
} catch (hata) {
console.error("[geri-bildirim] Gönderilemedi:", hata);
return {
ok: false,
error: `Mesajın gönderilemedi. ${DESTEK_ADRESI} adresine yazabilirsin.`,
};
}
}

View File

@@ -210,4 +210,40 @@ export function krediHatirlatmaEpostasi(): EpostaIcerik {
...METIN_ALT_BILGI, ...METIN_ALT_BILGI,
].join("\n"), ].join("\n"),
}; };
} }
/**
* Sürüm notları sayfasındaki geri bildirim kutusundan gelen mesaj — destek
* kutusuna düşer, kullanıcıya gitmez. `gonderen` oturum varsa hesabın
* e-postasıdır; yoksa mesaj anonimdir.
*/
export function geriBildirimEpostasi({
mesaj,
gonderen,
surum,
}: {
mesaj: string;
gonderen: string | null;
surum: string;
}): EpostaIcerik {
const konu = `Geri bildirim (v${surum})`;
const kimden = gonderen ?? "Anonim (oturum yok)";
const govde = `
<h1 style="margin:0 0 12px;font-family:${BASLIK_TIPI};font-size:22px;line-height:1.35;font-weight:700;color:${LACIVERT};">
Yeni geri bildirim
</h1>
<p style="margin:0 0 16px;font-size:13px;">
Gönderen: <strong style="color:${LACIVERT};">${kacir(kimden)}</strong> &middot; v${kacir(surum)}
</p>
<p style="margin:0;white-space:pre-wrap;color:${LACIVERT};">${kacir(mesaj)}</p>`;
return {
subject: konu,
html: kabuk({
onizleme: kacir(mesaj.slice(0, 90)),
govde,
baslikEtiketi: konu,
}),
text: [`Gönderen: ${kimden} · v${surum}`, "", mesaj].join("\n"),
};
}