first commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 flex flex-col">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getCheatsheets } from '@/lib/actions/cheatsheetActions';
|
||||
import { BookOpen, Copy, Check, Loader2 } from 'lucide-react';
|
||||
|
||||
export default function CheatsheetsPage() {
|
||||
const [copiedItem, setCopiedItem] = useState<string | null>(null);
|
||||
const [cheatsheets, setCheatsheets] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
const res = await getCheatsheets();
|
||||
setCheatsheets(res);
|
||||
setLoading(false);
|
||||
}
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const handleCopyCommand = async (command: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopiedItem(command);
|
||||
setTimeout(() => setCopiedItem(null), 2000);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-10 flex-1 w-full">
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="space-y-3">
|
||||
<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">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span>Quick Reference Cheatsheets</span>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-5xl font-black text-white tracking-tight">
|
||||
Developer Cheatsheets & Essential Commands
|
||||
</h1>
|
||||
<p className="text-sm text-slate-400 max-w-2xl">
|
||||
Copy Git, Next.js, Docker, and Python commands in one click directly from ayris.tech developer reference guides.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cheatsheets List */}
|
||||
{loading ? (
|
||||
<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 cheatsheets...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{cheatsheets.map((sheet) => (
|
||||
<div key={sheet.id} className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-6 shadow-xl hover:border-slate-700 transition-colors">
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-800 pb-4">
|
||||
<div>
|
||||
<span className="text-[11px] font-bold text-red-400 bg-red-500/10 px-2.5 py-0.5 rounded-full border border-red-500/20">
|
||||
{sheet.category}
|
||||
</span>
|
||||
<h2 className="text-xl font-bold text-white mt-2">{sheet.title}</h2>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500 font-mono">
|
||||
Updated: {new Date(sheet.lastUpdated).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-400">{sheet.description}</p>
|
||||
|
||||
{/* Items */}
|
||||
<div className="space-y-3">
|
||||
{sheet.items?.map((item: any, idx: number) => (
|
||||
<div key={idx} className="p-3.5 rounded-2xl bg-slate-950 border border-slate-800 space-y-2 group">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<code className="text-xs font-mono font-bold text-red-300 bg-slate-900 px-2.5 py-1 rounded-lg border border-slate-800 break-all">
|
||||
{item.command}
|
||||
</code>
|
||||
|
||||
<button
|
||||
onClick={() => handleCopyCommand(item.command)}
|
||||
className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition-colors flex-shrink-0"
|
||||
title="Copy Command"
|
||||
>
|
||||
{copiedItem === item.command ? (
|
||||
<Check className="w-3.5 h-3.5 text-emerald-400" />
|
||||
) : (
|
||||
<Copy className="w-3.5 h-3.5 text-slate-400 group-hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-slate-400 pl-1">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { submitContactMessage } from '@/lib/actions/contactActions';
|
||||
import { Mail, Send, CheckCircle2, Sparkles } from 'lucide-react';
|
||||
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
||||
|
||||
export default function ContactPage() {
|
||||
const t = useTranslations('contact');
|
||||
const [form, setForm] = useState({ name: '', email: '', subject: '', message: '' });
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setErrorMsg(null);
|
||||
|
||||
const res = await submitContactMessage(form);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (res.success) {
|
||||
setSubmitted(true);
|
||||
setForm({ name: '', email: '', subject: '', message: '' });
|
||||
} else {
|
||||
setErrorMsg(res.error || 'Failed to submit message');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-12 flex-1 w-full">
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-3 max-w-2xl mx-auto">
|
||||
<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">
|
||||
<Mail className="w-4 h-4" />
|
||||
<span>Get in Touch with Creator</span>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-5xl font-black text-white tracking-tight">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-400">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 items-start max-w-5xl mx-auto">
|
||||
|
||||
{/* Contact Info Card */}
|
||||
<div className="lg:col-span-5 bg-slate-900/80 border border-slate-800 p-8 rounded-3xl space-y-6 shadow-xl">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-red-500" />
|
||||
<span>Why Reach Out?</span>
|
||||
</h2>
|
||||
|
||||
<ul className="space-y-4 text-xs text-slate-300">
|
||||
<li className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-red-500/10 text-red-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
✓
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-white block">Future Tutorial Requests</span>
|
||||
<span>Suggest coding topics, frameworks, or projects you want covered.</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-red-500/10 text-red-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
✓
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-white block">Sponsorship & Inquiries</span>
|
||||
<span>Discuss channel sponsorships or platform collaborations.</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-red-500/10 text-red-400 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
✓
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-white block">Code Issues & Feedback</span>
|
||||
<span>Provide feedback on video code snippets or starter templates.</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="pt-6 border-t border-slate-800 space-y-3 text-xs">
|
||||
<div className="flex items-center gap-3 text-slate-300">
|
||||
<Mail className="w-4 h-4 text-red-400" />
|
||||
<span>contact@youtube-devhub.com</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-slate-300">
|
||||
<Youtube className="w-4 h-4 text-red-500" />
|
||||
<span>YouTube: @DevHubChannel</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Area */}
|
||||
<div className="lg:col-span-7 bg-slate-900/60 border border-slate-800 p-8 rounded-3xl shadow-xl">
|
||||
{submitted ? (
|
||||
<div className="p-8 text-center space-y-4 bg-emerald-950/30 border border-emerald-800/40 rounded-2xl">
|
||||
<CheckCircle2 className="w-12 h-12 text-emerald-400 mx-auto animate-bounce" />
|
||||
<h3 className="text-xl font-bold text-white">{t('success')}</h3>
|
||||
<p className="text-xs text-slate-300">Your message has been saved in PostgreSQL. I will reply soon.</p>
|
||||
<button
|
||||
onClick={() => setSubmitted(false)}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-xs font-bold text-white rounded-xl transition-colors"
|
||||
>
|
||||
Send Another Message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{errorMsg && (
|
||||
<div className="p-3 rounded-xl bg-red-950/50 border border-red-800 text-xs text-red-400">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">{t('name')}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-red-500/50"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">{t('email')}</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-red-500/50"
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">{t('subject')}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={form.subject}
|
||||
onChange={(e) => setForm({ ...form, subject: e.target.value })}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-red-500/50"
|
||||
placeholder="Sponsorship Proposal or New Tutorial Request"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">{t('message')}</label>
|
||||
<textarea
|
||||
rows={5}
|
||||
required
|
||||
value={form.message}
|
||||
onChange={(e) => setForm({ ...form, message: e.target.value })}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-red-500/50"
|
||||
placeholder="Type your message here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-3.5 px-6 bg-red-600 hover:bg-red-500 text-white font-bold text-xs rounded-xl shadow-lg shadow-red-600/30 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span>Saving to PostgreSQL...</span>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-4 h-4" />
|
||||
<span>{t('send')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, setRequestLocale } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import "../globals.css";
|
||||
|
||||
const jakartaSans = Plus_Jakarta_Sans({
|
||||
variable: "--font-sans",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700", "800"],
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DevHub YouTube | Educational Code & Resource Portal",
|
||||
description: "Official resource portal featuring tabbed code snippets, lesson notes, starter packs, and project downloads for my YouTube tutorials.",
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}>) {
|
||||
const { locale } = await params;
|
||||
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
setRequestLocale(locale);
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={`${jakartaSans.variable} ${jetbrainsMono.variable} dark h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col bg-slate-950 text-slate-100 font-sans selection:bg-red-500 selection:text-white tracking-normal leading-relaxed" suppressHydrationWarning>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Navbar locale={locale} />
|
||||
<main className="flex-1 flex flex-col">
|
||||
{children}
|
||||
</main>
|
||||
<Footer locale={locale} />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, use } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getLessonBySlug, getLessons } from '@/lib/actions/lessonActions';
|
||||
import { VideoPlayer } from '@/components/VideoPlayer';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { ResourceCard } from '@/components/ResourceCard';
|
||||
import {
|
||||
FileText,
|
||||
Code,
|
||||
Download,
|
||||
Clock,
|
||||
FileArchive,
|
||||
FileSpreadsheet,
|
||||
ArrowLeft,
|
||||
Share2,
|
||||
Check,
|
||||
Eye,
|
||||
Calendar,
|
||||
Sparkles,
|
||||
ExternalLink,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
||||
import { GithubIcon as Github } from '@/components/icons/BrandIcons';
|
||||
|
||||
export default function LessonDetailPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ locale: string; slug: string }>;
|
||||
}) {
|
||||
const { locale, slug } = use(params);
|
||||
const [activeTab, setActiveTab] = useState<'code' | 'notes' | 'downloads' | 'chapters'>('code');
|
||||
const [copiedLink, setCopiedLink] = useState(false);
|
||||
|
||||
const [lesson, setLesson] = useState<any>(null);
|
||||
const [relatedLessons, setRelatedLessons] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
const data = await getLessonBySlug(slug);
|
||||
if (data) {
|
||||
setLesson(data);
|
||||
|
||||
// Auto-select first non-empty tab
|
||||
const hasCode = data.codeSnippets && data.codeSnippets.length > 0;
|
||||
const hasNotes = data.notesMarkdown && data.notesMarkdown.length > 0;
|
||||
const hasDownloads = data.downloads && data.downloads.length > 0;
|
||||
const hasChapters = data.chapters && data.chapters.length > 0;
|
||||
|
||||
if (hasCode) setActiveTab('code');
|
||||
else if (hasNotes) setActiveTab('notes');
|
||||
else if (hasDownloads) setActiveTab('downloads');
|
||||
else if (hasChapters) setActiveTab('chapters');
|
||||
|
||||
const all = await getLessons();
|
||||
setRelatedLessons(all.filter((v: any) => v.id !== data.id).slice(0, 3));
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
loadData();
|
||||
}, [slug]);
|
||||
|
||||
if (!loading && !lesson) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (loading || !lesson) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-24 text-center text-slate-400 flex flex-col items-center justify-center gap-3">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-red-500" />
|
||||
<p className="text-sm font-semibold">Loading tutorial...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasCode = lesson.codeSnippets && lesson.codeSnippets.length > 0;
|
||||
const hasNotes = lesson.notesMarkdown && lesson.notesMarkdown.length > 0;
|
||||
const hasDownloads = lesson.downloads && lesson.downloads.length > 0;
|
||||
const hasChapters = lesson.chapters && lesson.chapters.length > 0;
|
||||
const hasAnyResources = hasCode || hasNotes || hasDownloads || hasChapters;
|
||||
|
||||
const handleShare = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
setCopiedLink(true);
|
||||
setTimeout(() => setCopiedLink(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Share error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-10 flex-1 w-full">
|
||||
|
||||
{/* Breadcrumb & Navigation Back */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<Link href={`/${locale}`} className="hover:text-white transition-colors">Home</Link>
|
||||
<span>/</span>
|
||||
<Link href={`/${locale}/lessons`} className="hover:text-white transition-colors">Lessons</Link>
|
||||
<span>/</span>
|
||||
<span className="text-slate-200 font-semibold truncate max-w-xs">{lesson.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-300 text-xs font-semibold border border-slate-800 transition-colors"
|
||||
>
|
||||
{copiedLink ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span className="text-emerald-400">Link Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share2 className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>Share Page</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<Link
|
||||
href={`/${locale}/lessons`}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-300 text-xs font-semibold border border-slate-800 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
<span>Back to Lessons</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Header Info */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="px-3 py-1 rounded-full bg-red-500/10 text-red-400 text-xs font-bold border border-red-500/20">
|
||||
{lesson.category}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-xs text-slate-400 bg-slate-900 px-3 py-1 rounded-full border border-slate-800">
|
||||
<Calendar className="w-3.5 h-3.5 text-slate-500" />
|
||||
{new Date(lesson.publishDate).toLocaleDateString()}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-xs text-slate-400 bg-slate-900 px-3 py-1 rounded-full border border-slate-800">
|
||||
<Eye className="w-3.5 h-3.5 text-slate-500" />
|
||||
{lesson.viewsCount.toLocaleString()} Views
|
||||
</span>
|
||||
{hasDownloads && (
|
||||
<span className="flex items-center gap-1 text-xs text-slate-400 bg-slate-900 px-3 py-1 rounded-full border border-slate-800">
|
||||
<Download className="w-3.5 h-3.5 text-slate-500" />
|
||||
{lesson.downloadCount.toLocaleString()} Downloads
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl sm:text-4xl font-black text-white tracking-tight leading-tight">
|
||||
{lesson.title}
|
||||
</h1>
|
||||
|
||||
<p className="text-sm text-slate-300 max-w-3xl leading-relaxed">
|
||||
{lesson.summary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Embedded Video Player */}
|
||||
<VideoPlayer
|
||||
youtubeId={lesson.youtubeId}
|
||||
youtubeUrl={lesson.youtubeUrl}
|
||||
title={lesson.title}
|
||||
chapters={lesson.chapters}
|
||||
/>
|
||||
|
||||
{/* Resource Tabs (Shown ONLY if resources exist) */}
|
||||
{hasAnyResources && (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Tab Header Buttons */}
|
||||
<div className="flex items-center gap-2 border-b border-slate-800 pb-2 overflow-x-auto scrollbar-none">
|
||||
{hasCode && (
|
||||
<button
|
||||
onClick={() => setActiveTab('code')}
|
||||
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
||||
activeTab === 'code'
|
||||
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
||||
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
<Code className="w-4 h-4" />
|
||||
<span>Code Blocks ({lesson.codeSnippets.length} Files)</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasNotes && (
|
||||
<button
|
||||
onClick={() => setActiveTab('notes')}
|
||||
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
||||
activeTab === 'notes'
|
||||
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
||||
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Lesson Notes & Summary</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasDownloads && (
|
||||
<button
|
||||
onClick={() => setActiveTab('downloads')}
|
||||
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
||||
activeTab === 'downloads'
|
||||
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
||||
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Downloads & Links ({lesson.downloads.length})</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasChapters && (
|
||||
<button
|
||||
onClick={() => setActiveTab('chapters')}
|
||||
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
||||
activeTab === 'chapters'
|
||||
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
||||
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Video Chapters ({lesson.chapters.length})</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Contents */}
|
||||
<div>
|
||||
{/* TAB 1: CODE BLOCKS */}
|
||||
{activeTab === 'code' && hasCode && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 rounded-xl bg-red-950/20 border border-red-800/30 text-xs text-red-300 flex items-center justify-between">
|
||||
<span>💡 You can inspect all code snippets by clicking file tabs and copy them directly into your project using "Copy Code".</span>
|
||||
</div>
|
||||
|
||||
<CodeBlock snippets={lesson.codeSnippets} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: LESSON NOTES */}
|
||||
{activeTab === 'notes' && hasNotes && (
|
||||
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-4">
|
||||
<h3 className="font-extrabold text-white text-lg flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-red-400" />
|
||||
<span>Key Lesson Notes & Takeaways</span>
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{lesson.notesMarkdown.map((note: string, idx: number) => (
|
||||
<div key={idx} className="p-4 rounded-xl bg-slate-950 border border-slate-800 text-xs text-slate-200 leading-relaxed">
|
||||
{note}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 3: DOWNLOADS & LINKS */}
|
||||
{activeTab === 'downloads' && hasDownloads && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{lesson.downloads.map((item: any, idx: number) => (
|
||||
<div key={idx} className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-800 flex items-center justify-center text-red-400 flex-shrink-0">
|
||||
{item.type === 'zip' && <FileArchive className="w-5 h-5" />}
|
||||
{item.type === 'github' && <Github className="w-5 h-5 text-white" />}
|
||||
{item.type === 'pdf' && <FileSpreadsheet className="w-5 h-5 text-emerald-400" />}
|
||||
{item.type === 'link' && <ExternalLink className="w-5 h-5 text-sky-400" />}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-white text-xs">{item.title}</h4>
|
||||
{item.size && <p className="text-[11px] text-slate-500 font-mono">Size: {item.size}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-4 py-2 rounded-xl bg-red-600 hover:bg-red-500 text-white text-xs font-bold flex items-center gap-1.5 transition-colors shadow-md shadow-red-600/20"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
<span>Download / Open</span>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 4: CHAPTERS */}
|
||||
{activeTab === 'chapters' && hasChapters && (
|
||||
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-3">
|
||||
<h3 className="font-bold text-white text-sm mb-4">Timestamped Video Index</h3>
|
||||
<div className="space-y-2">
|
||||
{lesson.chapters.map((chap: any) => (
|
||||
<div key={chap.time} className="p-3 rounded-xl bg-slate-950 border border-slate-800 flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-red-400 font-bold bg-slate-900 px-2 py-1 rounded border border-slate-800">
|
||||
{chap.time}
|
||||
</span>
|
||||
<span className="text-white font-medium">{chap.title}</span>
|
||||
</div>
|
||||
<a
|
||||
href={`${lesson.youtubeUrl}&t=${chap.seconds}s`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-red-400 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Youtube className="w-3.5 h-3.5" />
|
||||
<span>Watch</span>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Related YouTube Videos */}
|
||||
<div className="pt-10 border-t border-slate-800 space-y-6">
|
||||
<h3 className="text-xl font-extrabold text-white flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-red-500" />
|
||||
<span>Other Popular Video Tutorials</span>
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{relatedLessons.map((rel) => (
|
||||
<ResourceCard key={rel.id} lesson={rel} locale={locale} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, use } from 'react';
|
||||
import { ResourceCard } from '@/components/ResourceCard';
|
||||
import { getLessons } from '@/lib/actions/lessonActions';
|
||||
import { getCategories } from '@/lib/actions/categoryActions';
|
||||
import { Search, Code2, SlidersHorizontal, Loader2 } from 'lucide-react';
|
||||
|
||||
export default function LessonsPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = use(params);
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedCat, setSelectedCat] = useState<string | null>(null);
|
||||
const [lessons, setLessons] = useState<any[]>([]);
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getCategories().then(setCategories);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
const res = await getLessons({ query: search, category: selectedCat || undefined });
|
||||
setLessons(res);
|
||||
setLoading(false);
|
||||
}
|
||||
loadData();
|
||||
}, [search, selectedCat]);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-8 flex-1 w-full">
|
||||
|
||||
{/* Header */}
|
||||
<div className="space-y-3">
|
||||
<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">
|
||||
<Code2 className="w-4 h-4" />
|
||||
<span>YouTube Content & Code Library</span>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-5xl font-black text-white tracking-tight">
|
||||
All Video Tutorials & Code Resources
|
||||
</h1>
|
||||
<p className="text-sm text-slate-400 max-w-2xl">
|
||||
Browse, filter, and inspect all source code snippets, lesson notes, starter zip packages, and multi-file project structures.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4 bg-slate-900/80 p-4 rounded-2xl border border-slate-800">
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="relative w-full md:w-96">
|
||||
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tutorial title or code..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-10 pr-4 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-red-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Pills */}
|
||||
<div className="flex items-center gap-2 overflow-x-auto w-full md:w-auto py-1 scrollbar-none">
|
||||
<SlidersHorizontal className="w-4 h-4 text-slate-500 mr-1 flex-shrink-0" />
|
||||
<button
|
||||
onClick={() => setSelectedCat(null)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold whitespace-nowrap transition-all ${
|
||||
selectedCat === null
|
||||
? 'bg-red-600 text-white shadow-md shadow-red-600/30'
|
||||
: 'bg-slate-950 text-slate-400 hover:text-white border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id || cat.name}
|
||||
onClick={() => setSelectedCat(selectedCat === cat.name ? null : cat.name)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold whitespace-nowrap transition-all ${
|
||||
selectedCat === cat.name
|
||||
? 'bg-red-600 text-white shadow-md shadow-red-600/30'
|
||||
: 'bg-slate-950 text-slate-400 hover:text-white border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{loading ? (
|
||||
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{lessons.map((item) => (
|
||||
<ResourceCard key={item.id} lesson={item} locale={locale} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-12 text-center bg-slate-900/40 rounded-2xl border border-slate-800 text-slate-400 text-xs">
|
||||
No YouTube tutorials match your search filters.
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ShieldAlert, Lock, Mail, ArrowRight, Loader2 } from 'lucide-react';
|
||||
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('admin@ayris.tech');
|
||||
const [password, setPassword] = useState('admin');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const result = await signIn('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
setError('Invalid email or password');
|
||||
setLoading(false);
|
||||
} else {
|
||||
router.push('/admin');
|
||||
router.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-slate-950 px-4 py-12">
|
||||
<div className="w-full max-w-md bg-slate-900 border border-slate-800 rounded-3xl p-8 shadow-2xl space-y-6">
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2">
|
||||
<div className="w-14 h-14 rounded-2xl bg-slate-950 border border-slate-800 flex items-center justify-center mx-auto overflow-hidden shadow-xl">
|
||||
<img src="/logo.jpeg" alt="ayris.tech Logo" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-black text-white tracking-tight">Creator Admin Login</h1>
|
||||
<p className="text-xs text-slate-400">Sign in to manage YouTube lessons, code snippets, and viewer messages.</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3.5 rounded-xl bg-red-950/50 border border-red-800 text-xs text-red-300 text-center font-medium">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1">Email Address</label>
|
||||
<div className="relative">
|
||||
<Mail className="w-4 h-4 text-slate-500 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-10 pr-4 py-2.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-red-500"
|
||||
placeholder="admin@ayris.tech"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1">Password</label>
|
||||
<div className="relative">
|
||||
<Lock className="w-4 h-4 text-slate-500 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-10 pr-4 py-2.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-red-500"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-3 px-4 bg-red-600 hover:bg-red-500 text-white font-bold text-xs rounded-xl shadow-lg shadow-red-600/30 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Signing in...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Sign In to Dashboard</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="p-3.5 rounded-xl bg-slate-950 border border-slate-800 text-center text-xs space-y-1">
|
||||
<span className="text-slate-500 block font-semibold">Demo Admin Credentials:</span>
|
||||
<div className="font-mono text-slate-300 text-[11px]">
|
||||
Email: <span className="text-red-400 font-bold">admin@ayris.tech</span>
|
||||
</div>
|
||||
<div className="font-mono text-slate-300 text-[11px]">
|
||||
Password: <span className="text-red-400 font-bold">admin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, use } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ResourceCard } from '@/components/ResourceCard';
|
||||
import { getLessons } from '@/lib/actions/lessonActions';
|
||||
import { getCheatsheets } from '@/lib/actions/cheatsheetActions';
|
||||
import { getCategories } from '@/lib/actions/categoryActions';
|
||||
import {
|
||||
Search,
|
||||
Sparkles,
|
||||
Code2,
|
||||
FileCheck2,
|
||||
Layers,
|
||||
Download,
|
||||
Users,
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
ShieldAlert,
|
||||
Zap,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
||||
|
||||
export default function HomePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = use(params);
|
||||
const t = useTranslations('hero');
|
||||
const sec = useTranslations('sections');
|
||||
const feat = useTranslations('features');
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [lessons, setLessons] = useState<any[]>([]);
|
||||
const [cheatsheets, setCheatsheets] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getCategories().then(setCategories);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
const [fetchedLessons, fetchedCheatsheets] = await Promise.all([
|
||||
getLessons({ query: searchQuery, category: selectedCategory || undefined }),
|
||||
getCheatsheets()
|
||||
]);
|
||||
setLessons(fetchedLessons);
|
||||
setCheatsheets(fetchedCheatsheets);
|
||||
setLoading(false);
|
||||
}
|
||||
loadData();
|
||||
}, [searchQuery, selectedCategory]);
|
||||
|
||||
const featuredVideo = lessons.find((v) => v.isFeatured) || lessons[0];
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col space-y-16 pb-20">
|
||||
|
||||
{/* HERO SECTION */}
|
||||
<section className="relative overflow-hidden pt-12 pb-16 bg-gradient-to-b from-slate-900/80 via-slate-950 to-slate-950 border-b border-slate-800/80">
|
||||
|
||||
{/* Glow Effects */}
|
||||
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[300px] bg-red-600/10 blur-[140px] pointer-events-none rounded-full" />
|
||||
<div className="absolute top-1/3 left-1/4 w-[300px] h-[200px] bg-indigo-600/10 blur-[120px] pointer-events-none rounded-full" />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 text-center space-y-8">
|
||||
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-red-500/10 border border-red-500/20 text-red-400 text-xs font-bold shadow-inner">
|
||||
<Youtube className="w-4 h-4 fill-red-400" />
|
||||
<span>{t('badge')}</span>
|
||||
</div>
|
||||
|
||||
{/* Main Heading */}
|
||||
<h1 className="text-4xl sm:text-6xl lg:text-7xl font-extrabold text-white tracking-tight leading-[1.15] max-w-4xl mx-auto">
|
||||
{t('title')}
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-base sm:text-lg text-slate-400 max-w-2xl mx-auto font-normal leading-relaxed">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="max-w-2xl mx-auto relative group">
|
||||
<div className="absolute -inset-1 rounded-2xl bg-gradient-to-r from-red-600 via-rose-500 to-indigo-600 opacity-30 group-hover:opacity-60 blur-lg transition duration-500" />
|
||||
<div className="relative flex items-center bg-slate-900 border border-slate-700/80 rounded-2xl p-2 shadow-2xl">
|
||||
<Search className="w-5 h-5 text-slate-400 ml-3 flex-shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-transparent px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="px-3 py-1 text-xs text-slate-400 hover:text-white bg-slate-800 rounded-lg mr-1"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Chips */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 pt-2">
|
||||
<button
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-all ${
|
||||
selectedCategory === null
|
||||
? 'bg-red-600 text-white shadow-lg shadow-red-600/30 ring-2 ring-red-400/50'
|
||||
: 'bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
All Topics
|
||||
</button>
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id || cat.name}
|
||||
onClick={() => setSelectedCategory(selectedCategory === cat.name ? null : cat.name)}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold transition-all ${
|
||||
selectedCategory === cat.name
|
||||
? 'bg-red-600 text-white shadow-lg shadow-red-600/30 ring-2 ring-red-400/50'
|
||||
: 'bg-slate-900 hover:bg-slate-800 text-slate-300 border border-slate-800'
|
||||
}`}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Statistics Bar */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-6 max-w-4xl mx-auto border-t border-slate-800/80">
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60 text-center">
|
||||
<div className="text-2xl font-black text-white flex items-center justify-center gap-1.5">
|
||||
<Youtube className="w-5 h-5 text-red-500" />
|
||||
<span>120+</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1 font-medium">{t('statVideos')}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60 text-center">
|
||||
<div className="text-2xl font-black text-white flex items-center justify-center gap-1.5">
|
||||
<Download className="w-5 h-5 text-emerald-400" />
|
||||
<span>100K+</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1 font-medium">{t('statDownloads')}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60 text-center">
|
||||
<div className="text-2xl font-black text-white flex items-center justify-center gap-1.5">
|
||||
<Users className="w-5 h-5 text-sky-400" />
|
||||
<span>50K+</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1 font-medium">{t('statSubscribers')}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60 text-center">
|
||||
<div className="text-2xl font-black text-white flex items-center justify-center gap-1.5">
|
||||
<Zap className="w-5 h-5 text-amber-400" />
|
||||
<span>99.8%</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1 font-medium">{t('statSatisfaction')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FEATURED LESSON BANNER */}
|
||||
{featuredVideo && !selectedCategory && !searchQuery && (
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-extrabold text-white flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-red-500" />
|
||||
<span>{sec('featured')}</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative rounded-3xl bg-slate-900/80 border border-slate-800 overflow-hidden shadow-2xl p-6 sm:p-8 grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
|
||||
|
||||
{/* Thumbnail preview */}
|
||||
<div className="lg:col-span-7 relative aspect-video rounded-2xl overflow-hidden bg-slate-950 group">
|
||||
<img
|
||||
src={featuredVideo.thumbnailUrl}
|
||||
alt={featuredVideo.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-slate-950/40 flex items-center justify-center">
|
||||
<Link
|
||||
href={`/${locale}/dersler/${featuredVideo.slug}`}
|
||||
className="w-16 h-16 rounded-full bg-red-600 hover:bg-red-500 text-white flex items-center justify-center shadow-xl shadow-red-600/40 transition-all hover:scale-110"
|
||||
>
|
||||
<Youtube className="w-8 h-8 fill-white ml-0.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info & CTA */}
|
||||
<div className="lg:col-span-5 space-y-4">
|
||||
<div className="inline-block px-3 py-1 rounded-full bg-red-500/10 text-red-400 text-xs font-bold border border-red-500/20">
|
||||
{featuredVideo.category}
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-black text-white leading-tight">
|
||||
{featuredVideo.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
{featuredVideo.summary}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="text-xs font-semibold text-slate-400">Files Included:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{featuredVideo.codeSnippets.map((snip: any) => (
|
||||
<span key={snip.fileName} className="px-2.5 py-1 rounded-lg bg-slate-800 text-slate-300 text-[11px] font-mono border border-slate-700">
|
||||
{snip.fileName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href={`/${locale}/dersler/${featuredVideo.slug}`}
|
||||
className="px-5 py-3 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"
|
||||
>
|
||||
<span>Explore Code & Notes</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href={featuredVideo.youtubeUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-4 py-3 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold flex items-center gap-2 transition-colors border border-slate-700"
|
||||
>
|
||||
<Youtube className="w-4 h-4 text-red-500" />
|
||||
<span>Watch on YouTube</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ALL LESSONS GRID */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full space-y-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-extrabold text-white flex items-center gap-2">
|
||||
<Code2 className="w-6 h-6 text-red-500" />
|
||||
<span>{sec('allLessons')}</span>
|
||||
</h2>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{lessons.length} tutorials found in database {selectedCategory && `(${selectedCategory})`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/${locale}/dersler`}
|
||||
className="text-xs font-bold text-red-400 hover:text-red-300 flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<span>View All Lessons</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{lessons.map((lesson) => (
|
||||
<ResourceCard key={lesson.id} lesson={lesson} locale={locale} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-12 text-center bg-slate-900/40 rounded-2xl border border-slate-800 space-y-3">
|
||||
<ShieldAlert className="w-10 h-10 text-slate-500 mx-auto" />
|
||||
<h3 className="text-base font-bold text-white">No Tutorials Found</h3>
|
||||
<p className="text-xs text-slate-400 max-w-sm mx-auto">
|
||||
Try adjusting your search terms or clearing selected filters.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => { setSearchQuery(''); setSelectedCategory(null); }}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-xs font-bold text-white rounded-lg transition-colors"
|
||||
>
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* AYRIS.TECH YOUTUBE CHANNEL FEATURES */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||
<div className="bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900 rounded-3xl border border-slate-800 p-8 sm:p-12 space-y-8 shadow-2xl relative overflow-hidden">
|
||||
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-red-600/5 blur-[120px] pointer-events-none rounded-full" />
|
||||
|
||||
<div className="text-center space-y-3 max-w-2xl mx-auto">
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-red-400 bg-red-500/10 px-3 py-1 rounded-full border border-red-500/20">
|
||||
Creator Innovation
|
||||
</span>
|
||||
<h2 className="text-3xl sm:text-4xl font-black text-white tracking-tight">
|
||||
{sec('whyUsTitle')}
|
||||
</h2>
|
||||
<p className="text-xs sm:text-sm text-slate-400">
|
||||
{sec('whyUsSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 pt-4">
|
||||
|
||||
{/* Feature 1 */}
|
||||
<div className="p-6 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3 hover:border-red-500/40 transition-colors">
|
||||
<div className="w-10 h-10 rounded-xl bg-red-500/10 text-red-400 flex items-center justify-center">
|
||||
<Layers className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">{feat('codeTabsTitle')}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{feat('codeTabsDesc')}</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<div className="p-6 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3 hover:border-red-500/40 transition-colors">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-400 flex items-center justify-center">
|
||||
<Youtube className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">{feat('chaptersTitle')}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{feat('chaptersDesc')}</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<div className="p-6 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3 hover:border-red-500/40 transition-colors">
|
||||
<div className="w-10 h-10 rounded-xl bg-sky-500/10 text-sky-400 flex items-center justify-center">
|
||||
<Download className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">{feat('zipDownloadTitle')}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{feat('zipDownloadDesc')}</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 4 */}
|
||||
<div className="p-6 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-3 hover:border-red-500/40 transition-colors">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/10 text-amber-400 flex items-center justify-center">
|
||||
<FileCheck2 className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-base">{feat('noDocsTitle')}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{feat('noDocsDesc')}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FEATURED CHEATSHEETS PREVIEW */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-extrabold text-white flex items-center gap-2">
|
||||
<FileCheck2 className="w-6 h-6 text-red-500" />
|
||||
<span>{sec('cheatsheetsTitle')}</span>
|
||||
</h2>
|
||||
|
||||
<Link
|
||||
href={`/${locale}/rehberler`}
|
||||
className="text-xs font-bold text-red-400 hover:text-red-300 flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<span>Explore All Cheatsheets</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{cheatsheets.map((cs) => (
|
||||
<div key={cs.id} className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-4 hover:bg-slate-900 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] font-semibold text-red-400 bg-red-500/10 px-2.5 py-0.5 rounded-full border border-red-500/20">
|
||||
{cs.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="font-bold text-white text-base">{cs.title}</h3>
|
||||
<p className="text-xs text-slate-400 line-clamp-2">{cs.description}</p>
|
||||
|
||||
<div className="space-y-2 pt-2 border-t border-slate-800">
|
||||
{cs.items.slice(0, 3).map((item: any, idx: number) => (
|
||||
<div key={idx} className="bg-slate-950 p-2 rounded-lg text-xs font-mono text-slate-300 flex items-center justify-between">
|
||||
<span className="text-red-300 font-bold truncate max-w-[200px]">{item.command}</span>
|
||||
<span className="text-[10px] text-slate-500 truncate">{item.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/${locale}/rehberler`}
|
||||
className="w-full py-2 flex items-center justify-center gap-1.5 text-xs font-bold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 rounded-xl transition-colors"
|
||||
>
|
||||
<span>Open Cheatsheet</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user