feat: add multi-user admin panel and featured partners toggle on home page

This commit is contained in:
AyrisAI
2026-05-17 13:48:05 +03:00
parent 36e98a3883
commit 0504f12f5b
29 changed files with 1110 additions and 182 deletions

View File

@@ -8,7 +8,6 @@ import {
Instagram, Instagram,
TrendingUp TrendingUp
} from "lucide-react"; } from "lucide-react";
import Footer from "@/components/Footer";
import { Metadata } from "next"; import { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -259,8 +258,6 @@ export default function AboutPage() {
</div> </div>
</div> </div>
</section> </section>
<Footer />
</main> </main>
); );
} }

View File

@@ -1,6 +1,9 @@
'use server' 'use server'
import { cache } from 'react'; import { cache } from 'react';
import sql from '@/lib/db'; import sql from '@/lib/db';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
// Vercel Best Practice: server-cache-react - Deduplicate data fetching per request // Vercel Best Practice: server-cache-react - Deduplicate data fetching per request
export const getSettings = cache(async function() { export const getSettings = cache(async function() {
@@ -40,16 +43,58 @@ export async function submitLead(formData: {
projectType: string; projectType: string;
message: string; message: string;
}) { }) {
const fullName = `${formData.firstName} ${formData.lastName}`;
console.log(`[Lead] Submitting lead for ${fullName}...`);
try { try {
const fullName = `${formData.firstName} ${formData.lastName}`; // 1. Save to Database First
await sql` await sql`
INSERT INTO leads (full_name, email, service_type, message, status) INSERT INTO leads (full_name, email, service_type, message, status)
VALUES (${fullName}, ${formData.email}, ${formData.projectType}, ${formData.message}, 'new') VALUES (${fullName}, ${formData.email}, ${formData.projectType}, ${formData.message}, 'new')
`; `;
console.log(`[Lead] Successfully saved to database.`);
// 2. Try sending email in a separate block
try {
const settings = await getSettings();
const destEmail = settings?.contact_email || 'ayrisdev@gmail.com';
console.log(`[Lead] Attempting to send notification to ${destEmail}...`);
const emailResult = await resend.emails.send({
from: 'Muğla Dijital <onboarding@resend.dev>',
to: destEmail,
subject: `Yeni Mesaj: ${fullName} - ${formData.projectType}`,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee;">
<h2 style="color: #000; border-bottom: 2px solid #ff0000; padding-bottom: 10px;">Yeni İletişim Formu Mesajı</h2>
<p><strong>Gönderen:</strong> ${fullName}</p>
<p><strong>E-posta:</strong> ${formData.email}</p>
<p><strong>Hizmet:</strong> ${formData.projectType}</p>
<p><strong>Mesaj:</strong></p>
<div style="background: #f9f9f9; padding: 15px; border-radius: 5px;">
${formData.message.replace(/\n/g, '<br/>')}
</div>
<hr style="margin-top: 30px; border: 0; border-top: 1px solid #eee;" />
<p style="font-size: 12px; color: #999;">Bu mesaj Muğla Dijital web sitesi üzerinden gönderilmiştir.</p>
</div>
`
});
if (emailResult.error) {
console.error(`[Lead] Resend Error:`, emailResult.error);
} else {
console.log(`[Lead] Email sent successfully:`, emailResult.data?.id);
}
} catch (emailErr) {
// We don't want to fail the whole action if only email fails
console.error(`[Lead] Email sending failed but DB record was saved:`, emailErr);
}
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error submitting lead:', error); console.error('[Lead] Critical Error submitting lead:', error);
return { error: 'Failed to submit' }; return { error: 'Form gönderilirken bir hata oluştu. Lütfen teknik ekiple iletişime geçin.' };
} }
} }
export async function getPartners() { export async function getPartners() {
@@ -70,6 +115,44 @@ export async function getPartners() {
} }
} }
export async function getFeaturedPartners() {
try {
// Try to fetch featured partners (max 5)
const featured = await sql`
SELECT p.*, (
SELECT slug FROM projects
WHERE LOWER(TRIM(p.name)) LIKE '%' || LOWER(TRIM(title)) || '%'
OR LOWER(TRIM(title)) LIKE '%' || LOWER(TRIM(p.name)) || '%'
LIMIT 1
) as project_slug
FROM partners p
WHERE p.is_featured = true
ORDER BY p.display_order ASC
LIMIT 5
`;
if (featured.length > 0) {
return featured;
}
// Fallback to top 5 standard partners if none are marked featured
return await sql`
SELECT p.*, (
SELECT slug FROM projects
WHERE LOWER(TRIM(p.name)) LIKE '%' || LOWER(TRIM(title)) || '%'
OR LOWER(TRIM(title)) LIKE '%' || LOWER(TRIM(p.name)) || '%'
LIMIT 1
) as project_slug
FROM partners p
ORDER BY p.display_order ASC
LIMIT 5
`;
} catch (error) {
console.error('Error fetching featured partners:', error);
return [];
}
}
export const getProjectBySlug = cache(async function(slug: string) { export const getProjectBySlug = cache(async function(slug: string) {
try { try {
const projects = await sql`SELECT * FROM projects WHERE slug = ${slug} LIMIT 1`; const projects = await sql`SELECT * FROM projects WHERE slug = ${slug} LIMIT 1`;

View File

@@ -1,5 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { LayoutDashboard, Users, Briefcase, Settings, FileText, LogOut, Handshake } from 'lucide-react'; import { LayoutDashboard, Users, Briefcase, Settings, FileText, LogOut, Handshake, Shield } from 'lucide-react';
import { logout } from '../actions'; import { logout } from '../actions';
export default function AdminLayout({ children }: { children: React.ReactNode }) { export default function AdminLayout({ children }: { children: React.ReactNode }) {
@@ -37,6 +37,10 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
<Handshake className="w-5 h-5 text-white/60" /> <Handshake className="w-5 h-5 text-white/60" />
Partnerler Partnerler
</Link> </Link>
<Link href="/admin/users" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
<Shield className="w-5 h-5 text-white/60" />
Yöneticiler
</Link>
</nav> </nav>
<div className="mt-auto border-t border-white/10 pt-4"> <div className="mt-auto border-t border-white/10 pt-4">

View File

@@ -0,0 +1,156 @@
"use client";
import { useState } from "react";
import { Plus, X, Save, AlertCircle } from "lucide-react";
import { createPartnerAdmin } from "../../actions";
import { useRouter } from "next/navigation";
export default function AddPartnerModal() {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [name, setName] = useState("");
const [logo, setLogo] = useState<File | null>(null);
const [displayOrder, setDisplayOrder] = useState("0");
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!logo) {
setErrorMsg("Lütfen bir logo görseli seçin.");
return;
}
setIsLoading(true);
setErrorMsg("");
const formData = new FormData();
formData.append("name", name);
formData.append("logo", logo);
formData.append("display_order", displayOrder);
const res = await createPartnerAdmin(formData);
setIsLoading(false);
if (res.error) {
setErrorMsg(res.error);
} else {
// Success
setIsOpen(false);
setName("");
setLogo(null);
setDisplayOrder("0");
router.refresh();
}
};
return (
<>
<button
onClick={() => setIsOpen(true)}
className="bg-primary text-white px-6 py-2 rounded-xl font-bold hover:bg-primary/80 transition-colors flex items-center gap-2 cursor-pointer"
>
<Plus className="w-4 h-4" />
Yeni Partner Ekle
</button>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fadeIn">
<div className="relative w-full max-w-md bg-zinc-950 border border-white/10 rounded-3xl p-6 shadow-2xl space-y-6">
{/* Header */}
<div className="flex justify-between items-center pb-4 border-b border-white/10">
<h2 className="text-xl font-black uppercase tracking-widest text-white">Yeni Partner Ekle</h2>
<button
onClick={() => setIsOpen(false)}
className="text-white/40 hover:text-white transition-colors p-1"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Marka Adı</label>
<input
required
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Örn: ABC A.Ş."
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Marka Logosu (Görsel Seçin)</label>
<div className="relative flex flex-col items-center justify-center border border-dashed border-white/10 hover:border-primary/50 transition-colors rounded-xl p-4 bg-white/5 cursor-pointer min-h-[100px]">
<input
required={!logo}
type="file"
accept="image/*"
onChange={(e) => {
if (e.target.files && e.target.files[0]) {
setLogo(e.target.files[0]);
}
}}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
{logo ? (
<div className="text-center">
<p className="text-xs text-primary font-bold">{logo.name}</p>
<p className="text-[10px] text-white/40">{(logo.size / 1024).toFixed(1)} KB</p>
</div>
) : (
<div className="text-center space-y-1">
<p className="text-xs text-white/60 font-medium">Görsel seçmek için tıklayın</p>
<p className="text-[9px] text-white/30 uppercase tracking-wider">PNG, JPG veya SVG</p>
</div>
)}
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Görünüm Sırası</label>
<input
required
type="number"
value={displayOrder}
onChange={(e) => setDisplayOrder(e.target.value)}
placeholder="0"
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none"
/>
</div>
{errorMsg && (
<div className="flex items-center gap-2 text-red-500 text-xs font-bold bg-red-500/10 p-4 rounded-xl">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{errorMsg}</span>
</div>
)}
<div className="pt-4 flex gap-3">
<button
type="button"
onClick={() => setIsOpen(false)}
className="flex-1 border border-white/10 text-white font-bold py-3 px-6 rounded-xl hover:bg-white/5 transition-colors"
>
İptal
</button>
<button
type="submit"
disabled={isLoading}
className="flex-1 bg-primary text-white font-bold py-3 px-6 rounded-xl hover:bg-primary/80 transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
>
<Save className="w-4 h-4" />
{isLoading ? "Kaydediliyor..." : "Kaydet"}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Trash2, Save, Edit2, X } from "lucide-react"; import { Trash2, Save, Edit2, X, Star } from "lucide-react";
import { updatePartner } from "../../actions"; import { updatePartner, togglePartnerFeatured } from "../../actions";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Image from "next/image"; import Image from "next/image";
@@ -11,6 +11,7 @@ export default function PartnerRow({ partner, onDelete }: { partner: any, onDele
const [name, setName] = useState(partner.name); const [name, setName] = useState(partner.name);
const [displayOrder, setDisplayOrder] = useState(partner.display_order); const [displayOrder, setDisplayOrder] = useState(partner.display_order);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isFeatured, setIsFeatured] = useState(partner.is_featured);
const router = useRouter(); const router = useRouter();
const handleSave = async () => { const handleSave = async () => {
@@ -25,6 +26,18 @@ export default function PartnerRow({ partner, onDelete }: { partner: any, onDele
} }
}; };
const handleToggleFeatured = async () => {
const nextState = !isFeatured;
setIsFeatured(nextState);
const res = await togglePartnerFeatured(partner.id, nextState);
if (res.success) {
router.refresh();
} else {
setIsFeatured(!nextState);
alert("Öne çıkarma durumu güncellenemedi");
}
};
return ( return (
<tr className="hover:bg-white/[0.02] transition-colors border-b border-white/10 last:border-0"> <tr className="hover:bg-white/[0.02] transition-colors border-b border-white/10 last:border-0">
<td className="px-6 py-4"> <td className="px-6 py-4">
@@ -73,6 +86,19 @@ export default function PartnerRow({ partner, onDelete }: { partner: any, onDele
<div className="font-bold text-white tracking-wide">{partner.name}</div> <div className="font-bold text-white tracking-wide">{partner.name}</div>
)} )}
</td> </td>
<td className="px-6 py-4">
<button
onClick={handleToggleFeatured}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
isFeatured
? "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500/20"
: "bg-white/5 text-white/20 hover:text-white/60 hover:bg-white/10"
}`}
>
<Star className={`w-4 h-4 ${isFeatured ? "fill-yellow-500" : ""}`} />
{isFeatured ? "Öne Çıkarıldı" : "Öne Çıkar"}
</button>
</td>
<td className="px-6 py-4 text-right"> <td className="px-6 py-4 text-right">
<div className="flex justify-end items-center gap-2"> <div className="flex justify-end items-center gap-2">
{isEditing ? ( {isEditing ? (

View File

@@ -1,7 +1,7 @@
import { getPartnersAdmin, deletePartner } from '../../actions'; import { getPartnersAdmin, deletePartner } from '../../actions';
import { Plus } from 'lucide-react';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import PartnerRow from './PartnerRow'; import PartnerRow from './PartnerRow';
import AddPartnerModal from './AddPartnerModal';
export default async function PartnersAdminPage() { export default async function PartnersAdminPage() {
const partners = await getPartnersAdmin(); const partners = await getPartnersAdmin();
@@ -20,10 +20,7 @@ export default async function PartnersAdminPage() {
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Partnerler</h1> <h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Partnerler</h1>
<p className="text-white/40">Referans markaların listesi.</p> <p className="text-white/40">Referans markaların listesi.</p>
</div> </div>
<button className="bg-primary text-white px-6 py-2 rounded-xl font-bold hover:bg-primary/80 transition-colors flex items-center gap-2"> <AddPartnerModal />
<Plus className="w-4 h-4" />
Yeni Partner Ekle
</button>
</div> </div>
<div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden shadow-2xl"> <div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
@@ -33,6 +30,7 @@ export default async function PartnersAdminPage() {
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 w-24">Sıra</th> <th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 w-24">Sıra</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Logo Önizleme</th> <th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Logo Önizleme</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Marka Adı</th> <th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Marka Adı</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Durum</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 text-right">İşlemler</th> <th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 text-right">İşlemler</th>
</tr> </tr>
</thead> </thead>
@@ -42,7 +40,7 @@ export default async function PartnersAdminPage() {
))} ))}
{(!partners || partners.length === 0) && ( {(!partners || partners.length === 0) && (
<tr> <tr>
<td colSpan={4} className="px-6 py-8 text-center text-white/40"> <td colSpan={5} className="px-6 py-8 text-center text-white/40">
Henüz partner eklenmemiş. Henüz partner eklenmemiş.
</td> </td>
</tr> </tr>

View File

@@ -0,0 +1,119 @@
"use client";
import { useState } from "react";
import { Plus, X, Save, AlertCircle } from "lucide-react";
import { createAdminAdmin } from "../../actions";
import { useRouter } from "next/navigation";
export default function AddAdminModal() {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setErrorMsg("");
const formData = new FormData();
formData.append("username", username);
formData.append("password", password);
const res = await createAdminAdmin(formData);
setIsLoading(false);
if (res.error) {
setErrorMsg(res.error);
} else {
setIsOpen(false);
setUsername("");
setPassword("");
router.refresh();
}
};
return (
<>
<button
onClick={() => setIsOpen(true)}
className="bg-primary text-white px-6 py-2 rounded-xl font-bold hover:bg-primary/80 transition-colors flex items-center gap-2 cursor-pointer text-sm"
>
<Plus className="w-4 h-4" />
Yeni Yönetici Ekle
</button>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/85 backdrop-blur-sm animate-fadeIn">
<div className="relative w-full max-w-md bg-zinc-950 border border-white/10 rounded-3xl p-6 shadow-2xl space-y-6">
{/* Header */}
<div className="flex justify-between items-center pb-4 border-b border-white/10">
<h2 className="text-xl font-black uppercase tracking-widest text-white">Yeni Yönetici Ekle</h2>
<button
onClick={() => setIsOpen(false)}
className="text-white/40 hover:text-white transition-colors p-1"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Kullanıcı Adı</label>
<input
required
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Örn: ayrisdev"
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Geçici Şifre</label>
<input
required
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none"
/>
</div>
{errorMsg && (
<div className="flex items-center gap-2 text-red-500 text-xs font-bold bg-red-500/10 p-4 rounded-xl">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{errorMsg}</span>
</div>
)}
<div className="pt-4 flex gap-3">
<button
type="button"
onClick={() => setIsOpen(false)}
className="flex-1 border border-white/10 text-white font-bold py-3 px-6 rounded-xl hover:bg-white/5 transition-colors"
>
İptal
</button>
<button
type="submit"
disabled={isLoading}
className="flex-1 bg-primary text-white font-bold py-3 px-6 rounded-xl hover:bg-primary/80 transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
>
<Save className="w-4 h-4" />
{isLoading ? "Kaydediliyor..." : "Kaydet"}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,117 @@
"use client";
import { useState } from "react";
import { X, Save, AlertCircle, KeyRound, CheckCircle } from "lucide-react";
import { updateAdminPassword } from "../../actions";
import { useRouter } from "next/navigation";
export default function ChangePasswordModal({ admin }: { admin: { id: number; username: string } }) {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setErrorMsg("");
setSuccessMsg("");
const res = await updateAdminPassword(admin.id, password);
setIsLoading(false);
if (res.error) {
setErrorMsg(res.error);
} else {
setSuccessMsg("Şifre başarıyla güncellendi!");
setPassword("");
setTimeout(() => {
setIsOpen(false);
setSuccessMsg("");
router.refresh();
}, 1000);
}
};
return (
<>
<button
onClick={() => setIsOpen(true)}
className="p-2 bg-white/5 text-white/40 rounded-lg hover:bg-primary hover:text-white transition-all cursor-pointer"
title="Şifre Değiştir"
>
<KeyRound className="w-4.5 h-4.5" />
</button>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/85 backdrop-blur-sm animate-fadeIn">
<div className="relative w-full max-w-md bg-zinc-950 border border-white/10 rounded-3xl p-6 shadow-2xl space-y-6">
{/* Header */}
<div className="flex justify-between items-center pb-4 border-b border-white/10">
<div>
<h2 className="text-xl font-black uppercase tracking-widest text-white">Şifre Değiştir</h2>
<p className="text-xs text-white/40 mt-1">"{admin.username}" kullanıcısının şifresini güncelleyin.</p>
</div>
<button
onClick={() => setIsOpen(false)}
className="text-white/40 hover:text-white transition-colors p-1"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Yeni Şifre</label>
<input
required
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none"
/>
</div>
{errorMsg && (
<div className="flex items-center gap-2 text-red-500 text-xs font-bold bg-red-500/10 p-4 rounded-xl">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{errorMsg}</span>
</div>
)}
{successMsg && (
<div className="flex items-center gap-2 text-green-500 text-xs font-bold bg-green-500/10 p-4 rounded-xl">
<CheckCircle className="w-4 h-4 shrink-0" />
<span>{successMsg}</span>
</div>
)}
<div className="pt-4 flex gap-3">
<button
type="button"
onClick={() => setIsOpen(false)}
className="flex-1 border border-white/10 text-white font-bold py-3 px-6 rounded-xl hover:bg-white/5 transition-colors"
>
İptal
</button>
<button
type="submit"
disabled={isLoading}
className="flex-1 bg-primary text-white font-bold py-3 px-6 rounded-xl hover:bg-primary/80 transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
>
<Save className="w-4 h-4" />
{isLoading ? "Güncelleniyor..." : "Güncelle"}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,104 @@
import { getAdminsAdmin, deleteAdminAdmin, getCurrentAdmin } from '../../actions';
import { revalidatePath } from 'next/cache';
import { Shield, KeyRound, Trash2 } from 'lucide-react';
import AddAdminModal from './AddAdminModal';
import ChangePasswordModal from './ChangePasswordModal';
export default async function AdminsAdminPage() {
const admins = await getAdminsAdmin();
const currentAdmin = await getCurrentAdmin();
async function handleDelete(formData: FormData) {
'use server';
const id = Number(formData.get('id'));
const res = await deleteAdminAdmin(id);
if (res.error) {
// Usually we want to return feedback, but a simple redirect/refresh works
console.error(res.error);
}
revalidatePath('/admin/users');
}
return (
<div className="space-y-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Yöneticiler</h1>
<p className="text-white/40">Sistem yöneticileri ve erişim kontrolü.</p>
</div>
<AddAdminModal />
</div>
<div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-white/10 bg-white/[0.02]">
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">ID</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Kullanıcı Adı</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Oluşturulma Tarihi</th>
<th className="px-6 py-4 text-right text-[10px] font-bold uppercase tracking-widest text-white/40">İşlemler</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{admins.map((admin: any) => {
const isSelf = admin.username === currentAdmin;
return (
<tr key={admin.id} className="hover:bg-white/[0.01] transition-colors">
<td className="px-6 py-4 text-sm text-white/40 font-mono">#{admin.id}</td>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded-lg ${isSelf ? 'bg-primary/20 text-primary' : 'bg-white/5 text-white/60'}`}>
<Shield className="w-4 h-4" />
</div>
<div className="font-bold text-white tracking-wide">
{admin.username}
{isSelf && (
<span className="ml-2 bg-primary/20 text-primary text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded-md">
Siz
</span>
)}
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-white/60">
{new Date(admin.created_at).toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 text-right">
<div className="flex justify-end items-center gap-3">
{/* Change Password */}
<ChangePasswordModal admin={admin} />
{/* Delete Admin */}
{!isSelf && (
<form action={handleDelete} onSubmit={(e) => {
if (!confirm(`${admin.username} yöneticisini silmek istediğinize emin misiniz?`)) {
e.preventDefault();
}
}} className="inline">
<input type="hidden" name="id" value={admin.id} />
<button
type="submit"
className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500 hover:text-white transition-all cursor-pointer"
title="Yöneticiyi Sil"
>
<Trash2 className="w-4.5 h-4.5" />
</button>
</form>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -3,25 +3,84 @@
import sql from '@/lib/db'; import sql from '@/lib/db';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { uploadToCloudinary } from '@/lib/cloudinary';
import crypto from 'crypto';
function hashPassword(password: string): string {
return crypto.createHash('sha256').update(password).digest('hex');
}
export async function ensureAdminsTable() {
try {
await sql`
CREATE TABLE IF NOT EXISTS admins (
id SERIAL PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`;
const result = await sql`SELECT count(*) FROM admins`;
if (Number(result[0].count) === 0) {
const defaultPass = hashPassword(process.env.ADMIN_PASSWORD || 'admin123');
await sql`
INSERT INTO admins (username, password)
VALUES ('admin', ${defaultPass})
`;
console.log("Admins table seeded with default user 'admin'");
}
// Add is_featured column to partners table if it doesn't exist
await sql`
ALTER TABLE partners ADD COLUMN IF NOT EXISTS is_featured BOOLEAN DEFAULT false
`;
} catch (e) {
console.error("Error ensuring admins table:", e);
}
}
export async function login(prevState: any, formData: FormData) { export async function login(prevState: any, formData: FormData) {
const password = formData.get('password') as string; try {
if (password === process.env.ADMIN_PASSWORD) { await ensureAdminsTable();
const cookieStore = await cookies();
cookieStore.set('admin_session', 'authenticated', { const username = (formData.get('username') as string || 'admin').trim();
httpOnly: true, const password = formData.get('password') as string;
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 1 week const hashed = hashPassword(password);
path: '/',
}); const users = await sql`
redirect('/admin'); SELECT * FROM admins
WHERE username = ${username} AND password = ${hashed}
`;
if (users.length > 0) {
const cookieStore = await cookies();
cookieStore.set('admin_session', 'authenticated', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 1 week
path: '/',
});
cookieStore.set('admin_user', username, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7,
path: '/',
});
redirect('/admin');
}
return { error: 'Hatalı kullanıcı adı veya şifre' };
} catch (e: any) {
console.error("Login error:", e);
return { error: 'Giriş sırasında hata oluştu: ' + (e.message || '') };
} }
return { error: 'Hatalı şifre' };
} }
export async function logout() { export async function logout() {
const cookieStore = await cookies(); const cookieStore = await cookies();
cookieStore.delete('admin_session'); cookieStore.delete('admin_session');
cookieStore.delete('admin_user');
redirect('/admin/login'); redirect('/admin/login');
} }
@@ -231,12 +290,18 @@ export async function deletePartner(id: number) {
export async function createPartnerAdmin(formData: FormData) { export async function createPartnerAdmin(formData: FormData) {
try { try {
const name = formData.get('name') as string; const name = formData.get('name') as string;
const logo = formData.get('logo') as string; const logoFile = formData.get('logo') as File;
let logoUrl = '';
if (logoFile && logoFile.size > 0) {
logoUrl = await uploadToCloudinary(logoFile, 'partners');
}
const display_order = Number(formData.get('display_order')) || 0; const display_order = Number(formData.get('display_order')) || 0;
await sql` await sql`
INSERT INTO partners (name, logo, display_order) INSERT INTO partners (name, logo, display_order)
VALUES (${name}, ${logo}, ${display_order}) VALUES (${name}, ${logoUrl}, ${display_order})
`; `;
return { success: true }; return { success: true };
} catch (e: any) { } catch (e: any) {
@@ -258,3 +323,100 @@ export async function updatePartner(id: number, name: string, display_order: num
} }
} }
export async function getAdminsAdmin() {
try {
await ensureAdminsTable();
return await sql`SELECT id, username, created_at FROM admins ORDER BY id ASC`;
} catch (e) {
console.error("Error getting admins:", e);
return [];
}
}
export async function createAdminAdmin(formData: FormData) {
try {
await ensureAdminsTable();
const username = (formData.get('username') as string).trim();
const password = formData.get('password') as string;
if (!username || !password) {
return { error: 'Kullanıcı adı ve şifre gereklidir.' };
}
const hashed = hashPassword(password);
await sql`
INSERT INTO admins (username, password)
VALUES (${username}, ${hashed})
`;
return { success: true };
} catch (e: any) {
console.error('Error creating admin:', e);
if (e.message?.includes('unique constraint')) {
return { error: 'Bu kullanıcı adı zaten alınmış.' };
}
return { error: 'Ekleme başarısız: ' + (e.message || 'Bilinmeyen hata') };
}
}
export async function updateAdminPassword(id: number, newPassword: string) {
try {
await ensureAdminsTable();
if (!newPassword || newPassword.length < 4) {
return { error: 'Şifre en az 4 karakter olmalıdır.' };
}
const hashed = hashPassword(newPassword);
await sql`
UPDATE admins
SET password = ${hashed}
WHERE id = ${id}
`;
return { success: true };
} catch (e: any) {
console.error('Error updating admin password:', e);
return { error: 'Şifre güncellenemedi.' };
}
}
export async function deleteAdminAdmin(id: number) {
try {
await ensureAdminsTable();
// Prevent deleting the last remaining admin
const adminsCount = await sql`SELECT count(*) FROM admins`;
if (Number(adminsCount[0].count) <= 1) {
return { error: 'Sistemde en az bir yönetici bulunmalıdır. Son yönetici silinemez!' };
}
await sql`
DELETE FROM admins
WHERE id = ${id}
`;
return { success: true };
} catch (e) {
console.error('Error deleting admin:', e);
return { error: 'Silinemedi.' };
}
}
export async function getCurrentAdmin() {
const cookieStore = await cookies();
return cookieStore.get('admin_user')?.value || 'admin';
}
export async function togglePartnerFeatured(id: number, isFeatured: boolean) {
try {
await sql`
UPDATE partners
SET is_featured = ${isFeatured}
WHERE id = ${id}
`;
return { success: true };
} catch (e) {
console.error('Error toggling partner featured:', e);
return { error: 'Öne çıkarma durumu güncellenemedi' };
}
}

View File

@@ -20,6 +20,18 @@ export default function LoginPage() {
</div> </div>
<form action={formAction} className="space-y-6"> <form action={formAction} className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Kullanıcı Adı</label>
<input
type="text"
name="username"
defaultValue="admin"
required
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder:text-white/20 focus:border-[#1e9a83] focus:ring-1 focus:ring-[#1e9a83] outline-none transition-all"
placeholder="örn: admin"
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Şifre</label> <label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Şifre</label>
<input <input

View File

@@ -1,5 +1,3 @@
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import Contact from "@/components/Contact"; import Contact from "@/components/Contact";
import { Metadata } from "next"; import { Metadata } from "next";
@@ -20,7 +18,6 @@ export default function ContactPage() {
return ( return (
<main className="min-h-screen bg-[#f5f5f0] text-black pt-12"> <main className="min-h-screen bg-[#f5f5f0] text-black pt-12">
<Contact /> <Contact />
<Footer />
</main> </main>
); );
} }

View File

@@ -16,11 +16,13 @@
--border: rgba(0, 0, 0, 0.1); --border: rgba(0, 0, 0, 0.1);
} }
html,
body { body {
max-width: 100vw;
overflow-x: hidden;
background-color: var(--background); background-color: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: var(--font-martian), monospace; font-family: var(--font-martian), monospace;
overflow-x: hidden;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }

View File

@@ -1,9 +1,7 @@
import Navbar from "@/components/Navbar";
import Hero from "@/components/Hero"; import Hero from "@/components/Hero";
import SelectedWorks from "@/components/SelectedWorks";
import Partners from "@/components/Partners"; import Partners from "@/components/Partners";
import ServicesGrid from "@/components/ServicesGrid"; import ServicesGrid from "@/components/ServicesGrid";
import Footer from "@/components/Footer"; import SelectedWorks from "@/components/SelectedWorks";
export default function Home() { export default function Home() {
return ( return (
@@ -12,7 +10,6 @@ export default function Home() {
<Partners /> <Partners />
<ServicesGrid /> <ServicesGrid />
<SelectedWorks /> <SelectedWorks />
<Footer />
</main> </main>
); );
} }

View File

@@ -1,6 +1,4 @@
import { getPartners } from "@/app/actions"; import { getPartners } from "@/app/actions";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { Metadata } from "next"; import { Metadata } from "next";
import PartnersList from "@/components/PartnersList"; import PartnersList from "@/components/PartnersList";
@@ -22,7 +20,6 @@ export default async function PartnersPage() {
return ( return (
<main className="min-h-screen bg-[#f5f5f0] text-black pt-24"> <main className="min-h-screen bg-[#f5f5f0] text-black pt-24">
<Navbar />
<section className="pt-24 pb-16 px-6 md:px-12 border-b border-black/10"> <section className="pt-24 pb-16 px-6 md:px-12 border-b border-black/10">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
@@ -50,8 +47,6 @@ export default async function PartnersPage() {
</a> </a>
</div> </div>
</section> </section>
<Footer />
</main> </main>
); );
} }

View File

@@ -1,6 +1,4 @@
import { getServiceBySlug, getLocationBySlug, getProjectsByService, getSettings } from "@/app/actions"; import { getServiceBySlug, getLocationBySlug, getProjectsByService, getSettings } from "@/app/actions";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight, CheckCircle2 } from "lucide-react"; import { ArrowRight, CheckCircle2 } from "lucide-react";
@@ -121,7 +119,6 @@ export default async function ServiceLocationPage({ params }: Props) {
return ( return (
<main className="min-h-screen bg-[#f5f5f0] text-black pt-24"> <main className="min-h-screen bg-[#f5f5f0] text-black pt-24">
<Navbar />
<script <script
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }}
@@ -245,8 +242,6 @@ export default async function ServiceLocationPage({ params }: Props) {
</Link> </Link>
</div> </div>
</section> </section>
<Footer />
</main> </main>
); );
} }

View File

@@ -40,8 +40,8 @@ export default function Capabilities() {
<section className="py-16 md:py-24 px-6 md:px-16 lg:px-24"> <section className="py-16 md:py-24 px-6 md:px-16 lg:px-24">
<div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-8 md:gap-16 items-start"> <div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-8 md:gap-16 items-start">
{/* Left Content */} {/* Left Content */}
<div className="lg:w-1/3"> <div className="lg:w-1/3 w-full">
<h2 className="text-4xl md:text-5xl font-bold mb-6 text-white italic tracking-tighter uppercase">Yeteneklerimiz</h2> <h2 className="text-3xl md:text-5xl font-bold mb-6 text-black italic tracking-tighter uppercase leading-tight">Yeteneklerimiz</h2>
<p className="text-white/60 text-lg mb-8 leading-relaxed"> <p className="text-white/60 text-lg mb-8 leading-relaxed">
Fikir aşamasından final renk düzenlemesine kadar, görsel üretim sürecinin her adımını takıntılı derecede yüksek standartlarla yönetiyoruz. Fikir aşamasından final renk düzenlemesine kadar, görsel üretim sürecinin her adımını takıntılı derecede yüksek standartlarla yönetiyoruz.
</p> </p>

View File

@@ -3,6 +3,7 @@
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import Footer from "./Footer";
export default function ClientLayout({ children }: { children: React.ReactNode }) { export default function ClientLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
@@ -22,6 +23,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
{children} {children}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
{!isAdmin && <Footer />}
</> </>
); );
} }

View File

@@ -13,9 +13,9 @@ export default function Hero() {
initial={{ y: 40, opacity: 0 }} initial={{ y: 40, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }} transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }}
className="editorial-headline text-4xl md:text-6xl lg:text-[5.5rem] text-black uppercase" className="editorial-headline text-[2.5rem] sm:text-5xl md:text-6xl lg:text-[5.5rem] text-black uppercase leading-[1.1] break-words"
> >
Dijital Varlık ve Görsel<br /> Dijital Varlık <span className="hidden sm:inline">ve Görsel</span><br className="hidden sm:block" />
Hikaye <span className="text-primary">Mimarlığı.</span> Hikaye <span className="text-primary">Mimarlığı.</span>
</motion.h1> </motion.h1>

View File

@@ -1,84 +1,155 @@
"use client"; "use client";
import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import DynamicLogo from "./DynamicLogo"; import DynamicLogo from "./DynamicLogo";
import { motion } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
const NAV_LINKS = [
{ label: "Ana Sayfa", href: "/" },
{ label: "Çalışmalar", href: "/works" },
{ label: "Hizmetler", href: "/services" },
{ label: "Partnerler", href: "/partners" },
{ label: "Hakkımızda", href: "/about" },
{ label: "İletişim", href: "/contact" },
];
export default function Navbar() { export default function Navbar() {
const [isOpen, setIsOpen] = useState(false);
// Body scroll lock
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen]);
return ( return (
<motion.nav <>
initial={{ y: -100 }} <motion.nav
animate={{ y: 0 }} initial={{ y: -100 }}
transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }} animate={{ y: 0 }}
className="fixed top-0 left-0 w-full z-50 bg-[#f5f5f0]/90 backdrop-blur-md border-b border-black/10" transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }}
> className="fixed top-0 left-0 w-full z-50 bg-[#f5f5f0]/90 backdrop-blur-md border-b border-black/10"
<div className="flex items-center justify-between px-6 md:px-12 py-5"> >
{/* Logo */} <div className=" flex items-center justify-between py-2 md:py-5">
<Link href="/" className="flex items-center gap-3 focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-4 rounded-sm" aria-label="Muğla Dijital - Ana Sayfaya Dön"> {/* Logo */}
<motion.div <Link
initial={{ opacity: 0 }} href="/"
animate={{ opacity: 1 }} className="flex items-center gap-3 focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-4 rounded-sm z-50"
transition={{ delay: 0.5 }} aria-label="Muğla Dijital - Ana Sayfaya Dön"
className="relative w-36 h-10" onClick={() => setIsOpen(false)}
> >
<DynamicLogo <motion.div
src="/logo.png" initial={{ opacity: 0 }}
color="black" animate={{ opacity: 1 }}
width="100%" transition={{ delay: 0.5 }}
height="100%" className="relative md:w-36 w-18 h-7 md:h-10"
size="contain" >
/> <DynamicLogo
</motion.div> src="/logo.png"
</Link> color="black"
width="100%"
height="100%"
size="contain"
/>
</motion.div>
</Link>
{/* Center Nav */} {/* Center Nav - Desktop */}
<div className="hidden lg:flex items-center"> <div className="hidden lg:flex items-center">
{[ {NAV_LINKS.map((item, idx) => (
{ label: "Çalışmalar", href: "/works" }, <div key={item.label} className="flex items-center">
{ label: "Hizmetler", href: "/services" }, {/* Diagonal separator */}
{ label: "Partnerler", href: "/partners" }, <motion.div
{ label: "Hakkımızda", href: "/about" }, initial={{ opacity: 0, scaleY: 0 }}
{ label: "İletişim", href: "/contact" }, animate={{ opacity: 1, scaleY: 1 }}
].map((item, idx) => ( transition={{ delay: 0.2 + idx * 0.1 }}
<div key={item.label} className="flex items-center"> className="w-16 h-10 relative mx-1"
{/* Diagonal separator */}
<motion.div
initial={{ opacity: 0, scaleY: 0 }}
animate={{ opacity: 1, scaleY: 1 }}
transition={{ delay: 0.2 + idx * 0.1 }}
className="w-16 h-10 relative mx-1"
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-px h-14 bg-black/10 rotate-[-25deg]" />
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + idx * 0.1 }}
>
<Link
href={item.href}
className="text-[11px] tracking-[0.15em] uppercase text-black/70 hover:text-black transition-colors nav-link-hover px-3 py-2 focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-4 rounded-sm"
> >
{item.label} <div className="absolute inset-0 flex items-center justify-center">
</Link> <div className="w-px h-14 bg-black/10 rotate-[-25deg]" />
</motion.div> </div>
</div> </motion.div>
))} <motion.div
</div> initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + idx * 0.1 }}
>
<Link
href={item.href}
className="text-[11px] tracking-[0.15em] uppercase text-black/70 hover:text-black transition-colors nav-link-hover px-3 py-2 focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-4 rounded-sm"
>
{item.label}
</Link>
</motion.div>
</div>
))}
</div>
{/* Right - Brand name */} {/* Right - Mobile Toggle & Brand */}
<motion.span <div className="flex items-center gap-0 md:gap-6">
initial={{ opacity: 0 }} <motion.span
animate={{ opacity: 1 }} initial={{ opacity: 0 }}
transition={{ delay: 1 }} animate={{ opacity: 1 }}
className="text-[13px] font-bold tracking-[0.1em] uppercase text-black" transition={{ delay: 1 }}
aria-hidden="true" className="hidden md:block text-[13px] font-bold tracking-[0.1em] uppercase text-black"
> aria-hidden="true"
Muğla Dijital >
</motion.span> Muğla Dijital
</div> </motion.span>
</motion.nav>
<button
onClick={() => setIsOpen(!isOpen)}
className="lg:hidden p-2 flex items-center justify-center text-black/70 hover:text-black transition-colors z-50"
aria-label={isOpen ? "Menüyü Kapat" : "Menüyü Aç"}
>
{isOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
</motion.nav>
{/* Mobile Menu Overlay */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, x: "100%" }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: "100%" }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className="fixed inset-0 z-40 bg-[#f5f5f0] flex flex-col justify-center px-8 md:px-16"
>
<div className="flex flex-col gap-8">
{NAV_LINKS.map((item, idx) => (
<motion.div
key={item.label}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + idx * 0.05 }}
>
<Link
href={item.href}
onClick={() => setIsOpen(false)}
className="text-4xl md:text-6xl font-bold tracking-tighter text-black/90 hover:text-black transition-all hover:pl-4"
style={{ fontFamily: "var(--font-martian)" }}
>
{item.label}
</Link>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</>
); );
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getPartners } from "@/app/actions"; import { getFeaturedPartners } from "@/app/actions";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import DynamicLogo from "./DynamicLogo"; import DynamicLogo from "./DynamicLogo";
import Link from "next/link"; import Link from "next/link";
@@ -11,7 +11,7 @@ export default function Partners() {
useEffect(() => { useEffect(() => {
async function fetchPartners() { async function fetchPartners() {
const data = await getPartners(); const data = await getFeaturedPartners();
setPartners(data || []); setPartners(data || []);
} }
fetchPartners(); fetchPartners();
@@ -34,7 +34,7 @@ export default function Partners() {
{/* Partners List Section */} {/* Partners List Section */}
<div className="flex-1 flex items-center pl-4 md:pl-8 pr-4 md:pr-8 overflow-hidden"> <div className="flex-1 flex items-center pl-4 md:pl-8 pr-4 md:pr-8 overflow-hidden">
<div className="flex flex-nowrap items-center justify-start w-full gap-4 md:gap-10"> <div className="flex flex-wrap md:flex-nowrap items-center justify-center md:justify-start w-full gap-6 md:gap-10 py-4">
{partners.slice(0, 5).map((partner, index) => { {partners.slice(0, 5).map((partner, index) => {
const content = ( const content = (
<motion.div <motion.div

View File

@@ -12,7 +12,7 @@ import {
Camera, Camera,
Zap Zap
} from "lucide-react"; } from "lucide-react";
import Footer from "@/components/Footer";
const processSteps = [ const processSteps = [
{ icon: Search, title: "Analiz", desc: "İşletmenizi ve hedef kitlenizi analiz ediyoruz." }, { icon: Search, title: "Analiz", desc: "İşletmenizi ve hedef kitlenizi analiz ediyoruz." },
@@ -156,7 +156,6 @@ export default function ServicesClient({ services: initialServices, locations: i
</div> </div>
</section> </section>
<Footer />
</main> </main>
); );
} }

View File

@@ -89,7 +89,7 @@ export default function ServicesGrid() {
whileInView={{ opacity: 1 }} whileInView={{ opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ delay: 0.6 }} transition={{ delay: 0.6 }}
className="p-8 md:p-12 border-r border-black/10 relative cell-diagonal min-h-[200px] flex items-end" className="p-8 md:p-12 border-r border-black/10 relative cell-diagonal overflow-hidden min-h-[200px] flex items-end"
> >
<h3 className="editorial-headline text-2xl md:text-3xl text-black uppercase"> <h3 className="editorial-headline text-2xl md:text-3xl text-black uppercase">
Profesyonel<br /> Profesyonel<br />
@@ -103,7 +103,7 @@ export default function ServicesGrid() {
whileInView={{ opacity: 1 }} whileInView={{ opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ delay: 0.8 }} transition={{ delay: 0.8 }}
className="p-8 md:p-12 relative cell-diagonal min-h-[200px] flex items-end" className="p-8 md:p-12 relative cell-diagonal overflow-hidden min-h-[200px] flex items-end"
> >
<h3 className="editorial-headline text-2xl md:text-3xl text-black uppercase"> <h3 className="editorial-headline text-2xl md:text-3xl text-black uppercase">
Dijital<br /> Dijital<br />

View File

@@ -2,8 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight, Share2 } from "lucide-react";
import Footer from "@/components/Footer";
export default function WorkDetailClient({ project, nextProject }: { project: any, nextProject: any }) { export default function WorkDetailClient({ project, nextProject }: { project: any, nextProject: any }) {
const isInstagram = (url: string) => { const isInstagram = (url: string) => {
@@ -94,7 +93,7 @@ export default function WorkDetailClient({ project, nextProject }: { project: an
<div className="max-w-4xl mx-auto space-y-32"> <div className="max-w-4xl mx-auto space-y-32">
{/* Hero Image First */} {/* Hero Image First */}
<div className="relative aspect-[16/10] w-full overflow-hidden border border-black/5 shadow-2xl bg-[#e2d1c1] p-1"> <div className="relative aspect-[16/10] w-full overflow-hidden border border-black/5 shadow-2xl bg-[#e2d1c1] p-1">
<div className="relative w-full h-full overflow-hidden"> <div className="relative w-full h-full overflow-hidden">
<Image <Image
src={project.hero_image || "https://images.unsplash.com/photo-1550745165-9bc0b252726f"} src={project.hero_image || "https://images.unsplash.com/photo-1550745165-9bc0b252726f"}
alt={project.title} alt={project.title}
@@ -102,50 +101,50 @@ export default function WorkDetailClient({ project, nextProject }: { project: an
className="object-cover" className="object-cover"
priority priority
/> />
</div> </div>
</div> </div>
{/* Gallery / Instagram Feed */} {/* Gallery / Instagram Feed */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-24 mt-24"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-24 mt-24">
{gallery.map((item: string, idx: number) => ( {gallery.map((item: string, idx: number) => (
<div key={idx} className="space-y-8 animate-reveal"> <div key={idx} className="space-y-8 animate-reveal">
<div className="flex items-center gap-4 text-[10px] font-bold text-black/20 tracking-widest uppercase"> <div className="flex items-center gap-4 text-[10px] font-bold text-black/20 tracking-widest uppercase">
<span>0{idx + 1}</span> <span>0{idx + 1}</span>
<div className="h-px flex-grow bg-black/5" /> <div className="h-px flex-grow bg-black/5" />
<span>{isInstagram(item) ? "INSTAGRAM FEED" : "PRODUCTION VIEW"}</span> <span>{isInstagram(item) ? "INSTAGRAM FEED" : "PRODUCTION VIEW"}</span>
</div> </div>
{isInstagram(item) ? ( {isInstagram(item) ? (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-full max-w-[540px] border border-black/10 bg-white shadow-xl overflow-hidden rounded-xl"> <div className="w-full max-w-[540px] border border-black/10 bg-white shadow-xl overflow-hidden rounded-xl">
<iframe <iframe
src={getInstaEmbedUrl(item)} src={getInstaEmbedUrl(item)}
className="w-full h-[700px] border-0" className="w-full h-[700px] border-0"
scrolling="no" scrolling="no"
allowFullScreen allowFullScreen
/> />
</div>
</div> </div>
</div> ) : (
) : ( <div className="relative aspect-[16/10] border border-black/5 bg-black/5 shadow-lg overflow-hidden group">
<div className="relative aspect-[16/10] border border-black/5 bg-black/5 shadow-lg overflow-hidden group"> {isVideo(item) ? (
{isVideo(item) ? ( <iframe
<iframe src={item}
src={item} className="w-full h-full"
className="w-full h-full" allowFullScreen
allowFullScreen />
/> ) : (
) : ( <Image
<Image src={item}
src={item} alt={`Gallery ${idx}`}
alt={`Gallery ${idx}`} fill
fill className="object-cover transition-transform duration-1000 group-hover:scale-105"
className="object-cover transition-transform duration-1000 group-hover:scale-105" />
/> )}
)} </div>
</div> )}
)} </div>
</div> ))}
))}
</div> </div>
</div> </div>
</section> </section>
@@ -186,7 +185,6 @@ export default function WorkDetailClient({ project, nextProject }: { project: an
</section> </section>
)} )}
<Footer />
</main> </main>
); );
} }

View File

@@ -4,7 +4,7 @@ import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { ArrowUpRight } from "lucide-react"; import { ArrowUpRight } from "lucide-react";
import Footer from "@/components/Footer";
interface ProjectCardProps { interface ProjectCardProps {
hero_image: string; hero_image: string;
@@ -97,7 +97,7 @@ export default function WorksClient({ projects: initialProjects }: { projects: a
break; break;
} }
} }
} catch (e) {} } catch (e) { }
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []); return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
})))]; })))];
@@ -113,7 +113,7 @@ export default function WorksClient({ projects: initialProjects }: { projects: a
break; break;
} }
} }
} catch (e) {} } catch (e) { }
const cats = Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []); const cats = Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
return cats.includes(activeCategory); return cats.includes(activeCategory);
}); });
@@ -181,7 +181,6 @@ export default function WorksClient({ projects: initialProjects }: { projects: a
</div> </div>
</section> </section>
<Footer />
</main> </main>
); );
} }

35
lib/cloudinary.ts Normal file
View File

@@ -0,0 +1,35 @@
import { v2 as cloudinary } from 'cloudinary';
// Configure Cloudinary
cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true,
});
export default cloudinary;
/**
* Uploads a file buffer or base64 to Cloudinary
*/
export async function uploadToCloudinary(file: File, folder: string = 'partners'): Promise<string> {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return new Promise((resolve, reject) => {
cloudinary.uploader.upload_stream(
{
folder: `mugladijital/${folder}`,
resource_type: 'auto',
},
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result?.secure_url || '');
}
}
).end(buffer);
});
}

61
package-lock.json generated
View File

@@ -15,7 +15,8 @@
"next": "16.1.0", "next": "16.1.0",
"postgres": "^3.4.9", "postgres": "^3.4.9",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3",
"resend": "^6.12.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
@@ -1252,6 +1253,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -3549,6 +3556,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -5426,6 +5439,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/postal-mime": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz",
"integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==",
"license": "MIT-0"
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.14", "version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
@@ -5593,6 +5612,27 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/resend": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.12.3.tgz",
"integrity": "sha512-FkEi6YPnVL96/LvH8+QP7NaeaBy5brYXwlRqUCqZZeNL0/iyKij18IPmyPXYauT/2ODn1JG04qKz+qlJfzqzTw==",
"license": "MIT",
"dependencies": {
"postal-mime": "2.7.4",
"svix": "1.92.2"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@react-email/render": "*"
},
"peerDependenciesMeta": {
"@react-email/render": {
"optional": true
}
}
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "2.0.0-next.6", "version": "2.0.0-next.6",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
@@ -5965,6 +6005,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/standardwebhooks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
"license": "MIT",
"dependencies": {
"@stablelib/base64": "^1.0.0",
"fast-sha256": "^1.3.0"
}
},
"node_modules/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -6164,6 +6214,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svix": {
"version": "1.92.2",
"resolved": "https://registry.npmjs.org/svix/-/svix-1.92.2.tgz",
"integrity": "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ==",
"license": "MIT",
"dependencies": {
"standardwebhooks": "1.0.0"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",

View File

@@ -16,7 +16,8 @@
"next": "16.1.0", "next": "16.1.0",
"postgres": "^3.4.9", "postgres": "^3.4.9",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3",
"resend": "^6.12.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB