2026-06-14 11:58:04 +02:00

345 lines
14 KiB
HTML

<!-- OnlyPrompt - Create Prompt page:
- Form to publish new AI prompts with title, description, category, content, example output, image upload, and pricing toggle -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OnlyPrompt - Create New Prompt</title>
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/base.css">
<link rel="stylesheet" href="../css/sidebar.css">
<link rel="stylesheet" href="../css/login.css">
<link rel="stylesheet" href="../css/topbar.css">
<link rel="stylesheet" href="../css/create.css">
<script src="../js/profile-shared.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>
<div class="layout">
<div id="sidebar-container"></div>
<div class="page-body">
<div id="topbar-container"></div>
<main class="create-main" id="main-content" tabindex="-1">
<div class="create-container">
<div class="create-header">
<h1 id="create-title">Create AI Prompt</h1>
<p id="create-subtitle">Design and save custom prompts for your AI workflows.</p>
</div>
<form id="createPromptForm" class="create-form" enctype="multipart/form-data">
<!-- Title -->
<div class="form-group">
<label for="title">Prompt Title *</label>
<input type="text" id="title" name="title" placeholder="e.g., Write an inspiring startup story about innovation" autocomplete="off" required>
</div>
<!-- Description -->
<div class="form-group">
<label for="description">Description *</label>
<textarea id="description" name="description" rows="2" placeholder="Draft a narrative about a small team overcoming challenges to launch a groundbreaking app" required></textarea>
</div>
<!-- Category -->
<div class="form-group">
<label for="category">Category *</label>
<select id="category" name="category" required>
<option value="creative-writing">Creative Writing</option>
<option value="coding">Coding</option>
<option value="art">Art</option>
<option value="marketing">Marketing</option>
<option value="video">Video</option>
<option value="data">Data</option>
</select>
</div>
<!-- Prompt Content -->
<div class="form-group">
<label for="promptContent">Prompt Content *</label>
<textarea id="promptContent" name="promptContent" rows="6" placeholder="Write your prompt instructions here..." aria-describedby="promptContentHint" required></textarea>
<small class="form-hint" id="promptContentHint">Use clear, step-by-step instructions for the AI.</small>
</div>
<!-- Example Output (Text) -->
<div class="form-group">
<label for="exampleOutput">Example Output (optional)</label>
<textarea id="exampleOutput" name="exampleOutput" rows="4" placeholder="Show an example of what the AI might generate..."></textarea>
</div>
<!-- Example Image (optional) -->
<div class="form-group">
<label for="exampleImage">Example Image (optional)</label>
<input type="file" id="exampleImage" name="exampleImage" accept="image/png, image/jpeg, image/jpg" aria-describedby="exampleImageHint">
<small class="form-hint" id="exampleImageHint">Upload a PNG or JPG. Preview will appear below.</small>
<div id="imagePreview" aria-live="polite">
<img id="previewImg" src="#" alt="Selected example image preview">
</div>
</div>
<!-- Pricing (with toggle) -->
<div class="form-group pricing-group">
<span class="form-label" id="access-label">Access</span>
<div class="pricing-toggle" role="group" aria-labelledby="access-label">
<button type="button" id="freeBtn" class="price-option active" aria-pressed="true">Free</button>
<button type="button" id="tierBtn" class="price-option" aria-pressed="false">Tier</button>
</div>
<div id="tierField">
<label for="subscriptionTier" class="sr-only">Subscription tier</label>
<select id="subscriptionTier" name="subscriptionTier">
<option value="">No tiers created yet</option>
</select>
<a class="tier-manage-link" href="subscription-tiers.html">Manage tiers</a>
</div>
<small class="form-hint">Free prompts are public. Tier prompts require a monthly creator subscription.</small>
</div>
<!-- Submit Button -->
<div class="form-actions">
<button type="submit" class="submit-btn" id="submitPromptBtn">Publish Prompt</button>
<button type="button" class="cancel-btn">Cancel</button>
</div>
<p id="create-status" role="status" aria-live="polite"></p>
</form>
</div>
</main>
</div>
</div>
<script>
// Toggle between free and paid
const freeBtn = document.getElementById('freeBtn');
const tierBtn = document.getElementById('tierBtn');
const tierField = document.getElementById('tierField');
const tierSelect = document.getElementById('subscriptionTier');
const editPromptId = new URLSearchParams(location.search).get('id');
const submitPromptBtn = document.getElementById('submitPromptBtn');
let ownSubscriptionTiers = [];
freeBtn.addEventListener('click', () => {
freeBtn.classList.add('active');
tierBtn.classList.remove('active');
freeBtn.setAttribute('aria-pressed', 'true');
tierBtn.setAttribute('aria-pressed', 'false');
tierField.style.display = 'none';
tierSelect.removeAttribute('required');
});
tierBtn.addEventListener('click', () => {
tierBtn.classList.add('active');
freeBtn.classList.remove('active');
tierBtn.setAttribute('aria-pressed', 'true');
freeBtn.setAttribute('aria-pressed', 'false');
tierField.style.display = 'grid';
tierSelect.setAttribute('required', 'required');
});
// Image preview for example image
const imageInput = document.getElementById('exampleImage');
const imagePreview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
let exampleImageUrl = '';
if (imageInput) {
imageInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (file && (file.type === 'image/png' || file.type === 'image/jpeg' || file.type === 'image/jpg')) {
const reader = new FileReader();
reader.onload = function(e) {
exampleImageUrl = e.target.result;
previewImg.src = exampleImageUrl;
imagePreview.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
imagePreview.style.display = 'none';
previewImg.src = '#';
exampleImageUrl = '';
if (file) alert('Please upload a PNG or JPG image.');
}
});
}
async function loadCategories() {
const categorySelect = document.getElementById('category');
try {
const response = await fetch('/api/v1/categories/minimal');
if (!response.ok) return;
const categories = await response.json();
if (!categories.length) return;
categorySelect.innerHTML = categories
.map((category) => `<option value="${category.slug}">${category.name}</option>`)
.join('');
} catch {
// Keep the static fallback categories.
}
}
async function loadSubscriptionTiers() {
try {
const response = await fetch('/api/v1/subscriptions/tiers', {
credentials: 'same-origin'
});
if (response.status === 401) {
location.href = '/login';
return;
}
if (!response.ok) return;
ownSubscriptionTiers = await response.json();
if (!ownSubscriptionTiers.length) {
tierSelect.innerHTML = '<option value="">Create a tier first</option>';
return;
}
tierSelect.innerHTML = ownSubscriptionTiers
.map((tier) => `<option value="${tier.level}">${tier.name} - $${Number(tier.monthlyPrice || 0).toFixed(2)}/mo</option>`)
.join('');
} catch {
tierSelect.innerHTML = '<option value="">Tiers could not be loaded</option>';
}
}
async function loadPromptForEdit() {
if (!editPromptId) return;
document.getElementById('create-title').textContent = 'Edit AI Prompt';
document.getElementById('create-subtitle').textContent = 'Update your published prompt.';
submitPromptBtn.textContent = 'Save Changes';
const status = document.getElementById('create-status');
status.textContent = 'Loading prompt...';
try {
const response = await fetch(`/api/v1/prompts/${editPromptId}`);
if (response.status === 401) {
location.href = '/login';
return;
}
if (!response.ok) throw new Error('Prompt could not be loaded.');
const prompt = await response.json();
document.getElementById('title').value = prompt.title || '';
document.getElementById('description').value = prompt.description || '';
document.getElementById('category').value = prompt.categorySlug || document.getElementById('category').value;
document.getElementById('promptContent').value = prompt.content || '';
document.getElementById('exampleOutput').value = prompt.exampleOutput || '';
exampleImageUrl = prompt.exampleImageUrl || '';
if (exampleImageUrl) {
previewImg.src = exampleImageUrl;
imagePreview.style.display = 'block';
}
if (prompt.tierLevel != null) {
tierBtn.click();
tierSelect.value = String(prompt.tierLevel);
} else {
freeBtn.click();
}
status.textContent = '';
} catch (error) {
status.textContent = error.message;
}
}
// Handle form submission
document.getElementById('createPromptForm').addEventListener('submit', async (e) => {
e.preventDefault();
const status = document.getElementById('create-status');
const submitBtn = document.querySelector('.submit-btn');
status.textContent = editPromptId ? 'Saving...' : 'Publishing...';
submitBtn.disabled = true;
try {
const isTier = tierBtn.classList.contains('active');
const payload = {
title: document.getElementById('title').value.trim(),
description: document.getElementById('description').value.trim(),
category: document.getElementById('category').value,
content: document.getElementById('promptContent').value.trim(),
exampleOutput: document.getElementById('exampleOutput').value.trim() || null,
exampleImageUrl: exampleImageUrl || null,
price: null,
subscriptionTier: isTier ? Number(tierSelect.value) : null,
slug: null
};
const response = await fetch(editPromptId ? `/api/v1/prompts/${editPromptId}` : '/api/v1/prompts', {
method: editPromptId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(payload)
});
if (response.status === 401) {
location.href = '/login';
return;
}
if (!response.ok) {
const error = await response.text();
throw new Error(getCreateErrorMessage(error, response.status));
}
const prompt = await response.json();
location.href = `/post-detail?id=${prompt.id}`;
} catch (error) {
status.textContent = error.message;
submitBtn.disabled = false;
}
});
function getCreateErrorMessage(errorText, status) {
if (!errorText) return `Server error ${status}`;
try {
const error = JSON.parse(errorText);
const messages = error.errors
? Object.values(error.errors).flat()
: [error.title || errorText];
return messages.join(' ');
} catch {
return errorText;
}
}
// Cancel button (go back)
document.querySelector('.cancel-btn').addEventListener('click', () => {
window.history.back();
});
// Fetch sidebar and topbar
fetch('/sidebar.html')
.then(r => r.text())
.then(data => {
document.getElementById('sidebar-container').innerHTML = data;
// Remove active class from all sidebar links
document.querySelectorAll('#sidebar-container .sidebar a').forEach(link => {
link.classList.remove('active');
link.removeAttribute('aria-current');
});
// Optionally set active on "Create New" if it exists, otherwise keep none
const createLink = document.querySelector('#sidebar-container a[href="create.html"]');
if (createLink) {
createLink.classList.add('active');
createLink.setAttribute('aria-current', 'page');
}
});
fetch('/topbar.html')
.then(r => r.text())
.then(data => document.getElementById('topbar-container').innerHTML = data);
Promise.all([loadCategories(), loadSubscriptionTiers()]).then(loadPromptForEdit);
</script>
</body>
</html>