From e927cd6af3a27754418cb3082a13114723be0b13 Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Sat, 30 May 2026 18:23:21 +0300 Subject: [PATCH] first commit --- .gitignore | 18 + Dockerfile | 63 + app/[lang]/[slug]/page.tsx | 42 + app/[lang]/admin/login/page.tsx | 173 ++ app/[lang]/admin/page.tsx | 99 + app/[lang]/blog/[slug]/page.tsx | 87 + app/[lang]/blog/page.tsx | 47 + app/[lang]/contact/page.tsx | 19 + app/[lang]/faq/page.tsx | 19 + app/[lang]/layout.tsx | 49 + app/[lang]/page.tsx | 19 + app/[lang]/partners/page.tsx | 25 + app/[lang]/process/page.tsx | 19 + app/[lang]/services/page.tsx | 19 + app/[lang]/work/[slug]/page.tsx | 73 + app/[lang]/work/page.tsx | 21 + app/actions.ts | 352 +++ app/favicon.ico | Bin 0 -> 15406 bytes app/globals.css | 145 + components/AdminClient.tsx | 1667 ++++++++++ components/BlogClient.tsx | 196 ++ components/BlogDetailClient.tsx | 192 ++ components/ContactClient.tsx | 141 + components/FAQClient.tsx | 101 + components/Footer.tsx | 117 + components/Header.tsx | 163 + components/HomeClient.tsx | 863 ++++++ components/PartnersClient.tsx | 93 + components/ProcessClient.tsx | 76 + components/ServicesClient.tsx | 144 + components/WorkClient.tsx | 148 + components/WorkDetailClient.tsx | 555 ++++ components/templates/DentalTemplate.tsx | 474 +++ components/templates/KlinikTemplate.tsx | 423 +++ components/templates/KurumsalTemplate.tsx | 335 ++ components/templates/RestoranTemplate.tsx | 508 +++ components/templates/RestoranTemplate2.tsx | 460 +++ components/ui/AnimatedCounter.tsx | 34 + data/blog.ts | 243 ++ data/demos.ts | 269 ++ dictionaries/en.json | 291 ++ dictionaries/tr.json | 319 ++ get-dictionary.ts | 11 + i18n-config.ts | 6 + lib/auth.ts | 50 + lib/prisma.ts | 25 + next-env.d.ts | 6 + next.config.mjs | 6 + package-lock.json | 3254 ++++++++++++++++++++ package.json | 33 + postcss.config.mjs | 7 + prisma.config.ts | 15 + prisma/schema.prisma | 67 + prisma/seed.ts | 258 ++ proxy.ts | 64 + tsconfig.json | 41 + tsconfig.tsbuildinfo | 1 + 57 files changed, 12945 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app/[lang]/[slug]/page.tsx create mode 100644 app/[lang]/admin/login/page.tsx create mode 100644 app/[lang]/admin/page.tsx create mode 100644 app/[lang]/blog/[slug]/page.tsx create mode 100644 app/[lang]/blog/page.tsx create mode 100644 app/[lang]/contact/page.tsx create mode 100644 app/[lang]/faq/page.tsx create mode 100644 app/[lang]/layout.tsx create mode 100644 app/[lang]/page.tsx create mode 100644 app/[lang]/partners/page.tsx create mode 100644 app/[lang]/process/page.tsx create mode 100644 app/[lang]/services/page.tsx create mode 100644 app/[lang]/work/[slug]/page.tsx create mode 100644 app/[lang]/work/page.tsx create mode 100644 app/actions.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 components/AdminClient.tsx create mode 100644 components/BlogClient.tsx create mode 100644 components/BlogDetailClient.tsx create mode 100644 components/ContactClient.tsx create mode 100644 components/FAQClient.tsx create mode 100644 components/Footer.tsx create mode 100644 components/Header.tsx create mode 100644 components/HomeClient.tsx create mode 100644 components/PartnersClient.tsx create mode 100644 components/ProcessClient.tsx create mode 100644 components/ServicesClient.tsx create mode 100644 components/WorkClient.tsx create mode 100644 components/WorkDetailClient.tsx create mode 100644 components/templates/DentalTemplate.tsx create mode 100644 components/templates/KlinikTemplate.tsx create mode 100644 components/templates/KurumsalTemplate.tsx create mode 100644 components/templates/RestoranTemplate.tsx create mode 100644 components/templates/RestoranTemplate2.tsx create mode 100644 components/ui/AnimatedCounter.tsx create mode 100644 data/blog.ts create mode 100644 data/demos.ts create mode 100644 dictionaries/en.json create mode 100644 dictionaries/tr.json create mode 100644 get-dictionary.ts create mode 100644 i18n-config.ts create mode 100644 lib/auth.ts create mode 100644 lib/prisma.ts create mode 100644 next-env.d.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma.config.ts create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100644 proxy.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.tsbuildinfo 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 0000000000000000000000000000000000000000..a0a329ea2a87bee572fd2a252d53a4f8ee77ac77 GIT binary patch literal 15406 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"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]" + /> +
+
+ +
+ +