first commit

This commit is contained in:
Space-Banane
2026-01-16 22:30:37 +01:00
commit d7712ea17d
4 changed files with 689 additions and 0 deletions

Binary file not shown.

465
index.html Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
redis==5.0.1