Files
kovakyazilim/app/admin/(protected)/projeler/form.tsx
T
egecankomur 2e001680bf feat: Çözümler bölümü + mobil menü; admin parser düzeltmeleri
- Çözümler: solutions tablosu, /cozumler liste + detay sayfası, anasayfa
  bölümü, tam admin CRUD (/admin/cozumler), header & footer linkleri,
  projelerde solution_slug ilişkisi, services-grid genelleştirildi
- Mobil menü (hamburger drawer) eklendi — header artık < lg'de gezilebilir
- Site ayarları parser: textarea CRLF (\r\n) normalizasyonu — neden biz,
  süreç adımları, değerler ve SSS blokları artık doğru parçalanıyor
- homepage_faq + garanti (title/description/items) saveSiteSettings'e
  bağlandı (daha önce hiç kaydedilmiyordu)
2026-06-02 18:21:58 +03:00

192 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Save } from "lucide-react";
import {
Checkbox,
Field,
FormActions,
FormShell,
GhostLink,
PageHeader,
PrimaryButton,
Textarea,
} from "@/components/admin/form";
import { MediaPicker } from "@/components/admin/media-picker";
import { RichEditor } from "@/components/admin/rich-editor";
import { saveProject } from "@/lib/admin-actions";
import { listServices, listSolutions } from "@/lib/data";
import type { ProjectRow } from "@/lib/types";
export async function ProjectForm({ project }: { project?: ProjectRow }) {
const [services, solutions] = await Promise.all([
listServices(),
listSolutions(),
]);
return (
<div>
<PageHeader
title={project ? "Projeyi düzenle" : "Yeni proje"}
backHref="/admin/projeler"
/>
<form action={saveProject}>
{project && <input type="hidden" name="id" value={project.$id} />}
<FormShell>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Başlık" name="title" required defaultValue={project?.title} />
<Field label="Slug" name="slug" defaultValue={project?.slug} />
<Field label="Kategori" name="category" defaultValue={project?.category} />
<label className="block">
<span className="text-sm font-medium text-[var(--navy)]">
İlgili hizmet
</span>
<select
name="service_slug"
defaultValue={project?.service_slug ?? ""}
className="mt-1.5 w-full rounded-xl border border-[var(--border)] bg-white px-4 py-2.5 text-sm outline-none focus:border-[var(--sky)] focus:ring-2 focus:ring-[var(--sky)]/20"
>
<option value=""> Yok </option>
{services.map((s) => (
<option key={s.slug} value={s.slug}>
{s.title}
</option>
))}
</select>
<span className="mt-1 block text-xs text-[var(--muted)]">
Bu projenin ait olduğu hizmet detay sayfasında "ilgili projeler" olarak görünür.
</span>
</label>
<label className="block">
<span className="text-sm font-medium text-[var(--navy)]">
İlgili çözüm
</span>
<select
name="solution_slug"
defaultValue={project?.solution_slug ?? ""}
className="mt-1.5 w-full rounded-xl border border-[var(--border)] bg-white px-4 py-2.5 text-sm outline-none focus:border-[var(--sky)] focus:ring-2 focus:ring-[var(--sky)]/20"
>
<option value=""> Yok </option>
{solutions.map((s) => (
<option key={s.slug} value={s.slug}>
{s.title}
</option>
))}
</select>
<span className="mt-1 block text-xs text-[var(--muted)]">
Bu projenin ait olduğu çözüm çözüm detay sayfasında "ilgili projeler" olarak görünür.
</span>
</label>
<Field label="Müşteri" name="client_name" defaultValue={project?.client_name} />
<Field label="Sektör" name="industry" defaultValue={project?.industry} />
<Field
label="Süre"
name="duration"
defaultValue={project?.duration}
placeholder="örn: 3 ay"
/>
<Field
label="Yıl"
name="year"
type="number"
defaultValue={project?.year ?? new Date().getFullYear()}
/>
<Field
label="Canlı URL"
name="live_url"
type="url"
defaultValue={project?.live_url}
/>
<Field
label="Teknolojiler"
name="technologies"
defaultValue={project?.technologies?.join(", ")}
placeholder="Next.js, Appwrite, Tailwind"
help="Virgülle ayırın."
/>
</div>
<div className="mt-5 space-y-5">
<MediaPicker
label="Kapak görseli"
name="image_url"
defaultValue={project?.image_url}
help="Liste ve detay sayfasının üstündeki ana görsel."
/>
<Textarea
label="Kısa açıklama (kart için)"
name="description"
required
defaultValue={project?.description}
rows={3}
/>
<div>
<span className="text-sm font-medium text-[var(--navy)]">
Vaka çalışması içeriği
</span>
<div className="mt-1.5">
<RichEditor
name="content"
defaultValue={project?.content}
placeholder="Müşteri / Problem / Çözüm / Sonuç…"
/>
</div>
<p className="mt-1 text-xs text-[var(--muted)]">
Proje detay sayfasında uzun anlatım olarak gösterilir.
</p>
</div>
<MediaPicker
label="Galeri görselleri"
name="gallery"
multiple
defaultValue={project?.gallery ?? []}
help="Detay sayfasında lightbox galeri olarak gösterilir. Birden fazla görsel yükleyebilirsiniz."
/>
<Textarea
label="Sonuç metrikleri"
name="metrics"
rows={4}
defaultValue={(() => {
if (!project?.metrics) return "";
return project.metrics
.map((raw) => {
try {
const m = JSON.parse(raw) as {
value?: string;
label?: string;
};
return m.value && m.label ? `${m.value} | ${m.label}` : "";
} catch {
return "";
}
})
.filter(Boolean)
.join("\n");
})()}
placeholder={
"+150% | Organik trafik artışı\n2x | Dönüşüm oranı\n-40% | Sayfa yüklenme süresi"
}
help='Her satır "Değer | Etiket". Proje detay sayfasında büyük metric kartları olarak gösterilir.'
/>
</div>
<div className="mt-5">
<Checkbox
label="Öne çıkar (Anasayfada göster)"
name="featured"
defaultChecked={project?.featured ?? false}
/>
</div>
<FormActions>
<GhostLink href="/admin/projeler">İptal</GhostLink>
<PrimaryButton>
<Save className="size-4" /> Kaydet
</PrimaryButton>
</FormActions>
</FormShell>
</form>
</div>
);
}