✨feature: Change name, set up for docker build
This commit is contained in:
12
app/astro.config.ts
Normal file
12
app/astro.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import react from '@astrojs/react';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [react()],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
7501
app/package-lock.json
generated
Normal file
7501
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
app/package.json
Normal file
24
app/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "prompt-catalog",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/node": "^9.3.0",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@supabase/supabase-js": "^2.51.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"alpinejs": "^3.14.9",
|
||||
"astro": "^5.11.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
0
app/public/css/styles.css
Normal file
0
app/public/css/styles.css
Normal file
9
app/public/favicon.svg
Normal file
9
app/public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
9
app/public/import-sample.json
Normal file
9
app/public/import-sample.json
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"type": "Task or System (ONLY one)",
|
||||
"title": "ActiveCollab Github Action",
|
||||
"description": "This is your prompt content.",
|
||||
"tags": "{These,Are,Tags}",
|
||||
"notes": "This is your prompt description."
|
||||
}
|
||||
]
|
||||
71
app/src/components/FilteredPromptList.astro
Normal file
71
app/src/components/FilteredPromptList.astro
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
import PromptCard from './PromptCard.astro';
|
||||
|
||||
const { prompts = [] } = Astro.props;
|
||||
|
||||
type Prompt = {
|
||||
slug: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
tags?: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
notes?: string;
|
||||
};
|
||||
---
|
||||
|
||||
<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 ↓</a>
|
||||
</div>
|
||||
|
||||
<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} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
const form = document.getElementById('filter-form');
|
||||
const cards = document.querySelectorAll('.prompt-card');
|
||||
|
||||
const clearBtn = document.getElementById('clear-filters');
|
||||
clearBtn?.addEventListener('click', () => {
|
||||
form.reset();
|
||||
filterCards();
|
||||
});
|
||||
|
||||
function filterCards() {
|
||||
const params = new URLSearchParams(new FormData(form));
|
||||
const type = params.get('type');
|
||||
const query = params.get('q')?.toLowerCase() || '';
|
||||
const tagParams = params.getAll('tag');
|
||||
|
||||
cards.forEach(card => {
|
||||
const cardType = card.dataset.type;
|
||||
const cardTags = card.dataset.tags.split(',');
|
||||
const cardText = card.innerText.toLowerCase();
|
||||
|
||||
const matchesType = !type || type === cardType;
|
||||
const matchesTags = tagParams.length === 0 || tagParams.some(t => cardTags.includes(t));
|
||||
const matchesSearch = !query || cardText.includes(query);
|
||||
|
||||
card.style.display = matchesType && matchesTags && matchesSearch ? 'block' : 'none';
|
||||
|
||||
const visibleCount = Array.from(cards).filter(c => c.style.display !== 'none').length;
|
||||
|
||||
document.getElementById('prompt-count').textContent =
|
||||
visibleCount === 1
|
||||
? "1 prompt shown"
|
||||
: `${visibleCount} prompts shown`;
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('input', filterCards);
|
||||
|
||||
// Trigger filter once on page load
|
||||
filterCards();
|
||||
</script>
|
||||
59
app/src/components/PromptCard.astro
Normal file
59
app/src/components/PromptCard.astro
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
const {
|
||||
slug,
|
||||
title,
|
||||
type,
|
||||
description,
|
||||
tags = [],
|
||||
created_at,
|
||||
updated_at,
|
||||
notes,
|
||||
} = Astro.props;
|
||||
|
||||
const formatDate = (dateStr: string | undefined) => {
|
||||
if (!dateStr) return "–";
|
||||
const date = new Date(dateStr);
|
||||
return isNaN(date.getTime())
|
||||
? "Invalid date"
|
||||
: date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
---
|
||||
|
||||
<div class="border border-gray-400 rounded p-4 bg-gray-700 text-gray-200 shadow-sm flex flex-col gap-2 min-h-[12rem]">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-xl font-semibold">{title}</h3>
|
||||
<span class={`text-sm font-medium px-2 py-1 rounded ${
|
||||
type === 'System' ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-md">{notes}</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{tags.map((tag: string) => (
|
||||
<span class="text-sm bg-gray-200 text-gray-800 px-2 py-1 pt-0 rounded">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<details name="prompt-details">
|
||||
<summary class="cursor-pointer font-semibold mt-2 text-lg">View Details</summary>
|
||||
<div class="text-md border-t mt-2 pt-2">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<p class="my-2 px-2 text-balance" set:html={description.replace(/\n/g, '<br />')} />
|
||||
|
||||
<hr class="my-2" />
|
||||
<p class="text-sm"><strong>Created:</strong> {formatDate(created_at)} • <strong>Updated:</strong> {formatDate(updated_at)}</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
0
app/src/components/PromptDetail.astro
Normal file
0
app/src/components/PromptDetail.astro
Normal file
70
app/src/components/PromptList.astro
Normal file
70
app/src/components/PromptList.astro
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
import PromptCard from './PromptCard.astro';
|
||||
|
||||
const { prompts, error } = Astro.props;
|
||||
|
||||
const typeFilter = typeof window !== "undefined"
|
||||
? new URLSearchParams(window.location.search).get("type")
|
||||
: null;
|
||||
|
||||
const tagFilter = typeof window !== "undefined"
|
||||
? new URLSearchParams(window.location.search).get("tag")
|
||||
: null;
|
||||
|
||||
console.log("🔍 searchParams:", typeFilter, tagFilter);
|
||||
|
||||
type Prompt = {
|
||||
slug: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
tags?: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
const filtered = prompts?.filter((p: Prompt) => {
|
||||
return (!typeFilter || p.type === typeFilter) &&
|
||||
(!tagFilter || p.tags?.includes(tagFilter));
|
||||
});
|
||||
---
|
||||
|
||||
{error ? (
|
||||
<p class="text-red-500">Failed to load prompts: {error.message}</p>
|
||||
) : (
|
||||
<form class="mb-4 flex gap-4 items-end" method="GET">
|
||||
<div>
|
||||
<label for="type" class="block text-sm font-medium">Type</label>
|
||||
<select name="type" id="type" class="border p-2 rounded w-full bg-gray-800">
|
||||
<option value="">All</option>
|
||||
<option value="System" selected={typeFilter === 'System'}>System</option>
|
||||
<option value="Task" selected={typeFilter === 'Task'}>Task</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="tag" class="block text-sm font-medium">Tag</label>
|
||||
<input type="text" name="tag" id="tag" class="border p-2 rounded w-full" value={tagFilter || ''} />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
|
||||
Filter
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filtered?.map((prompt: Prompt) => (
|
||||
<PromptCard
|
||||
slug={prompt.slug}
|
||||
title={prompt.title}
|
||||
type={prompt.type}
|
||||
description={prompt.description}
|
||||
tags={prompt.tags}
|
||||
createdAt={prompt.created_at}
|
||||
updatedAt={prompt.updated_at}
|
||||
notes={prompt.notes}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
60
app/src/components/SearchBar.astro
Normal file
60
app/src/components/SearchBar.astro
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
// SearchBar.astro
|
||||
|
||||
// TODO: Add options for AND/OR switching and sort by options
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
<form id="filter-form" method="GET">
|
||||
<div>
|
||||
<label for="q" class="block text-lg font-semibold mb-1">Search</label>
|
||||
<input
|
||||
name="q"
|
||||
id="q"
|
||||
type="text"
|
||||
placeholder="Search prompts..."
|
||||
class="w-full p-2 border rounded bg-gray-300 text-gray-800 focus:outline-none focus:ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex flex-col gap-8 items-start">
|
||||
<div>
|
||||
<label for="type" class="block text-lg font-semibold mb-1">Type</label>
|
||||
<select name="type" id="type" class="border p-2 rounded w-full bg-gray-800">
|
||||
<option value="">All</option>
|
||||
<option value="System">System</option>
|
||||
<option value="Task">Task</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<fieldset class="flex flex-col">
|
||||
<legend class="block text-lg font-semibold mb-1">Tags</legend>
|
||||
<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
|
||||
type="checkbox"
|
||||
name="tag"
|
||||
value={tag}
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<span class="px-3 py-1 pt-0 rounded-full border border-gray-300 peer-checked:bg-blue-600 peer-checked:text-white hover:bg-blue-600 hover:text-white transition-colors duration-300 text-sm">
|
||||
{tag}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
</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">
|
||||
Reset Filters
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
40
app/src/components/SidebarAdd.astro
Normal file
40
app/src/components/SidebarAdd.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
// Sidebar.astro
|
||||
|
||||
import { Code } from 'astro:components';
|
||||
|
||||
const code = `[
|
||||
{
|
||||
"type": "System or Task (ONLY one)",
|
||||
"title": "Example Prompt",
|
||||
"description": "This is an example prompt.",
|
||||
"tags": ["example", "test"],
|
||||
"notes": "The prompt description."
|
||||
}
|
||||
]
|
||||
`;
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
<p class="text-sm mt-2">All fields marked with <span class="text-red-600">*</span> are required.</p>
|
||||
|
||||
<h2 class="text-lg font-semibold mt-6 mb-4">Import</h2>
|
||||
|
||||
<p class="text-sm">To import prompts from a JSON file, upload a JSON file formatted as below, or download a sample <a class="underline" download href="/import-sample.json">here</a>.</p>
|
||||
|
||||
<Code class="bg-gray-700 rounded text-sm p-2 mt-4" code={code} lang="json" wrap />
|
||||
|
||||
<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 the checkboxes.</p>
|
||||
|
||||
<p class="text-sm mt-2">To export all prompts, leave all checkboxes unchecked.</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>
|
||||
</aside>
|
||||
13
app/src/components/SidebarEdit.astro
Normal file
13
app/src/components/SidebarEdit.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
// Sidebar.astro
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</aside>
|
||||
18
app/src/layouts/MainLayout.astro
Normal file
18
app/src/layouts/MainLayout.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
// MainLayout.astro
|
||||
import "../styles/global.css";
|
||||
|
||||
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 - {page}</title>
|
||||
</head>
|
||||
|
||||
<body class="font-sans antialiased bg-gray-800 text-gray-100">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
5
app/src/lib/supabase.js
Normal file
5
app/src/lib/supabase.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL
|
||||
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY
|
||||
export const supabase = createClient(supabaseUrl, supabaseKey)
|
||||
274
app/src/pages/add.astro
Normal file
274
app/src/pages/add.astro
Normal file
@@ -0,0 +1,274 @@
|
||||
---
|
||||
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;
|
||||
---
|
||||
|
||||
<MainLayout page="Add Prompt">
|
||||
<div
|
||||
id="supabase-env"
|
||||
data-url={supabaseUrl}
|
||||
data-key={supabaseKey}
|
||||
hidden
|
||||
></div>
|
||||
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<main class="flex flex-col lg:flex-row">
|
||||
<Sidebar />
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<label for="title" class="block text-md font-semibold mb-1">Title<span class="text-red-600">*</span></label>
|
||||
<input name="title" id="title" required class="border p-2 w-full rounded" />
|
||||
<input type="hidden" name="slug" id="slug" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<h2 class="text-lg font-semibold mb-2">Import Prompts</h2>
|
||||
<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 All Prompts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="border-t p-4 text-center text-sm">
|
||||
© {new Date().getFullYear()} Prompt Catalog
|
||||
</footer>
|
||||
|
||||
<script type="module">
|
||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||
|
||||
const SUPABASE_URL = document.getElementById('supabase-env').dataset.url;
|
||||
const SUPABASE_ANON_KEY = document.getElementById('supabase-env').dataset.key;
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||
|
||||
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()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.trim();
|
||||
document.getElementById('slug').value = value;
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
successBox.style.display = 'none';
|
||||
errorBox.style.display = 'none';
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = selectedIds.length > 0 ? 'selected-prompts.json' : 'all-prompts.json';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
</script>
|
||||
</MainLayout>
|
||||
154
app/src/pages/edit.astro
Normal file
154
app/src/pages/edit.astro
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
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;
|
||||
---
|
||||
|
||||
<MainLayout page="Edit Prompt">
|
||||
<div
|
||||
id="supabase-env"
|
||||
data-url={supabaseUrl}
|
||||
data-key={supabaseKey}
|
||||
hidden
|
||||
></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>
|
||||
|
||||
<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>
|
||||
</main>
|
||||
|
||||
<footer class="border-t p-4 text-center text-sm">
|
||||
© {new Date().getFullYear()} Prompt Catalog
|
||||
</footer>
|
||||
|
||||
<script type="module">
|
||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||
|
||||
const env = document.getElementById('supabase-env').dataset;
|
||||
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);
|
||||
|
||||
const prompt = data?.[0];
|
||||
|
||||
if (error || !prompt) {
|
||||
root.innerHTML = `<p class="text-red-600 font-medium">❌ Prompt not found or error loading it.</p>`;
|
||||
} else {
|
||||
root.innerHTML = `
|
||||
<form id="edit-form" class="space-y-4">
|
||||
<input type="hidden" name="slug" value="${prompt.slug}" />
|
||||
|
||||
<div>
|
||||
<label for="title" class="block text-md font-semibold mb-1">Title</label>
|
||||
<input name="title" id="title" value="${prompt.title}" required class="border p-2 w-full rounded" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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>
|
||||
<label for="description" class="block text-md font-semibold mb-1">Prompt</label>
|
||||
<textarea name="description" id="description" rows="6" required class="border p-2 w-full rounded">${prompt.description}</textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="success" class="text-green-600 mt-4 hidden">Prompt Updated.</div>
|
||||
<div id="error" class="text-red-600 mt-4 hidden"></div>
|
||||
`;
|
||||
|
||||
document.getElementById('edit-form').addEventListener('submit', async (e) => {
|
||||
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')
|
||||
};
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('prompts')
|
||||
.update(payload)
|
||||
.eq('slug', slug);
|
||||
|
||||
document.getElementById('success').style.display = updateError ? 'none' : 'block';
|
||||
document.getElementById('error').innerText = updateError?.message || '';
|
||||
document.getElementById('error').style.display = updateError ? 'block' : 'none';
|
||||
});
|
||||
|
||||
document.getElementById('delete-btn').addEventListener('click', async () => {
|
||||
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);
|
||||
|
||||
if (deleteError) {
|
||||
document.getElementById('error').innerText = deleteError.message;
|
||||
document.getElementById('error').style.display = 'block';
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</MainLayout>
|
||||
40
app/src/pages/index.astro
Normal file
40
app/src/pages/index.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import MainLayout from '../layouts/MainLayout.astro';
|
||||
import SearchBar from '../components/SearchBar.astro';
|
||||
import FilteredPromptList from '../components/FilteredPromptList.astro';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
const { data: prompts, error } = await supabase
|
||||
.from('prompts')
|
||||
.select('*')
|
||||
.order('title', { ascending: true });
|
||||
|
||||
const allTags = Array.from(
|
||||
new Set(
|
||||
prompts?.flatMap((p) => p.tags ?? [])
|
||||
)
|
||||
).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
<main class="flex flex-col lg:flex-row">
|
||||
<SearchBar allTags={allTags} />
|
||||
|
||||
<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>
|
||||
|
||||
<footer class="border-t p-4 text-center text-sm">
|
||||
© {new Date().getFullYear()} Prompt Catalog
|
||||
</footer>
|
||||
</MainLayout>
|
||||
2
app/src/styles/global.css
Normal file
2
app/src/styles/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
5
app/tsconfig.json
Normal file
5
app/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": ["astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user