Compare commits

...

10 Commits

Author SHA1 Message Date
Keith Solomon
b3bcb42974 feature: Make pages responsive
Some checks failed
Sync TODOs with Issues / sync_todos (push) Failing after 12s
2025-07-27 11:38:49 -05:00
Keith Solomon
a28ac0b16a 🐞 fix: Update templates to use MainLayout and set page title via parameter 2025-07-27 11:07:16 -05:00
Keith Solomon
3ad0762e17 🐞 fix: Remove unused scripts, remove sidebar height 2025-07-27 10:41:07 -05:00
Keith Solomon
6b3669b8bf 🐞 fix: Move sidebar border 2025-07-27 10:27:07 -05:00
Keith Solomon
fb5093a529 feature: Update layout to have footer, update export button for all/selected prompt export text 2025-07-27 09:59:09 -05:00
Keith Solomon
ba333343c6 🐞 fix: Correct syntax error in edit link in PromptCard component 2025-07-25 16:52:24 -05:00
Keith Solomon
334df0688e 📄 docs: Update readme 2025-07-22 12:19:07 -05:00
Keith Solomon
a95a530999 📄 docs: Update readme and .gitignore 2025-07-22 12:16:12 -05:00
Keith Solomon
7ab8ac600d 📄 docs: Update readme and .gitignore 2025-07-22 12:16:03 -05:00
Keith Solomon
1185e35a1d 📄 docs: Update readme 2025-07-22 12:11:40 -05:00
13 changed files with 451 additions and 366 deletions

4
.gitignore vendored
View File

