Compare commits
10 Commits
2f84569e60
...
b3bcb42974
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3bcb42974 | ||
|
|
a28ac0b16a | ||
|
|
3ad0762e17 | ||
|
|
6b3669b8bf | ||
|
|
fb5093a529 | ||
|
|
ba333343c6 | ||
|
|
334df0688e | ||
|
|
a95a530999 | ||
|
|
7ab8ac600d | ||
|
|
1185e35a1d |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,10 +3,11 @@ dist/
|
|||||||
# generated types
|
# generated types
|
||||||
.astro/
|
.astro/
|
||||||
|
|
||||||
# dependencies
|
# dependencies and extras
|
||||||
*.secrets
|
*.secrets
|
||||||
*.csv
|
*.csv
|
||||||
notes/*.json
|
notes/*.json
|
||||||
|
notes/*.png
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
@@ -15,7 +16,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
|
|||||||
81
README.md
81
README.md
@@ -1,5 +1,82 @@
|
|||||||
# Prompt Catalog
|
# 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 |
@@ -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>
|
<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 ↓</a>
|
||||||
</div>
|
</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) => (
|
{prompts.map((p: Prompt) => (
|
||||||
<div class="prompt-card" data-type={p.type} data-tags={(p.tags ?? []).join(',')}>
|
<div class="prompt-card" data-type={p.type} data-tags={(p.tags ?? []).join(',')}>
|
||||||
<PromptCard {...p} />
|
<PromptCard {...p} />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const formatDate = (dateStr: string | undefined) => {
|
|||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h3 class="text-xl font-semibold px-2">Prompt</h3>
|
<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>
|
</div>
|
||||||
|
|
||||||
<p class="my-2 px-2 text-balance" set:html={description.replace(/\n/g, '<br />')} />
|
<p class="my-2 px-2 text-balance" set:html={description.replace(/\n/g, '<br />')} />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// TODO: Add options for AND/OR switching and sort by options
|
// 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">
|
<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
|
Add Prompt
|
||||||
</a>
|
</a>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<fieldset class="flex flex-col">
|
<fieldset class="flex flex-col">
|
||||||
<legend class="block text-lg font-semibold mb-1">Tags</legend>
|
<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) => (
|
{Astro.props.allTags.map((tag: string) => (
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</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
|
Reset Filters
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</aside>
|
||||||
|
|||||||
@@ -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>
|
<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>
|
<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>
|
<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>
|
<p class="text-sm mt-2">To export all prompts, leave all checkboxes unchecked.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,11 @@
|
|||||||
// Sidebar.astro
|
// 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>
|
<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">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">
|
<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
|
Go Back
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -2,17 +2,14 @@
|
|||||||
// MainLayout.astro
|
// MainLayout.astro
|
||||||
import "../styles/global.css";
|
import "../styles/global.css";
|
||||||
|
|
||||||
const { children } = Astro.props;
|
const page = Astro.props.page;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Prompt Catalog</title>
|
<title>Prompt Catalog - {page}</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>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="font-sans antialiased bg-gray-800 text-gray-100">
|
<body class="font-sans antialiased bg-gray-800 text-gray-100">
|
||||||
|
|||||||
@@ -1,264 +1,274 @@
|
|||||||
---
|
---
|
||||||
import "../styles/global.css";
|
import MainLayout from '../layouts/MainLayout.astro';
|
||||||
import Sidebar from "../components/SidebarAdd.astro";
|
import Sidebar from "../components/SidebarAdd.astro";
|
||||||
|
|
||||||
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
|
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
|
||||||
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html>
|
<MainLayout page="Add Prompt">
|
||||||
<head>
|
<div
|
||||||
<title>Prompt Catalog - Add New Prompt</title>
|
id="supabase-env"
|
||||||
</head>
|
data-url={supabaseUrl}
|
||||||
|
data-key={supabaseKey}
|
||||||
|
hidden
|
||||||
|
></div>
|
||||||
|
|
||||||
<body class="font-sans antialiased bg-gray-800 text-gray-100">
|
<header class="border-b p-4">
|
||||||
<div
|
<h1 class="text-2xl font-bold"><a href="/">Prompt Catalog - Add New Prompt</a></h1>
|
||||||
id="supabase-env"
|
<p class="text-sm mt-1">Add or import new AI prompts to the catalog</p>
|
||||||
data-url={supabaseUrl}
|
</header>
|
||||||
data-key={supabaseKey}
|
|
||||||
hidden
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="border-b p-4">
|
<main class="flex flex-col lg:flex-row">
|
||||||
<h1 class="text-2xl font-bold">Prompt Catalog - Add New Prompt</h1>
|
<Sidebar />
|
||||||
<p class="text-sm mt-1">Add or import new AI prompts to the catalog</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex h-screen">
|
<div class="border-b lg:border-l flex-1 flex flex-col overflow-hidden">
|
||||||
<Sidebar />
|
<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>
|
||||||
|
<div id="error" class="bg-red-100 text-red-700 p-4 rounded mb-4 hidden"></div>
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<form id="add-form" class="space-y-4">
|
||||||
<main class="flex-1 overflow-y-auto p-4">
|
<div>
|
||||||
<div id="success" class="bg-green-100 text-green-700 p-4 rounded mb-4 hidden">
|
<label for="title" class="block text-md font-semibold mb-1">Title<span class="text-red-600">*</span></label>
|
||||||
Prompt added successfully!
|
<input name="title" id="title" required class="border p-2 w-full rounded" />
|
||||||
|
<input type="hidden" name="slug" id="slug" />
|
||||||
</div>
|
</div>
|
||||||
<div id="error" class="bg-red-100 text-red-700 p-4 rounded mb-4 hidden"></div>
|
|
||||||
|
|
||||||
<form id="add-form" class="space-y-4">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="title" class="block text-md font-semibold mb-1">Title<span class="text-red-600">*</span></label>
|
<label for="type" class="block text-md font-semibold mb-1">Type<span class="text-red-600">*</span></label>
|
||||||
<input name="title" id="title" required class="border p-2 w-full rounded" />
|
<select name="type" id="type" required class="border border-gray-100 p-2 rounded w-full bg-gray-800">
|
||||||
<input type="hidden" name="slug" id="slug" />
|
<option value="System">System</option>
|
||||||
</div>
|
<option value="Task">Task</option>
|
||||||
|
</select>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label for="type" class="block text-md font-semibold mb-1">Type<span class="text-red-600">*</span></label>
|
|
||||||
<select name="type" id="type" required class="border border-gray-100 p-2 rounded w-full bg-gray-800">
|
|
||||||
<option value="System">System</option>
|
|
||||||
<option value="Task">Task</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="tags" class="block text-md font-semibold mb-1">Tags (comma-separated)</label>
|
|
||||||
<input name="tags" id="tags" class="border p-2 pt-1 w-full rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="description" class="block text-md font-semibold mb-1">Prompt<span class="text-red-600">*</span></label>
|
<label for="tags" class="block text-md font-semibold mb-1">Tags (comma-separated)</label>
|
||||||
<textarea name="description" id="description" rows="6" required class="border p-2 w-full rounded"></textarea>
|
<input name="tags" id="tags" class="border p-2 pt-1 w-full rounded" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="description" class="block text-md font-semibold mb-1">Prompt<span class="text-red-600">*</span></label>
|
||||||
|
<textarea name="description" id="description" rows="6" required class="border p-2 w-full rounded"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="notes" class="block text-md font-semibold mb-1">Description<span class="text-red-600">*</span></label>
|
||||||
|
<textarea name="notes" id="notes" rows="3" required class="border p-2 w-full rounded"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors duration-300 cursor-pointer">
|
||||||
|
Add Prompt
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<hr class="my-6 border-gray-600" />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="notes" class="block text-md font-semibold mb-1">Description<span class="text-red-600">*</span></label>
|
<h2 class="text-lg font-semibold mb-2">Import Prompts</h2>
|
||||||
<textarea name="notes" id="notes" rows="3" required class="border p-2 w-full rounded"></textarea>
|
<p class="text-sm text-gray-400 mb-2">Upload a JSON file to import prompts.</p>
|
||||||
|
|
||||||
|
<label for="importFile" class="block text-md font-semibold mb-1">Import Prompts from JSON</label>
|
||||||
|
<input type="file" id="importFile" accept="application/json" class="border p-2 w-full rounded bg-gray-800 text-white" />
|
||||||
|
<button type="button" id="importBtn" class="mt-2 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition-colors duration-300 cursor-pointer">
|
||||||
|
Import Prompts
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors duration-300 cursor-pointer">
|
<div id="export-section">
|
||||||
Add Prompt
|
<h2 class="text-lg font-semibold mb-2">Export Prompts</h2>
|
||||||
</button>
|
<p class="text-sm text-gray-400 mb-2">Select prompts to export, or leave all unchecked to export everything.</p>
|
||||||
|
|
||||||
<hr class="my-6 border-gray-600" />
|
<label class="block text-md font-semibold mb-1">Export Prompts to JSON</label>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<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">
|
||||||
<div>
|
Export All Prompts
|
||||||
<h2 class="text-lg font-semibold mb-2">Import Prompts</h2>
|
</button>
|
||||||
<p class="text-sm text-gray-400 mb-2">Upload a JSON file to import prompts.</p>
|
|
||||||
|
|
||||||
<label for="importFile" class="block text-md font-semibold mb-1">Import Prompts from JSON</label>
|
|
||||||
<input type="file" id="importFile" accept="application/json" class="border p-2 w-full rounded bg-gray-800 text-white" />
|
|
||||||
<button type="button" id="importBtn" class="mt-2 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition-colors duration-300 cursor-pointer">
|
|
||||||
Import Prompts
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="export-section">
|
|
||||||
<h2 class="text-lg font-semibold mb-2">Export Prompts</h2>
|
|
||||||
<p class="text-sm text-gray-400 mb-2">Select prompts to export, or leave all unchecked to export everything.</p>
|
|
||||||
|
|
||||||
<label class="block text-md font-semibold mb-1">Export Prompts to JSON</label>
|
|
||||||
<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
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</main>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<script type="module">
|
<footer class="border-t p-4 text-center text-sm">
|
||||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
© {new Date().getFullYear()} Prompt Catalog
|
||||||
|
</footer>
|
||||||
|
|
||||||
const SUPABASE_URL = document.getElementById('supabase-env').dataset.url;
|
<script type="module">
|
||||||
const SUPABASE_ANON_KEY = document.getElementById('supabase-env').dataset.key;
|
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||||
|
|
||||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
const SUPABASE_URL = document.getElementById('supabase-env').dataset.url;
|
||||||
|
const SUPABASE_ANON_KEY = document.getElementById('supabase-env').dataset.key;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||||
const form = document.getElementById('add-form');
|
|
||||||
const successBox = document.getElementById('success');
|
|
||||||
const errorBox = document.getElementById('error');
|
|
||||||
|
|
||||||
document.getElementById('title').addEventListener('input', (e) => {
|
const form = document.getElementById('add-form');
|
||||||
const value = e.target.value
|
const importBtn = document.getElementById('importBtn');
|
||||||
.toLowerCase()
|
const exportBtn = document.getElementById('exportBtn');
|
||||||
.replace(/[^\w\s-]/g, '')
|
const successBox = document.getElementById('success');
|
||||||
.replace(/\s+/g, '-')
|
const errorBox = document.getElementById('error');
|
||||||
.trim();
|
|
||||||
document.getElementById('slug').value = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
form.addEventListener('submit', async (e) => {
|
async function loadPromptCheckboxes() {
|
||||||
e.preventDefault();
|
const promptList = document.getElementById('prompt-list');
|
||||||
|
const { data, error } = await supabase.from('prompts').select('id, title');
|
||||||
|
|
||||||
successBox.style.display = 'none';
|
if (error) {
|
||||||
errorBox.style.display = 'none';
|
promptList.innerHTML = `<p class="text-red-400">Failed to load prompts: ${error.message}</p>`;
|
||||||
|
return;
|
||||||
const formData = new FormData(form);
|
|
||||||
const payload = {
|
|
||||||
title: formData.get('title'),
|
|
||||||
slug: formData.get('slug'),
|
|
||||||
type: formData.get('type'),
|
|
||||||
description: formData.get('description'),
|
|
||||||
tags: formData.get('tags')?.split(',').map(t => t.trim()).filter(Boolean),
|
|
||||||
notes: formData.get('notes')
|
|
||||||
};
|
|
||||||
|
|
||||||
const { error } = await supabase.from('prompts').insert([payload]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
errorBox.innerText = error.message;
|
|
||||||
errorBox.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
successBox.style.display = 'block';
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('importBtn').addEventListener('click', async () => {
|
|
||||||
const fileInput = document.getElementById('importFile');
|
|
||||||
const file = fileInput.files[0];
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
alert('Please select a JSON file.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const successBox = document.getElementById('success');
|
|
||||||
const errorBox = document.getElementById('error');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = await file.text();
|
|
||||||
const prompts = JSON.parse(text);
|
|
||||||
|
|
||||||
const formatted = prompts.map(p => ({
|
|
||||||
type: p.type.charAt(0).toUpperCase() + p.type.slice(1), // normalize to "System"/"Task"
|
|
||||||
title: p.title,
|
|
||||||
description: p.description,
|
|
||||||
tags: p.tags.replace(/[{}"]/g, '').split(',').map(t => t.trim()), // parse tag string
|
|
||||||
notes: p.notes,
|
|
||||||
slug: p.title
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\w\s-]/g, '')
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.trim()
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { error } = await supabase.from('prompts').insert(formatted);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
errorBox.innerText = `Import failed: ${error.message}`;
|
|
||||||
errorBox.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
successBox.innerText = 'Prompts imported successfully!';
|
|
||||||
successBox.style.display = 'block';
|
|
||||||
fileInput.value = '';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
errorBox.innerText = `Error: ${err.message}`;
|
|
||||||
errorBox.style.display = 'block';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 () => {
|
promptList.innerHTML = data
|
||||||
const checkboxes = Array.from(document.querySelectorAll('#prompt-list input[type="checkbox"]'));
|
.map(
|
||||||
const selectedIds = checkboxes.filter(cb => cb.checked).map(cb => cb.value);
|
prompt => `
|
||||||
|
<label class="block mb-0">
|
||||||
|
<input type="checkbox" value="${prompt.id}" class="mr-0" />
|
||||||
|
${prompt.title}
|
||||||
|
</label>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
const { data, error } = selectedIds.length > 0
|
// After rendering, attach change listeners to checkboxes
|
||||||
? await supabase.from('prompts').select('*').in('id', selectedIds)
|
const checkboxes = Array.from(document.querySelectorAll('#prompt-list input[type="checkbox"]'));
|
||||||
: await supabase.from('prompts').select('*');
|
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()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.trim();
|
||||||
|
document.getElementById('slug').value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
const successBox = document.getElementById('success');
|
|
||||||
const errorBox = document.getElementById('error');
|
|
||||||
successBox.style.display = 'none';
|
successBox.style.display = 'none';
|
||||||
errorBox.style.display = 'none';
|
errorBox.style.display = 'none';
|
||||||
|
|
||||||
if (error) {
|
const formData = new FormData(form);
|
||||||
errorBox.innerText = `Export failed: ${error.message}`;
|
const payload = {
|
||||||
errorBox.style.display = 'block';
|
title: formData.get('title'),
|
||||||
return;
|
slug: formData.get('slug'),
|
||||||
}
|
type: formData.get('type'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
tags: formData.get('tags')?.split(',').map(t => t.trim()).filter(Boolean),
|
||||||
|
notes: formData.get('notes')
|
||||||
|
};
|
||||||
|
|
||||||
const output = data.map(p => ({
|
const { error } = await supabase.from('prompts').insert([payload]);
|
||||||
id: p.id,
|
|
||||||
type: p.type.toLowerCase(),
|
if (error) {
|
||||||
|
errorBox.innerText = error.message;
|
||||||
|
errorBox.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
successBox.style.display = 'block';
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
importBtn.addEventListener('click', async () => {
|
||||||
|
const fileInput = document.getElementById('importFile');
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
alert('Please select a JSON file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const successBox = document.getElementById('success');
|
||||||
|
const errorBox = document.getElementById('error');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const prompts = JSON.parse(text);
|
||||||
|
|
||||||
|
const formatted = prompts.map(p => ({
|
||||||
|
type: p.type.charAt(0).toUpperCase() + p.type.slice(1), // normalize to "System"/"Task"
|
||||||
title: p.title,
|
title: p.title,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
tags: `{${(p.tags || []).join(',')}}`,
|
tags: p.tags.replace(/[{}"]/g, '').split(',').map(t => t.trim()), // parse tag string
|
||||||
createdat: p.createdat,
|
notes: p.notes,
|
||||||
notes: p.notes || ''
|
slug: p.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.trim()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(output, null, 2)], {
|
const { error } = await supabase.from('prompts').insert(formatted);
|
||||||
type: 'application/json'
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = URL.createObjectURL(blob);
|
if (error) {
|
||||||
const link = document.createElement('a');
|
errorBox.innerText = `Import failed: ${error.message}`;
|
||||||
link.href = url;
|
errorBox.style.display = 'block';
|
||||||
link.download = selectedIds.length > 0 ? 'selected-prompts.json' : 'all-prompts.json';
|
} else {
|
||||||
link.click();
|
successBox.innerText = 'Prompts imported successfully!';
|
||||||
URL.revokeObjectURL(url);
|
successBox.style.display = 'block';
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorBox.innerText = `Error: ${err.message}`;
|
||||||
|
errorBox.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const { data, error } = selectedIds.length > 0
|
||||||
|
? await supabase.from('prompts').select('*').in('id', selectedIds)
|
||||||
|
: await supabase.from('prompts').select('*');
|
||||||
|
|
||||||
|
successBox.style.display = 'none';
|
||||||
|
errorBox.style.display = 'none';
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
errorBox.innerText = `Export failed: ${error.message}`;
|
||||||
|
errorBox.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = data.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
type: p.type.toLowerCase(),
|
||||||
|
title: p.title,
|
||||||
|
description: p.description,
|
||||||
|
tags: `{${(p.tags || []).join(',')}}`,
|
||||||
|
createdat: p.createdat,
|
||||||
|
notes: p.notes || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(output, null, 2)], {
|
||||||
|
type: 'application/json'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load checkboxes on DOM load
|
const url = URL.createObjectURL(blob);
|
||||||
loadPromptCheckboxes();
|
const link = document.createElement('a');
|
||||||
</script>
|
link.href = url;
|
||||||
</body>
|
link.download = selectedIds.length > 0 ? 'selected-prompts.json' : 'all-prompts.json';
|
||||||
</html>
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</MainLayout>
|
||||||
|
|||||||
@@ -1,156 +1,154 @@
|
|||||||
---
|
---
|
||||||
import "../styles/global.css";
|
import MainLayout from '../layouts/MainLayout.astro';
|
||||||
import Sidebar from "../components/SidebarEdit.astro";
|
import Sidebar from "../components/SidebarEdit.astro";
|
||||||
|
|
||||||
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
|
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL;
|
||||||
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<MainLayout page="Edit Prompt">
|
||||||
<head>
|
<div
|
||||||
<title>Prompt Catalog - Edit Prompt</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="font-sans antialiased bg-gray-800 text-gray-100">
|
|
||||||
<div
|
|
||||||
id="supabase-env"
|
id="supabase-env"
|
||||||
data-url={supabaseUrl}
|
data-url={supabaseUrl}
|
||||||
data-key={supabaseKey}
|
data-key={supabaseKey}
|
||||||
hidden
|
hidden
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="border-b p-4">
|
<header class="border-b p-4">
|
||||||
<h1 class="text-2xl font-bold">Prompt Catalog - Add New Prompt</h1>
|
<h1 class="text-2xl font-bold"><a href="/">Prompt Catalog - Edit Prompt</a></h1>
|
||||||
<p class="text-sm mt-1">Add a new AI prompt to the catalog</p>
|
<p class="text-sm mt-1">Edit an existing AI prompt in the catalog</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex flex-col lg:flex-row">
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="flex h-screen">
|
<footer class="border-t p-4 text-center text-sm">
|
||||||
<Sidebar />
|
© {new Date().getFullYear()} Prompt Catalog
|
||||||
|
</footer>
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<script type="module">
|
||||||
<main class="flex-1 overflow-y-auto p-4">
|
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||||
<div id="edit-root">Loading...</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module">
|
const env = document.getElementById('supabase-env').dataset;
|
||||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
const supabase = createClient(env.url, env.key);
|
||||||
|
const slug = new URLSearchParams(window.location.search).get('slug');
|
||||||
|
|
||||||
const env = document.getElementById('supabase-env').dataset;
|
const root = document.getElementById('edit-root');
|
||||||
const supabase = createClient(env.url, env.key);
|
|
||||||
const slug = new URLSearchParams(window.location.search).get('slug');
|
|
||||||
|
|
||||||
const root = document.getElementById('edit-root');
|
if (!slug) {
|
||||||
|
root.innerHTML = `<p class="text-red-600 font-medium">❌ No slug provided.</p>`;
|
||||||
|
} else {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('prompts')
|
||||||
|
.select('*')
|
||||||
|
.eq('slug', slug)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!slug) {
|
const prompt = data?.[0];
|
||||||
root.innerHTML = `<p class="text-red-600 font-medium">❌ No slug provided.</p>`;
|
|
||||||
|
if (error || !prompt) {
|
||||||
|
root.innerHTML = `<p class="text-red-600 font-medium">❌ Prompt not found or error loading it.</p>`;
|
||||||
} else {
|
} else {
|
||||||
const { data, error } = await supabase
|
root.innerHTML = `
|
||||||
.from('prompts')
|
<form id="edit-form" class="space-y-4">
|
||||||
.select('*')
|
<input type="hidden" name="slug" value="${prompt.slug}" />
|
||||||
.eq('slug', slug)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const prompt = data?.[0];
|
<div>
|
||||||
|
<label for="title" class="block text-md font-semibold mb-1">Title</label>
|
||||||
if (error || !prompt) {
|
<input name="title" id="title" value="${prompt.title}" required class="border p-2 w-full rounded" />
|
||||||
root.innerHTML = `<p class="text-red-600 font-medium">❌ Prompt not found or error loading it.</p>`;
|
</div>
|
||||||
} else {
|
|
||||||
root.innerHTML = `
|
|
||||||
<form id="edit-form" class="space-y-4">
|
|
||||||
<input type="hidden" name="slug" value="${prompt.slug}" />
|
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="title" class="block text-md font-semibold mb-1">Title</label>
|
<label for="type" class="block text-md font-semibold mb-1">Type</label>
|
||||||
<input name="title" id="title" value="${prompt.title}" required class="border p-2 w-full rounded" />
|
<select name="type" id="type" required class="border p-2 w-full rounded">
|
||||||
</div>
|
<option value="System" ${prompt.type === 'System' ? 'selected' : ''}>System</option>
|
||||||
|
<option value="Task" ${prompt.type === 'Task' ? 'selected' : ''}>Task</option>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
</select>
|
||||||
<div>
|
|
||||||
<label for="type" class="block text-md font-semibold mb-1">Type</label>
|
|
||||||
<select name="type" id="type" required class="border p-2 w-full rounded">
|
|
||||||
<option value="System" ${prompt.type === 'System' ? 'selected' : ''}>System</option>
|
|
||||||
<option value="Task" ${prompt.type === 'Task' ? 'selected' : ''}>Task</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="tags" class="block text-md font-semibold mb-1">Tags (comma-separated)</label>
|
|
||||||
<input name="tags" id="tags" value="${(prompt.tags ?? []).join(', ')}" class="border p-2 pt-1 w-full rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="description" class="block text-md font-semibold mb-1">Prompt</label>
|
<label for="tags" class="block text-md font-semibold mb-1">Tags (comma-separated)</label>
|
||||||
<textarea name="description" id="description" rows="6" required class="border p-2 w-full rounded">${prompt.description}</textarea>
|
<input name="tags" id="tags" value="${(prompt.tags ?? []).join(', ')}" class="border p-2 pt-1 w-full rounded" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="notes" class="block text-md font-semibold mb-1">Description</label>
|
<label for="description" class="block text-md font-semibold mb-1">Prompt</label>
|
||||||
<textarea name="notes" id="notes" rows="3" class="border p-2 w-full rounded">${prompt.notes ?? ''}</textarea>
|
<textarea name="description" id="description" rows="6" required class="border p-2 w-full rounded">${prompt.description}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors duration-300 cursor-pointer">
|
<div>
|
||||||
Save Changes
|
<label for="notes" class="block text-md font-semibold mb-1">Description</label>
|
||||||
|
<textarea name="notes" id="notes" rows="3" class="border p-2 w-full rounded">${prompt.notes ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors duration-300 cursor-pointer">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center pt-4 border-t mt-6">
|
||||||
|
<span class="text-lg text-red-400 font-bold">Be careful! Deletion is permanent.</span>
|
||||||
|
<button
|
||||||
|
id="delete-btn"
|
||||||
|
type="button"
|
||||||
|
class="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors duration-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
Delete Prompt
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="flex justify-between items-center pt-4 border-t mt-6">
|
<div id="success" class="text-green-600 mt-4 hidden">Prompt Updated.</div>
|
||||||
<span class="text-lg text-red-400 font-bold">Be careful! Deletion is permanent.</span>
|
<div id="error" class="text-red-600 mt-4 hidden"></div>
|
||||||
<button
|
`;
|
||||||
id="delete-btn"
|
|
||||||
type="button"
|
|
||||||
class="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors duration-300 cursor-pointer"
|
|
||||||
>
|
|
||||||
Delete Prompt
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="success" class="text-green-600 mt-4 hidden">Prompt Updated.</div>
|
document.getElementById('edit-form').addEventListener('submit', async (e) => {
|
||||||
<div id="error" class="text-red-600 mt-4 hidden"></div>
|
e.preventDefault();
|
||||||
`;
|
const formData = new FormData(e.target);
|
||||||
|
const payload = {
|
||||||
|
title: formData.get('title'),
|
||||||
|
type: formData.get('type'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
tags: formData.get('tags')?.split(',').map(t => t.trim()).filter(Boolean),
|
||||||
|
notes: formData.get('notes')
|
||||||
|
};
|
||||||
|
|
||||||
document.getElementById('edit-form').addEventListener('submit', async (e) => {
|
const { error: updateError } = await supabase
|
||||||
e.preventDefault();
|
.from('prompts')
|
||||||
const formData = new FormData(e.target);
|
.update(payload)
|
||||||
const payload = {
|
.eq('slug', slug);
|
||||||
title: formData.get('title'),
|
|
||||||
type: formData.get('type'),
|
|
||||||
description: formData.get('description'),
|
|
||||||
tags: formData.get('tags')?.split(',').map(t => t.trim()).filter(Boolean),
|
|
||||||
notes: formData.get('notes')
|
|
||||||
};
|
|
||||||
|
|
||||||
const { error: updateError } = await supabase
|
document.getElementById('success').style.display = updateError ? 'none' : 'block';
|
||||||
.from('prompts')
|
document.getElementById('error').innerText = updateError?.message || '';
|
||||||
.update(payload)
|
document.getElementById('error').style.display = updateError ? 'block' : 'none';
|
||||||
.eq('slug', slug);
|
});
|
||||||
|
|
||||||
document.getElementById('success').style.display = updateError ? 'none' : 'block';
|
document.getElementById('delete-btn').addEventListener('click', async () => {
|
||||||
document.getElementById('error').innerText = updateError?.message || '';
|
const confirmed = window.confirm("Are you sure you want to delete this prompt? This cannot be undone.");
|
||||||
document.getElementById('error').style.display = updateError ? 'block' : 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('delete-btn').addEventListener('click', async () => {
|
if (!confirmed) return;
|
||||||
const confirmed = window.confirm("Are you sure you want to delete this prompt? This cannot be undone.");
|
|
||||||
|
|
||||||
if (!confirmed) return;
|
const { error: deleteError } = await supabase
|
||||||
|
.from('prompts')
|
||||||
|
.delete()
|
||||||
|
.eq('slug', slug);
|
||||||
|
|
||||||
const { error: deleteError } = await supabase
|
if (deleteError) {
|
||||||
.from('prompts')
|
document.getElementById('error').innerText = deleteError.message;
|
||||||
.delete()
|
document.getElementById('error').style.display = 'block';
|
||||||
.eq('slug', slug);
|
} else {
|
||||||
|
window.location.href = '/';
|
||||||
if (deleteError) {
|
}
|
||||||
document.getElementById('error').innerText = deleteError.message;
|
});
|
||||||
document.getElementById('error').style.display = 'block';
|
|
||||||
} else {
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
}
|
||||||
</body>
|
</script>
|
||||||
</html>
|
</MainLayout>
|
||||||
|
|||||||
@@ -13,24 +13,28 @@ const allTags = Array.from(
|
|||||||
new Set(
|
new Set(
|
||||||
prompts?.flatMap((p) => p.tags ?? [])
|
prompts?.flatMap((p) => p.tags ?? [])
|
||||||
)
|
)
|
||||||
).sort();
|
).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout>
|
<MainLayout page="Home">
|
||||||
<div class="border-b p-4">
|
<header class="border-b p-4">
|
||||||
<h1 class="text-2xl font-bold">Prompt Catalog</h1>
|
<h1 class="text-2xl font-bold"><a href="/">Prompt Catalog</a></h1>
|
||||||
<p class="text-sm mt-1">Explore and filter AI prompts</p>
|
<p class="text-sm mt-1">Save and explore AI prompts</p>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<div class="flex h-screen">
|
<main class="flex flex-col lg:flex-row">
|
||||||
<SearchBar allTags={allTags} />
|
<SearchBar allTags={allTags} />
|
||||||
|
|
||||||
<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 class="flex-1 overflow-y-auto px-4 lg:px-6 pt-2 pb-4">
|
||||||
{error
|
{error
|
||||||
? <p class="text-red-500">Supabase error: {error.message}</p>
|
? <p class="text-red-500">Supabase error: {error.message}</p>
|
||||||
: <FilteredPromptList prompts={prompts} />}
|
: <FilteredPromptList prompts={prompts} />}
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
|
<footer class="border-t p-4 text-center text-sm">
|
||||||
|
© {new Date().getFullYear()} Prompt Catalog
|
||||||
|
</footer>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
2
supabase.env.example
Normal file
2
supabase.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
PUBLIC_SUPABASE_URL=<your Supabase url>
|
||||||
|
PUBLIC_SUPABASE_ANON_KEY=<your Supabase anon key>
|
||||||
Reference in New Issue
Block a user