v1-beta-tema #9
@@ -1,147 +1,112 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ArrowRight, Mail } from "lucide-react";
|
||||
import { ArayuzBilgisi } from "@/components/arayuz-bilgisi";
|
||||
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 { Kbd } from "@/components/ui/kbd";
|
||||
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, 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",
|
||||
});
|
||||
|
||||
/**
|
||||
* 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; "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: {
|
||||
surum: string;
|
||||
tarih: string;
|
||||
baslik: string;
|
||||
maddeler: string[];
|
||||
arayuz?: string[];
|
||||
}[] = [
|
||||
{
|
||||
surum: "1.1.0",
|
||||
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.",
|
||||
"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",
|
||||
tarih: "7 Ekim 2026",
|
||||
baslik: "Beta açılışı",
|
||||
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ı.",
|
||||
"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() {
|
||||
return (
|
||||
<>
|
||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||
<SectionEyebrow>Beta · v{SURUM}</SectionEyebrow>
|
||||
<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>
|
||||
<PagePixelDivider seed={61} className="mt-6" />
|
||||
|
||||
<section
|
||||
aria-labelledby="beta-baslik"
|
||||
className="mt-6 rounded-2xl border border-slate-200 bg-card p-5 shadow-xs sm:p-6"
|
||||
>
|
||||
<h2 id="beta-baslik" className="font-heading text-xl font-bold">
|
||||
KolayTercih şu an beta sürümünde
|
||||
</h2>
|
||||
<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">
|
||||
{/* 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-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">
|
||||
v{s.surum}
|
||||
</h3>
|
||||
<span className="text-sm font-semibold text-slate-700">
|
||||
{s.baslik}
|
||||
)}
|
||||
<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>
|
||||
<time className="text-xs text-slate-500">{s.tarih}</time>
|
||||
</div>
|
||||
<ul className="mt-3 list-disc space-y-1.5 pl-5 text-sm leading-7 text-slate-700">
|
||||
{s.maddeler.map((m) => (
|
||||
<li key={m}>{m}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<section aria-labelledby="geliyor-baslik" className="mt-12">
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -211,3 +211,39 @@ export function krediHatirlatmaEpostasi(): EpostaIcerik {
|
||||
].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