feat: add AI Prompts section, database model, public pages, and admin CRUD
This commit is contained in:
+259
-6
@@ -24,6 +24,12 @@ import {
|
|||||||
updateCheatsheet,
|
updateCheatsheet,
|
||||||
deleteCheatsheet
|
deleteCheatsheet
|
||||||
} from '@/lib/actions/cheatsheetActions';
|
} from '@/lib/actions/cheatsheetActions';
|
||||||
|
import {
|
||||||
|
getPrompts,
|
||||||
|
createPrompt,
|
||||||
|
updatePrompt,
|
||||||
|
deletePrompt
|
||||||
|
} from '@/lib/actions/promptActions';
|
||||||
import {
|
import {
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -48,7 +54,8 @@ import {
|
|||||||
Link as LinkIcon,
|
Link as LinkIcon,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
Tag,
|
Tag,
|
||||||
Terminal
|
Terminal,
|
||||||
|
Sparkles
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
||||||
|
|
||||||
@@ -89,7 +96,7 @@ function parseNotesText(text: string) {
|
|||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const t = useTranslations('admin');
|
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
|
// Stats
|
||||||
const [stats, setStats] = useState({ totalLessons: 0, totalViews: 0, totalDownloads: 0, totalMessages: 0 });
|
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);
|
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)
|
// Lesson Form Modal State (Create or Edit)
|
||||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||||
const [editingLessonId, setEditingLessonId] = useState<string | null>(null);
|
const [editingLessonId, setEditingLessonId] = useState<string | null>(null);
|
||||||
@@ -186,6 +204,9 @@ export default function AdminPage() {
|
|||||||
} else if (activeTab === 'cheatsheets') {
|
} else if (activeTab === 'cheatsheets') {
|
||||||
const sheets = await getCheatsheets();
|
const sheets = await getCheatsheets();
|
||||||
setCheatsheetsList(sheets);
|
setCheatsheetsList(sheets);
|
||||||
|
} else if (activeTab === 'prompts') {
|
||||||
|
const prs = await getPrompts();
|
||||||
|
setPromptsList(prs);
|
||||||
} else if (activeTab === 'categories') {
|
} else if (activeTab === 'categories') {
|
||||||
const cats = await getCategories();
|
const cats = await getCategories();
|
||||||
setCategoriesList(cats);
|
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
|
// Cheatsheet Handlers
|
||||||
const handleOpenCheatCreate = () => {
|
const handleOpenCheatCreate = () => {
|
||||||
setEditingCheatId(null);
|
setEditingCheatId(null);
|
||||||
@@ -526,6 +615,18 @@ export default function AdminPage() {
|
|||||||
<span>Cheatsheets</span>
|
<span>Cheatsheets</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setActiveTab('categories')}
|
onClick={() => setActiveTab('categories')}
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-bold transition-all text-left ${
|
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>
|
</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' && (
|
{activeTab === 'categories' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
||||||
@@ -899,7 +1055,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TAB 5: USERS / ADMINS */}
|
{/* TAB 6: USERS / ADMINS */}
|
||||||
{activeTab === 'users' && (
|
{activeTab === 'users' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Create User Form */}
|
{/* Create User Form */}
|
||||||
@@ -971,7 +1127,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TAB 6: SETTINGS */}
|
{/* TAB 7: SETTINGS */}
|
||||||
{activeTab === '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">
|
<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">
|
<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>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TAB 7: MESSAGES */}
|
{/* TAB 8: MESSAGES */}
|
||||||
{activeTab === 'messages' && (
|
{activeTab === 'messages' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-bold text-white">Viewer Contact Messages ({messages.length})</h2>
|
<h2 className="text-lg font-bold text-white">Viewer Contact Messages ({messages.length})</h2>
|
||||||
@@ -1063,6 +1219,103 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
</main>
|
</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 */}
|
{/* CREATE / EDIT CHEATSHEET FORM MODAL */}
|
||||||
{isCheatFormOpen && (
|
{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">
|
<div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4 overflow-y-auto">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ export function Footer({ locale }: { locale: string }) {
|
|||||||
<ul className="space-y-2 text-xs">
|
<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}/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}/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>
|
<li><Link href={`/${locale}/contact`} className="hover:text-red-400 transition-colors">Contact & Suggestions</Link></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useTranslations } from 'next-intl';
|
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 }) {
|
export function Navbar({ locale }: { locale: string }) {
|
||||||
const t = useTranslations('nav');
|
const t = useTranslations('nav');
|
||||||
@@ -13,6 +13,7 @@ export function Navbar({ locale }: { locale: string }) {
|
|||||||
{ href: `/${locale}`, label: t('home'), icon: Sparkles },
|
{ href: `/${locale}`, label: t('home'), icon: Sparkles },
|
||||||
{ href: `/${locale}/lessons`, label: t('lessons'), icon: Code2 },
|
{ href: `/${locale}/lessons`, label: t('lessons'), icon: Code2 },
|
||||||
{ href: `/${locale}/cheatsheets`, label: t('cheatsheets'), icon: BookOpen },
|
{ href: `/${locale}/cheatsheets`, label: t('cheatsheets'), icon: BookOpen },
|
||||||
|
{ href: `/${locale}/prompts`, label: 'Prompts', icon: Terminal },
|
||||||
{ href: `/${locale}/contact`, label: t('contact'), icon: Mail },
|
{ href: `/${locale}/contact`, label: t('contact'), icon: Mail },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,3 +160,15 @@ model Setting {
|
|||||||
value String @db.Text
|
value String @db.Text
|
||||||
updatedAt DateTime @updatedAt
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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!');
|
console.log('Seeding completed successfully!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user