335 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 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>
<div class="layout">
<div id="sidebar-container"></div>
<div class="page-body">
<div id="topbar-container"></div>
<main class="create-main">
<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" 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..." required></textarea>
<small class="form-hint">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">
<small class="form-hint">Upload a PNG or JPG preview will appear below.</small>
<div id="imagePreview">
<img id="previewImg" src="#" alt="Preview">
</div>
</div>
<!-- Pricing (with toggle) -->
<div class="form-group pricing-group">
<label>Access</label>
<div class="pricing-toggle">
<button type="button" id="freeBtn" class="price-option active">Free</button>
<button type="button" id="tierBtn" class="price-option">Tier</button>
</div>
<div id="tierField">
<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"></p></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');
tierField.style.display = 'none';
tierSelect.removeAttribute('required');
});
tierBtn.addEventListener('click', () => {
tierBtn.classList.add('active');
freeBtn.classList.remove('active');
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');
});
// 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');
});
fetch('/topbar.html')
.then(r => r.text())
.then(data => document.getElementById('topbar-container').innerHTML = data);
Promise.all([loadCategories(), loadSubscriptionTiers()]).then(loadPromptForEdit);
</script>
</body>
</html>