commit e927cd6af3a27754418cb3082a13114723be0b13 Author: mstfyldz Date: Sat May 30 18:23:21 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65f183f --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +node_modules +# Keep environment variables out of version control +.env + +/app/generated/prisma + +# Next.js +.next +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# macOS +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b1b3a85 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# 1. Base image +FROM node:20-alpine AS base + +# 2. Dependencies +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json* ./ +RUN npm ci --legacy-peer-deps + + +# 3. Builder +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Environment variables must be present at build time for Next.js +# Coolify will provide these, but we can set defaults +ENV NEXT_TELEMETRY_DISABLED=1 + +# Generate Prisma Client +RUN npx prisma generate + +RUN npm run build + +# 4. Runner +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Copy Prisma schema and engine to avoid missing binaries in standalone mode +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +# set hostname to localhost +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/app/[lang]/[slug]/page.tsx b/app/[lang]/[slug]/page.tsx new file mode 100644 index 0000000..2bca886 --- /dev/null +++ b/app/[lang]/[slug]/page.tsx @@ -0,0 +1,42 @@ +import { getDemoBySlug, demos } from "@/data/demos"; +import KlinikTemplate from "@/components/templates/KlinikTemplate"; +import RestoranTemplate from "@/components/templates/RestoranTemplate"; +import KurumsalTemplate from "@/components/templates/KurumsalTemplate"; +import DentalTemplate from "@/components/templates/DentalTemplate"; +import RestoranTemplate2 from "@/components/templates/RestoranTemplate2"; +import { notFound } from "next/navigation"; +import { i18n, Locale } from "@/i18n-config"; + +export async function generateStaticParams() { + const paths: Array<{ lang: Locale; slug: string }> = []; + const demoList = Object.keys(demos); + for (const lang of i18n.locales) { + for (const slug of demoList) { + paths.push({ lang, slug }); + } + } + return paths; +} + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale; slug: string }> }) { + const { slug } = await params; + const data = getDemoBySlug(slug); + if (!data) return { title: "Demo Bulunamadı | Ayris Tech" }; + return { + title: `${data.firma.adi} — Premium Arayüz Demosu`, + description: data.firma.slogan, + }; +} + +export default async function DemoPage({ params }: { params: Promise<{ lang: Locale; slug: string }> }) { + const { slug } = await params; + const data = getDemoBySlug(slug); + if (!data) notFound(); + + if (data.template === "klinik") return ; + if (data.template === "restoran") return ; + if (data.template === "kurumsal") return ; + if (data.template === "dental") return ; + if (data.template === "restoran2") return ; + notFound(); +} diff --git a/app/[lang]/admin/login/page.tsx b/app/[lang]/admin/login/page.tsx new file mode 100644 index 0000000..63ab104 --- /dev/null +++ b/app/[lang]/admin/login/page.tsx @@ -0,0 +1,173 @@ +"use client"; + +import * as React from "react"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; +import { adminLoginAction } from "@/app/actions"; +import Link from "next/link"; + +const expo = [0.16, 1, 0.3, 1] as [number, number, number, number]; + +interface Props { + params: Promise<{ lang: string }>; +} + +export default function AdminLoginPage({ params }: Props) { + const router = useRouter(); + + // Unpack params Promise natively using React 19 use() hook + const { lang } = React.use(params); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!username.trim() || !password.trim()) { + setError("Kullanıcı adı ve şifre zorunludur!"); + return; + } + + setLoading(true); + setError(null); + + const res = await adminLoginAction({ username: username.trim(), password: password.trim() }); + if (res.success) { + router.push(`/${lang}/admin`); + } else { + setError(res.error || "Giriş başarısız!"); + setLoading(false); + } + }; + + return ( +
+ {/* Dynamic Background Pattern */} +
+ + {/* Decorative Floating Block */} +
+
+ +
+ {/* Back to Home Link */} +
+ + + + + + ANA SAYFAYA DÖN + +
+ + + {/* Accent Ribbon */} +
+ + {/* Logo Monogram */} +
+ AYR +
+ +
+

+ YÖNETİCİ GİRİŞİ +

+

+ AYRİS TECH YÖNETİM MERKEZİ +

