Initial prototype

This commit is contained in:
Aun Ali 2025-08-09 15:38:19 +00:00
parent ef5f6bcabc
commit 0326822621
16 changed files with 626 additions and 41 deletions

BIN
.idx/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

0
.modified Normal file
View file

19
docs/blueprint.md Normal file
View file

@ -0,0 +1,19 @@
# **App Name**: PromptVerse
## Core Features:
- Prompt Creation: Create and save new prompts with a user-friendly interface, offering options to format the prompt and add context notes.
- Prompt Organization: Organize prompts using tags and categories. Include hierarchical categories.
- Robust Search: Enable powerful search functionality to quickly find prompts by keywords, tags, or categories.
- One-Click Copy: Allow users to copy prompts to the clipboard with a single click for easy use in other applications.
- AI-Powered Tagging: Suggest relevant tags for the prompt using AI. The AI tool should extract important keywords from the prompt to improve searchability and organization.
## Style Guidelines:
- Primary color: Deep indigo (#4B0082) for a professional feel with a hint of creativity.
- Background color: Dark slate gray (#303030) to implement a sleek dark mode theme.
- Accent color: Electric purple (#BF40BF) for interactive elements and highlights to guide the user.
- Body and headline font: 'Inter' sans-serif for a modern, machined, objective, neutral look.
- Use minimalist icons that are easily recognizable on a dark background. Prefer sharp, geometric designs for a modern feel.
- Implement a grid-based layout, similar to Google Keep, for organizing prompts. Use subtle shadows to create a frosted glass effect (similar to Apple Vision Pro). Utilize a card-based interface for each prompt.
- Subtle animations on hover and click events. Smooth transitions between pages or sections. A brief 'copied to clipboard' confirmation that elegantly fades in and out when copying prompts.

View file

@ -1 +1,4 @@
// Flows will be imported for their side effects in this file.
import { config } from 'dotenv';
config();
import '@/ai/flows/suggest-tags.ts';

View file

@ -0,0 +1,56 @@
// This file uses server-side code.
'use server';
/**
* @fileOverview AI-powered tag suggestion for prompts.
*
* - suggestTags - A function that suggests relevant tags for a given prompt.
* - SuggestTagsInput - The input type for the suggestTags function.
* - SuggestTagsOutput - The return type for the suggestTags function.
*/
import {ai} from '@/ai/genkit';
import {z} from 'genkit';
const SuggestTagsInputSchema = z.object({
promptText: z
.string()
.describe('The text content of the prompt for which tags are to be suggested.'),
});
export type SuggestTagsInput = z.infer<typeof SuggestTagsInputSchema>;
const SuggestTagsOutputSchema = z.object({
tags: z
.array(z.string())
.describe('An array of suggested tags for the given prompt.'),
});
export type SuggestTagsOutput = z.infer<typeof SuggestTagsOutputSchema>;
export async function suggestTags(input: SuggestTagsInput): Promise<SuggestTagsOutput> {
return suggestTagsFlow(input);
}
const suggestTagsPrompt = ai.definePrompt({
name: 'suggestTagsPrompt',
input: {schema: SuggestTagsInputSchema},
output: {schema: SuggestTagsOutputSchema},
prompt: `You are a prompt categorization expert.
Given the following prompt, suggest 5 relevant tags that can be used to categorize and search for this prompt later. Return them as a simple array of strings.
Prompt: {{{promptText}}}
Tags:`,
});
const suggestTagsFlow = ai.defineFlow(
{
name: 'suggestTagsFlow',
inputSchema: SuggestTagsInputSchema,
outputSchema: SuggestTagsOutputSchema,
},
async input => {
const {output} = await suggestTagsPrompt(input);
return output!;
}
);

View file

@ -14,67 +14,51 @@ body {
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary: 300 50% 50%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--secondary: 275 100% 25.1%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent: 300 50% 60%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--ring: 300 50% 50%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--background: 0 0% 18.8%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card: 0 0% 22%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover: 0 0% 18.8%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--primary: 300 50% 50%;
--primary-foreground: 0 0% 98%;
--secondary: 275 100% 35%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted: 0 0% 25%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent: 300 50% 60%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--border: 0 0% 30%;
--input: 0 0% 25%;
--ring: 300 50% 60%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}

View file

@ -1,9 +1,10 @@
import type {Metadata} from 'next';
import './globals.css';
import { Toaster } from "@/components/ui/toaster";
export const metadata: Metadata = {
title: 'Firebase Studio App',
description: 'Generated by Firebase Studio',
title: 'PromptVerse',
description: 'An AI prompt management tool to create, organize, and share your best prompts.',
};
export default function RootLayout({
@ -12,13 +13,16 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="en" className="dark">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></link>
</head>
<body className="font-body antialiased">{children}</body>
<body className="font-body antialiased bg-background text-foreground min-h-screen">
{children}
<Toaster />
</body>
</html>
);
}

View file

@ -1,3 +1,22 @@
export default function Home() {
return <></>;
import { Header } from '@/components/header';
import { PromptList, PromptListSkeleton } from '@/components/prompt-list';
import { Suspense } from 'react';
export default function Home({
searchParams,
}: {
searchParams?: { q?: string };
}) {
const query = searchParams?.q || '';
return (
<div className="flex flex-col min-h-screen bg-background">
<Header />
<main className="container mx-auto py-6 px-4 sm:px-6 lg:px-8">
<Suspense fallback={<PromptListSkeleton />}>
<PromptList query={query} />
</Suspense>
</main>
</div>
);
}

View file

@ -0,0 +1,168 @@
'use client';
import { useEffect, useRef, useState, useTransition } from 'react';
import { useFormState, useFormStatus } from 'react-dom';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { createPrompt, suggestPromptTags } from '@/lib/actions';
import { useToast } from '@/hooks/use-toast';
import { PlusCircle, Loader2, Sparkles, XIcon } from 'lucide-react';
import { Badge } from './ui/badge';
const initialState = {
message: '',
errors: {},
success: false,
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" disabled={pending}>
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Prompt
</Button>
);
}
export function CreatePromptDialog() {
const [open, setOpen] = useState(false);
const [state, formAction] = useFormState(createPrompt, initialState);
const { toast } = useToast();
const formRef = useRef<HTMLFormElement>(null);
const [promptContent, setPromptContent] = useState('');
const [tags, setTags] = useState<string[]>([]);
const [tagInput, setTagInput] = useState('');
const [isSuggesting, startSuggestionTransition] = useTransition();
const handleSuggestTags = async () => {
startSuggestionTransition(async () => {
const result = await suggestPromptTags(promptContent);
if (result.error) {
toast({ title: 'Suggestion Failed', description: result.error, variant: 'destructive' });
} else if (result.tags) {
setTags((prev) => [...new Set([...prev, ...result.tags!])]);
toast({ title: 'Tags Suggested!', description: 'AI-powered tags have been added.' });
}
});
};
const handleTagInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && tagInput.trim()) {
e.preventDefault();
setTags((prev) => [...new Set([...prev, tagInput.trim().toLowerCase()])]);
setTagInput('');
}
};
const removeTag = (tagToRemove: string) => {
setTags((prev) => prev.filter((tag) => tag !== tagToRemove));
};
useEffect(() => {
if (state.success) {
toast({
title: 'Success!',
description: state.message,
});
setOpen(false);
formRef.current?.reset();
setTags([]);
setPromptContent('');
} else if (state.message && !state.success && Object.keys(state.errors ?? {}).length > 0) {
toast({
title: 'Error',
description: state.message,
variant: 'destructive',
});
}
}, [state, toast]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
New Prompt
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[625px]">
<DialogHeader>
<DialogTitle>Create a new prompt</DialogTitle>
<DialogDescription>
Craft your next masterpiece. Add notes and tags to keep it organized.
</DialogDescription>
</DialogHeader>
<form ref={formRef} action={formAction} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input id="title" name="title" placeholder="e.g., Blog Post Ideas" required />
{state.errors?.title && <p className="text-sm text-destructive">{state.errors.title[0]}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="content">Prompt</Label>
<Textarea
id="content"
name="content"
placeholder="Your prompt text here..."
className="min-h-[120px]"
onChange={(e) => setPromptContent(e.target.value)}
required
/>
{state.errors?.content && <p className="text-sm text-destructive">{state.errors.content[0]}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notes (Optional)</Label>
<Textarea
id="notes"
name="notes"
placeholder="Add context or notes about this prompt..."
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="tags">Tags</Label>
<Button type="button" size="sm" variant="ghost" onClick={handleSuggestTags} disabled={isSuggesting || promptContent.length < 20}>
{isSuggesting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Sparkles className="mr-2 h-4 w-4" />}
Suggest
</Button>
</div>
<div className="flex flex-wrap gap-2 p-2 border rounded-md min-h-[40px] bg-muted/50">
{tags.map(tag => (
<Badge key={tag} variant="secondary">
{tag}
<button type="button" onClick={() => removeTag(tag)} className="ml-1 rounded-full hover:bg-destructive/80 p-0.5">
<XIcon className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<Input id="tags-input" value={tagInput} onChange={(e) => setTagInput(e.target.value)} onKeyDown={handleTagInputKeyDown} placeholder="Type a tag and press Enter" />
<input type="hidden" name="tags" value={JSON.stringify(tags)} />
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">Cancel</Button>
</DialogClose>
<SubmitButton />
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

52
src/components/header.tsx Normal file
View file

@ -0,0 +1,52 @@
'use client';
import { Logo } from '@/components/icons';
import { Input } from '@/components/ui/input';
import { Search } from 'lucide-react';
import { CreatePromptDialog } from '@/components/create-prompt-dialog';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useRef } from 'react';
export function Header() {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleSearch = (term: string) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('q', term);
} else {
params.delete('q');
}
replace(`${pathname}?${params.toString()}`);
}, 300);
};
return (
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 max-w-7xl items-center justify-between mx-auto px-4 sm:px-6 lg:px-8">
<div className="mr-8 flex items-center">
<Logo className="h-6 w-6 mr-2 text-primary" />
<h1 className="font-bold text-lg hidden sm:block">PromptVerse</h1>
</div>
<div className="flex flex-1 items-center justify-end gap-4">
<div className="relative flex-1 max-w-xs sm:max-w-sm md:max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search prompts..."
className="pl-9"
onChange={(e) => handleSearch(e.target.value)}
defaultValue={searchParams.get('q')?.toString()}
/>
</div>
<CreatePromptDialog />
</div>
</div>
</header>
);
}

10
src/components/icons.tsx Normal file
View file

@ -0,0 +1,10 @@
import type { SVGProps } from 'react';
export function Logo(props: SVGProps<SVGSVGElement>) {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M12.25 5.75H8.75V18.25H12.25C14.7018 18.25 16.75 16.2018 16.75 13.75V10.25C16.75 7.79822 14.7018 5.75 12.25 5.75Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8.75 12H13.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
}

View file

@ -0,0 +1,66 @@
'use client';
import type { Prompt } from '@/lib/types';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Copy } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { formatDistanceToNow } from 'date-fns';
interface PromptCardProps {
prompt: Prompt;
}
export function PromptCard({ prompt }: PromptCardProps) {
const { toast } = useToast();
const handleCopy = () => {
navigator.clipboard.writeText(prompt.content);
toast({
title: 'Prompt Copied!',
description: 'The prompt text is now on your clipboard.',
});
};
const timeAgo = formatDistanceToNow(new Date(prompt.createdAt), { addSuffix: true });
return (
<Card className="flex flex-col bg-secondary/30 backdrop-blur-lg border border-white/10 shadow-lg hover:border-white/20 transition-all duration-300 h-full">
<CardHeader>
<CardTitle className="text-base font-semibold">{prompt.title}</CardTitle>
<CardDescription className="text-xs text-muted-foreground">{timeAgo}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<p className="text-sm text-foreground/80 line-clamp-4">
{prompt.content}
</p>
</CardContent>
<CardFooter className="flex flex-col items-start gap-4">
<div className="flex flex-wrap gap-2">
{prompt.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="bg-primary/20 text-primary-foreground/80 border-none">
{tag}
</Badge>
))}
</div>
<Button
onClick={handleCopy}
variant="ghost"
size="sm"
className="w-full justify-center group"
>
<Copy className="h-4 w-4 mr-2 group-hover:text-accent transition-colors" />
Copy Prompt
</Button>
</CardFooter>
</Card>
);
}

