merge(main): 1.1.0 v2'ye alındı — tema geçişi, sürüm notları, geri bildirim formu, footer yazı logosu
main'deki PR #9 (v1-beta-tema) ile 1.0.0/1.1.0 commit'leri v2'ye birleştirildi. Çakışma çözümleri: - package.json sürümü 2.0.0 kaldı - animasyonlu-mark, tema-secici, hero-form, DESIGN.md §4.34: v2'deki üst küme korundu (main'in temaSekli / ikonlu radyogrubu v2'de sekilOynat / SecimHaplari ile kapsanıyor) - surum-rozeti: main'in /surum-notlari'na bağlantılı hâli alındı; header'da logo Link'inin içindeki eski kopya kaldırıldı (bağlantı içinde bağlantı olmasın) - globals.css: tema geçişi bloğu iki kez gelmişti, yinelenen silindi - geri-bildirim-formu: sonner yerine v2 bildirim yığını (ui/bildirim), buton variant="cta" - DESIGN.md §2.3: temaSekli → sekilOynat tsc, eslint ve tasarım denetimi sıfır hata. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ const staticEntries: MetadataRoute.Sitemap = [
|
||||
{ url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" },
|
||||
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
|
||||
{ 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}/cerez-politikasi`,
|
||||
|
||||
114
src/app/surum-notlari/page.tsx
Normal file
114
src/app/surum-notlari/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
57
src/components/arayuz-bilgisi.tsx
Normal file
57
src/components/arayuz-bilgisi.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -201,6 +201,12 @@ export function SiteFooter() {
|
||||
>
|
||||
İletişim
|
||||
</Link>
|
||||
<Link
|
||||
href="/surum-notlari"
|
||||
className="hover:text-slate-700 hover:underline"
|
||||
>
|
||||
Sürüm notları
|
||||
</Link>
|
||||
</nav>
|
||||
<TemaSecici className="mt-6" />
|
||||
{/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları
|
||||
|
||||
@@ -33,10 +33,12 @@ export function SiteHeader() {
|
||||
Tercih
|
||||
</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>
|
||||
{/* 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
|
||||
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"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Link from "next/link";
|
||||
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
|
||||
* 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ı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 }) {
|
||||
return (
|
||||
<span className={`inline-flex shrink-0 items-center gap-1.5 ${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">
|
||||
<Link
|
||||
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
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
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}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
82
src/features/geri-bildirim/geri-bildirim-actions.ts
Normal file
82
src/features/geri-bildirim/geri-bildirim-actions.ts
Normal 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.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -210,4 +210,40 @@ export function krediHatirlatmaEpostasi(): EpostaIcerik {
|
||||
...METIN_ALT_BILGI,
|
||||
].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> · 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"),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user