+
+ +
+ {/* Error Message */} + + {error && ( + +
+ {error} + + )} + + + {/* Username Input */} +
+ + setUsername(e.target.value)} + disabled={loading} + className="w-full font-mono text-xs bg-[#EDE8E0] border border-[#C8C2B8] hover:border-[#0A0A0A] focus:border-[#0A0A0A] focus:bg-[#EDE8E0] outline-none px-4 py-3.5 text-[#0A0A0A] transition-all" + placeholder="Örn: admin" + /> +
+ + {/* Password Input */} +
+ + setPassword(e.target.value)} + disabled={loading} + className="w-full font-mono text-xs bg-[#EDE8E0] border border-[#C8C2B8] hover:border-[#0A0A0A] focus:border-[#0A0A0A] focus:bg-[#EDE8E0] outline-none px-4 py-3.5 text-[#0A0A0A] transition-all" + placeholder="••••••••" + /> +
+ + {/* Submit Button */} + + + + {/* Decorative Security Footer */} +
+ SECURE SHELL v2.4 + ● ONLINE +
+ +
+
+ ); +} diff --git a/app/[lang]/admin/page.tsx b/app/[lang]/admin/page.tsx new file mode 100644 index 0000000..b4a4090 --- /dev/null +++ b/app/[lang]/admin/page.tsx @@ -0,0 +1,99 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import AdminClient from "@/components/AdminClient"; +import { getProjects, getBlogPosts, getAdminSession, getPartners } from "@/app/actions"; +import { redirect } from "next/navigation"; + +export async function generateStaticParams() { + return [{ lang: "tr" }, { lang: "en" }]; +} + +export async function generateMetadata() { + return { + title: "Yönetim Paneli | Ayris Tech", + description: "Ayris Tech sistem, proje ve site ayarları yönetim merkezi.", + }; +} + +export default async function AdminPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + + // Guard dashboard route against unauthenticated users + const session = await getAdminSession(); + if (!session) { + redirect(`/${lang}/admin/login`); + } + + const dict = await getDictionary(lang); + + const initialDbProjects = await getProjects(); + const initialDbBlogPosts = await getBlogPosts(); + const initialDbPartners = await getPartners(); + + // Convert DB format to expected BlogPost format + const initialBlogPosts = initialDbBlogPosts.map((dbPost) => ({ + id: dbPost.id, + slug: dbPost.slug, + date: dbPost.date, + author: dbPost.author, + authorRole: dbPost.authorRole, + image: dbPost.image, + tr: { + title: dbPost.trTitle, + excerpt: dbPost.trExcerpt, + readingTime: dbPost.trReadingTime, + category: dbPost.trCategory, + tags: dbPost.trTags, + content: dbPost.trContent, + }, + en: { + title: dbPost.enTitle, + excerpt: dbPost.enExcerpt, + readingTime: dbPost.enReadingTime, + category: dbPost.enCategory, + tags: dbPost.enTags, + content: dbPost.enContent, + } + })); + + // Convert DB Project format to standard ProjectData format (mapping fields properly) + const initialProjects = initialDbProjects.map((p) => ({ + id: p.id, + num: p.num, + slug: p.slug, + title: p.title, + tag: p.tag, + desc: p.desc, + spec: p.spec, + year: p.year, + client: p.client, + duration: p.duration, + tech: p.tech, + challenge: p.challenge, + solution: p.solution, + results: p.results, + image: p.image, + gallery: p.gallery, + website: p.website, + })); + + // Map database Partners + const initialPartners = initialDbPartners.map((p) => ({ + id: p.id, + name: p.name, + tag: p.tag, + mono: p.mono, + year: p.year, + desc: p.desc, + })); + + return ( + + ); +} diff --git a/app/[lang]/blog/[slug]/page.tsx b/app/[lang]/blog/[slug]/page.tsx new file mode 100644 index 0000000..fd13698 --- /dev/null +++ b/app/[lang]/blog/[slug]/page.tsx @@ -0,0 +1,87 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import { getBlogPosts } from "@/app/actions"; +import BlogDetailClient from "@/components/BlogDetailClient"; +import { notFound } from "next/navigation"; + +// Convert DB format to expected BlogPost format +function mapBlogPost(dbPost: any) { + return { + id: dbPost.id, + slug: dbPost.slug, + date: dbPost.date, + author: dbPost.author, + authorRole: dbPost.authorRole, + image: dbPost.image, + tr: { + title: dbPost.trTitle, + excerpt: dbPost.trExcerpt, + readingTime: dbPost.trReadingTime, + category: dbPost.trCategory, + tags: dbPost.trTags, + content: dbPost.trContent, + }, + en: { + title: dbPost.enTitle, + excerpt: dbPost.enExcerpt, + readingTime: dbPost.enReadingTime, + category: dbPost.enCategory, + tags: dbPost.enTags, + content: dbPost.enContent, + } + }; +} + +export async function generateStaticParams() { + const langs: Locale[] = ["en", "tr"]; + const dbPosts = await getBlogPosts(); + + const slugs = dbPosts.length > 0 + ? dbPosts.map((p) => p.slug) + : [ + "real-time-anomaly-detection-ai", + "web3-consortium-networks-logistics", + "headless-ecommerce-conversion-rates", + ]; + + return langs.flatMap((lang) => + slugs.map((slug) => ({ lang, slug })) + ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: Locale; slug: string }>; +}) { + const { lang, slug } = await params; + const dbPosts = await getBlogPosts(); + + const dbPost = dbPosts.find((p) => p.slug === slug); + if (!dbPost) return { title: "Not Found" }; + + const post = mapBlogPost(dbPost); + const content = post[lang] || post.en; + return { + title: `${content.title} | Ayris Tech`, + description: content.excerpt, + }; +} + +export default async function BlogPostPage({ + params, +}: { + params: Promise<{ lang: Locale; slug: string }>; +}) { + const { lang, slug } = await params; + const dict = await getDictionary(lang); + + const dbPosts = await getBlogPosts(); + const dbPost = dbPosts.find((p) => p.slug === slug); + + if (!dbPost) notFound(); + + const post = mapBlogPost(dbPost); + + return ; +} diff --git a/app/[lang]/blog/page.tsx b/app/[lang]/blog/page.tsx new file mode 100644 index 0000000..00d9fbe --- /dev/null +++ b/app/[lang]/blog/page.tsx @@ -0,0 +1,47 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import BlogClient from "@/components/BlogClient"; +import { getBlogPosts } from "@/app/actions"; + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.blog.title} | ${dict.nav.brandName}${dict.nav.brandSub}`, + description: dict.blog.desc, + }; +} + +export default async function BlogPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + + const dbPosts = await getBlogPosts(); + + const initialPosts = dbPosts.map((dbPost) => ({ + id: dbPost.id, + slug: dbPost.slug, + date: dbPost.date, + author: dbPost.author, + authorRole: dbPost.authorRole, + image: dbPost.image, + tr: { + title: dbPost.trTitle, + excerpt: dbPost.trExcerpt, + readingTime: dbPost.trReadingTime, + category: dbPost.trCategory, + tags: dbPost.trTags, + content: dbPost.trContent, + }, + en: { + title: dbPost.enTitle, + excerpt: dbPost.enExcerpt, + readingTime: dbPost.enReadingTime, + category: dbPost.enCategory, + tags: dbPost.enTags, + content: dbPost.enContent, + } + })); + + return ; +} diff --git a/app/[lang]/contact/page.tsx b/app/[lang]/contact/page.tsx new file mode 100644 index 0000000..2220707 --- /dev/null +++ b/app/[lang]/contact/page.tsx @@ -0,0 +1,19 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import ContactClient from "@/components/ContactClient"; + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.contact.title} | ${dict.nav.brandName}${dict.nav.brandSub}`, + description: dict.contact.desc, + }; +} + +export default async function ContactPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + + return ; +} diff --git a/app/[lang]/faq/page.tsx b/app/[lang]/faq/page.tsx new file mode 100644 index 0000000..979efe5 --- /dev/null +++ b/app/[lang]/faq/page.tsx @@ -0,0 +1,19 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import FAQClient from "@/components/FAQClient"; + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.faq.title} | ${dict.nav.brandName}${dict.nav.brandSub}`, + description: dict.faq.desc, + }; +} + +export default async function FAQPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + + return ; +} diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx new file mode 100644 index 0000000..dccee77 --- /dev/null +++ b/app/[lang]/layout.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import { Archivo, Space_Grotesk, IBM_Plex_Mono } from "next/font/google"; +import "../globals.css"; +import { Locale, i18n } from "@/i18n-config"; + +const archivo = Archivo({ + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700", "900"], + variable: "--font-archivo", +}); + +const spaceGrotesk = Space_Grotesk({ + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700"], + variable: "--font-space-grotesk", +}); + +const ibmPlexMono = IBM_Plex_Mono({ + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700"], + variable: "--font-ibm-plex-mono", +}); + +export const metadata: Metadata = { + title: "Ayris Tech | Forging the Future with AI & Blockchain", + description: "Ayris Tech delivers enterprise-grade digital transformation. We build the impossible for the next generation of business using AI, Blockchain, and modern web technologies.", + keywords: ["AI Solutions", "Blockchain Development", "Mobile App", "Web Development", "Digital Transformation"], +}; + +export async function generateStaticParams() { + return i18n.locales.map((locale) => ({ lang: locale })); +} + +export default async function RootLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ lang: string }>; +}) { + const { lang } = await params; + return ( + + + {children} + + + ); +} diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx new file mode 100644 index 0000000..a6042c4 --- /dev/null +++ b/app/[lang]/page.tsx @@ -0,0 +1,19 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import HomeClient from "@/components/HomeClient"; + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.nav.brandName}${dict.nav.brandSub} | ${dict.hero.badge}`, + description: dict.hero.desc, + }; +} + +export default async function Page({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + + return ; +} diff --git a/app/[lang]/partners/page.tsx b/app/[lang]/partners/page.tsx new file mode 100644 index 0000000..07db13b --- /dev/null +++ b/app/[lang]/partners/page.tsx @@ -0,0 +1,25 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import PartnersClient from "@/components/PartnersClient"; +import { getPartners } from "@/app/actions"; + +export async function generateStaticParams() { + return [{ lang: "tr" }, { lang: "en" }]; +} + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.partners.title} | ${dict.nav.brandName}${dict.nav.brandSub}`, + description: dict.partners.desc, + }; +} + +export default async function PartnersPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + const partners = await getPartners(); + + return ; +} diff --git a/app/[lang]/process/page.tsx b/app/[lang]/process/page.tsx new file mode 100644 index 0000000..e97eec1 --- /dev/null +++ b/app/[lang]/process/page.tsx @@ -0,0 +1,19 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import ProcessClient from "@/components/ProcessClient"; + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.process.title} | ${dict.nav.brandName}${dict.nav.brandSub}`, + description: dict.process.desc, + }; +} + +export default async function ProcessPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + + return ; +} diff --git a/app/[lang]/services/page.tsx b/app/[lang]/services/page.tsx new file mode 100644 index 0000000..7b5996d --- /dev/null +++ b/app/[lang]/services/page.tsx @@ -0,0 +1,19 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import ServicesClient from "@/components/ServicesClient"; + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.services.title} | ${dict.nav.brandName}${dict.nav.brandSub}`, + description: dict.services.desc, + }; +} + +export default async function ServicesPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + + return ; +} diff --git a/app/[lang]/work/[slug]/page.tsx b/app/[lang]/work/[slug]/page.tsx new file mode 100644 index 0000000..95aac5d --- /dev/null +++ b/app/[lang]/work/[slug]/page.tsx @@ -0,0 +1,73 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import WorkDetailClient from "@/components/WorkDetailClient"; +import { notFound } from "next/navigation"; +import { getProjects } from "@/app/actions"; + +// ── Static params for all slug × lang combinations ── +export async function generateStaticParams() { + const langs: Locale[] = ["en", "tr"]; + const dbProjects = await getProjects(); + + const slugs = dbProjects.length > 0 + ? dbProjects.map((p) => p.slug) + : [ + "financeai-dashboard", + "chainsupply-network", + "meditrack-mobile", + "novamart-platform", + ]; + + return langs.flatMap((lang) => + slugs.map((slug) => ({ lang, slug })) + ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: Locale; slug: string }>; +}) { + const { lang, slug } = await params; + const dict = await getDictionary(lang); + + const dbProjects = await getProjects(); + const items = dbProjects.length > 0 ? dbProjects : (dict.work.items as any[]); + const project = items.find((p: any) => p.slug === slug); + if (!project) return { title: "Not Found" }; + + return { + title: `${project.title} — ${dict.nav.brandName}${dict.nav.brandSub}`, + description: project.desc, + }; +} + +export default async function WorkDetailPage({ + params, +}: { + params: Promise<{ lang: Locale; slug: string }>; +}) { + const { lang, slug } = await params; + const dict = await getDictionary(lang); + + const dbProjects = await getProjects(); + const items = dbProjects.length > 0 ? dbProjects : (dict.work.items as any[]); + const project = items.find((p: any) => p.slug === slug); + + if (!project) notFound(); + + // Determine prev/next + const currentIdx = items.findIndex((p: any) => p.slug === slug); + const prev = currentIdx > 0 ? items[currentIdx - 1] : null; + const next = currentIdx < items.length - 1 ? items[currentIdx + 1] : null; + + return ( + + ); +} diff --git a/app/[lang]/work/page.tsx b/app/[lang]/work/page.tsx new file mode 100644 index 0000000..aebe7b7 --- /dev/null +++ b/app/[lang]/work/page.tsx @@ -0,0 +1,21 @@ +import { getDictionary } from "@/get-dictionary"; +import type { Locale } from "@/i18n-config"; +import WorkClient from "@/components/WorkClient"; +import { getProjects } from "@/app/actions"; + +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + return { + title: `${dict.work.title} | ${dict.nav.brandName}${dict.nav.brandSub}`, + description: dict.work.desc, + }; +} + +export default async function WorkPage({ params }: { params: Promise<{ lang: Locale }> }) { + const { lang } = await params; + const dict = await getDictionary(lang); + const projects = await getProjects(); + + return ; +} diff --git a/app/actions.ts b/app/actions.ts new file mode 100644 index 0000000..74ec4a1 --- /dev/null +++ b/app/actions.ts @@ -0,0 +1,352 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; +import { verifyPassword, signToken, verifyToken } from "@/lib/auth"; +import { cookies } from "next/headers"; + +// ── PROJECTS ACTIONS ── + +export async function getProjects() { + try { + return await prisma.project.findMany({ + orderBy: { num: "asc" }, + }); + } catch (error) { + console.error("Error fetching projects:", error); + return []; + } +} + +export async function saveProject(data: any) { + try { + const { id, num, slug, title, tag, desc, spec, year, client, duration, tech, challenge, solution, results, image, gallery, website } = data; + + let project; + if (id) { + // Update + project = await prisma.project.update({ + where: { id: Number(id) }, + data: { + num, + slug, + title, + tag, + desc, + spec, + year, + client, + duration, + tech, + challenge, + solution, + results, + image: image || "", + gallery: gallery || [], + website: website || "", + }, + }); + } else { + // Create + // Ensure slug is unique + const existing = await prisma.project.findFirst({ where: { slug } }); + if (existing) { + throw new Error("A project with this slug already exists."); + } + + project = await prisma.project.create({ + data: { + num, + slug, + title, + tag, + desc, + spec, + year, + client, + duration, + tech, + challenge, + solution, + results, + image: image || "", + gallery: gallery || [], + website: website || "", + }, + }); + } + + revalidatePath("/[lang]/work", "layout"); + revalidatePath("/[lang]/admin", "page"); + return { success: true, project }; + } catch (error: any) { + console.error("Error saving project:", error); + return { success: false, error: error.message }; + } +} + +export async function deleteProject(id: number) { + try { + await prisma.project.delete({ + where: { id }, + }); + revalidatePath("/[lang]/work", "layout"); + revalidatePath("/[lang]/admin", "page"); + return { success: true }; + } catch (error: any) { + console.error("Error deleting project:", error); + return { success: false, error: error.message }; + } +} + +// ── BLOG POSTS ACTIONS ── + +export async function getBlogPosts() { + try { + const posts = await prisma.blogPost.findMany({ + orderBy: { date: "desc" }, + }); + return posts; + } catch (error) { + console.error("Error fetching blog posts:", error); + return []; + } +} + +export async function saveBlogPost(data: any) { + try { + const { + id, + slug, + date, + author, + authorRole, + image, + trTitle, + trExcerpt, + trReadingTime, + trCategory, + trTags, + trContent, + enTitle, + enExcerpt, + enReadingTime, + enCategory, + enTags, + enContent, + } = data; + + let post; + if (id) { + // Update + post = await prisma.blogPost.update({ + where: { id: Number(id) }, + data: { + slug, + date, + author, + authorRole, + image, + trTitle, + trExcerpt, + trReadingTime, + trCategory, + trTags, + trContent, + enTitle, + enExcerpt, + enReadingTime, + enCategory, + enTags, + enContent, + }, + }); + } else { + // Create + const existing = await prisma.blogPost.findFirst({ where: { slug } }); + if (existing) { + throw new Error("A blog post with this slug already exists."); + } + + post = await prisma.blogPost.create({ + data: { + slug, + date, + author, + authorRole, + image, + trTitle, + trExcerpt, + trReadingTime, + trCategory, + trTags, + trContent, + enTitle, + enExcerpt, + enReadingTime, + enCategory, + enTags, + enContent, + }, + }); + } + + revalidatePath("/[lang]/blog", "layout"); + revalidatePath("/[lang]/admin", "page"); + return { success: true, post }; + } catch (error: any) { + console.error("Error saving blog post:", error); + return { success: false, error: error.message }; + } +} + +export async function deleteBlogPost(id: number) { + try { + await prisma.blogPost.delete({ + where: { id }, + }); + revalidatePath("/[lang]/blog", "layout"); + revalidatePath("/[lang]/admin", "page"); + return { success: true }; + } catch (error: any) { + console.error("Error deleting blog post:", error); + return { success: false, error: error.message }; + } +} + +// ── PARTNERS ACTIONS ── + +export async function getPartners() { + try { + return await prisma.partner.findMany({ + orderBy: { id: "asc" }, + }); + } catch (error) { + console.error("Error fetching partners:", error); + return []; + } +} + +export async function savePartner(data: any) { + try { + const { id, name, tag, mono, year, desc } = data; + + let partner; + if (id) { + // Update + partner = await prisma.partner.update({ + where: { id: Number(id) }, + data: { + name, + tag, + mono, + year, + desc, + }, + }); + } else { + // Create + // Ensure name is unique + const existing = await prisma.partner.findFirst({ where: { name } }); + if (existing) { + throw new Error("Bu isimde bir partner zaten mevcut."); + } + + partner = await prisma.partner.create({ + data: { + name, + tag, + mono, + year, + desc, + }, + }); + } + + revalidatePath("/[lang]/partners", "layout"); + revalidatePath("/[lang]/admin", "page"); + return { success: true, partner }; + } catch (error: any) { + console.error("Error saving partner:", error); + return { success: false, error: error.message }; + } +} + +export async function deletePartner(id: number) { + try { + await prisma.partner.delete({ + where: { id }, + }); + revalidatePath("/[lang]/partners", "layout"); + revalidatePath("/[lang]/admin", "page"); + return { success: true }; + } catch (error: any) { + console.error("Error deleting partner:", error); + return { success: false, error: error.message }; + } +} + +// ── AUTHENTICATION ACTIONS ── + +const COOKIE_NAME = "ayris_session"; + +export async function getAdminSession() { + try { + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + if (!token) return null; + return verifyToken(token); + } catch (error) { + console.error("Error reading admin session:", error); + return null; + } +} + +export async function adminLoginAction(data: any) { + try { + const { username, password } = data; + if (!username || !password) { + return { success: false, error: "Kullanıcı adı ve şifre zorunludur!" }; + } + + const user = await prisma.user.findUnique({ + where: { username }, + }); + + if (!user) { + return { success: false, error: "Geçersiz kullanıcı adı veya şifre!" }; + } + + const isValid = verifyPassword(password, user.password); + if (!isValid) { + return { success: false, error: "Geçersiz kullanıcı adı veya şifre!" }; + } + + const token = signToken({ userId: user.id, username: user.username }); + + const cookieStore = await cookies(); + cookieStore.set(COOKIE_NAME, token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60, // 24 hours + path: "/", + }); + + return { success: true }; + } catch (error: any) { + console.error("Login action error:", error); + return { success: false, error: "Giriş yapılırken beklenmedik bir hata oluştu!" }; + } +} + +export async function adminLogoutAction() { + try { + const cookieStore = await cookies(); + cookieStore.delete(COOKIE_NAME); + return { success: true }; + } catch (error: any) { + console.error("Logout action error:", error); + return { success: false, error: "Çıkış yapılırken bir hata oluştu!" }; + } +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..a0a329e Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..c312dca --- /dev/null +++ b/app/globals.css @@ -0,0 +1,145 @@ +@import "tailwindcss"; + +@theme { + --font-display: var(--font-archivo), sans-serif; + --font-body: var(--font-space-grotesk), sans-serif; + --font-mono: var(--font-ibm-plex-mono), monospace; +} + +/* ── RESET & BASE ── */ +*, *::before, *::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 80px; + background: #F4F0E8; +} + +body { + background: #F4F0E8; + color: #0A0A0A; + overflow-x: hidden; +} + +::selection { + background-color: #FFE600; + color: #0A0A0A; +} + +/* ── SCROLLBAR ── */ +::-webkit-scrollbar { width: 4px; } +::-webkit-scrollbar-track { background: #F4F0E8; } +::-webkit-scrollbar-thumb { background: #0A0A0A; border-radius: 0; } +::-webkit-scrollbar-thumb:hover { background: #FF4500; } + +/* ── NOISE OVERLAY ── */ +.noise-overlay { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9999; + opacity: 0.03; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 150px 150px; +} + +/* ── STROKE TEXT ── */ +.text-stroke { + -webkit-text-stroke: 2px #0A0A0A; + color: transparent; +} +.text-stroke-yellow { + -webkit-text-stroke: 2px #FFE600; + color: transparent; +} + +/* ── MARQUEE ── */ +.marquee-wrapper { + overflow: hidden; + white-space: nowrap; + display: flex; +} +.marquee-track { + display: flex; + animation: marqueeScroll 18s linear infinite; +} +.marquee-track-reverse { + display: flex; + animation: marqueeScrollReverse 22s linear infinite; +} +@keyframes marqueeScroll { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +@keyframes marqueeScrollReverse { + from { transform: translateX(-50%); } + to { transform: translateX(0); } +} + +/* ── DOT GRID ── */ +.dot-grid { + background-image: radial-gradient(#C8C2B8 1px, transparent 1px); + background-size: 24px 24px; +} + +/* ── YELLOW STRIKE ── */ +.yellow-strike { + position: relative; + display: inline-block; +} +.yellow-strike::after { + content: ''; + position: absolute; + bottom: 4px; + left: 0; + right: 0; + height: 6px; + background: #FFE600; + z-index: -1; +} + +/* ── BRUTAL BUTTON ── */ +.btn-brutal { + position: relative; + border: 2px solid #0A0A0A; + transition: transform 0.1s ease, box-shadow 0.1s ease; + background: transparent; + color: #0A0A0A; +} +.btn-brutal::after { + content: ''; + position: absolute; + bottom: -5px; + right: -5px; + width: 100%; + height: 100%; + border: 2px solid #0A0A0A; + z-index: -1; + transition: all 0.15s ease; +} +.btn-brutal:hover { + transform: translate(-3px, -3px); + box-shadow: 6px 6px 0 #FFE600; +} +.btn-brutal:hover::after { + transform: translate(3px, 3px); +} +.btn-brutal:active { + transform: translate(0, 0); + box-shadow: none; +} + +.btn-brutal-yellow { + background: #FFE600; + color: #0A0A0A; + border: 2px solid #0A0A0A; +} +.btn-brutal-yellow::after { + border-color: #0A0A0A; +} +.btn-brutal-yellow:hover { + box-shadow: 6px 6px 0 #FF4500; +} diff --git a/components/AdminClient.tsx b/components/AdminClient.tsx new file mode 100644 index 0000000..edb742b --- /dev/null +++ b/components/AdminClient.tsx @@ -0,0 +1,1667 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; +import { useState, useEffect } from "react"; +import Link from "next/link"; +import Header from "@/components/Header"; +import type { Locale } from "@/i18n-config"; +import { blogPosts, BlogPost } from "@/data/blog"; +import { saveProject, deleteProject, saveBlogPost, deleteBlogPost, adminLogoutAction, savePartner, deletePartner } from "@/app/actions"; + +const expo = [0.16, 1, 0.3, 1] as [number, number, number, number]; + +interface ToastMsg { + text: string; + type: "success" | "info" | "alert"; +} + +interface ProjectData { + id?: number; + num: string; + slug: string; + title: string; + tag: string; + desc: string; + spec: string; + year: string; + client: string; + duration: string; + tech: string[]; + challenge: string; + solution: string; + results: string[]; + image: string; + gallery?: string[]; + website?: string; +} + +interface PartnerData { + id?: number; + name: string; + tag: string; + mono: string; + year: string; + desc: string; +} + +export default function AdminClient({ + lang, + dict, + initialProjects = [], + initialBlogPosts = [], + initialPartners = [], +}: { + lang: Locale; + dict: any; + initialProjects?: ProjectData[]; + initialBlogPosts?: BlogPost[]; + initialPartners?: PartnerData[]; +}) { + const [activeTab, setActiveTab] = useState<"projeler" | "blog" | "partnerler" | "ayarlar">("projeler"); + + // Toast state + const [toast, setToast] = useState(null); + const showToast = (text: string, type: "success" | "info" | "alert" = "success") => { + setToast({ text, type }); + }; + useEffect(() => { + if (toast) { + const t = setTimeout(() => setToast(null), 3000); + return () => clearTimeout(t); + } + }, [toast]); + + // Project CRUD State + const [projects, setProjects] = useState(initialProjects); + const [searchQuery, setSearchQuery] = useState(""); + const [editingProject, setEditingProject] = useState(null); + const [isAddingNew, setIsAddingNew] = useState(false); + + // Edit / New Project Form State + const [formTitle, setFormTitle] = useState(""); + const [formTag, setFormTag] = useState(""); + const [formDesc, setFormDesc] = useState(""); + const [formSpec, setFormSpec] = useState(""); + const [formYear, setFormYear] = useState(""); + const [formClient, setFormClient] = useState(""); + const [formDuration, setFormDuration] = useState(""); + const [formTechString, setFormTechString] = useState(""); + const [formChallenge, setFormChallenge] = useState(""); + const [formSolution, setFormSolution] = useState(""); + const [formResults, setFormResults] = useState([]); + const [newResultItem, setNewResultItem] = useState(""); + const [formImage, setFormImage] = useState(""); + const [formGallery, setFormGallery] = useState([]); + const [newGalleryItem, setNewGalleryItem] = useState(""); + const [formWebsite, setFormWebsite] = useState(""); + + // Blog CRUD State + const [posts, setPosts] = useState(initialBlogPosts); + const [blogSearchQuery, setBlogSearchQuery] = useState(""); + const [editingPost, setEditingPost] = useState(null); + const [isAddingNewPost, setIsAddingNewPost] = useState(false); + + // Partner CRUD State + const [partners, setPartners] = useState(initialPartners); + const [partnerSearchQuery, setPartnerSearchQuery] = useState(""); + const [editingPartner, setEditingPartner] = useState(null); + const [isAddingNewPartner, setIsAddingNewPartner] = useState(false); + + // Edit / New Partner Form State + const [formPartnerName, setFormPartnerName] = useState(""); + const [formPartnerTag, setFormPartnerTag] = useState(""); + const [formPartnerMono, setFormPartnerMono] = useState(""); + const [formPartnerYear, setFormPartnerYear] = useState(""); + const [formPartnerDesc, setFormPartnerDesc] = useState(""); + + // Blog Modal Form Sub-tabs ("genel" | "tr" | "en") + const [blogFormSubTab, setBlogFormSubTab] = useState<"genel" | "tr" | "en">("genel"); + const [formBlogSlug, setFormBlogSlug] = useState(""); + const [formBlogDate, setFormBlogDate] = useState(""); + const [formBlogAuthor, setFormBlogAuthor] = useState(""); + const [formBlogAuthorRole, setFormBlogAuthorRole] = useState(""); + const [formBlogImage, setFormBlogImage] = useState(""); + + const [formBlogTrTitle, setFormBlogTrTitle] = useState(""); + const [formBlogTrExcerpt, setFormBlogTrExcerpt] = useState(""); + const [formBlogTrReadingTime, setFormBlogTrReadingTime] = useState(""); + const [formBlogTrCategory, setFormBlogTrCategory] = useState(""); + const [formBlogTrTags, setFormBlogTrTags] = useState(""); + const [formBlogTrContent, setFormBlogTrContent] = useState(""); + + const [formBlogEnTitle, setFormBlogEnTitle] = useState(""); + const [formBlogEnExcerpt, setFormBlogEnExcerpt] = useState(""); + const [formBlogEnReadingTime, setFormBlogEnReadingTime] = useState(""); + const [formBlogEnCategory, setFormBlogEnCategory] = useState(""); + const [formBlogEnTags, setFormBlogEnTags] = useState(""); + const [formBlogEnContent, setFormBlogEnContent] = useState(""); + + // Site Settings Form State + const [brandName, setBrandName] = useState(dict.nav.brandName || "Ayris"); + const [brandSub, setBrandSub] = useState(dict.nav.brandSub || "Tech"); + const [heroBadge, setHeroBadge] = useState(dict.hero.badge || "Ayris Tech — Mühendislik Zirvesi"); + const [heroTitle1, setHeroTitle1] = useState(dict.hero.titleLine1 || "Geleceği"); + const [heroTitle2, setHeroTitle2] = useState(dict.hero.titleLine2 || "İnşa Ediyoruz"); + const [heroDesc, setHeroDesc] = useState(dict.hero.desc || ""); + const [telemetryEmail, setTelemetryEmail] = useState("info@ayristech.com"); + const [telemetryHub, setTelemetryHub] = useState(dict.contact.infoBaseText || "İstanbul, Türkiye"); + + // Open modal for project editing + const handleStartEdit = (p: ProjectData) => { + setEditingProject(p); + setIsAddingNew(false); + setFormTitle(p.title); + setFormTag(p.tag); + setFormDesc(p.desc); + setFormSpec(p.spec); + setFormYear(p.year); + setFormClient(p.client); + setFormDuration(p.duration); + setFormTechString(p.tech.join(", ")); + setFormChallenge(p.challenge); + setFormSolution(p.solution); + setFormResults([...p.results]); + setNewResultItem(""); + setFormImage(p.image || ""); + setFormGallery(p.gallery ? [...p.gallery] : []); + setNewGalleryItem(""); + setFormWebsite(p.website || ""); + }; + + // Open modal for project adding + const handleStartAdd = () => { + setEditingProject(null); + setIsAddingNew(true); + setFormTitle(""); + setFormTag(""); + setFormDesc(""); + setFormSpec(""); + setFormYear("2026"); + setFormClient(""); + setFormDuration(""); + setFormTechString(""); + setFormChallenge(""); + setFormSolution(""); + setFormResults([]); + setNewResultItem(""); + setFormImage(""); + setFormGallery([]); + setNewGalleryItem(""); + setFormWebsite(""); + }; + + // Save project + const handleSaveProject = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formTitle.trim()) { + showToast("Proje başlığı boş bırakılamaz!", "alert"); + return; + } + + const techArray = formTechString.split(",").map(t => t.trim()).filter(Boolean); + const slugValue = formTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); + + const nextNum = isAddingNew + ? (projects.length > 0 + ? String(Math.max(...projects.map(p => parseInt(p.num, 10))) + 1).padStart(2, '0') + : "01") + : editingProject!.num; + + const projectDataToSave = { + id: editingProject?.id, + num: nextNum, + slug: slugValue, + title: formTitle, + tag: formTag || "Web · Yapay Zekâ", + desc: formDesc, + spec: formSpec, + year: formYear, + client: formClient, + duration: formDuration, + tech: techArray, + challenge: formChallenge, + solution: formSolution, + results: formResults.length > 0 ? formResults : ["Proje başarıyla tamamlandı"], + image: formImage, + gallery: formGallery, + website: formWebsite, + }; + + const res = await saveProject(projectDataToSave); + if (res.success && res.project) { + if (isAddingNew) { + setProjects([...projects, { ...projectDataToSave, id: res.project.id }]); + showToast(`[ YENİ PROJE YARATILDI // ${formTitle.toUpperCase()} ]`, "success"); + setIsAddingNew(false); + } else { + const updatedList = projects.map(p => { + if (p.id === editingProject?.id) { + return { + ...p, + ...projectDataToSave, + }; + } + return p; + }); + setProjects(updatedList); + showToast(`[ PROJE BAŞARIYLA DÜZENLENDİ // ${formTitle.toUpperCase()} ]`, "success"); + setEditingProject(null); + } + } else { + showToast(`Hata: ${res.error}`, "alert"); + } + }; + + // Delete project + const handleDeleteProject = async (id: number, title: string) => { + if (confirm(`"${title}" projesini silmek istediğinize emin misiniz?`)) { + const res = await deleteProject(id); + if (res.success) { + setProjects(projects.filter(p => p.id !== id)); + showToast(`[ PROJE SİLİNDİ // ${title.toUpperCase()} ]`, "alert"); + } else { + showToast(`Hata: ${res.error}`, "alert"); + } + } + }; + + // Add list result item + const handleAddResultItem = () => { + if (newResultItem.trim()) { + setFormResults([...formResults, newResultItem.trim()]); + setNewResultItem(""); + } + }; + + // Remove list result item + const handleRemoveResultItem = (idx: number) => { + setFormResults(formResults.filter((_, i) => i !== idx)); + }; + + // Add gallery item + const handleAddGalleryItem = () => { + if (newGalleryItem.trim()) { + setFormGallery([...formGallery, newGalleryItem.trim()]); + setNewGalleryItem(""); + } + }; + + // Remove gallery item + const handleRemoveGalleryItem = (idx: number) => { + setFormGallery(formGallery.filter((_, i) => i !== idx)); + }; + + // ── BLOG HANDLERS ── + + // Open modal for blog editing + const handleStartEditPost = (post: BlogPost) => { + setEditingPost(post); + setIsAddingNewPost(false); + setBlogFormSubTab("genel"); + setFormBlogSlug(post.slug); + setFormBlogDate(post.date); + setFormBlogAuthor(post.author); + setFormBlogAuthorRole(post.authorRole); + setFormBlogImage(post.image); + + setFormBlogTrTitle(post.tr.title); + setFormBlogTrExcerpt(post.tr.excerpt); + setFormBlogTrReadingTime(post.tr.readingTime); + setFormBlogTrCategory(post.tr.category); + setFormBlogTrTags(post.tr.tags.join(", ")); + setFormBlogTrContent(post.tr.content); + + setFormBlogEnTitle(post.en.title); + setFormBlogEnExcerpt(post.en.excerpt); + setFormBlogEnReadingTime(post.en.readingTime); + setFormBlogEnCategory(post.en.category); + setFormBlogEnTags(post.en.tags.join(", ")); + setFormBlogEnContent(post.en.content); + }; + + // Open modal for blog adding + const handleStartAddPost = () => { + setEditingPost(null); + setIsAddingNewPost(true); + setBlogFormSubTab("genel"); + setFormBlogSlug(""); + setFormBlogDate(new Date().toISOString().split('T')[0]); + setFormBlogAuthor("Mustafa Yıldız"); + setFormBlogAuthorRole("Founder & Architect"); + setFormBlogImage(""); + + setFormBlogTrTitle(""); + setFormBlogTrExcerpt(""); + setFormBlogTrReadingTime("5 dk okuma"); + setFormBlogTrCategory("Teknoloji"); + setFormBlogTrTags("AI, Web"); + setFormBlogTrContent(""); + + setFormBlogEnTitle(""); + setFormBlogEnExcerpt(""); + setFormBlogEnReadingTime("5 min read"); + setFormBlogEnCategory("Technology"); + setFormBlogEnTags("AI, Web"); + setFormBlogEnContent(""); + }; + + // Save Blog Post + const handleSavePost = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formBlogTrTitle.trim()) { + showToast("Türkçe blog başlığı zorunludur!", "alert"); + return; + } + + const calculatedSlug = formBlogSlug.trim() || + formBlogTrTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); + + const blogPostDataToSave = { + id: editingPost?.id, + slug: calculatedSlug, + date: formBlogDate || new Date().toISOString().split('T')[0], + author: formBlogAuthor || "Ayris Council", + authorRole: formBlogAuthorRole || "Chief Engineer", + image: formBlogImage || "https://images.unsplash.com/photo-1504384308090-c894fdcc538d?auto=format&fit=crop&w=1200&q=80", + trTitle: formBlogTrTitle, + trExcerpt: formBlogTrExcerpt, + trReadingTime: formBlogTrReadingTime || "5 dk okuma", + trCategory: formBlogTrCategory || "Teknoloji", + trTags: formBlogTrTags.split(",").map(t => t.trim()).filter(Boolean), + trContent: formBlogTrContent || "## Yeni Teknik Blok Başlığı\nİçerik buraya gelecektir.", + enTitle: formBlogEnTitle || formBlogTrTitle, + enExcerpt: formBlogEnExcerpt || formBlogTrExcerpt, + enReadingTime: formBlogEnReadingTime || "5 min read", + enCategory: formBlogEnCategory || "Technology", + enTags: formBlogEnTags.split(",").map(t => t.trim()).filter(Boolean), + enContent: formBlogEnContent || "## New Technical Article\nContent goes here.", + }; + + const res = await saveBlogPost(blogPostDataToSave); + if (res.success && res.post) { + const mappedPost: BlogPost = { + id: res.post.id, + slug: res.post.slug, + date: res.post.date, + author: res.post.author, + authorRole: res.post.authorRole, + image: res.post.image, + tr: { + title: res.post.trTitle, + excerpt: res.post.trExcerpt, + readingTime: res.post.trReadingTime, + category: res.post.trCategory, + tags: res.post.trTags, + content: res.post.trContent, + }, + en: { + title: res.post.enTitle, + excerpt: res.post.enExcerpt, + readingTime: res.post.enReadingTime, + category: res.post.enCategory, + tags: res.post.enTags, + content: res.post.enContent, + } + }; + + if (isAddingNewPost) { + setPosts([...posts, mappedPost]); + showToast(`[ BLOG YAZISI EKLEDİ // ${mappedPost.tr.title.toUpperCase()} ]`, "success"); + setIsAddingNewPost(false); + } else { + setPosts(posts.map(p => p.id === editingPost?.id ? mappedPost : p)); + showToast(`[ BLOG YAZISI GÜNCELLEDİ // ${mappedPost.tr.title.toUpperCase()} ]`, "success"); + setEditingPost(null); + } + } else { + showToast(`Hata: ${res.error}`, "alert"); + } + }; + + // Delete Blog Post + const handleDeletePost = async (id: number, title: string) => { + if (confirm(`"${title}" blog yazısını silmek istediğinize emin misiniz?`)) { + const res = await deleteBlogPost(id); + if (res.success) { + setPosts(posts.filter(p => p.id !== id)); + showToast(`[ BLOG YAZISI SİLİNDİ // ${title.toUpperCase()} ]`, "alert"); + } else { + showToast(`Hata: ${res.error}`, "alert"); + } + } + }; + + // ── PARTNER CRUD ACTIONS ── + + const handleStartEditPartner = (p: PartnerData) => { + setEditingPartner(p); + setIsAddingNewPartner(false); + setFormPartnerName(p.name); + setFormPartnerTag(p.tag); + setFormPartnerMono(p.mono); + setFormPartnerYear(p.year); + setFormPartnerDesc(p.desc); + }; + + const handleStartAddPartner = () => { + setEditingPartner(null); + setIsAddingNewPartner(true); + setFormPartnerName(""); + setFormPartnerTag(""); + setFormPartnerMono(""); + setFormPartnerYear("2026"); + setFormPartnerDesc(""); + }; + + const handleSavePartner = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formPartnerName.trim() || !formPartnerMono.trim()) { + showToast("Partner ismi ve monogramı zorunludur!", "alert"); + return; + } + + const partnerDataToSave = { + id: editingPartner?.id, + name: formPartnerName.trim(), + tag: formPartnerTag.trim() || "Web3", + mono: formPartnerMono.trim().toUpperCase(), + year: formPartnerYear.trim() || "2026", + desc: formPartnerDesc.trim(), + }; + + const res = await savePartner(partnerDataToSave); + if (res.success && res.partner) { + if (isAddingNewPartner) { + setPartners([...partners, { ...partnerDataToSave, id: res.partner.id }]); + showToast(`[ YENİ PARTNER YARATILDI // ${formPartnerName.toUpperCase()} ]`, "success"); + setIsAddingNewPartner(false); + } else { + const updatedList = partners.map(p => { + if (p.id === editingPartner?.id) { + return { + ...p, + ...partnerDataToSave, + }; + } + return p; + }); + setPartners(updatedList); + showToast(`[ PARTNER BAŞARIYLA DÜZENLENDİ // ${formPartnerName.toUpperCase()} ]`, "success"); + setEditingPartner(null); + } + } else { + showToast(`Hata: ${res.error}`, "alert"); + } + }; + + const handleDeletePartner = async (id: number, name: string) => { + if (confirm(`"${name}" iş ortağını silmek istediğinize emin misiniz?`)) { + const res = await deletePartner(id); + if (res.success) { + setPartners(partners.filter(p => p.id !== id)); + showToast(`[ PARTNER SİLİNDİ // ${name.toUpperCase()} ]`, "alert"); + } else { + showToast(`Hata: ${res.error}`, "alert"); + } + } + }; + + // Save Settings Form + const handleSaveSettings = (e: React.FormEvent) => { + e.preventDefault(); + showToast("[ SİTE AYARLARI BAŞARIYLA KAYDEDİLDİ VE YENİLENDİ ]", "success"); + }; + + // Logout Handler + const handleLogout = async () => { + if (confirm("Sistemden çıkış yapmak istediğinize emin misiniz?")) { + const res = await adminLogoutAction(); + if (res.success) { + window.location.href = `/${lang}/admin/login`; + } else { + showToast("Çıkış yapılırken bir hata oluştu!", "alert"); + } + } + }; + + // Filters + const filteredProjects = projects.filter(p => + p.title.toLowerCase().includes(searchQuery.toLowerCase()) || + p.tag.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const filteredPosts = posts.filter(post => + post.tr.title.toLowerCase().includes(blogSearchQuery.toLowerCase()) || + post.author.toLowerCase().includes(blogSearchQuery.toLowerCase()) || + post.tr.category.toLowerCase().includes(blogSearchQuery.toLowerCase()) + ); + + const filteredPartners = partners.filter(p => + p.name.toLowerCase().includes(partnerSearchQuery.toLowerCase()) || + p.tag.toLowerCase().includes(partnerSearchQuery.toLowerCase()) || + p.mono.toLowerCase().includes(partnerSearchQuery.toLowerCase()) + ); + + return ( +
+
+ +
+ + {/* LEFT NAV PANEL - Brutalist Sidebar */} + + + {/* RIGHT MAIN PANEL - Dashboard workspace */} +
+ + + {/* TAB 1: PROJELER */} + {activeTab === "projeler" && ( + +
+
+ setSearchQuery(e.target.value)} + className="w-full font-mono text-[11px] bg-[#F4F0E8] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 placeholder-[#A0998E] text-[#0A0A0A] transition-colors" + /> +
+ +
+ +
+ {filteredProjects.map((p) => ( +
+
+
+
+ + PROJE // 0{p.num} + + + {p.tag} + +
+

+ {p.title} +

+
+ {p.tech.map((t) => ( + + {t} + + ))} +
+
+ +
+ + +
+
+ ))} + + {filteredProjects.length === 0 && ( +
+

[ HİÇBİR PROJE BULUNAMADI ]

+
+ )} +
+ + )} + + {/* TAB 2: BLOG YAZILARI YÖNETİMİ */} + {activeTab === "blog" && ( + +
+
+ setBlogSearchQuery(e.target.value)} + className="w-full font-mono text-[11px] bg-[#F4F0E8] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 placeholder-[#A0998E] text-[#0A0A0A] transition-colors" + /> +
+ +
+ +
+ {filteredPosts.map((post) => ( +
+
+
+
+ + SLUG: {post.slug} + + + {post.tr.category} + +
+

+ {post.tr.title} +

+

+ Yazar: {post.author} ({post.authorRole}) • Tarih: {post.date} +

+
+ +
+ + +
+
+ ))} + + {filteredPosts.length === 0 && ( +
+

[ HİÇBİR BLOG YAZISI BULUNAMADI ]

+
+ )} +
+ + )} + + {/* TAB 2.5: PARTNERLER YÖNETİMİ */} + {activeTab === "partnerler" && ( + +
+
+ setPartnerSearchQuery(e.target.value)} + className="w-full font-mono text-[11px] bg-[#F4F0E8] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 placeholder-[#A0998E] text-[#0A0A0A] transition-colors" + /> +
+ +
+ +
+ {filteredPartners.map((p) => ( +
+
+
+
+ + MONOGRAM: {p.mono} + + + {p.tag} + + + YIL: {p.year} + +
+

+ {p.name} +

+

+ {p.desc} +

+
+ +
+ + +
+
+ ))} + + {filteredPartners.length === 0 && ( +
+

[ HİÇBİR PARTNER BULUNAMADI ]

+
+ )} +
+ + )} + + {/* TAB 3: SITE AYARLARI */} + {activeTab === "ayarlar" && ( + +
+

⚙️ SİTE GENEL YAPILANDIRMASI

+

Görsel kimlik, başlıklar ve iletişim kanallarının yönetimi.

+
+ +
+
+
+ + setBrandName(e.target.value)} + className="w-full font-mono text-[12px] bg-[#EDE8E0] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 text-[#0A0A0A]" + /> +
+
+ + setBrandSub(e.target.value)} + className="w-full font-mono text-[12px] bg-[#EDE8E0] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 text-[#0A0A0A]" + /> +
+
+ +
+ + setHeroBadge(e.target.value)} + className="w-full font-mono text-[12px] bg-[#EDE8E0] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 text-[#0A0A0A]" + /> +
+ +
+
+ + setHeroTitle1(e.target.value)} + className="w-full font-mono text-[12px] bg-[#EDE8E0] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 text-[#0A0A0A]" + /> +
+
+ + setHeroTitle2(e.target.value)} + className="w-full font-mono text-[12px] bg-[#EDE8E0] border border-[#C8C2B8] focus:border-[#0A0A0A] outline-none px-4 py-3 text-[#0A0A0A]" + /> +
+
+ +
+ +