View file

@ -0,0 +1,51 @@
import { getPrompts } from '@/lib/actions';
import { PromptCard } from './prompt-card';
import { Card, CardContent, CardHeader } from './ui/card';
import { Skeleton } from './ui/skeleton';
export async function PromptList({ query }: { query: string }) {
const prompts = await getPrompts(query);
if (prompts.length === 0) {
return (
<div className="text-center py-16">
<h2 className="text-2xl font-semibold">No Prompts Found</h2>
<p className="text-muted-foreground mt-2">
Try adjusting your search or create a new prompt.
</p>
</div>
);
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{prompts.map((prompt) => (
<PromptCard key={prompt.id} prompt={prompt} />
))}
</div>
);
}
export function PromptListSkeleton() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{Array.from({ length: 10 }).map((_, i) => (
<Card key={i} className="bg-secondary/20 border-white/10">
<CardHeader>
<Skeleton className="h-5 w-3/4" />
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
</div>
<div className="flex gap-2">
<Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-20 rounded-full" />
</div>
</CardContent>
</Card>
))}
</div>
);
}

82
src/lib/actions.ts Normal file
View file

@ -0,0 +1,82 @@
// This file uses server-side code.
'use server';
import { revalidatePath } from 'next/cache';
import { prompts as dbPrompts } from './data';
import type { Prompt } from './types';
import { suggestTags as suggestTagsFlow } from '@/ai/flows/suggest-tags';
import { z } from 'zod';
// In-memory store, mimicking a database.
let prompts: Prompt[] = [...dbPrompts];
export async function getPrompts(query: string = ''): Promise<Prompt[]> {
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network delay
const lowerCaseQuery = query.toLowerCase();
return prompts.filter(prompt => {
const inTitle = prompt.title.toLowerCase().includes(lowerCaseQuery);
const inContent = prompt.content.toLowerCase().includes(lowerCaseQuery);
const inTags = prompt.tags.some(tag => tag.toLowerCase().includes(lowerCaseQuery));
return inTitle || inContent || inTags;
}).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}
const CreatePromptSchema = z.object({
title: z.string().min(1, { message: 'Title is required.' }),
content: z.string().min(1, { message: 'Content is required.' }),
notes: z.string().optional(),
tags: z.string(), // JSON string of tags array
});
export async function createPrompt(prevState: any, formData: FormData) {
const validatedFields = CreatePromptSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
notes: formData.get('notes'),
tags: formData.get('tags'),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Failed to create prompt.',
};
}
const { title, content, notes, tags } = validatedFields.data;
try {
const newPrompt: Prompt = {
id: Date.now().toString(),
title,
content,
notes,
tags: JSON.parse(tags),
createdAt: new Date().toISOString(),
};
// Add to the start of the array to show newest first
prompts.unshift(newPrompt);
revalidatePath('/');
return { message: 'Prompt created successfully.', success: true };
} catch (e) {
return { message: 'An unexpected error occurred.', errors: {} };
}
}
export async function suggestPromptTags(promptText: string): Promise<{tags?: string[]; error?: string}> {
if (!promptText || promptText.trim().length < 20) {
return { error: 'Please provide a more detailed prompt for better suggestions.' };
}
try {
const result = await suggestTagsFlow({ promptText });
return { tags: result.tags };
} catch (error) {
return { error: 'Failed to get AI suggestions.' };
}
}

