main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-03-17 13:16:22 +03:00
parent d09b1fbb6f
commit c339cb1382
15 changed files with 740 additions and 261 deletions

View File

@@ -3,8 +3,70 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Harun CAN - Oyuncu, Seslendirme Sanatçısı, Müzisyen</title>
<meta name="description" content="Kariyerine 1986 yılında başlayan Harun CAN; oyuncu, seslendirme sanatçısı, müzisyen ve içerik üreticisi olarak sanat hayatını çok yönlü bir şekilde sürdürmektedir." />
<title>HARUNCAN SoundArts | Professional Turkish Voice Actor & Game Localization</title>
<meta name="description" content="Harun CAN SoundArts offers premium video game localization, character voice acting, and professional dubbing services. Over 30 years of experience in audio localization for AAA games and global brands." />
<meta name="keywords" content="Turkish voice actor, video game localization, game dubbing turkey, Harun CAN, character voice acting, professional localization studio, Turkish voice over services, AAA game localization" />
<meta name="author" content="Harun CAN" />
<meta property="og:title" content="HARUNCAN SoundArts | Professional Turkish Voice Actor" />
<meta property="og:description" content="Premium video game localization and character voice acting services by Harun CAN." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://haruncan.com" />
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"name": "HARUNCAN SoundArts",
"image": "https://haruncan.com/logo.png",
"@id": "https://haruncan.com",
"url": "https://haruncan.com",
"telephone": "",
"address": {
"@type": "PostalAddress",
"streetAddress": "",
"addressLocality": "Istanbul",
"postalCode": "",
"addressCountry": "TR"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 41.0082,
"longitude": 28.9784
},
"openingHoursSpecification": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday"
],
"opens": "09:00",
"closes": "18:00"
},
"sameAs": [
"https://www.instagram.com/haruncan",
"https://twitter.com/haruncan",
"https://www.youtube.com/haruncan"
],
"knowsAbout": ["Video Game Localization", "Voice Acting", "Dubbing", "Audio Engineering", "Music Direction"]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Harun CAN",
"jobTitle": "Voice Actor, Music Director, Actor",
"url": "https://haruncan.com",
"sameAs": [
"https://www.imdb.com/name/nm10851457/"
]
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;700&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://haruncan.com/sitemap.xml

15
public/sitemap.xml Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://haruncan.com/</loc>
<lastmod>2026-03-16</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://haruncan.com/sysop</loc>
<lastmod>2026-03-16</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>

View File

