From bd543c3bda61d3c7e94630a7ac7c16bb8b4601b1 Mon Sep 17 00:00:00 2001 From: AyrisAI Date: Thu, 23 Jul 2026 16:55:44 +0300 Subject: [PATCH] first commit --- .dockerignore | 9 + .gitignore | 41 + AGENTS.md | 42 + CLAUDE.md | 1 + Dockerfile | 34 + README.md | 36 + app/[locale]/admin/layout.tsx | 7 + app/[locale]/admin/page.tsx | 1474 ++++ app/[locale]/cheatsheets/page.tsx | 108 + app/[locale]/contact/page.tsx | 194 + app/[locale]/layout.tsx | 64 + app/[locale]/lessons/[slug]/page.tsx | 351 + app/[locale]/lessons/page.tsx | 113 + app/[locale]/login/page.tsx | 118 + app/[locale]/page.tsx | 418 + app/api/auth/[...nextauth]/route.ts | 3 + app/favicon.ico | Bin 0 -> 15406 bytes app/globals.css | 130 + components.json | 25 + components/CodeBlock.tsx | 128 + components/Footer.tsx | 105 + components/Navbar.tsx | 74 + components/ResourceCard.tsx | 108 + components/VideoPlayer.tsx | 78 + components/icons/BrandIcons.tsx | 17 + components/icons/YoutubeIcon.tsx | 14 + components/ui/button.tsx | 58 + components/ui/card.tsx | 103 + components/ui/input.tsx | 20 + eslint.config.mjs | 18 + i18n/request.ts | 15 + i18n/routing.ts | 10 + lib/actions/categoryActions.ts | 128 + lib/actions/cheatsheetActions.ts | 113 + lib/actions/contactActions.ts | 40 + lib/actions/lessonActions.ts | 288 + lib/actions/settingsActions.ts | 47 + lib/actions/userActions.ts | 56 + lib/auth.ts | 47 + lib/cloudinary.ts | 20 + lib/data.ts | 448 + lib/db.ts | 30 + lib/utils.ts | 6 + messages/de.json | 75 + messages/en.json | 75 + messages/tr.json | 75 + next.config.ts | 16 + package-lock.json | 11401 +++++++++++++++++++++++++ package.json | 43 + postcss.config.mjs | 7 + prisma.config.ts | 7 + prisma/schema.prisma | 162 + prisma/seed.ts | 254 + proxy.ts | 20 + public/file.svg | 1 + public/globe.svg | 1 + public/logo.jpeg | Bin 0 -> 418833 bytes public/next.svg | 1 + public/vercel.svg | 1 + public/window.svg | 1 + tsconfig.json | 34 + 61 files changed, 17313 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/[locale]/admin/layout.tsx create mode 100644 app/[locale]/admin/page.tsx create mode 100644 app/[locale]/cheatsheets/page.tsx create mode 100644 app/[locale]/contact/page.tsx create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/lessons/[slug]/page.tsx create mode 100644 app/[locale]/lessons/page.tsx create mode 100644 app/[locale]/login/page.tsx create mode 100644 app/[locale]/page.tsx create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 components.json create mode 100644 components/CodeBlock.tsx create mode 100644 components/Footer.tsx create mode 100644 components/Navbar.tsx create mode 100644 components/ResourceCard.tsx create mode 100644 components/VideoPlayer.tsx create mode 100644 components/icons/BrandIcons.tsx create mode 100644 components/icons/YoutubeIcon.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/input.tsx create mode 100644 eslint.config.mjs create mode 100644 i18n/request.ts create mode 100644 i18n/routing.ts create mode 100644 lib/actions/categoryActions.ts create mode 100644 lib/actions/cheatsheetActions.ts create mode 100644 lib/actions/contactActions.ts create mode 100644 lib/actions/lessonActions.ts create mode 100644 lib/actions/settingsActions.ts create mode 100644 lib/actions/userActions.ts create mode 100644 lib/auth.ts create mode 100644 lib/cloudinary.ts create mode 100644 lib/data.ts create mode 100644 lib/db.ts create mode 100644 lib/utils.ts create mode 100644 messages/de.json create mode 100644 messages/en.json create mode 100644 messages/tr.json create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma.config.ts create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100644 proxy.ts create mode 100644 public/file.svg create mode 100644 public/globe.svg create mode 100644 public/logo.jpeg create mode 100644 public/next.svg create mode 100644 public/vercel.svg create mode 100644 public/window.svg create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..430f1b2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.next +node_modules +.env +.env.*.local +.git +.vscode +docs +README.md +AGENTS.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f7f0cbb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# AGENTS.md + +## Stack +- Framework: Next.js 16, App Router, TypeScript strict +- Styling: Tailwind CSS v4 +- UI: shadcn/ui (new-york style, OKLCH) +- Animation: Framer Motion +- Icons: Lucide React +- i18n: next-intl +- ORM: Prisma + PostgreSQL +- Auth: NextAuth.js v5 +- Media: Cloudinary +- Deploy: Coolify (Docker, standalone output) + +## Sabit Tercihler +- Mock data: USE_MOCK=true (demo aşaması) +- proxy.ts kullan — middleware.ts deprecated (Next.js 15.3+) +- İletişim formu sadece /iletisim sayfasında — ana sayfada olmaz +- Footer'da "Created by ayris.tech" linki zorunlu +- Dockerfile'da dummy DATABASE_URL (prisma generate için) + +## Altyapı +- Gitea: https://git.ayris.tech (kullanıcı: ayrisdev) +- Coolify: https://client2.ayris.tech +- Cloudflare zone: ayris.tech +- Server IP: 188.245.175.169 + +## docs/ Klasörü +- docs/prd.md → ana içerik kaynağı +- docs/*.html → varsa mevcut site içeriği +- docs/*.md → ek belgeler + +## Aktif Skill'ler +- nextjs-seo → sitemap, metadata, robots.txt +- next-best-practices → kod kalitesi +- nextjs-app-router-patterns → Server Actions, Suspense +- demo-site → komple site üretimi +- design-demo → görsel kalite +- coolify-deploy → deploy pipeline + +## Proje Özel Notlar + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..20dd2ca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM node:22-alpine AS base + +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci --legacy-peer-deps + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +# Prisma generate için dummy URL — build sırasında gerçek DB gerekmez +ARG DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy +ENV DATABASE_URL=$DATABASE_URL +RUN npx prisma generate +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +RUN mkdir .next && chown nextjs:nodejs .next +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/[locale]/admin/layout.tsx b/app/[locale]/admin/layout.tsx new file mode 100644 index 0000000..c1a842a --- /dev/null +++ b/app/[locale]/admin/layout.tsx @@ -0,0 +1,7 @@ +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/app/[locale]/admin/page.tsx b/app/[locale]/admin/page.tsx new file mode 100644 index 0000000..2627aba --- /dev/null +++ b/app/[locale]/admin/page.tsx @@ -0,0 +1,1474 @@ +'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([]); + const [lessonSearch, setLessonSearch] = useState(''); + const [loadingLessons, setLoadingLessons] = useState(false); + + // Dynamic Categories State + const [categoriesList, setCategoriesList] = useState([]); + const [newCatName, setNewCatName] = useState(''); + const [editingCatId, setEditingCatId] = useState(null); + const [editingCatName, setEditingCatName] = useState(''); + + // Cheatsheets State + const [cheatsheetsList, setCheatsheetsList] = useState([]); + const [isCheatFormOpen, setIsCheatFormOpen] = useState(false); + const [editingCheatId, setEditingCheatId] = useState(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(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([]); + const [newAdminName, setNewAdminName] = useState(''); + const [newAdminEmail, setNewAdminEmail] = useState(''); + const [newAdminPass, setNewAdminPass] = useState(''); + const [savingUser, setSavingUser] = useState(false); + + // Settings State + const [settings, setSettingsData] = useState>({ + channelName: '', + youtubeUrl: '', + githubUrl: '', + contactEmail: '', + defaultCategory: 'Next.js', + }); + const [savingSettings, setSavingSettings] = useState(false); + + // Messages State + const [messages, setMessages] = useState([]); + + // 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 ( +
+ + {/* Left Vertical Sidebar Navigation */} + + + {/* Main Content View */} +
+ + {/* Alert Banner */} + {alert && ( +
+
+ + {alert.msg} +
+ +
+ )} + + {/* TAB 1: DASHBOARD STATS */} + {activeTab === 'dashboard' && ( +
+ + {/* Stat Cards Grid */} +
+
+
+ Total Tutorials +
+
{stats.totalLessons}
+
+ +
+
+ Total Views + +
+
{stats.totalViews.toLocaleString()}
+
+ +
+
+ Project Downloads + +
+
{stats.totalDownloads.toLocaleString()}
+
+ +
+
+ Viewer Inquiries + +
+
{stats.totalMessages}
+
+
+ + {/* Recent Lessons Table Preview */} +
+
+

Recent YouTube Tutorials

+ +
+ +
+ {lessons.map((lesson) => ( +
+
+ + {lesson.category} + +

{lesson.title}

+
+ +
+ +
+
+ ))} +
+
+ +
+ )} + + {/* TAB 2: LESSONS & EDIT MODAL */} + {activeTab === 'lessons' && ( +
+
+ {/* Search Input */} +
+ + 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" + /> +
+ + +
+ + {/* Lessons Table / List */} + {loadingLessons ? ( +
+ + Loading tutorials... +
+ ) : lessons.length > 0 ? ( +
+ {lessons.map((item) => ( +
+
+
+ + {item.category} + + + {item.codeSnippets?.length || 0} Files | {item.chapters?.length || 0} Timestamps + +
+

{item.title}

+

{item.summary}

+
+ +
+ + + +
+
+ ))} +
+ ) : ( +
+ No tutorials found. +
+ )} +
+ )} + + {/* TAB 3: CHEATSHEETS CRUD */} + {activeTab === 'cheatsheets' && ( +
+
+

Cheatsheets & Commands ({cheatsheetsList.length})

+ + +
+ +
+ {cheatsheetsList.map((sheet) => ( +
+
+
+ + {sheet.category} + +

{sheet.title}

+

{sheet.description}

+
+ +
+ + + +
+
+ +
+ {sheet.items?.slice(0, 3).map((item: any, idx: number) => ( +
+ {item.command} + {item.description} +
+ ))} +
+
+ ))} +
+
+ )} + + {/* TAB 4: CATEGORIES CRUD */} + {activeTab === 'categories' && ( +
+ + {/* Create Category Form */} +
+

+ + Create New Category +

+ +
+ 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" + /> + + +
+
+ + {/* Categories List */} +
+

Categories ({categoriesList.length})

+ +
+ {categoriesList.map((cat) => ( +
+ {editingCatId === cat.id ? ( +
+ 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" + /> + + +
+ ) : ( + <> +
+ + {cat.name} +
+ +
+ + + +
+ + )} +
+ ))} +
+
+ +
+ )} + + {/* TAB 5: USERS / ADMINS */} + {activeTab === 'users' && ( +
+ {/* Create User Form */} +
+

+ + Add New Admin User +

+ +
+ 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" + /> + + 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" + /> + + 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" + /> +
+ +
+ +
+
+ + {/* Users List Table */} +
+

Admin Users ({usersList.length})

+ +
+ {usersList.map((u) => ( +
+
+ {u.name || 'Admin User'} + {u.email} +
+ + + {u.role} + +
+ ))} +
+
+
+ )} + + {/* TAB 6: SETTINGS */} + {activeTab === 'settings' && ( +
+

+ + Channel & Creator Settings +

+ +
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+
+ +
+ +
+
+ )} + + {/* TAB 7: MESSAGES */} + {activeTab === 'messages' && ( +
+

Viewer Contact Messages ({messages.length})

+ + {messages.length > 0 ? ( +
+ {messages.map((msg) => ( +
+
+ {msg.name} ({msg.email}) + {new Date(msg.createdAt).toLocaleString()} +
+

{msg.subject}

+

+ {msg.message} +

+
+ ))} +
+ ) : ( +
+ No viewer contact messages received yet. +
+ )} +
+ )} + +
+ + {/* CREATE / EDIT CHEATSHEET FORM MODAL */} + {isCheatFormOpen && ( +
+
+
+

+ + {editingCheatId ? 'Edit Cheatsheet' : 'Create New Cheatsheet'} +

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