1475 lines
61 KiB
TypeScript
1475 lines
61 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import {
|
|
createLesson,
|
|
updateLesson,
|
|
getLessons,
|
|
deleteLesson,
|
|
getDashboardStats
|
|
} from '@/lib/actions/lessonActions';
|
|
import { getUsers, createAdminUser } from '@/lib/actions/userActions';
|
|
import { getSettings, updateSettings } from '@/lib/actions/settingsActions';
|
|
import { getContactMessages } from '@/lib/actions/contactActions';
|
|
import {
|
|
getCategories,
|
|
createCategory,
|
|
updateCategory,
|
|
deleteCategory
|
|
} from '@/lib/actions/categoryActions';
|
|
import {
|
|
getCheatsheets,
|
|
createCheatsheet,
|
|
updateCheatsheet,
|
|
deleteCheatsheet
|
|
} from '@/lib/actions/cheatsheetActions';
|
|
import {
|
|
ShieldAlert,
|
|
Plus,
|
|
Save,
|
|
FileCode,
|
|
CheckCircle2,
|
|
Trash2,
|
|
Edit,
|
|
LayoutDashboard,
|
|
BookOpen,
|
|
Users,
|
|
Settings as SettingsIcon,
|
|
MessageSquare,
|
|
Eye,
|
|
Download,
|
|
Video,
|
|
Loader2,
|
|
Search,
|
|
X,
|
|
Clock,
|
|
FileText,
|
|
Link as LinkIcon,
|
|
FolderTree,
|
|
Tag,
|
|
Terminal
|
|
} from 'lucide-react';
|
|
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
|
|
|
function parseChaptersText(text: string) {
|
|
if (!text || !text.trim()) return [];
|
|
const lines = text.split('\n');
|
|
const result: { time: string; seconds: number; title: string }[] = [];
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
|
|
const match = trimmed.match(/^(\d{1,2}:\d{2}(?::\d{2})?)\s*[-:]?\s*(.+)$/);
|
|
if (match) {
|
|
const timeStr = match[1];
|
|
const titleStr = match[2];
|
|
const parts = timeStr.split(':').map(Number);
|
|
let seconds = 0;
|
|
if (parts.length === 2) {
|
|
seconds = parts[0] * 60 + parts[1];
|
|
} else if (parts.length === 3) {
|
|
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
}
|
|
result.push({ time: timeStr, seconds, title: titleStr });
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function parseNotesText(text: string) {
|
|
if (!text || !text.trim()) return [];
|
|
return text
|
|
.split('\n')
|
|
.map((l) => l.trim())
|
|
.filter((l) => l.length > 0);
|
|
}
|
|
|
|
export default function AdminPage() {
|
|
const t = useTranslations('admin');
|
|
const [activeTab, setActiveTab] = useState<'dashboard' | 'lessons' | 'cheatsheets' | 'categories' | 'users' | 'settings' | 'messages'>('dashboard');
|
|
|
|
// Stats
|
|
const [stats, setStats] = useState({ totalLessons: 0, totalViews: 0, totalDownloads: 0, totalMessages: 0 });
|
|
|
|
// Lessons list & Search
|
|
const [lessons, setLessons] = useState<any[]>([]);
|
|
const [lessonSearch, setLessonSearch] = useState('');
|
|
const [loadingLessons, setLoadingLessons] = useState(false);
|
|
|
|
// Dynamic Categories State
|
|
const [categoriesList, setCategoriesList] = useState<any[]>([]);
|
|
const [newCatName, setNewCatName] = useState('');
|
|
const [editingCatId, setEditingCatId] = useState<string | null>(null);
|
|
const [editingCatName, setEditingCatName] = useState('');
|
|
|
|
// Cheatsheets State
|
|
const [cheatsheetsList, setCheatsheetsList] = useState<any[]>([]);
|
|
const [isCheatFormOpen, setIsCheatFormOpen] = useState(false);
|
|
const [editingCheatId, setEditingCheatId] = useState<string | null>(null);
|
|
const [cheatTitle, setCheatTitle] = useState('');
|
|
const [cheatCategory, setCheatCategory] = useState('Git');
|
|
const [cheatDesc, setCheatDesc] = useState('');
|
|
const [cheatItems, setCheatItems] = useState<{ command: string; description: string }[]>([
|
|
{ command: 'git status', description: 'Check working tree status' },
|
|
]);
|
|
const [savingCheat, setSavingCheat] = useState(false);
|
|
|
|
// Lesson Form Modal State (Create or Edit)
|
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
const [editingLessonId, setEditingLessonId] = useState<string | null>(null);
|
|
const [formTitle, setFormTitle] = useState('');
|
|
const [formYoutubeUrl, setFormYoutubeUrl] = useState('');
|
|
const [formCategory, setFormCategory] = useState('Next.js');
|
|
const [formDuration, setFormDuration] = useState('15:00');
|
|
const [formSummary, setFormSummary] = useState('');
|
|
const [formChaptersText, setFormChaptersText] = useState('');
|
|
const [formNotesText, setFormNotesText] = useState('');
|
|
const [formDownloads, setFormDownloads] = useState<
|
|
{ title: string; type: string; url: string; size?: string }[]
|
|
>([]);
|
|
const [formSnippets, setFormSnippets] = useState<
|
|
{ fileName: string; language: string; code: string }[]
|
|
>([{ fileName: 'app/page.tsx', language: 'typescript', code: '' }]);
|
|
const [savingLesson, setSavingLesson] = useState(false);
|
|
|
|
// Users State
|
|
const [usersList, setUsersList] = useState<any[]>([]);
|
|
const [newAdminName, setNewAdminName] = useState('');
|
|
const [newAdminEmail, setNewAdminEmail] = useState('');
|
|
const [newAdminPass, setNewAdminPass] = useState('');
|
|
const [savingUser, setSavingUser] = useState(false);
|
|
|
|
// Settings State
|
|
const [settings, setSettingsData] = useState<Record<string, string>>({
|
|
channelName: '',
|
|
youtubeUrl: '',
|
|
githubUrl: '',
|
|
contactEmail: '',
|
|
defaultCategory: 'Next.js',
|
|
});
|
|
const [savingSettings, setSavingSettings] = useState(false);
|
|
|
|
// Messages State
|
|
const [messages, setMessages] = useState<any[]>([]);
|
|
|
|
// Alert Notifications
|
|
const [alert, setAlert] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
|
|
|
|
useEffect(() => {
|
|
loadTabContent();
|
|
}, [activeTab]);
|
|
|
|
useEffect(() => {
|
|
// Pre-fetch categories for lesson modal
|
|
getCategories().then((cats) => {
|
|
setCategoriesList(cats);
|
|
if (cats.length > 0 && !formCategory) {
|
|
setFormCategory(cats[0].name);
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
async function loadTabContent() {
|
|
if (activeTab === 'dashboard') {
|
|
const [st, les] = await Promise.all([getDashboardStats(), getLessons()]);
|
|
setStats(st);
|
|
setLessons(les.slice(0, 5));
|
|
} else if (activeTab === 'lessons') {
|
|
setLoadingLessons(true);
|
|
const [les, cats] = await Promise.all([getLessons({ query: lessonSearch }), getCategories()]);
|
|
setLessons(les);
|
|
setCategoriesList(cats);
|
|
setLoadingLessons(false);
|
|
} else if (activeTab === 'cheatsheets') {
|
|
const sheets = await getCheatsheets();
|
|
setCheatsheetsList(sheets);
|
|
} else if (activeTab === 'categories') {
|
|
const cats = await getCategories();
|
|
setCategoriesList(cats);
|
|
} else if (activeTab === 'users') {
|
|
const u = await getUsers();
|
|
setUsersList(u);
|
|
} else if (activeTab === 'settings') {
|
|
const s = await getSettings();
|
|
setSettingsData(s);
|
|
} else if (activeTab === 'messages') {
|
|
const m = await getContactMessages();
|
|
setMessages(m);
|
|
}
|
|
}
|
|
|
|
// Filter lessons on search typing
|
|
useEffect(() => {
|
|
if (activeTab === 'lessons') {
|
|
getLessons({ query: lessonSearch }).then(setLessons);
|
|
}
|
|
}, [lessonSearch]);
|
|
|
|
const showAlert = (type: 'success' | 'error', msg: string) => {
|
|
setAlert({ type, msg });
|
|
setTimeout(() => setAlert(null), 4000);
|
|
};
|
|
|
|
// Open Create Form
|
|
const handleOpenCreate = () => {
|
|
setEditingLessonId(null);
|
|
setFormTitle('');
|
|
setFormYoutubeUrl('');
|
|
setFormCategory(categoriesList[0]?.name || 'Next.js');
|
|
setFormDuration('15:00');
|
|
setFormSummary('');
|
|
setFormChaptersText('');
|
|
setFormNotesText('');
|
|
setFormDownloads([]);
|
|
setFormSnippets([{ fileName: 'app/page.tsx', language: 'typescript', code: '' }]);
|
|
setIsFormOpen(true);
|
|
};
|
|
|
|
// Open Edit Form
|
|
const handleOpenEdit = (lesson: any) => {
|
|
setEditingLessonId(lesson.id);
|
|
setFormTitle(lesson.title);
|
|
setFormYoutubeUrl(lesson.youtubeUrl);
|
|
setFormCategory(lesson.category);
|
|
setFormDuration(lesson.duration || '15:00');
|
|
setFormSummary(lesson.summary);
|
|
setFormNotesText(lesson.notesMarkdown ? lesson.notesMarkdown.join('\n') : '');
|
|
setFormChaptersText(
|
|
lesson.chapters ? lesson.chapters.map((c: any) => `${c.time} - ${c.title}`).join('\n') : ''
|
|
);
|
|
setFormDownloads(
|
|
lesson.downloads
|
|
? lesson.downloads.map((d: any) => ({
|
|
title: d.title,
|
|
type: d.type,
|
|
url: d.url,
|
|
size: d.size || '1.0 MB',
|
|
}))
|
|
: []
|
|
);
|
|
|
|
if (lesson.codeSnippets && lesson.codeSnippets.length > 0) {
|
|
setFormSnippets(
|
|
lesson.codeSnippets.map((s: any) => ({
|
|
fileName: s.fileName,
|
|
language: s.language,
|
|
code: s.code,
|
|
}))
|
|
);
|
|
} else {
|
|
setFormSnippets([{ fileName: 'app/page.tsx', language: 'typescript', code: '' }]);
|
|
}
|
|
setIsFormOpen(true);
|
|
};
|
|
|
|
const handleAddSnippet = () => {
|
|
setFormSnippets([...formSnippets, { fileName: 'app/actions.ts', language: 'typescript', code: '' }]);
|
|
};
|
|
|
|
const handleRemoveSnippet = (idx: number) => {
|
|
setFormSnippets(formSnippets.filter((_, i) => i !== idx));
|
|
};
|
|
|
|
const handleAddDownload = () => {
|
|
setFormDownloads([
|
|
...formDownloads,
|
|
{ title: 'Completed Project (.zip)', type: 'zip', url: '#', size: '2.5 MB' },
|
|
]);
|
|
};
|
|
|
|
const handleRemoveDownload = (idx: number) => {
|
|
setFormDownloads(formDownloads.filter((_, i) => i !== idx));
|
|
};
|
|
|
|
// Save (Create or Update) Lesson
|
|
const handleSaveLesson = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSavingLesson(true);
|
|
|
|
const chapters = parseChaptersText(formChaptersText);
|
|
const notesMarkdown = parseNotesText(formNotesText);
|
|
|
|
let res;
|
|
if (editingLessonId) {
|
|
res = await updateLesson(editingLessonId, {
|
|
title: formTitle,
|
|
youtubeUrl: formYoutubeUrl,
|
|
category: formCategory,
|
|
duration: formDuration,
|
|
summary: formSummary,
|
|
chapters,
|
|
notesMarkdown,
|
|
downloads: formDownloads,
|
|
codeSnippets: formSnippets,
|
|
});
|
|
} else {
|
|
res = await createLesson({
|
|
title: formTitle,
|
|
youtubeUrl: formYoutubeUrl,
|
|
category: formCategory,
|
|
duration: formDuration,
|
|
summary: formSummary,
|
|
chapters,
|
|
notesMarkdown,
|
|
downloads: formDownloads,
|
|
codeSnippets: formSnippets,
|
|
});
|
|
}
|
|
|
|
setSavingLesson(false);
|
|
|
|
if (res.success) {
|
|
showAlert('success', editingLessonId ? 'Lesson updated successfully!' : 'New lesson created successfully!');
|
|
setIsFormOpen(false);
|
|
loadTabContent();
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to save lesson');
|
|
}
|
|
};
|
|
|
|
// Cheatsheet Handlers
|
|
const handleOpenCheatCreate = () => {
|
|
setEditingCheatId(null);
|
|
setCheatTitle('');
|
|
setCheatCategory('Git');
|
|
setCheatDesc('');
|
|
setCheatItems([{ command: 'git status', description: 'Check working tree status' }]);
|
|
setIsCheatFormOpen(true);
|
|
};
|
|
|
|
const handleOpenCheatEdit = (sheet: any) => {
|
|
setEditingCheatId(sheet.id);
|
|
setCheatTitle(sheet.title);
|
|
setCheatCategory(sheet.category);
|
|
setCheatDesc(sheet.description);
|
|
setCheatItems(
|
|
sheet.items && sheet.items.length > 0
|
|
? sheet.items.map((i: any) => ({ command: i.command, description: i.description }))
|
|
: [{ command: '', description: '' }]
|
|
);
|
|
setIsCheatFormOpen(true);
|
|
};
|
|
|
|
const handleSaveCheatsheet = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSavingCheat(true);
|
|
|
|
let res;
|
|
if (editingCheatId) {
|
|
res = await updateCheatsheet(editingCheatId, {
|
|
title: cheatTitle,
|
|
category: cheatCategory,
|
|
description: cheatDesc,
|
|
items: cheatItems,
|
|
});
|
|
} else {
|
|
res = await createCheatsheet({
|
|
title: cheatTitle,
|
|
category: cheatCategory,
|
|
description: cheatDesc,
|
|
items: cheatItems,
|
|
});
|
|
}
|
|
|
|
setSavingCheat(false);
|
|
|
|
if (res.success) {
|
|
showAlert('success', editingCheatId ? 'Cheatsheet updated successfully!' : 'New cheatsheet created successfully!');
|
|
setIsCheatFormOpen(false);
|
|
getCheatsheets().then(setCheatsheetsList);
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to save cheatsheet');
|
|
}
|
|
};
|
|
|
|
const handleDeleteCheatsheet = async (id: string, title: string) => {
|
|
if (!confirm(`Are you sure you want to delete cheatsheet "${title}"?`)) return;
|
|
const res = await deleteCheatsheet(id);
|
|
if (res.success) {
|
|
showAlert('success', `Cheatsheet "${title}" deleted`);
|
|
getCheatsheets().then(setCheatsheetsList);
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to delete cheatsheet');
|
|
}
|
|
};
|
|
|
|
// Category CRUD Handlers
|
|
const handleCreateCategory = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!newCatName.trim()) return;
|
|
const res = await createCategory(newCatName);
|
|
if (res.success) {
|
|
showAlert('success', `Category "${newCatName}" created successfully!`);
|
|
setNewCatName('');
|
|
getCategories().then(setCategoriesList);
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to create category');
|
|
}
|
|
};
|
|
|
|
const handleUpdateCategory = async (id: string) => {
|
|
if (!editingCatName.trim()) return;
|
|
const res = await updateCategory(id, editingCatName);
|
|
if (res.success) {
|
|
showAlert('success', 'Category updated successfully!');
|
|
setEditingCatId(null);
|
|
setEditingCatName('');
|
|
getCategories().then(setCategoriesList);
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to update category');
|
|
}
|
|
};
|
|
|
|
const handleDeleteCategory = async (id: string, name: string) => {
|
|
if (!confirm(`Are you sure you want to delete category "${name}"?`)) return;
|
|
const res = await deleteCategory(id);
|
|
if (res.success) {
|
|
showAlert('success', `Category "${name}" deleted`);
|
|
getCategories().then(setCategoriesList);
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to delete category');
|
|
}
|
|
};
|
|
|
|
// Delete Lesson
|
|
const handleDeleteLesson = async (id: string) => {
|
|
if (!confirm('Are you sure you want to delete this tutorial?')) return;
|
|
const res = await deleteLesson(id);
|
|
if (res.success) {
|
|
showAlert('success', 'Lesson deleted');
|
|
setLessons(lessons.filter((l) => l.id !== id));
|
|
}
|
|
};
|
|
|
|
// Create Admin User
|
|
const handleCreateUser = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSavingUser(true);
|
|
const res = await createAdminUser({ name: newAdminName, email: newAdminEmail, password: newAdminPass });
|
|
setSavingUser(false);
|
|
if (res.success) {
|
|
showAlert('success', 'New Admin user created successfully!');
|
|
setNewAdminName('');
|
|
setNewAdminEmail('');
|
|
setNewAdminPass('');
|
|
getUsers().then(setUsersList);
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to create user');
|
|
}
|
|
};
|
|
|
|
// Save Settings
|
|
const handleSaveSettings = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSavingSettings(true);
|
|
const res = await updateSettings(settings);
|
|
setSavingSettings(false);
|
|
if (res.success) {
|
|
showAlert('success', 'Settings updated successfully!');
|
|
} else {
|
|
showAlert('error', res.error || 'Failed to update settings');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 flex-1 w-full flex flex-col md:flex-row gap-8">
|
|
|
|
{/* Left Vertical Sidebar Navigation */}
|
|
<aside className="w-full md:w-64 flex-shrink-0 space-y-6">
|
|
<div className="p-4 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-2">
|
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-red-500/10 text-red-400 text-xs font-bold border border-red-500/20">
|
|
<ShieldAlert className="w-3.5 h-3.5" />
|
|
<span>Creator Admin</span>
|
|
</div>
|
|
<h1 className="text-xl font-black text-white tracking-tight">CMS Portal</h1>
|
|
<p className="text-[11px] text-slate-400">ayris.tech Management</p>
|
|
</div>
|
|
|
|
{/* Sidebar Nav Buttons */}
|
|
<nav className="flex flex-col gap-1.5 bg-slate-900/80 p-2 rounded-2xl border border-slate-800">
|
|
<button
|
|
onClick={() => setActiveTab('dashboard')}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
|
activeTab === 'dashboard'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<LayoutDashboard className="w-4 h-4" />
|
|
<span>Dashboard</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('lessons')}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
|
activeTab === 'lessons'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<BookOpen className="w-4 h-4" />
|
|
<span>Lessons & Code</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('cheatsheets')}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
|
activeTab === 'cheatsheets'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<Terminal className="w-4 h-4" />
|
|
<span>Cheatsheets</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('categories')}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
|
activeTab === 'categories'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<FolderTree className="w-4 h-4" />
|
|
<span>Categories</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('users')}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
|
activeTab === 'users'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<Users className="w-4 h-4" />
|
|
<span>Users</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('settings')}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
|
activeTab === 'settings'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<SettingsIcon className="w-4 h-4" />
|
|
<span>Settings</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('messages')}
|
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
|
activeTab === 'messages'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
|
|
}`}
|
|
>
|
|
<MessageSquare className="w-4 h-4" />
|
|
<span>Messages</span>
|
|
</button>
|
|
</nav>
|
|
</aside>
|
|
|
|
{/* Main Content View */}
|
|
<main className="flex-1 space-y-6 min-w-0">
|
|
|
|
{/* Alert Banner */}
|
|
{alert && (
|
|
<div className={`p-4 rounded-2xl border text-xs font-bold flex items-center justify-between shadow-lg ${
|
|
alert.type === 'success' ? 'bg-emerald-950/60 border-emerald-800 text-emerald-300' : 'bg-red-950/60 border-red-800 text-red-300'
|
|
}`}>
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle2 className="w-4 h-4" />
|
|
<span>{alert.msg}</span>
|
|
</div>
|
|
<button onClick={() => setAlert(null)}><X className="w-4 h-4" /></button>
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 1: DASHBOARD STATS */}
|
|
{activeTab === 'dashboard' && (
|
|
<div className="space-y-8">
|
|
|
|
{/* Stat Cards Grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-2">
|
|
<div className="flex items-center justify-between text-slate-400">
|
|
<span className="text-xs font-semibold">Total Tutorials</span>
|
|
<Video className="w-5 h-5 text-red-500" />
|
|
</div>
|
|
<div className="text-3xl font-black text-white">{stats.totalLessons}</div>
|
|
</div>
|
|
|
|
<div className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-2">
|
|
<div className="flex items-center justify-between text-slate-400">
|
|
<span className="text-xs font-semibold">Total Views</span>
|
|
<Eye className="w-5 h-5 text-sky-400" />
|
|
</div>
|
|
<div className="text-3xl font-black text-white">{stats.totalViews.toLocaleString()}</div>
|
|
</div>
|
|
|
|
<div className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-2">
|
|
<div className="flex items-center justify-between text-slate-400">
|
|
<span className="text-xs font-semibold">Project Downloads</span>
|
|
<Download className="w-5 h-5 text-emerald-400" />
|
|
</div>
|
|
<div className="text-3xl font-black text-white">{stats.totalDownloads.toLocaleString()}</div>
|
|
</div>
|
|
|
|
<div className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-2">
|
|
<div className="flex items-center justify-between text-slate-400">
|
|
<span className="text-xs font-semibold">Viewer Inquiries</span>
|
|
<MessageSquare className="w-5 h-5 text-amber-400" />
|
|
</div>
|
|
<div className="text-3xl font-black text-white">{stats.totalMessages}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Lessons Table Preview */}
|
|
<div className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-extrabold text-white">Recent YouTube Tutorials</h2>
|
|
<button
|
|
onClick={() => setActiveTab('lessons')}
|
|
className="text-xs font-bold text-red-400 hover:underline"
|
|
>
|
|
Manage All Lessons →
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{lessons.map((lesson) => (
|
|
<div key={lesson.id} className="p-4 rounded-2xl bg-slate-950 border border-slate-800 flex items-center justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<span className="text-[10px] font-bold text-red-400 bg-red-500/10 px-2 py-0.5 rounded border border-red-500/20">
|
|
{lesson.category}
|
|
</span>
|
|
<h3 className="font-bold text-white text-sm">{lesson.title}</h3>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => { setActiveTab('lessons'); handleOpenEdit(lesson); }}
|
|
className="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs font-bold text-slate-200 flex items-center gap-1"
|
|
>
|
|
<Edit className="w-3.5 h-3.5 text-sky-400" />
|
|
<span>Edit</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 2: LESSONS & EDIT MODAL */}
|
|
{activeTab === 'lessons' && (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
|
{/* Search Input */}
|
|
<div className="relative w-full sm:w-80">
|
|
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
|
<input
|
|
type="text"
|
|
placeholder="Filter lessons by title or code..."
|
|
value={lessonSearch}
|
|
onChange={(e) => setLessonSearch(e.target.value)}
|
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-10 pr-4 py-2 text-xs text-white placeholder-slate-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleOpenCreate}
|
|
className="px-5 py-2.5 rounded-xl bg-red-600 hover:bg-red-500 text-white text-xs font-bold flex items-center gap-2 shadow-lg shadow-red-600/30 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Create New Tutorial</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Lessons Table / List */}
|
|
{loadingLessons ? (
|
|
<div className="p-12 text-center text-slate-400 flex items-center justify-center gap-2">
|
|
<Loader2 className="w-5 h-5 animate-spin text-red-500" />
|
|
<span>Loading tutorials...</span>
|
|
</div>
|
|
) : lessons.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{lessons.map((item) => (
|
|
<div key={item.id} className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 flex items-center justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-[10px] font-bold text-red-400 bg-red-500/10 px-2 py-0.5 rounded border border-red-500/20">
|
|
{item.category}
|
|
</span>
|
|
<span className="text-[10px] text-slate-500 font-mono">
|
|
{item.codeSnippets?.length || 0} Files | {item.chapters?.length || 0} Timestamps
|
|
</span>
|
|
</div>
|
|
<h3 className="font-bold text-white text-sm">{item.title}</h3>
|
|
<p className="text-xs text-slate-400 line-clamp-1">{item.summary}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => handleOpenEdit(item)}
|
|
className="px-3 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-bold flex items-center gap-1.5 transition-colors border border-slate-700/60"
|
|
>
|
|
<Edit className="w-3.5 h-3.5 text-sky-400" />
|
|
<span>Edit</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleDeleteLesson(item.id)}
|
|
className="p-2 rounded-xl bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white transition-colors"
|
|
title="Delete Lesson"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="p-12 text-center bg-slate-900/40 rounded-2xl border border-slate-800 text-slate-400 text-xs">
|
|
No tutorials found.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 3: CHEATSHEETS CRUD */}
|
|
{activeTab === 'cheatsheets' && (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-extrabold text-white">Cheatsheets & Commands ({cheatsheetsList.length})</h2>
|
|
|
|
<button
|
|
onClick={handleOpenCheatCreate}
|
|
className="px-5 py-2.5 rounded-xl bg-red-600 hover:bg-red-500 text-white text-xs font-bold flex items-center gap-2 shadow-lg shadow-red-600/30 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Create New Cheatsheet</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{cheatsheetsList.map((sheet) => (
|
|
<div key={sheet.id} className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-3">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div>
|
|
<span className="text-[10px] font-bold text-red-400 bg-red-500/10 px-2 py-0.5 rounded border border-red-500/20">
|
|
{sheet.category}
|
|
</span>
|
|
<h3 className="font-bold text-white text-base mt-1">{sheet.title}</h3>
|
|
<p className="text-xs text-slate-400">{sheet.description}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => handleOpenCheatEdit(sheet)}
|
|
className="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-bold text-sky-400 flex items-center gap-1"
|
|
>
|
|
<Edit className="w-3.5 h-3.5" />
|
|
<span>Edit</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleDeleteCheatsheet(sheet.id, sheet.title)}
|
|
className="p-1.5 rounded-xl bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white transition-colors"
|
|
title="Delete Cheatsheet"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 pt-2 border-t border-slate-800">
|
|
{sheet.items?.slice(0, 3).map((item: any, idx: number) => (
|
|
<div key={idx} className="p-2.5 rounded-lg bg-slate-950 border border-slate-800 flex items-center justify-between text-xs font-mono">
|
|
<span className="text-red-300">{item.command}</span>
|
|
<span className="text-slate-500 text-[11px]">{item.description}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 4: CATEGORIES CRUD */}
|
|
{activeTab === 'categories' && (
|
|
<div className="space-y-6">
|
|
|
|
{/* Create Category Form */}
|
|
<form onSubmit={handleCreateCategory} className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-4">
|
|
<h2 className="text-base font-bold text-white flex items-center gap-2">
|
|
<FolderTree className="w-4 h-4 text-red-400" />
|
|
<span>Create New Category</span>
|
|
</h2>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Category Name (e.g. Rust & WASM, Next.js, Go Microservices)"
|
|
value={newCatName}
|
|
onChange={(e) => setNewCatName(e.target.value)}
|
|
className="bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none flex-1"
|
|
/>
|
|
|
|
<button
|
|
type="submit"
|
|
className="px-5 py-2.5 bg-red-600 hover:bg-red-500 text-white text-xs font-bold rounded-xl flex items-center gap-2 shadow-lg shadow-red-600/30 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Add Category</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Categories List */}
|
|
<div className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-4">
|
|
<h2 className="text-base font-bold text-white">Categories ({categoriesList.length})</h2>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{categoriesList.map((cat) => (
|
|
<div key={cat.id} className="p-4 rounded-2xl bg-slate-950 border border-slate-800 flex items-center justify-between gap-3">
|
|
{editingCatId === cat.id ? (
|
|
<div className="flex items-center gap-2 flex-1">
|
|
<input
|
|
type="text"
|
|
value={editingCatName}
|
|
onChange={(e) => setEditingCatName(e.target.value)}
|
|
className="bg-slate-900 border border-slate-700 rounded-lg px-3 py-1 text-xs text-white focus:outline-none flex-1"
|
|
/>
|
|
<button
|
|
onClick={() => handleUpdateCategory(cat.id)}
|
|
className="p-1.5 rounded-lg bg-emerald-600 text-white text-xs font-bold"
|
|
>
|
|
Save
|
|
</button>
|
|
<button
|
|
onClick={() => setEditingCatId(null)}
|
|
className="p-1.5 rounded-lg bg-slate-800 text-slate-400 hover:text-white"
|
|
>
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center gap-2">
|
|
<Tag className="w-4 h-4 text-red-400" />
|
|
<span className="font-bold text-white text-xs">{cat.name}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
<button
|
|
onClick={() => { setEditingCatId(cat.id); setEditingCatName(cat.name); }}
|
|
className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-sky-400 transition-colors"
|
|
title="Edit Category Name"
|
|
>
|
|
<Edit className="w-3.5 h-3.5" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleDeleteCategory(cat.id, cat.name)}
|
|
className="p-1.5 rounded-lg bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white transition-colors"
|
|
title="Delete Category"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 5: USERS / ADMINS */}
|
|
{activeTab === 'users' && (
|
|
<div className="space-y-6">
|
|
{/* Create User Form */}
|
|
<form onSubmit={handleCreateUser} className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-4">
|
|
<h2 className="text-base font-bold text-white flex items-center gap-2">
|
|
<Users className="w-4 h-4 text-red-400" />
|
|
<span>Add New Admin User</span>
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Full Name"
|
|
value={newAdminName}
|
|
onChange={(e) => setNewAdminName(e.target.value)}
|
|
className="bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
|
|
/>
|
|
|
|
<input
|
|
type="email"
|
|
required
|
|
placeholder="Email Address"
|
|
value={newAdminEmail}
|
|
onChange={(e) => setNewAdminEmail(e.target.value)}
|
|
className="bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
|
|
/>
|
|
|
|
<input
|
|
type="password"
|
|
required
|
|
placeholder="Password"
|
|
value={newAdminPass}
|
|
onChange={(e) => setNewAdminPass(e.target.value)}
|
|
className="bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<button
|
|
type="submit"
|
|
disabled={savingUser}
|
|
className="px-5 py-2.5 bg-red-600 hover:bg-red-500 text-white text-xs font-bold rounded-xl flex items-center gap-2 shadow-lg shadow-red-600/30"
|
|
>
|
|
{savingUser ? 'Creating...' : 'Create Admin User'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Users List Table */}
|
|
<div className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-4">
|
|
<h2 className="text-base font-bold text-white">Admin Users ({usersList.length})</h2>
|
|
|
|
<div className="space-y-3">
|
|
{usersList.map((u) => (
|
|
<div key={u.id} className="p-4 rounded-2xl bg-slate-950 border border-slate-800 flex items-center justify-between text-xs">
|
|
<div>
|
|
<span className="font-bold text-white block">{u.name || 'Admin User'}</span>
|
|
<span className="text-slate-400">{u.email}</span>
|
|
</div>
|
|
|
|
<span className="px-3 py-1 rounded-full bg-red-500/10 text-red-400 font-bold border border-red-500/20">
|
|
{u.role}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 6: SETTINGS */}
|
|
{activeTab === 'settings' && (
|
|
<form onSubmit={handleSaveSettings} className="p-6 sm:p-8 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-6">
|
|
<h2 className="text-lg font-extrabold text-white flex items-center gap-2 border-b border-slate-800 pb-4">
|
|
<SettingsIcon className="w-5 h-5 text-red-500" />
|
|
<span>Channel & Creator Settings</span>
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">YouTube Channel Name</label>
|
|
<input
|
|
type="text"
|
|
value={settings.channelName || ''}
|
|
onChange={(e) => setSettingsData({ ...settings, channelName: e.target.value })}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">YouTube Channel URL</label>
|
|
<input
|
|
type="text"
|
|
value={settings.youtubeUrl || ''}
|
|
onChange={(e) => setSettingsData({ ...settings, youtubeUrl: e.target.value })}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">GitHub Repositories URL</label>
|
|
<input
|
|
type="text"
|
|
value={settings.githubUrl || ''}
|
|
onChange={(e) => setSettingsData({ ...settings, githubUrl: e.target.value })}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">Contact Email Address</label>
|
|
<input
|
|
type="email"
|
|
value={settings.contactEmail || ''}
|
|
onChange={(e) => setSettingsData({ ...settings, contactEmail: e.target.value })}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<button
|
|
type="submit"
|
|
disabled={savingSettings}
|
|
className="px-6 py-3 bg-red-600 hover:bg-red-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-red-600/30 flex items-center gap-2"
|
|
>
|
|
{savingSettings ? 'Saving...' : 'Save Settings'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* TAB 7: MESSAGES */}
|
|
{activeTab === 'messages' && (
|
|
<div className="space-y-4">
|
|
<h2 className="text-lg font-bold text-white">Viewer Contact Messages ({messages.length})</h2>
|
|
|
|
{messages.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{messages.map((msg) => (
|
|
<div key={msg.id} className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-2">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="font-bold text-white">{msg.name} ({msg.email})</span>
|
|
<span className="text-[10px] text-slate-500">{new Date(msg.createdAt).toLocaleString()}</span>
|
|
</div>
|
|
<h4 className="text-xs font-semibold text-red-400">{msg.subject}</h4>
|
|
<p className="text-xs text-slate-300 leading-relaxed bg-slate-950 p-3 rounded-xl border border-slate-800">
|
|
{msg.message}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="p-8 text-center bg-slate-900/40 rounded-2xl border border-slate-800 text-slate-400 text-xs">
|
|
No viewer contact messages received yet.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
</main>
|
|
|
|
{/* CREATE / EDIT CHEATSHEET FORM MODAL */}
|
|
{isCheatFormOpen && (
|
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 overflow-y-auto">
|
|
<div className="w-full max-w-2xl bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-2xl my-8">
|
|
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
|
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
|
<Terminal className="w-5 h-5 text-red-500" />
|
|
<span>{editingCheatId ? 'Edit Cheatsheet' : 'Create New Cheatsheet'}</span>
|
|
</h2>
|
|
<button onClick={() => setIsCheatFormOpen(false)} className="p-1.5 rounded-lg bg-slate-800 text-slate-400 hover:text-white">
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSaveCheatsheet} className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">Title</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={cheatTitle}
|
|
onChange={(e) => setCheatTitle(e.target.value)}
|
|
placeholder="Git Essential Commands"
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">Category</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={cheatCategory}
|
|
onChange={(e) => setCheatCategory(e.target.value)}
|
|
placeholder="Git / Next.js / Docker"
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">Description</label>
|
|
<textarea
|
|
rows={2}
|
|
required
|
|
value={cheatDesc}
|
|
onChange={(e) => setCheatDesc(e.target.value)}
|
|
placeholder="Daily Git commands for branch management..."
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Command Items */}
|
|
<div className="space-y-3 pt-3 border-t border-slate-800">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-bold text-white">Commands ({cheatItems.length})</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => setCheatItems([...cheatItems, { command: '', description: '' }])}
|
|
className="px-3 py-1 bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold rounded-lg flex items-center gap-1"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Add Command</span>
|
|
</button>
|
|
</div>
|
|
|
|
{cheatItems.map((item, idx) => (
|
|
<div key={idx} className="p-3 bg-slate-950 rounded-xl border border-slate-800 flex items-center gap-3">
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="git status"
|
|
value={item.command}
|
|
onChange={(e) => {
|
|
const updated = [...cheatItems];
|
|
updated[idx].command = e.target.value;
|
|
setCheatItems(updated);
|
|
}}
|
|
className="bg-slate-900 border border-slate-800 rounded-lg px-3 py-1.5 text-xs font-mono text-white focus:outline-none flex-1"
|
|
/>
|
|
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Check tree status"
|
|
value={item.description}
|
|
onChange={(e) => {
|
|
const updated = [...cheatItems];
|
|
updated[idx].description = e.target.value;
|
|
setCheatItems(updated);
|
|
}}
|
|
className="bg-slate-900 border border-slate-800 rounded-lg px-3 py-1.5 text-xs text-white focus:outline-none flex-1"
|
|
/>
|
|
|
|
{cheatItems.length > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setCheatItems(cheatItems.filter((_, i) => i !== idx))}
|
|
className="p-1 text-slate-500 hover:text-red-400"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 pt-4 border-t border-slate-800">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsCheatFormOpen(false)}
|
|
className="px-4 py-2 rounded-xl bg-slate-800 text-xs font-bold text-slate-300"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={savingCheat}
|
|
className="px-5 py-2 bg-red-600 hover:bg-red-500 text-white text-xs font-bold rounded-xl flex items-center gap-2"
|
|
>
|
|
{savingCheat ? 'Saving...' : 'Save Cheatsheet'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* CREATE / EDIT LESSON FORM MODAL */}
|
|
{isFormOpen && (
|
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 overflow-y-auto">
|
|
<div className="w-full max-w-3xl bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-2xl my-8">
|
|
|
|
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
|
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
|
<FileCode className="w-5 h-5 text-red-500" />
|
|
<span>{editingLessonId ? 'Edit Tutorial Resources' : 'Create New YouTube Tutorial'}</span>
|
|
</h2>
|
|
|
|
<button onClick={() => setIsFormOpen(false)} className="p-1.5 rounded-lg bg-slate-800 text-slate-400 hover:text-white">
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSaveLesson} className="space-y-5">
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">{t('videoTitle')}</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formTitle}
|
|
onChange={(e) => setFormTitle(e.target.value)}
|
|
placeholder="Next.js 16 & Server Actions Masterclass"
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none focus:border-red-500"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">{t('youtubeUrl')}</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formYoutubeUrl}
|
|
onChange={(e) => setFormYoutubeUrl(e.target.value)}
|
|
placeholder="https://www.youtube.com/watch?v=..."
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none focus:border-red-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">{t('category')}</label>
|
|
<select
|
|
value={formCategory}
|
|
onChange={(e) => setFormCategory(e.target.value)}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none focus:border-red-500"
|
|
>
|
|
{categoriesList.map((cat) => (
|
|
<option key={cat.id} value={cat.name}>
|
|
{cat.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">Video Duration</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formDuration}
|
|
onChange={(e) => setFormDuration(e.target.value)}
|
|
placeholder="18:45"
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none focus:border-red-500 font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-slate-300 mb-1">Lesson Overview & Summary</label>
|
|
<textarea
|
|
rows={2}
|
|
value={formSummary}
|
|
onChange={(e) => setFormSummary(e.target.value)}
|
|
placeholder="Short overview of topics covered in this video..."
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white focus:outline-none focus:border-red-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Timestamps / Chapters Input */}
|
|
<div className="space-y-1.5 pt-3 border-t border-slate-800">
|
|
<label className="block text-xs font-bold text-slate-300 flex items-center justify-between">
|
|
<span className="flex items-center gap-1.5">
|
|
<Clock className="w-4 h-4 text-red-400" />
|
|
<span>Video Chapters & Timestamps (Optional)</span>
|
|
</span>
|
|
<span className="text-[10px] text-slate-500 font-mono">Format: 00:00 - Introduction</span>
|
|
</label>
|
|
<textarea
|
|
rows={3}
|
|
value={formChaptersText}
|
|
onChange={(e) => setFormChaptersText(e.target.value)}
|
|
placeholder={`00:00 - Introduction\n02:30 - Next.js Setup\n08:45 - Server Actions`}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs font-mono text-slate-200 focus:outline-none focus:border-red-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Lesson Notes Input */}
|
|
<div className="space-y-1.5 pt-3 border-t border-slate-800">
|
|
<label className="block text-xs font-bold text-slate-300 flex items-center justify-between">
|
|
<span className="flex items-center gap-1.5">
|
|
<FileText className="w-4 h-4 text-red-400" />
|
|
<span>Lesson Notes & Bullet Takeaways (Optional)</span>
|
|
</span>
|
|
<span className="text-[10px] text-slate-500 font-mono">Optional / Leave empty if none</span>
|
|
</label>
|
|
<textarea
|
|
rows={3}
|
|
value={formNotesText}
|
|
onChange={(e) => setFormNotesText(e.target.value)}
|
|
placeholder={`📌 Optional: Type key notes line by line, or leave empty...`}
|
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-slate-200 focus:outline-none focus:border-red-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Resource Downloads & Links */}
|
|
<div className="space-y-3 pt-3 border-t border-slate-800">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-bold text-white flex items-center gap-1.5">
|
|
<LinkIcon className="w-4 h-4 text-red-400" />
|
|
<span>Downloadable Starter & Project Links ({formDownloads.length})</span>
|
|
</h3>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleAddDownload}
|
|
className="px-3 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold flex items-center gap-1"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Add Link</span>
|
|
</button>
|
|
</div>
|
|
|
|
{formDownloads.map((dl, idx) => (
|
|
<div key={idx} className="p-3 bg-slate-950 rounded-xl border border-slate-800 flex items-center justify-between gap-3 text-xs">
|
|
<input
|
|
type="text"
|
|
value={dl.title}
|
|
onChange={(e) => {
|
|
const updated = [...formDownloads];
|
|
updated[idx].title = e.target.value;
|
|
setFormDownloads(updated);
|
|
}}
|
|
placeholder="Completed Code (.zip)"
|
|
className="bg-slate-900 border border-slate-800 rounded-lg px-3 py-1.5 text-xs text-white focus:outline-none flex-1"
|
|
/>
|
|
|
|
<select
|
|
value={dl.type}
|
|
onChange={(e) => {
|
|
const updated = [...formDownloads];
|
|
updated[idx].type = e.target.value;
|
|
setFormDownloads(updated);
|
|
}}
|
|
className="bg-slate-900 border border-slate-800 rounded-lg px-2 py-1.5 text-xs text-white focus:outline-none"
|
|
>
|
|
<option value="zip">ZIP File</option>
|
|
<option value="github">GitHub Repo</option>
|
|
<option value="pdf">PDF File</option>
|
|
<option value="link">External Link</option>
|
|
</select>
|
|
|
|
<input
|
|
type="text"
|
|
value={dl.url}
|
|
onChange={(e) => {
|
|
const updated = [...formDownloads];
|
|
updated[idx].url = e.target.value;
|
|
setFormDownloads(updated);
|
|
}}
|
|
placeholder="https://..."
|
|
className="bg-slate-900 border border-slate-800 rounded-lg px-3 py-1.5 text-xs text-white focus:outline-none flex-1"
|
|
/>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveDownload(idx)}
|
|
className="p-1 text-slate-500 hover:text-red-400"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Multi-file Code Snippets */}
|
|
<div className="space-y-4 pt-3 border-t border-slate-800">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-bold text-white flex items-center gap-2">
|
|
<FileCode className="w-4 h-4 text-red-400" />
|
|
<span>Tabbed Code Files ({formSnippets.length})</span>
|
|
</h3>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleAddSnippet}
|
|
className="px-3 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-white text-xs font-semibold flex items-center gap-1"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Add File Tab</span>
|
|
</button>
|
|
</div>
|
|
|
|
{formSnippets.map((snip, idx) => (
|
|
<div key={idx} className="p-4 bg-slate-950 rounded-2xl border border-slate-800 space-y-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<input
|
|
type="text"
|
|
value={snip.fileName}
|
|
onChange={(e) => {
|
|
const updated = [...formSnippets];
|
|
updated[idx].fileName = e.target.value;
|
|
setFormSnippets(updated);
|
|
}}
|
|
placeholder="app/page.tsx"
|
|
className="bg-slate-900 border border-slate-800 rounded-lg px-3 py-1.5 text-xs font-mono text-white focus:outline-none"
|
|
/>
|
|
|
|
{formSnippets.length > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveSnippet(idx)}
|
|
className="p-1 text-slate-500 hover:text-red-400"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<textarea
|
|
rows={5}
|
|
value={snip.code}
|
|
onChange={(e) => {
|
|
const updated = [...formSnippets];
|
|
updated[idx].code = e.target.value;
|
|
setFormSnippets(updated);
|
|
}}
|
|
placeholder="// Paste code snippet from the video here..."
|
|
className="w-full bg-slate-900 border border-slate-800 rounded-xl p-3 text-xs font-mono text-slate-200 focus:outline-none"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Form Action Buttons */}
|
|
<div className="pt-4 flex justify-end gap-3 border-t border-slate-800">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsFormOpen(false)}
|
|
className="px-4 py-2.5 rounded-xl bg-slate-800 text-slate-300 text-xs font-bold"
|
|
>
|
|
Cancel
|
|
</button>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={savingLesson}
|
|
className="px-6 py-2.5 bg-red-600 hover:bg-red-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-red-600/30 flex items-center gap-2"
|
|
>
|
|
{savingLesson ? (
|
|
<span>Saving...</span>
|
|
) : (
|
|
<>
|
|
<Save className="w-4 h-4" />
|
|
<span>{editingLessonId ? 'Save Changes' : 'Publish Tutorial'}</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}
|