@@ -9,10 +9,22 @@ import Contact from './components/Contact';
import Footer from './components/Footer';
import DynamicBackground from './components/DynamicBackground';
import Admin from './components/Admin';
import SEO from './components/SEO';
export default function App() {
const isSysop = window.location.pathname === '/sysop';
if (isSysop) {
return (
<div className="min-h-screen bg-[#050505]">
<Admin forceOpen={true} />
</div>
);
}
return (
<div className="min-h-screen relative bg-[#050505]">
<SEO />
<DynamicBackground />
<div className="relative z-10">
<Navbar />
@@ -27,7 +39,6 @@ export default function App() {
</main>
<Footer />
</div>
<Admin />
</div>
);
}

View File

@@ -13,7 +13,7 @@ export default function About() {
{/* Bio */}
<div>
<h2 className="text-sm font-mono text-[#FF5733] tracking-widest mb-2">{t.about.badge.toLocaleUpperCase(language)}</h2>
<h3 className="text-4xl md:text-5xl font-display font-bold mb-8">{t.about.title} <span className="text-lg font-normal text-slate-400">(Kurucu)</span></h3>
<h3 className="text-4xl md:text-5xl font-display font-bold mb-8">{t.about.title} <span className="text-lg font-normal text-slate-400">{t.about.founder}</span></h3>
<div className="space-y-6 text-slate-300 leading-relaxed">
<p>{t.about.desc1}</p>

View File

@@ -2,14 +2,14 @@ import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { useData } from '../context/DataContext';
import * as api from '../lib/api';
import { X, Plus, Trash2, GripVertical, Save, Settings, Upload, LogIn, LogOut, Image, FolderOpen, AlertCircle, RotateCcw, ScrollText } from 'lucide-react';
import { X, Plus, Trash2, GripVertical, Save, Settings, Upload, LogIn, LogOut, FolderOpen, AlertCircle, RotateCcw, ScrollText } from 'lucide-react';
type Tab = 'projects' | 'clients' | 'logs' | 'login';
type Tab = 'projects' | 'clients' | 'settings' | 'seo' | 'logs' | 'login';
export default function Admin() {
export default function Admin({ forceOpen = false }: { forceOpen?: boolean }) {
const { data, addProject, updateProject, removeProject, restoreProject, refreshProjects, refreshClients } = useData();
const [isOpen, setIsOpen] = useState(false);
const [tab, setTab] = useState<Tab>('projects');
const [isOpen, setIsOpen] = useState(forceOpen);
const [tab, setTab] = useState<Tab>(api.isAuthenticated() ? 'projects' : 'login');
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState(api.isAuthenticated());
@@ -41,6 +41,25 @@ export default function Admin() {
const [auditLoading, setAuditLoading] = useState(false);
const [auditFilter, setAuditFilter] = useState<string>('');
// Mail Settings state
const [mailSettings, setMailSettings] = useState({
targetEmail: '',
host: '',
port: 587,
secure: false,
user: '',
pass: '',
from: '',
});
// SEO Settings state
const [seoSettings, setSeoSettings] = useState({
siteTitle: '',
siteDescription: '',
siteKeywords: '',
});
const [settingsLoading, setSettingsLoading] = useState(false);
const loadAuditLogs = async (entityFilter?: string) => {
if (!isLoggedIn) return;
setAuditLoading(true);
@@ -62,8 +81,66 @@ export default function Admin() {
if (tab === 'logs' && isLoggedIn) {
loadAuditLogs(auditFilter);
}
if (tab === 'settings' && isLoggedIn) {
loadMailSettings();
}
if (tab === 'seo' && isLoggedIn) {
loadSeoSettings();
}
}, [tab, isLoggedIn, auditFilter]);
const loadSeoSettings = async () => {
setSettingsLoading(true);
try {
const content = await api.getContentBySection('seo_settings', 'en'); // Global SEO settings usually in EN or general
if (content) {
setSeoSettings({
siteTitle: content.siteTitle || '',
siteDescription: content.siteDescription || '',
siteKeywords: content.siteKeywords || '',
});
}
} catch (err: any) {
showMessage('SEO ayarları yüklenemedi', 'error');
} finally {
setSettingsLoading(false);
}
};
const loadMailSettings = async () => {
setSettingsLoading(true);
try {
const content = await api.getContentBySection('mail_settings', 'tr');
if (content) {
setMailSettings({
targetEmail: content.targetEmail || '',
host: content.host || '',
port: content.port || 587,
secure: content.secure === 'true' || content.secure === true,
user: content.user || '',
pass: content.pass || '',
from: content.from || '',
});
}
} catch (err: any) {
showMessage('Ayarlar yüklenemedi', 'error');
} finally {
setSettingsLoading(false);
}
};
const handleSaveSettings = async () => {
setSaving(true);
try {
await api.updateContent('mail_settings', 'tr', { content: mailSettings });
showMessage('Ayarlar başarıyla kaydedildi!', 'success');
} catch (err: any) {
showMessage(err.message || 'Ayarlar kaydedilemedi', 'error');
} finally {
setSaving(false);
}
};
const handleAddClient = async (file: File) => {
if (!newClientName.trim()) {
showMessage('Marka adı gerekli', 'error');
@@ -75,17 +152,22 @@ export default function Admin() {
}
try {
setClientUploading(true);
// Upload file - returns MediaFileAPI with url like /uploads/filename.ext
const media = await api.uploadFile(file);
const response = await api.uploadFile(file);
const media = (response as any).data || response;
let logoUrl = '';
const m = (media as any)?.data || media; // Unwrap completely if deeply nested
if (m && m.url) {
logoUrl = m.url;
} else if (m && m.filename) {
logoUrl = `/uploads/${m.filename}`;
} else {
logoUrl = `/uploads/${file.name}`;
if (media && typeof media === 'object') {
if (media.url) {
logoUrl = media.url;
} else if (media.filename) {
logoUrl = `/uploads/${media.filename}`;
}
}
if (!logoUrl) {
throw new Error('Sunucu görsel URL\'sini döndüremedi. Lütfen tekrar deneyin.');
}
await api.createClient({ name: newClientName.trim(), logo: logoUrl });
await refreshClients();
setNewClientName('');
@@ -117,8 +199,6 @@ export default function Admin() {
setTimeout(() => setMessage(null), 3000);
};
// ── Auth ────────────────────────────────────
const handleLogin = async () => {
setLoginLoading(true);
try {
@@ -136,11 +216,10 @@ export default function Admin() {
const handleLogout = () => {
api.logout();
setIsLoggedIn(false);
setTab('login');
showMessage(ıkış yapıldı', 'success');
};
// ── Projects ────────────────────────────────
const handleAddProject = () => {
const newProject = {
id: `temp-${Date.now()}`,
@@ -177,7 +256,6 @@ export default function Admin() {
try {
await restoreProject(entityId);
showMessage('Proje geri getirildi!', 'success');
// Refresh audit logs too
if (tab === 'logs') loadAuditLogs(auditFilter);
} catch (err: any) {
showMessage(err.message || 'Geri getirme başarısız', 'error');
@@ -194,14 +272,17 @@ export default function Admin() {
setEditingProjects(newProjects);
};
// ── Image Upload ────────────────────────────
const handleImageUpload = async (projectId: string, file: File) => {
try {
setUploadingFor(projectId);
const rawMedia = await api.uploadFile(file);
let imageUrl = '';
if (rawMedia && typeof rawMedia === 'string') {
imageUrl = rawMedia;
} else {
const m = (rawMedia as any)?.data || rawMedia;
const imageUrl = m?.url || `/uploads/${m?.filename || file.name}`;
imageUrl = m?.url || (m?.filename ? `/uploads/${m.filename}` : `/uploads/${file.name}`);
}
handleUpdateField(projectId, 'image', imageUrl);
showMessage('Görsel yüklendi!', 'success');
} catch (err: any) {
@@ -224,8 +305,6 @@ export default function Admin() {
e.target.value = '';
};
// ── Save All ────────────────────────────────
const handleSaveAll = async () => {
setSaving(true);
try {
@@ -234,20 +313,19 @@ export default function Admin() {
await addProject({
title: project.title,
image: project.image,
roles: project.roles,
roles: project.roles.map(r => r.trim()).filter(r => r !== ""),
color: project.color,
});
} else {
await updateProject(project.id, {
title: project.title,
image: project.image,
roles: project.roles,
roles: project.roles.map(r => r.trim()).filter(r => r !== ""),
color: project.color,
});
}
}
// Reorder
const reorderItems = editingProjects
.filter(p => !p.id.startsWith('temp-'))
.map((p, i) => ({ id: p.id, sortOrder: i }));
@@ -264,8 +342,6 @@ export default function Admin() {
}
};
// ── Audit Log Helpers ─────────────────────────
const getActionBadge = (action: string) => {
switch (action) {
case 'CREATE': return 'bg-green-500/20 text-green-400 border-green-500/30';
@@ -296,7 +372,6 @@ export default function Admin() {
return (
<>
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
@@ -305,16 +380,6 @@ export default function Admin() {
onChange={onFileSelected}
/>
{/* Admin Toggle Button */}
<button
onClick={() => setIsOpen(true)}
className="fixed bottom-6 right-6 z-50 w-12 h-12 bg-slate-900 border border-white/20 rounded-full flex items-center justify-center text-slate-400 hover:text-[#FF5733] hover:border-[#FF5733] transition-all box-glow-hover"
title="Admin Panel"
>
<Settings className="w-5 h-5" />
</button>
{/* Admin Modal */}
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-6 bg-black/80 backdrop-blur-sm">
@@ -334,7 +399,11 @@ export default function Admin() {
{/* Tabs */}
<div className="flex gap-1 ml-4">
{(['projects', 'clients', 'logs', 'login'] as Tab[]).map(t => (
{(['projects', 'clients', 'settings', 'logs', 'login'] as Tab[]).map(t => {
if (!isLoggedIn && t !== 'login') return null;
if (isLoggedIn && t === 'login') return null;
return (
<button
key={t}
onClick={() => setTab(t)}
@@ -345,10 +414,13 @@ export default function Admin() {
>
{t === 'projects' ? '📁 Projeler' :
t === 'clients' ? '🏢 Markalar' :
t === 'seo' ? '📈 SEO' :
t === 'settings' ? '⚙️ Ayarlar' :
t === 'logs' ? '📋 Log' :
'🔐 Giriş'}
</button>
))}
);
})}
</div>
</div>
@@ -358,7 +430,16 @@ export default function Admin() {
<LogOut className="w-3 h-3" /> Çıkış
</button>
)}
<button onClick={() => setIsOpen(false)} className="text-slate-400 hover:text-white transition-colors">
<button
onClick={() => {
if (forceOpen) {
window.location.href = '/';
} else {
setIsOpen(false);
}
}}
className="text-slate-400 hover:text-white transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
@@ -382,10 +463,8 @@ export default function Admin() {
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* ── Login Tab ── */}
{tab === 'login' && (
<section className="max-w-md mx-auto">
{tab === 'login' && !isLoggedIn && (
<section className="max-w-md mx-auto py-12">
<div className="p-8 bg-slate-900 border border-white/10 rounded-lg space-y-6">
<div className="text-center">
<LogIn className="w-12 h-12 text-[#FF5733] mx-auto mb-4" />
@@ -400,7 +479,7 @@ export default function Admin() {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:border-[#FF5733] outline-none transition-colors"
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:border-[#FF5733] outline-none transition-colors rounded-sm"
placeholder="admin@haruncan.com"
/>
</div>
@@ -411,30 +490,23 @@ export default function Admin() {
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:border-[#FF5733] outline-none transition-colors"
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:border-[#FF5733] outline-none transition-colors rounded-sm"
placeholder="••••••••"
/>
</div>
<button
onClick={handleLogin}
disabled={loginLoading || !email || !password}
className="w-full py-3 bg-[#FF5733] text-white font-bold hover:bg-white hover:text-slate-950 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
className="w-full py-3 bg-[#FF5733] text-white font-bold hover:bg-white hover:text-slate-950 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 rounded-sm"
>
{loginLoading ? 'Giriş yapılıyor...' : <><LogIn className="w-4 h-4" /> Giriş Yap</>}
</button>
</div>
{isLoggedIn && (
<div className="mt-4 p-3 bg-green-500/10 border border-green-500/20 rounded-md text-sm text-green-400 text-center">
Oturum ık
</div>
)}
</div>
</section>
)}
{/* ── Clients Tab ── */}
{tab === 'clients' && (
{tab === 'clients' && isLoggedIn && (
<section>
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
@@ -442,7 +514,6 @@ export default function Admin() {
</h3>
</div>
{/* Add new client */}
<div className="p-4 bg-slate-900/50 border border-white/10 rounded-lg mb-6">
<h4 className="text-sm font-mono text-slate-400 mb-3">YENİ MARKA EKLE</h4>
<div className="flex items-end gap-4">
@@ -452,7 +523,7 @@ export default function Admin() {
type="text"
value={newClientName}
onChange={(e) => setNewClientName(e.target.value)}
className="w-full bg-slate-950 border border-white/10 px-4 py-2.5 text-white focus:border-[#FF5733] outline-none transition-colors text-sm"
className="w-full bg-slate-950 border border-white/10 px-4 py-2.5 text-white focus:border-[#FF5733] outline-none transition-colors text-sm rounded-sm"
placeholder="Netflix, Disney, Warner Bros..."
/>
</div>
@@ -471,7 +542,7 @@ export default function Admin() {
<button
onClick={() => clientFileRef.current?.click()}
disabled={clientUploading || !newClientName.trim()}
className="flex items-center gap-2 px-4 py-2.5 bg-[#FF5733] text-white font-bold text-sm hover:bg-white hover:text-slate-950 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="flex items-center gap-2 px-4 py-2.5 bg-[#FF5733] text-white font-bold text-sm hover:bg-white hover:text-slate-950 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-sm"
>
<Upload className="w-4 h-4" />
{clientUploading ? 'Yükleniyor...' : 'Logo Yükle & Ekle'}
@@ -480,12 +551,11 @@ export default function Admin() {
</div>
</div>
{/* Client list */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{data.clients.map((client) => (
<div key={client.id} className="relative group p-4 bg-slate-900/50 border border-white/10 rounded-lg flex flex-col items-center">
<img
src={client.logo}
src={api.getMediaUrl(client.logo)}
alt={client.name}
className="h-[60px] w-auto object-contain mb-3"
style={{ maxWidth: '200px' }}
@@ -494,23 +564,16 @@ export default function Admin() {
<button
onClick={() => handleRemoveClient(client.id)}
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity p-1 text-red-400 hover:text-red-300"
title="Sil"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
{data.clients.length === 0 && (
<div className="col-span-full text-center py-8 text-slate-500 text-sm font-mono">
Henüz marka eklenmemiş. Yukarıdan yeni marka ekleyebilirsiniz.
</div>
)}
</div>
</section>
)}
{/* ── Projects Tab ── */}
{tab === 'projects' && (
{tab === 'projects' && isLoggedIn && (
<section>
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
@@ -528,16 +591,14 @@ export default function Admin() {
<div className="space-y-4">
{editingProjects.map((project, index) => (
<div key={project.id} className="flex gap-4 p-4 bg-slate-900 border border-white/5 rounded-lg hover:border-white/10 transition-colors">
{/* Move Buttons */}
<div className="flex flex-col gap-2 justify-center text-slate-500">
<button onClick={() => handleMoveProject(index, 'up')} disabled={index === 0} className="hover:text-white disabled:opacity-30 transition-colors"></button>
<GripVertical className="w-5 h-5" />
<button onClick={() => handleMoveProject(index, 'down')} disabled={index === editingProjects.length - 1} className="hover:text-white disabled:opacity-30 transition-colors"></button>
</div>
{/* Image Preview */}
<div className="relative w-24 h-24 rounded-md overflow-hidden border border-white/10 bg-slate-950 shrink-0 group">
<img src={project.image} alt={project.title} className="w-full h-full object-cover" />
<img src={api.getMediaUrl(project.image)} alt={project.title} className="w-full h-full object-cover" />
<button
onClick={() => triggerFileUpload(project.id)}
className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
@@ -550,7 +611,6 @@ export default function Admin() {
</button>
</div>
{/* Fields */}
<div className="flex-1 grid md:grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-mono text-slate-400">BAŞLIK</label>
@@ -575,7 +635,7 @@ export default function Admin() {
<input
type="text"
value={project.roles.join(', ')}
onChange={(e) => handleUpdateField(project.id, 'roles', e.target.value.split(',').map(r => r.trim()))}
onChange={(e) => handleUpdateField(project.id, 'roles', e.target.value.split(','))}
className="w-full bg-slate-950 border border-white/10 px-3 py-2 text-sm text-white focus:border-[#FF5733] outline-none rounded-sm"
/>
</div>
@@ -598,27 +658,109 @@ export default function Admin() {
</div>
</div>
{/* Delete */}
<button
onClick={() => handleRemoveProject(project.id)}
className="p-2 text-slate-500 hover:text-red-500 hover:bg-red-500/10 rounded-md transition-colors h-fit"
title="Projeyi Sil"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
))}
{editingProjects.length === 0 && (
<div className="text-center p-8 text-slate-500 border border-dashed border-white/10 rounded-lg">
Henüz proje yok. Yeni bir proje ekleyin.
</div>
)}
</div>
</section>
)}
{/* ── Audit Logs Tab ── */}
{tab === 'logs' && (
{tab === 'settings' && isLoggedIn && (
<section className="max-w-2xl mx-auto">
<div className="flex items-center gap-2 mb-6">
<Settings className="w-5 h-5 text-[#FF5733]" />
<h3 className="text-lg font-bold text-white">Mail & Sistem Ayarları</h3>
</div>
<div className="bg-slate-900 border border-white/10 rounded-lg overflow-hidden">
<div className="p-6 border-b border-white/10">
<h4 className="text-sm font-mono text-[#FF5733] mb-4">E-POSTA YÖNLENDİRME</h4>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-mono text-slate-400">ALICI E-POSTA ADRESİ</label>
<input
type="email"
value={mailSettings.targetEmail}
onChange={(e) => setMailSettings({ ...mailSettings, targetEmail: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-2.5 text-white focus:border-[#FF5733] outline-none rounded-sm text-sm"
placeholder="haruncanmedia@gmail.com"
/>
<p className="text-[10px] text-slate-500 font-mono">İletişim formundan gelen mesajlar bu adrese iletilir.</p>
</div>
</div>
</div>
<div className="p-6 bg-slate-900/50">
<h4 className="text-sm font-mono text-[#FF5733] mb-4">SMTP YAPILANDIRMASI (OPSİYONEL)</h4>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-xs font-mono text-slate-400">SMTP HOST</label>
<input
type="text"
value={mailSettings.host}
onChange={(e) => setMailSettings({ ...mailSettings, host: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-2 text-white focus:border-[#FF5733] outline-none rounded-sm text-sm"
placeholder="smtp.gmail.com"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-mono text-slate-400">SMTP PORT</label>
<input
type="number"
value={mailSettings.port}
onChange={(e) => setMailSettings({ ...mailSettings, port: parseInt(e.target.value) })}
className="w-full bg-slate-950 border border-white/10 px-4 py-2 text-white focus:border-[#FF5733] outline-none rounded-sm text-sm"
placeholder="587"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-mono text-slate-400">SMTP USER</label>
<input
type="text"
value={mailSettings.user}
onChange={(e) => setMailSettings({ ...mailSettings, user: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-2 text-white focus:border-[#FF5733] outline-none rounded-sm text-sm"
placeholder="your-email@gmail.com"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-mono text-slate-400">SMTP PASS</label>
<input
type="password"
value={mailSettings.pass}
onChange={(e) => setMailSettings({ ...mailSettings, pass: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-2 text-white focus:border-[#FF5733] outline-none rounded-sm text-sm"
placeholder="••••••••"
/>
</div>
<div className="md:col-span-2 space-y-2">
<label className="text-xs font-mono text-slate-400">GÖNDEREN (FROM)</label>
<input
type="text"
value={mailSettings.from}
onChange={(e) => setMailSettings({ ...mailSettings, from: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-2 text-white focus:border-[#FF5733] outline-none rounded-sm text-sm"
placeholder='"HarunCAN Studio" <noreply@haruncan.com>'
/>
</div>
</div>
<div className="mt-6 p-4 bg-yellow-500/5 border border-yellow-500/10 rounded">
<p className="text-[11px] text-yellow-500/80 font-mono leading-relaxed">
<AlertCircle className="w-3 h-3 inline mr-1 mb-0.5" />
SMTP bilgilerini boş bırakırsanız sistem varsayılan sunucu ayarlarını kullanmaya devam eder.
</p>
</div>
</div>
</div>
</section>
)}
{tab === 'logs' && isLoggedIn && (
<section>
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
@@ -643,45 +785,22 @@ export default function Admin() {
>
<RotateCcw className="w-3 h-3" /> Yenile
</button>
<span className="text-xs text-slate-500 font-mono">{auditTotal} kayıt</span>
</div>
</div>
{!isLoggedIn ? (
<div className="text-center py-12 text-slate-500 font-mono">
Logları görmek için giriş yapmanız gerekiyor.
</div>
) : auditLoading ? (
<div className="text-center py-12 text-slate-500 font-mono animate-pulse">
Loglar yükleniyor...
</div>
) : auditLogs.length === 0 ? (
<div className="text-center py-12 text-slate-500 font-mono">
Henüz işlem logu yok.
</div>
) : (
<div className="space-y-2">
{auditLogs.map((log) => (
<div key={log.id} className="flex items-center gap-4 p-3 bg-slate-900/50 border border-white/5 rounded-lg hover:border-white/10 transition-colors text-sm">
{/* Entity Icon */}
<span className="text-lg shrink-0">{getEntityIcon(log.entity)}</span>
{/* Action Badge */}
<span className={`px-2 py-0.5 text-xs font-mono border rounded shrink-0 ${getActionBadge(log.action)}`}>
{log.action}
</span>
{/* Entity Type */}
<span className="text-xs font-mono text-slate-500 shrink-0 w-16">
{log.entity}
</span>
{/* Entity ID (truncated) */}
<span className="text-xs font-mono text-slate-600 truncate max-w-[120px]" title={log.entityId}>
{log.entityId.substring(0, 8)}
</span>
{/* Details */}
<span className="flex-1 text-xs text-slate-400 truncate">
{log.after ? (() => {
try {
@@ -695,18 +814,13 @@ export default function Admin() {
} catch { return '—'; }
})() : '—'}
</span>
{/* Timestamp */}
<span className="text-xs font-mono text-slate-600 shrink-0">
{formatDate(log.createdAt)}
</span>
{/* Restore button for deleted projects */}
{log.action === 'DELETE' && log.entity === 'project' && (
<button
onClick={() => handleRestoreProject(log.entityId)}
className="flex items-center gap-1 px-2 py-1 text-xs font-mono text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded hover:bg-yellow-500/20 transition-colors shrink-0"
title="Projeyi geri getir"
>
<RotateCcw className="w-3 h-3" /> Geri Getir
</button>
@@ -714,27 +828,31 @@ export default function Admin() {
</div>
))}
</div>
)}
</section>
)}
</div>
{/* Footer */}
<div className="p-6 border-t border-white/10 bg-slate-900/50 flex items-center justify-between">
<div className="text-xs text-slate-500 font-mono">
{isLoggedIn ? '🟢 Bağlı — API: localhost:3000' : '🔴 Oturum kapalı'}
{isLoggedIn ? '🟢 Bağlı' : '🔴 Oturum kapalı'}
</div>
<div className="flex gap-4">
<button
onClick={() => setIsOpen(false)}
onClick={() => {
if (forceOpen) {
window.location.href = '/';
} else {
setIsOpen(false);
}
}}
className="px-6 py-2.5 text-sm font-medium text-slate-300 hover:text-white transition-colors"
>
İptal
{forceOpen ? 'Siteye Dön' : 'İptal'}
</button>
{(tab === 'projects' || tab === 'clients') && (
{isLoggedIn && (tab === 'projects' || tab === 'clients' || tab === 'settings') && (
<button
onClick={handleSaveAll}
onClick={tab === 'settings' ? handleSaveSettings : handleSaveAll}
disabled={saving}
className="flex items-center gap-2 px-6 py-2.5 bg-[#FF5733] text-slate-950 font-bold text-sm hover:bg-white transition-colors rounded-md disabled:opacity-50"
>

View File

@@ -1,5 +1,6 @@
import { useLanguage } from '../i18n/LanguageContext';
import { useData } from '../context/DataContext';
import { getMediaUrl } from '../lib/api';
export default function Clients() {
const { t, language } = useLanguage();
@@ -20,7 +21,7 @@ export default function Clients() {
</div>
{/* Marquee of client logos */}
<div className="w-full overflow-hidden relative">
<div className="w-full overflow-hidden relative bg-white/20 py-12 border-y border-white/20">
{/* Fade masks */}
<div className="absolute top-0 left-0 w-16 md:w-48 h-full bg-gradient-to-r from-[#050505] to-transparent z-10 pointer-events-none" />
<div className="absolute top-0 right-0 w-16 md:w-48 h-full bg-gradient-to-l from-[#050505] to-transparent z-10 pointer-events-none" />
@@ -36,10 +37,10 @@ export default function Clients() {
title={client.name}
>
<img
src={client.logo}
src={getMediaUrl(client.logo)}
alt={client.name}
className="h-[50px] md:h-[60px] w-auto object-contain opacity-60 group-hover:opacity-100 transition-all duration-500"
style={{ maxWidth: '200px' }}
className="h-[50px] md:h-[60px] w-auto object-contain opacity-95 group-hover:opacity-100 transition-all duration-500"
style={{ maxWidth: '250px' }}
/>
</a>
))}

View File

@@ -1,9 +1,84 @@
import { motion } from 'motion/react';
import { MessageSquare, Send, Instagram, Youtube } from 'lucide-react';
import { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { MessageSquare, Send, Instagram, Youtube, CheckCircle2, AlertCircle, RotateCcw } from 'lucide-react';
import { useLanguage } from '../i18n/LanguageContext';
import { useEffect } from 'react';
import * as api from '../lib/api';
export default function Contact() {
const { t, language } = useLanguage();
const [formData, setFormData] = useState<api.ContactFormData>({
name: '',
email: '',
type: t.contact.form.types[0],
details: ''
});
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null);
const [captcha, setCaptcha] = useState<{ id: string; question: string } | null>(null);
const [captchaLoading, setCaptchaLoading] = useState(false);
const fetchCaptcha = async () => {
setCaptchaLoading(true);
try {
const data = await api.getCaptcha();
setCaptcha(data);
} catch (err) {
console.error('Failed to load captcha');
} finally {
setCaptchaLoading(false);
}
};
useEffect(() => {
fetchCaptcha();
}, []);
const validateEmail = (email: string) => {
return String(email)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.email || !formData.details || !formData.captchaAnswer) {
setStatus({ type: 'error', message: language === 'tr' ? 'Lütfen tüm alanları (güvenlik dahil) doldurun.' : 'Please fill in all fields (including security).' });
return;
}
if (!validateEmail(formData.email)) {
setStatus({ type: 'error', message: language === 'tr' ? 'Lütfen geçerli bir e-posta adresi girin.' : 'Please enter a valid email address.' });
return;
}
setLoading(true);
setStatus(null);
try {
await api.sendContactForm({
...formData,
captchaId: captcha?.id
});
setStatus({
type: 'success',
message: language === 'tr' ? 'Mesajınız başarıyla gönderildi!' : 'Your message has been sent successfully!'
});
setFormData({ name: '', email: '', type: t.contact.form.types[0], details: '', captchaAnswer: '' });
fetchCaptcha();
} catch (error: any) {
setStatus({
type: 'error',
message: error.message || (language === 'tr' ? 'Bir hata oluştu. Lütfen tekrar deneyin.' : 'An error occurred. Please try again.')
});
fetchCaptcha();
} finally {
setLoading(false);
setTimeout(() => setStatus(null), 5000);
}
};
return (
<section id="contact" className="py-24 relative overflow-hidden">
@@ -19,12 +94,15 @@ export default function Contact() {
</p>
</div>
<form className="space-y-6 bg-slate-900/50 border border-white/10 p-8 md:p-12 backdrop-blur-sm">
<form onSubmit={handleSubmit} className="space-y-6 bg-slate-900/50 border border-white/10 p-8 md:p-12 backdrop-blur-sm">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-xs font-mono text-slate-400">{t.contact.form.name.toLocaleUpperCase(language)}</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:outline-none focus:border-[#FF5733] transition-colors"
placeholder="John Doe / Epic Games"
/>
@@ -33,6 +111,9 @@ export default function Contact() {
<label className="text-xs font-mono text-slate-400">{t.contact.form.email.toLocaleUpperCase(language)}</label>
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:outline-none focus:border-[#FF5733] transition-colors"
placeholder="john@example.com"
/>
@@ -41,9 +122,13 @@ export default function Contact() {
<div className="space-y-2">
<label className="text-xs font-mono text-slate-400">{t.contact.form.type.toLocaleUpperCase(language)}</label>
<select className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:outline-none focus:border-[#FF5733] transition-colors appearance-none">
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:outline-none focus:border-[#FF5733] transition-colors appearance-none"
>
{t.contact.form.types.map((type, index) => (
<option key={index}>{type}</option>
<option key={index} value={type}>{type}</option>
))}
</select>
</div>
@@ -52,16 +137,74 @@ export default function Contact() {
<label className="text-xs font-mono text-slate-400">{t.contact.form.details.toLocaleUpperCase(language)}</label>
<textarea
rows={4}
required
value={formData.details}
onChange={(e) => setFormData({ ...formData, details: e.target.value })}
className="w-full bg-slate-950 border border-white/10 px-4 py-3 text-white focus:outline-none focus:border-[#FF5733] transition-colors resize-none"
placeholder="..."
/>
</div>
{captcha && (
<div className="space-y-4 p-4 bg-slate-950/50 border border-white/5 rounded-sm">
<label className="text-xs font-mono text-[#FF5733] flex items-center gap-2">
{language === 'tr' ? 'GÜVENLİK DOĞRULAMASI' : 'SECURITY CHECK'}
{captchaLoading && <div className="w-3 h-3 border border-[#FF5733] border-t-transparent rounded-full animate-spin" />}
</label>
<div className="flex items-center gap-4">
<div className="bg-slate-900 border border-white/10 px-4 py-2 text-white font-mono text-lg rounded-sm min-w-[120px] text-center flex items-center justify-between gap-3">
{captcha.question}
<button
type="button"
className="w-full py-4 bg-[#FF5733] text-slate-950 font-bold font-mono tracking-wider hover:bg-white transition-colors flex items-center justify-center gap-2"
onClick={fetchCaptcha}
className="text-slate-500 hover:text-white transition-colors"
>
<RotateCcw className="w-4 h-4" />
</button>
</div>
<input
type="text"
required
value={formData.captchaAnswer || ''}
onChange={(e) => setFormData({ ...formData, captchaAnswer: e.target.value })}
className="flex-1 bg-slate-950 border border-white/10 px-4 py-2 text-white focus:outline-none focus:border-[#FF5733] transition-colors rounded-sm text-center font-mono"
placeholder="?"
/>
</div>
</div>
)}
<AnimatePresence>
{status && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className={`p-4 flex items-center gap-3 text-sm ${status.type === 'success' ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-red-500/10 text-red-400 border border-red-500/20'
}`}
>
{status.type === 'success' ? <CheckCircle2 className="w-4 h-4" /> : <AlertCircle className="w-4 h-4" />}
{status.message}
</motion.div>
)}
</AnimatePresence>
<button
type="submit"
disabled={loading}
className="w-full py-4 bg-[#FF5733] text-slate-950 font-bold font-mono tracking-wider hover:bg-white transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-slate-950 border-t-transparent rounded-full animate-spin" />
{language === 'tr' ? 'GÖNDERİLİYOR...' : 'SENDING...'}
</span>
) : (
<>
<Send className="w-5 h-5" />
{t.contact.form.submit.toLocaleUpperCase(language)}
</>
)}
</button>
</form>

64
src/components/SEO.tsx Normal file
View File

@@ -0,0 +1,64 @@
import { useEffect } from 'react';
import * as api from '../lib/api';
interface SEOSettings {
siteTitle: string;
siteDescription: string;
siteKeywords: string;
}
export default function SEO() {
useEffect(() => {
const updateSEO = async () => {
try {
const settings = await api.getContentBySection('seo_settings', 'en') as SEOSettings;
if (settings) {
// Update Title
if (settings.siteTitle) {
document.title = settings.siteTitle;
}
// Update Meta Description
if (settings.siteDescription) {
let metaDesc = document.querySelector('meta[name="description"]');
if (!metaDesc) {
metaDesc = document.createElement('meta');
metaDesc.setAttribute('name', 'description');
document.head.appendChild(metaDesc);
}
metaDesc.setAttribute('content', settings.siteDescription);
// Also update OpenGraph description
let ogDesc = document.querySelector('meta[property="og:description"]');
if (ogDesc) ogDesc.setAttribute('content', settings.siteDescription);
}
// Update Meta Keywords
if (settings.siteKeywords) {
let metaKeywords = document.querySelector('meta[name="keywords"]');
if (!metaKeywords) {
metaKeywords = document.createElement('meta');
metaKeywords.setAttribute('name', 'keywords');
document.head.appendChild(metaKeywords);
}
metaKeywords.setAttribute('content', settings.siteKeywords);
}
// Update OpenGraph Title
if (settings.siteTitle) {
let ogTitle = document.querySelector('meta[property="og:title"]');
if (ogTitle) ogTitle.setAttribute('content', settings.siteTitle);
}
}
} catch (err) {
// Silently fail if settings can't be loaded, fallback to index.html defaults
console.warn('SEO settings could not be loaded from CMS, using defaults.');
}
};
updateSEO();
}, []);
return null; // This component doesn't render anything
}

View File

@@ -1,6 +1,7 @@
import { motion } from 'motion/react';
import { useLanguage } from '../i18n/LanguageContext';
import { useData } from '../context/DataContext';
import { getMediaUrl } from '../lib/api';
export default function Works() {
const { t, language } = useLanguage();
@@ -34,12 +35,12 @@ export default function Works() {
className="relative w-[85vw] md:w-[800px] h-[400px] md:h-[520px] rounded-3xl overflow-hidden group shrink-0 border border-white/5 hover:border-white/20 transition-all duration-500 cursor-pointer bg-slate-900/50"
>
<img
src={project.image}
src={getMediaUrl(project.image)}
alt={project.title}
className="absolute inset-0 w-full h-full object-cover opacity-40 group-hover:opacity-70 group-hover:scale-110 transition-all duration-700 ease-out"
className="absolute inset-0 w-full h-full object-cover opacity-95 group-hover:opacity-100 group-hover:scale-110 transition-all duration-700 ease-out"
referrerPolicy="no-referrer"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/40 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/50 via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 p-8 w-full transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 ease-out">
<h4 className="text-2xl md:text-3xl font-display font-bold mb-4 transition-colors duration-300" style={{ textShadow: `0 0 30px ${project.color}80` }}>

View File

@@ -6,56 +6,57 @@ export const translations = {
hero: {
badge: "Actor, Voice Actor, Musician",
title1: "Breathing ", title1_highlight: "Life", title2: "into ", title2_highlight: "Characters...",
desc: "With experience earned since 1986, we are here to add value to your projects. The meticulousness and experience of Harun CAN, who has voiced over 100,000 projects and world-famous characters like Deadpool, Spider-Man, and Bugs Bunny, is with you...",
desc: "Providing professional video game localization and native Turkish voice acting services since 1986. HARUNCAN SoundArts delivers high-end audio solutions for AAA games, including iconic roles like Deadpool, Spider-Man, and Bugs Bunny. Experience matters in character creation.",
btn_works: "View Portfolio", btn_contact: "Get in Touch"
},
works: { badge: "01 // Portfolio", title: "Featured Projects", roles: "Roles:" },
services: {
badge: "02 // Expertise", title: "What We Do",
badge: "02 // Expertise", title: "Premium Localization & Audio Services",
items: [
{ title: "Character Voice Acting", desc: "Games, films, series, trailers, teasers, documentaries or commercials... Unique localization solutions with the perfect cast and performance for the characters you create..." },
{ title: "Musical Performance", desc: "Vocal performances for Disney classics like The Lion King (Simba), Aladdin, and Wicked (Fiyero)." },
{ title: "Music Direction", desc: "With experience earned as music director of countless musicals (Over The Moon, Jingle Jangle, Vivo, Arlo), flawless music localization solutions for your projects... Song localization, musical adaptation, vocal coaching, recording and mixing solutions." },
{ title: "Commercial & Corporate", desc: "Commercial, teaser, trailer localization, casting, recording and mixing solutions." },
{ title: "Music Production", desc: "Music production, composition, arrangement, and production services..." },
{ title: "Content Creation & Events", desc: "Custom content creation and event planning tailored to your brand, product, and needs." },
{ title: "Game Translation & Localization", desc: "Professional game translation and localization services tailored for your target audience." },
{ title: "Voice Casting", desc: "Finding and directing the perfect voices that bring your characters and projects to life." },
{ title: "Audio Artistic Direction", desc: "Comprehensive audio artistic direction ensuring the highest quality soundscape for your productions." }
{ title: "Character Voice Acting", desc: "Expert voice acting for Triple-A games, films, and animations. We provide the perfect Turkish cast and performance for your global characters." },
{ title: "Video Game Localization", desc: "End-to-end game localization and dubbing services. Professional Turkish translation, casting, and recording tailored for the gaming industry." },
{ title: "Music Direction", desc: "Music direction and vocal coaching for major productions (Netflix, Disney). Song localization and musical adaptation with world-class audio mixing." },
{ title: "Commercial & Corporate", desc: "Commercial localization, trailer recording, and corporate identity voice-overs for leading global brands." },
{ title: "Music Production", desc: "High-end music production, composition, and arrangement for cinematic games and diverse media projects." },
{ title: "Content Creation & Events", desc: "Strategic content creation and brand-focused events tailored to your unique marketing objectives." },
{ title: "Game Translation", desc: "Professional video game translation and localization services focusing on cultural nuance and player immersion." },
{ title: "Voice Casting", desc: "Industry-leading voice casting services for games and animations, ensuring the best Turkish vocal talent for your project." },
{ title: "Audio Artistic Direction", desc: "Expert audio artistic direction and sound design, ensuring a premium auditory experience for your productions." }
]
},
clients: { badge: "02.5 // Clients", title: "Trusted By" },
clients: { badge: "02.5 // Clients", title: "Trusted By Global Brands" },
process: {
badge: "03 // Career", title: "Harun CAN's Career",
badge: "03 // Career", title: "Harun CAN's Legacy",
items: [
{ title: "1986 - The Beginning", desc: "His career, which began in 1986, reached today with his graduation from Hacettepe University State Conservatory, Department of Theatre." },
{ title: "Iconic Characters", desc: "The official Turkish voice of countless iconic characters, led by Deadpool, Spider-Man, and Bugs Bunny." },
{ title: "Musical Journey", desc: "He voiced the lead roles of world-renowned musicals, sang their songs, and adapted countless musicals into Turkish. He served as music director for projects by globally acclaimed organizations like Netflix, Cartoon Network, and Disney." },
{ title: "Voice of Brands", desc: "Since the 80s, he has voiced thousands of commercials as the voice trusted by generations. He reached millions as the corporate voice of brands." }
{ title: "1986 - The Beginning", desc: "A career spanning decades, starting in 1986 with a foundation in classical theatre at Hacettepe University Conservatory." },
{ title: "Iconic Voice Acting", desc: "The official Turkish voice of world-famous characters, defining the sound of Deadpool, Spider-Man, and Bugs Bunny for a generation." },
{ title: "Musical Projects", desc: "Starring in and music-directing world-renowned musicals for Netflix and Disney, bringing complex melodies to life in Turkish." },
{ title: "Brand Identity", desc: "The trusted corporate voice for thousands of commercials and major global brands since the 1980s." }
]
},
about: {
badge: "04 // About Us", title: "Harun CAN",
desc1: "Starting his career in 1986, Harun CAN continues his artistic life in a versatile way as an actor, voice actor, musician, and content creator. He is a graduate of Hacettepe University State Conservatory.",
desc2: "In his voiceover career, he has established a strong bond with 3 generations in Turkey as the official Turkish voice of iconic characters like Deadpool, Spider-Man, and Bugs Bunny. He is highly preferred in prestigious projects requiring serious acting performance.",
desc3: "Having taken part in over 100,000 projects to date, he is a thought leader who influences masses not only digitally but also in the physical world with his ~2 million loyal followers on social media, university talks, and fair participations.",
tech_title: "Musical & Corporate",
badge: "04 // About Us", title: "HARUNCAN SoundArts",
desc1: "HARUNCAN SoundArts is a premier studio specializing in video game localization and professional voice acting. Founded by Harun CAN, we bring over 30 years of artistic expertise to your projects.",
desc2: "As the official Turkish voice of iconic characters like Deadpool and Spider-Man, Harun CAN provides an unparalleled level of acting performance and brand recognition in the Turkish market.",
desc3: "With over 100,000 projects delivered, we are industry leaders in audio localization, digital content creation, and professional talk/event appearances.",
tech_title: "Musical & Corporate Excellence",
tech_items: [
{ title: "Disney Classics", desc: "The Lion King, Aladdin, Mary Poppins Returns, Beauty and the Beast." },
{ title: "Netflix Musicals", desc: "Music director for Vivo, Arlo, Over The Moon, Jingle Jangle." },
{ title: "Korkuluk Album", desc: "Solo album released in 2013 under the Universal Music label." },
{ title: "Corporate Identity", desc: "Discovery Channel, TRT Çocuk, Disney Channel, and Monster Notebook." }
]
{ title: "Netflix Musicals", desc: "Audio lead for Vivo, Arlo, Over The Moon, Jingle Jangle." },
{ title: "Korkuluk Album", desc: "Solo album released in 2013 under Universal Music." },
{ title: "Corporate Voice", desc: "Discovery Channel, TRT Çocuk, Disney Channel, and Monster Notebook." }
],
founder: "(Founder)"
},
contact: {
badge: "05 // Contact", title: "Get in Touch",
desc: "Get in touch for projects, collaborations, or event invitations.",
badge: "05 // Contact", title: "Start Your Project",
desc: "Reach out for professional voice acting, game localization, or artistic collaboration.",
form: {
name: "Name / Organization", email: "Email Address", type: "Subject", details: "Your Message", submit: "Send Message",
types: ["Voiceover / Dubbing", "Music / Vocals", "Collaboration / Sponsorship", "Event / Talk", "Other"]
name: "Name / Organization", email: "Email Address", type: "Inquiry Type", details: "Your Message", submit: "Send Message",
types: ["Video Game Localization", "Character Voice Over", "Musical/Vocal Project", "Speaking Engagement", "Other"]
}
},
footer: { text: "HARUNCAN SoundArts - Sound & Art Studio" }
footer: { text: "HARUNCAN SoundArts - Premium Audio & Localization" }
},
tr: {
nav: { works: "Projeler", services: "Uzmanlık", clients: "Müşteriler", process: "Kariyer", about: "Hakkımda", contact: "İletişim" },
@@ -101,7 +102,8 @@ export const translations = {
{ title: "Netflix Müzikalleri", desc: "Vivo, Arlo, Over The Moon, Jingle Jangle müzik direktörlüğü." },
{ title: "Korkuluk Albümü", desc: "2013 yılında Universal Music etiketiyle yayınlanan solo albüm." },
{ title: "Kurumsal Kimlik", desc: "Discovery Channel, TRT Çocuk, Disney Channel ve Monster Notebook." }
]
],
founder: "(Kurucu)"
},
contact: {
badge: "05 // İletişim", title: "Bağlantı Kur",
@@ -157,7 +159,8 @@ export const translations = {
{ title: "Netflix-Musicals", desc: "Musikalischer Leiter für Vivo, Arlo, Over The Moon, Jingle Jangle." },
{ title: "Korkuluk Album", desc: "Soloalbum, veröffentlicht 2013 unter dem Label Universal Music." },
{ title: "Corporate Identity", desc: "Discovery Channel, TRT Çocuk, Disney Channel und Monster Notebook." }
]
],
founder: "(Gründer)"
},
contact: {
badge: "05 // Kontakt", title: "Kontakt aufnehmen",
@@ -167,7 +170,7 @@ export const translations = {
types: ["Voiceover / Synchronisation", "Musik / Gesang", "Kooperation / Sponsoring", "Event / Vortrag", "Andere"]
}
},
footer: { text: "Harun CAN - Schauspieler, Synchronsprecher, Musiker" }
footer: { text: "HARUNCAN SoundArts - Premium Audio & Localization" }
},
es: {
nav: { works: "Proyectos", services: "Experiencia", clients: "Clientes", process: "Carrera", about: "Sobre mí", contact: "Contacto" },
@@ -213,7 +216,8 @@ export const translations = {
{ title: "Musicales de Netflix", desc: "Director musical de Vivo, Arlo, Over The Moon, Jingle Jangle." },
{ title: "Álbum Korkuluk", desc: "Álbum en solitario lanzado en 2013 bajo el sello Universal Music." },
{ title: "Identidad Corporativa", desc: "Discovery Channel, TRT Çocuk, Disney Channel y Monster Notebook." }
]
],
founder: "(Fundador)"
},
contact: {
badge: "05 // Contacto", title: "Ponerse en Contacto",
@@ -223,7 +227,7 @@ export const translations = {
types: ["Voz en off / Doblaje", "Música / Voces", "Colaboración / Patrocinio", "Evento / Charla", "Otro"]
}
},
footer: { text: "Harun CAN - Actor, Actor de Doblaje, Músico" }
footer: { text: "HARUNCAN SoundArts - Premium Audio & Localization" }
},
fr: {
nav: { works: "Projets", services: "Expertise", clients: "Clients", process: "Carrière", about: "À propos", contact: "Contact" },
@@ -265,11 +269,12 @@ export const translations = {
desc3: "Ayant participé à plus de 100 000 projets à ce jour, il est un leader d'opinion qui influence les masses non seulement numériquement mais aussi dans le monde physique avec ses ~2 millions d'abonnés fidèles sur les réseaux sociaux.",
tech_title: "Musical & Corporate",
tech_items: [
{ title: "Classiques de Disney", desc: "Le Roi Lion, Aladdin, Le Retour de Mary Poppins, La Belle et la Bête." },
{ title: "Classiques de Disney", desc: "Le Roi Lion, Aladdin, Le Retour de Mary Poppins, La Belle et la Beauté." },
{ title: "Comédies Musicales Netflix", desc: "Directeur musical pour Vivo, Arlo, Over The Moon, Jingle Jangle." },
{ title: "Album Korkuluk", desc: "Album solo sorti en 2013 sous le label Universal Music." },
{ title: "Identité d'Entreprise", desc: "Discovery Channel, TRT Çocuk, Disney Channel et Monster Notebook." }
]
],
founder: "(Fondateur)"
},
contact: {
badge: "05 // Contact", title: "Prendre Contact",
@@ -279,7 +284,7 @@ export const translations = {
types: ["Voix off / Doublage", "Musique / Voix", "Collaboration / Sponsoring", "Événement / Conférence", "Autre"]
}
},
footer: { text: "Harun CAN - Acteur, Acteur Vocal, Musicien" }
footer: { text: "HARUNCAN SoundArts - Premium Audio & Localization" }
},
ja: {
nav: { works: "プロジェクト", services: "専門知識", clients: "クライアント", process: "キャリア", about: "私について", contact: "お問い合わせ" },
@@ -323,9 +328,10 @@ export const translations = {
tech_items: [
{ title: "ディズニー名作", desc: "ライオン・キング、アラジン、メリー・ポピンズ リターンズ、美女と野獣。" },
{ title: "Netflixミュージカル", desc: "ビーボ、アーロ、フェイフェイと月の冒険、ジングル・ジャングルの音楽監督。" },
{ title: "Korkuluk アルバム", desc: "2013年にユニバーサルミュージックレーベルからリリースされたソロアルバム。" },
{ title: "Korkuluk アルバム", desc: "2013年にユニバーサルミュージックレーベルからリリースされた solo アルバム。" },
{ title: "コーポレートアイデンティティ", desc: "ディスカバリーチャンネル、TRT Çocuk、ディズニーチャンネル、Monster Notebook。" }
]
],
founder: "(設立者)"
},
contact: {
badge: "05 // お問い合わせ", title: "連絡を取る",
@@ -335,6 +341,6 @@ export const translations = {
types: ["ボイスオーバー / 吹き替え", "音楽 / ボーカル", "コラボレーション / スポンサーシップ", "イベント / 講演", "その他"]
}
},
footer: { text: "Harun CAN - 俳優、声優、ミュージシャン" }
footer: { text: "HARUNCAN SoundArts - Premium Audio & Localization" }
}
};

View File

@@ -30,16 +30,32 @@ function authHeaders(): HeadersInit {
return headers;
}
// ── Media Helper ──────────────────────────────
export function getMediaUrl(path: string | null | undefined): string {
if (!path) return '';
if (path.startsWith('http') || path.startsWith('data:')) return path;
// Yalnızca relative path döndürülür. Vite proxy (dev) veya production public dizini
// bunu güvenli ve hatasız şekilde sunacaktır. Port 3000 firewall engellerine takılmaz.
return path.startsWith('/') ? path : `/${path}`;
}
// ── Generic fetch wrapper ─────────────────────
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${url}`, {
let res: Response;
try {
res = await fetch(`${API_BASE}${url}`, {
...options,
headers: {
...authHeaders(),
...options?.headers,
},
});
} catch (err) {
throw new Error('Sunucuya bağlanılamadı (Ağ Hatası). Lütfen internet bağlantınızı veya sunucu durumunu kontrol edin.');
}
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
@@ -52,11 +68,16 @@ async function apiFetch<T>(url: string, options?: RequestInit): Promise<T> {
const json = JSON.parse(text);
// NestJS ResponseInterceptor wraps data in { success, status, message, data }
// Unwrap to get the actual payload
if (json && typeof json === 'object' && 'success' in json && 'data' in json) {
// Backend returns all responses as HTTP 200, so we must check json.success
if (json && typeof json === 'object' && 'success' in json) {
if (json.success === false) {
const errorMsg = json.errors?.length ? `${json.message}: ${json.errors.join(', ')}` : json.message;
throw new Error(errorMsg || `API Error: ${json.status}`);
}
if ('data' in json) {
return json.data as T;
}
}
return json as T;
}
@@ -169,11 +190,12 @@ export async function uploadFile(file: File): Promise<MediaFileAPI> {
const formData = new FormData();
formData.append('file', file);
const headers = authHeaders() as Record<string, string>;
delete headers['Content-Type']; // Let browser auto-set multipart boundary
const res = await fetch(`${API_BASE}/cms/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
},
headers,
body: formData,
});
@@ -183,16 +205,17 @@ export async function uploadFile(file: File): Promise<MediaFileAPI> {
}
const json = await res.json();
console.log('[uploadFile] raw json:', json);
// NestJS ResponseInterceptor wraps in { success, data }
if (json && typeof json === 'object' && 'success' in json && 'data' in json) {
console.log('[uploadFile] returning data:', json.data);
return json.data as MediaFileAPI;
// Backend returns status 200 even for errors (wrapped in ApiResponse)
if (json && typeof json === 'object' && 'success' in json && json.success === false) {
const errorMsg = json.errors?.length ? `${json.message}: ${json.errors.join(', ')}` : json.message;
throw new Error(errorMsg || `Upload failed: ${json.status}`);
}
console.log('[uploadFile] returning un-wrapped json:', json);
return json as MediaFileAPI;
// NestJS ResponseInterceptor wrappers or standard Prisma data
const payload = json?.data && typeof json.data === 'object' ? json.data : json;
return payload as MediaFileAPI;
}
export async function getMedia(): Promise<MediaFileAPI[]> {
@@ -276,3 +299,25 @@ export async function getAuditLogs(options?: {
const qs = params.toString();
return apiFetch<{ logs: AuditLogAPI[]; total: number }>(`/cms/audit-logs${qs ? `?${qs}` : ''}`);
}
// ── Contact API ───────────────────────────────
export interface ContactFormData {
name: string;
email: string;
type: string;
details: string;
captchaId?: string;
captchaAnswer?: string;
}
export async function sendContactForm(data: ContactFormData) {
return apiFetch('/mail/send', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function getCaptcha(): Promise<{ id: string; question: string }> {
return apiFetch<{ id: string; question: string }>('/mail/captcha');
}

6
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module "*.JPG" {
const src: string;
export default src;
}

View File

@@ -22,5 +22,8 @@
},
"allowImportingTsExtensions": true,
"noEmit": true
}
},
"include": [
"src"
]
}

View File

@@ -20,11 +20,11 @@ export default defineConfig(({ mode }) => {
hmr: process.env.DISABLE_HMR !== 'true',
proxy: {
'/api': {
target: 'http://localhost:3000',
target: 'http://127.0.0.1:3000',
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:3000',
target: 'http://127.0.0.1:3000',
changeOrigin: true,
},
},