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' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user