Files
mstfyldz/app/admin/page.tsx
T
2026-08-16 17:18:23 +03:00

600 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useEffect, useMemo } from "react";
import { JournalPost, MoodType } from "@/components/types";
import { MOODS } from "@/components/mockData";
import { EditPostModal } from "@/components/EditPostModal";
import { NewPostModal } from "@/components/NewPostModal";
import { PostDetailModal } from "@/components/PostDetailModal";
import {
Lock,
User,
KeyRound,
LogOut,
Plus,
Edit3,
Trash2,
Eye,
Search,
RefreshCw,
ShieldCheck,
BarChart2,
FileText,
Heart,
Tag,
CheckCircle,
AlertCircle
} from "lucide-react";
export default function AdminPage() {
// Auth state
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [checkingAuth, setCheckingAuth] = useState<boolean>(true);
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [loginError, setLoginError] = useState<string>("");
const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false);
// Data state
const [posts, setPosts] = useState<JournalPost[]>([]);
const [loadingPosts, setLoadingPosts] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState<string>("");
const [selectedMoodFilter, setSelectedMoodFilter] = useState<string>("all");
// Modal states
const [editingPost, setEditingPost] = useState<JournalPost | null>(null);
const [isNewPostOpen, setIsNewPostOpen] = useState<boolean>(false);
const [detailPost, setDetailPost] = useState<JournalPost | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
// Check auth status on mount
useEffect(() => {
async function checkAuth() {
try {
const res = await fetch("/api/admin/check");
if (res.ok) {
const data = await res.json();
setIsAuthenticated(data.authenticated);
}
} catch (err) {
console.error("Auth check failed:", err);
} finally {
setCheckingAuth(false);
}
}
checkAuth();
}, []);
// Fetch posts if authenticated
const fetchPosts = async () => {
setLoadingPosts(true);
try {
const res = await fetch("/api/posts");
if (res.ok) {
const data = await res.json();
setPosts(data);
}
} catch (err) {
console.error("Error fetching posts:", err);
} finally {
setLoadingPosts(false);
}
};
useEffect(() => {
if (isAuthenticated) {
fetchPosts();
}
}, [isAuthenticated]);
// Handle Login
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError("");
setIsLoggingIn(true);
try {
const res = await fetch("/api/admin/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (res.ok) {
setIsAuthenticated(true);
setUsername("");
setPassword("");
} else {
setLoginError(data.error || "Giriş başarısız oldu");
}
} catch (err) {
setLoginError("Sunucuya bağlanılamadı");
} finally {
setIsLoggingIn(false);
}
};
// Handle Logout
const handleLogout = async () => {
try {
await fetch("/api/admin/logout", { method: "POST" });
setIsAuthenticated(false);
} catch (err) {
console.error("Logout failed:", err);
}
};
// Handle Add Post
const handleAddPost = async (newPost: JournalPost) => {
try {
const res = await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newPost),
});
if (res.ok) {
fetchPosts();
}
} catch (err) {
console.error("Error adding post:", err);
}
};
// Handle Update Post
const handleUpdatePost = async (updatedPost: JournalPost) => {
try {
const res = await fetch(`/api/posts/${updatedPost.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updatedPost),
});
if (res.ok) {
setPosts((prev) =>
prev.map((p) => (p.id === updatedPost.id ? updatedPost : p))
);
}
} catch (err) {
console.error("Error updating post:", err);
}
};
// Handle Delete Post
const handleDeletePost = async (id: string) => {
try {
const res = await fetch(`/api/posts/${id}`, {
method: "DELETE",
});
if (res.ok) {
setPosts((prev) => prev.filter((p) => p.id !== id));
setDeletingId(null);
}
} catch (err) {
console.error("Error deleting post:", err);
}
};
// Filtered posts for admin table
const filteredPosts = useMemo(() => {
return posts.filter((post) => {
if (selectedMoodFilter !== "all" && post.mood !== selectedMoodFilter) {
return false;
}
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const matchesContent = post.content.toLowerCase().includes(q);
const matchesTitle = post.title?.toLowerCase().includes(q);
const matchesTags = post.tags.some((t) => t.toLowerCase().includes(q));
return matchesContent || matchesTitle || matchesTags;
}
return true;
});
}, [posts, selectedMoodFilter, searchQuery]);
// Total likes counter
const totalLikes = useMemo(() => {
return posts.reduce((sum, p) => sum + (p.likes || 0), 0);
}, [posts]);
if (checkingAuth) {
return (
<div className="min-h-screen flex items-center justify-center desk-background">
<div className="flex items-center gap-3 text-xs font-mono text-[#52525B]">
<span className="w-2.5 h-2.5 rounded-full bg-[#E11D48] animate-ping" />
<span>Admin Yetkisi Kontrol Ediliyor...</span>
</div>
</div>
);
}
// --- LOGIN VIEW ---
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center p-4 desk-background">
<div className="relative w-full max-w-md notebook-sheet rounded-2xl p-7 sm:p-9 shadow-2xl border border-[#18181B]/15">
{/* Top Washi Tape */}
<div className="washi-tape washi-tape-rose top-[-12px] left-1/2 -translate-x-1/2 w-44" />
{/* Header */}
<div className="text-center mb-6 pt-2">
<div className="w-14 h-14 rounded-full wax-seal mx-auto flex flex-col items-center justify-center text-white mb-3">
<span className="font-heading font-black text-xl leading-none">M</span>
<span className="text-[7px] font-mono tracking-tighter uppercase">ADM</span>
</div>
<h1 className="font-heading font-extrabold text-2xl text-[#18181B]">
Yönetici Girişi
</h1>
<p className="text-xs font-handwritten text-[#787D89] text-lg leading-none mt-1">
mstfyldz journal Defter Yönetim Paneli
</p>
</div>
{/* Error Banner */}
{loginError && (
<div className="mb-5 p-3.5 rounded-xl bg-[#FFE4E6] border border-[#E11D48]/30 flex items-center gap-2.5 text-xs text-[#9F1239] font-mono">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{loginError}</span>
</div>
)}
{/* Login Form */}
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-[11px] font-mono uppercase tracking-wider text-[#787D89] font-bold mb-1">
Kullanıcı Adı (.env):
</label>
<div className="relative">
<User className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-[#787D89]" />
<input
type="text"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="admin"
className="w-full pl-9 pr-3 py-2 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-mono"
/>
</div>
</div>
<div>
<label className="block text-[11px] font-mono uppercase tracking-wider text-[#787D89] font-bold mb-1">
Şifre (.env):
</label>
<div className="relative">
<KeyRound className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-[#787D89]" />
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full pl-9 pr-3 py-2 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-mono"
/>
</div>
</div>
<button
type="submit"
disabled={isLoggingIn}
className="w-full py-2.5 rounded-lg bg-[#E11D48] hover:bg-[#9F1239] text-white font-heading font-bold text-sm shadow-sm hover:shadow transition-all flex items-center justify-center gap-2 cursor-pointer mt-2"
>
<ShieldCheck className="w-4 h-4" />
<span>{isLoggingIn ? "Giriş Yapılıyor..." : "Deftere Giriş Yap"}</span>
</button>
</form>
<div className="mt-6 pt-4 border-t border-[#18181B]/10 text-center">
<p className="text-[11px] font-mono text-[#787D89]">
Giriş bilgileri <code className="bg-[#18181B]/5 px-1 py-0.5 rounded">.env</code> içindeki <code className="text-[#E11D48]">ADMIN_USERNAME</code> ve <code className="text-[#E11D48]">ADMIN_PASSWORD</code> değişkenlerinden okunur.
</p>
</div>
</div>
</div>
);
}
// --- AUTHENTICATED DASHBOARD VIEW ---
return (
<div className="min-h-screen flex flex-col w-full desk-background">
{/* Top Admin Telemetry Header */}
<header className="w-full border-b border-[#18181B]/10 bg-[#FAF7F2]/90 backdrop-blur-md sticky top-0 z-30">
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#E11D48] text-white flex items-center justify-center font-heading font-black text-sm shadow-xs">
M
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-heading font-extrabold text-lg text-[#18181B]">
mstfyldz admin
</span>
<span className="text-[10px] font-mono px-2 py-0.5 rounded bg-[#059669]/10 text-[#059669] border border-[#059669]/20 font-bold flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-[#059669] animate-pulse" />
YETKİLENDİRİLDİ
</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<a
href="/"
target="_blank"
rel="noreferrer"
className="text-xs font-mono text-[#52525B] hover:text-[#18181B] flex items-center gap-1"
>
<Eye className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Siteye Git</span>
</a>
<button
onClick={handleLogout}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#FAF7F2] border border-[#18181B]/15 text-xs font-mono text-[#9F1239] hover:bg-[#FFE4E6] transition-colors cursor-pointer"
>
<LogOut className="w-3.5 h-3.5" />
<span>Çıkış Yap</span>
</button>
</div>
</div>
</header>
{/* Main Admin Dashboard Content */}
<main className="flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 py-8">
{/* Metric Cards Overview */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<div className="notebook-sheet p-4 sm:p-5 rounded-xl border border-[#18181B]/10 flex items-center justify-between">
<div>
<div className="text-xs font-mono text-[#787D89] uppercase tracking-wider font-bold">
TOPLAM KARALAMA
</div>
<div className="text-3xl font-heading font-black text-[#18181B] mt-1">
{posts.length}
</div>
</div>
<div className="w-10 h-10 rounded-full bg-[#F3ECE2] text-[#18181B] flex items-center justify-center">
<FileText className="w-5 h-5" />
</div>
</div>
<div className="notebook-sheet p-4 sm:p-5 rounded-xl border border-[#18181B]/10 flex items-center justify-between">
<div>
<div className="text-xs font-mono text-[#787D89] uppercase tracking-wider font-bold">
TOPLAM BEĞENİ
</div>
<div className="text-3xl font-heading font-black text-[#E11D48] mt-1">
{totalLikes}
</div>
</div>
<div className="w-10 h-10 rounded-full bg-[#FFE4E6] text-[#E11D48] flex items-center justify-center">
<Heart className="w-5 h-5 fill-current" />
</div>
</div>
<div className="notebook-sheet p-4 sm:p-5 rounded-xl border border-[#18181B]/10 flex items-center justify-between">
<div>
<div className="text-xs font-mono text-[#787D89] uppercase tracking-wider font-bold">
VERİTABANI DURUMU
</div>
<div className="text-sm font-mono font-bold text-[#059669] mt-1 flex items-center gap-1.5">
<CheckCircle className="w-4 h-4" />
<span>PostgreSQL (65.109...)</span>
</div>
</div>
<div className="w-10 h-10 rounded-full bg-[#D1FAE5] text-[#059669] flex items-center justify-center">
<BarChart2 className="w-5 h-5" />
</div>
</div>
</div>
{/* Controls & Search Bar */}
<div className="notebook-sheet rounded-2xl p-5 sm:p-6 mb-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<div>
<h2 className="font-heading font-extrabold text-xl text-[#18181B]">
Not &amp; Karalama Yönetimi
</h2>
<p className="text-xs font-serif text-[#52525B] mt-0.5">
PostgreSQL veritabanındaki tüm notları görüntüleyin, güncelleyin veya yeni içerik girin.
</p>
</div>
<div className="flex items-center gap-2.5 w-full md:w-auto">
<button
onClick={fetchPosts}
title="Yenile"
className="p-2 rounded-lg bg-[#FAF7F2] border border-[#18181B]/15 text-[#18181B] hover:bg-[#EFE7D6] transition-colors cursor-pointer"
>
<RefreshCw className={`w-4 h-4 ${loadingPosts ? "animate-spin" : ""}`} />
</button>
<button
onClick={() => setIsNewPostOpen(true)}
className="flex-1 md:flex-initial flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg bg-[#E11D48] text-white text-xs font-heading font-bold hover:bg-[#9F1239] shadow-xs transition-colors cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>+ Yeni Not Ekle</span>
</button>
</div>
</div>
{/* Search & Mood Filter Inputs */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-4 pt-4 border-t border-[#18181B]/10">
<div className="relative sm:col-span-2">
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-[#787D89]" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Başlık, içerik veya etiketlerde ara..."
className="w-full pl-8 pr-3 py-1.5 text-xs bg-white border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-serif"
/>
</div>
<div>
<select
value={selectedMoodFilter}
onChange={(e) => setSelectedMoodFilter(e.target.value)}
className="w-full px-3 py-1.5 text-xs bg-white border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-mono text-[#18181B]"
>
{MOODS.map((m) => (
<option key={m.id} value={m.id}>
{m.label} ({m.stampText})
</option>
))}
</select>
</div>
</div>
</div>
{/* Data Table */}
<div className="notebook-sheet rounded-2xl overflow-hidden shadow-sm border border-[#18181B]/10">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-[#F3ECE2] border-b border-[#18181B]/10 text-[11px] font-mono uppercase text-[#787D89]">
<th className="py-3 px-4 font-bold">Format / Tür</th>
<th className="py-3 px-4 font-bold">Tarih</th>
<th className="py-3 px-4 font-bold">Mood</th>
<th className="py-3 px-4 font-bold">Başlık / İçerik</th>
<th className="py-3 px-4 font-bold">Beğeni</th>
<th className="py-3 px-4 font-bold">Etiketler</th>
<th className="py-3 px-4 font-bold text-right">İşlemler</th>
</tr>
</thead>
<tbody className="divide-y divide-[#18181B]/8 text-xs font-serif text-[#18181B]">
{filteredPosts.length > 0 ? (
filteredPosts.map((post) => (
<tr key={post.id} className="hover:bg-[#FAF7F2]/60 transition-colors">
{/* Format Badge */}
<td className="py-3 px-4 font-mono text-[11px] whitespace-nowrap">
<span className="px-2 py-0.5 rounded uppercase font-bold bg-[#18181B]/5 border border-[#18181B]/10">
{post.type}
</span>
</td>
{/* Date */}
<td className="py-3 px-4 font-mono text-[11px] text-[#52525B] whitespace-nowrap">
{post.date}
</td>
{/* Mood */}
<td className="py-3 px-4 whitespace-nowrap">
<span className="inline-flex items-center gap-1 font-mono text-[11px] text-[#52525B]">
<span className="w-2 h-2 rounded-full bg-[#E11D48]" />
<span>{post.moodLabel}</span>
</span>
</td>
{/* Title / Content Preview */}
<td className="py-3 px-4 max-w-xs">
<div className="font-heading font-bold text-[#18181B] truncate">
{post.title || "(Başlıksız Not)"}
</div>
<div className="text-[11px] text-[#787D89] truncate">
{post.content}
</div>
</td>
{/* Likes */}
<td className="py-3 px-4 font-mono font-bold text-[#E11D48] whitespace-nowrap">
{post.likes}
</td>
{/* Tags */}
<td className="py-3 px-4">
<div className="flex items-center gap-1 flex-wrap">
{post.tags.map((t) => (
<span key={t} className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-[#F3ECE2] text-[#52525B]">
#{t}
</span>
))}
</div>
</td>
{/* Actions */}
<td className="py-3 px-4 text-right whitespace-nowrap">
<div className="flex items-center justify-end gap-1.5">
<button
onClick={() => setDetailPost(post)}
title="Önizle"
className="p-1.5 rounded bg-[#FAF7F2] border border-[#18181B]/10 text-[#52525B] hover:text-[#18181B] hover:bg-[#EFE7D6] transition-colors"
>
<Eye className="w-3.5 h-3.5" />
</button>
<button
onClick={() => setEditingPost(post)}
title="Düzenle"
className="p-1.5 rounded bg-[#FEF3C7] border border-[#D97706]/30 text-[#D97706] hover:bg-[#FDE68A] transition-colors"
>
<Edit3 className="w-3.5 h-3.5" />
</button>
{deletingId === post.id ? (
<div className="inline-flex items-center gap-1">
<button
onClick={() => handleDeletePost(post.id)}
className="px-2 py-0.5 rounded bg-[#E11D48] text-white text-[10px] font-mono font-bold hover:bg-[#9F1239]"
>
Eminim Sil
</button>
<button
onClick={() => setDeletingId(null)}
className="text-[10px] font-mono text-[#787D89] hover:underline"
>
İptal
</button>
</div>
) : (
<button
onClick={() => setDeletingId(post.id)}
title="Sil"
className="p-1.5 rounded bg-[#FFE4E6] border border-[#E11D48]/30 text-[#E11D48] hover:bg-[#FECDD3] transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan={7} className="py-8 text-center text-xs font-mono text-[#787D89]">
Filtrelerle eşleşen not bulunamadı.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</main>
{/* Modals */}
<EditPostModal
post={editingPost}
isOpen={!!editingPost}
onClose={() => setEditingPost(null)}
onUpdatePost={handleUpdatePost}
/>
<NewPostModal
isOpen={isNewPostOpen}
onClose={() => setIsNewPostOpen(false)}
onAddPost={handleAddPost}
/>
<PostDetailModal
post={detailPost}
onClose={() => setDetailPost(null)}
onTagClick={() => {}}
/>
</div>
);
}