Spaces:
Build error
Build error
{% extends "base.html" %} | |
{% block content %} | |
<div class="container mx-auto px-4 py-8"> | |
<div class="max-w-7xl mx-auto"> | |
<!-- Header --> | |
<div class="flex items-center justify-between mb-8"> | |
<div> | |
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Prompt Marketplace</h1> | |
<p class="mt-2 text-gray-600 dark:text-gray-300">Discover and purchase high-quality prompts for various AI tools</p> | |
</div> | |
<div class="text-right"> | |
<div class="text-2xl font-bold text-blue-600">{{ user_credits }}</div> | |
<div class="text-sm text-gray-500">credits available</div> | |
</div> | |
</div> | |
<!-- Filters and Search --> | |
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8"> | |
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> | |
<!-- Search --> | |
<div class="flex-1"> | |
<div class="relative"> | |
<input type="text" id="search-input" | |
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 pl-10 pr-4 py-2 focus:ring-blue-500 focus:border-blue-500" | |
placeholder="Search prompts..."> | |
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | |
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" | |
fill="currentColor"> | |
<path fill-rule="evenodd" | |
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" | |
clip-rule="evenodd"/> | |
</svg> | |
</div> | |
</div> | |
</div> | |
<!-- Category Filter --> | |
<div class="flex-shrink-0"> | |
<select id="category-filter" | |
class="rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="">All Categories</option> | |
<option value="text" {% if category == 'text' %}selected{% endif %}>Text Generation</option> | |
<option value="image" {% if category == 'image' %}selected{% endif %}>Image Generation</option> | |
<option value="code" {% if category == 'code' %}selected{% endif %}>Code Generation</option> | |
</select> | |
</div> | |
<!-- Sort By --> | |
<div class="flex-shrink-0"> | |
<select id="sort-by" | |
class="rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="popular" {% if sort_by == 'popular' %}selected{% endif %}>Most Popular</option> | |
<option value="newest" {% if sort_by == 'newest' %}selected{% endif %}>Newest First</option> | |
<option value="rating" {% if sort_by == 'rating' %}selected{% endif %}>Highest Rated</option> | |
</select> | |
</div> | |
</div> | |
</div> | |
<!-- Prompts Grid --> | |
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | |
{% for prompt in prompts %} | |
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden prompt-card" | |
data-category="{{ prompt.tool_id }}" | |
data-title="{{ prompt.title|lower }}" | |
data-rating="{{ prompt.avg_rating }}" | |
data-usage="{{ prompt.usage_count }}" | |
data-date="{{ prompt.created_at }}"> | |
<!-- Prompt Header --> | |
<div class="p-6"> | |
<div class="flex items-start justify-between"> | |
<div> | |
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ prompt.title }}</h3> | |
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ prompt.description }}</p> | |
</div> | |
<div class="flex items-center"> | |
<span class="text-yellow-500">★</span> | |
<span class="ml-1 text-sm text-gray-600 dark:text-gray-300">{{ "%.1f"|format(prompt.avg_rating) }}</span> | |
</div> | |
</div> | |
<!-- Tags --> | |
<div class="mt-4 flex flex-wrap gap-2"> | |
{% for tag in prompt.tags %} | |
<span class="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-full"> | |
{{ tag }} | |
</span> | |
{% endfor %} | |
</div> | |
<!-- Usage Stats --> | |
<div class="mt-4 flex items-center text-sm text-gray-500 dark:text-gray-400"> | |
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/> | |
</svg> | |
{{ prompt.usage_count }} uses | |
</div> | |
</div> | |
<!-- Preview --> | |
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-700"> | |
<div class="text-sm text-gray-600 dark:text-gray-300 line-clamp-3"> | |
{{ prompt.content }} | |
</div> | |
</div> | |
<!-- Footer --> | |
<div class="px-6 py-4 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700"> | |
<div class="flex items-center justify-between"> | |
<div class="text-sm text-gray-500 dark:text-gray-400"> | |
Created by {{ prompt.creator_id }} | |
</div> | |
<button onclick="purchasePrompt('{{ prompt.id }}')" | |
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"> | |
Purchase | |
</button> | |
</div> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
<!-- Empty State --> | |
<div id="empty-state" class="hidden text-center py-12"> | |
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | |
</svg> | |
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No prompts found</h3> | |
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"> | |
Try adjusting your search or filter criteria | |
</p> | |
</div> | |
</div> | |
</div> | |
<!-- Purchase Confirmation Modal --> | |
<div id="purchase-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center"> | |
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-lg w-full mx-4"> | |
<h2 class="text-xl font-semibold mb-4">Confirm Purchase</h2> | |
<p class="text-gray-600 dark:text-gray-300 mb-4"> | |
Are you sure you want to purchase this prompt? This will cost 5 credits. | |
</p> | |
<div class="flex justify-end space-x-4"> | |
<button id="cancel-purchase" | |
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"> | |
Cancel | |
</button> | |
<button id="confirm-purchase" | |
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700"> | |
Confirm Purchase | |
</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
let selectedPromptId = null; | |
// Initialize the page | |
document.addEventListener('DOMContentLoaded', function() { | |
setupEventListeners(); | |
setupFilters(); | |
}); | |
// Set up event listeners | |
function setupEventListeners() { | |
// Purchase modal | |
document.getElementById('cancel-purchase').addEventListener('click', hidePurchaseModal); | |
document.getElementById('confirm-purchase').addEventListener('click', handlePurchase); | |
} | |
// Set up filters | |
function setupFilters() { | |
const searchInput = document.getElementById('search-input'); | |
const categoryFilter = document.getElementById('category-filter'); | |
const sortBy = document.getElementById('sort-by'); | |
function filterPrompts() { | |
const searchTerm = searchInput.value.toLowerCase(); | |
const category = categoryFilter.value; | |
const sortValue = sortBy.value; | |
const cards = document.querySelectorAll('.prompt-card'); | |
let visibleCount = 0; | |
cards.forEach(card => { | |
const title = card.dataset.title; | |
const cardCategory = card.dataset.category; | |
const rating = parseFloat(card.dataset.rating); | |
const usage = parseInt(card.dataset.usage); | |
const date = new Date(card.dataset.date); | |
const matchesSearch = title.includes(searchTerm); | |
const matchesCategory = !category || cardCategory === category; | |
if (matchesSearch && matchesCategory) { | |
card.style.display = 'block'; | |
visibleCount++; | |
} else { | |
card.style.display = 'none'; | |
} | |
}); | |
// Show/hide empty state | |
const emptyState = document.getElementById('empty-state'); | |
emptyState.classList.toggle('hidden', visibleCount > 0); | |
// Sort visible cards | |
const container = document.querySelector('.grid'); | |
const visibleCards = Array.from(cards).filter(card => card.style.display !== 'none'); | |
visibleCards.sort((a, b) => { | |
switch (sortValue) { | |
case 'rating': | |
return parseFloat(b.dataset.rating) - parseFloat(a.dataset.rating); | |
case 'newest': | |
return new Date(b.dataset.date) - new Date(a.dataset.date); | |
case 'popular': | |
default: | |
return parseInt(b.dataset.usage) - parseInt(a.dataset.usage); | |
} | |
}); | |
visibleCards.forEach(card => container.appendChild(card)); | |
} | |
searchInput.addEventListener('input', filterPrompts); | |
categoryFilter.addEventListener('change', filterPrompts); | |
sortBy.addEventListener('change', filterPrompts); | |
} | |
// Purchase prompt | |
function purchasePrompt(promptId) { | |
selectedPromptId = promptId; | |
showPurchaseModal(); | |
} | |
// Show purchase modal | |
function showPurchaseModal() { | |
document.getElementById('purchase-modal').classList.remove('hidden'); | |
document.getElementById('purchase-modal').classList.add('flex'); | |
} | |
// Hide purchase modal | |
function hidePurchaseModal() { | |
document.getElementById('purchase-modal').classList.add('hidden'); | |
document.getElementById('purchase-modal').classList.remove('flex'); | |
selectedPromptId = null; | |
} | |
// Handle purchase | |
async function handlePurchase() { | |
if (!selectedPromptId) return; | |
try { | |
const response = await fetch('/api/marketplace/purchase', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
prompt_id: selectedPromptId | |
}) | |
}); | |
const data = await response.json(); | |
if (data.success) { | |
showToast('Prompt purchased successfully!', 'success'); | |
hidePurchaseModal(); | |
// Refresh the page to update credits | |
window.location.reload(); | |
} else { | |
showToast(data.error || 'Failed to purchase prompt', 'error'); | |
} | |
} catch (error) { | |
console.error('Error purchasing prompt:', error); | |
showToast('Failed to purchase prompt', 'error'); | |
} | |
} | |
</script> | |
{% endblock %} |