feat: add AI Prompts section, database model, public pages, and admin CRUD

This commit is contained in:
AyrisAI
2026-07-23 17:38:06 +03:00
parent bd543c3bda
commit bf4e0da112
8 changed files with 762 additions and 7 deletions
+259 -6
View File
@@ -24,6 +24,12 @@ import {
updateCheatsheet,
deleteCheatsheet
} from '@/lib/actions/cheatsheetActions';
import {
getPrompts,
createPrompt,
updatePrompt,
deletePrompt
} from '@/lib/actions/promptActions';
import {
ShieldAlert,
Plus,
@@ -48,7 +54,8 @@ import {
Link as LinkIcon,
FolderTree,
Tag,
Terminal
Terminal,
Sparkles
} from 'lucide-react';
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
@@ -89,7 +96,7 @@ function parseNotesText(text: string) {
export default function AdminPage() {
const t = useTranslations('admin');
const [activeTab, setActiveTab] = useState<'dashboard' | 'lessons' | 'cheatsheets' | 'categories' | 'users' | 'settings' | 'messages'>('dashboard');
const [activeTab, setActiveTab] = useState<'dashboard' | 'lessons' | 'cheatsheets' | 'prompts' | 'categories' | 'users' | 'settings' | 'messages'>('dashboard');
// Stats
const [stats, setStats] = useState({ totalLessons: 0, totalViews: 0, totalDownloads: 0, totalMessages: 0 });
@@ -117,6 +124,17 @@ export default function AdminPage() {
]);
const [savingCheat, setSavingCheat] = useState(false);
// Prompts State
const [promptsList, setPromptsList] = useState<any[]>([]);
const [isPromptFormOpen, setIsPromptFormOpen] = useState(false);
const [editingPromptId, setEditingPromptId] = useState<string | null>(null);
const [promptTitle, setPromptTitle] = useState('');
const [promptCategory, setPromptCategory] = useState('System Architecture');
const [promptDesc, setPromptDesc] = useState('');
const [promptContent, setPromptContent] = useState('');
const [promptTagsText, setPromptTagsText] = useState('Next.js, System Prompt');
const [savingPrompt, setSavingPrompt] = useState(false);
// Lesson Form Modal State (Create or Edit)
const [isFormOpen, setIsFormOpen] = useState(false);
const [editingLessonId, setEditingLessonId] = useState<string | null>(null);
@@ -186,6 +204,9 @@ export default function AdminPage() {
} else if (activeTab === 'cheatsheets') {
const sheets = await getCheatsheets();
setCheatsheetsList(sheets);
} else if (activeTab === 'prompts') {
const prs = await getPrompts();
setPromptsList(prs);
} else if (activeTab === 'categories') {
const cats = await getCategories();
setCategoriesList(cats);
@@ -330,6 +351,74 @@ export default function AdminPage() {
}
};
// Prompt Handlers
const handleOpenPromptCreate = () => {
setEditingPromptId(null);
setPromptTitle('');
setPromptCategory('System Architecture');
setPromptDesc('');
setPromptContent('');
setPromptTagsText('Next.js, System Prompt');
setIsPromptFormOpen(true);
};
const handleOpenPromptEdit = (prompt: any) => {
setEditingPromptId(prompt.id);
setPromptTitle(prompt.title);
setPromptCategory(prompt.category);
setPromptDesc(prompt.description);
setPromptContent(prompt.content);
setPromptTagsText(prompt.tags ? prompt.tags.join(', ') : '');
setIsPromptFormOpen(true);
};
const handleSavePrompt = async (e: React.FormEvent) => {
e.preventDefault();
setSavingPrompt(true);
const tags = promptTagsText.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
let res;
if (editingPromptId) {
res = await updatePrompt(editingPromptId, {
title: promptTitle,
category: promptCategory,
description: promptDesc,
content: promptContent,
tags,
});
} else {
res = await createPrompt({
title: promptTitle,
category: promptCategory,
description: promptDesc,
content: promptContent,
tags,
});
}
setSavingPrompt(false);
if (res.success) {
showAlert('success', editingPromptId ? 'Prompt updated successfully!' : 'New prompt created successfully!');
setIsPromptFormOpen(false);
getPrompts().then(setPromptsList);
} else {
showAlert('error', res.error || 'Failed to save prompt');
}
};
const handleDeletePrompt = async (id: string, title: string) => {
if (!confirm(`Are you sure you want to delete prompt "${title}"?`)) return;
const res = await deletePrompt(id);
if (res.success) {
showAlert('success', `Prompt "${title}" deleted`);
getPrompts().then(setPromptsList);
} else {
showAlert('error', res.error || 'Failed to delete prompt');
}
};
// Cheatsheet Handlers
const handleOpenCheatCreate = () => {
setEditingCheatId(null);
@@ -526,6 +615,18 @@ export default function AdminPage() {
<span>Cheatsheets</span>
</button>
<button
onClick={() => setActiveTab('prompts')}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
activeTab === 'prompts'
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
: 'text-slate-400 hover:text-white hover:bg-slate-800/50'
}`}
>
<Sparkles className="w-4 h-4" />
<span>AI Prompts</span>
</button>
<button
onClick={() => setActiveTab('categories')}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
@@ -806,7 +907,62 @@ export default function AdminPage() {
</div>
)}
{/* TAB 4: CATEGORIES CRUD */}
{/* TAB 4: PROMPTS CRUD */}
{activeTab === 'prompts' && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-extrabold text-white">AI Prompts & Templates ({promptsList.length})</h2>
<button
onClick={handleOpenPromptCreate}
className="px-5 py-2.5 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"
>
<Plus className="w-4 h-4" />
<span>Create New AI Prompt</span>
</button>
</div>
<div className="space-y-4">
{promptsList.map((prompt) => (
<div key={prompt.id} className="p-5 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-3">
<div className="flex items-center justify-between gap-4">
<div>
<span className="text-[10px] font-bold text-red-400 bg-red-500/10 px-2 py-0.5 rounded border border-red-500/20">
{prompt.category}
</span>
<h3 className="font-bold text-white text-base mt-1">{prompt.title}</h3>
<p className="text-xs text-slate-400">{prompt.description}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleOpenPromptEdit(prompt)}
className="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-bold text-sky-400 flex items-center gap-1"
>
<Edit className="w-3.5 h-3.5" />
<span>Edit</span>
</button>
<button
onClick={() => handleDeletePrompt(prompt.id, prompt.title)}
className="p-1.5 rounded-xl bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white transition-colors"
title="Delete Prompt"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="p-3 rounded-xl bg-slate-950 border border-slate-800 text-xs font-mono text-slate-300 line-clamp-2">
{prompt.content}
</div>
</div>
))}
</div>
</div>
)}
{/* TAB 5: CATEGORIES CRUD */}
{activeTab === 'categories' && (
<div className="space-y-6">
@@ -899,7 +1055,7 @@ export default function AdminPage() {
</div>
)}
{/* TAB 5: USERS / ADMINS */}
{/* TAB 6: USERS / ADMINS */}
{activeTab === 'users' && (
<div className="space-y-6">
{/* Create User Form */}
@@ -971,7 +1127,7 @@ export default function AdminPage() {
</div>
)}
{/* TAB 6: SETTINGS */}
{/* TAB 7: SETTINGS */}
{activeTab === 'settings' && (
<form onSubmit={handleSaveSettings} className="p-6 sm:p-8 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-6">
<h2 className="text-lg font-extrabold text-white flex items-center gap-2 border-b border-slate-800 pb-4">
@@ -1033,7 +1189,7 @@ export default function AdminPage() {
</form>
)}
{/* TAB 7: MESSAGES */}
{/* TAB 8: MESSAGES */}
{activeTab === 'messages' && (
<div className="space-y-4">
<h2 className="text-lg font-bold text-white">Viewer Contact Messages ({messages.length})</h2>
@@ -1063,6 +1219,103 @@ export default function AdminPage() {
</main>
{/* CREATE / EDIT PROMPT FORM MODAL */}
{isPromptFormOpen && (
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 overflow-y-auto">
<div className="w-full max-w-2xl bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-2xl my-8">
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Sparkles className="w-5 h-5 text-red-500" />
<span>{editingPromptId ? 'Edit AI Prompt' : 'Create New AI Prompt'}</span>
</h2>
<button onClick={() => setIsPromptFormOpen(false)} className="p-1.5 rounded-lg bg-slate-800 text-slate-400 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSavePrompt} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">Title</label>
<input
type="text"
required
value={promptTitle}
onChange={(e) => setPromptTitle(e.target.value)}
placeholder="Full-Stack Next.js Architect"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">Category</label>
<input
type="text"
required
value={promptCategory}
onChange={(e) => setPromptCategory(e.target.value)}
placeholder="System Architecture / UI Design"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
/>
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">Short Description</label>
<textarea
rows={2}
required
value={promptDesc}
onChange={(e) => setPromptDesc(e.target.value)}
placeholder="Overview of what this system prompt does..."
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">System Prompt Content</label>
<textarea
rows={6}
required
value={promptContent}
onChange={(e) => setPromptContent(e.target.value)}
placeholder="You are a Senior Next.js 16 Architect..."
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs font-mono text-slate-200 focus:outline-none"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">Tags (Comma separated)</label>
<input
type="text"
value={promptTagsText}
onChange={(e) => setPromptTagsText(e.target.value)}
placeholder="Next.js, System Prompt, AI"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-xs text-white focus:outline-none"
/>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-slate-800">
<button
type="button"
onClick={() => setIsPromptFormOpen(false)}
className="px-4 py-2 rounded-xl bg-slate-800 text-xs font-bold text-slate-300"
>
Cancel
</button>
<button
type="submit"
disabled={savingPrompt}
className="px-5 py-2 bg-red-600 hover:bg-red-500 text-white text-xs font-bold rounded-xl flex items-center gap-2"
>
{savingPrompt ? 'Saving...' : 'Save Prompt'}
</button>
</div>
</form>
</div>
</div>
)}
{/* CREATE / EDIT CHEATSHEET FORM MODAL */}
{isCheatFormOpen && (
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 overflow-y-auto">
+152
View File
@@ -0,0 +1,152 @@
'use client';
import { useState, useEffect, use } from 'react';
import Link from 'next/link';
import { getPromptBySlug } from '@/lib/actions/promptActions';
import { ArrowLeft, Copy, Check, Sparkles, Tag, Share2 } from 'lucide-react';
export default function PromptDetailPage({ params }: { params: Promise<{ locale: string; slug: string }> }) {
const { locale, slug } = use(params);
const [prompt, setPrompt] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [copied, setCopied] = useState(false);
const [copiedLink, setCopiedLink] = useState(false);
useEffect(() => {
async function loadData() {
setLoading(true);
const res = await getPromptBySlug(slug);
setPrompt(res);
setLoading(false);
}
loadData();
}, [slug]);
const handleCopyPrompt = async () => {
if (!prompt) return;
try {
await navigator.clipboard.writeText(prompt.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Copy error:', err);
}
};
const handleShare = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
setCopiedLink(true);
setTimeout(() => setCopiedLink(false), 2000);
} catch (err) {
console.error('Share error:', err);
}
};
if (loading) {
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">
<p className="text-sm font-semibold">Loading prompt...</p>
</div>
);
}
if (!prompt) {
return (
<div className="max-w-7xl mx-auto px-4 py-24 text-center space-y-4">
<h1 className="text-2xl font-bold text-white">Prompt Not Found</h1>
<Link href={`/${locale}/prompts`} className="text-xs font-bold text-red-400 hover:underline">
Back to Prompts Library
</Link>
</div>
);
}
return (
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8 flex-1 w-full">
{/* Navigation & Share Bar */}
<div className="flex items-center justify-between gap-4">
<Link
href={`/${locale}/prompts`}
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-300 text-xs font-bold border border-slate-800 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span>Back to Prompts</span>
</Link>
<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 Prompt</span>
</>
)}
</button>
</div>
{/* Header Banner */}
<div className="p-8 rounded-3xl bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900 border border-slate-800 space-y-4 shadow-2xl">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-bold text-red-400 bg-red-500/10 px-3 py-1 rounded-full border border-red-500/20">
{prompt.category}
</span>
{prompt.tags?.map((t: string) => (
<span key={t} className="text-[11px] text-slate-400 px-2.5 py-0.5 rounded-full bg-slate-800 border border-slate-700">
#{t}
</span>
))}
</div>
<h1 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-snug">
{prompt.title}
</h1>
<p className="text-sm text-slate-400 max-w-3xl leading-relaxed">
{prompt.description}
</p>
</div>
{/* Full Prompt Container */}
<div className="p-6 sm:p-8 rounded-3xl bg-slate-900/60 border border-slate-800 space-y-4 shadow-2xl">
<div className="flex items-center justify-between">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<Sparkles className="w-4 h-4 text-red-400" />
<span>AI System Prompt Content</span>
</h2>
<button
onClick={handleCopyPrompt}
className="px-4 py-2 bg-red-600 hover:bg-red-500 text-white text-xs font-bold rounded-xl flex items-center gap-2 shadow-lg shadow-red-600/30 transition-all"
>
{copied ? (
<>
<Check className="w-4 h-4 text-emerald-300" />
<span>Copied to Clipboard!</span>
</>
) : (
<>
<Copy className="w-4 h-4" />
<span>Copy Full Prompt</span>
</>
)}
</button>
</div>
<div className="p-6 bg-slate-950 rounded-2xl border border-slate-800 font-mono text-xs text-slate-200 leading-relaxed overflow-x-auto">
<pre className="whitespace-pre-wrap">{prompt.content}</pre>
</div>
</div>
</div>
);
}
+177
View File
@@ -0,0 +1,177 @@
'use client';
import { useState, useEffect, use } from 'react';
import Link from 'next/link';
import { getPrompts } from '@/lib/actions/promptActions';
import { Search, Sparkles, Copy, Check, Terminal, ArrowRight, Tag, BookOpen } from 'lucide-react';
export default function PromptsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = use(params);
const [search, setSearch] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [prompts, setPrompts] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [copiedId, setCopiedId] = useState<string | null>(null);
useEffect(() => {
async function loadData() {
setLoading(true);
const res = await getPrompts({ query: search, category: selectedCategory || undefined });
setPrompts(res);
setLoading(false);
}
loadData();
}, [search, selectedCategory]);
const handleCopy = async (id: string, content: string) => {
try {
await navigator.clipboard.writeText(content);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
} catch (err) {
console.error('Failed to copy prompt:', err);
}
};
const categories = Array.from(new Set(prompts.map((p) => p.category)));
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">
<Sparkles className="w-4 h-4" />
<span>Curated AI System Prompts & Templates</span>
</div>
<h1 className="text-3xl sm:text-5xl font-black text-white tracking-tight">
AI Prompts & Agent Templates Library
</h1>
<p className="text-sm text-slate-400 max-w-2xl">
Battle-tested system prompts, coding instructions, and LLM templates for ChatGPT, Claude, and Antigravity agents.
</p>
</div>
{/* Search & 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 AI prompts, keywords, or system roles..."
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 Filter Pills */}
<div className="flex items-center gap-2 overflow-x-auto w-full md:w-auto py-1 scrollbar-none">
<button
onClick={() => setSelectedCategory(null)}
className={`px-3 py-1.5 rounded-xl text-xs font-bold whitespace-nowrap transition-all ${
selectedCategory === 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 Prompts
</button>
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(selectedCategory === cat ? null : cat)}
className={`px-3 py-1.5 rounded-xl text-xs font-bold whitespace-nowrap transition-all ${
selectedCategory === cat
? '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}
</button>
))}
</div>
</div>
{/* Prompts Grid */}
{loading ? (
<div className="p-12 text-center text-slate-400 flex items-center justify-center gap-2">
<span>Loading prompts...</span>
</div>
) : prompts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{prompts.map((item) => (
<div
key={item.id}
className="p-6 rounded-3xl bg-slate-900/60 border border-slate-800 hover:border-slate-700 transition-all flex flex-col justify-between space-y-4 shadow-xl group"
>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-red-400 bg-red-500/10 px-2.5 py-0.5 rounded-full border border-red-500/20">
{item.category}
</span>
<div className="flex items-center gap-1.5 text-[10px] text-slate-500 font-mono">
<Tag className="w-3 h-3 text-slate-400" />
<span>{item.tags?.join(', ')}</span>
</div>
</div>
<h3 className="font-bold text-lg text-white group-hover:text-red-400 transition-colors">
<Link href={`/${locale}/prompts/${item.slug}`}>
{item.title}
</Link>
</h3>
<p className="text-xs text-slate-400 line-clamp-2 leading-relaxed">
{item.description}
</p>
{/* Prompt Code Preview Box */}
<div className="p-4 bg-slate-950 rounded-2xl border border-slate-800 text-xs font-mono text-slate-300 relative group/code overflow-hidden">
<pre className="whitespace-pre-wrap line-clamp-3 leading-relaxed">
{item.content}
</pre>
<button
onClick={() => handleCopy(item.id, item.content)}
className="absolute top-2 right-2 px-2.5 py-1 rounded-lg bg-slate-900 border border-slate-800 text-[11px] font-bold text-slate-300 hover:text-white flex items-center gap-1 shadow-md transition-colors"
>
{copiedId === item.id ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-emerald-400">Copied!</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5 text-slate-400" />
<span>Copy</span>
</>
)}
</button>
</div>
</div>
<div className="pt-2 flex items-center justify-between">
<Link
href={`/${locale}/prompts/${item.slug}`}
className="inline-flex items-center gap-1.5 text-xs font-bold text-red-400 hover:text-red-300 transition-colors"
>
<span>View Full Prompt & Usage</span>
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
))}
</div>
) : (
<div className="p-12 text-center bg-slate-900/40 rounded-2xl border border-slate-800 text-slate-400 text-xs">
No prompts found matching your search.
</div>
)}
</div>
);
}
+1
View File
@@ -57,6 +57,7 @@ export function Footer({ locale }: { locale: string }) {
<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}/prompts`} className="hover:text-red-400 transition-colors">AI Prompts & Templates</Link></li>
<li><Link href={`/${locale}/contact`} className="hover:text-red-400 transition-colors">Contact & Suggestions</Link></li>
</ul>
</div>
+2 -1
View File
@@ -3,7 +3,7 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Code2, BookOpen, Mail, Sparkles } from 'lucide-react';
import { Code2, BookOpen, Mail, Sparkles, Terminal } from 'lucide-react';
export function Navbar({ locale }: { locale: string }) {
const t = useTranslations('nav');
@@ -13,6 +13,7 @@ export function Navbar({ locale }: { locale: string }) {
{ 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}/prompts`, label: 'Prompts', icon: Terminal },
{ href: `/${locale}/contact`, label: t('contact'), icon: Mail },
];
+133
View File
@@ -0,0 +1,133 @@
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function getPrompts(params?: { category?: string; query?: string }) {
try {
const where: any = {};
if (params?.category) {
where.category = params.category;
}
if (params?.query && params.query.trim()) {
const q = params.query.trim();
where.OR = [
{ title: { contains: q, mode: 'insensitive' } },
{ description: { contains: q, mode: 'insensitive' } },
{ content: { contains: q, mode: 'insensitive' } },
{ tags: { hasSome: [q] } },
];
}
const prompts = await db.prompt.findMany({
where,
orderBy: {
createdAt: 'desc',
},
});
return prompts;
} catch (error) {
console.error('Error fetching prompts:', error);
return [];
}
}
export async function getPromptBySlug(slug: string) {
try {
const prompt = await db.prompt.findUnique({
where: { slug },
});
return prompt;
} catch (error) {
console.error('Error fetching prompt by slug:', error);
return null;
}
}
export async function createPrompt(data: {
title: string;
category: string;
description: string;
content: string;
tags?: string[];
}) {
try {
const slug = data.title
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-') + '-' + Date.now().toString().slice(-4);
const tags = data.tags && data.tags.length > 0 ? data.tags : [data.category, 'AI', 'Prompt'];
const newPrompt = await db.prompt.create({
data: {
title: data.title,
slug,
category: data.category,
description: data.description,
content: data.content,
tags,
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/prompts');
revalidatePath('/[locale]/admin');
return { success: true, prompt: newPrompt };
} catch (error: any) {
console.error('Error creating prompt:', error);
return { success: false, error: error.message || 'Failed to create prompt' };
}
}
export async function updatePrompt(id: string, data: {
title: string;
category: string;
description: string;
content: string;
tags?: string[];
}) {
try {
const tags = data.tags && data.tags.length > 0 ? data.tags : [data.category, 'AI', 'Prompt'];
const updated = await db.prompt.update({
where: { id },
data: {
title: data.title,
category: data.category,
description: data.description,
content: data.content,
tags,
updatedAt: new Date(),
},
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/prompts');
revalidatePath('/[locale]/admin');
return { success: true, prompt: updated };
} catch (error: any) {
console.error('Error updating prompt:', error);
return { success: false, error: error.message || 'Failed to update prompt' };
}
}
export async function deletePrompt(id: string) {
try {
await db.prompt.delete({
where: { id },
});
revalidatePath('/[locale]');
revalidatePath('/[locale]/prompts');
revalidatePath('/[locale]/admin');
return { success: true };
} catch (error: any) {
console.error('Error deleting prompt:', error);
return { success: false, error: error.message || 'Failed to delete prompt' };
}
}
+12
View File
@@ -160,3 +160,15 @@ model Setting {
value String @db.Text
updatedAt DateTime @updatedAt
}
model Prompt {
id String @id @default(cuid())
title String
slug String @unique
category String
description String @db.Text
content String @db.Text
tags String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+26
View File
@@ -240,6 +240,32 @@ async function main() {
}
});
// Clear existing prompts
await db.prompt.deleteMany();
// Create Seed Prompts
await db.prompt.create({
data: {
slug: 'fullstack-nextjs-architect-prompt',
title: 'Full-Stack Next.js 16 System Architect Prompt',
category: 'System Architecture',
description: 'System prompt to instruct AI agents to build production-ready Next.js 16 App Router code with Server Actions and Prisma.',
content: `You are a Senior Next.js 16 Architect. Always use TypeScript strict mode, App Router, proxy.ts for middleware, Server Actions with Zod validation, and Prisma ORM. Never write client components unless interactivity (onClick, useState) is required.`,
tags: ['Next.js', 'System Prompt', 'AI Architecture'],
}
});
await db.prompt.create({
data: {
slug: 'ui-ux-design-engineer-prompt',
title: 'Dark Mode Glassmorphism UI/UX Designer Prompt',
category: 'UI & Frontend',
description: 'Instructs AI to design modern dark-mode interfaces with Tailwind CSS v4, subtle glows, and micro-animations.',
content: `Act as a Lead UI/UX Engineer specializing in sleek dark mode interfaces. Use HSL/OKLCH color palettes, slate-950 backgrounds, red-500/10 glows, rounded-2xl containers, and Framer Motion micro-interactions. Avoid generic colors or white backgrounds.`,
tags: ['UI/UX', 'Tailwind CSS', 'Design System'],
}
});
console.log('Seeding completed successfully!');
}