345 lines
14 KiB
HTML
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>
|