63
src/lib/data.ts Normal file
View file

@ -0,0 +1,63 @@
import type { Prompt } from './types';
export const prompts: Prompt[] = [
{
id: '1',
title: 'Blog Post Ideas Generator',
content: 'Generate 10 blog post ideas for a blog about AI development. The ideas should be catchy, relevant to current trends, and suitable for a technical audience. For each idea, provide a brief description and a potential headline.',
tags: ['writing', 'ai', 'content-creation'],
createdAt: new Date(new Date().setDate(new Date().getDate()-1)).toISOString(),
notes: 'Great for weekly content planning.'
},
{
id: '2',
title: 'Python Code Refactoring',
content: 'Act as a senior Python developer. Take the following code snippet and refactor it for better readability, performance, and adherence to PEP 8 standards. Explain the changes you made and why.',
tags: ['python', 'code', 'development', 'refactoring'],
createdAt: new Date(new Date().setDate(new Date().getDate()-2)).toISOString(),
},
{
id: '3',
title: 'Marketing Campaign Slogans',
content: 'Create 5 compelling slogans for a new brand of eco-friendly coffee. The slogans should be short, memorable, and highlight the brand\'s commitment to sustainability.',
tags: ['marketing', 'copywriting', 'branding'],
createdAt: new Date(new Date().setDate(new Date().getDate()-3)).toISOString(),
},
{
id: '4',
title: 'Explain Quantum Computing',
content: 'Explain the concept of quantum computing to a 12-year-old. Use simple analogies and avoid technical jargon as much as possible. Cover the ideas of qubits, superposition, and entanglement.',
tags: ['education', 'science', 'complex-topics'],
createdAt: new Date(new Date().setDate(new Date().getDate()-4)).toISOString(),
notes: 'Useful for breaking down difficult concepts for presentations.'
},
{
id: '5',
title: 'Generate SQL Query',
content: 'Given a database schema with two tables, `users` (id, name, email, signup_date) and `orders` (id, user_id, amount, order_date), write a SQL query to find the total order amount for each user in the last 30 days.',
tags: ['sql', 'database', 'code'],
createdAt: new Date(new Date().setDate(new Date().getDate()-5)).toISOString(),
},
{
id: '6',
title: 'Social Media Post for Product Launch',
content: 'Write a social media post for Twitter announcing the launch of a new productivity app called "Zenith". The tone should be exciting and engaging. Include relevant hashtags.',
tags: ['social-media', 'marketing', 'launch'],
createdAt: new Date(new Date().setDate(new Date().getDate()-6)).toISOString(),
},
{
id: '7',
title: 'Midjourney Prompt for a Spaceship',
content: 'Generate a detailed Midjourney prompt to create a photorealistic image of a sleek, futuristic exploration spaceship floating in front of a nebula. The design should be minimalist, with glowing blue accents. Include parameters for aspect ratio and style.',
tags: ['midjourney', 'image-generation', 'art'],
createdAt: new Date(new Date().setDate(new Date().getDate()-7)).toISOString(),
},
{
id: '8',
title: 'Recipe from Ingredients',
content: 'I have chicken breasts, broccoli, garlic, and lemon in my fridge. Create a simple and healthy recipe I can make for dinner tonight. Provide step-by-step instructions.',
tags: ['food', 'recipe', 'lifestyle'],
createdAt: new Date(new Date().setDate(new Date().getDate()-8)).toISOString(),
notes: 'A practical prompt for everyday use.'
}
];

8
src/lib/types.ts Normal file
View file

@ -0,0 +1,8 @@
export interface Prompt {
id: string;
title: string;
content: string;
notes?: string;
tags: string[];
createdAt: string; // Using string to avoid serialization issues between server/client
}