v1-beta-tema #9
@@ -1,147 +1,112 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { ArrowRight, Mail } from "lucide-react";
|
import { ArayuzBilgisi } from "@/components/arayuz-bilgisi";
|
||||||
import { Parallax } from "@/components/parallax";
|
import { Parallax } from "@/components/parallax";
|
||||||
import { PagePixelDivider, SectionEyebrow } from "@/components/pixel-decor";
|
import { GeriBildirimFormu } from "@/features/geri-bildirim/components/geri-bildirim-formu";
|
||||||
import { SiteFooter } from "@/components/site-footer";
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
import { Kbd } from "@/components/ui/kbd";
|
|
||||||
import { SURUM } from "@/lib/surum";
|
import { SURUM } from "@/lib/surum";
|
||||||
import { sayfaMetadata } from "@/lib/seo";
|
import { sayfaMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
export const metadata: Metadata = sayfaMetadata({
|
export const metadata: Metadata = sayfaMetadata({
|
||||||
title: "Sürüm notları",
|
title: "Sürüm notları",
|
||||||
description:
|
description:
|
||||||
"KolayTercih beta sürüm notları: neler değişti, neler geliyor ve geri bildirimini nasıl iletirsin.",
|
"KolayTercih beta sürüm notları: neler değişti ve geri bildirimini nasıl iletirsin.",
|
||||||
path: "/surum-notlari",
|
path: "/surum-notlari",
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sürüm günlüğü. En yeni en üstte; her sürüm package.json'daki numarayla
|
* 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
|
* aynı adı taşır (AGENTS.md "Sürümleme"). Yeni bir sürüm çıkarken buraya bir
|
||||||
* kayıt eklenir; "Geliyor" listesi bitenlerden arındırılır.
|
* 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: {
|
const SURUMLER: {
|
||||||
surum: string;
|
surum: string;
|
||||||
tarih: string;
|
tarih: string;
|
||||||
baslik: string;
|
|
||||||
maddeler: string[];
|
maddeler: string[];
|
||||||
|
arayuz?: string[];
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
surum: "1.1.0",
|
surum: "1.1.0",
|
||||||
tarih: "7 Ekim 2026",
|
tarih: "7 Ekim 2026",
|
||||||
baslik: "Tema kısayolu ve sürüm notları",
|
maddeler: [],
|
||||||
maddeler: [
|
arayuz: [
|
||||||
"D tuşu açık / koyu temayı değiştirir; geçiş logodan yayılarak yumuşak oynar.",
|
"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 adresi. Header'daki Beta rozeti buraya getirir.",
|
"Bu sayfa: beta durumu, sürüm günlüğü ve geri bildirim kutusu. Header'daki Beta rozeti buraya getirir.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
surum: "1.0.0",
|
surum: "1.0.0",
|
||||||
tarih: "7 Ekim 2026",
|
tarih: "7 Ekim 2026",
|
||||||
baslik: "Beta açılışı",
|
|
||||||
maddeler: [
|
maddeler: [
|
||||||
"Sıralama, alan, il ve devlet/vakıf tercihine göre 24 tercihlik dengeli liste: hayal, dengeli ve güvenli dilimler.",
|
"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.",
|
"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.",
|
"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ı.",
|
"Üniversite ve bölüm kataloğu, üniversite sıralamaları, rehber yazıları.",
|
||||||
"Açık, koyu ve sistem teması.",
|
|
||||||
],
|
],
|
||||||
|
arayuz: ["Açık, koyu ve sistem teması."],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const GELIYOR = [
|
|
||||||
"Listeni PDF olarak indirme ve paylaşma.",
|
|
||||||
"Yerleşme sonuçları açıklandığında listenin gerçekleşme karnesi.",
|
|
||||||
"Veli görünümü: listeyi birlikte inceleyip not bırakma.",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function SurumNotlariPage() {
|
export default function SurumNotlariPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||||
<SectionEyebrow>Beta · v{SURUM}</SectionEyebrow>
|
|
||||||
<Parallax fromStart strength={10}>
|
<Parallax fromStart strength={10}>
|
||||||
<h1 className="mt-4 font-heading text-3xl font-bold">Sürüm notları</h1>
|
<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>
|
</Parallax>
|
||||||
<PagePixelDivider seed={61} className="mt-6" />
|
|
||||||
|
|
||||||
<section
|
{/* Zaman çizgisi: her sürümün piksel karesi solda durur, kareleri
|
||||||
aria-labelledby="beta-baslik"
|
ince bir çizgi birbirine bağlar (karenin ortasından bir sonraki
|
||||||
className="mt-6 rounded-2xl border border-slate-200 bg-card p-5 shadow-xs sm:p-6"
|
karenin ortasına). En eski sürümde çizgi biter. */}
|
||||||
>
|
<ol aria-label="Sürümler" className="mt-12">
|
||||||
<h2 id="beta-baslik" className="font-heading text-xl font-bold">
|
{SURUMLER.map((s, i) => (
|
||||||
KolayTercih şu an beta sürümünde
|
<li key={s.surum} className="relative pb-12 pl-7 last:pb-0">
|
||||||
</h2>
|
{i < SURUMLER.length - 1 && (
|
||||||
<p className="mt-3 text-sm leading-7 text-slate-700">
|
|
||||||
Ü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. Bir hata, yanlış bir sayı, anlaşılmayan bir ekran
|
|
||||||
ya da “keşke şöyle olsa” dediğin bir şey görürsen bize yaz: her
|
|
||||||
geri bildirim okunur ve çoğu bir sonraki sürüme girer.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="mailto:destek@kolaytercih.com?subject=KolayTercih%20beta%20geri%20bildirimi"
|
|
||||||
className="mt-5 inline-flex items-center gap-2 rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-slate-50 transition-[background-color,transform] duration-150 hover:bg-slate-700 active:scale-[0.97]"
|
|
||||||
>
|
|
||||||
<Mail className="size-4" aria-hidden />
|
|
||||||
Geri bildirim gönder
|
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
|
||||||
</a>
|
|
||||||
<p className="mt-3 text-xs text-slate-500">
|
|
||||||
destek@kolaytercih.com · Hangi sayfada, hangi sıralamayla ne
|
|
||||||
gördüğünü yazarsan daha hızlı çözeriz. Kart bilgini asla e-postayla
|
|
||||||
paylaşma.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section aria-labelledby="gunluk-baslik" className="mt-12">
|
|
||||||
<h2 id="gunluk-baslik" className="font-heading text-xl font-bold">
|
|
||||||
Neler değişti
|
|
||||||
</h2>
|
|
||||||
<ol className="mt-4 space-y-8">
|
|
||||||
{SURUMLER.map((s) => (
|
|
||||||
<li key={s.surum} className="relative pl-6">
|
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="absolute top-2 left-0 size-2.5 rounded-[2px] bg-primary"
|
className="absolute top-4 -bottom-4 left-[4.5px] w-px bg-slate-200 sm:top-[18px] sm:-bottom-[18px]"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
)}
|
||||||
<h3 className="font-heading text-lg font-bold tabular-nums">
|
<span
|
||||||
v{s.surum}
|
aria-hidden
|
||||||
</h3>
|
className="absolute top-[11px] left-0 size-2.5 rounded-[2px] bg-primary sm:top-[13px]"
|
||||||
<span className="text-sm font-semibold text-slate-700">
|
/>
|
||||||
{s.baslik}
|
<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>
|
</span>
|
||||||
<time className="text-xs text-slate-500">{s.tarih}</time>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ul className="mt-3 list-disc space-y-1.5 pl-5 text-sm leading-7 text-slate-700">
|
<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) => (
|
{s.maddeler.map((m) => (
|
||||||
<li key={m}>{m}</li>
|
<li key={m}>{m}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
{s.arayuz && (
|
||||||
</li>
|
<li>
|
||||||
))}
|
<span className="inline-flex items-center gap-2 align-middle">
|
||||||
</ol>
|
Arayüz iyileştirmeleri
|
||||||
</section>
|
<ArayuzBilgisi maddeler={s.arayuz} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
<section aria-labelledby="geliyor-baslik" className="mt-12">
|
<GeriBildirimFormu className="mt-16" />
|
||||||
<h2 id="geliyor-baslik" className="font-heading text-xl font-bold">
|
|
||||||
Sırada ne var
|
|
||||||
</h2>
|
|
||||||
<ul className="mt-4 list-disc space-y-1.5 pl-5 text-sm leading-7 text-slate-700">
|
|
||||||
{GELIYOR.map((m) => (
|
|
||||||
<li key={m}>{m}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<p className="mt-4 text-xs text-slate-500">
|
|
||||||
Sıra geri bildirimle değişir; en çok istenen öne alınır.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<p className="mt-12 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
|
|
||||||
İpucu: <Kbd>D</Kbd> açık / koyu temayı değiştirir,
|
|
||||||
<Kbd>⌘</Kbd>
|
|
||||||
<Kbd>K</Kbd> aramayı açar.
|
|
||||||
</p>
|
|
||||||
</main>
|
</main>
|
||||||
<SiteFooter />
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 "sonner";
|
||||||
|
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"
|
||||||
|
disabled={pending || mesaj.trim().length < 10}
|
||||||
|
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97]"
|
||||||
|
>
|
||||||
|
{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,
|
...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> · 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