@@ -3,10 +3,11 @@ dist/
# generated types
.astro/
# dependencies
# dependencies and extras
*.secrets
*.csv
notes/*.json
notes/*.png
node_modules/
# logs
@@ -15,7 +16,6 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production

View File

@@ -1,5 +1,82 @@
# Prompt Catalog
## Overview & Purpose
## Overview
The Prompt Catalog is a centralized repository for prompts used in various applications, such as AI models or chatbots. It aims to provide a structured way to store, search, and categorize prompts for easy access and management. It will serve as a comprehensive and user-friendly catalog of prompts that can be easily searched, categorized, and tagged, enhancing the usability and discoverability of prompts for developers and users alike.
The Prompt Catalog is a centralized, searchable repository for storing and managing prompts used in AI models, chatbots, or any generative application. It provides a structured way to organize, tag, search, and reuse prompts—streamlining prompt engineering for developers, teams, and enthusiasts.
## Features
- **Prompt Types**: Categorize prompts as `System` or `Task`
- **Tagging**: Add tags to describe and filter prompts
- **Search**: Keyword and full-text search with filter by type and tags
- **Metadata**: Each prompt includes:
- Title
- Description
- Tags
- Type
- Created/Last modified timestamps
- **Import/Export**: Import/export prompts via JSON files
- **Web UI**: Clean interface to view, edit, and manage prompts
- **Planned**:
- AI Integration: Suggest prompts using OpenAI, Together, or Ollama APIs
- User auth and profiles
- Prompt favorites and contributions
- Ratings, version history, and sharing
- External API access
## Tech Stack
- **Frontend**: Astro + HTMX + Alpine.js (AHA Stack)
- **Backend**: Supabase (self-hosted)
- **Deployment**: Vercel or Netlify (frontend), Supabase (backend)
## Database Schema
### `prompts`
| Column | Type |
|---------------|------------|
| id | UUID |
| type | text |
| title | text |
| description | text |
| tags | array(text)|
| created_at | timestamp |
| updated_at | timestamp |
*Future tables: `users`, `user_prompts`*
## Development Roadmap
Development is organized into phases. For details, see `Development Checklist.md`.
### MVP Phases
1. **Planning & Setup**
2. **Database & Supabase API**
3. **Front-End UI**
4. **Search & Tagging**
5. **Import/Export Functionality**
### Post-MVP Enhancements
- AI Prompt Suggestions
- User login with Supabase Auth
- User-contributed prompts and favorites
- Ratings, version control, external API
- Shareable links and embeds
## Usage
To run locally:
```bash
npm install
npm run dev
```
You'll need a .env file with your Supabase credentials. See `supabase.env.example`.
## Contributing
Pull requests are welcome! Please keep contributions focused on core functionality and usability improvements.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -15,11 +15,12 @@ type Prompt = {
};
---
<div class="border-b mb-4">
<div class="border-b mb-4 flex justify-between items-center">
<h2 id="prompt-count" class="text-xl font-semibold text-gray-300 mb-2"></h2>
<a href="#filters" class="block lg:hidden bg-blue-600 text-white px-2 py-0 pb-1 mb-2 rounded cursor-pointer hover:bg-blue-700 transition-colors duration-300">Filters &downarrow;</a>
</div>
<div id="prompt-grid" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div id="prompt-grid" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{prompts.map((p: Prompt) => (
<div class="prompt-card" data-type={p.type} data-tags={(p.tags ?? []).join(',')}>
<PromptCard {...p} />

View File

@@ -47,7 +47,7 @@ const formatDate = (dateStr: string | undefined) => {
<div class="flex justify-between items-center">
<h3 class="text-xl font-semibold px-2">Prompt</h3>
<a class="bg-green-600 text-white px-2 py-0 rounded text-sm hover:bg-green-700 transition-colors duration-300" href={`/edit?slug=${slug}`}>Edit</a>
<a class="bg-green-600 text-white px-2 py-0 rounded text-sm hover:bg-green-700 transition-colors duration-300" href={`/edit?slug=${slug}`}->Edit</a>
</div>
<p class="my-2 px-2 text-balance" set:html={description.replace(/\n/g, '<br />')} />

View File

@@ -4,7 +4,7 @@
// TODO: Add options for AND/OR switching and sort by options
---
<div class="w-64 border-r h-full p-4 text-gray-100">
<aside id="filters" class="w-full lg:w-64 h-full p-4 text-gray-100 order-1 lg:order-none">
<a href="/add" id="add-prompt" class="block w-fit bg-green-600 text-white px-4 py-2 mb-4 rounded hover:bg-green-700 transition-colors duration-300">
Add Prompt
</a>
@@ -34,7 +34,7 @@
<div>
<fieldset class="flex flex-col">
<legend class="block text-lg font-semibold mb-1">Tags</legend>
<div class="flex flex-wrap gap-2 max-w-[30rem]">
<div class="flex flex-wrap gap-2 lg:max-w-[30rem]">
{Astro.props.allTags.map((tag: string) => (
<label class="relative inline-flex items-center cursor-pointer">
<input
@@ -53,8 +53,8 @@
</div>
</div>
<button id="clear-filters"type="button" class="bg-blue-600 text-white px-4 py-2 rounded cursor-pointer hover:bg-blue-700 transition-colors duration-300">
<button id="clear-filters" type="button" class="bg-blue-600 text-white px-4 py-2 rounded cursor-pointer hover:bg-blue-700 transition-colors duration-300">
Reset Filters
</button>
</form>
</div>
</aside>

View File

@@ -15,7 +15,7 @@ const code = `[
`;
---
<aside class="w-64 border-r h-full p-4 text-gray-100">
<aside class="w-full lg:w-64 h-full p-4 text-gray-100 order-1 lg:order-none">
<h2 class="text-lg font-semibold mb-4">Add a prompt</h2>
<p class="text-sm">Use the form to add a new AI prompt to the catalog.</p>
@@ -30,7 +30,7 @@ const code = `[
<h2 class="text-lg font-semibold mt-6 mb-4">Export</h2>
<p class="text-sm">To export prompts to a JSON file, select the prompts using the checkboxes, and click the button below.</p>
<p class="text-sm">To export prompts to a JSON file, select the prompts using the checkboxes, and click the button below the checkboxes.</p>
<p class="text-sm mt-2">To export all prompts, leave all checkboxes unchecked.</p>

View File

@@ -2,15 +2,11 @@
// Sidebar.astro
---
<aside class="w-64 border-r h-full p-4 text-gray-100">
<aside class="w-full lg:w-64 h-full p-4 text-gray-100 order-1 lg:order-none">
<h2 class="text-lg font-semibold mb-4">Edit a prompt</h2>
<p class="text-sm">Use the form to edit an AI prompt in the catalog.</p>
<!-- <p class="text-sm mt-2">Make sure to include a title, type, description, and any relevant tags.</p> -->
<!-- <p class="text-sm mt-2">You can also add notes for your own reference.</p> -->
<a href="/" id="home" class="block w-fit bg-green-600 text-white px-4 py-2 mt-4 rounded hover:bg-green-700 transition-colors duration-300">
Go Back
</a>

View File

@@ -2,17 +2,14 @@
// MainLayout.astro
import "../styles/global.css";
const { children } = Astro.props;
const page = Astro.props.page;
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Prompt Catalog</title>
<script is:inline defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script is:inline defer src="https://unpkg.com/htmx.org@1.9.10"></script>
<title>Prompt Catalog - {page}</title>
</head>
<body class="font-sans antialiased bg-gray-800 text-gray-100">

View File

@@ -1,17 +1,12 @@
---
import "../styles/global.css";
import MainLayout from '../layouts/MainLayout.astro';
import Sidebar from "../components/SidebarAdd.astro";
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
---
<html>
<head>
<title>Prompt Catalog - Add New Prompt</title>
</head>
<body class="font-sans antialiased bg-gray-800 text-gray-100">
<MainLayout page="Add Prompt">
<div
id="supabase-env"
data-url={supabaseUrl}
@@ -19,16 +14,16 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
hidden
></div>
<div class="border-b p-4">
<h1 class="text-2xl font-bold">Prompt Catalog - Add New Prompt</h1>
<header class="border-b p-4">
<h1 class="text-2xl font-bold"><a href="/">Prompt Catalog - Add New Prompt</a></h1>
<p class="text-sm mt-1">Add or import new AI prompts to the catalog</p>
</div>
</header>
<div class="flex h-screen">
<main class="flex flex-col lg:flex-row">
<Sidebar />
<div class="flex-1 flex flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto p-4">
<div class="border-b lg:border-l flex-1 flex flex-col overflow-hidden">
<div class="flex-1 overflow-y-auto p-4">
<div id="success" class="bg-green-100 text-green-700 p-4 rounded mb-4 hidden">
Prompt added successfully!
</div>
@@ -92,14 +87,18 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
<div id="prompt-list" class="max-h-64 overflow-y-auto mb-4 border py-1 pb-2 px-3 rounded flex flex-wrap gap-x-4 gap-y-1"></div>
<button type="button" id="exportBtn" class="bg-yellow-600 text-white px-4 py-2 rounded hover:bg-yellow-700 transition-colors duration-300 cursor-pointer">
Export Selected Prompts
Export All Prompts
</button>
</div>
</div>
</form>
</div>
</div>
</main>
</div>
</div>
<footer class="border-t p-4 text-center text-sm">
&copy; {new Date().getFullYear()} Prompt Catalog
</footer>
<script type="module">
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
@@ -109,11 +108,48 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('add-form');
const importBtn = document.getElementById('importBtn');
const exportBtn = document.getElementById('exportBtn');
const successBox = document.getElementById('success');
const errorBox = document.getElementById('error');
async function loadPromptCheckboxes() {
const promptList = document.getElementById('prompt-list');
const { data, error } = await supabase.from('prompts').select('id, title');
if (error) {
promptList.innerHTML = `<p class="text-red-400">Failed to load prompts: ${error.message}</p>`;
return;
}
promptList.innerHTML = data
.map(
prompt => `
<label class="block mb-0">
<input type="checkbox" value="${prompt.id}" class="mr-0" />
${prompt.title}
</label>`
)
.join('');
// After rendering, attach change listeners to checkboxes
const checkboxes = Array.from(document.querySelectorAll('#prompt-list input[type="checkbox"]'));
checkboxes.forEach(cb => cb.addEventListener('change', updateExportBtnText));
updateExportBtnText();
}
// Load checkboxes on DOM load
loadPromptCheckboxes();
function updateExportBtnText() {
const checkboxes = Array.from(document.querySelectorAll('#prompt-list input[type="checkbox"]'));
const checkedCount = checkboxes.filter(cb => cb.checked).length;
exportBtn.textContent = checkedCount > 0 ? 'Export Selected Prompts' : 'Export All Prompts';
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('title').addEventListener('input', (e) => {
const value = e.target.value
.toLowerCase()
@@ -151,7 +187,7 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
});
});
document.getElementById('importBtn').addEventListener('click', async () => {
importBtn.addEventListener('click', async () => {
const fileInput = document.getElementById('importFile');
const file = fileInput.files[0];
@@ -196,27 +232,7 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
}
});
async function loadPromptCheckboxes() {
const promptList = document.getElementById('prompt-list');
const { data, error } = await supabase.from('prompts').select('id, title');
if (error) {
promptList.innerHTML = `<p class="text-red-400">Failed to load prompts: ${error.message}</p>`;
return;
}
promptList.innerHTML = data
.map(
prompt => `
<label class="block mb-0">
<input type="checkbox" value="${prompt.id}" class="mr-0" />
${prompt.title}
</label>`
)
.join('');
}
document.getElementById('exportBtn').addEventListener('click', async () => {
exportBtn.addEventListener('click', async () => {
const checkboxes = Array.from(document.querySelectorAll('#prompt-list input[type="checkbox"]'));
const selectedIds = checkboxes.filter(cb => cb.checked).map(cb => cb.value);
@@ -224,8 +240,6 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
? await supabase.from('prompts').select('*').in('id', selectedIds)
: await supabase.from('prompts').select('*');
const successBox = document.getElementById('success');
const errorBox = document.getElementById('error');
successBox.style.display = 'none';
errorBox.style.display = 'none';
@@ -256,9 +270,5 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
link.click();
URL.revokeObjectURL(url);
});
// Load checkboxes on DOM load
loadPromptCheckboxes();
</script>
</body>
</html>
</MainLayout>

View File

@@ -1,17 +1,12 @@
---
import "../styles/global.css";
import MainLayout from '../layouts/MainLayout.astro';
import Sidebar from "../components/SidebarEdit.astro";
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
---
<html lang="en">
<head>
<title>Prompt Catalog - Edit Prompt</title>
</head>
<body class="font-sans antialiased bg-gray-800 text-gray-100">
<MainLayout page="Edit Prompt">
<div
id="supabase-env"
data-url={supabaseUrl}
@@ -19,20 +14,24 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
hidden
></div>
<div class="border-b p-4">
<h1 class="text-2xl font-bold">Prompt Catalog - Add New Prompt</h1>
<p class="text-sm mt-1">Add a new AI prompt to the catalog</p>
</div>
<header class="border-b p-4">
<h1 class="text-2xl font-bold"><a href="/">Prompt Catalog - Edit Prompt</a></h1>
<p class="text-sm mt-1">Edit an existing AI prompt in the catalog</p>
</header>
<div class="flex h-screen">
<main class="flex flex-col lg:flex-row">
<Sidebar />
<div class="flex-1 flex flex-col overflow-hidden">
<div class="border-b lg:border-l flex-1 flex flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto p-4">
<div id="edit-root">Loading...</div>
</main>
</div>
</div>
</main>
<footer class="border-t p-4 text-center text-sm">
&copy; {new Date().getFullYear()} Prompt Catalog
</footer>
<script type="module">
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
@@ -152,5 +151,4 @@ const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
}
}
</script>
</body>
</html>
</MainLayout>

View File

@@ -13,24 +13,28 @@ const allTags = Array.from(
new Set(
prompts?.flatMap((p) => p.tags ?? [])
)
).sort();
).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
---
<MainLayout>
<div class="border-b p-4">
<h1 class="text-2xl font-bold">Prompt Catalog</h1>
<p class="text-sm mt-1">Explore and filter AI prompts</p>
</div>
<MainLayout page="Home">
<header class="border-b p-4">
<h1 class="text-2xl font-bold"><a href="/">Prompt Catalog</a></h1>
<p class="text-sm mt-1">Save and explore AI prompts</p>
</header>
<div class="flex h-screen">
<main class="flex flex-col lg:flex-row">
<SearchBar allTags={allTags} />
<div class="flex-1 flex flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto p-4">
<div class="border-b lg:border-l flex-1 flex flex-col overflow-hidden">
<div class="flex-1 overflow-y-auto px-4 lg:px-6 pt-2 pb-4">
{error
? <p class="text-red-500">Supabase error: {error.message}</p>
: <FilteredPromptList prompts={prompts} />}
</div>
</div>
</main>
</div>
</div>
<footer class="border-t p-4 text-center text-sm">
&copy; {new Date().getFullYear()} Prompt Catalog
</footer>
</MainLayout>

2
supabase.env.example Normal file
View File

@@ -0,0 +1,2 @@
PUBLIC_SUPABASE_URL=<your Supabase url>
PUBLIC_SUPABASE_ANON_KEY=<your Supabase anon key>