|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Midjourney Prompt Organizer</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.prompt-card:hover .prompt-actions { |
|
opacity: 1; |
|
} |
|
.tag:hover { |
|
transform: scale(1.05); |
|
} |
|
.grid-item { |
|
transition: all 0.3s ease; |
|
} |
|
.grid-item:hover { |
|
transform: translateY(-5px); |
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
|
} |
|
#lightbox { |
|
transition: opacity 0.3s ease; |
|
} |
|
.loader { |
|
border-top-color: #6366f1; |
|
animation: spin 1s linear infinite; |
|
} |
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-100 min-h-screen"> |
|
|
|
<header class="bg-indigo-600 text-white shadow-lg"> |
|
<div class="container mx-auto px-4 py-6"> |
|
<div class="flex flex-col md:flex-row justify-between items-center"> |
|
<div class="flex items-center mb-4 md:mb-0"> |
|
<i class="fas fa-robot text-3xl mr-3"></i> |
|
<h1 class="text-2xl md:text-3xl font-bold">Midjourney Prompt Vault</h1> |
|
</div> |
|
<button id="addNewBtn" class="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-full flex items-center transition duration-300"> |
|
<i class="fas fa-plus mr-2"></i> New Prompt |
|
</button> |
|
</div> |
|
</div> |
|
</header> |
|
|
|
|
|
<main class="container mx-auto px-4 py-8"> |
|
|
|
<div class="bg-white rounded-xl shadow-md p-6 mb-8"> |
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> |
|
<div class="relative flex-grow"> |
|
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
|
<input id="searchInput" type="text" placeholder="Search prompts..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
|
</div> |
|
<div class="flex flex-wrap gap-2"> |
|
<select id="sortSelect" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
|
<option value="newest">Newest First</option> |
|
<option value="oldest">Oldest First</option> |
|
<option value="az">A-Z</option> |
|
<option value="za">Z-A</option> |
|
</select> |
|
<select id="filterSelect" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
|
<option value="all">All Prompts</option> |
|
<option value="favorite">Favorites</option> |
|
<option value="character">Characters</option> |
|
<option value="landscape">Landscapes</option> |
|
<option value="concept">Concepts</option> |
|
<option value="portrait">Portraits</option> |
|
</select> |
|
</div> |
|
</div> |
|
<div id="activeTags" class="flex flex-wrap gap-2 mt-4"></div> |
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> |
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<div class="flex items-center"> |
|
<div class="p-3 rounded-full bg-indigo-100 text-indigo-600 mr-4"> |
|
<i class="fas fa-list text-lg"></i> |
|
</div> |
|
<div> |
|
<p class="text-gray-500">Total Prompts</p> |
|
<h3 id="totalPrompts" class="text-2xl font-bold">0</h3> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<div class="flex items-center"> |
|
<div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4"> |
|
<i class="fas fa-star text-lg"></i> |
|
</div> |
|
<div> |
|
<p class="text-gray-500">Favorites</p> |
|
<h3 id="favoritePrompts" class="text-2xl font-bold">0</h3> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<div class="flex items-center"> |
|
<div class="p-3 rounded-full bg-green-100 text-green-600 mr-4"> |
|
<i class="fas fa-tags text-lg"></i> |
|
</div> |
|
<div> |
|
<p class="text-gray-500">Unique Tags</p> |
|
<h3 id="uniqueTags" class="text-2xl font-bold">0</h3> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<div class="flex items-center"> |
|
<div class="p-3 rounded-full bg-purple-100 text-purple-600 mr-4"> |
|
<i class="fas fa-image text-lg"></i> |
|
</div> |
|
<div> |
|
<p class="text-gray-500">Images</p> |
|
<h3 id="totalImages" class="text-2xl font-bold">0</h3> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="promptsContainer" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> |
|
|
|
<div id="emptyState" class="col-span-full text-center py-20"> |
|
<i class="fas fa-robot text-6xl text-gray-300 mb-4"></i> |
|
<h3 class="text-2xl font-bold text-gray-700 mb-2">No prompts found</h3> |
|
<p class="text-gray-500 mb-6">Start by adding your first Midjourney prompt</p> |
|
<button id="emptyAddBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-6 rounded-full inline-flex items-center transition duration-300"> |
|
<i class="fas fa-plus mr-2"></i> Add Prompt |
|
</button> |
|
</div> |
|
</div> |
|
</main> |
|
|
|
|
|
<div id="promptModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center p-4"> |
|
<div class="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"> |
|
<div class="p-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-2xl font-bold" id="modalTitle">Add New Prompt</h2> |
|
<button id="closeModal" class="text-gray-500 hover:text-gray-700"> |
|
<i class="fas fa-times text-xl"></i> |
|
</button> |
|
</div> |
|
|
|
<form id="promptForm" class="space-y-4"> |
|
<input type="hidden" id="promptId"> |
|
|
|
<div> |
|
<label for="promptText" class="block text-sm font-medium text-gray-700 mb-1">Prompt</label> |
|
<textarea id="promptText" rows="5" class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your Midjourney prompt..."></textarea> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
<div> |
|
<label for="promptName" class="block text-sm font-medium text-gray-700 mb-1">Name</label> |
|
<input type="text" id="promptName" class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="My Awesome Art"> |
|
</div> |
|
<div> |
|
<label for="promptModel" class="block text-sm font-medium text-gray-700 mb-1">Model Version</label> |
|
<select id="promptModel" class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
|
<option value="MJ 5.2">Midjourney 5.2</option> |
|
<option value="MJ 5.1">Midjourney 5.1</option> |
|
<option value="Niji 5">Niji 5</option> |
|
<option value="Niji 4">Niji 4</option> |
|
<option value="Other">Other</option> |
|
</select> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<label for="promptParameters" class="block text-sm font-medium text-gray-700 mb-1">Parameters</label> |
|
<input type="text" id="promptParameters" class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="--ar 16:9 --v 5.2 --q 2"> |
|
</div> |
|
|
|
<div> |
|
<label for="promptImages" class="block text-sm font-medium text-gray-700 mb-1">Image URLs (one per line)</label> |
|
<textarea id="promptImages" rows="3" class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="https://example.com/image1.jpg"></textarea> |
|
</div> |
|
|
|
<div> |
|
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label> |
|
<div class="flex flex-wrap gap-2 mb-2" id="currentTags"></div> |
|
<div class="flex"> |
|
<input type="text" id="newTag" class="flex-grow p-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Add new tag"> |
|
<button type="button" id="addTagBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 rounded-r-lg">Add</button> |
|
</div> |
|
</div> |
|
|
|
<div class="flex items-center"> |
|
<input type="checkbox" id="isFavorite" class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> |
|
<label for="isFavorite" class="ml-2 text-sm text-gray-700">Mark as favorite</label> |
|
</div> |
|
|
|
<div class="flex justify-end space-x-3 pt-4"> |
|
<button type="button" id="cancelPrompt" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">Cancel</button> |
|
<button type="submit" id="savePrompt" class="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">Save Prompt</button> |
|
</div> |
|
</form> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="lightbox" class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden flex items-center justify-center p-4"> |
|
<div class="relative max-w-6xl w-full"> |
|
<button id="closeLightbox" class="absolute top-4 right-4 text-white text-3xl z-50"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
<div class="flex justify-between items-center absolute top-1/2 left-0 right-0 transform -translate-y-1/2 z-50 px-4"> |
|
<button id="prevImage" class="bg-black bg-opacity-50 text-white rounded-full w-10 h-10 flex items-center justify-center hover:bg-opacity-70"> |
|
<i class="fas fa-chevron-left"></i> |
|
</button> |
|
<button id="nextImage" class="bg-black bg-opacity-50 text-white rounded-full w-10 h-10 flex items-center justify-center hover:bg-opacity-70"> |
|
<i class="fas fa-chevron-right"></i> |
|
</button> |
|
</div> |
|
<img id="lightboxImage" src="" alt="" class="max-h-[90vh] w-auto mx-auto"> |
|
<div id="lightboxCaption" class="text-white text-center mt-4"></div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="loadingOverlay" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center"> |
|
<div class="bg-white p-8 rounded-lg shadow-xl flex flex-col items-center"> |
|
<div class="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12 mb-4"></div> |
|
<p class="text-gray-700">Loading prompts...</p> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const promptsContainer = document.getElementById('promptsContainer'); |
|
const emptyState = document.getElementById('emptyState'); |
|
const promptModal = document.getElementById('promptModal'); |
|
const lightbox = document.getElementById('lightbox'); |
|
const loadingOverlay = document.getElementById('loadingOverlay'); |
|
|
|
|
|
const addNewBtn = document.getElementById('addNewBtn'); |
|
const emptyAddBtn = document.getElementById('emptyAddBtn'); |
|
const closeModal = document.getElementById('closeModal'); |
|
const closeLightbox = document.getElementById('closeLightbox'); |
|
const prevImage = document.getElementById('prevImage'); |
|
const nextImage = document.getElementById('nextImage'); |
|
const cancelPrompt = document.getElementById('cancelPrompt'); |
|
const savePrompt = document.getElementById('savePrompt'); |
|
const addTagBtn = document.getElementById('addTagBtn'); |
|
|
|
|
|
const promptForm = document.getElementById('promptForm'); |
|
const promptId = document.getElementById('promptId'); |
|
const promptName = document.getElementById('promptName'); |
|
const promptText = document.getElementById('promptText'); |
|
const promptModel = document.getElementById('promptModel'); |
|
const promptParameters = document.getElementById('promptParameters'); |
|
const promptImages = document.getElementById('promptImages'); |
|
const currentTags = document.getElementById('currentTags'); |
|
const newTag = document.getElementById('newTag'); |
|
const isFavorite = document.getElementById('isFavorite'); |
|
|
|
|
|
const searchInput = document.getElementById('searchInput'); |
|
const sortSelect = document.getElementById('sortSelect'); |
|
const filterSelect = document.getElementById('filterSelect'); |
|
const activeTags = document.getElementById('activeTags'); |
|
|
|
|
|
const totalPrompts = document.getElementById('totalPrompts'); |
|
const favoritePrompts = document.getElementById('favoritePrompts'); |
|
const uniqueTags = document.getElementById('uniqueTags'); |
|
const totalImages = document.getElementById('totalImages'); |
|
|
|
|
|
let prompts = JSON.parse(localStorage.getItem('midjourneyPrompts')) || []; |
|
let currentTagsArray = []; |
|
let currentLightboxImages = []; |
|
let currentLightboxIndex = 0; |
|
|
|
|
|
updateStats(); |
|
renderPrompts(); |
|
renderTagFilters(); |
|
|
|
|
|
addNewBtn.addEventListener('click', openNewPromptModal); |
|
emptyAddBtn.addEventListener('click', openNewPromptModal); |
|
closeModal.addEventListener('click', closePromptModal); |
|
closeLightbox.addEventListener('click', closeLightboxModal); |
|
cancelPrompt.addEventListener('click', closePromptModal); |
|
promptForm.addEventListener('submit', savePromptHandler); |
|
addTagBtn.addEventListener('click', addTag); |
|
|
|
|
|
searchInput.addEventListener('input', renderPrompts); |
|
sortSelect.addEventListener('change', renderPrompts); |
|
filterSelect.addEventListener('change', renderPrompts); |
|
|
|
|
|
function openNewPromptModal() { |
|
promptId.value = ''; |
|
promptForm.reset(); |
|
currentTagsArray = []; |
|
renderCurrentTags(); |
|
document.getElementById('modalTitle').textContent = 'Add New Prompt'; |
|
promptModal.classList.remove('hidden'); |
|
} |
|
|
|
function openEditPromptModal(id) { |
|
const prompt = prompts.find(p => p.id === id); |
|
if (prompt) { |
|
promptId.value = prompt.id; |
|
promptName.value = prompt.name || ''; |
|
promptText.value = prompt.text; |
|
promptModel.value = prompt.model || 'MJ 5.2'; |
|
promptParameters.value = prompt.parameters || ''; |
|
promptImages.value = prompt.images?.join('\n') || ''; |
|
isFavorite.checked = prompt.favorite || false; |
|
currentTagsArray = [...(prompt.tags || [])]; |
|
renderCurrentTags(); |
|
|
|
document.getElementById('modalTitle').textContent = 'Edit Prompt'; |
|
promptModal.classList.remove('hidden'); |
|
} |
|
} |
|
|
|
function closePromptModal() { |
|
promptModal.classList.add('hidden'); |
|
} |
|
|
|
function closeLightboxModal() { |
|
lightbox.classList.add('hidden'); |
|
} |
|
|
|
function addTag() { |
|
const tag = newTag.value.trim(); |
|
if (tag && !currentTagsArray.includes(tag)) { |
|
currentTagsArray.push(tag); |
|
renderCurrentTags(); |
|
newTag.value = ''; |
|
} |
|
} |
|
|
|
function removeTag(tag) { |
|
currentTagsArray = currentTagsArray.filter(t => t !== tag); |
|
renderCurrentTags(); |
|
renderPrompts(); |
|
} |
|
|
|
function renderCurrentTags() { |
|
currentTags.innerHTML = ''; |
|
currentTagsArray.forEach(tag => { |
|
const tagElement = document.createElement('span'); |
|
tagElement.className = 'inline-flex items-center bg-indigo-100 text-indigo-800 text-sm px-3 py-1 rounded-full'; |
|
tagElement.innerHTML = ` |
|
${tag} |
|
<button data-tag="${tag}" class="ml-2 text-indigo-600 hover:text-indigo-900"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
`; |
|
currentTags.appendChild(tagElement); |
|
}); |
|
|
|
|
|
currentTags.querySelectorAll('button').forEach(button => { |
|
button.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
removeTag(button.dataset.tag); |
|
}); |
|
}); |
|
} |
|
|
|
function savePromptHandler(e) { |
|
e.preventDefault(); |
|
|
|
const id = promptId.value || Date.now().toString(); |
|
const name = promptName.value.trim(); |
|
const text = promptText.value.trim(); |
|
const model = promptModel.value; |
|
const parameters = promptParameters.value.trim(); |
|
const images = promptImages.value.split('\n').filter(url => url.trim()); |
|
const favorite = isFavorite.checked; |
|
const tags = [...currentTagsArray]; |
|
const createdAt = promptId.value ? |
|
(prompts.find(p => p.id === id)?.createdAt || new Date().toISOString()) : |
|
new Date().toISOString(); |
|
const updatedAt = new Date().toISOString(); |
|
|
|
if (!text) { |
|
alert('Prompt text is required'); |
|
return; |
|
} |
|
|
|
const prompt = { |
|
id, |
|
name, |
|
text, |
|
model, |
|
parameters, |
|
images, |
|
favorite, |
|
tags, |
|
createdAt, |
|
updatedAt |
|
}; |
|
|
|
|
|
const index = prompts.findIndex(p => p.id === id); |
|
if (index !== -1) { |
|
prompts[index] = prompt; |
|
} else { |
|
prompts.unshift(prompt); |
|
} |
|
|
|
|
|
localStorage.setItem('midjourneyPrompts', JSON.stringify(prompts)); |
|
|
|
|
|
updateStats(); |
|
renderPrompts(); |
|
renderTagFilters(); |
|
closePromptModal(); |
|
} |
|
|
|
function deletePrompt(id) { |
|
if (confirm('Are you sure you want to delete this prompt?')) { |
|
prompts = prompts.filter(p => p.id !== id); |
|
localStorage.setItem('midjourneyPrompts', JSON.stringify(prompts)); |
|
updateStats(); |
|
renderPrompts(); |
|
renderTagFilters(); |
|
} |
|
} |
|
|
|
function toggleFavorite(id) { |
|
const prompt = prompts.find(p => p.id === id); |
|
if (prompt) { |
|
prompt.favorite = !prompt.favorite; |
|
localStorage.setItem('midjourneyPrompts', JSON.stringify(prompts)); |
|
updateStats(); |
|
renderPrompts(); |
|
} |
|
} |
|
|
|
function updateStats() { |
|
totalPrompts.textContent = prompts.length; |
|
favoritePrompts.textContent = prompts.filter(p => p.favorite).length; |
|
|
|
|
|
const allTags = prompts.flatMap(p => p.tags || []); |
|
const uniqueTagsSet = new Set(allTags); |
|
uniqueTags.textContent = uniqueTagsSet.size; |
|
|
|
|
|
const totalImagesCount = prompts.reduce((sum, p) => sum + (p.images?.length || 0), 0); |
|
totalImages.textContent = totalImagesCount; |
|
} |
|
|
|
function renderPrompts() { |
|
loadingOverlay.classList.remove('hidden'); |
|
|
|
|
|
setTimeout(() => { |
|
let filteredPrompts = [...prompts]; |
|
|
|
|
|
const searchTerm = searchInput.value.toLowerCase(); |
|
if (searchTerm) { |
|
filteredPrompts = filteredPrompts.filter(p => |
|
p.text.toLowerCase().includes(searchTerm) || |
|
(p.name && p.name.toLowerCase().includes(searchTerm)) || |
|
(p.tags && p.tags.some(tag => tag.toLowerCase().includes(searchTerm))) |
|
); |
|
} |
|
|
|
|
|
const filterValue = filterSelect.value; |
|
if (filterValue !== 'all') { |
|
if (filterValue === 'favorite') { |
|
filteredPrompts = filteredPrompts.filter(p => p.favorite); |
|
} else { |
|
filteredPrompts = filteredPrompts.filter(p => |
|
p.tags && p.tags.includes(filterValue) |
|
); |
|
} |
|
} |
|
|
|
|
|
const activeTagButtons = activeTags.querySelectorAll('.tag-filter'); |
|
const activeTagsArray = Array.from(activeTagButtons).map(btn => btn.dataset.tag); |
|
|
|
if (activeTagsArray.length > 0) { |
|
filteredPrompts = filteredPrompts.filter(p => |
|
p.tags && activeTagsArray.every(tag => p.tags.includes(tag)) |
|
); |
|
} |
|
|
|
|
|
const sortValue = sortSelect.value; |
|
switch (sortValue) { |
|
case 'newest': |
|
filteredPrompts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); |
|
break; |
|
case 'oldest': |
|
filteredPrompts.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); |
|
break; |
|
case 'az': |
|
filteredPrompts.sort((a, b) => (a.name || a.text).localeCompare(b.name || b.text)); |
|
break; |
|
case 'za': |
|
filteredPrompts.sort((a, b) => (b.name || b.text).localeCompare(a.name || a.text)); |
|
break; |
|
} |
|
|
|
|
|
promptsContainer.innerHTML = ''; |
|
|
|
|
|
if (filteredPrompts.length === 0) { |
|
emptyState.classList.remove('hidden'); |
|
promptsContainer.appendChild(emptyState); |
|
} else { |
|
emptyState.classList.add('hidden'); |
|
|
|
|
|
filteredPrompts.forEach(prompt => { |
|
const promptCard = document.createElement('div'); |
|
promptCard.className = 'grid-item bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition duration-300'; |
|
|
|
|
|
const firstImage = prompt.images?.[0] || ''; |
|
const hasImages = prompt.images && prompt.images.length > 0; |
|
|
|
promptCard.innerHTML = ` |
|
<div class="relative"> |
|
${hasImages ? ` |
|
<img src="${firstImage}" alt="${prompt.name || 'Midjourney creation'}" class="w-full h-48 object-cover cursor-pointer view-image" data-prompt-id="${prompt.id}" data-image-index="0"> |
|
` : ` |
|
<div class="w-full h-48 bg-gray-100 flex items-center justify-center text-gray-400"> |
|
<i class="fas fa-image text-4xl"></i> |
|
</div> |
|
`} |
|
${prompt.favorite ? ` |
|
<div class="absolute top-2 right-2 bg-yellow-500 text-white p-2 rounded-full shadow-md"> |
|
<i class="fas fa-star"></i> |
|
</div> |
|
` : ''} |
|
<div class="prompt-actions absolute top-2 left-2 opacity-0 transition-opacity duration-300 flex gap-2"> |
|
<button class="edit-prompt bg-indigo-600 text-white p-2 rounded-full shadow-md hover:bg-indigo-700" data-id="${prompt.id}"> |
|
<i class="fas fa-pencil-alt text-sm"></i> |
|
</button> |
|
<button class="delete-prompt bg-red-600 text-white p-2 rounded-full shadow-md hover:bg-red-700" data-id="${prompt.id}"> |
|
<i class="fas fa-trash-alt text-sm"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="p-4"> |
|
<div class="flex justify-between items-start mb-2"> |
|
<h3 class="font-bold text-lg truncate">${prompt.name || 'Untitled Prompt'}</h3> |
|
<button class="favorite-prompt text-gray-400 hover:text-yellow-500 ${prompt.favorite ? 'text-yellow-500' : ''}" data-id="${prompt.id}"> |
|
<i class="fas fa-star"></i> |
|
</button> |
|
</div> |
|
<p class="text-gray-600 text-sm mb-3 line-clamp-3">${prompt.text}</p> |
|
<div class="flex flex-wrap gap-1 mb-2"> |
|
${(prompt.tags || []).slice(0, 3).map(tag => ` |
|
<span class="tag bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full">${tag}</span> |
|
`).join('')} |
|
${!prompt.tags || prompt.tags.length === 0 ? '' : prompt.tags.length > 3 ? ` |
|
<span class="tag bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full">+${prompt.tags.length - 3}</span> |
|
` : ''} |
|
</div> |
|
<div class="flex justify-between items-center text-xs text-gray-500"> |
|
<span>${new Date(prompt.createdAt).toLocaleDateString()}</span> |
|
${hasImages && prompt.images.length > 1 ? ` |
|
<span class="flex items-center"> |
|
<i class="fas fa-images mr-1"></i> |
|
${prompt.images.length} |
|
</span> |
|
` : ''} |
|
</div> |
|
</div> |
|
`; |
|
|
|
promptsContainer.appendChild(promptCard); |
|
}); |
|
} |
|
|
|
|
|
addPromptEventListeners(); |
|
loadingOverlay.classList.add('hidden'); |
|
}, 500); |
|
} |
|
|
|
function addPromptEventListeners() { |
|
|
|
document.querySelectorAll('.edit-prompt').forEach(button => { |
|
button.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
openEditPromptModal(button.dataset.id); |
|
}); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.delete-prompt').forEach(button => { |
|
button.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
deletePrompt(button.dataset.id); |
|
}); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.favorite-prompt').forEach(button => { |
|
button.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
toggleFavorite(button.dataset.id); |
|
}); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.view-image').forEach(img => { |
|
img.addEventListener('click', (e) => { |
|
const promptId = img.dataset.promptId; |
|
const prompt = prompts.find(p => p.id === promptId); |
|
|
|
if (prompt && prompt.images && prompt.images.length > 0) { |
|
currentLightboxImages = prompt.images; |
|
currentLightboxIndex = parseInt(img.dataset.imageIndex); |
|
openLightboxModal(currentLightboxIndex); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.tag').forEach(tag => { |
|
tag.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
const tagText = tag.textContent.replace(/^\+/, ''); |
|
toggleTagFilter(tagText); |
|
}); |
|
}); |
|
} |
|
|
|
function openLightboxModal(index) { |
|
if (currentLightboxImages.length === 0) return; |
|
|
|
const imageUrl = currentLightboxImages[index]; |
|
const promptId = currentLightboxImages.promptId; |
|
const prompt = promptId ? prompts.find(p => p.id === promptId) : null; |
|
|
|
document.getElementById('lightboxImage').src = imageUrl; |
|
document.getElementById('lightboxCaption').textContent = prompt ? (prompt.name || prompt.text.substring(0, 100) + (prompt.text.length > 100 ? '...' : '')) : ''; |
|
|
|
|
|
prevImage.style.visibility = index > 0 ? 'visible' : 'hidden'; |
|
nextImage.style.visibility = index < currentLightboxImages.length - 1 ? 'visible' : 'hidden'; |
|
|
|
lightbox.classList.remove('hidden'); |
|
} |
|
|
|
function renderTagFilters() { |
|
|
|
const allTags = prompts.flatMap(p => p.tags || []); |
|
const tagCounts = {}; |
|
|
|
allTags.forEach(tag => { |
|
tagCounts[tag] = (tagCounts[tag] || 0) + 1; |
|
}); |
|
|
|
const uniqueTags = Object.keys(tagCounts); |
|
|
|
|
|
const appliedTags = Array.from(activeTags.querySelectorAll('.tag-filter')).map(el => el.dataset.tag); |
|
activeTags.innerHTML = ''; |
|
|
|
|
|
appliedTags.forEach(tag => { |
|
if (uniqueTags.includes(tag)) { |
|
addActiveTagFilter(tag); |
|
} |
|
}); |
|
} |
|
|
|
function toggleTagFilter(tag) { |
|
const isActive = activeTags.querySelector(`[data-tag="${tag}"]`); |
|
|
|
if (isActive) { |
|
isActive.remove(); |
|
} else { |
|
addActiveTagFilter(tag); |
|
} |
|
|
|
renderPrompts(); |
|
} |
|
|
|
function addActiveTagFilter(tag) { |
|
const activeTag = document.createElement('span'); |
|
activeTag.className = 'tag-filter inline-flex items-center bg-indigo-600 text-white text-sm px-3 py-1 rounded-full cursor-pointer'; |
|
activeTag.dataset.tag = tag; |
|
activeTag.innerHTML = ` |
|
${tag} |
|
<button class="ml-2 text-white hover:text-indigo-200"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
`; |
|
|
|
activeTag.addEventListener('click', (e) => { |
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'I') { |
|
e.stopPropagation(); |
|
activeTag.remove(); |
|
renderPrompts(); |
|
} else { |
|
toggleTagFilter(tag); |
|
} |
|
}); |
|
|
|
activeTags.appendChild(activeTag); |
|
} |
|
|
|
|
|
prevImage.addEventListener('click', () => { |
|
if (currentLightboxIndex > 0) { |
|
currentLightboxIndex--; |
|
openLightboxModal(currentLightboxIndex); |
|
} |
|
}); |
|
|
|
nextImage.addEventListener('click', () => { |
|
if (currentLightboxIndex < currentLightboxImages.length - 1) { |
|
currentLightboxIndex++; |
|
openLightboxModal(currentLightboxIndex); |
|
} |
|
}); |
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
if (!lightbox.classList.contains('hidden')) { |
|
if (e.key === 'Escape') { |
|
closeLightboxModal(); |
|
} else if (e.key === 'ArrowLeft' && currentLightboxIndex > 0) { |
|
currentLightboxIndex--; |
|
openLightboxModal(currentLightboxIndex); |
|
} else if (e.key === 'ArrowRight' && currentLightboxIndex < currentLightboxImages.length - 1) { |
|
currentLightboxIndex++; |
|
openLightboxModal(currentLightboxIndex); |
|
} |
|
} |
|
}); |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Hairitics/midjourney-prompt-vault" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |