feat: hizmet ve proje detay sayfaları + galeri sistemi

Yeni Appwrite kolonları:
- services: content (markdown), features[], faq[] (JSON-encoded), hero_image
- projects: gallery[], content (markdown), client_name, industry, duration, service_slug

Public sayfalar:
- /hizmetler/[slug]: hero + features checklist + markdown content + FAQ accordion
  + ilgili projeler (service_slug eşleşmesi)
- /projeler/[slug]: hero + meta tablosu (müşteri/sektör/süre/yıl) + kapak görseli
  + markdown vaka çalışması + lightbox galeri + diğer projeler

Yeni componentler:
- components/gallery.tsx: lightbox galeri (keyboard nav, prev/next, ESC kapat)
- components/faq-list.tsx: accordion FAQ (tek seferde tek açık)

Admin formları:
- Hizmet formu: hero_image, content (markdown), features (virgülle), FAQ
  (her blok '---' ile ayrılır, ilk satır soru, kalanı cevap)
- Proje formu: gallery (her satıra bir URL), content (markdown), client_name,
  industry, duration, service_slug (dropdown — hizmetlerden seçim)

Linkler:
- ServicesGrid kartları → /hizmetler/[slug]
- ProjectsGrid kartları → /projeler/[slug] (live_url butonu ayrı, target=_blank)

29 route üretiliyor.
This commit is contained in:
Ege Can Komur
2026-05-20 02:46:11 +03:00
parent edd0af76dc
commit c0da5ae8d3
13 changed files with 792 additions and 47 deletions
+30
View File
@@ -129,6 +129,19 @@ export async function saveService(formData: FormData) {
if (!description) throw new Error("Açıklama zorunlu");
const slug = str(formData.get("slug")) || slugify(title);
// FAQ as JSON-encoded array. Each item: {"q":"...","a":"..."}
const faqRaw = String(formData.get("faq") ?? "");
const faq = faqRaw
.split("\n---\n")
.map((block) => {
const lines = block.trim().split("\n");
const q = lines[0]?.trim();
const a = lines.slice(1).join("\n").trim();
if (!q || !a) return null;
return JSON.stringify({ q, a });
})
.filter((x): x is string => x !== null);
const data = {
slug,
title,
@@ -136,6 +149,10 @@ export async function saveService(formData: FormData) {
icon: str(formData.get("icon")),
order: num(formData.get("order")) ?? 0,
featured: bool(formData.get("featured")),
content: str(formData.get("content")),
features: strArr(formData.get("features"))?.filter(Boolean) ?? null,
faq: faq.length > 0 ? faq : null,
hero_image: str(formData.get("hero_image")),
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.services, id, data, secret);
@@ -172,6 +189,13 @@ export async function saveProject(formData: FormData) {
const description = str(formData.get("description"));
if (!description) throw new Error("Açıklama zorunlu");
// Gallery: one URL per line
const galleryRaw = String(formData.get("gallery") ?? "");
const gallery = galleryRaw
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
const data = {
slug,
title,
@@ -182,6 +206,12 @@ export async function saveProject(formData: FormData) {
technologies: strArr(formData.get("technologies")),
year: num(formData.get("year")),
featured: bool(formData.get("featured")),
gallery: gallery.length > 0 ? gallery : null,
content: str(formData.get("content")),
client_name: str(formData.get("client_name")),
industry: str(formData.get("industry")),
duration: str(formData.get("duration")),
service_slug: str(formData.get("service_slug")),
};
if (id) {
+22 -1
View File
@@ -41,12 +41,33 @@ export async function listServices(opts?: { featured?: boolean }) {
return safeList<ServiceRow>(TABLES.services, q);
}
export async function listProjects(opts?: { featured?: boolean; limit?: number }) {
export async function listProjects(opts?: {
featured?: boolean;
limit?: number;
serviceSlug?: string;
}) {
const q = [Q.orderDesc("year"), Q.limit(opts?.limit ?? 50)];
if (opts?.featured) q.unshift(Q.equal("featured", true));
if (opts?.serviceSlug) q.unshift(Q.equal("service_slug", opts.serviceSlug));
return safeList<ProjectRow>(TABLES.projects, q);
}
export async function getServiceBySlug(slug: string): Promise<ServiceRow | null> {
const res = await safeList<ServiceRow>(TABLES.services, [
Q.equal("slug", slug),
Q.limit(1),
]);
return res[0] ?? null;
}
export async function getProjectBySlug(slug: string): Promise<ProjectRow | null> {
const res = await safeList<ProjectRow>(TABLES.projects, [
Q.equal("slug", slug),
Q.limit(1),
]);
return res[0] ?? null;
}
export async function listPublishedPosts(opts?: { limit?: number }) {
return safeList<BlogPostRow>(TABLES.blogPosts, [
Q.equal("status", "published"),
+15
View File
@@ -9,6 +9,15 @@ export interface ServiceRow extends AwRow {
icon?: string | null;
order?: number | null;
featured?: boolean | null;
content?: string | null;
features?: string[] | null;
faq?: string[] | null; // each item is JSON: {"q":"...","a":"..."}
hero_image?: string | null;
}
export interface FaqItem {
q: string;
a: string;
}
export interface ProjectRow extends AwRow {
@@ -21,6 +30,12 @@ export interface ProjectRow extends AwRow {
technologies?: string[] | null;
year?: number | null;
featured?: boolean | null;
gallery?: string[] | null;
content?: string | null;
client_name?: string | null;
industry?: string | null;
duration?: string | null;
service_slug?: string | null;
}
export interface BlogPostRow extends AwRow {