aa2b7280b6
Yeni bölümler ve component'ler: - WhatsAppFloat: sağ altta her sayfada görünen 'pulse' animasyonlu WhatsApp butonu - MobileCtaBar: mobilde alt sabit bar — Ara / WhatsApp / Teklif Al üç buton - TrustBand: hero altı 4 trust kartı (Google ★, proje sayısı, dönüş süresi, garanti) + Google rating + yorum sayısı satırı - LogoCloud: müşteri logoları grayscale strip - QuickLeadForm: ad + telefon iki alanlı inline mini form (anasayfada) - app/actions submitContact 'source' alanını destekliyor (quick lead → message zorunlu değil) - WhyUs: 4 USP kartı (Hızlı teslim, Yerel destek, Modern tech, Satış sonrası) - ProcessSteps: 4 adımlı 'nasıl çalışıyoruz' süreç akışı (numaralı timeline) Schema (JSON-LD): - OrganizationLd: LocalBusiness + Address + AggregateRating (Google review puanı/sayısı) - ServiceLd: hizmet detay sayfaları için - FaqLd: hizmet FAQ'leri için - BreadcrumbLd, ArticleLd: hazır Anasayfaya OrganizationLd ekli — Google Ads quality score + organic rich results. Performans: - REST GET çağrıları cache:'no-store' yerine next.revalidate=60 (ISR) - Public sayfalar artık static rendering — LCP düşer - Mutations ve session GET'ler hâlâ no-store site_settings yeni alanları (panelden yönetilebilir): - whatsapp_message (default WhatsApp opener) - client_logos[] (logo URL listesi) - trust_items[] (JSON: icon|value|label) - why_us[] (JSON: icon, title, description) - process_steps[] (JSON: title, description) - lead_form_title, lead_form_description - google_rating, google_review_count, google_review_url Admin /admin/site formuna yeni 'Conversion / reklam optimizasyonu', 'Neden Biz?' ve 'Süreç adımları' bölümleri eklendi. Mevcut anasayfa yapısı (üstten alta): 1. Hero 2. TrustBand (mini güven sinyalleri) 3. LogoCloud (varsa müşteri logoları) 4. Hızlı iletişim + QuickLeadForm (2 sütun: tel/WA CTA + mini form) 5. Hizmetler 6. WhyUs (Neden Biz?) 7. ProcessSteps (Nasıl çalışıyoruz?) 8. Projeler 9. Testimonials 10. CTA (Final + WhatsApp)
317 lines
9.0 KiB
TypeScript
317 lines
9.0 KiB
TypeScript
import "server-only";
|
|
|
|
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!;
|
|
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!;
|
|
|
|
if (!ENDPOINT || !PROJECT_ID) {
|
|
throw new Error(
|
|
"Missing NEXT_PUBLIC_APPWRITE_ENDPOINT or NEXT_PUBLIC_APPWRITE_PROJECT_ID",
|
|
);
|
|
}
|
|
|
|
export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
|
|
export const MEDIA_BUCKET_ID =
|
|
process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID ?? "kovak-yazilim-media";
|
|
|
|
export const TABLES = {
|
|
contactMessages: "contact_messages",
|
|
services: "services",
|
|
projects: "projects",
|
|
blogPosts: "blog_posts",
|
|
testimonials: "testimonials",
|
|
seoPages: "seo_pages",
|
|
seoSettings: "seo_settings",
|
|
siteSettings: "site_settings",
|
|
} as const;
|
|
|
|
export class AppwriteError extends Error {
|
|
code: number;
|
|
type?: string;
|
|
constructor(message: string, code: number, type?: string) {
|
|
super(message);
|
|
this.name = "AppwriteError";
|
|
this.code = code;
|
|
this.type = type;
|
|
}
|
|
}
|
|
|
|
type FetchOpts = {
|
|
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
body?: unknown;
|
|
session?: string;
|
|
query?: Record<string, string | string[] | number | undefined>;
|
|
formData?: FormData;
|
|
};
|
|
|
|
async function awRaw(path: string, opts: FetchOpts = {}): Promise<Response> {
|
|
const url = new URL(ENDPOINT + path);
|
|
if (opts.query) {
|
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
if (v === undefined) continue;
|
|
if (Array.isArray(v)) v.forEach((x) => url.searchParams.append(k + "[]", String(x)));
|
|
else url.searchParams.set(k, String(v));
|
|
}
|
|
}
|
|
|
|
const headers: Record<string, string> = {
|
|
"X-Appwrite-Project": PROJECT_ID,
|
|
"X-Appwrite-Response-Format": "1.9.0",
|
|
};
|
|
if (opts.session) headers["X-Appwrite-Session"] = opts.session;
|
|
|
|
let body: BodyInit | undefined;
|
|
if (opts.formData) {
|
|
body = opts.formData;
|
|
} else if (opts.body !== undefined) {
|
|
headers["Content-Type"] = "application/json";
|
|
body = JSON.stringify(opts.body);
|
|
}
|
|
|
|
// GET requests are cached (ISR-style) with 60s revalidate so public pages
|
|
// stay fast for ad traffic; mutations and session calls bypass cache.
|
|
const method = opts.method ?? "GET";
|
|
const isAuthenticated = !!opts.session;
|
|
const fetchInit: RequestInit & { next?: { revalidate?: number; tags?: string[] } } = {
|
|
method,
|
|
headers,
|
|
body,
|
|
};
|
|
if (method === "GET" && !isAuthenticated) {
|
|
fetchInit.next = { revalidate: 60 };
|
|
} else {
|
|
fetchInit.cache = "no-store";
|
|
}
|
|
const res = await fetch(url, fetchInit);
|
|
|
|
if (!res.ok) {
|
|
let data: { message?: string; type?: string } = {};
|
|
try {
|
|
data = await res.json();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new AppwriteError(
|
|
data.message || `HTTP ${res.status}`,
|
|
res.status,
|
|
data.type,
|
|
);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
async function aw<T>(path: string, opts: FetchOpts = {}): Promise<T> {
|
|
const res = await awRaw(path, opts);
|
|
const ct = res.headers.get("content-type") ?? "";
|
|
if (ct.includes("application/json")) {
|
|
return (await res.json()) as T;
|
|
}
|
|
return undefined as T;
|
|
}
|
|
|
|
function extractSessionFromHeaders(res: Response): string | null {
|
|
// Appwrite returns the session secret in X-Fallback-Cookies as JSON,
|
|
// or in Set-Cookie header. The response body may have empty/redacted secret.
|
|
const fallback = res.headers.get("x-fallback-cookies");
|
|
if (fallback) {
|
|
try {
|
|
const parsed = JSON.parse(fallback) as Record<string, string>;
|
|
const first = Object.values(parsed)[0];
|
|
if (first) return first;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
const setCookie = res.headers.get("set-cookie");
|
|
if (setCookie) {
|
|
const m = setCookie.match(/a_session_[^=]+=([^;]+)/);
|
|
if (m?.[1]) return decodeURIComponent(m[1]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ─── Query helpers ───────────────────────────────────────────────
|
|
|
|
export const Q = {
|
|
equal: (attr: string, value: string | number | boolean | string[]) =>
|
|
JSON.stringify({
|
|
method: "equal",
|
|
attribute: attr,
|
|
values: Array.isArray(value) ? value : [value],
|
|
}),
|
|
notEqual: (attr: string, value: string | number) =>
|
|
JSON.stringify({ method: "notEqual", attribute: attr, values: [value] }),
|
|
orderAsc: (attr: string) =>
|
|
JSON.stringify({ method: "orderAsc", attribute: attr }),
|
|
orderDesc: (attr: string) =>
|
|
JSON.stringify({ method: "orderDesc", attribute: attr }),
|
|
limit: (n: number) => JSON.stringify({ method: "limit", values: [n] }),
|
|
offset: (n: number) => JSON.stringify({ method: "offset", values: [n] }),
|
|
};
|
|
|
|
// ─── Types ───────────────────────────────────────────────────────
|
|
|
|
export interface AwRow {
|
|
$id: string;
|
|
$createdAt: string;
|
|
$updatedAt: string;
|
|
[k: string]: unknown;
|
|
}
|
|
export interface AwListResponse<T> {
|
|
total: number;
|
|
rows: T[];
|
|
}
|
|
export interface AwFile {
|
|
$id: string;
|
|
name: string;
|
|
sizeOriginal: number;
|
|
mimeType: string;
|
|
$createdAt: string;
|
|
}
|
|
export interface AwSession {
|
|
$id: string;
|
|
secret: string;
|
|
expire: string;
|
|
userId: string;
|
|
provider: string;
|
|
}
|
|
export interface AwUser {
|
|
$id: string;
|
|
email: string;
|
|
name: string;
|
|
status: boolean;
|
|
}
|
|
|
|
// ─── Account ─────────────────────────────────────────────────────
|
|
|
|
export const account = {
|
|
async createEmailPasswordSession(email: string, password: string) {
|
|
const res = await awRaw("/account/sessions/email", {
|
|
method: "POST",
|
|
body: { email, password },
|
|
});
|
|
const body = (await res.json()) as AwSession;
|
|
// Appwrite redacts `secret` in the response body — read it from the
|
|
// X-Fallback-Cookies header (or Set-Cookie) where the real secret lives.
|
|
const secretFromHeader = extractSessionFromHeaders(res);
|
|
return { ...body, secret: secretFromHeader || body.secret };
|
|
},
|
|
get(session: string) {
|
|
return aw<AwUser>("/account", { session });
|
|
},
|
|
deleteSession(sessionId: string, session: string) {
|
|
return aw<void>(`/account/sessions/${sessionId}`, {
|
|
method: "DELETE",
|
|
session,
|
|
});
|
|
},
|
|
};
|
|
|
|
// ─── TablesDB ────────────────────────────────────────────────────
|
|
|
|
export const tablesDB = {
|
|
listRows<T = AwRow>(
|
|
databaseId: string,
|
|
tableId: string,
|
|
queries: string[] = [],
|
|
session?: string,
|
|
) {
|
|
return aw<AwListResponse<T>>(
|
|
`/tablesdb/${databaseId}/tables/${tableId}/rows`,
|
|
{ query: { queries }, session },
|
|
);
|
|
},
|
|
getRow<T = AwRow>(
|
|
databaseId: string,
|
|
tableId: string,
|
|
rowId: string,
|
|
session?: string,
|
|
) {
|
|
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
|
|
session,
|
|
});
|
|
},
|
|
createRow<T = AwRow>(
|
|
databaseId: string,
|
|
tableId: string,
|
|
rowId: string,
|
|
data: Record<string, unknown>,
|
|
session?: string,
|
|
) {
|
|
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows`, {
|
|
method: "POST",
|
|
body: { rowId, data },
|
|
session,
|
|
});
|
|
},
|
|
updateRow<T = AwRow>(
|
|
databaseId: string,
|
|
tableId: string,
|
|
rowId: string,
|
|
data: Record<string, unknown>,
|
|
session?: string,
|
|
) {
|
|
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
|
|
method: "PATCH",
|
|
body: { data },
|
|
session,
|
|
});
|
|
},
|
|
deleteRow(
|
|
databaseId: string,
|
|
tableId: string,
|
|
rowId: string,
|
|
session?: string,
|
|
) {
|
|
return aw<void>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
|
|
method: "DELETE",
|
|
session,
|
|
});
|
|
},
|
|
};
|
|
|
|
// ─── Storage ─────────────────────────────────────────────────────
|
|
|
|
export const storage = {
|
|
listFiles(bucketId: string, queries: string[] = [], session?: string) {
|
|
return aw<{ total: number; files: AwFile[] }>(
|
|
`/storage/buckets/${bucketId}/files`,
|
|
{ query: { queries }, session },
|
|
);
|
|
},
|
|
createFile(
|
|
bucketId: string,
|
|
fileId: string,
|
|
file: File,
|
|
session?: string,
|
|
) {
|
|
const fd = new FormData();
|
|
fd.append("fileId", fileId);
|
|
fd.append("file", file);
|
|
return aw<AwFile>(`/storage/buckets/${bucketId}/files`, {
|
|
method: "POST",
|
|
formData: fd,
|
|
session,
|
|
});
|
|
},
|
|
deleteFile(bucketId: string, fileId: string, session?: string) {
|
|
return aw<void>(`/storage/buckets/${bucketId}/files/${fileId}`, {
|
|
method: "DELETE",
|
|
session,
|
|
});
|
|
},
|
|
fileViewUrl(bucketId: string, fileId: string) {
|
|
return `${ENDPOINT}/storage/buckets/${bucketId}/files/${fileId}/view?project=${PROJECT_ID}`;
|
|
},
|
|
};
|
|
|
|
// ─── ID generator (Appwrite "unique()") ──────────────────────────
|
|
|
|
export const ID = {
|
|
unique() {
|
|
return "unique()";
|
|
},
|
|
custom(id: string) {
|
|
return id;
|
|
},
|
|
};
|