✨feature: Filtered card list, connected to Supabase
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
|
import react from '@astrojs/react';
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
integrations: [react()],
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
},
|
},
|
||||||
32
notes/test data.txt
Normal file
32
notes/test data.txt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const prompts = [
|
||||||
|
{
|
||||||
|
slug: "summarize-document",
|
||||||
|
title: "Summarize Document",
|
||||||
|
type: "System",
|
||||||
|
description: "Summarizes a document or long input using GPT-4.",
|
||||||
|
tags: ["summary", "long-form", "NLP"],
|
||||||
|
createdAt: "2025-06-01",
|
||||||
|
updatedAt: "2025-07-10",
|
||||||
|
notes: "Summarizes input using GPT-4 with smart chunking."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "translate-text",
|
||||||
|
title: "Translate Text",
|
||||||
|
type: "Task",
|
||||||
|
description: "Translate English text into French, Spanish, or Japanese.",
|
||||||
|
tags: ["translate", "language"],
|
||||||
|
createdAt: "2025-05-15",
|
||||||
|
updatedAt: "2025-06-22",
|
||||||
|
notes: "Uses multilingual model for more accurate translation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "generate-code",
|
||||||
|
title: "Generate Code",
|
||||||
|
type: "Task",
|
||||||
|
description: "Generate Python or JavaScript functions from descriptions.",
|
||||||
|
tags: ["code", "generation", "devtools"],
|
||||||
|
createdAt: "2025-06-05",
|
||||||
|
updatedAt: "2025-07-01",
|
||||||
|
notes: "Includes language detection and function wrapping."
|
||||||
|
}
|
||||||
|
];
|
||||||
1831
package-lock.json
generated
1831
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,16 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/node": "^9.3.0",
|
||||||
|
"@astrojs/react": "^4.3.0",
|
||||||
"@supabase/supabase-js": "^2.51.0",
|
"@supabase/supabase-js": "^2.51.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"alpinejs": "^3.14.9",
|
"alpinejs": "^3.14.9",
|
||||||
"astro": "^5.11.1",
|
"astro": "^5.11.1",
|
||||||
"tailwindcss": "^4.1.11"
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/components/FilteredPromptList.astro
Normal file
70
src/components/FilteredPromptList.astro
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
import PromptCard from './PromptCard.astro';
|
||||||
|
|
||||||
|
const { prompts = [] } = Astro.props;
|
||||||
|
|
||||||
|
// console.log("📋 FilteredPromptList loaded with prompts:", prompts.length, prompts);
|
||||||
|
|
||||||
|
type Prompt = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
tags?: string[];
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
notes?: string;
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<h3 id="prompt-count" class="text-lg font-semibold text-gray-300 mb-2"></h3>
|
||||||
|
|
||||||
|
<div id="prompt-grid" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg: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>
|
||||||
28
src/components/FilteredPromptList.jsx
Normal file
28
src/components/FilteredPromptList.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import PromptCard from './PromptCard.astro'; // or use Astro version if needed
|
||||||
|
|
||||||
|
export default function FilteredPromptList({ prompts }) {
|
||||||
|
const [filtered, setFiltered] = useState(prompts);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const type = params.get('type');
|
||||||
|
const tag = params.get('tag');
|
||||||
|
|
||||||
|
const result = prompts.filter((p) => {
|
||||||
|
const matchesType = !type || p.type === type;
|
||||||
|
const matchesTag = !tag || (p.tags && p.tags.includes(tag));
|
||||||
|
return matchesType && matchesTag;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFiltered(result);
|
||||||
|
}, [prompts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{filtered.map((p) => (
|
||||||
|
<PromptCard key={p.slug} {...p} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
---
|
---
|
||||||
const {
|
const {
|
||||||
|
slug,
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
description,
|
description,
|
||||||
tags = [],
|
tags = [],
|
||||||
createdAt,
|
created_at,
|
||||||
updatedAt
|
updated_at
|
||||||
} = Astro.props;
|
} = 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 rounded p-4 bg-gray-400 text-gray-800 shadow-sm flex flex-col gap-2">
|
<div class="border rounded p-4 bg-gray-400 text-gray-800 shadow-sm flex flex-col gap-2">
|
||||||
@@ -22,12 +35,16 @@ const {
|
|||||||
<p class="text-sm text-gray-700">{description}</p>
|
<p class="text-sm text-gray-700">{description}</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
{tags.map(tag => (
|
{tags.map((tag: string) => (
|
||||||
<span class="text-xs bg-gray-200 text-gray-800 px-2 py-0.5 rounded">{tag}</span>
|
<span class="text-xs bg-gray-200 text-gray-800 px-2 py-0.5 rounded">{tag}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-800 mt-auto pt-2 border-t">
|
<details name="prompt-details">
|
||||||
Created: {createdAt} • Updated: {updatedAt}
|
<summary class="cursor-pointer font-semibold mt-2">View Details</summary>
|
||||||
|
<div class="text-sm text-gray-800 border-t mt-2 pt-2">
|
||||||
|
<p><strong>Created:</strong> {formatDate(created_at)} • <strong>Updated:</strong> {formatDate(updated_at)}</p>
|
||||||
|
<p class="mt-2">📝 Misc data, unused currently</p>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,70 @@
|
|||||||
---
|
---
|
||||||
import PromptCard from './PromptCard.astro';
|
import PromptCard from './PromptCard.astro';
|
||||||
|
|
||||||
const prompts = [
|
const { prompts, error } = Astro.props;
|
||||||
{
|
|
||||||
title: "Summarize Document",
|
const typeFilter = typeof window !== "undefined"
|
||||||
type: "System",
|
? new URLSearchParams(window.location.search).get("type")
|
||||||
description: "Summarizes a document or long input using GPT-4.",
|
: null;
|
||||||
tags: ["summary", "long-form", "NLP"],
|
|
||||||
createdAt: "2025-06-01",
|
const tagFilter = typeof window !== "undefined"
|
||||||
updatedAt: "2025-07-10"
|
? new URLSearchParams(window.location.search).get("tag")
|
||||||
},
|
: null;
|
||||||
{
|
|
||||||
title: "Translate Text",
|
console.log("🔍 searchParams:", typeFilter, tagFilter);
|
||||||
type: "Task",
|
|
||||||
description: "Translate English text into French, Spanish, or Japanese.",
|
type Prompt = {
|
||||||
tags: ["translate", "language"],
|
slug: string;
|
||||||
createdAt: "2025-05-15",
|
title: string;
|
||||||
updatedAt: "2025-06-22"
|
type: string;
|
||||||
},
|
description: string;
|
||||||
{
|
tags?: string[];
|
||||||
title: "Generate Code",
|
created_at: string;
|
||||||
type: "Task",
|
updated_at: string;
|
||||||
description: "Generate Python or JavaScript functions from descriptions.",
|
notes?: string;
|
||||||
tags: ["code", "generation", "devtools"],
|
};
|
||||||
createdAt: "2025-06-05",
|
|
||||||
updatedAt: "2025-07-01"
|
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">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{prompts.map(prompt => (
|
{filtered?.map((prompt: Prompt) => (
|
||||||
<PromptCard {...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>
|
</div>
|
||||||
|
)}
|
||||||
|
|||||||
@@ -2,10 +2,53 @@
|
|||||||
// SearchBar.astro
|
// SearchBar.astro
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="border-b p-4">
|
<div class="w-64 border-r h-full p-4 text-gray-100">
|
||||||
|
<form id="filter-form" method="GET">
|
||||||
|
<div>
|
||||||
|
<label for="q" class="block text-lg font-semibold mb-1">Search</label>
|
||||||
<input
|
<input
|
||||||
|
name="q"
|
||||||
|
id="q"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search prompts..."
|
placeholder="Search prompts..."
|
||||||
class="w-full p-2 border rounded bg-gray-300 text-gray-800 focus:outline-none focus:ring"
|
class="w-full p-2 border rounded bg-gray-300 text-gray-800 focus:outline-none focus:ring"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 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 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">
|
||||||
|
Reset Filters
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ const { children } = Astro.props;
|
|||||||
<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</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-500 text-gray-100">
|
<body class="font-sans antialiased bg-gray-500 text-gray-100">
|
||||||
|
|||||||
@@ -2,18 +2,31 @@
|
|||||||
import MainLayout from '../layouts/MainLayout.astro';
|
import MainLayout from '../layouts/MainLayout.astro';
|
||||||
import Sidebar from '../components/Sidebar.astro';
|
import Sidebar from '../components/Sidebar.astro';
|
||||||
import SearchBar from '../components/SearchBar.astro';
|
import SearchBar from '../components/SearchBar.astro';
|
||||||
import PromptList from '../components/PromptList.astro';
|
// import PromptList from '../components/PromptList.astro';
|
||||||
|
import FilteredPromptList from '../components/FilteredPromptList.astro';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
|
|
||||||
|
const { data: prompts, error } = await supabase
|
||||||
|
.from('prompts')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
const allTags = Array.from(
|
||||||
|
new Set(
|
||||||
|
prompts?.flatMap((p) => p.tags ?? [])
|
||||||
|
)
|
||||||
|
).sort();
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div class="flex h-screen bg-gray-800 text-gray-100">
|
<div class="flex h-screen bg-gray-800 text-gray-100">
|
||||||
<Sidebar />
|
<SearchBar allTags={allTags} />
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
<SearchBar />
|
|
||||||
|
|
||||||
<main class="flex-1 overflow-y-auto p-4">
|
<main class="flex-1 overflow-y-auto p-4">
|
||||||
<PromptList />
|
{error
|
||||||
|
? <p class="text-red-500">Supabase error: {error.message}</p>
|
||||||
|
: <FilteredPromptList prompts={prompts} />}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
5
src/pages/ping.endtype.ts
Normal file
5
src/pages/ping.endtype.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const GET = async () => {
|
||||||
|
return new Response("pong", {
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
};
|
||||||
48
src/pages/prompt-details/test.endpoint.ts
Normal file
48
src/pages/prompt-details/test.endpoint.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"summarize-document": {
|
||||||
|
createdAt: "2025-06-01",
|
||||||
|
updatedAt: "2025-07-10",
|
||||||
|
notes: "Summarizes input using GPT-4 with smart chunking."
|
||||||
|
},
|
||||||
|
"translate-text": {
|
||||||
|
createdAt: "2025-05-15",
|
||||||
|
updatedAt: "2025-06-22",
|
||||||
|
notes: "Uses multilingual model for more accurate translation."
|
||||||
|
},
|
||||||
|
"generate-code": {
|
||||||
|
createdAt: "2025-06-05",
|
||||||
|
updatedAt: "2025-07-01",
|
||||||
|
notes: "Includes language detection and function wrapping."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params, request }) => {
|
||||||
|
console.log("HTMX request received for:", request.url);
|
||||||
|
console.log("Slug param is:", params.slug);
|
||||||
|
|
||||||
|
const slug = params.slug!;
|
||||||
|
const prompt = data[slug as keyof typeof data];
|
||||||
|
|
||||||
|
if (!prompt) {
|
||||||
|
return new Response(`<details open><summary>View Details</summary><p>Prompt not found.</p></details>`, {
|
||||||
|
status: 404,
|
||||||
|
headers: { 'Content-Type': 'text/html' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<details name="prompt-details" open>
|
||||||
|
<summary class="cursor-pointer font-semibold mt-2">View Details</summary>
|
||||||
|
<div class="text-sm text-gray-800 border-t mt-2 pt-2">
|
||||||
|
<p><strong>Created:</strong> ${prompt.createdAt} • <strong>Updated:</strong> ${prompt.updatedAt}</p>
|
||||||
|
<p class="mt-2">📝 ${prompt.notes}</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return new Response(html, {
|
||||||
|
headers: { 'Content-Type': 'text/html' }
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"include": [".astro/types.d.ts", "**/*"],
|
"include": ["astro/types.d.ts", "**/*"],
|
||||||
"exclude": ["dist"]
|
"exclude": ["dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user