initial commit: project completion with proper gitignore
@@ -0,0 +1,57 @@
|
||||
const cloudinary = require('cloudinary').v2;
|
||||
const postgres = require('postgres');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
const sql = postgres(process.env.DATABASE_URL);
|
||||
|
||||
async function uploadAndSync() {
|
||||
const partnersDir = path.join(process.cwd(), 'public', 'partnerlogo');
|
||||
const files = fs.readdirSync(partnersDir).filter(file => file.endsWith('.png'));
|
||||
|
||||
console.log(`Found ${files.length} images to upload.`);
|
||||
|
||||
// First, let's clear or check existing partners
|
||||
// To keep it simple, we'll insert new records or update by a 'name' which we'll placeholder
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(partnersDir, file);
|
||||
const partnerId = file.replace('.png', ''); // e.g., "1"
|
||||
|
||||
try {
|
||||
console.log(`Uploading ${file}...`);
|
||||
const result = await cloudinary.uploader.upload(filePath, {
|
||||
folder: 'partners',
|
||||
public_id: `partner_${partnerId}`
|
||||
});
|
||||
|
||||
console.log(`Uploaded: ${result.secure_url}`);
|
||||
|
||||
// Insert or Update in DB
|
||||
// We'll use partnerId as id and display_order
|
||||
const idNum = parseInt(partnerId);
|
||||
await sql`
|
||||
INSERT INTO partners (id, name, logo, display_order)
|
||||
VALUES (${idNum}, ${`Partner ${partnerId}`}, ${result.secure_url}, ${idNum})
|
||||
ON CONFLICT (id)
|
||||
DO UPDATE SET logo = EXCLUDED.logo
|
||||
`;
|
||||
|
||||
console.log(`Synced ${file} to database (ID: ${idNum}).`);
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Sync complete.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
uploadAndSync();
|
||||
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.next
|
||||
.env*
|
||||
*.log
|
||||
build
|
||||
dist
|
||||
.DS_Store
|
||||
266
app/about/page.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowRight,
|
||||
Circle,
|
||||
Camera,
|
||||
Video,
|
||||
Instagram,
|
||||
TrendingUp
|
||||
} from "lucide-react";
|
||||
import Footer from "@/components/Footer";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Hakkımızda | Muğla Dijital Medya & Prodüksiyon Ajansı",
|
||||
description: "Muğla Dijital Medya Ajansı'nın hikayesini, değerlerini ve vizyonunu keşfedin. Drone çekimi, video prodüksiyon, sosyal medya yönetimi ve reklam hizmetleri.",
|
||||
alternates: {
|
||||
canonical: "/about",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Hakkımızda | Muğla Dijital",
|
||||
description: "Muğla'nın dijital çözüm ortağı.",
|
||||
url: "https://mugladijitalmedya.com/about",
|
||||
}
|
||||
};
|
||||
|
||||
const timeline = [
|
||||
{ year: "2020", title: "Ajans Kuruldu", desc: "Muğla'da küçük bir ekiple yola çıktık. Drone çekimleri ve sosyal medya yönetimi ile ilk projelerimizi tamamladık." },
|
||||
{ year: "2021", title: "İlk Büyük Projeler", desc: "Bölgedeki oteller ve işletmeler için profesyonel tanıtım çekimleri gerçekleştirdik. Müşteri portföyümüz hızla büyüdü." },
|
||||
{ year: "2023", title: "Dijital Genişleme", desc: "Reklam yönetimi, SEO ve web tasarım hizmetlerini de ekleyerek tam kapsamlı bir dijital ajansa dönüştük." },
|
||||
{ year: "2025", title: "Sektörde Güçlü Konum", desc: "150+ mutlu müşteri, 500+ tamamlanmış proje ile Muğla ve çevresinin güvenilir dijital çözüm ortağı olduk." }
|
||||
];
|
||||
|
||||
const highlights = [
|
||||
{ number: "500+", label: "Tamamlanan Proje" },
|
||||
{ number: "150+", label: "Mutlu Müşteri" },
|
||||
{ number: "5+", label: "Yıllık Deneyim" },
|
||||
{ number: "9", label: "Hizmet Alanı" }
|
||||
];
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#f5f5f0] text-black">
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="pt-48 pb-24 px-6 md:px-12">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40">Hakkımızda</span>
|
||||
<span className="text-[10px] tracking-[0.1em] text-black/20 italic">Son Güncelleme: 15 Mayıs 2026</span>
|
||||
</div>
|
||||
<h1 className="editorial-headline text-4xl md:text-6xl lg:text-[5.5rem] text-black reveal opacity-0">
|
||||
Muğla'nın Dijital<br />
|
||||
Hikaye <span className="text-primary">Mimarları.</span>
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats Bar - Editorial Style */}
|
||||
<section className="border-y border-black/10">
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4">
|
||||
{highlights.map((item, idx) => (
|
||||
<div key={idx} className={`py-12 px-8 text-center ${idx < 3 ? 'md:border-r border-black/10' : ''} ${idx % 2 === 0 ? 'border-r md:border-r-0 md:border-r border-black/10' : ''}`}>
|
||||
<div className="text-4xl font-black text-primary mb-2">{item.number}</div>
|
||||
<div className="text-[10px] font-bold uppercase tracking-widest text-black/40">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Mission Section - Editorial Layout */}
|
||||
<section className="py-24 px-6 md:px-12 border-b border-black/10">
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||
<div className="lg:col-span-7 space-y-10">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block">Misyonumuz</span>
|
||||
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">
|
||||
Markanızı Havadan ve<br /> Yerden <span className="text-primary">Kurguluyoruz.</span>
|
||||
</h2>
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<p className="text-black/60 text-[14px] leading-relaxed">
|
||||
<strong>Muğla Dijital Medya Ajansı</strong>, Muğla merkezli profesyonel bir dijital medya ve prodüksiyon ajansıdır. İşletmelerin dijital dünyada güçlü bir şekilde var olmasını sağlamak amacıyla; profesyonel drone çekimleri, kurumsal fotoğrafçılık ve video prodüksiyon hizmetleriyle markaların görsel kimliğini inşa ediyoruz.
|
||||
</p>
|
||||
<p className="text-black/60 text-[14px] leading-relaxed">
|
||||
Stratejik sosyal medya yönetimi, Google ve Meta reklam kampanyaları, SEO optimizasyonu ve modern web tasarım çözümlerimizle markanızın dijital varlığını güçlendiriyoruz. Ayrıca düğün ve özel gün çekimleriyle en değerli anlarınızı profesyonel kurgularla ölümsüzleştiriyoruz.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/services" className="button-primary group">
|
||||
Hizmetlerimiz
|
||||
<ArrowRight className="w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="lg:col-span-5 relative">
|
||||
<div className="aspect-[4/5] relative overflow-hidden border border-black/10 grayscale hover:grayscale-0 transition-all duration-1000">
|
||||
<Image src="https://images.unsplash.com/photo-1473968512647-3e447244af8f?q=80&w=800" alt="Muğla Dijital Profesyonel Drone Çekimi ve Havadan Görüntüleme Hizmetleri" fill className="object-cover" />
|
||||
</div>
|
||||
{/* Diagonal decoration */}
|
||||
<div className="absolute -bottom-8 -right-8 w-32 h-32 border-r border-b border-black/10 hidden md:block" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Core Values - Editorial Bento */}
|
||||
<section className="py-24 px-6 md:px-12 border-b border-black/10 bg-black/[0.02]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-16">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-4">Değerlerimiz</span>
|
||||
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">Temel İlkelerimiz</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 border-t border-l border-black/10">
|
||||
{[
|
||||
{
|
||||
title: "Profesyonel Prodüksiyon",
|
||||
desc: "Drone çekimi ve prodüksiyon alanında en son teknolojiyle markanız için unutulmaz görsel içerikler üretiyoruz.",
|
||||
icon: Camera
|
||||
},
|
||||
{
|
||||
title: "Dijital Büyüme",
|
||||
desc: "Sosyal medya yönetimi ve reklam stratejileri ile markanızın sürdürülebilir şekilde büyümesini sağlıyoruz.",
|
||||
icon: TrendingUp
|
||||
},
|
||||
{
|
||||
title: "Müşteri Odaklılık",
|
||||
desc: "Her projeyi müşterimizin ihtiyaçlarına özel tasarlıyor, şeffaf iletişim ve sonuç odaklı çalışma prensibiyle ilerliyoruz.",
|
||||
icon: Circle
|
||||
}
|
||||
].map((v, idx) => (
|
||||
<div key={idx} className="p-10 border-r border-b border-black/10 relative group hover:bg-black/[0.01] transition-colors overflow-hidden">
|
||||
<div className="w-10 h-10 border border-black/10 flex items-center justify-center text-primary mb-8">
|
||||
<v.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="editorial-headline text-xl text-black mb-4 uppercase">{v.title}</h3>
|
||||
<p className="text-[12px] text-black/40 leading-relaxed font-medium">
|
||||
{v.desc}
|
||||
</p>
|
||||
{/* Subtle diagonal on hover */}
|
||||
<div className="absolute top-0 right-0 w-16 h-16 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="absolute top-0 right-0 w-px h-24 bg-black/5 rotate-[-45deg] origin-top-right" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
{/* FAQ Section - AI Extraction Optimized */}
|
||||
<section className="py-24 px-6 md:px-12 border-b border-black/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-16">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-4">Destek</span>
|
||||
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">Sıkça Sorulan Sorular</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-16 gap-y-12">
|
||||
{[
|
||||
{
|
||||
q: "Muğla Dijital Medya Ajansı nedir?",
|
||||
a: "Muğla Dijital, Muğla merkezli profesyonel bir dijital medya ajansıdır. Drone çekimi, video prodüksiyon, sosyal medya yönetimi, SEO ve dijital reklam yönetimi (Google/Meta) alanlarında markalara yaratıcı ve sonuç odaklı çözümler sunuyoruz."
|
||||
},
|
||||
{
|
||||
q: "Drone çekimi hizmetleriniz neleri kapsıyor?",
|
||||
a: "Drone çekimi hizmetlerimiz; otel tanıtım filmleri, gayrimenkul çekimleri, arazi ve arsa havadan görüntüleme, etkinlik ve düğün çekimlerini kapsamaktadır. Tüm çekimler profesyonel lisanslı pilotlarımız ve 4K/5.4K çözünürlüklü ekipmanlarımızla gerçekleştirilir."
|
||||
},
|
||||
{
|
||||
q: "Sosyal medya yönetimi süreciniz nasıl işliyor?",
|
||||
a: "Sosyal medya yönetimi sürecimiz; marka analizi, içerik planlama, profesyonel çekimler, grafik tasarım, paylaşım yönetimi ve aylık performans raporlamasını içerir. Instagram, Facebook, LinkedIn ve TikTok platformlarında markanızı büyütecek stratejiler uyguluyoruz."
|
||||
},
|
||||
{
|
||||
q: "Muğla dışında hizmet veriyor musunuz?",
|
||||
a: "Evet, merkezimiz Muğla olmakla birlikte başta Aydın, Denizli, Antalya ve İzmir olmak üzere tüm Ege bölgesi ve Türkiye genelinde projeler gerçekleştiriyoruz."
|
||||
},
|
||||
{
|
||||
q: "Dijital reklam yönetimi (Google ve Meta) için bütçe nasıl belirlenir?",
|
||||
a: "Reklam bütçesi; sektörünüze, hedef kitlenize ve hedeflerinize göre analiz edilerek belirlenir. Minimum bütçe ile maksimum dönüşüm alabilmeniz için stratejik planlama yapıyor, kampanyalarınızı günlük olarak optimize ediyoruz."
|
||||
}
|
||||
].map((faq, idx) => (
|
||||
<div key={idx} className="space-y-4">
|
||||
<h3 className="text-[14px] font-bold uppercase tracking-tight text-black">{faq.q}</h3>
|
||||
<p className="text-[12px] text-black/50 leading-relaxed max-w-lg">{faq.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQ Schema */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Muğla Dijital Medya Ajansı nedir?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Muğla Dijital, Muğla merkezli profesyonel bir dijital medya ajansıdır. Drone çekimi, video prodüksiyon, sosyal medya yönetimi, SEO ve dijital reklam yönetimi (Google/Meta) alanlarında markalara yaratıcı ve sonuç odaklı çözümler sunuyoruz."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Drone çekimi hizmetleriniz neleri kapsıyor?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Drone çekimi hizmetlerimiz; otel tanıtım filmleri, gayrimenkul çekimleri, arazi ve arsa havadan görüntüleme, etkinlik ve düğün çekimlerini kapsamaktadır."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Muğla dışında hizmet veriyor musunuz?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Evet, merkezimiz Muğla olmakla birlikte başta Aydın, Denizli, Antalya ve İzmir olmak üzere tüm Ege bölgesi ve Türkiye genelinde projeler gerçekleştiriyoruz."
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* AboutPage Schema */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AboutPage",
|
||||
"mainEntity": {
|
||||
"@type": "ProfessionalService",
|
||||
"name": "Muğla Dijital Medya Ajansı",
|
||||
"description": "Muğla'nın öncü dijital medya ve prodüksiyon ajansı.",
|
||||
"areaServed": "Ege Bölgesi",
|
||||
"serviceType": ["Drone Çekimi", "Sosyal Medya Yönetimi", "Reklam Yönetimi"]
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 px-6 text-center">
|
||||
<div className="max-w-4xl mx-auto space-y-12">
|
||||
<h2 className="editorial-headline text-4xl md:text-6xl text-black uppercase">
|
||||
Projenizi Hayata <br /> <span className="text-primary">Geçirelim.</span>
|
||||
</h2>
|
||||
<p className="text-black/40 text-[13px] tracking-[0.1em] leading-relaxed max-w-xl mx-auto">
|
||||
Drone çekimi, sosyal medya yönetimi veya dijital reklam — ne ihtiyacınız varsa, birlikte çözüm üretelim.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-6">
|
||||
<Link href="/contact" className="button-primary px-10">
|
||||
Teklif Al
|
||||
</Link>
|
||||
<Link href="/works" className="button-secondary px-10">
|
||||
Portfolyo
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
100
app/actions.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
'use server'
|
||||
|
||||
import sql from '@/lib/db';
|
||||
|
||||
export async function getSettings() {
|
||||
try {
|
||||
const settings = await sql`SELECT * FROM settings WHERE id = 1 LIMIT 1`;
|
||||
return settings[0] || null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFeaturedServices() {
|
||||
try {
|
||||
const services = await sql`SELECT * FROM services WHERE is_featured = true ORDER BY display_order ASC LIMIT 4`;
|
||||
return services;
|
||||
} catch (error) {
|
||||
console.error('Error fetching services:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFeaturedProjects() {
|
||||
try {
|
||||
const projects = await sql`SELECT * FROM projects WHERE is_featured = true ORDER BY created_at DESC LIMIT 6`;
|
||||
return projects;
|
||||
} catch (error) {
|
||||
console.error('Error fetching projects:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitLead(formData: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
projectType: string;
|
||||
message: string;
|
||||
}) {
|
||||
try {
|
||||
const fullName = `${formData.firstName} ${formData.lastName}`;
|
||||
await sql`
|
||||
INSERT INTO leads (full_name, email, service_type, message, status)
|
||||
VALUES (${fullName}, ${formData.email}, ${formData.projectType}, ${formData.message}, 'new')
|
||||
`;
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error submitting lead:', error);
|
||||
return { error: 'Failed to submit' };
|
||||
}
|
||||
}
|
||||
export async function getPartners() {
|
||||
try {
|
||||
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
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error fetching partners:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjectBySlug(slug: string) {
|
||||
try {
|
||||
const projects = await sql`SELECT * FROM projects WHERE slug = ${slug} LIMIT 1`;
|
||||
if (projects.length === 0) return null;
|
||||
|
||||
const project = projects[0];
|
||||
|
||||
// Fetch next project for the bottom section
|
||||
const nextProjects = await sql`
|
||||
SELECT * FROM projects
|
||||
WHERE created_at < ${project.created_at}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
// If no older project, fetch the newest one
|
||||
let nextProject: any = nextProjects[0] || null;
|
||||
if (!nextProject) {
|
||||
const newest = await sql`SELECT * FROM projects ORDER BY created_at DESC LIMIT 1`;
|
||||
nextProject = newest[0] !== project ? newest[0] : null;
|
||||
}
|
||||
|
||||
return { project, nextProject };
|
||||
} catch (error) {
|
||||
console.error('Error fetching project:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
60
app/admin/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import Link from 'next/link';
|
||||
import { LayoutDashboard, Users, Briefcase, Settings, FileText, LogOut, Handshake } from 'lucide-react';
|
||||
import { logout } from '../actions';
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 border-r border-white/10 flex flex-col bg-zinc-950 p-4">
|
||||
<div className="mb-8 px-4">
|
||||
<h1 className="text-xl font-black uppercase tracking-widest text-[#1e9a83]">Muğla Dijital</h1>
|
||||
<p className="text-xs text-white/40 uppercase tracking-widest">Admin Panel</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-2">
|
||||
<Link href="/admin" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
|
||||
<LayoutDashboard className="w-5 h-5 text-white/60" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link href="/admin/leads" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
|
||||
<Users className="w-5 h-5 text-white/60" />
|
||||
Mesajlar & Başvurular
|
||||
</Link>
|
||||
<Link href="/admin/projects" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
|
||||
<Briefcase className="w-5 h-5 text-white/60" />
|
||||
Projeler
|
||||
</Link>
|
||||
<Link href="/admin/services" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
|
||||
<FileText className="w-5 h-5 text-white/60" />
|
||||
Hizmetler
|
||||
</Link>
|
||||
<Link href="/admin/settings" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
|
||||
<Settings className="w-5 h-5 text-white/60" />
|
||||
Site Ayarları
|
||||
</Link>
|
||||
<Link href="/admin/partners" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
|
||||
<Handshake className="w-5 h-5 text-white/60" />
|
||||
Partnerler
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto border-t border-white/10 pt-4">
|
||||
<form action={logout}>
|
||||
<button type="submit" className="flex w-full items-center gap-3 px-4 py-3 rounded-xl hover:bg-red-500/10 hover:text-red-500 transition-colors text-sm font-medium text-white/60">
|
||||
<LogOut className="w-5 h-5" />
|
||||
Çıkış Yap
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-6xl mx-auto p-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
app/admin/(dashboard)/leads/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { getLeadsAdmin, deleteLead } from '../../actions';
|
||||
import { Trash2, Mail, User, Briefcase, Calendar } from 'lucide-react';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function LeadsPage() {
|
||||
const leads = await getLeadsAdmin();
|
||||
|
||||
async function handleDelete(formData: FormData) {
|
||||
'use server';
|
||||
const id = Number(formData.get('id'));
|
||||
await deleteLead(id);
|
||||
revalidatePath('/admin/leads');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Mesajlar & Başvurular</h1>
|
||||
<p className="text-white/40">İletişim formundan gelen müşteri talepleri.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{leads.map((lead: any) => (
|
||||
<div key={lead.id} className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex flex-col md:flex-row gap-6 justify-between md:items-center group hover:border-[#1e9a83]/50 transition-colors">
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="flex items-center gap-4">
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-[#1e9a83]" />
|
||||
{lead.full_name}
|
||||
</h3>
|
||||
<span className="bg-white/5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest text-white/60 flex items-center gap-2">
|
||||
<Briefcase className="w-3 h-3" />
|
||||
{lead.service_type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 text-sm text-white/60">
|
||||
<a href={`mailto:${lead.email}`} className="flex items-center gap-2 hover:text-white transition-colors">
|
||||
<Mail className="w-4 h-4" /> {lead.email}
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{new Date(lead.created_at).toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 p-4 rounded-xl mt-4">
|
||||
<p className="text-white/80 text-sm leading-relaxed">{lead.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form action={handleDelete}>
|
||||
<input type="hidden" name="id" value={lead.id} />
|
||||
<button type="submit" className="p-3 bg-red-500/10 text-red-500 rounded-xl hover:bg-red-500 hover:text-white transition-colors">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(!leads || leads.length === 0) && (
|
||||
<div className="text-center py-12 bg-white/5 rounded-2xl border border-white/10 border-dashed">
|
||||
<p className="text-white/40">Henüz bir mesaj bulunmuyor.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
app/admin/(dashboard)/page.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { getDashboardStats } from '../actions';
|
||||
import { Users, Briefcase, FileText } from 'lucide-react';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const stats = await getDashboardStats();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Dashboard</h1>
|
||||
<p className="text-white/40">Sistem istatistikleri ve özet görünüm.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex items-center gap-6">
|
||||
<div className="w-14 h-14 bg-blue-500/10 text-blue-500 rounded-2xl flex items-center justify-center">
|
||||
<Users className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white/40 font-bold uppercase tracking-widest">Mesajlar</p>
|
||||
<p className="text-3xl font-black">{stats.leads}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex items-center gap-6">
|
||||
<div className="w-14 h-14 bg-[#1e9a83]/10 text-[#1e9a83] rounded-2xl flex items-center justify-center">
|
||||
<Briefcase className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white/40 font-bold uppercase tracking-widest">Projeler</p>
|
||||
<p className="text-3xl font-black">{stats.projects}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex items-center gap-6">
|
||||
<div className="w-14 h-14 bg-purple-500/10 text-purple-500 rounded-2xl flex items-center justify-center">
|
||||
<FileText className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-white/40 font-bold uppercase tracking-widest">Hizmetler</p>
|
||||
<p className="text-3xl font-black">{stats.services}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
app/admin/(dashboard)/partners/PartnerRow.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Trash2, Save, Edit2, X } from "lucide-react";
|
||||
import { updatePartner } from "../../actions";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function PartnerRow({ partner, onDelete }: { partner: any, onDelete: any }) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [name, setName] = useState(partner.name);
|
||||
const [displayOrder, setDisplayOrder] = useState(partner.display_order);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsLoading(true);
|
||||
const res = await updatePartner(partner.id, name, Number(displayOrder));
|
||||
setIsLoading(false);
|
||||
if (res.success) {
|
||||
setIsEditing(false);
|
||||
router.refresh();
|
||||
} else {
|
||||
alert("Güncellenirken bir hata oluştu");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tr className="hover:bg-white/[0.02] transition-colors border-b border-white/10 last:border-0">
|
||||
<td className="px-6 py-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={displayOrder}
|
||||
onChange={(e) => setDisplayOrder(e.target.value)}
|
||||
className="w-16 bg-white/5 border border-white/10 rounded px-2 py-1 text-white focus:border-primary outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="font-bold text-white/40">{partner.display_order}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Logo Preview */}
|
||||
<div className="w-24 h-16 bg-white/5 border border-white/10 rounded-lg flex items-center justify-center overflow-hidden p-2 group relative">
|
||||
{partner.logo ? (
|
||||
<img
|
||||
src={partner.logo}
|
||||
alt={partner.name}
|
||||
className="w-full h-full object-contain opacity-90 group-hover:opacity-100 transition-opacity transform scale-[4]"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[10px] text-white/20">Logo Yok</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Tiny Path Info */}
|
||||
<div className="hidden xl:block">
|
||||
<div className="text-[10px] text-white/20 font-mono max-w-[200px] truncate">
|
||||
{partner.logo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full max-w-md bg-white/5 border border-white/10 rounded px-3 py-1.5 text-white focus:border-primary outline-none"
|
||||
/>
|
||||
) : (
|
||||
<div className="font-bold text-white tracking-wide">{partner.name}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading}
|
||||
className="p-2 bg-green-500/10 text-green-500 rounded-lg hover:bg-green-500 hover:text-white transition-all disabled:opacity-50"
|
||||
title="Kaydet"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setName(partner.name);
|
||||
setDisplayOrder(partner.display_order);
|
||||
}}
|
||||
className="p-2 bg-white/5 text-white/40 rounded-lg hover:bg-white/10 hover:text-white transition-all"
|
||||
title="İptal"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="p-2 bg-white/5 text-white/40 rounded-lg hover:bg-primary hover:text-white transition-all"
|
||||
title="Düzenle"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<form action={onDelete} className="inline">
|
||||
<input type="hidden" name="id" value={partner.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"
|
||||
title="Sil"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
55
app/admin/(dashboard)/partners/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { getPartnersAdmin, deletePartner } from '../../actions';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import PartnerRow from './PartnerRow';
|
||||
|
||||
export default async function PartnersAdminPage() {
|
||||
const partners = await getPartnersAdmin();
|
||||
|
||||
async function handleDelete(formData: FormData) {
|
||||
'use server';
|
||||
const id = Number(formData.get('id'));
|
||||
await deletePartner(id);
|
||||
revalidatePath('/admin/partners');
|
||||
}
|
||||
|
||||
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">Partnerler</h1>
|
||||
<p className="text-white/40">Referans markaların listesi.</p>
|
||||
</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">
|
||||
<Plus className="w-4 h-4" />
|
||||
Yeni Partner Ekle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-white/5 border-b border-white/10">
|
||||
<tr>
|
||||
<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">Marka Adı</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{partners.map((partner: any) => (
|
||||
<PartnerRow key={partner.id} partner={partner} onDelete={handleDelete} />
|
||||
))}
|
||||
{(!partners || partners.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-6 py-8 text-center text-white/40">
|
||||
Henüz partner eklenmemiş.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
app/admin/(dashboard)/projects/[id]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getProjectByIdAdmin, getServicesAdmin, getPartnersAdmin } from '../../../actions';
|
||||
import ProjectForm from '../new/ProjectForm';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export default async function EditProjectPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const project = await getProjectByIdAdmin(Number(id));
|
||||
const services = await getServicesAdmin();
|
||||
const partners = await getPartnersAdmin();
|
||||
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-5xl">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/projects" className="p-2 bg-white/5 hover:bg-white/10 rounded-xl transition-colors group">
|
||||
<ArrowLeft className="w-5 h-5 text-white/40 group-hover:text-white" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Projeyi Düzenle</h1>
|
||||
<p className="text-white/40">{project.title} projesini güncelliyorsunuz.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectForm initialData={project} services={services} partners={partners} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
207
app/admin/(dashboard)/projects/new/ProjectForm.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createProjectAdmin, updateProjectAdmin } from '../../../actions';
|
||||
import { Save, CheckCircle2, AlertCircle, X } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const slugify = (text: string) => {
|
||||
return text
|
||||
.toString()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.replace(/--+/g, '-');
|
||||
};
|
||||
|
||||
export default function ProjectForm({
|
||||
initialData,
|
||||
services = [],
|
||||
partners = []
|
||||
}: {
|
||||
initialData?: any,
|
||||
services?: any[],
|
||||
partners?: any[]
|
||||
}) {
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [title, setTitle] = useState(initialData?.title || '');
|
||||
const [slug, setSlug] = useState(initialData?.slug || '');
|
||||
const router = useRouter();
|
||||
|
||||
// Auto-slug generation when title changes (only if it's a new project or manually changed title)
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setTitle(newTitle);
|
||||
if (!initialData) {
|
||||
setSlug(slugify(newTitle));
|
||||
}
|
||||
};
|
||||
|
||||
const parseSafe = (data: any) => {
|
||||
let current = data;
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (typeof current === 'string' && (current.trim().startsWith('[') || current.trim().startsWith('"'))) {
|
||||
current = JSON.parse(current);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
} catch (e) {
|
||||
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
}
|
||||
};
|
||||
|
||||
const initialTechStack = parseSafe(initialData?.tech_stack);
|
||||
const initialGallery = parseSafe(initialData?.gallery);
|
||||
const initialCategories = parseSafe(initialData?.category);
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
setStatus('loading');
|
||||
let res;
|
||||
if (initialData?.id) {
|
||||
res = await updateProjectAdmin(initialData.id, formData);
|
||||
} else {
|
||||
res = await createProjectAdmin(formData);
|
||||
}
|
||||
|
||||
if (res.error) {
|
||||
setErrorMsg(res.error);
|
||||
setStatus('error');
|
||||
} else {
|
||||
setStatus('success');
|
||||
setTimeout(() => {
|
||||
router.push('/admin/projects');
|
||||
router.refresh();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit} className="bg-zinc-950 border border-white/10 rounded-3xl p-8 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Proje Adı (Partner Seçin)</label>
|
||||
<select
|
||||
name="title"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => handleTitleChange(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none"
|
||||
>
|
||||
<option value="" className="bg-zinc-900">Seçiniz...</option>
|
||||
{partners.map(p => (
|
||||
<option key={p.id} value={p.name} className="bg-zinc-900">{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Kısa Başlık (URL Slug)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="slug"
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value)}
|
||||
required
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none font-mono text-xs"
|
||||
placeholder="örn: marka-ismi"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 lg:col-span-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Kategoriler (Hizmetler - Çoklu Seçim)</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4 bg-white/5 border border-white/10 rounded-xl">
|
||||
{services.map(s => {
|
||||
const isChecked = initialCategories.includes(s.title);
|
||||
return (
|
||||
<label key={s.id} className="flex items-center gap-3 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="category"
|
||||
value={s.title}
|
||||
defaultChecked={isChecked}
|
||||
className="w-4 h-4 rounded border-white/10 bg-white/5 accent-[#1e9a83]"
|
||||
/>
|
||||
<span className="text-[11px] font-bold uppercase text-white/60 group-hover:text-white transition-colors">
|
||||
{s.title}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Yıl</label>
|
||||
<input type="text" name="year" defaultValue={initialData?.year || new Date().getFullYear()} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Örn: 2024" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Müşteri (Görünen İsim)</label>
|
||||
<input type="text" name="client" defaultValue={initialData?.client || title} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Örn: ABC A.Ş." />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Lokasyon</label>
|
||||
<input type="text" name="location" defaultValue={initialData?.location || 'Muğla, TR'} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Örn: Muğla, TR" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Alt Başlık (Kısa Açıklama)</label>
|
||||
<input type="text" name="subtitle" defaultValue={initialData?.subtitle} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Projenin kısa özeti..." />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Ana Görsel (URL veya Yol)</label>
|
||||
<input type="text" name="hero_image" defaultValue={initialData?.hero_image} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="/images/projects/1.jpg" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-white/60 mt-8 mb-4 border-b border-white/10 pb-2">Hikaye & Detay</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Hikaye Başlığı</label>
|
||||
<input type="text" name="narrative_title" defaultValue={initialData?.narrative_title} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Geleceği Yeniden Şekillendirmek" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Hikaye İçeriği (Paragraf)</label>
|
||||
<textarea name="narrative_desc" defaultValue={initialData?.narrative_desc} rows={4} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" placeholder="Projenin detaylı açıklaması..."></textarea>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Teknolojiler (Her satıra bir tane)</label>
|
||||
<textarea name="tech_stack" defaultValue={initialTechStack.join('\n')} rows={5} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" placeholder="React Next.js TailwindCSS"></textarea>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Galeri & Instagram Linkleri (Her satıra bir tane)</label>
|
||||
<textarea name="gallery" defaultValue={initialGallery.join('\n')} rows={5} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" placeholder="https://instagram.com/p/... /img1.jpg /img2.jpg"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<input type="checkbox" name="is_featured" id="is_featured" defaultChecked={initialData?.is_featured} className="w-5 h-5 accent-[#1e9a83] rounded" />
|
||||
<label htmlFor="is_featured" className="text-sm font-bold text-white/80 cursor-pointer">Bu projeyi anasayfada (Öne Çıkanlar) göster</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<button type="submit" disabled={status === 'loading'} className="bg-[#1e9a83] hover:bg-[#157a67] text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 disabled:opacity-50">
|
||||
<Save className="w-5 h-5" />
|
||||
{status === 'loading' ? 'Kaydediliyor...' : initialData ? 'Projeyi Güncelle' : 'Projeyi Kaydet'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="flex items-center gap-2 text-green-500 text-sm font-bold bg-green-500/10 p-4 rounded-xl mt-4">
|
||||
<CheckCircle2 className="w-5 h-5" /> Proje başarıyla {initialData ? 'güncellendi' : 'eklendi'}! Yönlendiriliyorsunuz...
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div className="flex items-center gap-2 text-red-500 text-sm font-bold bg-red-500/10 p-4 rounded-xl mt-4">
|
||||
<AlertCircle className="w-5 h-5" /> {errorMsg}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
25
app/admin/(dashboard)/projects/new/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import ProjectForm from './ProjectForm';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { getServicesAdmin, getPartnersAdmin } from '../../../actions';
|
||||
|
||||
export default async function NewProjectPage() {
|
||||
const services = await getServicesAdmin();
|
||||
const partners = await getPartnersAdmin();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/projects" className="p-2 bg-white/5 hover:bg-white/10 rounded-xl transition-colors">
|
||||
<ArrowLeft className="w-5 h-5 text-white/60" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Yeni Proje Ekle</h1>
|
||||
<p className="text-white/40">Sisteme yeni bir proje dahil edin.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectForm services={services} partners={partners} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
90
app/admin/(dashboard)/projects/page.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { getProjectsAdmin, deleteProject } from '../../actions';
|
||||
import { Trash2, Edit } from 'lucide-react';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import Image from 'next/image';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export default async function ProjectsPage() {
|
||||
const projects = await getProjectsAdmin();
|
||||
|
||||
async function handleDelete(formData: FormData) {
|
||||
'use server';
|
||||
const id = Number(formData.get('id'));
|
||||
await deleteProject(id);
|
||||
revalidatePath('/admin/projects');
|
||||
}
|
||||
|
||||
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">Projeler</h1>
|
||||
<p className="text-white/40">Sistemdeki tüm projelerin listesi.</p>
|
||||
</div>
|
||||
<Link href="/admin/projects/new" className="bg-[#1e9a83] text-white px-6 py-2 rounded-xl font-bold hover:bg-[#157a67] transition-colors">
|
||||
Yeni Proje Ekle
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-white/5 border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Görsel</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Proje Adı</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Kategori</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Öne Çıkan</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/10">
|
||||
{projects.map((project: any) => (
|
||||
<tr key={project.id} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="w-16 h-12 relative rounded-lg overflow-hidden bg-white/10">
|
||||
{project.hero_image && (
|
||||
<Image src={project.hero_image} alt={project.title} fill className="object-cover" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-bold text-white">{project.title}</div>
|
||||
<div className="text-xs text-white/40">{project.slug}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="bg-white/5 px-2 py-1 rounded-md text-xs text-white/60">{project.category}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{project.is_featured ? (
|
||||
<span className="text-green-500 text-xs font-bold uppercase tracking-widest">Evet</span>
|
||||
) : (
|
||||
<span className="text-white/20 text-xs font-bold uppercase tracking-widest">Hayır</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right flex justify-end gap-2">
|
||||
<Link href={`/admin/projects/${project.id}`} className="p-2 bg-blue-500/10 text-blue-500 rounded-lg hover:bg-blue-500 hover:text-white transition-colors">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Link>
|
||||
<form action={handleDelete}>
|
||||
<input type="hidden" name="id" value={project.id} />
|
||||
<button type="submit" className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500 hover:text-white transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!projects || projects.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-white/40">
|
||||
Henüz proje eklenmemiş.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
app/admin/(dashboard)/services/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { getServicesAdmin, deleteService } from '../../actions';
|
||||
import { Trash2, Edit } from 'lucide-react';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function ServicesPage() {
|
||||
const services = await getServicesAdmin();
|
||||
|
||||
async function handleDelete(formData: FormData) {
|
||||
'use server';
|
||||
const id = Number(formData.get('id'));
|
||||
await deleteService(id);
|
||||
revalidatePath('/admin/services');
|
||||
}
|
||||
|
||||
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">Hizmetler</h1>
|
||||
<p className="text-white/40">Sistemdeki tüm hizmetlerin listesi.</p>
|
||||
</div>
|
||||
<button className="bg-[#1e9a83] text-white px-6 py-2 rounded-xl font-bold hover:bg-[#157a67] transition-colors">
|
||||
Yeni Hizmet Ekle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-white/5 border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 w-16">Sıra</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">İkon Adı</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Hizmet Adı</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Öne Çıkan</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/10">
|
||||
{services.map((service: any) => (
|
||||
<tr key={service.id} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="font-bold text-white/40">{service.display_order}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="bg-white/5 px-2 py-1 rounded-md text-xs text-[#1e9a83] font-mono">{service.icon_name || '-'}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-bold text-white">{service.title}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{service.is_featured ? (
|
||||
<span className="text-green-500 text-xs font-bold uppercase tracking-widest">Evet</span>
|
||||
) : (
|
||||
<span className="text-white/20 text-xs font-bold uppercase tracking-widest">Hayır</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right flex justify-end gap-2">
|
||||
<button className="p-2 bg-blue-500/10 text-blue-500 rounded-lg hover:bg-blue-500 hover:text-white transition-colors">
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<form action={handleDelete}>
|
||||
<input type="hidden" name="id" value={service.id} />
|
||||
<button type="submit" className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500 hover:text-white transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!services || services.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-white/40">
|
||||
Henüz hizmet eklenmemiş.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
app/admin/(dashboard)/settings/SettingsForm.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { updateSettings } from '../../actions';
|
||||
import { Save, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
|
||||
export default function SettingsForm({ initialData }: { initialData: any }) {
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
setStatus('loading');
|
||||
const res = await updateSettings(formData);
|
||||
if (res.error) setStatus('error');
|
||||
else setStatus('success');
|
||||
|
||||
setTimeout(() => setStatus('idle'), 3000);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit} className="bg-zinc-950 border border-white/10 rounded-3xl p-8 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Site Adı</label>
|
||||
<input type="text" name="site_name" defaultValue={initialData?.site_name} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">İletişim E-posta</label>
|
||||
<input type="email" name="contact_email" defaultValue={initialData?.contact_email} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">İletişim Telefon</label>
|
||||
<input type="text" name="contact_phone" defaultValue={initialData?.contact_phone} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Rozet Metni</label>
|
||||
<input type="text" name="status_badge_text" defaultValue={initialData?.status_badge_text} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Site Açıklaması (SEO)</label>
|
||||
<textarea name="site_description" rows={3} defaultValue={initialData?.site_description} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Ofis Adresi</label>
|
||||
<textarea name="office_address" rows={2} defaultValue={initialData?.office_address} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-white/60 mt-8 mb-4 border-b border-white/10 pb-2">Sosyal Medya Linkleri</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Instagram URL</label>
|
||||
<input type="url" name="instagram_url" defaultValue={initialData?.instagram_url} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Twitter URL</label>
|
||||
<input type="url" name="twitter_url" defaultValue={initialData?.twitter_url} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">LinkedIn URL</label>
|
||||
<input type="url" name="linkedin_url" defaultValue={initialData?.linkedin_url} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<button type="submit" disabled={status === 'loading'} className="bg-[#1e9a83] hover:bg-[#157a67] text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 disabled:opacity-50">
|
||||
<Save className="w-5 h-5" />
|
||||
{status === 'loading' ? 'Kaydediliyor...' : 'Ayarları Kaydet'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="flex items-center gap-2 text-green-500 text-sm font-bold bg-green-500/10 p-4 rounded-xl mt-4">
|
||||
<CheckCircle2 className="w-5 h-5" /> Ayarlar başarıyla güncellendi!
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div className="flex items-center gap-2 text-red-500 text-sm font-bold bg-red-500/10 p-4 rounded-xl mt-4">
|
||||
<AlertCircle className="w-5 h-5" /> Güncelleme sırasında bir hata oluştu.
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
17
app/admin/(dashboard)/settings/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getSettings } from '@/app/actions';
|
||||
import SettingsForm from './SettingsForm';
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const settings = await getSettings();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Site Ayarları</h1>
|
||||
<p className="text-white/40">Genel site bilgilerini ve iletişim adreslerini buradan güncelleyebilirsiniz.</p>
|
||||
</div>
|
||||
|
||||
<SettingsForm initialData={settings} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
260
app/admin/actions.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
'use server';
|
||||
|
||||
import sql from '@/lib/db';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export async function login(prevState: any, formData: FormData) {
|
||||
const password = formData.get('password') as string;
|
||||
if (password === process.env.ADMIN_PASSWORD) {
|
||||
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: '/',
|
||||
});
|
||||
redirect('/admin');
|
||||
}
|
||||
return { error: 'Hatalı şifre' };
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete('admin_session');
|
||||
redirect('/admin/login');
|
||||
}
|
||||
|
||||
export async function getDashboardStats() {
|
||||
try {
|
||||
const [leadsCount] = await sql`SELECT count(*) FROM leads`;
|
||||
const [projectsCount] = await sql`SELECT count(*) FROM projects`;
|
||||
const [servicesCount] = await sql`SELECT count(*) FROM services`;
|
||||
|
||||
return {
|
||||
leads: Number(leadsCount.count),
|
||||
projects: Number(projectsCount.count),
|
||||
services: Number(servicesCount.count),
|
||||
};
|
||||
} catch (e) {
|
||||
return { leads: 0, projects: 0, services: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLeadsAdmin() {
|
||||
try {
|
||||
return await sql`SELECT * FROM leads ORDER BY created_at DESC`;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteLead(id: number) {
|
||||
try {
|
||||
await sql`DELETE FROM leads WHERE id = ${id}`;
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { error: 'Silinemedi' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSettings(formData: FormData) {
|
||||
try {
|
||||
const data = {
|
||||
site_name: formData.get('site_name') as string,
|
||||
site_description: formData.get('site_description') as string,
|
||||
office_address: formData.get('office_address') as string,
|
||||
contact_email: formData.get('contact_email') as string,
|
||||
contact_phone: formData.get('contact_phone') as string,
|
||||
instagram_url: formData.get('instagram_url') as string,
|
||||
twitter_url: formData.get('twitter_url') as string,
|
||||
linkedin_url: formData.get('linkedin_url') as string,
|
||||
status_badge_text: formData.get('status_badge_text') as string,
|
||||
};
|
||||
|
||||
await sql`
|
||||
UPDATE settings SET
|
||||
site_name = ${data.site_name},
|
||||
site_description = ${data.site_description},
|
||||
office_address = ${data.office_address},
|
||||
contact_email = ${data.contact_email},
|
||||
contact_phone = ${data.contact_phone},
|
||||
instagram_url = ${data.instagram_url},
|
||||
twitter_url = ${data.twitter_url},
|
||||
linkedin_url = ${data.linkedin_url},
|
||||
status_badge_text = ${data.status_badge_text},
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = 1
|
||||
`;
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { error: 'Güncellenemedi' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjectsAdmin() {
|
||||
try {
|
||||
return await sql`SELECT * FROM projects ORDER BY created_at DESC`;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjectByIdAdmin(id: number) {
|
||||
try {
|
||||
const result = await sql`SELECT * FROM projects WHERE id = ${id}`;
|
||||
return result[0];
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteProject(id: number) {
|
||||
try {
|
||||
await sql`DELETE FROM projects WHERE id = ${id}`;
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { error: 'Silinemedi' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProjectAdmin(formData: FormData) {
|
||||
try {
|
||||
const slug = formData.get('slug') as string;
|
||||
const title = formData.get('title') as string;
|
||||
const subtitle = formData.get('subtitle') as string;
|
||||
const year = formData.get('year') as string;
|
||||
const hero_image = formData.get('hero_image') as string;
|
||||
const client = formData.get('client') as string;
|
||||
const role = formData.get('role') as string;
|
||||
const location = formData.get('location') as string;
|
||||
const narrative_title = formData.get('narrative_title') as string;
|
||||
const narrative_desc = formData.get('narrative_desc') as string;
|
||||
const is_featured = formData.get('is_featured') === 'on';
|
||||
|
||||
const categoriesRaw = formData.getAll('category') as string[];
|
||||
const techStackRaw = formData.get('tech_stack') as string;
|
||||
const galleryRaw = formData.get('gallery') as string;
|
||||
|
||||
const category = JSON.stringify(categoriesRaw.filter(Boolean));
|
||||
const tech_stack = JSON.stringify(techStackRaw ? techStackRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
|
||||
const gallery = JSON.stringify(galleryRaw ? galleryRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
|
||||
|
||||
await sql`
|
||||
INSERT INTO projects (
|
||||
slug, title, subtitle, category, year, hero_image, client, role, location,
|
||||
narrative_title, narrative_desc, is_featured, tech_stack, gallery
|
||||
) VALUES (
|
||||
${slug}, ${title}, ${subtitle}, ${category}::jsonb, ${year}, ${hero_image}, ${client}, ${role}, ${location},
|
||||
${narrative_title}, ${narrative_desc}, ${is_featured}, ${tech_stack}::jsonb, ${gallery}::jsonb
|
||||
)
|
||||
`;
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
console.error('Error creating project:', e);
|
||||
return { error: 'Ekleme başarısız: ' + (e.message || 'Bilinmeyen hata') };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProjectAdmin(id: number, formData: FormData) {
|
||||
try {
|
||||
const slug = formData.get('slug') as string;
|
||||
const title = formData.get('title') as string;
|
||||
const subtitle = formData.get('subtitle') as string;
|
||||
const year = formData.get('year') as string;
|
||||
const hero_image = formData.get('hero_image') as string;
|
||||
const client = formData.get('client') as string;
|
||||
const role = formData.get('role') as string;
|
||||
const location = formData.get('location') as string;
|
||||
const narrative_title = formData.get('narrative_title') as string;
|
||||
const narrative_desc = formData.get('narrative_desc') as string;
|
||||
const is_featured = formData.get('is_featured') === 'on';
|
||||
|
||||
const categoriesRaw = formData.getAll('category') as string[];
|
||||
const techStackRaw = formData.get('tech_stack') as string;
|
||||
const galleryRaw = formData.get('gallery') as string;
|
||||
|
||||
const category = JSON.stringify(categoriesRaw.filter(Boolean));
|
||||
const tech_stack = JSON.stringify(techStackRaw ? techStackRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
|
||||
const gallery = JSON.stringify(galleryRaw ? galleryRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
|
||||
|
||||
await sql`
|
||||
UPDATE projects SET
|
||||
slug = ${slug}, title = ${title}, subtitle = ${subtitle}, category = ${category}::jsonb,
|
||||
year = ${year}, hero_image = ${hero_image}, client = ${client}, role = ${role},
|
||||
location = ${location}, narrative_title = ${narrative_title},
|
||||
narrative_desc = ${narrative_desc}, is_featured = ${is_featured},
|
||||
tech_stack = ${tech_stack}::jsonb, gallery = ${gallery}::jsonb
|
||||
WHERE id = ${id}
|
||||
`;
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
console.error('Error updating project:', e);
|
||||
return { error: 'Güncelleme başarısız: ' + (e.message || 'Bilinmeyen hata') };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServicesAdmin() {
|
||||
try {
|
||||
return await sql`SELECT * FROM services ORDER BY display_order ASC`;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteService(id: number) {
|
||||
try {
|
||||
await sql`DELETE FROM services WHERE id = ${id}`;
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { error: 'Silinemedi' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPartnersAdmin() {
|
||||
try {
|
||||
return await sql`SELECT * FROM partners ORDER BY display_order ASC`;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePartner(id: number) {
|
||||
try {
|
||||
await sql`DELETE FROM partners WHERE id = ${id}`;
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { error: 'Silinemedi' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPartnerAdmin(formData: FormData) {
|
||||
try {
|
||||
const name = formData.get('name') as string;
|
||||
const logo = formData.get('logo') as string;
|
||||
const display_order = Number(formData.get('display_order')) || 0;
|
||||
|
||||
await sql`
|
||||
INSERT INTO partners (name, logo, display_order)
|
||||
VALUES (${name}, ${logo}, ${display_order})
|
||||
`;
|
||||
return { success: true };
|
||||
} catch (e: any) {
|
||||
console.error('Error creating partner:', e);
|
||||
return { error: 'Ekleme başarısız: ' + (e.message || 'Bilinmeyen hata') };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePartner(id: number, name: string, display_order: number) {
|
||||
try {
|
||||
await sql`
|
||||
UPDATE partners
|
||||
SET name = ${name}, display_order = ${display_order}
|
||||
WHERE id = ${id}
|
||||
`;
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { error: 'Güncellenemedi' };
|
||||
}
|
||||
}
|
||||
|
||||
51
app/admin/login/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { login } from '../actions';
|
||||
import { Lock, ArrowRight } from 'lucide-react';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [state, formAction, pending] = useActionState(login, null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-3xl p-8 space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="w-16 h-16 bg-[#1e9a83]/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Lock className="w-8 h-8 text-[#1e9a83]" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-black uppercase tracking-widest text-white">Yönetici Girişi</h1>
|
||||
<p className="text-white/40 text-sm">Devam etmek için şifrenizi giriniz.</p>
|
||||
</div>
|
||||
|
||||
<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">Şifre</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
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="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state?.error && (
|
||||
<p className="text-red-500 text-xs font-bold text-center bg-red-500/10 py-2 rounded-lg">{state.error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="w-full bg-[#1e9a83] hover:bg-[#157a67] text-white font-bold py-3 px-4 rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{pending ? 'Giriş Yapılıyor...' : 'Giriş Yap'}
|
||||
{!pending && <ArrowRight className="w-4 h-4" />}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
app/contact/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import Contact from "@/components/Contact";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "İletişim",
|
||||
description: "Muğla Dijital ekibiyle iletişime geçin. Projeleriniz hakkında konuşmak veya teklif almak için bizimle hemen bağlantı kurun.",
|
||||
alternates: {
|
||||
canonical: "/contact",
|
||||
},
|
||||
openGraph: {
|
||||
title: "İletişim | Muğla Dijital",
|
||||
description: "Bize ulaşın ve dijital hedeflerinizi birlikte gerçekleştirelim.",
|
||||
url: "https://mugladijitalmedya.com/contact",
|
||||
}
|
||||
};
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#f5f5f0] text-black pt-12">
|
||||
<Contact />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
224
app/globals.css
Normal file
@@ -0,0 +1,224 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-primary: #1e9a83;
|
||||
--color-dark-bg: #f5f5f0;
|
||||
--color-glass-bg: rgba(0, 0, 0, 0.02);
|
||||
--color-glass-border: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
|
||||
|
||||
:root {
|
||||
--background: #f5f5f0;
|
||||
--foreground: #1a1a1a;
|
||||
--accent: #1e9a83;
|
||||
--border: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-martian), monospace;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ===== DIAGONAL LINE DECORATION ===== */
|
||||
.diagonal-line {
|
||||
position: relative;
|
||||
}
|
||||
.diagonal-line::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 141%;
|
||||
background: var(--border);
|
||||
transform: rotate(-25deg);
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
/* ===== EDITORIAL GRID ===== */
|
||||
.editorial-grid {
|
||||
display: grid;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.editorial-cell {
|
||||
border: 1px solid var(--border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: background 0.4s ease;
|
||||
}
|
||||
|
||||
.editorial-cell:hover {
|
||||
background: rgba(30, 154, 131, 0.03);
|
||||
}
|
||||
|
||||
/* ===== SECTION DIVIDER ===== */
|
||||
.section-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
/* ===== BUTTONS ===== */
|
||||
.button-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 2rem;
|
||||
border: 1px solid var(--foreground);
|
||||
font-family: var(--font-martian), monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.4s ease;
|
||||
color: var(--foreground);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 2rem;
|
||||
border: 1px solid var(--border);
|
||||
font-family: var(--font-martian), monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.4s ease;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.button-secondary:hover {
|
||||
border-color: var(--foreground);
|
||||
}
|
||||
|
||||
/* ===== NAV LINK HOVER ===== */
|
||||
.nav-link-hover {
|
||||
position: relative;
|
||||
}
|
||||
.nav-link-hover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: var(--foreground);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.nav-link-hover:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== ANIMATIONS ===== */
|
||||
@keyframes reveal-up {
|
||||
from {
|
||||
transform: translateY(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.reveal {
|
||||
animation: reveal-up 1s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
.reveal-delayed-1 { animation-delay: 0.1s; }
|
||||
.reveal-delayed-2 { animation-delay: 0.2s; }
|
||||
.reveal-delayed-3 { animation-delay: 0.3s; }
|
||||
.reveal-delayed-4 { animation-delay: 0.4s; }
|
||||
.reveal-delayed-5 { animation-delay: 0.5s; }
|
||||
|
||||
.stagger-item {
|
||||
opacity: 0;
|
||||
animation: reveal-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* ===== STROKE TEXT ===== */
|
||||
.stroke-text {
|
||||
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.15);
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.stroke-text:hover {
|
||||
-webkit-text-stroke: 1px var(--accent);
|
||||
}
|
||||
|
||||
/* ===== CUSTOM SCROLLBAR ===== */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* ===== MARQUEE ===== */
|
||||
@keyframes marquee {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
.animate-marquee {
|
||||
animation: marquee 30s linear infinite;
|
||||
}
|
||||
|
||||
.animate-marquee:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
/* ===== EDITORIAL TYPOGRAPHY ===== */
|
||||
.editorial-headline {
|
||||
font-family: var(--font-martian), monospace;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* ===== CELL DIAGONAL ===== */
|
||||
.cell-diagonal::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 141%;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
transform: rotate(-20deg);
|
||||
transform-origin: top left;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Glass utility for admin panel compatibility */
|
||||
@utility glass {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
154
app/layout.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Martian_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import ClientWrapper from "@/components/ClientWrapper";
|
||||
|
||||
const martianMono = Martian_Mono({
|
||||
variable: "--font-martian",
|
||||
subsets: ["latin"],
|
||||
weight: ["100", "200", "300", "400", "500", "600", "700", "800"],
|
||||
});
|
||||
|
||||
import sql from "@/lib/db";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
let settings: any = null;
|
||||
try {
|
||||
const result = await sql`SELECT site_name, site_description FROM settings WHERE id = 1 LIMIT 1`;
|
||||
if (result.length > 0) settings = result[0];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
const siteName = settings?.site_name || "Muğla Dijital Medya Ajansı";
|
||||
const siteDesc = settings?.site_description || "Muğla Dijital Medya Ajansı olarak sosyal medya yönetimi, reklam yönetimi, SEO, web tasarım ve dijital pazarlama çözümleriyle markanızı öne çıkarıyoruz.";
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mugladijitalmedya.com";
|
||||
|
||||
return {
|
||||
metadataBase: new URL(baseUrl),
|
||||
title: {
|
||||
default: "Muğla Dijital | Medya & Prodüksiyon Ajansı",
|
||||
template: "%s | Muğla Dijital"
|
||||
},
|
||||
description: "Muğla'nın öncü dijital medya ajansı. Drone çekimi, sosyal medya yönetimi ve profesyonel reklam çözümleri.",
|
||||
authors: [{ name: "Muğla Dijital" }],
|
||||
creator: "Muğla Dijital",
|
||||
publisher: "Muğla Dijital",
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "tr_TR",
|
||||
url: "https://mugladijitalmedya.com",
|
||||
siteName: "Muğla Dijital",
|
||||
title: "Muğla Dijital | Medya & Prodüksiyon Ajansı",
|
||||
description: "Profesyonel drone çekimi ve dijital medya çözümleri.",
|
||||
images: [
|
||||
{
|
||||
url: "/og-image.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Muğla Dijital Medya",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Muğla Dijital | Medya & Prodüksiyon Ajansı",
|
||||
description: "Dijital dünyada markanızı zirveye taşıyın.",
|
||||
images: ["/og-image.jpg"],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="tr">
|
||||
<body
|
||||
className={`${martianMono.variable} antialiased`}
|
||||
>
|
||||
<ClientWrapper>
|
||||
{children}
|
||||
</ClientWrapper>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Muğla Dijital Medya Ajansı",
|
||||
"url": "https://mugladijitalmedya.com",
|
||||
"logo": "https://mugladijitalmedya.com/logo.png",
|
||||
"sameAs": [
|
||||
"https://www.instagram.com/mugladijitalmedya/",
|
||||
// Add other social media links here
|
||||
],
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+90-XXX-XXX-XXXX", // Should be updated with real phone
|
||||
"contactType": "customer service",
|
||||
"areaServed": "TR",
|
||||
"availableLanguage": ["Turkish", "English"]
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"name": "Muğla Dijital Medya Ajansı",
|
||||
"image": "https://mugladijitalmedya.com/og-image.jpg",
|
||||
"@id": "https://mugladijitalmedya.com",
|
||||
"url": "https://mugladijitalmedya.com",
|
||||
"telephone": "+90-XXX-XXX-XXXX",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "Muğla",
|
||||
"addressLocality": "Muğla",
|
||||
"addressRegion": "Muğla",
|
||||
"postalCode": "48000",
|
||||
"addressCountry": "TR"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": 37.2153,
|
||||
"longitude": 28.3636
|
||||
},
|
||||
"openingHoursSpecification": {
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday"
|
||||
],
|
||||
"opens": "09:00",
|
||||
"closes": "18:00"
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
18
app/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Hero from "@/components/Hero";
|
||||
import SelectedWorks from "@/components/SelectedWorks";
|
||||
import Partners from "@/components/Partners";
|
||||
import ServicesGrid from "@/components/ServicesGrid";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="relative min-h-screen bg-[#f5f5f0] text-black">
|
||||
<Hero />
|
||||
<Partners />
|
||||
<ServicesGrid />
|
||||
<SelectedWorks />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
57
app/partners/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { getPartners } from "@/app/actions";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import { Metadata } from "next";
|
||||
import PartnersList from "@/components/PartnersList";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Partnerlerimiz | İş Ortaklarımız",
|
||||
description: "Muğla Dijital olarak birlikte değer yarattığımız iş ortaklarımız ve partnerlerimiz. Sektöründe öncü markalarla dijital büyüme hikayeleri.",
|
||||
alternates: {
|
||||
canonical: "/partners",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Partnerlerimiz | Muğla Dijital İş Ortakları",
|
||||
description: "Birlikte büyüdüğümüz markalar ve iş ortaklıklarımız.",
|
||||
images: ["/partners-og.jpg"],
|
||||
}
|
||||
};
|
||||
|
||||
export default async function PartnersPage() {
|
||||
const partners = await getPartners();
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-6">Partnerlerimiz</span>
|
||||
<h1 className="editorial-headline text-4xl md:text-6xl lg:text-[5.5rem] text-black uppercase">
|
||||
Birlikte <br /> <span className="text-primary">Büyüyoruz.</span>
|
||||
</h1>
|
||||
<p className="text-black/40 text-[14px] max-w-2xl leading-relaxed mt-10">
|
||||
Güçlü iş ortaklıkları, başarılı dijital hikayelerin temelidir. Sektöründe öncü markalarla birlikte değer üretiyor ve markaları geleceğe hazırlıyoruz.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PartnersList partners={(partners || []) as any} />
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-24 px-6 text-center border-t border-black/10">
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">İş Ortaklarımız Arasına <br /><span className="text-primary">Katılın.</span></h2>
|
||||
<p className="text-black/40 text-[13px] tracking-[0.1em]">
|
||||
Markanızı dijitalde bir üst seviyeye taşımak için doğru partneri arıyorsanız, yanınızdayız.
|
||||
</p>
|
||||
<a href="/contact" className="button-primary mx-auto inline-flex">
|
||||
İş Birliği Başlat
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
20
app/robots.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mugladijitalmedya.com";
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: '/admin/',
|
||||
},
|
||||
{
|
||||
userAgent: ['GPTBot', 'ChatGPT-User', 'PerplexityBot', 'ClaudeBot', 'anthropic-ai', 'Google-Extended'],
|
||||
allow: '/',
|
||||
disallow: '/admin/',
|
||||
}
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
50
app/services/page.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import sql from "@/lib/db";
|
||||
import ServicesClient from "@/components/ServicesClient";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Hizmetlerimiz",
|
||||
description: "Muğla Dijital olarak sunduğumuz profesyonel dijital medya, sosyal medya yönetimi ve reklam çözümlerimizi inceleyin.",
|
||||
alternates: {
|
||||
canonical: "/services",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Hizmetlerimiz | Muğla Dijital",
|
||||
description: "Dijital dünyada fark yaratan profesyonel hizmetlerimiz.",
|
||||
url: "https://mugladijitalmedya.com/services",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ServicesPage() {
|
||||
const services = await sql`SELECT * FROM services ORDER BY display_order ASC`;
|
||||
|
||||
const servicesSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"itemListElement": (services || []).map((service: any, index: number) => ({
|
||||
"@type": "ListItem",
|
||||
"position": index + 1,
|
||||
"item": {
|
||||
"@type": "Service",
|
||||
"name": service.title,
|
||||
"description": service.description,
|
||||
"provider": {
|
||||
"@type": "Organization",
|
||||
"name": "Muğla Dijital"
|
||||
}
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(servicesSchema) }}
|
||||
/>
|
||||
<ServicesClient services={services || []} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
app/sitemap.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import sql from '@/lib/db';
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mugladijitalmedya.com";
|
||||
|
||||
// Static pages
|
||||
const staticPages = [
|
||||
'',
|
||||
'/about',
|
||||
'/services',
|
||||
'/works',
|
||||
'/contact',
|
||||
].map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: route === '' ? 1 : 0.8,
|
||||
}));
|
||||
|
||||
// Dynamic projects
|
||||
const projects = await sql`SELECT slug, updated_at FROM projects`;
|
||||
|
||||
const projectPages = (projects || []).map((project) => ({
|
||||
url: `${baseUrl}/works/${project.slug}`,
|
||||
lastModified: new Date(project.updated_at || new Date()),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
return [...staticPages, ...projectPages];
|
||||
}
|
||||
29
app/works/[slug]/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getProjectBySlug } from "@/app/actions";
|
||||
import WorkDetailClient from "@/components/WorkDetailClient";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const data = await getProjectBySlug(slug);
|
||||
if (!data) return {};
|
||||
|
||||
const { project } = data;
|
||||
return {
|
||||
title: project.title,
|
||||
description: project.subtitle,
|
||||
openGraph: {
|
||||
title: `${project.title} | Muğla Dijital`,
|
||||
description: project.subtitle,
|
||||
images: [project.hero_image],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProjectDetailPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const data = await getProjectBySlug(slug);
|
||||
if (!data) notFound();
|
||||
|
||||
return <WorkDetailClient project={data.project} nextProject={data.nextProject} />;
|
||||
}
|
||||
24
app/works/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import sql from "@/lib/db";
|
||||
import WorksClient from "@/components/WorksClient";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Çalışmalarımız",
|
||||
description: "Muğla Dijital olarak hayata geçirdiğimiz seçkin projeleri, sosyal medya kampanyalarını ve dijital pazarlama başarı hikayelerini inceleyin.",
|
||||
alternates: {
|
||||
canonical: "/works",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Çalışmalarımız | Muğla Dijital",
|
||||
description: "Markalar için yarattığımız dijital başarı hikayeleri.",
|
||||
url: "https://mugladijitalmedya.com/works",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function WorksPage() {
|
||||
const projects = await sql`SELECT * FROM projects ORDER BY created_at DESC`;
|
||||
|
||||
return <WorksClient projects={projects || []} />;
|
||||
}
|
||||
36
check_db.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
import postgres from 'postgres';
|
||||
|
||||
const DATABASE_URL = "postgres://postgres:xGhPj4IuE5VocaxUoYAj1dSr2xf6M3hh3c2C6YbnB7ZOeVJLRvmL0mzCbhvf14dh@65.109.236.58:8392/postgres";
|
||||
|
||||
const sql = postgres(DATABASE_URL);
|
||||
|
||||
async function checkDb() {
|
||||
try {
|
||||
console.log('Connecting to database...');
|
||||
|
||||
const settings = await sql`SELECT * FROM settings LIMIT 1`;
|
||||
const servicesCount = await sql`SELECT COUNT(*) FROM services`;
|
||||
const projectsCount = await sql`SELECT COUNT(*) FROM projects`;
|
||||
const leadsCount = await sql`SELECT COUNT(*) FROM leads`;
|
||||
|
||||
console.log('\n--- Database Connection Successful ---');
|
||||
console.log('Settings:', settings[0]);
|
||||
console.log('Services Count:', servicesCount[0].count);
|
||||
console.log('Projects Count:', projectsCount[0].count);
|
||||
console.log('Leads Count:', leadsCount[0].count);
|
||||
|
||||
if (parseInt(projectsCount[0].count) > 0) {
|
||||
const projects = await sql`SELECT title, category FROM projects LIMIT 5`;
|
||||
console.log('\n--- Recent Projects ---');
|
||||
console.table(projects);
|
||||
}
|
||||
|
||||
await sql.end();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Database connection error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkDb();
|
||||
65
components/BottomBar.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { Instagram, Twitter } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface StatItemProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
sublabel: string;
|
||||
}
|
||||
|
||||
function StatItem({ icon, label, sublabel }: StatItemProps) {
|
||||
return (
|
||||
<div className="glass p-4 rounded-2xl flex items-center gap-4 min-w-[200px]">
|
||||
<div className="w-10 h-10 glass rounded-lg flex items-center justify-center text-white/40">
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-bold text-sm leading-tight">{label}</h4>
|
||||
<p className="text-white/40 text-[10px] font-medium">{sublabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BottomBar() {
|
||||
return (
|
||||
<div className="absolute bottom-0 left-0 w-full px-6 md:px-8 py-6 md:py-10 flex flex-col md:flex-row items-center md:items-end justify-between gap-6 pointer-events-none">
|
||||
{/* Stats Section - Hidden on mobile */}
|
||||
<div className="hidden md:flex items-center gap-4 pointer-events-auto">
|
||||
<StatItem
|
||||
icon={<div className="text-[#1e9a83]">📸</div>}
|
||||
label="500+"
|
||||
sublabel="Tamamlanan Çekim"
|
||||
/>
|
||||
<StatItem
|
||||
icon={<div className="text-[#1e9a83]">🏢</div>}
|
||||
label="150+"
|
||||
sublabel="Mutlu Müşteri"
|
||||
/>
|
||||
<StatItem
|
||||
icon={<div className="text-[#1e9a83]">🚁</div>}
|
||||
label="Drone"
|
||||
sublabel="Profesyonel Çekim"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Socials & Scroll Section */}
|
||||
<div className="flex flex-col items-center md:items-end gap-4 md:gap-6 pointer-events-auto mb-2">
|
||||
<div className="flex items-center gap-6">
|
||||
<span className="text-[10px] font-bold tracking-[0.2em] text-white/30 uppercase">Bizi Takip Edin</span>
|
||||
<div className="flex items-center gap-4 text-white/60">
|
||||
<Link href="#" className="hover:text-white transition-colors"><Instagram className="w-4 h-4" /></Link>
|
||||
<Link href="#" className="hover:text-white transition-colors"><Twitter className="w-4 h-4" /></Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="md:absolute md:bottom-4 md:left-8 text-[10px] text-white/20 font-medium text-center md:text-left">
|
||||
© {new Date().getFullYear()} Muğla Dijital Medya Ajansı. Tüm hakları saklıdır.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
components/Capabilities.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFeaturedServices } from "@/app/actions";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Layers
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Capabilities() {
|
||||
const [services, setServices] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchServices() {
|
||||
const data = await getFeaturedServices();
|
||||
|
||||
if (data && data.length > 0) {
|
||||
setServices(data);
|
||||
}
|
||||
}
|
||||
fetchServices();
|
||||
}, []);
|
||||
|
||||
const DynamicIcon = ({ name, className }: { name: string, className?: string }) => {
|
||||
const IconComponent = (LucideIcons as any)[name] || Layers;
|
||||
return <IconComponent className={className} />;
|
||||
};
|
||||
|
||||
// Fallback static data if database is empty
|
||||
const displayData = services.length > 0 ? services : [
|
||||
{ title: "Art Direction", description: "Conceptualizing visual narratives that resonate. We define the look and feel before the camera even rolls.", icon_name: "Palette" },
|
||||
{ title: "Cinematography", description: "Capturing light and shadow with state-of-the-art gear. 8K workflows and cinema-grade optics.", icon_name: "Video" },
|
||||
{ title: "Motion Graphics", description: "Adding kinetic energy to static visuals. 2D and 3D animation that enhances the storytelling.", icon_name: "Clapperboard" },
|
||||
{ title: "Color Grading", description: "Setting the mood with precise color science. We ensure your visuals look perfect on every screen.", icon_name: "Contrast" }
|
||||
];
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Left Content */}
|
||||
<div className="lg:w-1/3">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-6 text-white italic tracking-tighter uppercase">Yeteneklerimiz</h2>
|
||||
<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.
|
||||
</p>
|
||||
<Link
|
||||
href="/services"
|
||||
className="group flex items-center gap-2 text-[#1e9a83] font-semibold hover:text-white transition-colors"
|
||||
>
|
||||
Tüm Hizmetleri Gör
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Right Grid - Restored to Original Design */}
|
||||
<div className="lg:w-2/3 w-full border border-white/10 rounded-3xl overflow-hidden grid grid-cols-1 md:grid-cols-2">
|
||||
{services.map((service, index) => (
|
||||
<div
|
||||
key={service.id || index}
|
||||
className={`p-10 flex flex-col gap-4 hover:bg-white/[0.02] transition-colors border-white/10
|
||||
${index === 0 ? "border-b md:border-r" : ""}
|
||||
${index === 1 ? "border-b md:border-b" : ""}
|
||||
${index === 2 ? "md:border-r border-b md:border-b-0" : ""}
|
||||
${index === 3 ? "" : ""}
|
||||
`}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-[#1e9a83]/10 flex items-center justify-center text-[#1e9a83]">
|
||||
<DynamicIcon name={service.icon_name} className="w-6 h-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mt-2 uppercase italic tracking-tight">{service.title}</h3>
|
||||
<p className="text-white/40 leading-relaxed text-sm font-medium">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
16
components/ClientWrapper.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function ClientLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const isAdmin = pathname?.startsWith("/admin");
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isAdmin && <Navbar />}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
232
components/Contact.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { MapPin, Mail, Send, ChevronDown, Instagram, Twitter, Linkedin, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
import { getSettings, submitLead } from "@/app/actions";
|
||||
|
||||
export default function Contact() {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
projectType: "Drone Çekimi",
|
||||
message: ""
|
||||
});
|
||||
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [settings, setSettings] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
const data = await getSettings();
|
||||
if (data) setSettings(data);
|
||||
}
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
|
||||
try {
|
||||
const result = await submitLead(formData);
|
||||
|
||||
if (result.error) throw new Error(result.error);
|
||||
setStatus('success');
|
||||
setFormData({ firstName: "", lastName: "", email: "", projectType: "Drone Çekimi", message: "" });
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
setStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="min-h-screen py-24 px-6 md:px-12 bg-[#f5f5f0]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header Area */}
|
||||
<div className="mb-20">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-6">İletişim</span>
|
||||
<h1 className="editorial-headline text-4xl md:text-6xl text-black reveal opacity-0 uppercase">
|
||||
Markanızı Dijitalde <br /> <span className="text-primary">Büyütelim.</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start border-t border-black/10 pt-16">
|
||||
|
||||
{/* Left Side - Info */}
|
||||
<div className="lg:col-span-5 space-y-16">
|
||||
<div className="space-y-6">
|
||||
<p className="text-black/60 text-[14px] leading-relaxed max-w-sm">
|
||||
{settings?.site_description || "Dijital dünyada fark yaratmaya hazır mısınız? Sosyal medya yönetimi, reklam stratejileri veya SEO çözümleri için vizyonunuzu hayata geçirelim."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Channels */}
|
||||
<div className="space-y-10">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 border border-black/10 flex items-center justify-center text-primary shrink-0">
|
||||
<MapPin className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[10px] tracking-[0.2em] uppercase text-black/40 mb-2">Konum</h3>
|
||||
<p className="text-[13px] text-black font-medium leading-relaxed">
|
||||
{settings?.office_address || "Muğla / Marmaris\nDijital Medya Merkezi"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 border border-black/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Mail className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[10px] tracking-[0.2em] uppercase text-black/40 mb-2">E-Posta & Telefon</h3>
|
||||
<p className="text-[13px] text-black font-medium hover:text-primary transition-colors cursor-pointer mb-1">
|
||||
{settings?.contact_email || "hello@mugladijital.com"}
|
||||
</p>
|
||||
<p className="text-[13px] text-black font-medium">
|
||||
{settings?.contact_phone || "+90 (555) 000 00 00"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-[10px] tracking-[0.2em] uppercase text-black/40">Sosyal Medya</h3>
|
||||
<div className="flex gap-4">
|
||||
{[
|
||||
{ icon: Instagram, label: "Instagram", href: settings?.instagram_url },
|
||||
{ icon: Twitter, label: "Twitter", href: settings?.twitter_url },
|
||||
{ icon: Linkedin, label: "LinkedIn", href: settings?.linkedin_url }
|
||||
].map((social) => (
|
||||
<a
|
||||
key={social.label}
|
||||
href={social.href || "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 border border-black/10 flex items-center justify-center text-black/40 hover:text-primary hover:border-primary/50 transition-all"
|
||||
aria-label={social.label}
|
||||
>
|
||||
<social.icon className="w-4 h-4" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Form */}
|
||||
<div className="lg:col-span-7">
|
||||
<div className="border border-black/10 p-8 md:p-12 relative overflow-hidden bg-white/30 backdrop-blur-sm">
|
||||
{status === 'success' ? (
|
||||
<div className="flex flex-col items-center justify-center text-center py-12 space-y-6">
|
||||
<div className="w-16 h-16 border border-primary/20 flex items-center justify-center text-primary">
|
||||
<CheckCircle2 className="w-8 h-8" />
|
||||
</div>
|
||||
<h3 className="editorial-headline text-2xl text-black uppercase">Mesajınız Alındı!</h3>
|
||||
<p className="text-black/40 text-[12px] max-w-sm">
|
||||
Ekibimiz en kısa sürede sizinle iletişime geçecektir.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setStatus('idle')}
|
||||
className="button-secondary text-[11px]"
|
||||
>
|
||||
Yeni Mesaj Gönder
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] tracking-[0.2em] uppercase text-black/40 ml-1">Adınız</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
|
||||
placeholder="Adınız"
|
||||
className="w-full bg-transparent border-b border-black/10 py-3 text-[13px] text-black placeholder:text-black/10 outline-none focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] tracking-[0.2em] uppercase text-black/40 ml-1">Soyadınız</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
|
||||
placeholder="Soyadınız"
|
||||
className="w-full bg-transparent border-b border-black/10 py-3 text-[13px] text-black placeholder:text-black/10 outline-none focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] tracking-[0.2em] uppercase text-black/40 ml-1">E-posta Adresiniz</label>
|
||||
<input
|
||||
required
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder="ornek@email.com"
|
||||
className="w-full bg-transparent border-b border-black/10 py-3 text-[13px] text-black placeholder:text-black/10 outline-none focus:border-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 relative">
|
||||
<label className="text-[10px] tracking-[0.2em] uppercase text-black/40 ml-1">Hizmet Türü</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={formData.projectType}
|
||||
onChange={(e) => setFormData({ ...formData, projectType: e.target.value })}
|
||||
className="w-full bg-transparent border-b border-black/10 py-3 text-[13px] text-black/60 appearance-none outline-none focus:border-primary transition-colors cursor-pointer"
|
||||
>
|
||||
<option>Drone Çekimi</option>
|
||||
<option>Fotoğraf & Video Çekimi</option>
|
||||
<option>Düğün / Nişan Çekimi</option>
|
||||
<option>Sosyal Medya Yönetimi</option>
|
||||
<option>Meta Reklam Yönetimi</option>
|
||||
<option>Google Reklam Yönetimi</option>
|
||||
<option>Web Site Tasarımı</option>
|
||||
<option>SEO Optimizasyonu</option>
|
||||
<option>Otel Tanıtım Çekimi</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-0 top-1/2 -translate-y-1/2 w-4 h-4 text-black/20 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] tracking-[0.2em] uppercase text-black/40 ml-1">Mesajınız</label>
|
||||
<textarea
|
||||
required
|
||||
rows={4}
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
placeholder="Projenizden bahsedin..."
|
||||
className="w-full bg-transparent border-b border-black/10 py-3 text-[13px] text-black placeholder:text-black/10 outline-none focus:border-primary transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'error' && (
|
||||
<div className="flex items-center gap-2 text-red-600 text-[11px] font-bold bg-red-50 p-4 border border-red-100">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
Bir hata oluştu. Lütfen tekrar deneyin.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
disabled={status === 'loading'}
|
||||
className="button-primary w-full justify-center group disabled:opacity-50"
|
||||
>
|
||||
{status === 'loading' ? 'Gönderiliyor...' : 'Ücretsiz Analiz Talebi Gönder'}
|
||||
<Send className="w-3 h-3 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
39
components/DynamicLogo.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
interface DynamicLogoProps {
|
||||
src: string;
|
||||
color?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
size?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function DynamicLogo({
|
||||
src,
|
||||
color = "currentColor",
|
||||
width = "100%",
|
||||
height = "40px",
|
||||
size = "contain",
|
||||
className = ""
|
||||
}: DynamicLogoProps) {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
maskImage: `url(${src})`,
|
||||
WebkitMaskImage: `url(${src})`,
|
||||
maskRepeat: "no-repeat",
|
||||
maskPosition: "center",
|
||||
maskSize: size,
|
||||
WebkitMaskRepeat: "no-repeat",
|
||||
WebkitMaskPosition: "center",
|
||||
WebkitMaskSize: size,
|
||||
width: width,
|
||||
height: height,
|
||||
transition: "background-color 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
91
components/Footer.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getSettings } from "@/app/actions";
|
||||
import Link from "next/link";
|
||||
import DynamicLogo from "./DynamicLogo";
|
||||
|
||||
export default function Footer() {
|
||||
const [settings, setSettings] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
const data = await getSettings();
|
||||
if (data) setSettings(data);
|
||||
}
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<footer className="border-t border-black/10">
|
||||
{/* Main Footer Content */}
|
||||
<div className="max-w-7xl mx-auto px-6 md:px-12 py-16 md:py-20">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-8">
|
||||
|
||||
{/* Left Column - Logo & Description */}
|
||||
<div className="md:col-span-5 flex flex-col justify-between gap-12">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="inline-block">
|
||||
<div className="relative w-40 h-12">
|
||||
<DynamicLogo
|
||||
src="/logo.png"
|
||||
color="black"
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="contain"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-[11px] text-black/40 leading-[1.8] max-w-xs">
|
||||
{settings?.footer_description || "Muğla Dijital olarak markaların dijital dünyada özgün bir şekilde var olmasını sağlıyoruz. Drone çekimi, video prodüksiyon ve sosyal medya yönetimi ile gerçek bağlantılar kuruyoruz."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Center Column - Menu label + divider */}
|
||||
<div className="md:col-span-2 flex flex-col items-center gap-6 hidden md:flex">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-px h-8 bg-black/10" />
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/30">Menu</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Navigation Links */}
|
||||
<div className="md:col-span-5 flex flex-col items-start md:items-end gap-3">
|
||||
{[
|
||||
{ label: "Çalışmalar", href: "/works" },
|
||||
{ label: "Hizmetler", href: "/services" },
|
||||
{ label: "Partnerler", href: "/partners" },
|
||||
{ label: "Hakkımızda", href: "/about" },
|
||||
{ label: "İletişim", href: "/contact" },
|
||||
].map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="text-2xl md:text-4xl font-extrabold uppercase tracking-tight text-black hover:text-primary transition-colors leading-tight"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-black/10">
|
||||
<div className="max-w-7xl mx-auto px-6 md:px-12 py-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
{/* Copyright */}
|
||||
<p className="text-[10px] text-black/25 tracking-[0.05em]">
|
||||
© {settings?.site_name || "Muğla Dijital"} {new Date().getFullYear()}. Dijital çözümler üreten ajans.
|
||||
</p>
|
||||
|
||||
{/* Credit */}
|
||||
<a href="https://ayris.tech" target="_blank" rel="noopener noreferrer" className="text-[10px] text-black/25 tracking-[0.05em] hover:text-primary transition-colors">
|
||||
created by ayris.tech
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
64
components/Hero.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="pt-32 pb-0 px-6 md:px-12">
|
||||
{/* Main Headline */}
|
||||
<div className="max-w-7xl mx-auto py-16 md:py-24">
|
||||
<motion.h1
|
||||
initial={{ y: 40, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 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"
|
||||
>
|
||||
Dijital Varlık ve Görsel<br />
|
||||
Hikaye <span className="text-primary">Mimarlığı.</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 1, delay: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-[11px] md:text-[13px] tracking-[0.15em] text-black/40 max-w-2xl leading-[2] mt-8"
|
||||
>
|
||||
Profesyonel prodüksiyondan akıllı reklam yönetimine kadar markanızın tüm dijital ekosistemini estetikle şekillendiriyoruz.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 1.5, delay: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="section-divider origin-left"
|
||||
/>
|
||||
|
||||
{/* Bottom info row */}
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 1, delay: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 py-8 gap-8"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40">Konum</span>
|
||||
<span className="text-[11px] text-black/70">Muğla, Türkiye</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40">Hizmetler</span>
|
||||
<span className="text-[11px] text-black/70">Drone · Video · Sosyal Medya · Reklam</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 md:justify-end">
|
||||
<Link href="/contact" className="button-primary group">
|
||||
Teklif Al
|
||||
<ArrowRight className="w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
83
components/Navbar.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import DynamicLogo from "./DynamicLogo";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<motion.nav
|
||||
initial={{ y: -100 }}
|
||||
animate={{ y: 0 }}
|
||||
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 */}
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="relative w-36 h-10"
|
||||
>
|
||||
<DynamicLogo
|
||||
src="/logo.png"
|
||||
color="black"
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="contain"
|
||||
/>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{/* Center Nav */}
|
||||
<div className="hidden lg:flex items-center">
|
||||
{[
|
||||
{ label: "Çalışmalar", href: "/works" },
|
||||
{ label: "Hizmetler", href: "/services" },
|
||||
{ label: "Partnerler", href: "/partners" },
|
||||
{ label: "Hakkımızda", href: "/about" },
|
||||
{ label: "İletişim", href: "/contact" },
|
||||
].map((item, idx) => (
|
||||
<div key={item.label} className="flex items-center">
|
||||
{/* 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/60 hover:text-black transition-colors nav-link-hover px-3 py-2"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right - Brand name */}
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 }}
|
||||
className="text-[13px] font-bold tracking-[0.1em] uppercase text-black"
|
||||
>
|
||||
Muğla Dijital
|
||||
</motion.span>
|
||||
</div>
|
||||
</motion.nav>
|
||||
);
|
||||
}
|
||||
120
components/Partners.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getPartners } from "@/app/actions";
|
||||
import { motion } from "framer-motion";
|
||||
import DynamicLogo from "./DynamicLogo";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Partners() {
|
||||
const [partners, setPartners] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPartners() {
|
||||
const data = await getPartners();
|
||||
setPartners(data || []);
|
||||
}
|
||||
fetchPartners();
|
||||
}, []);
|
||||
|
||||
if (partners.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="border-y border-black/10 bg-[#f5f5f0] overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-6 md:px-12 flex items-stretch min-h-[100px] md:min-h-[140px]">
|
||||
{/* Left Label Section */}
|
||||
<motion.div
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
whileInView={{ x: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex items-center gap-4 py-6 md:py-8 pr-8 border-r border-black/10 shrink-0"
|
||||
>
|
||||
<span className="text-[9px] md:text-[10px] tracking-[0.2em] uppercase text-black/30 font-bold">Partners</span>
|
||||
</motion.div>
|
||||
|
||||
{/* 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 flex-nowrap items-center justify-start w-full gap-4 md:gap-10">
|
||||
{partners.slice(0, 5).map((partner, index) => {
|
||||
const content = (
|
||||
<motion.div
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 + 0.5 }}
|
||||
className="relative flex items-center justify-center group"
|
||||
>
|
||||
{partner.logo ? (
|
||||
<div className="relative h-[60px] md:h-[75px] w-[90px] md:w-[130px] overflow-hidden">
|
||||
<motion.div
|
||||
whileHover={{ y: "-50%" }}
|
||||
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex flex-col items-center h-[200%]"
|
||||
>
|
||||
{/* Normal State */}
|
||||
<div className="h-1/2 w-full flex items-center justify-center opacity-70">
|
||||
<DynamicLogo
|
||||
src={partner.logo}
|
||||
color="black"
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="220%"
|
||||
/>
|
||||
</div>
|
||||
{/* Hover State */}
|
||||
<div className="h-1/2 w-full flex items-center justify-center">
|
||||
<DynamicLogo
|
||||
src={partner.logo}
|
||||
color="#ff5c00" // primary color
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="220%"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[14px] md:text-[18px] tracking-tight font-medium text-black/30 whitespace-nowrap">
|
||||
{partner.name}
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
if (partner.project_slug) {
|
||||
return (
|
||||
<Link key={index} href={`/works/${partner.project_slug}`}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <div key={index}>{content}</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right CTA Section */}
|
||||
<motion.div
|
||||
initial={{ x: 20, opacity: 0 }}
|
||||
whileInView={{ x: 0, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex items-center py-6 md:py-8 pl-8 border-l border-black/10 shrink-0"
|
||||
>
|
||||
<a
|
||||
href="/partners"
|
||||
className="text-[10px] md:text-[11px] tracking-[0.1em] font-bold uppercase text-black hover:text-primary transition-colors flex items-center gap-2 group"
|
||||
>
|
||||
Hepsini Gör
|
||||
<motion.span
|
||||
animate={{ x: [0, 5, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
>
|
||||
→
|
||||
</motion.span>
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
92
components/PartnersList.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import DynamicLogo from "./DynamicLogo";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Partner {
|
||||
id: number;
|
||||
name: string;
|
||||
logo?: string;
|
||||
url?: string;
|
||||
project_slug?: string;
|
||||
}
|
||||
|
||||
export default function PartnersList({ partners }: { partners: Partner[] }) {
|
||||
return (
|
||||
<section className="py-24 px-6 md:px-12">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 border-t border-l border-black/10">
|
||||
{partners.map((partner, index) => {
|
||||
const card = (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="p-4 md:p-8 border-r border-b border-black/10 flex items-center justify-center transition-all group relative bg-white/10 overflow-hidden cursor-pointer"
|
||||
>
|
||||
<div className="relative h-[140px] md:h-[180px] w-full overflow-hidden">
|
||||
<motion.div
|
||||
whileHover={{ y: "-50%" }}
|
||||
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex flex-col items-center h-[200%]"
|
||||
>
|
||||
{/* First state */}
|
||||
<div className="h-1/2 w-full flex items-center justify-center opacity-70 group-hover:opacity-100 transition-all">
|
||||
{partner.logo ? (
|
||||
<DynamicLogo
|
||||
src={partner.logo}
|
||||
color="black"
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="140%"
|
||||
/>
|
||||
) : (
|
||||
<span className="editorial-headline text-xl md:text-2xl text-black uppercase">
|
||||
{partner.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Second state (Slot effect) */}
|
||||
<div className="h-1/2 w-full flex items-center justify-center">
|
||||
{partner.logo ? (
|
||||
<DynamicLogo
|
||||
src={partner.logo}
|
||||
color="#ff5c00" // primary color
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="140%"
|
||||
/>
|
||||
) : (
|
||||
<span className="editorial-headline text-xl md:text-2xl text-primary uppercase">
|
||||
{partner.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Subtle diagonal hover effect */}
|
||||
<div className="absolute top-0 right-0 w-8 h-8 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||
<div className="absolute top-0 right-0 w-px h-12 bg-black/5 rotate-[-45deg] origin-top-right" />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
if (partner.project_slug) {
|
||||
return (
|
||||
<Link key={partner.id} href={`/works/${partner.project_slug}`} className="block">
|
||||
{card}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <div key={partner.id}>{card}</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
108
components/SelectedWorks.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Loader2 } from "lucide-react";
|
||||
import { getFeaturedProjects } from "@/app/actions";
|
||||
|
||||
interface ProjectCardProps {
|
||||
hero_image: string;
|
||||
category: string;
|
||||
title: string;
|
||||
year: string;
|
||||
subtitle: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
function ProjectCard({ hero_image, category, title, year, subtitle, slug }: ProjectCardProps) {
|
||||
return (
|
||||
<Link href={`/works/${slug}`} className="group cursor-pointer block">
|
||||
<div className="space-y-5">
|
||||
<div className="relative aspect-video overflow-hidden bg-black/5 border border-black/10">
|
||||
<Image
|
||||
src={hero_image || "https://images.unsplash.com/photo-1550745165-9bc0b252726f"}
|
||||
alt={title}
|
||||
fill
|
||||
className="object-cover transition-all duration-700 group-hover:scale-105 grayscale group-hover:grayscale-0"
|
||||
/>
|
||||
|
||||
{/* Category Badge */}
|
||||
<div className="absolute top-4 right-4">
|
||||
<span className="bg-white/90 backdrop-blur-sm border border-black/10 px-3 py-1 text-[9px] tracking-[0.15em] uppercase text-black/60">
|
||||
{category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Below Image */}
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-[14px] tracking-[0.05em] uppercase text-black group-hover:text-primary transition-colors leading-tight">
|
||||
{title}
|
||||
</h3>
|
||||
<span className="text-[10px] text-black/30 tracking-[0.1em] uppercase mt-0.5">
|
||||
{year}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-black/40 leading-relaxed line-clamp-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SelectedWorks() {
|
||||
const [projects, setProjects] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchFeatured() {
|
||||
setLoading(true);
|
||||
const data = await getFeaturedProjects();
|
||||
|
||||
if (data && data.length > 0) {
|
||||
setProjects(data);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
fetchFeatured();
|
||||
}, []);
|
||||
|
||||
if (loading) return (
|
||||
<div className="py-32 flex justify-center">
|
||||
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (projects.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="py-24 px-6 md:px-12 border-t border-black/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-8 mb-16">
|
||||
<div>
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-4">Son Projeler</span>
|
||||
<h2 className="editorial-headline text-4xl md:text-5xl text-black">
|
||||
Çalışma Örnekleri
|
||||
</h2>
|
||||
</div>
|
||||
<Link href="/works" className="button-primary group">
|
||||
Tüm Portfolyo
|
||||
<ArrowRight className="w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
{projects.map((project, index) => (
|
||||
<ProjectCard key={index} {...project} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
128
components/ServicesClient.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Layers,
|
||||
Search,
|
||||
Clapperboard,
|
||||
Camera,
|
||||
Zap
|
||||
} from "lucide-react";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const processSteps = [
|
||||
{ icon: Search, title: "Analiz", desc: "İşletmenizi ve hedef kitlenizi analiz ediyoruz." },
|
||||
{ icon: Clapperboard, title: "Strateji", desc: "Size özel dijital strateji planlıyoruz." },
|
||||
{ icon: Camera, title: "Uygulama", desc: "Çekim, tasarım ve kampanya yönetimi." },
|
||||
{ icon: Zap, title: "Raporlama", desc: "Sonuçları ölçüyor ve optimize ediyoruz." }
|
||||
];
|
||||
|
||||
export default function ServicesClient({ services: initialServices }: { services: any[] }) {
|
||||
const [services] = useState<any[]>(initialServices);
|
||||
|
||||
const DynamicIcon = ({ name, className }: { name: string, className?: string }) => {
|
||||
const IconComponent = (LucideIcons as any)[name] || Layers;
|
||||
return <IconComponent className={className} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#f5f5f0] text-black pt-24">
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="pt-24 pb-16 px-6 md:px-12 border-b border-black/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-6">Hizmetlerimiz</span>
|
||||
<h1 className="editorial-headline text-4xl md:text-6xl lg:text-[5.5rem] text-black reveal opacity-0 uppercase">
|
||||
Drone · Video <br /> <span className="text-primary">Reklam · Dijital</span>
|
||||
</h1>
|
||||
<p className="text-black/40 text-[14px] max-w-2xl leading-relaxed mt-10 reveal reveal-delayed-1 opacity-0">
|
||||
Profesyonel drone çekimlerinden sosyal medya yönetimine, Google reklamlarından web tasarıma — işletmenizin dijital dünyada parlaması için ihtiyacınız olan her şey burada.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Services Grid - Editorial Bento */}
|
||||
<section className="py-24 px-6 md:px-12 border-b border-black/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 border-t border-l border-black/10">
|
||||
{services.map((item) => (
|
||||
<div key={item.id} className="p-10 border-r border-b border-black/10 relative group hover:bg-black/[0.01] transition-colors overflow-hidden flex flex-col justify-between min-h-[350px]">
|
||||
<div>
|
||||
<div className="w-10 h-10 border border-black/10 flex items-center justify-center text-primary mb-8">
|
||||
<DynamicIcon name={item.icon_name} className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="editorial-headline text-2xl text-black mb-4 uppercase">{item.title}</h3>
|
||||
<p className="text-[12px] text-black/40 leading-relaxed font-medium mb-8">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{(item.sub_services || []).slice(0, 3).map((sub: string) => (
|
||||
<li key={sub} className="flex items-center gap-3 text-[9px] font-bold tracking-widest text-black/20 uppercase">
|
||||
<div className="w-1 h-1 rounded-full bg-primary" />
|
||||
{sub}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Subtle diagonal on hover */}
|
||||
<div className="absolute top-0 right-0 w-16 h-16 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="absolute top-0 right-0 w-px h-24 bg-black/5 rotate-[-45deg] origin-top-right" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Process - Editorial Layout */}
|
||||
<section className="py-24 px-6 md:px-12 border-b border-black/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-20">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-4">İş Akışımız</span>
|
||||
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">Süreç Nasıl İlerler?</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 border-t border-black/10">
|
||||
{processSteps.map((step, idx) => (
|
||||
<div key={step.title} className="py-12 md:px-8 border-b md:border-b-0 md:border-r border-black/10 last:border-r-0 group">
|
||||
<div className="text-4xl font-black text-black/5 mb-8 group-hover:text-primary/20 transition-colors">0{idx + 1}</div>
|
||||
<div className="w-8 h-8 border border-black/10 flex items-center justify-center text-primary mb-6">
|
||||
<step.icon className="w-4 h-4" />
|
||||
</div>
|
||||
<h3 className="editorial-headline text-lg text-black mb-4 uppercase">{step.title}</h3>
|
||||
<p className="text-[12px] text-black/40 leading-relaxed">{step.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24 px-6 text-center">
|
||||
<div className="max-w-4xl mx-auto space-y-12">
|
||||
<h2 className="editorial-headline text-4xl md:text-6xl text-black uppercase leading-tight">
|
||||
Sıradışı Bir Şeyler <br /> <span className="text-primary">Yaratmaya Hazır Mısınız?</span>
|
||||
</h2>
|
||||
<p className="text-black/40 text-[13px] tracking-[0.1em] max-w-xl mx-auto">
|
||||
Drone çekimi, sosyal medya yönetimi veya dijital reklam — ne ihtiyacınız varsa, birlikte çözüm üretelim.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-6">
|
||||
<Link href="/contact" className="button-primary px-10">
|
||||
Hemen Başlayalım
|
||||
</Link>
|
||||
<Link href="/works" className="button-secondary px-10">
|
||||
Portfolyomuz
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
143
components/ServicesGrid.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFeaturedServices } from "@/app/actions";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function ServicesGrid() {
|
||||
const [services, setServices] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchServices() {
|
||||
const data = await getFeaturedServices();
|
||||
if (data && data.length > 0) setServices(data);
|
||||
}
|
||||
fetchServices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="border-t border-black/10">
|
||||
<div className="max-w-7xl mx-auto px-6 md:px-12">
|
||||
{/* Grid - Editorial bento layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 border-b border-black/10">
|
||||
|
||||
{/* Left large cell with image */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1 }}
|
||||
className="md:col-span-4 border-b md:border-b-0 md:border-r border-black/10 relative overflow-hidden group"
|
||||
>
|
||||
<div className="relative aspect-square md:aspect-auto md:h-full min-h-[400px]">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1473968512647-3e447244af8f?q=80&w=800"
|
||||
alt="Drone çekimi"
|
||||
fill
|
||||
className="object-cover grayscale group-hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right content area */}
|
||||
<div className="md:col-span-8 grid grid-cols-1 md:grid-cols-2">
|
||||
{/* Top-left cell: Services list */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="p-8 md:p-12 border-b border-r border-black/10 flex flex-col justify-between min-h-[200px]"
|
||||
>
|
||||
<div>
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-6">Hizmetler</span>
|
||||
<div className="space-y-2">
|
||||
{["Sosyal Medya Yönetimi", "Drone Çekimi", "Video Prodüksiyon", "Reklam Yönetimi", "SEO"].map((item) => (
|
||||
<div key={item} className="text-[12px] text-black/60 hover:text-primary transition-colors cursor-pointer">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Top-right cell: Capabilities */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="p-8 md:p-12 border-b border-black/10 flex flex-col justify-between min-h-[200px]"
|
||||
>
|
||||
<div>
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-6">Çözümler</span>
|
||||
<div className="space-y-2">
|
||||
{["Fotoğraf Çekimi", "Web Tasarım", "Otel Tanıtım", "Düğün Çekimi", "İçerik Üretimi"].map((item) => (
|
||||
<div key={item} className="text-[12px] text-black/60 hover:text-primary transition-colors cursor-pointer">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bottom-left cell with diagonal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="p-8 md:p-12 border-r border-black/10 relative cell-diagonal min-h-[200px] flex items-end"
|
||||
>
|
||||
<h3 className="editorial-headline text-2xl md:text-3xl text-black uppercase">
|
||||
Profesyonel<br />
|
||||
Prodüksiyon
|
||||
</h3>
|
||||
</motion.div>
|
||||
|
||||
{/* Bottom-right cell */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.8 }}
|
||||
className="p-8 md:p-12 relative cell-diagonal min-h-[200px] flex items-end"
|
||||
>
|
||||
<h3 className="editorial-headline text-2xl md:text-3xl text-black uppercase">
|
||||
Dijital<br />
|
||||
Pazarlama
|
||||
</h3>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description row */}
|
||||
<div className="py-8 grid grid-cols-1 md:grid-cols-12 gap-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="md:col-span-4 flex items-center"
|
||||
>
|
||||
<p className="text-[12px] text-black/50 leading-relaxed">
|
||||
Drone çekimlerinden sosyal medya yönetimine, markanızın dijitaldeki her ihtiyacını karşılıyoruz.
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="md:col-span-4 md:col-start-9 flex items-center md:justify-end"
|
||||
>
|
||||
<Link href="/services" className="button-primary group">
|
||||
Tüm Hizmetler
|
||||
<span className="group-hover:translate-x-1 transition-transform">→</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
193
components/WorkDetailClient.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
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 }) {
|
||||
const isInstagram = (url: string) => {
|
||||
return url.includes('instagram.com');
|
||||
};
|
||||
|
||||
const isVideo = (url: string) => {
|
||||
return url.includes('youtube.com') || url.includes('vimeo.com') || url.endsWith('.mp4');
|
||||
};
|
||||
|
||||
// Ultra-safety check for gallery and category arrays
|
||||
const parseSafe = (data: any) => {
|
||||
let current = data;
|
||||
try {
|
||||
// Aggressively parse up to 3 times to handle double-quotes and double-encoding
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (typeof current === 'string' && (current.trim().startsWith('[') || current.trim().startsWith('"'))) {
|
||||
current = JSON.parse(current);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
} catch (e) {
|
||||
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
}
|
||||
};
|
||||
|
||||
const gallery = parseSafe(project.gallery);
|
||||
const categories = parseSafe(project.category);
|
||||
|
||||
// Helper to get Instagram embed URL
|
||||
const getInstaEmbedUrl = (url: string) => {
|
||||
const cleanUrl = url.split('?')[0];
|
||||
return `${cleanUrl}${cleanUrl.endsWith('/') ? '' : '/'}embed`;
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#f5f5f0] text-black pt-24">
|
||||
|
||||
{/* 1. Editorial Header Section */}
|
||||
<section className="pt-32 pb-20 px-6 md:px-12 border-b border-black/10">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Top Line */}
|
||||
<div className="w-full h-px bg-black/10 mb-20" />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
|
||||
{/* Left: Client Name */}
|
||||
<div className="lg:col-span-5">
|
||||
<span className="text-[10px] tracking-[0.3em] uppercase text-black/30 block mb-6">
|
||||
{categories.join(" / ")} / {project.year}
|
||||
</span>
|
||||
<h1 className="editorial-headline text-5xl md:text-7xl lg:text-8xl text-black uppercase leading-none">
|
||||
{project.client || project.title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Middle: Minimalist Line (Reference style) */}
|
||||
<div className="hidden lg:block lg:col-span-2 pt-12">
|
||||
<div className="w-20 h-px bg-black/40 mx-auto" />
|
||||
</div>
|
||||
|
||||
{/* Right: Description */}
|
||||
<div className="lg:col-span-5 pt-2 lg:pt-8">
|
||||
<p className="text-[15px] md:text-[18px] text-black/70 leading-[1.8] font-medium italic">
|
||||
{project.narrative_desc || project.subtitle}
|
||||
</p>
|
||||
<div className="mt-12 flex flex-wrap gap-x-8 gap-y-4">
|
||||
<div>
|
||||
<h4 className="text-[9px] tracking-[0.2em] uppercase text-black/30 mb-1">Rol</h4>
|
||||
<span className="text-[11px] font-bold uppercase">{project.role || "Kreatif Direktörlük"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[9px] tracking-[0.2em] uppercase text-black/30 mb-1">Konum</h4>
|
||||
<span className="text-[11px] font-bold uppercase">{project.location || "Muğla / Dijital"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Line */}
|
||||
<div className="w-full h-px bg-black/10 mt-20" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. Project Feed (Gallery & Instagram) */}
|
||||
<section className="py-24 px-6 md:px-12">
|
||||
<div className="max-w-4xl mx-auto space-y-32">
|
||||
{/* 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 w-full h-full overflow-hidden">
|
||||
<Image
|
||||
src={project.hero_image || "https://images.unsplash.com/photo-1550745165-9bc0b252726f"}
|
||||
alt={project.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gallery / Instagram Feed */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-24 mt-24">
|
||||
{gallery.map((item: string, idx: number) => (
|
||||
<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">
|
||||
<span>0{idx + 1}</span>
|
||||
<div className="h-px flex-grow bg-black/5" />
|
||||
<span>{isInstagram(item) ? "INSTAGRAM FEED" : "PRODUCTION VIEW"}</span>
|
||||
</div>
|
||||
|
||||
{isInstagram(item) ? (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-[540px] border border-black/10 bg-white shadow-xl overflow-hidden rounded-xl">
|
||||
<iframe
|
||||
src={getInstaEmbedUrl(item)}
|
||||
className="w-full h-[700px] border-0"
|
||||
scrolling="no"
|
||||
allowTransparency={true}
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative aspect-[16/10] border border-black/5 bg-black/5 shadow-lg overflow-hidden group">
|
||||
{isVideo(item) ? (
|
||||
<iframe
|
||||
src={item}
|
||||
className="w-full h-full"
|
||||
allowFullScreen
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={item}
|
||||
alt={`Gallery ${idx}`}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Minimal CTA Section */}
|
||||
<section className="py-32 px-6 md:px-12 bg-[#f5f5f0]">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<div className="w-full h-px bg-black/10 mb-24" />
|
||||
<h2 className="editorial-headline text-3xl md:text-5xl lg:text-6xl text-black uppercase leading-tight mb-16 max-w-4xl mx-auto">
|
||||
CESUR, İNSANCIL VE <br /> İZ BIRAKAN PROJELERİ <br /> <span className="text-primary">BİRLİKTE</span> ŞEKİLLENDİRELİM.
|
||||
</h2>
|
||||
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center justify-center w-32 h-32 md:w-40 md:h-40 rounded-full bg-primary text-white text-[10px] font-bold uppercase tracking-widest hover:scale-110 transition-all duration-500 shadow-xl shadow-primary/20 group"
|
||||
>
|
||||
<span className="group-hover:scale-110 transition-transform">İLETİŞİME GEÇ</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Next Project - Editorial Style */}
|
||||
{nextProject && (
|
||||
<section className="relative border-y border-black/10 group overflow-hidden bg-[#f9f6ef] hover:bg-[#e2d1c1] transition-colors duration-700">
|
||||
<Link href={`/works/${nextProject.slug}`} className="block py-40 px-6 md:px-12">
|
||||
<div className="max-w-7xl mx-auto text-center space-y-12">
|
||||
<span className="text-[10px] tracking-[0.5em] text-black/30 uppercase block">SIRADAKİ ÇALIŞMA</span>
|
||||
<h2 className="editorial-headline text-5xl md:text-8xl lg:text-9xl text-black uppercase transition-all duration-700 group-hover:scale-[0.98]">
|
||||
{nextProject.title}
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-6 text-black/40 font-bold text-[12px] tracking-[0.3em] uppercase">
|
||||
<div className="w-12 h-px bg-black/20" />
|
||||
PROJEYİ GÖR
|
||||
<div className="w-12 h-px bg-black/20" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
187
components/WorksClient.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
interface ProjectCardProps {
|
||||
hero_image: string;
|
||||
category: string;
|
||||
title: string;
|
||||
year: string;
|
||||
client: string;
|
||||
slug: string;
|
||||
narrative_desc: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function ProjectCard({ hero_image, category, title, year, client, slug, narrative_desc, index }: ProjectCardProps) {
|
||||
// Alternating background colors like in the reference screenshot
|
||||
const bgColors = ["bg-[#e2d1c1]", "bg-[#f9f6ef]", "bg-[#d8c7b8]", "bg-[#f5f1e8]"];
|
||||
const bgColor = bgColors[index % bgColors.length];
|
||||
|
||||
return (
|
||||
<Link href={`/works/${slug}`} className="group cursor-pointer block h-full">
|
||||
<div className={`p-8 md:p-10 h-full flex flex-col transition-all duration-500 group-hover:translate-y-[-8px] ${bgColor}`}>
|
||||
{/* 1. Image Area */}
|
||||
<div className="relative aspect-[16/10] overflow-hidden mb-12 shadow-sm">
|
||||
<Image
|
||||
src={hero_image || "https://images.unsplash.com/photo-1536440136628-849c177e76a1"}
|
||||
alt={title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 group-hover:scale-105"
|
||||
/>
|
||||
{/* Floating Title on Image like reference */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center p-6 bg-black/10">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-white/80 mb-2">
|
||||
{(() => {
|
||||
let current = category;
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (typeof current === 'string' && (current.trim().startsWith('[') || current.trim().startsWith('"'))) {
|
||||
current = JSON.parse(current);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Array.isArray(current) ? current.join(" / ") : current;
|
||||
} catch (e) {
|
||||
return current;
|
||||
}
|
||||
})()}
|
||||
</span>
|
||||
<h3 className="editorial-headline text-3xl md:text-4xl text-white uppercase leading-tight drop-shadow-md">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Description Area */}
|
||||
<div className="flex-grow mb-12">
|
||||
<p className="text-black/70 text-[13px] leading-[1.8] line-clamp-6 font-medium italic">
|
||||
{narrative_desc || "Dijital dünyada markanızın sesini duyurmak ve özgün bir kimlik kazandırmak için tasarladığımız özel projelerimizden biri."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 3. Footer / Client Area */}
|
||||
<div className="border-t border-black/10 pt-8 mt-auto">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<span className="text-[9px] tracking-[0.2em] uppercase text-black/30 block mb-2">Müşteri</span>
|
||||
<h4 className="editorial-headline text-3xl text-black uppercase opacity-80 group-hover:opacity-100 group-hover:text-primary transition-all">
|
||||
{client || "Muğla Dijital"}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="text-[10px] font-black text-black/20 group-hover:text-primary transition-colors border border-black/5 px-2 py-1 rounded">
|
||||
{year}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorksClient({ projects: initialProjects }: { projects: any[] }) {
|
||||
const [activeCategory, setActiveCategory] = useState("Hepsi");
|
||||
|
||||
const categories = ["Hepsi", ...Array.from(new Set(initialProjects.flatMap(p => {
|
||||
let current = p.category;
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (typeof current === 'string' && (current.trim().startsWith('[') || current.trim().startsWith('"'))) {
|
||||
current = JSON.parse(current);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
})))];
|
||||
|
||||
const filteredProjects = activeCategory === "Hepsi"
|
||||
? initialProjects
|
||||
: initialProjects.filter(p => {
|
||||
let current = p.category;
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (typeof current === 'string' && (current.trim().startsWith('[') || current.trim().startsWith('"'))) {
|
||||
current = JSON.parse(current);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
const cats = Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
return cats.includes(activeCategory);
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#f5f5f0] text-black pt-24">
|
||||
<section className="pt-24 pb-24 px-6 md:px-12">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header Area */}
|
||||
<div className="mb-20">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-black/40 block mb-6">Portfolyo</span>
|
||||
<h1 className="editorial-headline text-4xl md:text-6xl lg:text-[5.5rem] text-black reveal opacity-0 uppercase">
|
||||
Seçilmiş <br /> <span className="text-primary">Projeler.</span>
|
||||
</h1>
|
||||
<p className="text-black/40 text-[14px] max-w-2xl leading-relaxed mt-10 reveal reveal-delayed-1 opacity-0">
|
||||
Markanızın dijital dünyadaki serüvenini profesyonel dokunuşlarla şekillendiriyoruz. Global vizyon, yerel strateji ile başarı hikayeleri yazıyoruz.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-8 mb-16 border-y border-black/10 py-8">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
className={`px-6 py-2 rounded-full text-[10px] font-bold uppercase tracking-widest transition-all border
|
||||
${activeCategory === cat
|
||||
? "bg-black text-white border-black"
|
||||
: "text-black/40 border-black/10 hover:border-black/30 hover:text-black"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<span className="text-[10px] font-bold text-black/20 uppercase tracking-widest">
|
||||
{filteredProjects.length} Proje
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-12">
|
||||
{filteredProjects.map((project, index) => (
|
||||
<ProjectCard key={index} {...project} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredProjects.length === 0 && (
|
||||
<div className="py-32 text-center border border-black/10">
|
||||
<p className="text-black/40 text-sm">Bu kategoride henüz bir proje bulunmuyor.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA Section Area */}
|
||||
<div className="mt-32 border-t border-black/10 pt-24 text-center space-y-10">
|
||||
<h2 className="editorial-headline text-3xl md:text-5xl text-black uppercase">Sıradaki Başarı Hikayesi <br /><span className="text-primary">Sizinle</span> Yazılsın.</h2>
|
||||
<Link href="/contact" className="button-primary mx-auto">
|
||||
Projeyi Başlat
|
||||
<ArrowUpRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
13
lib/db.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import postgres from 'postgres';
|
||||
|
||||
const globalForSql = global as unknown as { sql: postgres.Sql<{}> };
|
||||
|
||||
const sql = globalForSql.sql || postgres(process.env.DATABASE_URL!, {
|
||||
max: 10, // Limit maximum connections
|
||||
idle_timeout: 20, // Close idle connections after 20 seconds
|
||||
connect_timeout: 10,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForSql.sql = sql;
|
||||
|
||||
export default sql;
|
||||
6
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
26
next.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'lh3.googleusercontent.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'llnpipqrizjlvufxhfwf.supabase.co',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'res.cloudinary.com',
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6684
package-lock.json
generated
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "mugladijitall",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"cloudinary": "^2.10.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"framer-motion": "^12.4.7",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.0",
|
||||
"postgres": "^3.4.9",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
27
proxy.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
const isAdminRoute = request.nextUrl.pathname.startsWith('/admin');
|
||||
const isLoginRoute = request.nextUrl.pathname === '/admin/login';
|
||||
|
||||
if (isAdminRoute && !isLoginRoute) {
|
||||
const session = request.cookies.get('admin_session');
|
||||
if (!session || session.value !== 'authenticated') {
|
||||
return NextResponse.redirect(new URL('/admin/login', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoginRoute) {
|
||||
const session = request.cookies.get('admin_session');
|
||||
if (session && session.value === 'authenticated') {
|
||||
return NextResponse.redirect(new URL('/admin', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/admin/:path*',
|
||||
};
|
||||
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/partnerlogo/1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/partnerlogo/10.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/partnerlogo/11.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/partnerlogo/12.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/partnerlogo/13.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/partnerlogo/14.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/partnerlogo/15.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/partnerlogo/16.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/partnerlogo/17.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/partnerlogo/18.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/partnerlogo/19.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/partnerlogo/2.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/partnerlogo/20.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/partnerlogo/21.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/partnerlogo/22.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/partnerlogo/23.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/partnerlogo/24.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/partnerlogo/25.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/partnerlogo/3.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/partnerlogo/4.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/partnerlogo/5.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/partnerlogo/6.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/partnerlogo/7.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/partnerlogo/8.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/partnerlogo/9.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
59
schema.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_name VARCHAR(255),
|
||||
site_description TEXT,
|
||||
office_address TEXT,
|
||||
contact_email VARCHAR(255),
|
||||
contact_phone VARCHAR(50),
|
||||
instagram_url VARCHAR(255),
|
||||
twitter_url VARCHAR(255),
|
||||
linkedin_url VARCHAR(255),
|
||||
status_badge_text VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS services (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
icon_name VARCHAR(100),
|
||||
display_order INTEGER DEFAULT 0,
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
slug VARCHAR(255) UNIQUE NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
subtitle TEXT,
|
||||
category VARCHAR(100),
|
||||
year VARCHAR(50),
|
||||
hero_image VARCHAR(255),
|
||||
client VARCHAR(255),
|
||||
role VARCHAR(255),
|
||||
location VARCHAR(255),
|
||||
narrative_title VARCHAR(255),
|
||||
narrative_desc TEXT,
|
||||
tech_stack JSONB DEFAULT '[]'::jsonb,
|
||||
gallery JSONB DEFAULT '[]'::jsonb,
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS leads (
|
||||
id SERIAL PRIMARY KEY,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
service_type VARCHAR(255),
|
||||
message TEXT,
|
||||
status VARCHAR(50) DEFAULT 'new',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert default settings row if not exists
|
||||
INSERT INTO settings (id, site_name, site_description, office_address, contact_email, contact_phone, status_badge_text)
|
||||
VALUES (1, 'Muğla Dijital Medya Ajansı', 'Sosyal medya yönetimi, reklam yönetimi, SEO, web tasarım ve dijital pazarlama çözümleriyle markanızı öne çıkarıyoruz.', 'Muğla / Marmaris', 'hello@mugladijital.com', '+90 555 000 0000', '2025 Proje Başvuruları Açık')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
24
scratch/update_contact.js
Normal file
@@ -0,0 +1,24 @@
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
const postgres = require('postgres');
|
||||
|
||||
const sql = postgres(process.env.DATABASE_URL);
|
||||
|
||||
async function updateSettings() {
|
||||
try {
|
||||
await sql`
|
||||
UPDATE settings
|
||||
SET
|
||||
contact_email = 'info@mugladijitalmedya.com',
|
||||
contact_phone = '0545 431 10 24',
|
||||
office_address = 'Emirbeyazıt, Fevzi Altınay Sk. Evim apt no 3, 48050 Menteşe/Muğla'
|
||||
WHERE id = (SELECT id FROM settings LIMIT 1)
|
||||
`;
|
||||
console.log('İletişim bilgileri başarıyla güncellendi.');
|
||||
} catch (error) {
|
||||
console.error('Hata:', error);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
updateSettings();
|
||||
90
seed_services.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
import postgres from 'postgres';
|
||||
|
||||
const DATABASE_URL = "postgres://postgres:xGhPj4IuE5VocaxUoYAj1dSr2xf6M3hh3c2C6YbnB7ZOeVJLRvmL0mzCbhvf14dh@65.109.236.58:8392/postgres";
|
||||
|
||||
const sql = postgres(DATABASE_URL);
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: 'Sosyal Medya Yönetimi',
|
||||
description: 'Markanıza özel içerikler üreterek sosyal medya hesaplarınızı profesyonel şekilde yönetiyor, etkileşiminizi ve marka bilinirliğinizi artırıyoruz.',
|
||||
icon_name: 'Instagram',
|
||||
display_order: 1
|
||||
},
|
||||
{
|
||||
title: 'Meta Reklam Yönetimi',
|
||||
description: 'Facebook ve Instagram reklamlarınızı hedef kitlenize uygun stratejilerle yöneterek daha fazla dönüşüm ve müşteri kazanımı sağlıyoruz.',
|
||||
icon_name: 'Facebook',
|
||||
display_order: 2
|
||||
},
|
||||
{
|
||||
title: 'Google Reklam Yönetimi',
|
||||
description: 'Google reklam kampanyalarınızı optimize ederek markanızın doğru kitleye ulaşmasını ve maksimum verim elde etmesini sağlıyoruz.',
|
||||
icon_name: 'Search',
|
||||
display_order: 3
|
||||
},
|
||||
{
|
||||
title: 'Profesyonel Drone Çekimi',
|
||||
description: 'Tarla, arazi, parsel ve işletmeler için yüksek kaliteli profesyonel drone çekimleri sunuyoruz.',
|
||||
icon_name: 'Video',
|
||||
display_order: 4
|
||||
},
|
||||
{
|
||||
title: 'Düğün Çekimi',
|
||||
description: 'En özel anlarınızı profesyonel ekipman ve yaratıcı çekimlerle unutulmaz hale getiriyoruz.',
|
||||
icon_name: 'Camera',
|
||||
display_order: 5
|
||||
},
|
||||
{
|
||||
title: 'Nişan, Evlilik Teklifi ve Özel Organizasyonlar',
|
||||
description: 'Özel günlerinizi estetik ve sinematik çekimlerle ölümsüzleştiriyoruz.',
|
||||
icon_name: 'Heart',
|
||||
display_order: 6
|
||||
},
|
||||
{
|
||||
title: 'Web Site Tasarımı',
|
||||
description: 'Markanıza özel modern, hızlı ve mobil uyumlu web siteleri tasarlıyoruz.',
|
||||
icon_name: 'Globe',
|
||||
display_order: 7
|
||||
},
|
||||
{
|
||||
title: 'Arama Motoru Optimizasyonu (SEO)',
|
||||
description: 'Web sitenizin Google’da daha görünür olması için profesyonel SEO çalışmaları gerçekleştiriyoruz.',
|
||||
icon_name: 'TrendingUp',
|
||||
display_order: 8
|
||||
},
|
||||
{
|
||||
title: 'Profesyonel Otel Çekimleri',
|
||||
description: 'Otel ve işletmeleriniz için tanıtım odaklı profesyonel fotoğraf ve video çekimleri yapıyoruz.',
|
||||
icon_name: 'Hotel',
|
||||
display_order: 9
|
||||
}
|
||||
];
|
||||
|
||||
async function seedServices() {
|
||||
try {
|
||||
console.log('Connecting to database...');
|
||||
|
||||
// Clear existing services
|
||||
console.log('Clearing existing services...');
|
||||
await sql`DELETE FROM services`;
|
||||
|
||||
// Insert new services
|
||||
console.log('Inserting new services...');
|
||||
for (const service of services) {
|
||||
await sql`
|
||||
INSERT INTO services (title, description, icon_name, display_order)
|
||||
VALUES (${service.title}, ${service.description}, ${service.icon_name}, ${service.display_order})
|
||||
`;
|
||||
}
|
||||
|
||||
console.log('Success! Services have been updated.');
|
||||
await sql.end();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error seeding services:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
seedServices();
|
||||
56
setup_partners.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import postgres from 'postgres';
|
||||
|
||||
const DATABASE_URL = "postgres://postgres:xGhPj4IuE5VocaxUoYAj1dSr2xf6M3hh3c2C6YbnB7ZOeVJLRvmL0mzCbhvf14dh@65.109.236.58:8392/postgres";
|
||||
|
||||
const sql = postgres(DATABASE_URL);
|
||||
|
||||
const partners = [
|
||||
{ name: "The Purest Solutions", logo: "/partners/purest.png" },
|
||||
{ name: "AKER", logo: "/partners/aker.png" },
|
||||
{ name: "MINISO", logo: "/partners/miniso.png" },
|
||||
{ name: "TRABZONSPOR", logo: "/partners/trabzon.png" },
|
||||
{ name: "MANUKA", logo: "/partners/manuka.png" },
|
||||
{ name: "COOK", logo: "/partners/cook.png" },
|
||||
{ name: "beyyoglo", logo: "/partners/beyyoglo.png" },
|
||||
{ name: "XENON SMART", logo: "/partners/xenon.png" },
|
||||
{ name: "HE-QA", logo: "/partners/heqa.png" },
|
||||
{ name: "Fresh Scarfs", logo: "/partners/fresh.png" },
|
||||
{ name: "Sabancı Üniversitesi", logo: "/partners/sabanci.png" },
|
||||
{ name: "VATKALI", logo: "/partners/vatkali.png" },
|
||||
];
|
||||
|
||||
async function setupPartners() {
|
||||
try {
|
||||
console.log('Creating partners table if not exists...');
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS partners (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
logo VARCHAR(255),
|
||||
display_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`;
|
||||
|
||||
console.log('Clearing existing partners...');
|
||||
await sql`DELETE FROM partners`;
|
||||
|
||||
console.log('Seeding partners...');
|
||||
for (let i = 0; i < partners.length; i++) {
|
||||
const partner = partners[i];
|
||||
await sql`
|
||||
INSERT INTO partners (name, logo, display_order)
|
||||
VALUES (${partner.name}, ${partner.logo}, ${i + 1})
|
||||
`;
|
||||
}
|
||||
|
||||
console.log('Success! Partners table setup and seeded.');
|
||||
await sql.end();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error setting up partners:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
setupPartners();
|
||||
42
tsconfig.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||