first commit
This commit is contained in:
BIN
__pycache__/main.cpython-39.pyc
Normal file
BIN
__pycache__/main.cpython-39.pyc
Normal file
Binary file not shown.
465
index.html
Normal file
465
index.html
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🎄 Christmas Wishlist</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.snowflake {
|
||||||
|
position: fixed;
|
||||||
|
top: -10px;
|
||||||
|
z-index: 9999;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: fall linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes fall {
|
||||||
|
to {
|
||||||
|
transform: translateY(100vh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-glow {
|
||||||
|
box-shadow: 0 0 20px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="text-gray-100">
|
||||||
|
<div id="snowflakes"></div>
|
||||||
|
|
||||||
|
<!-- Auth Screen -->
|
||||||
|
<div id="authScreen" class="min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="bg-slate-800 rounded-2xl shadow-2xl p-8 w-full max-w-md card-glow">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-4xl font-bold text-red-500 mb-2">🎄 Christmas Wishlist</h1>
|
||||||
|
<p class="text-gray-400">Made with ❤️ by Claude Sonnet 4.5</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Prompted by <a href="https://space.reversed.dev/" class="text-red-400 hover:underline">Space</a> • <span class="font-bold">ONE SHOT</span> try</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<input type="text" id="username" placeholder="🎅 Username"
|
||||||
|
class="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-red-500 transition">
|
||||||
|
<input type="password" id="password" placeholder="🔒 Password"
|
||||||
|
class="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-red-500 transition">
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick="login()"
|
||||||
|
class="flex-1 bg-red-600 hover:bg-red-700 text-white font-semibold py-3 rounded-lg transition">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button onclick="register()"
|
||||||
|
class="flex-1 bg-green-600 hover:bg-green-700 text-white font-semibold py-3 rounded-lg transition">
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="authError" class="text-red-400 text-sm text-center hidden"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wishlist Screen -->
|
||||||
|
<div id="wishlistScreen" class="hidden min-h-screen p-4 md:p-8">
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
|
<div class="bg-slate-800 rounded-2xl shadow-2xl p-6 md:p-8 card-glow">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-3xl font-bold text-red-500">🎁 My Wishlist</h1>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button onclick="shareWishlist()"
|
||||||
|
class="bg-yellow-500 hover:bg-yellow-600 px-4 py-2 rounded-lg transition text-black font-semibold">
|
||||||
|
Share
|
||||||
|
</button>
|
||||||
|
<button onclick="logout()"
|
||||||
|
class="bg-slate-700 hover:bg-slate-600 px-4 py-2 rounded-lg transition">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Item Form -->
|
||||||
|
<div class="bg-slate-700 rounded-xl p-6 mb-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4 text-green-400">✨ Add New Item</h2>
|
||||||
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
|
<input type="text" id="itemName" placeholder="🎁 Item name"
|
||||||
|
class="px-4 py-2 bg-slate-600 border border-slate-500 rounded-lg focus:outline-none focus:border-green-500 transition">
|
||||||
|
<input type="text" id="itemLink" placeholder="🔗 Link (optional)"
|
||||||
|
class="px-4 py-2 bg-slate-600 border border-slate-500 rounded-lg focus:outline-none focus:border-green-500 transition">
|
||||||
|
<input type="number" id="itemPrice" placeholder="💰 Price" step="0.01"
|
||||||
|
class="px-4 py-2 bg-slate-600 border border-slate-500 rounded-lg focus:outline-none focus:border-green-500 transition">
|
||||||
|
<input type="text" id="itemImage" placeholder="🖼️ Image URL (optional)"
|
||||||
|
class="px-4 py-2 bg-slate-600 border border-slate-500 rounded-lg focus:outline-none focus:border-green-500 transition">
|
||||||
|
</div>
|
||||||
|
<button onclick="addItem()"
|
||||||
|
class="mt-4 w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-3 rounded-lg transition">
|
||||||
|
Add to Wishlist
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items List -->
|
||||||
|
<div id="itemsList" class="space-y-4 mb-6"></div>
|
||||||
|
|
||||||
|
<!-- Summary -->
|
||||||
|
<div class="bg-gradient-to-r from-red-900 to-green-900 rounded-xl p-6 text-center">
|
||||||
|
<div class="flex justify-center gap-8">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-300 text-sm">Total Items</p>
|
||||||
|
<p class="text-3xl font-bold" id="itemCount">0</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-300 text-sm">Total Value</p>
|
||||||
|
<p class="text-3xl font-bold" id="totalPrice">€0.00</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div id="editModal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
|
||||||
|
<div class="bg-slate-800 rounded-2xl p-6 w-full max-w-md card-glow">
|
||||||
|
<h2 class="text-2xl font-bold text-red-500 mb-4">✏️ Edit Item</h2>
|
||||||
|
<input type="hidden" id="editItemId">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<input type="text" id="editItemName" placeholder="🎁 Item name"
|
||||||
|
class="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-red-500 transition">
|
||||||
|
<input type="text" id="editItemLink" placeholder="🔗 Link (optional)"
|
||||||
|
class="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-red-500 transition">
|
||||||
|
<input type="number" id="editItemPrice" placeholder="💰 Price" step="0.01"
|
||||||
|
class="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-red-500 transition">
|
||||||
|
<input type="text" id="editItemImage" placeholder="🖼️ Image URL (optional)"
|
||||||
|
class="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-red-500 transition">
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick="saveEdit()"
|
||||||
|
class="flex-1 bg-green-600 hover:bg-green-700 text-white font-semibold py-3 rounded-lg transition">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button onclick="closeEditModal()"
|
||||||
|
class="flex-1 bg-slate-700 hover:bg-slate-600 text-white font-semibold py-3 rounded-lg transition">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Share Modal -->
|
||||||
|
<div id="shareModal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-50">
|
||||||
|
<div class="bg-slate-800 rounded-2xl p-6 w-full max-w-md card-glow text-center">
|
||||||
|
<h2 class="text-2xl font-bold text-yellow-400 mb-4">🔗 Share Your Wishlist</h2>
|
||||||
|
<input id="shareLink" type="text" readonly class="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg text-yellow-300 mb-4 text-center" />
|
||||||
|
<a id="shareUiLink" href="#" target="_blank" class="block mb-4 text-blue-400 hover:underline break-all"></a>
|
||||||
|
<button onclick="copyShareLink()" class="bg-yellow-500 hover:bg-yellow-600 text-black font-semibold py-2 px-6 rounded-lg transition mb-2">Copy Link</button>
|
||||||
|
<button onclick="closeShareModal()" class="block w-full mt-2 bg-slate-700 hover:bg-slate-600 text-white font-semibold py-2 rounded-lg transition">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentUser = null;
|
||||||
|
|
||||||
|
// Create snowflakes
|
||||||
|
function createSnowflakes() {
|
||||||
|
const container = document.getElementById('snowflakes');
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const snowflake = document.createElement('div');
|
||||||
|
snowflake.className = 'snowflake';
|
||||||
|
snowflake.innerHTML = '❄️';
|
||||||
|
snowflake.style.left = Math.random() * 100 + '%';
|
||||||
|
snowflake.style.animationDuration = Math.random() * 3 + 2 + 's';
|
||||||
|
snowflake.style.animationDelay = Math.random() * 5 + 's';
|
||||||
|
snowflake.style.opacity = Math.random() * 0.6 + 0.4;
|
||||||
|
snowflake.style.fontSize = Math.random() * 10 + 10 + 'px';
|
||||||
|
container.appendChild(snowflake);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createSnowflakes();
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({username, password})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
currentUser = username;
|
||||||
|
showWishlist();
|
||||||
|
loadItems();
|
||||||
|
} else {
|
||||||
|
showError(data.error || 'Login failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError('Connection error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register() {
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({username, password})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
currentUser = username;
|
||||||
|
showWishlist();
|
||||||
|
loadItems();
|
||||||
|
} else {
|
||||||
|
showError(data.error || 'Registration failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError('Connection error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
const errorEl = document.getElementById('authError');
|
||||||
|
errorEl.textContent = message;
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
setTimeout(() => errorEl.classList.add('hidden'), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWishlist() {
|
||||||
|
document.getElementById('authScreen').classList.add('hidden');
|
||||||
|
document.getElementById('wishlistScreen').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
currentUser = null;
|
||||||
|
document.getElementById('authScreen').classList.remove('hidden');
|
||||||
|
document.getElementById('wishlistScreen').classList.add('hidden');
|
||||||
|
document.getElementById('username').value = '';
|
||||||
|
document.getElementById('password').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadItems() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/items?username=${currentUser}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
displayItems(data.items);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load items:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayItems(items) {
|
||||||
|
const container = document.getElementById('itemsList');
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
container.innerHTML = '<p class="text-center text-gray-400 py-8">🎄 No items yet. Add your first wish!</p>';
|
||||||
|
updateSummary(items);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = items.map(item => `
|
||||||
|
<div class="bg-slate-700 rounded-xl p-4 flex gap-4">
|
||||||
|
${item.image ? `<img src="${item.image}" alt="${item.name}" class="w-24 h-24 object-cover rounded-lg">` :
|
||||||
|
'<div class="w-24 h-24 bg-slate-600 rounded-lg flex items-center justify-center text-4xl">🎁</div>'}
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-xl font-semibold text-red-400">${item.name}</h3>
|
||||||
|
${item.link ? `<a href="${item.link}" target="_blank" class="text-blue-400 hover:underline text-sm">🔗 View Item</a>` : ''}
|
||||||
|
<p class="text-2xl font-bold text-green-400 mt-2">${item.price ? '€' + parseFloat(item.price).toFixed(2) : 'No price'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<button onclick="editItem('${item.id}')"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg transition">
|
||||||
|
✏️ Edit
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteItem('${item.id}')"
|
||||||
|
class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-lg transition">
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
updateSummary(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummary(items) {
|
||||||
|
const count = items.length;
|
||||||
|
const total = items.reduce((sum, item) => sum + (parseFloat(item.price) || 0), 0);
|
||||||
|
|
||||||
|
document.getElementById('itemCount').textContent = count;
|
||||||
|
document.getElementById('totalPrice').textContent = '€' + total.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addItem() {
|
||||||
|
const name = document.getElementById('itemName').value;
|
||||||
|
const link = document.getElementById('itemLink').value;
|
||||||
|
const price = document.getElementById('itemPrice').value;
|
||||||
|
const image = document.getElementById('itemImage').value;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
alert('Please enter an item name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({username: currentUser, name, link, price, image})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
document.getElementById('itemName').value = '';
|
||||||
|
document.getElementById('itemLink').value = '';
|
||||||
|
document.getElementById('itemPrice').value = '';
|
||||||
|
document.getElementById('itemImage').value = '';
|
||||||
|
loadItems();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add item:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editItem(id) {
|
||||||
|
fetch(`https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/items?username=${currentUser}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const item = data.items.find(i => i.id === id);
|
||||||
|
if (item) {
|
||||||
|
document.getElementById('editItemId').value = id;
|
||||||
|
document.getElementById('editItemName').value = item.name;
|
||||||
|
document.getElementById('editItemLink').value = item.link || '';
|
||||||
|
document.getElementById('editItemPrice').value = item.price || '';
|
||||||
|
document.getElementById('editItemImage').value = item.image || '';
|
||||||
|
document.getElementById('editModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdit() {
|
||||||
|
const id = document.getElementById('editItemId').value;
|
||||||
|
const name = document.getElementById('editItemName').value;
|
||||||
|
const link = document.getElementById('editItemLink').value;
|
||||||
|
const price = document.getElementById('editItemPrice').value;
|
||||||
|
const image = document.getElementById('editItemImage').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({username: currentUser, id, name, link, price, image})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
closeEditModal();
|
||||||
|
loadItems();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update item:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('editModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteItem(id) {
|
||||||
|
if (!confirm('Are you sure you want to delete this item?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({username: currentUser, id})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
loadItems();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete item:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shareWishlist() {
|
||||||
|
if (!currentUser) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/share', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({username: currentUser})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok && data.share_id) {
|
||||||
|
// UI link that triggers redirect to UI with share_id
|
||||||
|
const uiLink = `https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/?share_id=${data.share_id}`;
|
||||||
|
document.getElementById('shareLink').value = uiLink;
|
||||||
|
document.getElementById('shareUiLink').href = uiLink;
|
||||||
|
document.getElementById('shareUiLink').textContent = uiLink;
|
||||||
|
document.getElementById('shareModal').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Failed to share wishlist');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Failed to share wishlist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeShareModal() {
|
||||||
|
document.getElementById('shareModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyShareLink() {
|
||||||
|
const input = document.getElementById('shareLink');
|
||||||
|
input.select();
|
||||||
|
input.setSelectionRange(0, 99999);
|
||||||
|
document.execCommand('copy');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public wishlist view
|
||||||
|
async function loadPublicWishlist(shareId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/public?share_id=${shareId}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok && data.items) {
|
||||||
|
document.getElementById('authScreen').classList.add('hidden');
|
||||||
|
document.getElementById('wishlistScreen').classList.remove('hidden');
|
||||||
|
// Hide add/edit/delete controls for public view
|
||||||
|
document.querySelectorAll('[onclick^="addItem"]').forEach(el => el.style.display = 'none');
|
||||||
|
document.querySelectorAll('[onclick^="editItem"]').forEach(el => el.style.display = 'none');
|
||||||
|
document.querySelectorAll('[onclick^="deleteItem"]').forEach(el => el.style.display = 'none');
|
||||||
|
document.querySelectorAll('[onclick^="shareWishlist"]').forEach(el => el.style.display = 'none');
|
||||||
|
displayItems(data.items);
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Shared wishlist not found');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Failed to load shared wishlist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On page load, check for share_id in URL
|
||||||
|
(function() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const shareId = params.get('share_id');
|
||||||
|
if (shareId) {
|
||||||
|
loadPublicWishlist(shareId);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
223
main.py
Normal file
223
main.py
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import redis
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
r = redis.Redis(
|
||||||
|
host="185.14.92.202",
|
||||||
|
port=12003,
|
||||||
|
decode_responses=True,
|
||||||
|
username="default",
|
||||||
|
password=os.getenv("REDID"),
|
||||||
|
db=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
route = args.get("route", "default")
|
||||||
|
body = args.get("body", {})
|
||||||
|
try:
|
||||||
|
body = json.loads(body) if isinstance(body, str) else body
|
||||||
|
except:
|
||||||
|
body = {}
|
||||||
|
queries = args.get("queries", {})
|
||||||
|
|
||||||
|
# Serve HTML for default route
|
||||||
|
if route == "default":
|
||||||
|
try:
|
||||||
|
with open("/app/index.html", "r", encoding="utf-8") as f:
|
||||||
|
html = f.read()
|
||||||
|
return {
|
||||||
|
"_shsf": "v2",
|
||||||
|
"_code": 200,
|
||||||
|
"_headers": {"Content-Type": "text/html"},
|
||||||
|
"_res": html
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"_shsf": "v2", "_code": 500, "_res": {"error": str(e)}}
|
||||||
|
|
||||||
|
# Login endpoint
|
||||||
|
elif route == "login":
|
||||||
|
username = body.get("username", "").strip()
|
||||||
|
password = body.get("password", "").strip()
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Username and password required"}}
|
||||||
|
|
||||||
|
# Get stored user
|
||||||
|
user_key = f"storage1:user:{username}"
|
||||||
|
stored_pass = r.get(user_key)
|
||||||
|
|
||||||
|
if stored_pass == password:
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"success": True, "username": username}}
|
||||||
|
else:
|
||||||
|
return {"_shsf": "v2", "_code": 401, "_res": {"error": "Invalid credentials"}}
|
||||||
|
|
||||||
|
# Register endpoint
|
||||||
|
elif route == "register":
|
||||||
|
username = body.get("username", "").strip()
|
||||||
|
password = body.get("password", "").strip()
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Username and password required"}}
|
||||||
|
|
||||||
|
user_key = f"storage1:user:{username}"
|
||||||
|
|
||||||
|
if r.exists(user_key):
|
||||||
|
return {"_shsf": "v2", "_code": 409, "_res": {"error": "Username already exists"}}
|
||||||
|
|
||||||
|
r.set(user_key, password)
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"success": True, "username": username}}
|
||||||
|
|
||||||
|
# Get wishlist items
|
||||||
|
elif route == "items":
|
||||||
|
username = queries.get("username", "")
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Username required"}}
|
||||||
|
|
||||||
|
items_key = f"storage1:items:{username}"
|
||||||
|
items_json = r.get(items_key)
|
||||||
|
|
||||||
|
if items_json:
|
||||||
|
items = json.loads(items_json)
|
||||||
|
else:
|
||||||
|
items = []
|
||||||
|
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"items": items}}
|
||||||
|
|
||||||
|
# Add wishlist item
|
||||||
|
elif route == "add":
|
||||||
|
username = body.get("username", "")
|
||||||
|
name = body.get("name", "").strip()
|
||||||
|
link = body.get("link", "").strip()
|
||||||
|
price = body.get("price", "")
|
||||||
|
image = body.get("image", "").strip()
|
||||||
|
|
||||||
|
if not username or not name:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Username and name required"}}
|
||||||
|
|
||||||
|
items_key = f"storage1:items:{username}"
|
||||||
|
items_json = r.get(items_key)
|
||||||
|
|
||||||
|
if items_json:
|
||||||
|
items = json.loads(items_json)
|
||||||
|
else:
|
||||||
|
items = []
|
||||||
|
|
||||||
|
# Generate ID
|
||||||
|
item_id = str(len(items) + 1)
|
||||||
|
|
||||||
|
new_item = {
|
||||||
|
"id": item_id,
|
||||||
|
"name": name,
|
||||||
|
"link": link,
|
||||||
|
"price": price,
|
||||||
|
"image": image
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(new_item)
|
||||||
|
r.set(items_key, json.dumps(items))
|
||||||
|
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"success": True, "item": new_item}}
|
||||||
|
|
||||||
|
# Update wishlist item
|
||||||
|
elif route == "update":
|
||||||
|
username = body.get("username", "")
|
||||||
|
item_id = body.get("id", "")
|
||||||
|
name = body.get("name", "").strip()
|
||||||
|
link = body.get("link", "").strip()
|
||||||
|
price = body.get("price", "")
|
||||||
|
image = body.get("image", "").strip()
|
||||||
|
|
||||||
|
if not username or not item_id:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Username and item ID required"}}
|
||||||
|
|
||||||
|
items_key = f"storage1:items:{username}"
|
||||||
|
items_json = r.get(items_key)
|
||||||
|
|
||||||
|
if not items_json:
|
||||||
|
return {"_shsf": "v2", "_code": 404, "_res": {"error": "No items found"}}
|
||||||
|
|
||||||
|
items = json.loads(items_json)
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for item in items:
|
||||||
|
if item["id"] == item_id:
|
||||||
|
item["name"] = name
|
||||||
|
item["link"] = link
|
||||||
|
item["price"] = price
|
||||||
|
item["image"] = image
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
return {"_shsf": "v2", "_code": 404, "_res": {"error": "Item not found"}}
|
||||||
|
|
||||||
|
r.set(items_key, json.dumps(items))
|
||||||
|
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"success": True}}
|
||||||
|
|
||||||
|
# Delete wishlist item
|
||||||
|
elif route == "delete":
|
||||||
|
username = body.get("username", "")
|
||||||
|
item_id = body.get("id", "")
|
||||||
|
|
||||||
|
if not username or not item_id:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Username and item ID required"}}
|
||||||
|
|
||||||
|
items_key = f"storage1:items:{username}"
|
||||||
|
items_json = r.get(items_key)
|
||||||
|
|
||||||
|
if not items_json:
|
||||||
|
return {"_shsf": "v2", "_code": 404, "_res": {"error": "No items found"}}
|
||||||
|
|
||||||
|
items = json.loads(items_json)
|
||||||
|
items = [item for item in items if item["id"] != item_id]
|
||||||
|
|
||||||
|
r.set(items_key, json.dumps(items))
|
||||||
|
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"success": True}}
|
||||||
|
|
||||||
|
# Share wishlist (create public copy and return link)
|
||||||
|
elif route == "share":
|
||||||
|
username = body.get("username", "")
|
||||||
|
if not username:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Username required"}}
|
||||||
|
items_key = f"storage1:items:{username}"
|
||||||
|
items_json = r.get(items_key)
|
||||||
|
if not items_json:
|
||||||
|
return {"_shsf": "v2", "_code": 404, "_res": {"error": "No items found"}}
|
||||||
|
# Generate unique share ID
|
||||||
|
share_id = str(uuid.uuid4())
|
||||||
|
public_key = f"storage1:public:{share_id}"
|
||||||
|
r.set(public_key, items_json)
|
||||||
|
share_url = f"https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/public?share_id={share_id}"
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"share_id": share_id, "share_url": share_url}}
|
||||||
|
|
||||||
|
# Get public wishlist by share_id
|
||||||
|
elif route == "public":
|
||||||
|
share_id = queries.get("share_id", "")
|
||||||
|
if not share_id:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Share ID required"}}
|
||||||
|
public_key = f"storage1:public:{share_id}"
|
||||||
|
items_json = r.get(public_key)
|
||||||
|
if not items_json:
|
||||||
|
return {"_shsf": "v2", "_code": 404, "_res": {"error": "Shared wishlist not found"}}
|
||||||
|
items = json.loads(items_json)
|
||||||
|
return {"_shsf": "v2", "_code": 200, "_res": {"items": items}}
|
||||||
|
|
||||||
|
# Redirect to UI with share_id for public viewing
|
||||||
|
elif route == "redirect_share":
|
||||||
|
share_id = queries.get("share_id", "")
|
||||||
|
if not share_id:
|
||||||
|
return {"_shsf": "v2", "_code": 400, "_res": {"error": "Share ID required"}}
|
||||||
|
# Change this URL if your UI is hosted elsewhere
|
||||||
|
ui_url = f"https://shsf-api.reversed.dev/api/exec/4/57aca15b-665e-4015-b7e3-526a87b39b33/?share_id={share_id}"
|
||||||
|
return {
|
||||||
|
"_shsf": "v2",
|
||||||
|
"_code": 302,
|
||||||
|
"_location": ui_url
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {"_shsf": "v2", "_code": 404, "_res": {"error": "Route not found"}}
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
redis==5.0.1
|
||||||
Reference in New Issue
Block a user