Live demo: https://luckytoolkit.com/promptmanager/
If you spend serious time with AI tools — ChatGPT, Claude, Gemini — you quickly accumulate dozens of carefully crafted prompts. They end up scattered across Notion pages, browser bookmarks, and sticky notes. I built Prompt Manager to solve exactly that: a fully-featured prompt library that runs entirely in the browser with zero backend, zero accounts, and zero cost to host.
This post is a complete technical breakdown of how it works.
Here is the source https://github.com/kalyanraju/promptManager
Stack Overview
- Next.js 16 (App Router) with React 19
- TypeScript throughout
- Tailwind CSS v4 for styling
- No backend —
localStorageis the database - No icon library — custom inline SVGs
- No state management library — React built-ins only
The entire application lives in a single file: src/app/page.tsx, approximately 1,400 lines. This was a deliberate architectural choice — one file means zero inter-file dependencies, easy to deploy as a static export, and trivially portable.
Data Model
Every prompt is represented as a flat TypeScript interface:
interface Prompt {
id: string;
title: string;
content: string;
tags: string[];
category: string;
isFavorite: boolean;
createdAt: number;
updatedAt: number;
usageCount: number;
variables: string[];
}
The variables field is derived automatically by scanning the prompt content for {{variable}} syntax using a regex:
const extractVariables = (content: string): string[] => {
const matches = content.match(/\{\{(\w+)\}\}/g);
return matches
? [...new Set(matches.map((m) => m.replace(/[{}]/g, "")))]
: [];
};
This lets users write prompts like:
Write a blog post about {{topic}} for {{audience}} in a {{tone}} tone.
And the app automatically detects and displays the variable names, turning prompts into reusable templates.
Persistence Layer
There is no server. All data lives in two localStorage keys:
const STORAGE_KEY = "ai-prompt-optimizer-data";
const CATEGORIES_STORAGE_KEY = "ai-prompt-optimizer-categories";
The load functions handle SSR safely (Next.js runs components on the server during build):
const loadPrompts = (): Prompt[] => {
if (typeof window === "undefined") return [];
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : SAMPLE_PROMPTS;
} catch {
return SAMPLE_PROMPTS;
}
};
State initialization uses lazy initializers to avoid reading localStorage during SSR:
const [prompts, setPrompts] = useState<Prompt[]>(() => {
if (typeof window === "undefined") return [];
return loadPrompts();
});
Saving is triggered by useEffect watching the prompts array:
useEffect(() => {
if (prompts.length > 0) {
savePrompts(prompts);
}
}, [prompts]);
Search and Filter Pipeline
The filtered prompt list is computed with useMemo, chaining four independent filters before sorting:
const filteredPrompts = useMemo(() => {
let result = [...prompts];
// 1. Full-text search across title, content, and tags
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter(
(p) =>
p.title.toLowerCase().includes(query) ||
p.content.toLowerCase().includes(query) ||
p.tags.some((t) => t.toLowerCase().includes(query))
);
}
// 2. Category filter
if (selectedCategory) {
result = result.filter((p) => p.category === selectedCategory);
}
// 3. Multi-select tag filter (OR logic)
if (selectedTags.length > 0) {
result = result.filter((p) =>
selectedTags.some((tag) => p.tags.includes(tag))
);
}
// 4. Favorites filter
if (showFavorites) {
result = result.filter((p) => p.isFavorite);
}
// Sort: recent / most-used / alphabetical
switch (sortBy) {
case "recent":
result.sort((a, b) => b.updatedAt - a.updatedAt);
break;
case "usage":
result.sort((a, b) => b.usageCount - a.usageCount);
break;
case "name":
result.sort((a, b) => a.title.localeCompare(b.title));
break;
}
return result;
}, [prompts, searchQuery, selectedCategory, selectedTags, showFavorites, sortBy]);
Category and tag counts for the sidebar are also memoized:
const categoryCounts = useMemo(() => {
const counts: Record<string, number> = {};
prompts.forEach((p) => {
counts[p.category] = (counts[p.category] || 0) + 1;
});
return counts;
}, [prompts]);
The Prompt Optimizer
This is the most interesting feature. It’s a purely algorithmic analyzer — no AI API, no network call — that inspects prompt text and returns structured suggestions:
interface OptimizerSuggestion {
type: "clarity" | "structure" | "specificity" | "context" | "output";
message: string;
improvement?: string;
}
The analysis runs five heuristic checks:
const analyzePrompt = (content: string): OptimizerSuggestion[] => {
const suggestions: OptimizerSuggestion[] = [];
// 1. Too short — likely missing context
if (content.length < 50) {
suggestions.push({
type: "clarity",
message: "Prompt is quite short. Consider adding more context and details.",
improvement: "Expand with specific instructions, context, and expected output format.",
});
}
// 2. Wall of text — no structure
if (!content.includes("\n") && content.length > 100) {
suggestions.push({
type: "structure",
message: "Consider breaking down your prompt into sections for better clarity.",
improvement: "Use bullet points or numbered lists to organize instructions.",
});
}
// 3. Vague language
const vagueWords = ["good", "nice", "better", "appropriate", "suitable"];
if (vagueWords.some((w) => content.toLowerCase().includes(w))) {
suggestions.push({
type: "specificity",
message: "Some words are vague. Use specific, measurable criteria.",
improvement: 'Replace "good" with specific quality metrics or examples.',
});
}
// 4. Missing context signal
if (!content.toLowerCase().includes("context") &&
!content.toLowerCase().includes("background")) {
suggestions.push({
type: "context",
message: "Consider adding context about the use case or target audience.",
});
}
// 5. No output format specified
if (!content.toLowerCase().includes("format") &&
!content.toLowerCase().includes("output")) {
suggestions.push({
type: "output",
message: "Specify the expected output format for more consistent results.",
improvement: 'Add: "Format your response as..." or "Output should include..."',
});
}
return suggestions;
};
The auto-optimize function applies structural transformations:
const optimizePrompt = (content: string): string => {
let optimized = content;
// Break wall-of-text into paragraphs
if (!content.includes("\n\n") && content.length > 100) {
const sentences = content.split(". ");
if (sentences.length > 3) {
optimized = sentences.join(".\n\n");
}
}
// Append output format reminder if absent
if (!content.toLowerCase().includes("format") &&
!content.toLowerCase().includes("output")) {
optimized += "\n\nProvide your response in a clear, structured format.";
}
return optimized;
};
Export / Import
The export handler serializes the full prompts array to a timestamped JSON file using the Blob + Object URL pattern — no server required:
const handleExportPrompts = useCallback(() => {
const data = JSON.stringify(prompts, null, 2);
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `prompts-export-${new Date().toISOString().split("T")[0]}.json`;
a.click();
URL.revokeObjectURL(url);
}, [prompts]);
Import validates the structure and re-extracts variables in case the imported file is from an older schema version:
const validPrompts = imported.map((p) => ({
...p,
id: p.id || generateId(),
variables: extractVariables(p.content), // re-derive, don't trust import
}));
setPrompts((prev) => [...validPrompts, ...prev]);
Custom Category Management
On top of 8 built-in categories (General, Code Generation, Writing, Analysis, Creative, Business, Education, Research), users can create their own. Custom categories persist to their own localStorage key separately from prompts.
Deleting a custom category safely migrates its prompts to “General”:
setPrompts((prev) =>
prev.map((p) =>
p.category === category ? { ...p, category: "General" } : p
)
);
Renaming cascades across all prompts that reference the old name.
Usage Tracking
Every time a user copies a prompt, the usageCount increments:
const handleCopyPrompt = useCallback((prompt: Prompt) => {
navigator.clipboard.writeText(prompt.content);
setCopiedId(prompt.id);
setTimeout(() => setCopiedId(null), 2000); // visual feedback
setPrompts((prev) =>
prev.map((p) =>
p.id === prompt.id ? { ...p, usageCount: p.usageCount + 1 } : p
)
);
}, []);
This feeds the “Most Used” sort, helping you surface the prompts that actually matter.
Dark Mode
Theme preference is initialized from localStorage with a fallback to the OS prefers-color-scheme media query:
const [isDarkMode, setIsDarkMode] = useState(() => {
if (typeof window === "undefined") return false;
const savedTheme = localStorage.getItem("ai-prompt-optimizer-theme");
if (savedTheme) return savedTheme === "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches;
});
Tailwind’s class-based dark mode is toggled by adding/removing the dark class on documentElement:
useEffect(() => {
document.documentElement.classList.toggle("dark", isDarkMode);
localStorage.setItem("ai-prompt-optimizer-theme", isDarkMode ? "dark" : "light");
}, [isDarkMode]);
Layout and UI
The layout is a responsive 4-column grid (1 sidebar + 3 content columns on large screens, single column on mobile):
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<aside className="lg:col-span-1">/* filters, categories, tags */</aside>
<main className="lg:col-span-3">/* prompt cards + detail panel */</main>
</div>
The header is sticky with a subtle backdrop blur:
<header className="sticky top-0 z-40 border-b bg-white/95 backdrop-blur-sm">
All icons are hand-crafted inline SVG components to keep the bundle lean — no Lucide, no Heroicons, no font-awesome dependency.
Performance Considerations
- All derived state (filtered lists, tag counts, category counts) is wrapped in
useMemowith precise dependency arrays - All event handlers use
useCallbackto prevent unnecessary re-renders of card components - State lazy initialization prevents synchronous
localStoragereads during React’s render phase useReffor the hidden file input avoids state churn on upload interactions
Deployment
Built as a Next.js static export (next build), the output is plain HTML/CSS/JS — no Node.js server required. Hosted on a static file server under /promptmanager/.
What I’d Add Next
- Import from URL — paste a shared prompt link
- Prompt versioning — track edit history per prompt
- Variable fill-in UI — form that replaces
{{variable}}before copying - Cloud sync — optional account-based backup (Supabase or similar)
- Prompt scoring — track which prompts produce the best AI outputs
Try it live: https://luckytoolkit.com/promptmanager/
All your prompts stay in your browser. No account. No server. No data leaves your machine.
0 Comments