Update dependencies, enhance AI integration, and improve email content
Some checks failed
Deploy / deploy (push) Failing after 1h35m43s
Some checks failed
Deploy / deploy (push) Failing after 1h35m43s
- Added @google/genai dependency for enhanced AI capabilities. - Updated CI workflow to include new environment variables for AI integration. - Refactored AI client to support multiple providers and improved key management. - Enhanced email templates for better user engagement and clarity. - Updated error handling in AI-related functions to provide clearer feedback. - Made various improvements to the application structure and code clarity.
This commit is contained in:
@@ -3,7 +3,8 @@ import type { NextRequest } from "next/server";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { appDb, schema } from "@/lib/appdb";
|
||||
import { spendCreditForMessage, grantCredits } from "@/lib/credits";
|
||||
import { getAnthropic, SOHBET_MODEL, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
||||
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
||||
import { sohbetAkisi } from "@/lib/ai/cagri";
|
||||
import { listeOzetiCikar, type RaporSonuc } from "@/lib/ai/rapor";
|
||||
import type { RaporParams } from "@/lib/rapor-havuzu";
|
||||
import { secimOzeti } from "@/lib/sihirbaz";
|
||||
@@ -183,15 +184,11 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
// Dev'de API key yoksa danışman mock cevap stream'ler (prod'da asla)
|
||||
if (
|
||||
process.env.NODE_ENV !== "production" &&
|
||||
!process.env.ANTHROPIC_API_KEY &&
|
||||
!process.env.ANTHROPIC_AUTH_TOKEN
|
||||
) {
|
||||
if (process.env.NODE_ENV !== "production" && !anahtarVar()) {
|
||||
const mockEncoder = new TextEncoder();
|
||||
const mockUserId = session.user.id;
|
||||
const cevap =
|
||||
`(Dev mock — ANTHROPIC_API_KEY tanımlı değil.) Sorunu aldım: "${mesaj.slice(0, 120)}". ` +
|
||||
`(Dev mock — Yapay Zeka anahtarı tanımlı değil.) Sorunu aldım: "${mesaj.slice(0, 120)}". ` +
|
||||
`Gerçek danışman burada listen ve seçimlerin bağlamında yanıt verecek. ` +
|
||||
`${SORUMLULUK_REDDI}`;
|
||||
const mockReadable = new ReadableStream({
|
||||
@@ -219,17 +216,14 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = getAnthropic();
|
||||
} catch {
|
||||
if (!anahtarVar()) {
|
||||
await grantCredits({
|
||||
userId: session.user.id,
|
||||
delta: 1,
|
||||
reason: "refund",
|
||||
refId: kullaniciMesajId,
|
||||
});
|
||||
console.error("[soru] Yapay Zeka istemcisi kurulamadı (anahtar eksik olabilir)");
|
||||
console.error("[soru] Yapay Zeka anahtarı tanımlı değil");
|
||||
return Response.json(
|
||||
{
|
||||
error:
|
||||
@@ -250,36 +244,20 @@ export async function POST(request: NextRequest) {
|
||||
const encoder = new TextEncoder();
|
||||
const userId = session.user.id;
|
||||
|
||||
let stream;
|
||||
try {
|
||||
stream = client.messages.stream({
|
||||
model: SOHBET_MODEL,
|
||||
max_tokens: 1024,
|
||||
system,
|
||||
messages: mesajlar,
|
||||
});
|
||||
} catch {
|
||||
await grantCredits({ userId, delta: 1, reason: "refund", refId: kullaniciMesajId });
|
||||
return Response.json({ error: "Yapay Zeka'ya ulaşılamadı." }, { status: 502 });
|
||||
}
|
||||
|
||||
const readable = new ReadableStream({
|
||||
async start(controller) {
|
||||
let tamamlandi = false;
|
||||
try {
|
||||
for await (const event of stream) {
|
||||
if (
|
||||
event.type === "content_block_delta" &&
|
||||
event.delta.type === "text_delta"
|
||||
) {
|
||||
controller.enqueue(encoder.encode(event.delta.text));
|
||||
}
|
||||
// Sağlayıcıya bağlanma hatası da ilk parçada burada yakalanır
|
||||
let metin = "";
|
||||
for await (const parca of sohbetAkisi({
|
||||
system,
|
||||
mesajlar,
|
||||
maxTokens: 1024,
|
||||
})) {
|
||||
metin += parca;
|
||||
controller.enqueue(encoder.encode(parca));
|
||||
}
|
||||
const final = await stream.finalMessage();
|
||||
const metin = final.content
|
||||
.filter((b) => b.type === "text")
|
||||
.map((b) => b.text)
|
||||
.join("");
|
||||
await appDb.insert(schema.chatMessages).values({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
|
||||
@@ -43,7 +43,7 @@ function aiHataMesaji(err: unknown): string {
|
||||
if (err instanceof RaporUretimHatasi) return err.message;
|
||||
if (err instanceof Error && err.message === "AI_KEY_MISSING") {
|
||||
// Yapılandırma detayı log'a; kullanıcıya altyapı sızdırmayan mesaj
|
||||
console.error("[liste] ANTHROPIC_API_KEY eksik — üretim başlatılamadı");
|
||||
console.error("[liste] Yapay Zeka anahtarı eksik — üretim başlatılamadı");
|
||||
return "Liste üretimi şu anda başlatılamıyor. Birazdan tekrar dener misin? Harcanan kredin otomatik iade edilir.";
|
||||
}
|
||||
console.error("[liste] üretim hatası:", err);
|
||||
|
||||
158
src/lib/ai/cagri.ts
Normal file
158
src/lib/ai/cagri.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { z } from "zod";
|
||||
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
|
||||
import { ThinkingLevel } from "@google/genai";
|
||||
import {
|
||||
getAnthropic,
|
||||
getGemini,
|
||||
raporModeli,
|
||||
saglayici,
|
||||
sohbetModeli,
|
||||
} from "./client";
|
||||
|
||||
/**
|
||||
* Sağlayıcıdan bağımsız AI çağrı katmanı. Anthropic ve Gemini'nin SDK'ları
|
||||
* yapılandırılmış çıktı ve stream için farklı şekiller kullanıyor; rapor ve
|
||||
* sohbet kodu bu farkı görmesin diye tek arayüz burada toplanıyor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gemini JSON Schema'nın yalnızca bir alt kümesini destekliyor; zod'un ürettiği
|
||||
* $schema ve additionalProperties alanları isteği reddettiriyor, ayıklanıyor.
|
||||
*/
|
||||
function geminiSemasi(sema: z.ZodType): unknown {
|
||||
const temizle = (dugum: unknown): unknown => {
|
||||
if (Array.isArray(dugum)) return dugum.map(temizle);
|
||||
if (dugum && typeof dugum === "object") {
|
||||
const cikti: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(dugum as Record<string, unknown>)) {
|
||||
if (k === "$schema" || k === "additionalProperties") continue;
|
||||
cikti[k] = temizle(v);
|
||||
}
|
||||
return cikti;
|
||||
}
|
||||
return dugum;
|
||||
};
|
||||
return temizle(z.toJSONSchema(sema));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini ücretsiz katmanı yoğunlukta 503/429 döndürüyor; bu geçici hatalarda
|
||||
* artan bekleme ile yeniden dene. Kalıcı hatalar (400, 403) olduğu gibi fırlar.
|
||||
*/
|
||||
const GECICI_KODLAR = new Set([429, 500, 502, 503, 504]);
|
||||
|
||||
async function geciciHatadaTekrarla<T>(
|
||||
islem: () => Promise<T>,
|
||||
denemeSayisi = 3,
|
||||
): Promise<T> {
|
||||
let sonHata: unknown;
|
||||
for (let i = 0; i < denemeSayisi; i++) {
|
||||
try {
|
||||
return await islem();
|
||||
} catch (hata) {
|
||||
const kod = (hata as { status?: number })?.status;
|
||||
if (kod == null || !GECICI_KODLAR.has(kod) || i === denemeSayisi - 1) {
|
||||
throw hata;
|
||||
}
|
||||
sonHata = hata;
|
||||
await new Promise((r) => setTimeout(r, 2000 * 2 ** i));
|
||||
}
|
||||
}
|
||||
throw sonHata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Şemaya uyan tek seferlik çıktı üretir. Şemaya uymayan yanıtta null döner
|
||||
* (çağıran taraf yeniden dener).
|
||||
*/
|
||||
export async function yapilandirilmisUret<T extends z.ZodType>(opts: {
|
||||
system: string;
|
||||
kullanici: string;
|
||||
sema: T;
|
||||
maxTokens: number;
|
||||
}): Promise<z.infer<T> | null> {
|
||||
if (saglayici() === "gemini") {
|
||||
const yanit = await geciciHatadaTekrarla(() =>
|
||||
getGemini().models.generateContent({
|
||||
model: raporModeli(),
|
||||
contents: opts.kullanici,
|
||||
config: {
|
||||
systemInstruction: opts.system,
|
||||
maxOutputTokens: opts.maxTokens,
|
||||
responseMimeType: "application/json",
|
||||
responseJsonSchema: geminiSemasi(opts.sema),
|
||||
thinkingConfig: { thinkingLevel: ThinkingLevel.HIGH },
|
||||
},
|
||||
}),
|
||||
);
|
||||
const metin = yanit.text;
|
||||
if (!metin) return null;
|
||||
try {
|
||||
const parsed = opts.sema.safeParse(JSON.parse(metin));
|
||||
return parsed.success ? parsed.data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const yanit = await getAnthropic().messages.parse({
|
||||
model: raporModeli(),
|
||||
max_tokens: opts.maxTokens,
|
||||
thinking: { type: "adaptive" },
|
||||
system: opts.system,
|
||||
messages: [{ role: "user", content: opts.kullanici }],
|
||||
output_config: { format: zodOutputFormat(opts.sema) },
|
||||
});
|
||||
return yanit.parsed_output ?? null;
|
||||
}
|
||||
|
||||
export type SohbetMesaji = { role: "user" | "assistant"; content: string };
|
||||
|
||||
/**
|
||||
* Sohbet yanıtını parça parça üretir. Sağlayıcıya bakmaksızın düz metin
|
||||
* parçaları yield eder; çağıran taraf hem stream'ler hem biriktirip kaydeder.
|
||||
*/
|
||||
export async function* sohbetAkisi(opts: {
|
||||
system: string;
|
||||
mesajlar: SohbetMesaji[];
|
||||
maxTokens: number;
|
||||
}): AsyncGenerator<string> {
|
||||
if (saglayici() === "gemini") {
|
||||
const akis = await geciciHatadaTekrarla(() =>
|
||||
getGemini().models.generateContentStream({
|
||||
model: sohbetModeli(),
|
||||
contents: opts.mesajlar.map((m) => ({
|
||||
// Gemini "assistant" yerine "model" rolünü kullanır
|
||||
role: m.role === "assistant" ? "model" : "user",
|
||||
parts: [{ text: m.content }],
|
||||
})),
|
||||
config: {
|
||||
systemInstruction: opts.system,
|
||||
maxOutputTokens: opts.maxTokens,
|
||||
// Sohbet kısa ve hızlı olmalı; uzun düşünme adayı bekletiyor
|
||||
thinkingConfig: { thinkingLevel: ThinkingLevel.LOW },
|
||||
},
|
||||
}),
|
||||
);
|
||||
for await (const parca of akis) {
|
||||
const metin = parca.text;
|
||||
if (metin) yield metin;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const akis = getAnthropic().messages.stream({
|
||||
model: sohbetModeli(),
|
||||
max_tokens: opts.maxTokens,
|
||||
system: opts.system,
|
||||
messages: opts.mesajlar,
|
||||
});
|
||||
for await (const olay of akis) {
|
||||
if (
|
||||
olay.type === "content_block_delta" &&
|
||||
olay.delta.type === "text_delta"
|
||||
) {
|
||||
yield olay.delta.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,44 @@
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
// Model seçimi (bilinçli): rapor tek seferlik ve yüksek değerli -> Opus 4.8;
|
||||
export type Saglayici = "anthropic" | "gemini";
|
||||
|
||||
/** AI_SAGLAYICI env'i ile seçilir; tanımsızsa hangi anahtar varsa o kullanılır. */
|
||||
export function saglayici(): Saglayici {
|
||||
const secim = process.env.AI_SAGLAYICI?.trim().toLowerCase();
|
||||
if (secim === "gemini" || secim === "anthropic") return secim;
|
||||
return process.env.GEMINI_API_KEY ? "gemini" : "anthropic";
|
||||
}
|
||||
|
||||
/** Seçili sağlayıcının anahtarı tanımlı mı (mock moduna düşülecek mi)? */
|
||||
export function anahtarVar(): boolean {
|
||||
return saglayici() === "gemini"
|
||||
? Boolean(process.env.GEMINI_API_KEY)
|
||||
: Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN);
|
||||
}
|
||||
|
||||
// Model seçimi (bilinçli):
|
||||
// Anthropic — rapor tek seferlik ve yüksek değerli -> Opus 4.8;
|
||||
// sohbet kredi başına marj hassas -> Haiku 4.5 (kullanıcı kararı, 22 Tem 2026).
|
||||
export const RAPOR_MODEL = "claude-opus-4-8";
|
||||
export const SOHBET_MODEL = "claude-haiku-4-5";
|
||||
// Gemini — ücretsiz katmanda yalnızca Flash ailesi açık (Pro'da kota 0),
|
||||
// bu yüzden rapor ve sohbet aynı modeli kullanır; ayrım thinking seviyesinde.
|
||||
const MODELLER: Record<Saglayici, { rapor: string; sohbet: string }> = {
|
||||
anthropic: { rapor: "claude-opus-4-8", sohbet: "claude-haiku-4-5" },
|
||||
gemini: { rapor: "gemini-3.6-flash", sohbet: "gemini-3.6-flash" },
|
||||
};
|
||||
|
||||
const globalForAi = globalThis as unknown as { __anthropic?: Anthropic };
|
||||
export function raporModeli(): string {
|
||||
return MODELLER[saglayici()].rapor;
|
||||
}
|
||||
|
||||
export function sohbetModeli(): string {
|
||||
return MODELLER[saglayici()].sohbet;
|
||||
}
|
||||
|
||||
const globalForAi = globalThis as unknown as {
|
||||
__anthropic?: Anthropic;
|
||||
__gemini?: GoogleGenAI;
|
||||
};
|
||||
|
||||
export function getAnthropic(): Anthropic {
|
||||
if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) {
|
||||
@@ -17,5 +50,16 @@ export function getAnthropic(): Anthropic {
|
||||
return globalForAi.__anthropic;
|
||||
}
|
||||
|
||||
export function getGemini(): GoogleGenAI {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("AI_KEY_MISSING");
|
||||
}
|
||||
if (!globalForAi.__gemini) {
|
||||
globalForAi.__gemini = new GoogleGenAI({ apiKey });
|
||||
}
|
||||
return globalForAi.__gemini;
|
||||
}
|
||||
|
||||
export const SORUMLULUK_REDDI =
|
||||
"KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas verisine dayanır ancak yerleşme garantisi verilmez. Nihai tercih sorumluluğu adaya aittir.";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
|
||||
import { getAnthropic, RAPOR_MODEL } from "./client";
|
||||
import { anahtarVar } from "./client";
|
||||
import { yapilandirilmisUret } from "./cagri";
|
||||
import {
|
||||
havuzOlustur,
|
||||
havuzuKompaktJson,
|
||||
@@ -88,11 +88,7 @@ function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] {
|
||||
// gerçek Yapay Zeka olmadan uçtan uca test edilebilsin diye. Prod'da asla çalışmaz.
|
||||
|
||||
function mockAktif(): boolean {
|
||||
return (
|
||||
process.env.NODE_ENV !== "production" &&
|
||||
!process.env.ANTHROPIC_API_KEY &&
|
||||
!process.env.ANTHROPIC_AUTH_TOKEN
|
||||
);
|
||||
return process.env.NODE_ENV !== "production" && !anahtarVar();
|
||||
}
|
||||
|
||||
function mockRapor(
|
||||
@@ -140,7 +136,7 @@ function mockRapor(
|
||||
|
||||
return {
|
||||
tercihler,
|
||||
genelDegerlendirme: `(Dev mock — ANTHROPIC_API_KEY tanımlı değil, bu rapor gerçek Yapay Zeka çıktısı değildir.) ${params.sira.toLocaleString(
|
||||
genelDegerlendirme: `(Dev mock — Yapay Zeka anahtarı tanımlı değil, bu rapor gerçek Yapay Zeka çıktısı değildir.) ${params.sira.toLocaleString(
|
||||
"tr-TR",
|
||||
)}. sıra için hayal/dengeli/garanti dağılımlı 24 tercihlik örnek liste kuruldu.${
|
||||
revizyonNotu ? ` Revizyon isteğin not alındı: "${revizyonNotu}".` : ""
|
||||
@@ -189,7 +185,6 @@ export async function raporUret(
|
||||
return mockRapor(havuz, params, revizyonNotu);
|
||||
}
|
||||
|
||||
const client = getAnthropic();
|
||||
const havuzJson = havuzuKompaktJson(havuz);
|
||||
const kullaniciMesaji = [
|
||||
`Aday bilgisi: başarı sıralaması ${params.sira.toLocaleString("tr-TR")}, puan türü ${turLabel(params.tur)}.`,
|
||||
@@ -215,24 +210,16 @@ export async function raporUret(
|
||||
let sonuc: z.infer<typeof RaporSchema> | null = null;
|
||||
let sonHata = "";
|
||||
for (let deneme = 0; deneme < 2; deneme++) {
|
||||
const response = await client.messages.parse({
|
||||
model: RAPOR_MODEL,
|
||||
max_tokens: 16000,
|
||||
thinking: { type: "adaptive" },
|
||||
const parsed = await yapilandirilmisUret({
|
||||
system: SISTEM,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
deneme === 0
|
||||
? kullaniciMesaji
|
||||
: `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`,
|
||||
},
|
||||
],
|
||||
output_config: { format: zodOutputFormat(RaporSchema) },
|
||||
kullanici:
|
||||
deneme === 0
|
||||
? kullaniciMesaji
|
||||
: `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`,
|
||||
sema: RaporSchema,
|
||||
maxTokens: 16000,
|
||||
});
|
||||
|
||||
const parsed = response.parsed_output;
|
||||
if (!parsed) {
|
||||
sonHata = "Çıktı şemaya uymadı.";
|
||||
continue;
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
*
|
||||
* Kurallar: mail istemcileri harici CSS, <style> kuralı, flexbox ve SVG
|
||||
* desteğini güvenilir biçimde sunmaz — bu yüzden düzen tablo tabanlı ve tüm
|
||||
* stiller satır içi. Logo işareti de SVG yerine renkli hücrelerle yeniden
|
||||
* çizilir (Outlook masaüstü border-radius'u yok sayar; kareler köşeli görünür,
|
||||
* kabul edilebilir bozulma).
|
||||
* stiller satır içi. Logo işareti sitedeki SVG'nin raster kopyası olan
|
||||
* `/kolay-tercih-mark.png` ile gösterilir (SVG'yi Gmail/Outlook engeller).
|
||||
* Web fontları (Bricolage Grotesque, Cal Sans) <link> ile progresif olarak
|
||||
* yüklenir; desteklemeyen istemciler sistem yazı tipine düşer.
|
||||
*/
|
||||
|
||||
import { SITE_URL } from "./seo";
|
||||
|
||||
const MAVI = "#3b82f6";
|
||||
const SEFTALI = "#fdc7a7";
|
||||
const TURUNCU = "#ff5a00";
|
||||
const LACIVERT = "#0f172b";
|
||||
const GRI = "#475569";
|
||||
const KENAR = "#e2e8f0";
|
||||
@@ -18,6 +19,10 @@ const ZEMIN = "#f8fafc";
|
||||
|
||||
const YAZI_TIPI =
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
||||
/** Sitedeki başlıklar Cal Sans (--font-heading); destekleyen istemcide o görünür. */
|
||||
const BASLIK_TIPI = `'Cal Sans', ${YAZI_TIPI}`;
|
||||
/** Sitedeki kelime markası Bricolage Grotesque (font-bricolage). */
|
||||
const MARKA_TIPI = `'Bricolage Grotesque', ${YAZI_TIPI}`;
|
||||
|
||||
export type EpostaIcerik = {
|
||||
subject: string;
|
||||
@@ -34,33 +39,15 @@ function kacir(deger: string): string {
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
/** Logo işareti: üç yuvarlak kare + KolayTercih kelime markası. */
|
||||
/** Logo: sitedeki işaretin raster kopyası + KolayTercih kelime markası. */
|
||||
function baslik(): string {
|
||||
return `
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="padding-right:8px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td width="19" height="19" style="width:19px;height:19px;background-color:${MAVI};border-radius:5px;font-size:0;line-height:0;"> </td>
|
||||
<td width="4" style="width:4px;font-size:0;line-height:0;"> </td>
|
||||
<td>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td width="18" height="18" style="width:18px;height:18px;background-color:${SEFTALI};border-radius:5px;font-size:0;line-height:0;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="4" style="height:4px;font-size:0;line-height:0;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="18" height="18" style="width:18px;height:18px;background-color:${TURUNCU};border-radius:5px;font-size:0;line-height:0;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<td style="padding-right:6px;">
|
||||
<img src="${SITE_URL}/kolay-tercih-mark.png" width="36" height="36" alt="" style="display:block;width:36px;height:36px;border:0;">
|
||||
</td>
|
||||
<td style="font-family:${YAZI_TIPI};font-size:20px;letter-spacing:-0.5px;color:${LACIVERT};">
|
||||
<td style="font-family:${MARKA_TIPI};font-size:22px;letter-spacing:-0.05em;color:${LACIVERT};">
|
||||
<span style="font-weight:300;">Kolay</span><span style="font-weight:600;">Tercih</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -84,7 +71,9 @@ function kabuk({
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>KolayTercih</title>
|
||||
<title>Merhaba, giriş bağlantını gönderdik</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,600&family=Cal+Sans&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:${ZEMIN};">
|
||||
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;font-size:1px;line-height:1px;">${onizleme}</div>
|
||||
@@ -129,8 +118,8 @@ function buton(url: string, etiket: string): string {
|
||||
export function magicLinkEpostasi(url: string): EpostaIcerik {
|
||||
const guvenliUrl = kacir(url);
|
||||
const govde = `
|
||||
<h1 style="margin:0 0 12px;font-family:${YAZI_TIPI};font-size:22px;line-height:1.35;font-weight:700;color:${LACIVERT};">
|
||||
Giriş bağlantın hazır
|
||||
<h1 style="margin:0 0 12px;font-family:${BASLIK_TIPI};font-size:22px;line-height:1.35;font-weight:700;color:${LACIVERT};">
|
||||
Merhaba, giriş bağlantını gönderdik
|
||||
</h1>
|
||||
<p style="margin:0 0 24px;">
|
||||
Aşağıdaki butona tıklayarak KolayTercih hesabına giriş yapabilirsin.
|
||||
@@ -148,13 +137,13 @@ export function magicLinkEpostasi(url: string): EpostaIcerik {
|
||||
</div>`;
|
||||
|
||||
return {
|
||||
subject: "KolayTercih giriş bağlantın",
|
||||
subject: "Merhaba, giriş bağlantını gönderdik",
|
||||
html: kabuk({
|
||||
onizleme: "Giriş bağlantın 5 dakika geçerli.",
|
||||
govde,
|
||||
}),
|
||||
text: [
|
||||
"KolayTercih giriş bağlantın",
|
||||
"Merhaba, giriş bağlantını gönderdik",
|
||||
"",
|
||||
"Hesabına giriş yapmak için bu adresi tarayıcında aç:",
|
||||
url,
|
||||
|
||||
@@ -69,6 +69,11 @@ export async function olusturulanListeSayisi(): Promise<number> {
|
||||
export async function anlikZiyaretci(): Promise<number | null> {
|
||||
if (!HOST || !API_KEY || !STATS_SITE_ID) return null;
|
||||
|
||||
// `Date.now()`den önce şart: prerender sırasında saat okumak yasak, istek
|
||||
// anına bağlanmamız gerekiyor. Kardeş fonksiyondaki `connection()` bunu
|
||||
// karşılamıyor — ikisi `Promise.all` ile paralel koşuyor.
|
||||
await connection();
|
||||
|
||||
const onceki = bellek.canli;
|
||||
if (onceki && Date.now() - onceki.zaman < CANLI_TTL_MS) return onceki.deger;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user