first commit

This commit is contained in:
AyrisAI
2026-07-23 16:55:44 +03:00
commit bd543c3bda
61 changed files with 17313 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
.next
node_modules
.env
.env.*.local
.git
.vscode
docs
README.md
AGENTS.md
+41
View File
@@ -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
+42
View File
@@ -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
<!-- Buraya proje bazlı notlar ekle -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+34
View File
@@ -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"]
+36
View File
@@ -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.
+7
View File
@@ -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
+108
View File
@@ -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>
);
}
+194
View File
@@ -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>
);
}
+64
View File
@@ -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>
);
}
+351
View File
@@ -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>
);
}
+113
View File
@@ -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>
);
}
+118
View File
@@ -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>
);
}
+418
View File
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/lib/auth"
export const { GET, POST } = handlers
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+130
View File
@@ -0,0 +1,130 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
+128
View File
@@ -0,0 +1,128 @@
'use client';
import { useState } from 'react';
import { CodeSnippet } from '@/lib/data';
import { Check, Copy, Download, FileCode, Terminal } from 'lucide-react';
interface CodeBlockProps {
snippets: CodeSnippet[];
}
export function CodeBlock({ snippets }: CodeBlockProps) {
const [activeTab, setActiveTab] = useState(0);
const [copied, setCopied] = useState(false);
if (!snippets || snippets.length === 0) return null;
const currentSnippet = snippets[activeTab] || snippets[0];
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(currentSnippet.code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Copy error:', err);
}
};
const handleDownloadFile = () => {
const element = document.createElement('a');
const file = new Blob([currentSnippet.code], { type: 'text/plain;charset=utf-8' });
element.href = URL.createObjectURL(file);
element.download = currentSnippet.fileName.split('/').pop() || 'code.txt';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const lines = currentSnippet.code.split('\n');
return (
<div className="w-full rounded-2xl bg-slate-950 border border-slate-800 shadow-2xl overflow-hidden my-6">
{/* Tab Navigation Header */}
<div className="flex flex-wrap items-center justify-between bg-slate-900/90 px-4 py-2.5 border-b border-slate-800 gap-3">
{/* File Tabs */}
<div className="flex items-center gap-1.5 overflow-x-auto py-1 scrollbar-none">
{snippets.map((snip, index) => (
<button
key={snip.fileName}
onClick={() => setActiveTab(index)}
className={`flex items-center gap-2 px-3.5 py-1.5 rounded-lg text-xs font-mono font-medium transition-all ${
activeTab === index
? 'bg-slate-800 text-white shadow-sm border border-slate-700/80'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/40'
}`}
>
<FileCode className={`w-3.5 h-3.5 ${activeTab === index ? 'text-red-400' : 'text-slate-500'}`} />
<span>{snip.fileName}</span>
</button>
))}
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2">
<button
onClick={handleDownloadFile}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold transition-colors border border-slate-700/60"
title="Download File"
>
<Download className="w-3.5 h-3.5 text-slate-400" />
<span className="hidden sm:inline">Download</span>
</button>
<button
onClick={handleCopy}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all border ${
copied
? 'bg-emerald-950/80 text-emerald-300 border-emerald-800/80'
: 'bg-red-600 hover:bg-red-500 text-white border-red-500 shadow-sm shadow-red-600/20'
}`}
>
{copied ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-400" />
<span>Copied!</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copy Code</span>
</>
)}
</button>
</div>
</div>
{/* Snippet Description bar */}
{currentSnippet.description && (
<div className="px-4 py-2 bg-slate-900/50 border-b border-slate-800/60 text-xs text-slate-400 font-sans flex items-center gap-2">
<Terminal className="w-3.5 h-3.5 text-red-400 flex-shrink-0" />
<span>{currentSnippet.description}</span>
</div>
)}
{/* Code Display Area with Line Numbers */}
<div className="p-4 overflow-x-auto font-mono text-xs leading-relaxed max-h-[480px] bg-slate-950">
<table className="w-full border-collapse">
<tbody>
{lines.map((line, idx) => (
<tr key={idx} className="hover:bg-slate-900/40 transition-colors group">
<td className="w-10 select-none text-right pr-4 text-slate-600 group-hover:text-slate-500 font-mono text-[11px]">
{idx + 1}
</td>
<td className="whitespace-pre text-slate-200 pl-2">
{line}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
+105
View File
@@ -0,0 +1,105 @@
'use client';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { MessageSquare, Heart } from 'lucide-react';
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
import { GithubIcon as Github, TwitterIcon as Twitter } from '@/components/icons/BrandIcons';
export function Footer({ locale }: { locale: string }) {
const t = useTranslations('footer');
return (
<footer className="w-full border-t border-slate-800 bg-slate-950 text-slate-400 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8 mb-12">
{/* Brand column */}
<div className="space-y-4 md:col-span-1">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-slate-900 border border-slate-800 flex items-center justify-center overflow-hidden">
<img src="/logo.jpeg" alt="ayris.tech Logo" className="w-full h-full object-cover" />
</div>
<span className="font-bold text-white text-lg tracking-tight">ayris.tech YouTube</span>
</div>
<p className="text-xs leading-relaxed text-slate-400">
Official resource portal featuring tabbed code snippets, lesson notes, starter zip downloads, and timestamps for my YouTube tutorials.
</p>
<div className="flex items-center gap-3 text-slate-400">
<a href="https://youtube.com" target="_blank" rel="noopener noreferrer" className="hover:text-red-500 transition-colors p-2 bg-slate-900 rounded-lg border border-slate-800">
<Youtube className="w-4 h-4" />
</a>
<a href="https://github.com/ayrisdev" target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors p-2 bg-slate-900 rounded-lg border border-slate-800">
<Github className="w-4 h-4" />
</a>
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer" className="hover:text-sky-400 transition-colors p-2 bg-slate-900 rounded-lg border border-slate-800">
<Twitter className="w-4 h-4" />
</a>
<a href="https://discord.com" target="_blank" rel="noopener noreferrer" className="hover:text-indigo-400 transition-colors p-2 bg-slate-900 rounded-lg border border-slate-800">
<MessageSquare className="w-4 h-4" />
</a>
</div>
</div>
{/* Categories */}
<div>
<h4 className="font-semibold text-white text-sm mb-4">Popular Topics</h4>
<ul className="space-y-2 text-xs">
<li><Link href={`/${locale}/lessons?category=Next.js`} className="hover:text-red-400 transition-colors">Next.js 16 App Router</Link></li>
<li><Link href={`/${locale}/lessons?category=Python%20%26%20AI`} className="hover:text-red-400 transition-colors">Python AI Agents</Link></li>
<li><Link href={`/${locale}/lessons?category=Tailwind%20CSS`} className="hover:text-red-400 transition-colors">Tailwind CSS v4 Tips</Link></li>
<li><Link href={`/${locale}/lessons?category=Docker%20%26%20DevOps`} className="hover:text-red-400 transition-colors">Docker & Coolify Deploy</Link></li>
</ul>
</div>
{/* Quick Links */}
<div>
<h4 className="font-semibold text-white text-sm mb-4">Quick Links</h4>
<ul className="space-y-2 text-xs">
<li><Link href={`/${locale}/lessons`} className="hover:text-red-400 transition-colors">All YouTube Lessons</Link></li>
<li><Link href={`/${locale}/cheatsheets`} className="hover:text-red-400 transition-colors">Quick Cheatsheets</Link></li>
<li><Link href={`/${locale}/contact`} className="hover:text-red-400 transition-colors">Contact & Suggestions</Link></li>
</ul>
</div>
{/* Info & Banner */}
<div className="bg-slate-900/60 p-5 rounded-2xl border border-slate-800 flex flex-col justify-between">
<div>
<span className="text-xs font-bold text-red-400 block mb-1">ayris.tech YouTube Channel</span>
<p className="text-[11px] text-slate-300 leading-normal">
Official resource portal for all video code snippets, starter packages, and lesson notes.
</p>
</div>
<div className="mt-4 pt-3 border-t border-slate-800 flex items-center justify-between text-[11px]">
<span className="text-slate-400">Educational Portal</span>
<span className="text-emerald-400 font-semibold flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> Live
</span>
</div>
</div>
</div>
{/* Bottom Bar with mandatory link */}
<div className="max-w-7xl mx-auto pt-8 border-t border-slate-900 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs">
<p className="text-slate-400">
© {new Date().getFullYear()} ayris.tech YouTube Channel. {t('rights')}
</p>
{/* MANDATORY Created by ayris.tech LINK */}
<div className="flex items-center gap-2">
<span className="text-slate-400 flex items-center gap-1">
Built with <Heart className="w-3.5 h-3.5 text-red-500 fill-red-500" />
</span>
<a
href="https://ayris.tech"
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1 bg-red-950/60 hover:bg-red-900/80 text-red-400 hover:text-red-300 border border-red-800/40 rounded-full font-bold transition-all shadow-sm shadow-red-900/30"
>
Created by ayris.tech
</a>
</div>
</div>
</footer>
);
}
+74
View File
@@ -0,0 +1,74 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Code2, BookOpen, Mail, Sparkles } from 'lucide-react';
export function Navbar({ locale }: { locale: string }) {
const t = useTranslations('nav');
const pathname = usePathname();
const navItems = [
{ href: `/${locale}`, label: t('home'), icon: Sparkles },
{ href: `/${locale}/lessons`, label: t('lessons'), icon: Code2 },
{ href: `/${locale}/cheatsheets`, label: t('cheatsheets'), icon: BookOpen },
{ href: `/${locale}/contact`, label: t('contact'), icon: Mail },
];
return (
<header className="sticky top-0 z-50 w-full border-b border-slate-800 bg-slate-950/80 backdrop-blur-xl transition-all">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
{/* Logo & Brand */}
<Link href={`/${locale}`} className="flex items-center gap-3 group">
<div className="w-10 h-10 rounded-xl bg-slate-900 border border-slate-800 flex items-center justify-center shadow-lg shadow-red-500/20 group-hover:scale-105 transition-transform overflow-hidden">
<img src="/logo.jpeg" alt="ayris.tech Logo" className="w-full h-full object-cover" />
</div>
<div className="flex flex-col">
<span className="font-extrabold text-lg text-white tracking-tight flex items-center gap-1.5">
ayris.tech <span className="text-xs px-2 py-0.5 rounded-full bg-red-500/10 text-red-400 border border-red-500/20">YouTube</span>
</span>
<span className="text-[11px] text-slate-400 font-medium">Educational Code & Resource Hub</span>
</div>
</Link>
{/* Center Nav Links */}
<nav className="hidden md:flex items-center gap-1 bg-slate-900/60 p-1.5 rounded-full border border-slate-800">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href || (item.href !== `/${locale}` && pathname.startsWith(item.href));
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-xs font-semibold transition-all ${
isActive
? 'bg-red-600 text-white shadow-md shadow-red-600/20'
: 'text-slate-300 hover:text-white hover:bg-slate-800/60'
}`}
>
<Icon className="w-3.5 h-3.5" />
{item.label}
</Link>
);
})}
</nav>
{/* Right Action: YouTube Subscribe */}
<div className="flex items-center gap-3">
<a
href="https://youtube.com"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600 text-white text-xs font-bold rounded-full shadow-lg shadow-red-600/20 hover:shadow-red-600/30 transition-all hover:scale-105"
>
<img src="/logo.jpeg" alt="Logo" className="w-4 h-4 rounded-full object-cover" />
<span>{t('subscribe')}</span>
</a>
</div>
</div>
</header>
);
}
+108
View File
@@ -0,0 +1,108 @@
'use client';
import Link from 'next/link';
import { VideoLesson } from '@/lib/data';
import { Download, Eye, FileCode2, Clock, ArrowRight, Sparkles } from 'lucide-react';
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
interface ResourceCardProps {
lesson: VideoLesson;
locale: string;
}
export function ResourceCard({ lesson, locale }: ResourceCardProps) {
return (
<div className="group relative bg-slate-900/60 hover:bg-slate-900 border border-slate-800 hover:border-slate-700/80 rounded-2xl overflow-hidden transition-all duration-300 flex flex-col justify-between shadow-xl hover:shadow-2xl hover:shadow-red-500/5 hover:-translate-y-1">
<div>
{/* Thumbnail Container */}
<div className="relative aspect-video w-full overflow-hidden bg-slate-950">
<img
src={lesson.thumbnailUrl}
alt={lesson.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/20 to-transparent opacity-80" />
{/* Featured Badge */}
{lesson.isFeatured && (
<div className="absolute top-3 left-3 flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-red-600/90 text-white text-[11px] font-bold shadow-lg backdrop-blur-md">
<Sparkles className="w-3 h-3" />
<span>FEATURED TUTORIAL</span>
</div>
)}
{/* Category Tag */}
<div className="absolute top-3 right-3 px-2.5 py-1 rounded-full bg-slate-900/90 border border-slate-700/80 text-slate-200 text-[11px] font-semibold backdrop-blur-md">
{lesson.category}
</div>
{/* Duration Badge */}
<div className="absolute bottom-3 right-3 flex items-center gap-1 px-2 py-0.5 rounded bg-slate-950/90 text-slate-300 text-[11px] font-mono border border-slate-800">
<Clock className="w-3 h-3 text-red-400" />
<span>{lesson.duration}</span>
</div>
</div>
{/* Card Body */}
<div className="p-5 space-y-3">
{/* Tags */}
<div className="flex flex-wrap gap-1.5">
{lesson.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="text-[10px] font-medium px-2 py-0.5 rounded bg-slate-800/60 text-slate-400 border border-slate-800"
>
#{tag}
</span>
))}
</div>
{/* Title */}
<h3 className="font-bold text-base text-white group-hover:text-red-400 transition-colors line-clamp-2 leading-snug">
<Link href={`/${locale}/lessons/${lesson.slug}`}>
{lesson.title}
</Link>
</h3>
{/* Summary */}
<p className="text-xs text-slate-400 line-clamp-2 leading-relaxed">
{lesson.summary}
</p>
{/* Stats Bar */}
<div className="pt-2 flex items-center justify-between text-[11px] text-slate-400 border-t border-slate-800/60">
<span className="flex items-center gap-1">
<FileCode2 className="w-3.5 h-3.5 text-red-400" />
<span>{lesson.codeSnippets.length} Code Files</span>
</span>
<span className="flex items-center gap-1">
<Download className="w-3.5 h-3.5 text-emerald-400" />
<span>{lesson.downloadCount.toLocaleString()} Downloads</span>
</span>
<span className="flex items-center gap-1">
<Eye className="w-3.5 h-3.5 text-sky-400" />
<span>{lesson.viewsCount.toLocaleString()} Views</span>
</span>
</div>
</div>
</div>
{/* Card Action Footer */}
<div className="px-5 pb-5 pt-1">
<Link
href={`/${locale}/lessons/${lesson.slug}`}
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-red-600 text-slate-200 hover:text-white text-xs font-bold transition-all group-hover:shadow-lg group-hover:shadow-red-600/20"
>
<span>View Code & Notes</span>
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
);
}
+78
View File
@@ -0,0 +1,78 @@
'use client';
import { useState } from 'react';
import { Play, Clock, ExternalLink } from 'lucide-react';
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
import { VideoChapter } from '@/lib/data';
interface VideoPlayerProps {
youtubeId: string;
youtubeUrl: string;
title: string;
chapters?: VideoChapter[];
}
export function VideoPlayer({ youtubeId, youtubeUrl, title, chapters }: VideoPlayerProps) {
const [activeSeconds, setActiveSeconds] = useState(0);
const embedUrl = `https://www.youtube.com/embed/${youtubeId}?autoplay=0&start=${activeSeconds}`;
const handleChapterClick = (seconds: number) => {
setActiveSeconds(seconds);
};
return (
<div className="w-full space-y-4">
{/* Video Container */}
<div className="relative w-full aspect-video rounded-2xl overflow-hidden bg-slate-900 border border-slate-800 shadow-2xl group">
<iframe
src={embedUrl}
title={title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="w-full h-full border-0"
/>
</div>
{/* Chapters / Timestamps Quick Jumps */}
{chapters && chapters.length > 0 && (
<div className="bg-slate-900/80 p-4 rounded-xl border border-slate-800 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-300 uppercase tracking-wider flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-red-400" />
Timestamped Chapters
</span>
<a
href={youtubeUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-red-400 hover:text-red-300 flex items-center gap-1 font-medium transition-colors"
>
<span>Watch on YouTube</span>
<ExternalLink className="w-3 h-3" />
</a>
</div>
<div className="flex flex-wrap gap-2">
{chapters.map((chap) => (
<button
key={chap.time}
onClick={() => handleChapterClick(chap.seconds)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
activeSeconds === chap.seconds
? 'bg-red-600 text-white shadow-md shadow-red-600/30 ring-1 ring-red-400'
: 'bg-slate-800/80 text-slate-300 hover:bg-slate-800 hover:text-white border border-slate-700/60'
}`}
>
<span className="font-mono text-[11px] text-red-400 font-bold bg-slate-950/60 px-1.5 py-0.5 rounded">
{chap.time}
</span>
<span>{chap.title}</span>
</button>
))}
</div>
</div>
)}
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
export function GithubIcon({ className = "w-5 h-5", ...props }: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} {...props}>
<path fillRule="evenodd" clipRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
</svg>
);
}
export function TwitterIcon({ className = "w-5 h-5", ...props }: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" className={className} {...props}>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
export function YoutubeIcon({ className = "w-5 h-5", ...props }: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
className={className}
{...props}
>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+103
View File
@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-(--card-spacing)", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+20
View File
@@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+15
View File
@@ -0,0 +1,15 @@
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default
};
});
+10
View File
@@ -0,0 +1,10 @@
import {defineRouting} from 'next-intl/routing';
import {createNavigation} from 'next-intl/navigation';
export const routing = defineRouting({
locales: ['en'],
defaultLocale: 'en'
});
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
+128
View File
@@ -0,0 +1,128 @@
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
const INITIAL_CATEGORIES = [
'Next.js',
'React',
'Python & AI',
'Tailwind CSS',
'Docker & DevOps',
];
export async function getCategories() {
try {
if (!db || !(db as any).category) {
console.error('db.category is not initialized yet');
return INITIAL_CATEGORIES.map((name) => ({ id: name, name, slug: name.toLowerCase() }));
}
let categories = await db.category.findMany({
orderBy: { name: 'asc' },
});
if (categories.length === 0) {
// Seed default categories if database has none
for (const name of INITIAL_CATEGORIES) {
const slug = name
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-');
await db.category.upsert({
where: { slug },
update: { name },
create: { name, slug },
}).catch(err => console.error('Failed seeding category:', name, err));
}
categories = await db.category.findMany({
orderBy: { name: 'asc' },
});
}
return categories;
} catch (error) {
console.error('Error fetching categories:', error);
return INITIAL_CATEGORIES.map((name) => ({ id: name, name, slug: name.toLowerCase() }));
}
}
export async function createCategory(name: string) {
try {
const trimmed = name.trim();
if (!trimmed) return { success: false, error: 'Category name cannot be empty' };
const slug = trimmed
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-') + '-' + Date.now().toString().slice(-4);
const newCategory = await db.category.create({
data: {
name: trimmed,
slug,
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/lessons');
revalidatePath('/[locale]/admin');
return { success: true, category: newCategory };
} catch (error: any) {
console.error('Error creating category:', error);
return { success: false, error: error.message || 'Failed to create category' };
}
}
export async function updateCategory(id: string, name: string) {
try {
const trimmed = name.trim();
if (!trimmed) return { success: false, error: 'Category name cannot be empty' };
const slug = trimmed
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-') + '-' + Date.now().toString().slice(-4);
const updatedCategory = await db.category.updateMany({
where: {
OR: [{ id }, { name: id }, { slug: id }],
},
data: {
name: trimmed,
slug,
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/lessons');
revalidatePath('/[locale]/admin');
return { success: true, count: updatedCategory.count };
} catch (error: any) {
console.error('Error updating category:', error);
return { success: false, error: error.message || 'Failed to update category' };
}
}
export async function deleteCategory(id: string) {
try {
if (!db || !(db as any).category) {
return { success: false, error: 'Category database table is not ready' };
}
await db.category.deleteMany({
where: {
OR: [{ id }, { name: id }, { slug: id }],
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/dersler');
revalidatePath('/[locale]/admin');
return { success: true };
} catch (error: any) {
console.error('Error deleting category:', error);
return { success: false, error: error.message || 'Failed to delete category' };
}
}
+113
View File
@@ -0,0 +1,113 @@
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function getCheatsheets() {
try {
const cheatsheets = await db.cheatsheet.findMany({
include: {
items: true,
},
orderBy: {
createdAt: 'desc',
},
});
return cheatsheets;
} catch (error) {
console.error('Error fetching cheatsheets:', error);
return [];
}
}
export async function createCheatsheet(data: {
title: string;
category: string;
description: string;
items: { command: string; description: string }[];
}) {
try {
const slug = data.title
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-') + '-' + Date.now().toString().slice(-4);
const newCheatsheet = await db.cheatsheet.create({
data: {
title: data.title,
slug,
category: data.category,
description: data.description,
tags: [data.category, 'Cheatsheet', 'Commands'],
items: {
create: data.items.map((item) => ({
command: item.command,
description: item.description,
})),
},
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/cheatsheets');
revalidatePath('/[locale]/admin');
return { success: true, cheatsheet: newCheatsheet };
} catch (error: any) {
console.error('Error creating cheatsheet:', error);
return { success: false, error: error.message || 'Failed to create cheatsheet' };
}
}
export async function updateCheatsheet(id: string, data: {
title: string;
category: string;
description: string;
items: { command: string; description: string }[];
}) {
try {
// Delete existing items and recreate
await db.cheatsheetItem.deleteMany({ where: { cheatsheetId: id } });
const updated = await db.cheatsheet.update({
where: { id },
data: {
title: data.title,
category: data.category,
description: data.description,
lastUpdated: new Date(),
items: {
create: data.items.map((item) => ({
command: item.command,
description: item.description,
})),
},
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/cheatsheets');
revalidatePath('/[locale]/admin');
return { success: true, cheatsheet: updated };
} catch (error: any) {
console.error('Error updating cheatsheet:', error);
return { success: false, error: error.message || 'Failed to update cheatsheet' };
}
}
export async function deleteCheatsheet(id: string) {
try {
await db.cheatsheet.delete({
where: { id },
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/rehberler');
revalidatePath('/[locale]/admin');
return { success: true };
} catch (error: any) {
console.error('Error deleting cheatsheet:', error);
return { success: false, error: error.message || 'Failed to delete cheatsheet' };
}
}
+40
View File
@@ -0,0 +1,40 @@
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function submitContactMessage(data: {
name: string;
email: string;
subject: string;
message: string;
}) {
try {
const newMessage = await db.contactMessage.create({
data: {
name: data.name,
email: data.email,
subject: data.subject,
message: data.message,
},
});
revalidatePath('/[locale]/admin');
return { success: true, message: newMessage };
} catch (error: any) {
console.error('Error saving contact message:', error);
return { success: false, error: error.message || 'Failed to submit contact message' };
}
}
export async function getContactMessages() {
try {
const messages = await db.contactMessage.findMany({
orderBy: { createdAt: 'desc' },
});
return messages;
} catch (error) {
console.error('Error fetching contact messages:', error);
return [];
}
}
+288
View File
@@ -0,0 +1,288 @@
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
function extractYoutubeId(url: string): string {
if (!url) return 'dQw4w9WgXcQ';
const trimmed = url.trim();
if (trimmed.includes('v=')) {
return trimmed.split('v=')[1].split('&')[0];
}
if (trimmed.includes('youtu.be/')) {
return trimmed.split('youtu.be/')[1].split('?')[0];
}
if (trimmed.includes('embed/')) {
return trimmed.split('embed/')[1].split('?')[0];
}
if (trimmed.length === 11) {
return trimmed;
}
return 'dQw4w9WgXcQ';
}
export async function getLessons(options?: { query?: string; category?: string }) {
try {
const { query, category } = options || {};
const where: any = {};
if (category) {
where.category = category;
}
if (query && query.trim() !== '') {
const q = query.trim();
where.OR = [
{ title: { contains: q, mode: 'insensitive' } },
{ summary: { contains: q, mode: 'insensitive' } },
{ tags: { hasSome: [q] } },
];
}
const lessons = await db.lesson.findMany({
where,
include: {
codeSnippets: true,
downloads: true,
chapters: true,
},
orderBy: {
createdAt: 'desc',
},
});
return lessons;
} catch (error) {
console.error('Error fetching lessons:', error);
return [];
}
}
export async function getLessonBySlug(slug: string) {
try {
const lesson = await db.lesson.findUnique({
where: { slug },
include: {
codeSnippets: true,
downloads: true,
chapters: {
orderBy: { seconds: 'asc' },
},
},
});
if (lesson) {
await db.lesson.update({
where: { id: lesson.id },
data: { viewsCount: { increment: 1 } },
}).catch(err => console.error('Failed to increment view count:', err));
}
return lesson;
} catch (error) {
console.error('Error fetching lesson by slug:', error);
return null;
}
}
export async function getDashboardStats() {
try {
const [totalLessons, aggregate, totalMessages] = await Promise.all([
db.lesson.count(),
db.lesson.aggregate({
_sum: {
viewsCount: true,
downloadCount: true,
},
}),
db.contactMessage.count(),
]);
return {
totalLessons,
totalViews: aggregate._sum.viewsCount || 0,
totalDownloads: aggregate._sum.downloadCount || 0,
totalMessages,
};
} catch (error) {
console.error('Error fetching dashboard stats:', error);
return {
totalLessons: 0,
totalViews: 0,
totalDownloads: 0,
totalMessages: 0,
};
}
}
export async function createLesson(data: {
title: string;
youtubeUrl: string;
category: string;
summary: string;
duration?: string;
notesMarkdown?: string[];
codeSnippets?: { fileName: string; language: string; code: string }[];
downloads?: { title: string; type: string; url: string; size?: string }[];
chapters?: { time: string; seconds: number; title: string }[];
}) {
try {
const slug = data.title
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '') + '-' + Date.now().toString().slice(-4);
const youtubeId = extractYoutubeId(data.youtubeUrl);
const thumbnailUrl = `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;
const codeSnippets = data.codeSnippets || [];
const downloads = data.downloads || [];
const chapters = data.chapters || [];
const notesMarkdown = data.notesMarkdown || [];
const newLesson = await db.lesson.create({
data: {
title: data.title,
slug,
youtubeId,
youtubeUrl: data.youtubeUrl || `https://www.youtube.com/watch?v=${youtubeId}`,
thumbnailUrl,
duration: data.duration || '15:00',
category: data.category,
tags: [data.category, 'Tutorial', 'Source Code'],
summary: data.summary || data.title,
notesMarkdown,
codeSnippets: {
create: codeSnippets.map((snip) => ({
fileName: snip.fileName || 'app/page.tsx',
language: snip.language || 'typescript',
code: snip.code,
})),
},
downloads: {
create: downloads.map((dl) => ({
title: dl.title,
type: dl.type || 'zip',
url: dl.url || '#',
size: dl.size || '1.0 MB',
})),
},
chapters: {
create: chapters.map((chap) => ({
time: chap.time,
seconds: chap.seconds || 0,
title: chap.title,
})),
},
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/lessons');
return { success: true, lesson: newLesson };
} catch (error: any) {
console.error('Error creating lesson:', error);
return { success: false, error: error.message || 'Failed to create lesson in database' };
}
}
export async function updateLesson(id: string, data: {
title: string;
youtubeUrl: string;
category: string;
summary: string;
duration?: string;
notesMarkdown?: string[];
codeSnippets?: { fileName: string; language: string; code: string }[];
downloads?: { title: string; type: string; url: string; size?: string }[];
chapters?: { time: string; seconds: number; title: string }[];
}) {
try {
const youtubeId = extractYoutubeId(data.youtubeUrl);
const thumbnailUrl = `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;
const codeSnippets = data.codeSnippets || [];
const downloads = data.downloads || [];
const chapters = data.chapters || [];
const notesMarkdown = data.notesMarkdown || [];
// Delete existing sub-models and recreate
await Promise.all([
db.codeSnippet.deleteMany({ where: { lessonId: id } }),
db.resourceDownload.deleteMany({ where: { lessonId: id } }),
db.videoChapter.deleteMany({ where: { lessonId: id } }),
]);
const updatedLesson = await db.lesson.update({
where: { id },
data: {
title: data.title,
youtubeId,
youtubeUrl: data.youtubeUrl,
thumbnailUrl,
duration: data.duration || '15:00',
category: data.category,
summary: data.summary,
notesMarkdown,
codeSnippets: {
create: codeSnippets.map((snip) => ({
fileName: snip.fileName || 'app/page.tsx',
language: snip.language || 'typescript',
code: snip.code,
})),
},
downloads: {
create: downloads.map((dl) => ({
title: dl.title,
type: dl.type || 'zip',
url: dl.url || '#',
size: dl.size || '1.0 MB',
})),
},
chapters: {
create: chapters.map((chap) => ({
time: chap.time,
seconds: chap.seconds || 0,
title: chap.title,
})),
},
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/lessons');
return { success: true, lesson: updatedLesson };
} catch (error: any) {
console.error('Error updating lesson:', error);
return { success: false, error: error.message || 'Failed to update lesson' };
}
}
export async function deleteLesson(id: string) {
try {
await db.lesson.delete({ where: { id } });
revalidatePath('/[locale]');
revalidatePath('/[locale]/lessons');
return { success: true };
} catch (error) {
console.error('Error deleting lesson:', error);
return { success: false };
}
}
export async function incrementDownloadCount(lessonId: string) {
try {
await db.lesson.update({
where: { id: lessonId },
data: { downloadCount: { increment: 1 } },
});
revalidatePath('/[locale]/dersler');
return { success: true };
} catch (error) {
console.error('Error incrementing download count:', error);
return { success: false };
}
}
+47
View File
@@ -0,0 +1,47 @@
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
const DEFAULT_SETTINGS: Record<string, string> = {
channelName: 'DevHub YouTube',
youtubeUrl: 'https://youtube.com/@DevHubChannel',
githubUrl: 'https://github.com/ayrisdev',
contactEmail: 'contact@youtube-devhub.com',
defaultCategory: 'Next.js',
};
export async function getSettings() {
try {
const dbSettings = await db.setting.findMany();
const result = { ...DEFAULT_SETTINGS };
dbSettings.forEach((item) => {
result[item.key] = item.value;
});
return result;
} catch (error) {
console.error('Error fetching settings:', error);
return DEFAULT_SETTINGS;
}
}
export async function updateSettings(data: Record<string, string>) {
try {
const promises = Object.entries(data).map(([key, value]) =>
db.setting.upsert({
where: { key },
update: { value },
create: { key, value },
})
);
await Promise.all(promises);
revalidatePath('/[locale]/admin');
return { success: true };
} catch (error: any) {
console.error('Error updating settings:', error);
return { success: false, error: error.message || 'Failed to update settings' };
}
}
+56
View File
@@ -0,0 +1,56 @@
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function getUsers() {
try {
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true,
name: true,
email: true,
role: true,
createdAt: true,
},
});
// If database has no users yet, return default admin
if (users.length === 0) {
return [
{
id: '1',
name: 'Admin User',
email: 'admin@ayris.tech',
role: 'ADMIN',
createdAt: new Date(),
},
];
}
return users;
} catch (error) {
console.error('Error fetching users:', error);
return [];
}
}
export async function createAdminUser(data: { name: string; email: string; password?: string }) {
try {
const newUser = await db.user.create({
data: {
name: data.name,
email: data.email,
password: data.password || 'admin123',
role: 'ADMIN',
},
});
revalidatePath('/[locale]/admin');
return { success: true, user: newUser };
} catch (error: any) {
console.error('Error creating admin user:', error);
return { success: false, error: error.message || 'Failed to create admin user' };
}
}
+47
View File
@@ -0,0 +1,47 @@
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
// Boilerplate mock logic
// TODO: In production, lookup user in Prisma and verify password using bcrypt
// const user = await db.user.findUnique({ where: { email: credentials.email } })
if (credentials?.email === "admin@ayris.tech" && credentials?.password === "admin") {
return {
id: "1",
name: "Admin User",
email: "admin@ayris.tech",
role: "ADMIN"
}
}
return null
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = (user as any).role
}
return token
},
async session({ session, token }) {
if (session.user && token.role) {
(session.user as any).role = token.role
}
return session
}
},
pages: {
signIn: '/login'
}
})
+20
View File
@@ -0,0 +1,20 @@
import { v2 as cloudinary } from 'cloudinary'
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!,
})
export async function uploadImage(file: string, folder: string) {
const result = await cloudinary.uploader.upload(file, {
folder, transformation: [{ quality: 'auto', fetch_format: 'auto' }],
})
return { url: result.secure_url, publicId: result.public_id }
}
export async function deleteImage(publicId: string) {
await cloudinary.uploader.destroy(publicId)
}
export { cloudinary }
+448
View File
@@ -0,0 +1,448 @@
export interface CodeSnippet {
fileName: string;
language: string;
code: string;
description?: string;
}
export interface ResourceDownload {
title: string;
type: 'zip' | 'github' | 'pdf' | 'link';
url: string;
size?: string;
}
export interface VideoChapter {
time: string;
seconds: number;
title: string;
}
export interface VideoLesson {
id: string;
slug: string;
title: string;
youtubeId: string;
youtubeUrl: string;
thumbnailUrl: string;
duration: string;
publishDate: string;
category: string;
tags: string[];
viewsCount: number;
downloadCount: number;
likesCount: number;
summary: string;
notesMarkdown: string[];
codeSnippets: CodeSnippet[];
downloads: ResourceDownload[];
chapters: VideoChapter[];
isFeatured?: boolean;
}
export interface Cheatsheet {
id: string;
slug: string;
title: string;
category: string;
description: string;
tags: string[];
lastUpdated: string;
items: {
command: string;
description: string;
example?: string;
}[];
}
export const CATEGORIES = [
"Next.js",
"React",
"Python & AI",
"Tailwind CSS",
"Docker & DevOps",
"JavaScript"
];
export const MOCK_VIDEOS: VideoLesson[] = [
{
id: "vid-1",
slug: "nextjs-16-app-router-full-course",
title: "Next.js 16 App Router & Server Actions Complete Masterclass (Code & Project)",
youtubeId: "dQw4w9WgXcQ",
youtubeUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
thumbnailUrl: "https://images.unsplash.com/photo-1618401471353-b98afee0b2eb?w=800&auto=format&fit=crop&q=80",
duration: "42:15",
publishDate: "2026-07-15",
category: "Next.js",
tags: ["Next.js 16", "App Router", "Server Actions", "TypeScript", "Tailwind CSS v4"],
viewsCount: 24500,
downloadCount: 4120,
likesCount: 1890,
isFeatured: true,
summary: "In this tutorial, we build a full-stack Next.js 16 application from scratch covering App Router architecture, proxy.ts middleware, Server Actions, and PostgreSQL integration.",
notesMarkdown: [
"📌 In Next.js 16, using `proxy.ts` is recommended over deprecated `middleware.ts`.",
"📌 Server Actions functions are declared with `'use server'` directive and can be invoked directly from client components.",
"📌 Data fetching revalidation uses `revalidatePath` or `revalidateTag` helpers.",
"📌 Form handling UX is significantly improved using `useActionState` and `useFormStatus` hooks."
],
codeSnippets: [
{
fileName: "app/actions.ts",
language: "typescript",
description: "Server Action function handling form submission",
code: `'use server'
import { revalidatePath } from 'next/cache';
export async function submitProjectIdea(formData: FormData) {
const title = formData.get('title') as string;
const description = formData.get('description') as string;
if (!title || title.length < 3) {
return { success: false, error: 'Title must be at least 3 characters long.' };
}
// Database insertion simulation
console.log('Saved Idea:', { title, description });
revalidatePath('/ideas');
return { success: true, message: 'Your idea was submitted successfully!' };
}`
},
{
fileName: "proxy.ts",
language: "typescript",
description: "Next.js 16 proxy configuration (middleware alternative)",
code: `import { NextRequest, NextResponse } from 'next/server';
import createMiddleware from 'next-intl/middleware';
import { routing } from '@/i18n/routing';
const intlMiddleware = createMiddleware(routing);
export async function proxy(request: NextRequest) {
// Admin route protection example
if (request.nextUrl.pathname.includes('/admin')) {
// Auth check logic
}
return intlMiddleware(request);
}
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\\\..*).*)']
};`
},
{
fileName: "components/IdeaForm.tsx",
language: "tsx",
description: "Client Form component using useActionState",
code: `'use client';
import { useActionState } from 'react';
import { submitProjectIdea } from '@/app/actions';
export function IdeaForm() {
const [state, formAction, isPending] = useActionState(submitProjectIdea, null);
return (
<form action={formAction} className="space-y-4 max-w-md p-6 bg-slate-900 rounded-xl border border-slate-800">
<h3 className="text-xl font-bold text-white">Add New Idea</h3>
<div>
<label className="block text-sm text-slate-400 mb-1">Idea Title</label>
<input name="title" required className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white" />
</div>
<button disabled={isPending} className="w-full py-2.5 bg-red-600 hover:bg-red-500 text-white rounded-lg font-medium">
{isPending ? 'Submitting...' : 'Share Idea'}
</button>
{state?.error && <p className="text-red-400 text-sm">{state.error}</p>}
{state?.success && <p className="text-emerald-400 text-sm">{state.message}</p>}
</form>
);
}`
}
],
downloads: [
{
title: "Full Project Source Code (Starter Pack)",
type: "zip",
url: "https://github.com/ayrisdev/nextjs-16-starter/archive/refs/heads/main.zip",
size: "4.2 MB"
},
{
title: "GitHub Repository",
type: "github",
url: "https://github.com/ayrisdev/nextjs-16-starter"
},
{
title: "Lesson Notes & Architecture (PDF Document)",
type: "pdf",
url: "#",
size: "1.8 MB"
}
],
chapters: [
{ time: "00:00", seconds: 0, title: "Introduction & Demo" },
{ time: "04:15", seconds: 255, title: "Next.js 16 Project Setup & Structure" },
{ time: "11:30", seconds: 690, title: "Routing & Middleware with proxy.ts" },
{ time: "21:00", seconds: 1260, title: "Form Handling via Server Actions" },
{ time: "32:45", seconds: 1965, title: "PostgreSQL & Prisma Integration" },
{ time: "40:00", seconds: 2400, title: "Deployment & Summary" }
]
},
{
id: "vid-2",
slug: "python-ai-agent-automation",
title: "Building Autonomous AI Agents with Python (CrewAI & Gemini API)",
youtubeId: "dQw4w9WgXcQ",
youtubeUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
thumbnailUrl: "https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&auto=format&fit=crop&q=80",
duration: "28:50",
publishDate: "2026-07-10",
category: "Python & AI",
tags: ["Python", "CrewAI", "Gemini API", "AI Agents", "Automation"],
viewsCount: 18200,
downloadCount: 3200,
likesCount: 1450,
summary: "Build an autonomous AI agent in Python that crawls web data, summarizes tech news, and generates structured reports automatically.",
notesMarkdown: [
"🤖 AI agents are configured with role, goal, and backstory.",
"🤖 Equip agents with real-time web search using Serper API or Tavily.",
"🤖 Gemini 1.5 Flash provides high token throughput with minimal latency."
],
codeSnippets: [
{
fileName: "agent.py",
language: "python",
description: "CrewAI Agent Definition",
code: `from crewai import Agent, Task, Crew, Process
from langchain_google_genai import ChatGoogleGenerativeAI
import os
# Initialize Gemini LLM
llm = ChatGoogleGenerativeAI(
model="gemini-1.5-flash",
google_api_key=os.getenv("GEMINI_API_KEY")
)
# Researcher Agent
researcher = Agent(
role='Tech Researcher',
goal='Discover top AI trends from the past 24 hours',
backstory='You are an expert technology journalist following AI developments.',
verbose=True,
llm=llm
)
# Task Definition
task1 = Task(
description='Summarize 2026 AI trends in 5 bullet points.',
expected_output='A markdown list with 5 key takeaways.',
agent=researcher
)
crew = Crew(
agents=[researcher],
tasks=[task1],
process=Process.sequential
)
result = crew.kickoff()
print(result)`
}
],
downloads: [
{
title: "Python AI Agent Project Files",
type: "zip",
url: "#",
size: "2.1 MB"
},
{
title: "GitHub Repository",
type: "github",
url: "https://github.com/ayrisdev/python-ai-agent"
}
],
chapters: [
{ time: "00:00", seconds: 0, title: "Introduction & What is an AI Agent?" },
{ time: "05:20", seconds: 320, title: "Installing Dependencies (CrewAI & Gemini)" },
{ time: "14:10", seconds: 850, title: "Agent & Task Coding" },
{ time: "24:00", seconds: 1440, title: "Testing & Output Review" }
]
},
{
id: "vid-3",
slug: "tailwind-css-v4-tricks-and-setup",
title: "Modern UI Design with Tailwind CSS v4: Tricks, Themes & Components",
youtubeId: "dQw4w9WgXcQ",
youtubeUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
thumbnailUrl: "https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?w=800&auto=format&fit=crop&q=80",
duration: "35:10",
publishDate: "2026-07-02",
category: "Tailwind CSS",
tags: ["Tailwind CSS v4", "OKLCH", "CSS Grid", "UI Design", "Shadcn"],
viewsCount: 31000,
downloadCount: 5400,
likesCount: 2600,
summary: "Learn CSS-first configuration in Tailwind CSS v4 using `@import \"tailwindcss\";`, OKLCH color spaces, and advanced glassmorphism component design.",
notesMarkdown: [
"🎨 In Tailwind v4, `@theme` directive in `globals.css` replaces `tailwind.config.js`.",
"🎨 OKLCH color space ensures consistent perceived contrast across light and dark modes.",
"🎨 `@custom-variant dark` provides flexible dark mode scoping."
],
codeSnippets: [
{
fileName: "globals.css",
language: "css",
description: "Tailwind CSS v4 Theme Configuration",
code: `@import "tailwindcss";
@theme inline {
--color-primary: oklch(0.65 0.22 260);
--color-accent: oklch(0.72 0.19 145);
--radius-lg: 1rem;
}
:root {
--background: oklch(0.98 0.01 250);
--foreground: oklch(0.15 0.02 250);
}`
}
],
downloads: [
{
title: "Tailwind v4 UI Starter Pack",
type: "zip",
url: "#",
size: "1.2 MB"
}
],
chapters: [
{ time: "00:00", seconds: 0, title: "What's New in Tailwind v4" },
{ time: "08:30", seconds: 510, title: "Defining Color Palettes with @theme" },
{ time: "20:15", seconds: 1215, title: "Building Glassmorphism Cards" }
]
},
{
id: "vid-4",
slug: "docker-container-nextjs-deploy",
title: "Deploying Next.js & PostgreSQL Applications using Docker & Coolify",
youtubeId: "dQw4w9WgXcQ",
youtubeUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
thumbnailUrl: "https://images.unsplash.com/photo-1605745341112-85968b19335b?w=800&auto=format&fit=crop&q=80",
duration: "31:45",
publishDate: "2026-06-25",
category: "Docker & DevOps",
tags: ["Docker", "Docker Compose", "Coolify", "Next.js", "DevOps"],
viewsCount: 15400,
downloadCount: 2900,
likesCount: 1120,
summary: "Production-ready multi-stage Dockerfile and docker-compose setup to deploy Next.js applications seamlessly to VPS or Coolify.",
notesMarkdown: [
"🐳 Setting `output: 'standalone'` in Next.js reduces image size from 1GB to 120MB.",
"🐳 Define a dummy `DATABASE_URL` during Prisma generate step inside Dockerfile."
],
codeSnippets: [
{
fileName: "Dockerfile",
language: "dockerfile",
description: "Optimized Multi-stage Dockerfile",
code: `FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
RUN npx prisma generate
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]`
}
],
downloads: [
{
title: "Dockerfile & Docker Compose Template",
type: "zip",
url: "#",
size: "350 KB"
}
],
chapters: [
{ time: "00:00", seconds: 0, title: "Introduction" },
{ time: "06:00", seconds: 360, title: "Writing Multi-stage Dockerfile" },
{ time: "18:20", seconds: 1100, title: "Coolify & VPS Integration" }
]
}
];
export const MOCK_CHEATSHEETS: Cheatsheet[] = [
{
id: "cs-1",
slug: "git-komutlari-hizli-rehber",
title: "Essential Git & GitHub Commands Cheatsheet",
category: "DevOps & Tooling",
description: "Handy reference for daily Git workflow, branching, and conflict resolution commands.",
tags: ["Git", "GitHub", "Terminal"],
lastUpdated: "2026-07-20",
items: [
{ command: "git checkout -b feature/new-feature", description: "Creates a new branch and switches to it." },
{ command: "git status", description: "Lists modified and untracked files." },
{ command: "git commit -m 'feat: add new page'", description: "Saves changes with a descriptive commit message." },
{ command: "git push origin feature/new-feature", description: "Pushes the branch to remote repository." },
{ command: "git log --oneline -n 5", description: "Displays last 5 commits in clean single line format." }
]
},
{
id: "cs-2",
slug: "nextjs-app-router-cheatsheet",
title: "Next.js App Router File Structure & Route Handlers",
category: "Next.js",
description: "Special file conventions (page, layout, loading, error) and their responsibilities.",
tags: ["Next.js", "App Router", "React"],
lastUpdated: "2026-07-18",
items: [
{ command: "app/[locale]/page.tsx", description: "Main page component for the route." },
{ command: "app/layout.tsx", description: "Root layout component wrapping all pages." },
{ command: "app/loading.tsx", description: "Suspense fallback displayed while page loads." },
{ command: "app/error.tsx", description: "Client-side Error Boundary component." },
{ command: "app/api/route.ts", description: "REST API Endpoint handlers (GET, POST, PUT, DELETE)." }
]
},
{
id: "cs-3",
slug: "docker-temel-komutlar",
title: "Quick Docker & Container Command Reference",
category: "Docker & DevOps",
description: "Commands for building, running, inspecting logs, and cleaning container resources.",
tags: ["Docker", "Containers", "DevOps"],
lastUpdated: "2026-07-12",
items: [
{ command: "docker build -t app-name .", description: "Builds a Docker image from Dockerfile in current directory." },
{ command: "docker run -d -p 3000:3000 --name my-app app-name", description: "Runs container in detached mode with port forwarding." },
{ command: "docker compose up -d --build", description: "Rebuilds and launches Docker Compose services." },
{ command: "docker logs -f my-app", description: "Follows live container output logs." },
{ command: "docker system prune -af", description: "Removes all unused containers, networks, and images." }
]
}
];
+30
View File
@@ -0,0 +1,30 @@
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '@prisma/client';
const connectionString =
process.env.DATABASE_URL ||
'postgres://postgres:mBTWE2cDKGExtpktguD3HPe8y9Xr9kWFYdxV4WHQKISLLGoBAS4UdtfSCfXwvPpq@65.109.236.58:37298/postgres';
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
function getPrismaClient(): PrismaClient {
if (process.env.NODE_ENV !== 'production' && globalForPrisma.prisma) {
// Check if newly added models (e.g. category) exist on cached client
if ((globalForPrisma.prisma as any).category) {
return globalForPrisma.prisma;
}
}
const client = new PrismaClient({ adapter });
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = client;
}
return client;
}
export const db = getPrismaClient();
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+75
View File
@@ -0,0 +1,75 @@
{
"nav": {
"home": "Startseite",
"lessons": "YouTube Kurse",
"cheatsheets": "Cheatsheets",
"contact": "Kontakt",
"admin": "Dashboard",
"subscribe": "YouTube Abonnieren"
},
"hero": {
"badge": "YouTube Bildungs- und Ressourcenportal",
"title": "Schluss mit Google Docs Chaos: Alle Codes & Ressourcen hier!",
"subtitle": "Zugriff auf Quellcodes, Tabbed-Codeblöcke, Lektionsnotizen und Projekt-Downloads für meine YouTube-Tutorials an einem sauberen Ort.",
"searchPlaceholder": "Suchen Sie nach Kurstitel, Code-Snippet oder Technologie (z. B. Next.js, Python, Docker)...",
"statVideos": "YouTube Tutorials",
"statDownloads": "Downloads",
"statSubscribers": "Abonnenten",
"statSatisfaction": "Zufriedenheit"
},
"sections": {
"featured": "Neuestes YouTube Tutorial & Quellcode",
"allLessons": "Alle Video-Tutorials & Codedateien",
"whyUsTitle": "Warum diese Website statt Google Docs?",
"whyUsSubtitle": "Google Docs-Links zerstören die Codeformatierung. Hier sind die Vorteile unseres neuen Portals:",
"cheatsheetsTitle": "Beliebte Cheatsheets",
"relatedTitle": "Ähnliche Kurse"
},
"features": {
"codeTabsTitle": "Tabbed-Code-Viewer",
"codeTabsDesc": "Untersuchen Sie mehrere Projektdateien mit sauberer Syntaxhervorhebung.",
"chaptersTitle": "Zeitgestempelter YouTube-Player",
"chaptersDesc": "Springen Sie direkt zum gewünschten Thema im Video.",
"zipDownloadTitle": "Ein-Klick-Projekt-Download",
"zipDownloadDesc": "Laden Sie Starter-Templates ohne Google Drive-Berechtigungen herunter.",
"noDocsTitle": "Saubere Notizen statt unübersichtlicher Docs",
"noDocsDesc": "Strukturierte Aufzählungspunkte und hervorgehobene Zusammenfassungen."
},
"lessonDetail": {
"watchOnYoutube": "Auf YouTube ansehen",
"tabNotes": "Lektionsnotizen & Zusammenfassung",
"tabCode": "Code-Blöcke & Dateien",
"tabDownloads": "Downloads & Links",
"tabChapters": "Video-Kapitel",
"copyCode": "Code kopieren",
"codeCopied": "Kopiert!",
"downloadFile": "Datei herunterladen",
"downloadZip": "Gesamtes Projekt herunterladen (.zip)",
"githubRepo": "GitHub Repository",
"slidesPdf": "Präsentation / PDF herunterladen"
},
"contact": {
"title": "Kontakt & Themenwünsche",
"subtitle": "Kontaktieren Sie mich für Fragen zu Videos, Themenwünsche oder Sponsoring.",
"name": "Ihr Name",
"email": "Ihre E-Mail-Adresse",
"subject": "Betreff",
"message": "Ihre Nachricht oder Ihr Themenwunsch",
"send": "Nachricht senden",
"success": "Ihre Nachricht wurde erfolgreich gesendet!"
},
"admin": {
"title": "Creator Dashboard",
"subtitle": "Fügen Sie neue YouTube-Lektionen und Code-Snippets hinzu.",
"addVideo": "Neues Tutorial erstellen",
"videoTitle": "Videotitel",
"youtubeUrl": "YouTube URL",
"category": "Kategorie",
"codeSnippets": "Codeblöcke",
"save": "Speichern & Veröffentlichen"
},
"footer": {
"rights": "Alle Rechte vorbehalten.",
"createdWith": "Created by ayris.tech"
}
}
+75
View File
@@ -0,0 +1,75 @@
{
"nav": {
"home": "Home",
"lessons": "YouTube Lessons",
"cheatsheets": "Cheatsheets",
"contact": "Contact",
"admin": "Creator Dashboard",
"subscribe": "Subscribe on YouTube"
},
"hero": {
"badge": "ayris.tech YouTube Channel Portal",
"title": "Official Code & Resource Portal for ayris.tech YouTube Channel",
"subtitle": "Access code snippets, tabbed project files, lesson notes, and starter downloads for all my YouTube video tutorials in one clean location.",
"searchPlaceholder": "Search lesson title, code snippet, or technology (e.g. Next.js, Python, Docker)...",
"statVideos": "YouTube Tutorials",
"statDownloads": "Resource Downloads",
"statSubscribers": "Channel Subscribers",
"statSatisfaction": "Viewer Rating"
},
"sections": {
"featured": "Latest YouTube Tutorial & Source Code",
"allLessons": "All Video Tutorials & Code Files",
"whyUsTitle": "Everything You Need for ayris.tech Tutorials",
"whyUsSubtitle": "Built specifically for ayris.tech YouTube channel viewers to make learning and copying tutorial code effortless:",
"cheatsheetsTitle": "Featured Cheatsheets & Quick Reference",
"relatedTitle": "Related YouTube Lessons"
},
"features": {
"codeTabsTitle": "Tabbed Code Viewer",
"codeTabsDesc": "Inspect multiple project files (page.tsx, actions.ts, etc.) with clean syntax highlighting and copy support.",
"chaptersTitle": "Timestamped YouTube Player",
"chaptersDesc": "Jump directly to the specific topic inside the video with a single click.",
"zipDownloadTitle": "One-Click Project Download",
"zipDownloadDesc": "Download complete starter templates and project files directly without broken links.",
"noDocsTitle": "Structured Lesson Notes",
"noDocsDesc": "Structured bullet points, callout warnings, and formatted lesson summaries for every tutorial."
},
"lessonDetail": {
"watchOnYoutube": "Watch on YouTube",
"tabNotes": "Lesson Notes & Summary",
"tabCode": "Code Blocks & Files",
"tabDownloads": "Downloads & Links",
"tabChapters": "Video Chapters",
"copyCode": "Copy Code",
"codeCopied": "Copied!",
"downloadFile": "Download File",
"downloadZip": "Download Full Project (.zip)",
"githubRepo": "GitHub Repository",
"slidesPdf": "Download Presentation / PDF"
},
"contact": {
"title": "Contact & Video Topic Suggestions",
"subtitle": "Reach out for video questions, future tutorial requests, or business/sponsorship inquiries.",
"name": "Your Full Name",
"email": "Your Email Address",
"subject": "Subject",
"message": "Your Message or Requested Tutorial Topic",
"send": "Send Message",
"success": "Your message has been sent successfully! I will reply as soon as possible."
},
"admin": {
"title": "Creator Dashboard - Add New Lesson & Resources",
"subtitle": "Attach code blocks, snippets, and download links for your latest YouTube videos.",
"addVideo": "Create New YouTube Tutorial",
"videoTitle": "Video Title",
"youtubeUrl": "YouTube URL or Video ID",
"category": "Category",
"codeSnippets": "Code Blocks",
"save": "Save & Publish"
},
"footer": {
"rights": "All rights reserved.",
"createdWith": "Created by ayris.tech"
}
}
+75
View File
@@ -0,0 +1,75 @@
{
"nav": {
"home": "Ana Sayfa",
"lessons": "YouTube Dersleri",
"cheatsheets": "Rehberler",
"contact": "İletişim",
"admin": "Yayıncı Paneli",
"subscribe": "YouTube'da Abone Ol"
},
"hero": {
"badge": "ayris.tech YouTube Kanal Portalı",
"title": "ayris.tech YouTube Kanalı Resmi Kod ve Kaynak Portalı",
"subtitle": "YouTube eğitim videolarımızdaki kaynak kodlara, sekmeli dosyalara, ders notlarına ve proje indirmelerine tek adresten ulaşın.",
"searchPlaceholder": "Ders başlığı, kod parçacığı veya teknoloji ara (ör. Next.js, Python, Docker)...",
"statVideos": "YouTube Videosu",
"statDownloads": "Proje İndirmesi",
"statSubscribers": "Kanal Abonesi",
"statSatisfaction": "İzleyici Memnuniyeti"
},
"sections": {
"featured": "Son YouTube Dersi ve Kaynak Kodları",
"allLessons": "Tüm Video Dersler ve Kod Dosyaları",
"whyUsTitle": "ayris.tech YouTube Eğitimleri İçin Her Şey",
"whyUsSubtitle": "ayris.tech YouTube kanalı izleyicilerinin kodları kolayca incelemesi ve indirmesi için tasarlandı:",
"cheatsheetsTitle": "Öne Çıkan Rehberler ve Hızlı Komutlar",
"relatedTitle": "İlgili YouTube Dersleri"
},
"features": {
"codeTabsTitle": "Sekmeli Kod İnceleyici",
"codeTabsDesc": "Projenin tüm dosyalarını (page.tsx, actions.ts vb.) renkli sözdizimi ve tek tıkla kopyalama ile inceleyin.",
"chaptersTitle": "Zaman Damgalı YouTube Oynatıcı",
"chaptersDesc": "Tek tıkla videodaki ilgili konunun başladığı saniyeye doğrudan atlayın.",
"zipDownloadTitle": "Tek Tıkla Proje İndirme",
"zipDownloadDesc": "Tamamlanmış başlangıç şablonlarını ve proje dosyalarını kırık link olmadan indirin.",
"noDocsTitle": "Düzenli Ders Notları",
"noDocsDesc": "Her video eğitimi için özel hazırlanmış özet maddeler ve önemli ipuçları."
},
"lessonDetail": {
"watchOnYoutube": "YouTube'da İzle",
"tabNotes": "Ders Notları ve Özet",
"tabCode": "Kod Blokları ve Dosyalar",
"tabDownloads": "İndirmeler ve Linkler",
"tabChapters": "Video Bölümleri",
"copyCode": "Kodu Kopyala",
"codeCopied": "Kopyalandı!",
"downloadFile": "Dosyayı İndir",
"downloadZip": "Tüm Projeyi İndir (.zip)",
"githubRepo": "GitHub Reposu",
"slidesPdf": "Sunumu / PDF İndir"
},
"contact": {
"title": "İletişim ve Video Konusu İstekleri",
"subtitle": "Video sorularınız, gelecek eğitim istekleriniz veya sponsorluk görüşmeleri için bana ulaşın.",
"name": "Adınız Soyadınız",
"email": "E-posta Adresiniz",
"subject": "Konu",
"message": "Mesajınız veya İstediğiniz Eğitim Konusu",
"send": "Mesajı Gönder",
"success": "Mesajınız başarıyla gönderildi! En kısa sürede yanıtlayacağım."
},
"admin": {
"title": "Yayıncı Paneli - Yeni Ders & Kaynak Ekle",
"subtitle": "Son YouTube videolarınız için kod blokları ve indirme linkleri ekleyin.",
"addVideo": "Yeni YouTube Dersi Oluştur",
"videoTitle": "Video Başlığı",
"youtubeUrl": "YouTube Linki veya Video ID",
"category": "Kategori",
"codeSnippets": "Kod Blokları",
"save": "Kaydet & Yayınla"
},
"footer": {
"rights": "Tüm hakları saklıdır.",
"createdWith": "Created by ayris.tech"
}
}
+16
View File
@@ -0,0 +1,16 @@
import type { NextConfig } from 'next'
import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'res.cloudinary.com' },
{ protocol: 'https', hostname: 'images.unsplash.com' },
],
},
}
export default withNextIntl(nextConfig)
+11401
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
{
"name": "ayrisai",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@prisma/adapter-pg": "^7.9.0",
"@prisma/client": "^7.8.0",
"class-variance-authority": "^0.7.1",
"cloudinary": "^2.10.0",
"clsx": "^2.1.1",
"developer-icons": "^7.0.1",
"framer-motion": "^12.40.0",
"lucide-react": "^1.18.0",
"next": "16.2.9",
"next-auth": "^5.0.0-beta.31",
"next-intl": "^4.13.0",
"pg": "^8.22.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"shadcn": "^4.11.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pg": "^8.20.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.9",
"prisma": "^7.8.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from '@prisma/config';
export default defineConfig({
datasource: {
url: process.env.DATABASE_URL || 'postgres://postgres:mBTWE2cDKGExtpktguD3HPe8y9Xr9kWFYdxV4WHQKISLLGoBAS4UdtfSCfXwvPpq@65.109.236.58:37298/postgres',
},
});
+162
View File
@@ -0,0 +1,162 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
enum Role {
ADMIN
USER
}
model User {
id String @id @default(cuid())
name String?
email String @unique
password String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
accounts Account[]
sessions Session[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Lesson {
id String @id @default(cuid())
slug String @unique
title String
youtubeId String
youtubeUrl String
thumbnailUrl String
duration String
publishDate DateTime @default(now())
category String
tags String[]
viewsCount Int @default(0)
downloadCount Int @default(0)
likesCount Int @default(0)
summary String @db.Text
notesMarkdown String[]
isFeatured Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
codeSnippets CodeSnippet[]
downloads ResourceDownload[]
chapters VideoChapter[]
}
model CodeSnippet {
id String @id @default(cuid())
fileName String
language String
code String @db.Text
description String?
lessonId String
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
model ResourceDownload {
id String @id @default(cuid())
title String
type String // zip, github, pdf, link
url String
size String?
lessonId String
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
model VideoChapter {
id String @id @default(cuid())
time String
seconds Int
title String
lessonId String
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
model Cheatsheet {
id String @id @default(cuid())
slug String @unique
title String
category String
description String @db.Text
tags String[]
lastUpdated DateTime @default(now())
createdAt DateTime @default(now())
items CheatsheetItem[]
}
model CheatsheetItem {
id String @id @default(cuid())
command String
description String
example String?
cheatsheetId String
cheatsheet Cheatsheet @relation(fields: [cheatsheetId], references: [id], onDelete: Cascade)
}
model ContactMessage {
id String @id @default(cuid())
name String
email String
subject String
message String @db.Text
isRead Boolean @default(false)
createdAt DateTime @default(now())
}
model Setting {
id String @id @default(cuid())
key String @unique
value String @db.Text
updatedAt DateTime @updatedAt
}
+254
View File
@@ -0,0 +1,254 @@
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '@prisma/client';
const connectionString = process.env.DATABASE_URL || 'postgres://postgres:mBTWE2cDKGExtpktguD3HPe8y9Xr9kWFYdxV4WHQKISLLGoBAS4UdtfSCfXwvPpq@65.109.236.58:37298/postgres';
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const db = new PrismaClient({ adapter });
async function main() {
console.log('Seeding PostgreSQL database...');
// Clear existing records
await db.videoChapter.deleteMany();
await db.resourceDownload.deleteMany();
await db.codeSnippet.deleteMany();
await db.lesson.deleteMany();
await db.cheatsheetItem.deleteMany();
await db.cheatsheet.deleteMany();
// Create Lessons
await db.lesson.create({
data: {
slug: 'nextjs-16-app-router-full-course',
title: 'Next.js 16 App Router & Server Actions Complete Masterclass (Code & Project)',
youtubeId: 'dQw4w9WgXcQ',
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
thumbnailUrl: 'https://images.unsplash.com/photo-1618401471353-b98afee0b2eb?w=800&auto=format&fit=crop&q=80',
duration: '42:15',
category: 'Next.js',
tags: ['Next.js 16', 'App Router', 'Server Actions', 'TypeScript', 'Tailwind CSS v4'],
viewsCount: 24500,
downloadCount: 4120,
likesCount: 1890,
isFeatured: true,
summary: 'In this tutorial, we build a full-stack Next.js 16 application from scratch covering App Router architecture, proxy.ts middleware, Server Actions, and PostgreSQL integration.',
notesMarkdown: [
'In Next.js 16, using proxy.ts is recommended over deprecated middleware.ts.',
'Server Actions functions are declared with "use server" directive and can be invoked directly from client components.',
'Data fetching revalidation uses revalidatePath or revalidateTag helpers.',
'Form handling UX is significantly improved using useActionState and useFormStatus hooks.'
],
codeSnippets: {
create: [
{
fileName: 'app/actions.ts',
language: 'typescript',
description: 'Server Action function handling form submission',
code: `'use server'\n\nimport { revalidatePath } from 'next/cache';\n\nexport async function submitProjectIdea(formData: FormData) {\n const title = formData.get('title') as string;\n const description = formData.get('description') as string;\n\n if (!title || title.length < 3) {\n return { success: false, error: 'Title must be at least 3 characters long.' };\n }\n\n // Database insertion simulation\n console.log('Saved Idea:', { title, description });\n\n revalidatePath('/ideas');\n return { success: true, message: 'Your idea was submitted successfully!' };\n}`
},
{
fileName: 'proxy.ts',
language: 'typescript',
description: 'Next.js 16 proxy configuration (middleware alternative)',
code: `import { NextRequest, NextResponse } from 'next/server';\nimport createMiddleware from 'next-intl/middleware';\nimport { routing } from '@/i18n/routing';\n\nconst intlMiddleware = createMiddleware(routing);\n\nexport async function proxy(request: NextRequest) {\n if (request.nextUrl.pathname.includes('/admin')) {\n // Auth check logic\n }\n return intlMiddleware(request);\n}\n\nexport const config = {\n matcher: ['/((?!api|_next|_vercel|.*\\\\..*).*)']\n};`
},
{
fileName: 'components/IdeaForm.tsx',
language: 'tsx',
description: 'Client Form component using useActionState',
code: `'use client';\n\nimport { useActionState } from 'react';\nimport { submitProjectIdea } from '@/app/actions';\n\nexport function IdeaForm() {\n const [state, formAction, isPending] = useActionState(submitProjectIdea, null);\n\n return (\n <form action={formAction} className="space-y-4 max-w-md p-6 bg-slate-900 rounded-xl border border-slate-800">\n <h3 className="text-xl font-bold text-white">Add New Idea</h3>\n \n <div>\n <label className="block text-sm text-slate-400 mb-1">Idea Title</label>\n <input name="title" required className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white" />\n </div>\n\n <button disabled={isPending} className="w-full py-2.5 bg-red-600 hover:bg-red-500 text-white rounded-lg font-medium">\n {isPending ? 'Submitting...' : 'Share Idea'}\n </button>\n\n {state?.error && <p className="text-red-400 text-sm">{state.error}</p>}\n {state?.success && <p className="text-emerald-400 text-sm">{state.message}</p>}\n </form>\n );\n}`
}
]
},
downloads: {
create: [
{
title: 'Full Project Source Code (Starter Pack)',
type: 'zip',
url: 'https://github.com/ayrisdev/nextjs-16-starter/archive/refs/heads/main.zip',
size: '4.2 MB'
},
{
title: 'GitHub Repository',
type: 'github',
url: 'https://github.com/ayrisdev/nextjs-16-starter'
},
{
title: 'Lesson Notes & Architecture (PDF Document)',
type: 'pdf',
url: '#',
size: '1.8 MB'
}
]
},
chapters: {
create: [
{ time: '00:00', seconds: 0, title: 'Introduction & Demo' },
{ time: '04:15', seconds: 255, title: 'Next.js 16 Project Setup & Structure' },
{ time: '11:30', seconds: 690, title: 'Routing & Middleware with proxy.ts' },
{ time: '21:00', seconds: 1260, title: 'Form Handling via Server Actions' },
{ time: '32:45', seconds: 1965, title: 'PostgreSQL & Prisma Integration' },
{ time: '40:00', seconds: 2400, title: 'Deployment & Summary' }
]
}
}
});
await db.lesson.create({
data: {
slug: 'python-ai-agent-automation',
title: 'Building Autonomous AI Agents with Python (CrewAI & Gemini API)',
youtubeId: 'dQw4w9WgXcQ',
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
thumbnailUrl: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&auto=format&fit=crop&q=80',
duration: '28:50',
category: 'Python & AI',
tags: ['Python', 'CrewAI', 'Gemini API', 'AI Agents', 'Automation'],
viewsCount: 18200,
downloadCount: 3200,
likesCount: 1450,
summary: 'Build an autonomous AI agent in Python that crawls web data, summarizes tech news, and generates structured reports automatically.',
notesMarkdown: [
'🤖 AI agents are configured with role, goal, and backstory.',
'🤖 Equip agents with real-time web search using Serper API or Tavily.',
'🤖 Gemini 1.5 Flash provides high token throughput with minimal latency.'
],
codeSnippets: {
create: [
{
fileName: 'agent.py',
language: 'python',
description: 'CrewAI Agent Definition',
code: `from crewai import Agent, Task, Crew, Process\nfrom langchain_google_genai import ChatGoogleGenerativeAI\nimport os\n\nllm = ChatGoogleGenerativeAI(\n model="gemini-1.5-flash",\n google_api_key=os.getenv("GEMINI_API_KEY")\n)\n\nresearcher = Agent(\n role='Tech Researcher',\n goal='Discover top AI trends from the past 24 hours',\n backstory='You are an expert technology journalist following AI developments.',\n verbose=True,\n llm=llm\n)\n\ntask1 = Task(\n description='Summarize 2026 AI trends in 5 bullet points.',\n expected_output='A markdown list with 5 key takeaways.',\n agent=researcher\n)\n\ncrew = Crew(\n agents=[researcher],\n tasks=[task1],\n process=Process.sequential\n)\n\nresult = crew.kickoff()\nprint(result)`
}
]
},
downloads: {
create: [
{
title: 'Python AI Agent Project Files',
type: 'zip',
url: '#',
size: '2.1 MB'
},
{
title: 'GitHub Repository',
type: 'github',
url: 'https://github.com/ayrisdev/python-ai-agent'
}
]
},
chapters: {
create: [
{ time: '00:00', seconds: 0, title: 'Introduction & What is an AI Agent?' },
{ time: '05:20', seconds: 320, title: 'Installing Dependencies (CrewAI & Gemini)' },
{ time: '14:10', seconds: 850, title: 'Agent & Task Coding' },
{ time: '24:00', seconds: 1440, title: 'Testing & Output Review' }
]
}
}
});
await db.lesson.create({
data: {
slug: 'tailwind-css-v4-tricks-and-setup',
title: 'Modern UI Design with Tailwind CSS v4: Tricks, Themes & Components',
youtubeId: 'dQw4w9WgXcQ',
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
thumbnailUrl: 'https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?w=800&auto=format&fit=crop&q=80',
duration: '35:10',
category: 'Tailwind CSS',
tags: ['Tailwind CSS v4', 'OKLCH', 'CSS Grid', 'UI Design', 'Shadcn'],
viewsCount: 31000,
downloadCount: 5400,
likesCount: 2600,
summary: 'Learn CSS-first configuration in Tailwind CSS v4 using @import "tailwindcss";, OKLCH color spaces, and advanced glassmorphism component design.',
notesMarkdown: [
'🎨 In Tailwind v4, @theme directive in globals.css replaces tailwind.config.js.',
'🎨 OKLCH color space ensures consistent perceived contrast across light and dark modes.',
'🎨 @custom-variant dark provides flexible dark mode scoping.'
],
codeSnippets: {
create: [
{
fileName: 'globals.css',
language: 'css',
description: 'Tailwind CSS v4 Theme Configuration',
code: `@import "tailwindcss";\n\n@theme inline {\n --color-primary: oklch(0.65 0.22 260);\n --color-accent: oklch(0.72 0.19 145);\n --radius-lg: 1rem;\n}\n\n:root {\n --background: oklch(0.98 0.01 250);\n --foreground: oklch(0.15 0.02 250);\n}`
}
]
},
downloads: {
create: [
{
title: 'Tailwind v4 UI Starter Pack',
type: 'zip',
url: '#',
size: '1.2 MB'
}
]
},
chapters: {
create: [
{ time: '00:00', seconds: 0, title: "What's New in Tailwind v4" },
{ time: '08:30', seconds: 510, title: 'Defining Color Palettes with @theme' },
{ time: '20:15', seconds: 1215, title: 'Building Glassmorphism Cards' }
]
}
}
});
// Create Cheatsheets
await db.cheatsheet.create({
data: {
slug: 'git-komutlari-hizli-rehber',
title: 'Essential Git & GitHub Commands Cheatsheet',
category: 'DevOps & Tooling',
description: 'Handy reference for daily Git workflow, branching, and conflict resolution commands.',
tags: ['Git', 'GitHub', 'Terminal'],
items: {
create: [
{ command: 'git checkout -b feature/new-feature', description: 'Creates a new branch and switches to it.' },
{ command: 'git status', description: 'Lists modified and untracked files.' },
{ command: 'git commit -m "feat: add new page"', description: 'Saves changes with a descriptive commit message.' },
{ command: 'git push origin feature/new-feature', description: 'Pushes the branch to remote repository.' },
{ command: 'git log --oneline -n 5', description: 'Displays last 5 commits in clean single line format.' }
]
}
}
});
await db.cheatsheet.create({
data: {
slug: 'nextjs-app-router-cheatsheet',
title: 'Next.js App Router File Structure & Route Handlers',
category: 'Next.js',
description: 'Special file conventions (page, layout, loading, error) and their responsibilities.',
tags: ['Next.js', 'App Router', 'React'],
items: {
create: [
{ command: 'app/[locale]/page.tsx', description: 'Main page component for the route.' },
{ command: 'app/layout.tsx', description: 'Root layout component wrapping all pages.' },
{ command: 'app/loading.tsx', description: 'Suspense fallback displayed while page loads.' },
{ command: 'app/error.tsx', description: 'Client-side Error Boundary component.' },
{ command: 'app/api/route.ts', description: 'REST API Endpoint handlers (GET, POST, PUT, DELETE).' }
]
}
}
});
console.log('Seeding completed successfully!');
}
main()
.catch((e) => {
console.error('Seed error:', e);
process.exit(1);
})
.finally(async () => {
await db.$disconnect();
await pool.end();
});
+20
View File
@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server'
import createMiddleware from 'next-intl/middleware'
import { auth } from '@/lib/auth'
import { routing } from '@/i18n/routing'
const intlMiddleware = createMiddleware(routing)
export async function proxy(request: NextRequest) {
if (request.nextUrl.pathname.includes('/admin')) {
const session = await auth()
if (!session || (session.user as any)?.role !== 'ADMIN') {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return intlMiddleware(request)
}
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
}
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}