first commit
This commit is contained in:
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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'
|
||||
}
|
||||
})
|
||||
@@ -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
@@ -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." }
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -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();
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user