fix(auth): SDK'yı kaldırıp ince REST katmanına geç (lib/appwrite-rest.ts)
Sorun: - node-appwrite v20-25 hepsi Node 26'da bozuk (node-fetch-native-with-agent polyfill) - appwrite browser SDK Server Action context'inde 'unexpected response' veriyor (büyük olasılıkla browser-only global'ları kontrol ederken) Çözüm: - Tüm Appwrite SDK'larını sil (appwrite + node-appwrite) - lib/appwrite-rest.ts: native fetch üzerinde ~250 satırlık ince REST wrapper - account: createEmailPasswordSession, get, deleteSession - tablesDB: listRows, getRow, createRow, updateRow, deleteRow - storage: listFiles, createFile, deleteFile, fileViewUrl - Q helpers: equal, orderAsc/Desc, limit, offset - AppwriteError class - Session secret cookie tabanlı auth korundu (isletmem-kovakcrm'deki desen) - Tüm CRUD action ve query'ler REST katmanına bağlandı End-to-end test edildi: ✓ Login (proper 401 hata mesajları, başarılı durumda redirect) ✓ Public read (services, blog, testimonials) ✓ Anonim create (contact form) ✓ Build (24 route) ✓ Sıfır SDK bağımlılığı — Node 26 sorunu yok
This commit is contained in:
+6
-9
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { ID } from "appwrite";
|
||||
import { publicDB, DATABASE_ID, TABLES } from "@/lib/appwrite-server";
|
||||
import { DATABASE_ID, ID, TABLES, tablesDB } from "@/lib/appwrite-rest";
|
||||
|
||||
export type ContactFormState = {
|
||||
ok: boolean;
|
||||
@@ -34,20 +33,18 @@ export async function submitContact(
|
||||
}
|
||||
|
||||
try {
|
||||
await publicDB.createRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.contactMessages,
|
||||
rowId: ID.unique(),
|
||||
data: {
|
||||
await tablesDB.createRow(DATABASE_ID, TABLES.contactMessages, ID.unique(), {
|
||||
name,
|
||||
email,
|
||||
phone: phone || null,
|
||||
subject: subject || null,
|
||||
message,
|
||||
status: "new",
|
||||
},
|
||||
});
|
||||
return { ok: true, message: "Mesajınız iletildi. En kısa sürede dönüş yapacağız." };
|
||||
return {
|
||||
ok: true,
|
||||
message: "Mesajınız iletildi. En kısa sürede dönüş yapacağız.",
|
||||
};
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : "Bilinmeyen hata";
|
||||
return { ok: false, message: `Kayıt başarısız: ${detail}` };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRow } from "@/lib/data";
|
||||
import { TABLES } from "@/lib/appwrite-server";
|
||||
import { TABLES } from "@/lib/appwrite-rest";
|
||||
import type { BlogPostRow } from "@/lib/types";
|
||||
import { BlogForm } from "../../form";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRow } from "@/lib/data";
|
||||
import { TABLES } from "@/lib/appwrite-server";
|
||||
import { TABLES } from "@/lib/appwrite-rest";
|
||||
import type { ServiceRow } from "@/lib/types";
|
||||
import { ServiceForm } from "../../form";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PageHeader } from "@/components/admin/form";
|
||||
import { MEDIA_BUCKET_ID, userStorage } from "@/lib/appwrite-server";
|
||||
import { MEDIA_BUCKET_ID, Q, storage } from "@/lib/appwrite-rest";
|
||||
import { requireSessionSecret } from "@/lib/auth";
|
||||
import { Query } from "appwrite";
|
||||
import { Upload } from "lucide-react";
|
||||
import { DeleteButton } from "@/components/admin/delete-button";
|
||||
import { uploadMedia, deleteMediaFile } from "@/lib/admin-actions";
|
||||
@@ -9,10 +8,11 @@ import { uploadMedia, deleteMediaFile } from "@/lib/admin-actions";
|
||||
async function listFiles() {
|
||||
try {
|
||||
const secret = await requireSessionSecret();
|
||||
const res = await userStorage(secret).listFiles({
|
||||
bucketId: MEDIA_BUCKET_ID,
|
||||
queries: [Query.orderDesc("$createdAt"), Query.limit(100)],
|
||||
});
|
||||
const res = await storage.listFiles(
|
||||
MEDIA_BUCKET_ID,
|
||||
[Q.orderDesc("$createdAt"), Q.limit(100)],
|
||||
secret,
|
||||
);
|
||||
return res.files;
|
||||
} catch {
|
||||
return [];
|
||||
@@ -20,7 +20,7 @@ async function listFiles() {
|
||||
}
|
||||
|
||||
function fileViewUrl(id: string) {
|
||||
return `${process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT}/storage/buckets/${MEDIA_BUCKET_ID}/files/${id}/view?project=${process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID}`;
|
||||
return storage.fileViewUrl(MEDIA_BUCKET_ID, id);
|
||||
}
|
||||
|
||||
export default async function MediaPage() {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Query } from "appwrite";
|
||||
import { DATABASE_ID, TABLES, userDB } from "@/lib/appwrite-server";
|
||||
import { DATABASE_ID, Q, TABLES, tablesDB } from "@/lib/appwrite-rest";
|
||||
import { requireSessionSecret } from "@/lib/auth";
|
||||
|
||||
async function safeCount(tableId: string, queries: string[] = []) {
|
||||
try {
|
||||
const secret = await requireSessionSecret();
|
||||
const res = await userDB(secret).listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
const res = await tablesDB.listRows(
|
||||
DATABASE_ID,
|
||||
tableId,
|
||||
queries: [...queries, Query.limit(1)],
|
||||
});
|
||||
[...queries, Q.limit(1)],
|
||||
secret,
|
||||
);
|
||||
return res.total ?? 0;
|
||||
} catch {
|
||||
return 0;
|
||||
@@ -21,12 +21,12 @@ async function safeCount(tableId: string, queries: string[] = []) {
|
||||
export default async function AdminDashboard() {
|
||||
const [posts, drafts, services, projects, testimonials, newMessages, totalMessages] =
|
||||
await Promise.all([
|
||||
safeCount(TABLES.blogPosts, [Query.equal("status", "published")]),
|
||||
safeCount(TABLES.blogPosts, [Query.equal("status", "draft")]),
|
||||
safeCount(TABLES.blogPosts, [Q.equal("status", "published")]),
|
||||
safeCount(TABLES.blogPosts, [Q.equal("status", "draft")]),
|
||||
safeCount(TABLES.services),
|
||||
safeCount(TABLES.projects),
|
||||
safeCount(TABLES.testimonials),
|
||||
safeCount(TABLES.contactMessages, [Query.equal("status", "new")]),
|
||||
safeCount(TABLES.contactMessages, [Q.equal("status", "new")]),
|
||||
safeCount(TABLES.contactMessages),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRow } from "@/lib/data";
|
||||
import { TABLES } from "@/lib/appwrite-server";
|
||||
import { TABLES } from "@/lib/appwrite-rest";
|
||||
import type { ProjectRow } from "@/lib/types";
|
||||
import { ProjectForm } from "../../form";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRow } from "@/lib/data";
|
||||
import { TABLES } from "@/lib/appwrite-server";
|
||||
import { TABLES } from "@/lib/appwrite-rest";
|
||||
import type { TestimonialRow } from "@/lib/types";
|
||||
import { TestimonialForm } from "../../form";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRow } from "@/lib/data";
|
||||
import { TABLES } from "@/lib/appwrite-server";
|
||||
import { TABLES } from "@/lib/appwrite-rest";
|
||||
import type { SeoPageRow } from "@/lib/types";
|
||||
import { SeoPageForm } from "../../page-form";
|
||||
|
||||
|
||||
+31
-11
@@ -2,14 +2,27 @@
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Account, Client } from "appwrite";
|
||||
import { account, AppwriteError } from "@/lib/appwrite-rest";
|
||||
import { SESSION_COOKIE } from "@/lib/auth";
|
||||
|
||||
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!;
|
||||
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!;
|
||||
|
||||
export type LoginState = { error?: string };
|
||||
|
||||
function friendly(err: unknown): string {
|
||||
if (err instanceof AppwriteError) {
|
||||
switch (err.type) {
|
||||
case "user_invalid_credentials":
|
||||
return "E-posta veya şifre hatalı.";
|
||||
case "user_blocked":
|
||||
return "Hesabınız engellenmiş.";
|
||||
case "general_rate_limit_exceeded":
|
||||
return "Çok fazla deneme. Birkaç dakika sonra tekrar deneyin.";
|
||||
default:
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
return err instanceof Error ? err.message : "Giriş başarısız";
|
||||
}
|
||||
|
||||
export async function loginAction(
|
||||
_prev: LoginState | undefined,
|
||||
formData: FormData,
|
||||
@@ -19,10 +32,13 @@ export async function loginAction(
|
||||
|
||||
if (!email || !password) return { error: "E-posta ve şifre zorunlu" };
|
||||
|
||||
let session;
|
||||
try {
|
||||
const client = new Client().setEndpoint(endpoint).setProject(projectId);
|
||||
const account = new Account(client);
|
||||
const session = await account.createEmailPasswordSession({ email, password });
|
||||
session = await account.createEmailPasswordSession(email, password);
|
||||
} catch (err) {
|
||||
return { error: friendly(err) };
|
||||
}
|
||||
|
||||
const store = await cookies();
|
||||
store.set(SESSION_COOKIE, session.secret, {
|
||||
httpOnly: true,
|
||||
@@ -31,16 +47,20 @@ export async function loginAction(
|
||||
path: "/",
|
||||
expires: new Date(session.expire),
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Giriş başarısız";
|
||||
return { error: msg };
|
||||
}
|
||||
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
export async function logoutAction() {
|
||||
const store = await cookies();
|
||||
const secret = store.get(SESSION_COOKIE)?.value;
|
||||
if (secret) {
|
||||
try {
|
||||
await account.deleteSession("current", secret);
|
||||
} catch {
|
||||
// ignore — cookie is cleared anyway
|
||||
}
|
||||
}
|
||||
store.delete(SESSION_COOKIE);
|
||||
redirect("/admin/login");
|
||||
}
|
||||
|
||||
+92
-140
@@ -1,25 +1,16 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { ID } from "appwrite";
|
||||
import {
|
||||
DATABASE_ID,
|
||||
ID,
|
||||
MEDIA_BUCKET_ID,
|
||||
storage,
|
||||
TABLES,
|
||||
userDB,
|
||||
userStorage,
|
||||
} from "@/lib/appwrite-server";
|
||||
tablesDB,
|
||||
} from "@/lib/appwrite-rest";
|
||||
import { requireSessionSecret } from "@/lib/auth";
|
||||
|
||||
async function db() {
|
||||
const secret = await requireSessionSecret();
|
||||
return userDB(secret);
|
||||
}
|
||||
async function storage() {
|
||||
const secret = await requireSessionSecret();
|
||||
return userStorage(secret);
|
||||
}
|
||||
|
||||
function slugify(s: string) {
|
||||
return s
|
||||
.toLowerCase()
|
||||
@@ -55,37 +46,34 @@ function strArr(v: FormDataEntryValue | null) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// ─── Media upload ────────────────────────────────────────────────
|
||||
// ─── Media ───────────────────────────────────────────────────────
|
||||
|
||||
export async function uploadMedia(formData: FormData): Promise<void> {
|
||||
const secret = await requireSessionSecret();
|
||||
const file = formData.get("file");
|
||||
if (!(file instanceof File) || file.size === 0) {
|
||||
throw new Error("Dosya seçilmedi");
|
||||
}
|
||||
const s = await storage();
|
||||
await s.createFile({
|
||||
bucketId: MEDIA_BUCKET_ID,
|
||||
fileId: ID.unique(),
|
||||
file,
|
||||
});
|
||||
await storage.createFile(MEDIA_BUCKET_ID, ID.unique(), file, secret);
|
||||
revalidatePath("/admin/medya");
|
||||
}
|
||||
|
||||
export async function deleteMediaFile(fileId: string) {
|
||||
const s = await storage();
|
||||
await s.deleteFile({ bucketId: MEDIA_BUCKET_ID, fileId });
|
||||
const secret = await requireSessionSecret();
|
||||
await storage.deleteFile(MEDIA_BUCKET_ID, fileId, secret);
|
||||
revalidatePath("/admin/medya");
|
||||
}
|
||||
|
||||
// ─── Blog ────────────────────────────────────────────────────────
|
||||
|
||||
export async function saveBlogPost(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
const title = str(formData.get("title"));
|
||||
if (!title) throw new Error("Başlık zorunlu");
|
||||
const slug = str(formData.get("slug")) || slugify(title);
|
||||
const status = (str(formData.get("status")) ?? "draft") as "draft" | "published";
|
||||
const status =
|
||||
(str(formData.get("status")) ?? "draft") as "draft" | "published";
|
||||
|
||||
const data = {
|
||||
slug,
|
||||
@@ -107,19 +95,15 @@ export async function saveBlogPost(formData: FormData) {
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await d.updateRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.blogPosts,
|
||||
rowId: id,
|
||||
data,
|
||||
});
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.blogPosts, id, data, secret);
|
||||
} else {
|
||||
await d.createRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.blogPosts,
|
||||
rowId: ID.unique(),
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.blogPosts,
|
||||
ID.unique(),
|
||||
data,
|
||||
});
|
||||
secret,
|
||||
);
|
||||
}
|
||||
revalidatePath("/admin/blog");
|
||||
revalidatePath("/blog");
|
||||
@@ -127,13 +111,9 @@ export async function saveBlogPost(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteBlogPost(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await d.deleteRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.blogPosts,
|
||||
rowId: id,
|
||||
});
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.blogPosts, id, secret);
|
||||
revalidatePath("/admin/blog");
|
||||
revalidatePath("/blog");
|
||||
}
|
||||
@@ -141,7 +121,7 @@ export async function deleteBlogPost(formData: FormData) {
|
||||
// ─── Services ────────────────────────────────────────────────────
|
||||
|
||||
export async function saveService(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
const title = str(formData.get("title"));
|
||||
if (!title) throw new Error("Başlık zorunlu");
|
||||
@@ -158,19 +138,15 @@ export async function saveService(formData: FormData) {
|
||||
featured: bool(formData.get("featured")),
|
||||
};
|
||||
if (id) {
|
||||
await d.updateRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.services,
|
||||
rowId: id,
|
||||
data,
|
||||
});
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.services, id, data, secret);
|
||||
} else {
|
||||
await d.createRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.services,
|
||||
rowId: slug,
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.services,
|
||||
slug,
|
||||
data,
|
||||
});
|
||||
secret,
|
||||
);
|
||||
}
|
||||
revalidatePath("/admin/hizmetler");
|
||||
revalidatePath("/hizmetler");
|
||||
@@ -178,13 +154,9 @@ export async function saveService(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteService(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await d.deleteRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.services,
|
||||
rowId: id,
|
||||
});
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.services, id, secret);
|
||||
revalidatePath("/admin/hizmetler");
|
||||
revalidatePath("/hizmetler");
|
||||
}
|
||||
@@ -192,7 +164,7 @@ export async function deleteService(formData: FormData) {
|
||||
// ─── Projects ────────────────────────────────────────────────────
|
||||
|
||||
export async function saveProject(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
const title = str(formData.get("title"));
|
||||
if (!title) throw new Error("Başlık zorunlu");
|
||||
@@ -213,19 +185,15 @@ export async function saveProject(formData: FormData) {
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await d.updateRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.projects,
|
||||
rowId: id,
|
||||
data,
|
||||
});
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.projects, id, data, secret);
|
||||
} else {
|
||||
await d.createRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.projects,
|
||||
rowId: ID.unique(),
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.projects,
|
||||
ID.unique(),
|
||||
data,
|
||||
});
|
||||
secret,
|
||||
);
|
||||
}
|
||||
revalidatePath("/admin/projeler");
|
||||
revalidatePath("/projeler");
|
||||
@@ -233,13 +201,9 @@ export async function saveProject(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteProject(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await d.deleteRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.projects,
|
||||
rowId: id,
|
||||
});
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.projects, id, secret);
|
||||
revalidatePath("/admin/projeler");
|
||||
revalidatePath("/projeler");
|
||||
}
|
||||
@@ -247,7 +211,7 @@ export async function deleteProject(formData: FormData) {
|
||||
// ─── Testimonials ────────────────────────────────────────────────
|
||||
|
||||
export async function saveTestimonial(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
const name = str(formData.get("name"));
|
||||
if (!name) throw new Error("Ad zorunlu");
|
||||
@@ -266,39 +230,31 @@ export async function saveTestimonial(formData: FormData) {
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await d.updateRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.testimonials,
|
||||
rowId: id,
|
||||
data,
|
||||
});
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.testimonials, id, data, secret);
|
||||
} else {
|
||||
await d.createRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.testimonials,
|
||||
rowId: ID.unique(),
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.testimonials,
|
||||
ID.unique(),
|
||||
data,
|
||||
});
|
||||
secret,
|
||||
);
|
||||
}
|
||||
revalidatePath("/admin/referanslar");
|
||||
revalidatePath("/");
|
||||
}
|
||||
|
||||
export async function deleteTestimonial(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await d.deleteRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.testimonials,
|
||||
rowId: id,
|
||||
});
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.testimonials, id, secret);
|
||||
revalidatePath("/admin/referanslar");
|
||||
}
|
||||
|
||||
// ─── SEO Settings (singleton) ────────────────────────────────────
|
||||
// ─── SEO Settings ────────────────────────────────────────────────
|
||||
|
||||
export async function saveSeoSettings(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const data = {
|
||||
site_name: str(formData.get("site_name")),
|
||||
site_description: str(formData.get("site_description")),
|
||||
@@ -311,28 +267,30 @@ export async function saveSeoSettings(formData: FormData) {
|
||||
gtm_id: str(formData.get("gtm_id")),
|
||||
};
|
||||
try {
|
||||
await d.updateRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.seoSettings,
|
||||
rowId: "global",
|
||||
await tablesDB.updateRow(
|
||||
DATABASE_ID,
|
||||
TABLES.seoSettings,
|
||||
"global",
|
||||
data,
|
||||
});
|
||||
secret,
|
||||
);
|
||||
} catch {
|
||||
await d.createRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.seoSettings,
|
||||
rowId: "global",
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.seoSettings,
|
||||
"global",
|
||||
data,
|
||||
});
|
||||
secret,
|
||||
);
|
||||
}
|
||||
revalidatePath("/", "layout");
|
||||
revalidatePath("/admin/seo");
|
||||
}
|
||||
|
||||
// ─── SEO Page (per path) ─────────────────────────────────────────
|
||||
// ─── SEO Page ────────────────────────────────────────────────────
|
||||
|
||||
export async function saveSeoPage(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = str(formData.get("id"));
|
||||
const path = str(formData.get("path"));
|
||||
if (!path) throw new Error("Path zorunlu");
|
||||
@@ -347,61 +305,55 @@ export async function saveSeoPage(formData: FormData) {
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await d.updateRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.seoPages,
|
||||
rowId: id,
|
||||
data,
|
||||
});
|
||||
await tablesDB.updateRow(DATABASE_ID, TABLES.seoPages, id, data, secret);
|
||||
} else {
|
||||
await d.createRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.seoPages,
|
||||
rowId: ID.unique(),
|
||||
await tablesDB.createRow(
|
||||
DATABASE_ID,
|
||||
TABLES.seoPages,
|
||||
ID.unique(),
|
||||
data,
|
||||
});
|
||||
secret,
|
||||
);
|
||||
}
|
||||
revalidatePath(path);
|
||||
revalidatePath("/admin/seo");
|
||||
}
|
||||
|
||||
export async function deleteSeoPage(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await d.deleteRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.seoPages,
|
||||
rowId: id,
|
||||
});
|
||||
await tablesDB.deleteRow(DATABASE_ID, TABLES.seoPages, id, secret);
|
||||
revalidatePath("/admin/seo");
|
||||
}
|
||||
|
||||
// ─── Contact messages ────────────────────────────────────────────
|
||||
// ─── Contact ─────────────────────────────────────────────────────
|
||||
|
||||
export async function updateMessageStatus(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
const status = String(formData.get("status")) as
|
||||
| "new"
|
||||
| "read"
|
||||
| "replied"
|
||||
| "archived";
|
||||
await d.updateRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.contactMessages,
|
||||
rowId: id,
|
||||
data: { status },
|
||||
});
|
||||
await tablesDB.updateRow(
|
||||
DATABASE_ID,
|
||||
TABLES.contactMessages,
|
||||
id,
|
||||
{ status },
|
||||
secret,
|
||||
);
|
||||
revalidatePath("/admin/iletisim");
|
||||
}
|
||||
|
||||
export async function deleteMessage(formData: FormData) {
|
||||
const d = await db();
|
||||
const secret = await requireSessionSecret();
|
||||
const id = String(formData.get("id"));
|
||||
await d.deleteRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.contactMessages,
|
||||
rowId: id,
|
||||
});
|
||||
await tablesDB.deleteRow(
|
||||
DATABASE_ID,
|
||||
TABLES.contactMessages,
|
||||
id,
|
||||
secret,
|
||||
);
|
||||
revalidatePath("/admin/iletisim");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
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",
|
||||
} 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 aw<T>(path: string, opts: FetchOpts = {}): Promise<T> {
|
||||
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);
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: opts.method ?? "GET",
|
||||
headers,
|
||||
body,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
const ct = res.headers.get("content-type") ?? "";
|
||||
if (ct.includes("application/json")) {
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
// ─── 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 = {
|
||||
createEmailPasswordSession(email: string, password: string) {
|
||||
return aw<AwSession>("/account/sessions/email", {
|
||||
method: "POST",
|
||||
body: { email, password },
|
||||
});
|
||||
},
|
||||
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;
|
||||
},
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import "server-only";
|
||||
import { Account, Client, Storage, TablesDB } from "appwrite";
|
||||
|
||||
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!;
|
||||
const projectId = process.env.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",
|
||||
} as const;
|
||||
|
||||
function newClient() {
|
||||
return new Client().setEndpoint(endpoint).setProject(projectId);
|
||||
}
|
||||
|
||||
export function publicClient() {
|
||||
return newClient();
|
||||
}
|
||||
|
||||
export function sessionClient(sessionSecret: string) {
|
||||
return newClient().setSession(sessionSecret);
|
||||
}
|
||||
|
||||
export const publicDB = new TablesDB(publicClient());
|
||||
export const publicStorage = new Storage(publicClient());
|
||||
export const publicAccount = new Account(publicClient());
|
||||
|
||||
export function userDB(secret: string) {
|
||||
return new TablesDB(sessionClient(secret));
|
||||
}
|
||||
export function userStorage(secret: string) {
|
||||
return new Storage(sessionClient(secret));
|
||||
}
|
||||
export function userAccount(secret: string) {
|
||||
return new Account(sessionClient(secret));
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Client, TablesDB } from "appwrite";
|
||||
|
||||
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!;
|
||||
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!;
|
||||
|
||||
export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
|
||||
|
||||
export const TABLES = {
|
||||
contactMessages: "contact_messages",
|
||||
services: "services",
|
||||
projects: "projects",
|
||||
} as const;
|
||||
|
||||
export const client = new Client().setEndpoint(endpoint).setProject(projectId);
|
||||
|
||||
export const tablesDB = new TablesDB(client);
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { userAccount } from "@/lib/appwrite-server";
|
||||
import { account } from "@/lib/appwrite-rest";
|
||||
|
||||
export const SESSION_COOKIE = "kovak_session";
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function getCurrentUser() {
|
||||
const secret = await getSessionSecret();
|
||||
if (!secret) return null;
|
||||
try {
|
||||
return await userAccount(secret).get();
|
||||
return await account.get(secret);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
+53
-37
@@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
import { Query } from "appwrite";
|
||||
import { publicDB, DATABASE_ID, TABLES } from "@/lib/appwrite-server";
|
||||
import { DATABASE_ID, Q, TABLES, tablesDB } from "@/lib/appwrite-rest";
|
||||
import { getSessionSecret } from "@/lib/auth";
|
||||
import type {
|
||||
BlogPostRow,
|
||||
ContactMessageRow,
|
||||
@@ -13,98 +13,114 @@ import type {
|
||||
|
||||
async function safeList<T>(tableId: string, queries: string[]): Promise<T[]> {
|
||||
try {
|
||||
const res = await publicDB.listRows({
|
||||
databaseId: DATABASE_ID,
|
||||
const res = await tablesDB.listRows<T>(DATABASE_ID, tableId, queries);
|
||||
return res.rows;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function safeListAuth<T>(tableId: string, queries: string[]): Promise<T[]> {
|
||||
try {
|
||||
const secret = await getSessionSecret();
|
||||
const res = await tablesDB.listRows<T>(
|
||||
DATABASE_ID,
|
||||
tableId,
|
||||
queries,
|
||||
});
|
||||
return res.rows as T[];
|
||||
secret ?? undefined,
|
||||
);
|
||||
return res.rows;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function listServices(opts?: { featured?: boolean }) {
|
||||
const q = [Query.orderAsc("order"), Query.limit(50)];
|
||||
if (opts?.featured) q.unshift(Query.equal("featured", true));
|
||||
const q = [Q.orderAsc("order"), Q.limit(50)];
|
||||
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||
return safeList<ServiceRow>(TABLES.services, q);
|
||||
}
|
||||
|
||||
export async function listProjects(opts?: { featured?: boolean; limit?: number }) {
|
||||
const q = [Query.orderDesc("year"), Query.limit(opts?.limit ?? 50)];
|
||||
if (opts?.featured) q.unshift(Query.equal("featured", true));
|
||||
const q = [Q.orderDesc("year"), Q.limit(opts?.limit ?? 50)];
|
||||
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||
return safeList<ProjectRow>(TABLES.projects, q);
|
||||
}
|
||||
|
||||
export async function listPublishedPosts(opts?: { limit?: number }) {
|
||||
return safeList<BlogPostRow>(TABLES.blogPosts, [
|
||||
Query.equal("status", "published"),
|
||||
Query.orderDesc("published_at"),
|
||||
Query.limit(opts?.limit ?? 50),
|
||||
Q.equal("status", "published"),
|
||||
Q.orderDesc("published_at"),
|
||||
Q.limit(opts?.limit ?? 50),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function listAllPosts() {
|
||||
return safeList<BlogPostRow>(TABLES.blogPosts, [
|
||||
Query.orderDesc("$createdAt"),
|
||||
Query.limit(200),
|
||||
return safeListAuth<BlogPostRow>(TABLES.blogPosts, [
|
||||
Q.orderDesc("$createdAt"),
|
||||
Q.limit(200),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function getPostBySlug(slug: string): Promise<BlogPostRow | null> {
|
||||
const res = await safeList<BlogPostRow>(TABLES.blogPosts, [
|
||||
Query.equal("slug", slug),
|
||||
Query.limit(1),
|
||||
Q.equal("slug", slug),
|
||||
Q.limit(1),
|
||||
]);
|
||||
return res[0] ?? null;
|
||||
}
|
||||
|
||||
export async function listTestimonials(opts?: { featured?: boolean }) {
|
||||
const q = [Query.orderAsc("order"), Query.limit(50)];
|
||||
if (opts?.featured) q.unshift(Query.equal("featured", true));
|
||||
const q = [Q.orderAsc("order"), Q.limit(50)];
|
||||
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||
return safeList<TestimonialRow>(TABLES.testimonials, q);
|
||||
}
|
||||
|
||||
export async function listMessages(status?: ContactMessageRow["status"]) {
|
||||
const q = [Query.orderDesc("$createdAt"), Query.limit(200)];
|
||||
if (status) q.unshift(Query.equal("status", status));
|
||||
return safeList<ContactMessageRow>(TABLES.contactMessages, q);
|
||||
const q = [Q.orderDesc("$createdAt"), Q.limit(200)];
|
||||
if (status) q.unshift(Q.equal("status", status));
|
||||
return safeListAuth<ContactMessageRow>(TABLES.contactMessages, q);
|
||||
}
|
||||
|
||||
export async function getSeoPage(path: string): Promise<SeoPageRow | null> {
|
||||
const res = await safeList<SeoPageRow>(TABLES.seoPages, [
|
||||
Query.equal("path", path),
|
||||
Query.limit(1),
|
||||
Q.equal("path", path),
|
||||
Q.limit(1),
|
||||
]);
|
||||
return res[0] ?? null;
|
||||
}
|
||||
|
||||
export async function listSeoPages() {
|
||||
return safeList<SeoPageRow>(TABLES.seoPages, [
|
||||
Query.orderAsc("path"),
|
||||
Query.limit(200),
|
||||
return safeListAuth<SeoPageRow>(TABLES.seoPages, [
|
||||
Q.orderAsc("path"),
|
||||
Q.limit(200),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function getSeoSettings(): Promise<SeoSettingsRow | null> {
|
||||
try {
|
||||
return (await publicDB.getRow({
|
||||
databaseId: DATABASE_ID,
|
||||
tableId: TABLES.seoSettings,
|
||||
rowId: "global",
|
||||
})) as unknown as SeoSettingsRow;
|
||||
return await tablesDB.getRow<SeoSettingsRow>(
|
||||
DATABASE_ID,
|
||||
TABLES.seoSettings,
|
||||
"global",
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRow<T>(tableId: string, rowId: string): Promise<T | null> {
|
||||
export async function getRow<T>(
|
||||
tableId: string,
|
||||
rowId: string,
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return (await publicDB.getRow({
|
||||
databaseId: DATABASE_ID,
|
||||
const secret = await getSessionSecret();
|
||||
return await tablesDB.getRow<T>(
|
||||
DATABASE_ID,
|
||||
tableId,
|
||||
rowId,
|
||||
})) as unknown as T;
|
||||
secret ?? undefined,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
+10
-8
@@ -1,6 +1,8 @@
|
||||
import type { Models } from "appwrite";
|
||||
import type { AwRow } from "@/lib/appwrite-rest";
|
||||
|
||||
export interface ServiceRow extends Models.Row {
|
||||
export type Row = AwRow;
|
||||
|
||||
export interface ServiceRow extends AwRow {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -9,7 +11,7 @@ export interface ServiceRow extends Models.Row {
|
||||
featured?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ProjectRow extends Models.Row {
|
||||
export interface ProjectRow extends AwRow {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -21,7 +23,7 @@ export interface ProjectRow extends Models.Row {
|
||||
featured?: boolean | null;
|
||||
}
|
||||
|
||||
export interface BlogPostRow extends Models.Row {
|
||||
export interface BlogPostRow extends AwRow {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt?: string | null;
|
||||
@@ -37,7 +39,7 @@ export interface BlogPostRow extends Models.Row {
|
||||
seo_image?: string | null;
|
||||
}
|
||||
|
||||
export interface TestimonialRow extends Models.Row {
|
||||
export interface TestimonialRow extends AwRow {
|
||||
name: string;
|
||||
role?: string | null;
|
||||
company?: string | null;
|
||||
@@ -48,7 +50,7 @@ export interface TestimonialRow extends Models.Row {
|
||||
featured?: boolean | null;
|
||||
}
|
||||
|
||||
export interface SeoPageRow extends Models.Row {
|
||||
export interface SeoPageRow extends AwRow {
|
||||
path: string;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
@@ -57,7 +59,7 @@ export interface SeoPageRow extends Models.Row {
|
||||
noindex?: boolean | null;
|
||||
}
|
||||
|
||||
export interface SeoSettingsRow extends Models.Row {
|
||||
export interface SeoSettingsRow extends AwRow {
|
||||
site_name?: string | null;
|
||||
site_description?: string | null;
|
||||
default_og_image?: string | null;
|
||||
@@ -69,7 +71,7 @@ export interface SeoSettingsRow extends Models.Row {
|
||||
gtm_id?: string | null;
|
||||
}
|
||||
|
||||
export interface ContactMessageRow extends Models.Row {
|
||||
export interface ContactMessageRow extends AwRow {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string | null;
|
||||
|
||||
Generated
-31
@@ -8,7 +8,6 @@
|
||||
"name": "kovak-yazilim",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"appwrite": "^25.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"marked": "^18.0.4",
|
||||
"next": "16.2.6",
|
||||
@@ -1079,18 +1078,6 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/appwrite": {
|
||||
"version": "25.1.1",
|
||||
"resolved": "https://registry.npmjs.org/appwrite/-/appwrite-25.1.1.tgz",
|
||||
"integrity": "sha512-h+vVPErfCLZ9FCnWH1cZOphgX8MsXNNN0PvxCxoOCbqyX6XubtvvOEpp/BpVOAHp7jIDFEJu72ingBu9kh1vDQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"json-bigint": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.31",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
||||
@@ -1103,15 +1090,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001793",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
|
||||
@@ -1186,15 +1164,6 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"appwrite": "^25.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"marked": "^18.0.4",
|
||||
"next": "16.2.6",
|
||||
|
||||
Reference in New Issue
Block a user