From 5a48605c356db5311148c4fd38e05b5ee4f91b97 Mon Sep 17 00:00:00 2001 From: ayrisdev Date: Fri, 17 Apr 2026 11:16:00 +0300 Subject: [PATCH] db connect --- Dockerfile | 15 +- app/HomePageClient.tsx | 102 ++ app/about/page.tsx | 28 +- app/admin/api/login/route.ts | 29 + app/admin/layout.tsx | 97 ++ app/admin/login/page.tsx | 109 ++ app/admin/page.tsx | 5 + app/admin/projects/ProjectsClient.tsx | 298 +++++ app/admin/projects/actions.ts | 122 ++ app/admin/projects/page.tsx | 13 + app/page.tsx | 118 +- app/projects/ProjectsPageClient.tsx | 88 ++ app/projects/[slug]/ProjectDetailClient.tsx | 108 ++ app/projects/[slug]/page.tsx | 103 +- app/projects/page.tsx | 98 +- components/LayoutContent.tsx | 5 + lib/cloudinary.ts | 31 + lib/prisma.ts | 20 + middleware.ts | 28 + next.config.ts | 10 + package-lock.json | 1334 ++++++++++++++++++- package.json | 7 + prisma.config.ts | 11 + prisma/schema.prisma | 28 + prisma/seed.js | 98 ++ 25 files changed, 2607 insertions(+), 298 deletions(-) create mode 100644 app/HomePageClient.tsx create mode 100644 app/admin/api/login/route.ts create mode 100644 app/admin/layout.tsx create mode 100644 app/admin/login/page.tsx create mode 100644 app/admin/page.tsx create mode 100644 app/admin/projects/ProjectsClient.tsx create mode 100644 app/admin/projects/actions.ts create mode 100644 app/admin/projects/page.tsx create mode 100644 app/projects/ProjectsPageClient.tsx create mode 100644 app/projects/[slug]/ProjectDetailClient.tsx create mode 100644 lib/cloudinary.ts create mode 100644 lib/prisma.ts create mode 100644 middleware.ts create mode 100644 prisma.config.ts create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js diff --git a/Dockerfile b/Dockerfile index 288e979..1979a4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app -# Install dependencies based on the preferred package manager +# Install dependencies COPY package.json package-lock.json* ./ RUN npm ci --legacy-peer-deps @@ -17,8 +17,10 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . +# Prisma generate step - CRITICAL for standalone mode +RUN npx prisma generate + # 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 RUN npm run build @@ -40,16 +42,21 @@ 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 +# We might need the prisma schema for runtime tasks if used, +# but for standalone, the client is already bundled. +# However, adding it doesn't hurt for 'db push' tasks in Coolify. +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" +# Note: In Coolify, you can run 'npx prisma db push' as a post-deployment script +# or change CMD to a wrapper script that runs db push then starts the server. CMD ["node", "server.js"] diff --git a/app/HomePageClient.tsx b/app/HomePageClient.tsx new file mode 100644 index 0000000..d6085c5 --- /dev/null +++ b/app/HomePageClient.tsx @@ -0,0 +1,102 @@ +'use client' + +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import ProjectSlider from '@/components/ProjectSlider' + +export default function HomePageClient({ initialProjects }: { initialProjects: any[] }) { + const [isRevealed, setIsRevealed] = useState(false) + const [isAtCorners, setIsAtCorners] = useState(false) + + useEffect(() => { + const reveal = setTimeout(() => setIsRevealed(true), 500) + const move = setTimeout(() => setIsAtCorners(true), 2500) + + return () => { + clearTimeout(reveal) + clearTimeout(move) + } + }, []) + + const transition = { duration: 1.8, ease: [0.76, 0, 0.24, 1] as const } + + return ( +
+ +
+
+ + + + + AYÇA NUR TURHAN + + + + A.N.T + + + + + + + ARCHITECTURE + + + +
+
+ + + {isAtCorners && ( + + + + )} + + +
+ ) +} diff --git a/app/about/page.tsx b/app/about/page.tsx index 9b6b72d..6326546 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -201,20 +201,38 @@ export default function AboutPage() { function ServiceCard({ service, index, scrollYProgress }: { service: any, index: number, scrollYProgress: any }) { const initialOffsets = [0, 200, 400] const ranges: [number, number][] = [ - [0, 0.01], - [0, 0.50], - [0.15, 0.85], + [0, 0.40], + [0.10, 0.60], + [0.20, 0.80], ] + const x = useTransform( + scrollYProgress, + ranges[index], + [index === 0 ? -300 : index === 2 ? 300 : 0, 0] + ) + const y = useTransform( scrollYProgress, ranges[index], - [initialOffsets[index], 0] + [index === 1 ? 400 : 200, 0] + ) + + const opacity = useTransform( + scrollYProgress, + ranges[index], + [0, 1] + ) + + const scale = useTransform( + scrollYProgress, + ranges[index], + [0.8, 1] ) return (
diff --git a/app/admin/api/login/route.ts b/app/admin/api/login/route.ts new file mode 100644 index 0000000..47bf6a1 --- /dev/null +++ b/app/admin/api/login/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +export async function POST(request: Request) { + try { + const { username, password } = await request.json(); + + const envUser = process.env.ADMIN_USER; + const envPass = process.env.ADMIN_PASS; + + if (username === envUser && password === envPass) { + // Set the session cookie + const cookieStore = await cookies(); + cookieStore.set('admin_session', 'authenticated', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24, // 24 hours + path: '/', + }); + + return NextResponse.json({ message: 'Login successful' }, { status: 200 }); + } + + return NextResponse.json({ message: 'Invalid credentials' }, { status: 401 }); + } catch (error) { + return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..d01aa49 --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,97 @@ +"use client"; + +import React from 'react'; +import { motion } from 'framer-motion'; +import Link from 'next/link'; +import { usePathname, useRouter } from 'next/navigation'; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + + if (pathname === '/admin/login') { + return <>{children}; + } + + const handleLogout = async () => { + document.cookie = "admin_session=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + router.push('/admin/login'); + router.refresh(); + }; + + const navItems = [ + { name: 'Projeler', href: '/admin/projects' }, + ]; + + return ( +
+ {/* Sidebar */} + +
+
A
+ AYCANUR Admin +
+ + + +
+

Sistem Durumu

+
+
+

Tüm sistemler aktif

+
+
+ + + {/* Main Content Area */} +
+
+
+
+
+ +
+ {children} +
+
+ + +
+ ); +} diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx new file mode 100644 index 0000000..6241f56 --- /dev/null +++ b/app/admin/login/page.tsx @@ -0,0 +1,109 @@ +"use client"; + +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { useRouter } from 'next/navigation'; + +export default function LoginPage() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + const res = await fetch('/admin/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + + if (res.ok) { + router.push('/admin/projects'); + router.refresh(); + } else { + setError('Geçersiz kullanıcı adı veya şifre.'); + } + } catch (err) { + setError('Bir hata oluştu. Lütfen tekrar deneyin.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+ + +
+
+
A
+

Hoş Geldiniz

+

Yönetim Paneli Girişi

+
+ +
+
+ + setUsername(e.target.value)} + className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 text-white placeholder:text-gray-700 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 transition-all font-medium" + placeholder="isminiz..." + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 text-white placeholder:text-gray-700 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 transition-all font-medium" + placeholder="••••••••" + /> +
+ + {error && ( + + {error} + + )} + + +
+ +
+

SECURED BY ANT-ARCHITECT

+
+
+
+
+ ); +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..fbb32e0 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function AdminPage() { + redirect('/admin/projects'); +} diff --git a/app/admin/projects/ProjectsClient.tsx b/app/admin/projects/ProjectsClient.tsx new file mode 100644 index 0000000..8fdbcea --- /dev/null +++ b/app/admin/projects/ProjectsClient.tsx @@ -0,0 +1,298 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import Image from 'next/image'; +import { createProject, deleteProject, updateProject } from './actions'; + +interface Project { + id: number; + title: string; + year: string; + location: string; + image: string; + gallery: string[]; + category: string | null; + description: string | null; + slug: string; +} + +export default function ProjectsClient({ initialProjects }: { initialProjects: Project[] }) { + const [projects, setProjects] = useState(initialProjects); + const [isAdding, setIsAdding] = useState(false); + const [editingProject, setEditingProject] = useState(null); + const [currentCover, setCurrentCover] = useState(null); + const [currentGallery, setCurrentGallery] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + const filteredProjects = projects.filter(p => + p.title.toLowerCase().includes(searchQuery.toLowerCase()) || + p.location.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + useEffect(() => { + if (editingProject) { + setCurrentCover(editingProject.image || null); + setCurrentGallery(editingProject.gallery || []); + } else { + setCurrentCover(null); + setCurrentGallery([]); + } + }, [editingProject]); + + const handleDelete = async (id: number) => { + if (confirm('Emin misiniz? Bu işlem projeyi kalıcı olarak silecektir.')) { + const prevProjects = [...projects]; + setProjects(projects.filter(p => p.id !== id)); + try { + await deleteProject(id); + } catch (err) { + setProjects(prevProjects); + alert('Proje silinemedi'); + } + } + }; + + const removeGalleryImage = (url: string) => { + setCurrentGallery(prev => prev.filter(img => img !== url)); + }; + + const removeCoverImage = () => { + setCurrentCover(null); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + const formData = new FormData(e.currentTarget); + formData.append('existingCover', currentCover || ''); + formData.append('keepGallery', JSON.stringify(currentGallery)); + + try { + if (editingProject) { + await updateProject(editingProject.id, formData); + } else { + await createProject(formData); + } + setIsAdding(false); + setEditingProject(null); + window.location.reload(); + } catch (err) { + alert('Kaydedilemedi. Bağlantınızı veya dosya boyutlarını kontrol edin.'); + } finally { + setIsLoading(false); + } + }; + + const openEdit = (project: Project) => { + setEditingProject(project); + setIsAdding(true); + }; + + return ( + <> +
+
+

Proje Yönetimi

+

Veritabanı Bağlı // {projects.length} Toplam Kayıt

+
+ +
+ setSearchQuery(e.target.value)} + className="flex-1 md:w-64 bg-white/5 border border-white/10 rounded-2xl px-6 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 transition-all backdrop-blur-md" + /> + +
+
+ +
+ + {filteredProjects.map((project) => ( + +
+ {project.image ? ( + {project.title} + ) : ( +
GÖRSEL_YOK
+ )} +
+
+ {project.category} +
+
+ +
+
+

{project.title}

+ {project.year} +
+

{project.location}

+ +
+ + +
+
+ + ))} + +
+ + + {isAdding && ( +
+ !isLoading && (setIsAdding(false), setEditingProject(null))} + className="absolute inset-0 bg-black/90 backdrop-blur-sm" + /> + +

+ + {editingProject ? 'Projeyi Düzenle' : 'Yeni Proje Ekle'} +

+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +