diff --git a/index.html b/index.html index 91b926a..98bd059 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,70 @@ - Harun CAN - Oyuncu, Seslendirme Sanatçısı, Müzisyen - + HARUNCAN SoundArts | Professional Turkish Voice Actor & Game Localization + + + + + + + + + + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..ef5d3ee --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://haruncan.com/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..bddfeb9 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,15 @@ + + + + https://haruncan.com/ + 2026-03-16 + weekly + 1.0 + + + https://haruncan.com/sysop + 2026-03-16 + monthly + 0.5 + + diff --git a/src/App.tsx b/src/App.tsx index 87309ec..86caf2c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( +
+ +
+ ); + } + return (
+
@@ -27,7 +39,6 @@ export default function App() {
-
); } diff --git a/src/components/About.tsx b/src/components/About.tsx index 0be8fcf..0dc26e8 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -13,7 +13,7 @@ export default function About() { {/* Bio */}

{t.about.badge.toLocaleUpperCase(language)}

-

{t.about.title} (Kurucu)

+

{t.about.title} {t.about.founder}

{t.about.desc1}

diff --git a/src/components/Admin.tsx b/src/components/Admin.tsx index a129010..99f81b9 100644 --- a/src/components/Admin.tsx +++ b/src/components/Admin.tsx @@ -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('projects'); + const [isOpen, setIsOpen] = useState(forceOpen); + const [tab, setTab] = useState(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(''); + // 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); - const m = (rawMedia as any)?.data || rawMedia; - const imageUrl = m?.url || `/uploads/${m?.filename || file.name}`; + let imageUrl = ''; + if (rawMedia && typeof rawMedia === 'string') { + imageUrl = rawMedia; + } else { + const m = (rawMedia as any)?.data || rawMedia; + 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 */} - {/* Admin Toggle Button */} - - - {/* Admin Modal */} {isOpen && (
@@ -334,21 +399,28 @@ export default function Admin() { {/* Tabs */}
- {(['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 ( + + ); + })}
@@ -358,7 +430,16 @@ export default function Admin() { Çıkış )} -
@@ -382,10 +463,8 @@ export default function Admin() { {/* Content */}
- - {/* ── Login Tab ── */} - {tab === 'login' && ( -
+ {tab === 'login' && !isLoggedIn && ( +
@@ -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" />
@@ -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="••••••••" />
- - {isLoggedIn && ( -
- ✅ Oturum açık -
- )}
)} - {/* ── Clients Tab ── */} - {tab === 'clients' && ( + {tab === 'clients' && isLoggedIn && (

@@ -442,7 +514,6 @@ export default function Admin() {

- {/* Add new client */}

YENİ MARKA EKLE

@@ -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..." />
@@ -471,7 +542,7 @@ export default function Admin() {
- {/* Client list */}
{data.clients.map((client) => (
{client.name} 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" >
))} - {data.clients.length === 0 && ( -
- Henüz marka eklenmemiş. Yukarıdan yeni marka ekleyebilirsiniz. -
- )}
)} - {/* ── Projects Tab ── */} - {tab === 'projects' && ( + {tab === 'projects' && isLoggedIn && (

@@ -528,16 +591,14 @@ export default function Admin() {
{editingProjects.map((project, index) => (
- {/* Move Buttons */}
- {/* Image Preview */}
- {project.title} + {project.title}
- {/* Fields */}
@@ -575,7 +635,7 @@ export default function Admin() { 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" />
@@ -598,27 +658,109 @@ export default function Admin() {
- {/* Delete */}
))} - {editingProjects.length === 0 && ( -
- Henüz proje yok. Yeni bir proje ekleyin. -
- )}

)} - {/* ── Audit Logs Tab ── */} - {tab === 'logs' && ( + {tab === 'settings' && isLoggedIn && ( +
+
+ +

Mail & Sistem Ayarları

+
+ +
+
+

E-POSTA YÖNLENDİRME

+
+
+ + 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" + /> +

İletişim formundan gelen mesajlar bu adrese iletilir.

+
+
+
+ +
+

SMTP YAPILANDIRMASI (OPSİYONEL)

+
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + 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="••••••••" + /> +
+
+ + 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" ' + /> +
+
+
+

+ + SMTP bilgilerini boş bırakırsanız sistem varsayılan sunucu ayarlarını kullanmaya devam eder. +

+
+
+
+
+ )} + + {tab === 'logs' && isLoggedIn && (

@@ -643,98 +785,74 @@ export default function Admin() { > Yenile - {auditTotal} kayıt

- {!isLoggedIn ? ( -
- Logları görmek için giriş yapmanız gerekiyor. -
- ) : auditLoading ? ( -
- Loglar yükleniyor... -
- ) : auditLogs.length === 0 ? ( -
- Henüz işlem logu yok. -
- ) : ( -
- {auditLogs.map((log) => ( -
- {/* Entity Icon */} - {getEntityIcon(log.entity)} - - {/* Action Badge */} - - {log.action} - - - {/* Entity Type */} - - {log.entity} - - - {/* Entity ID (truncated) */} - - {log.entityId.substring(0, 8)}… - - - {/* Details */} - - {log.after ? (() => { - try { - const data = JSON.parse(log.after); - return data.title || data.name || data.section || '—'; - } catch { return '—'; } - })() : log.before ? (() => { - try { - const data = JSON.parse(log.before); - return data.title || data.name || data.section || '—'; - } catch { return '—'; } - })() : '—'} - - - {/* Timestamp */} - - {formatDate(log.createdAt)} - - - {/* Restore button for deleted projects */} - {log.action === 'DELETE' && log.entity === 'project' && ( - - )} -
- ))} -
- )} +
+ {auditLogs.map((log) => ( +
+ {getEntityIcon(log.entity)} + + {log.action} + + + {log.entity} + + + {log.entityId.substring(0, 8)}… + + + {log.after ? (() => { + try { + const data = JSON.parse(log.after); + return data.title || data.name || data.section || '—'; + } catch { return '—'; } + })() : log.before ? (() => { + try { + const data = JSON.parse(log.before); + return data.title || data.name || data.section || '—'; + } catch { return '—'; } + })() : '—'} + + + {formatDate(log.createdAt)} + + {log.action === 'DELETE' && log.entity === 'project' && ( + + )} +
+ ))} +
)} - {/* Footer */}
- {isLoggedIn ? '🟢 Bağlı — API: localhost:3000' : '🔴 Oturum kapalı'} + {isLoggedIn ? '🟢 Bağlı' : '🔴 Oturum kapalı'}
- {(tab === 'projects' || tab === 'clients') && ( + {isLoggedIn && (tab === 'projects' || tab === 'clients' || tab === 'settings') && (
{/* Marquee of client logos */} -
+
{/* Fade masks */}
@@ -36,10 +37,10 @@ export default function Clients() { title={client.name} > {client.name} ))} diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx index e4cfdd4..b527fb7 100644 --- a/src/components/Contact.tsx +++ b/src/components/Contact.tsx @@ -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({ + 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 (
@@ -19,12 +94,15 @@ export default function Contact() {

-
+
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() { 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() {
- 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) => ( - + ))}
@@ -52,16 +137,74 @@ export default function Contact() {