Spaces:
Running
Running
| {% 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 %} |