fuck i did it again - STOP DOING EVEYRTHING COMMIT ONCE

This commit is contained in:
End 2025-12-17 20:19:59 -07:00
parent 42a9b3802f
commit 97b8f1fd7d
No known key found for this signature in database
7 changed files with 471 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
venv/
gifts.db
__pycache__/
.env
*.pyc

View file

@ -0,0 +1,55 @@
# Gift Tracker 🎁
A simple web app to keep track of gifts you need to give people. Built with Flask.
## Features
- Password protected
- Add gifts with recipient name and category
- Mark gifts as purchased or delivered
- Delete gifts when done
- Tracks stats (total, purchased, delivered)
## Setup
1. Clone this repo
2. Create a virtual environment:
```
python -m venv venv
source venv/bin/activate
```
On Windows use: `venv\Scripts\activate`
3. Install dependencies:
```
pip install -r requirements.txt
```
4. Create a `.env` file with your password:
```
APP_PASSWORD=your-secret-password
```
5. Run it:
```
python main.py
```
6. Go to http://127.0.0.1:5000
## Deploying to Railway
1. Push to GitHub
2. Connect repo to Railway
3. Add environment variable `APP_PASSWORD` in Railway settings
4. Set start command: `gunicorn main:app --bind 0.0.0.0:$PORT`
5. Generate a domain
## Deploying to PythonAnywhere
1. Clone repo in PythonAnywhere bash console
2. Create venv and install requirements
3. Create `.env` file with your password
4. Configure WSGI file to point to your app
5. Reload the web app

145
main.py Normal file
View file

@ -0,0 +1,145 @@
import os
import sqlite3
import flask
from dotenv import load_dotenv
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
# Load .env file
load_dotenv()
app = flask.Flask(__name__, static_folder="static", static_url_path="/")
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day"],
storage_uri="memory://",
)
APP_PASSWORD = os.environ.get("APP_PASSWORD", "changeme")
# Database setup
conn = sqlite3.connect("gifts.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS gifts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
gift TEXT NOT NULL,
category TEXT DEFAULT '🎄 Christmas',
status TEXT DEFAULT 'pending'
)
""")
conn.commit()
conn.close()
def check_password(request):
"""Check if the password in headers matches."""
password = request.headers.get("X-Password", "")
return password == APP_PASSWORD
@app.get("/")
@limiter.exempt
def index():
return flask.send_from_directory("static", "index.html")
@app.post("/login")
@limiter.limit("10 per minute")
def login():
data = flask.request.get_json()
password = data.get("password", "")
if password == APP_PASSWORD:
return "", 200
else:
return "", 401
@app.post("/gifts")
@limiter.limit("30 per minute")
def create_gift():
if not check_password(flask.request):
return "", 401
data = flask.request.get_json()
name = data.get("name")
gift = data.get("gift")
category = data.get("category", "🎄 Christmas")
conn = sqlite3.connect("gifts.db")
cursor = conn.cursor()
cursor.execute(
"INSERT INTO gifts (name, gift, category) VALUES (?, ?, ?)",
(name, gift, category),
)
conn.commit()
conn.close()
return "", 201
@app.get("/gifts")
def get_gifts():
if not check_password(flask.request):
return "", 401
conn = sqlite3.connect("gifts.db")
cursor = conn.cursor()
cursor.execute(
"SELECT id, name, gift, category, status FROM gifts ORDER BY id DESC"
)
rows = cursor.fetchall()
conn.close()
gifts = [
{
"id": row[0],
"name": row[1],
"gift": row[2],
"category": row[3],
"status": row[4],
}
for row in rows
]
return flask.jsonify(gifts)
@app.patch("/gifts/<int:gift_id>")
def update_gift(gift_id):
if not check_password(flask.request):
return "", 401
data = flask.request.get_json()
status = data.get("status")
conn = sqlite3.connect("gifts.db")
cursor = conn.cursor()
cursor.execute("UPDATE gifts SET status = ? WHERE id = ?", (status, gift_id))
conn.commit()
conn.close()
return "", 200
@app.delete("/gifts/<int:gift_id>")
def delete_gift(gift_id):
if not check_password(flask.request):
return "", 401
conn = sqlite3.connect("gifts.db")
cursor = conn.cursor()
cursor.execute("DELETE FROM gifts WHERE id = ?", (gift_id,))
conn.commit()
conn.close()
return "", 200
if __name__ == "__main__":
app.run(debug=True)

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
flask
Flask-Limiter
gunicorn
python-dotenv

57
static/index.html Normal file
View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gift Tracker</title>
<script src="./main.js" defer></script>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div id="loginScreen" class="content">
<h1>Gift Tracker</h1>
<img src="https://haxmas.hackclub.com/haxmas-logo.png" alt="Haxmas Logo">
<p>Enter password to access your gift list</p>
<form id="loginForm">
<input id="password" type="password" name="password" placeholder="Enter password..." required>
<br>
<input type="submit" value="🔓 Unlock">
</form>
<p id="loginError" class="error"></p>
</div>
<div id="mainApp" class="content hidden">
<h1>Gift Tracker</h1>
<img src="https://haxmas.hackclub.com/haxmas-logo.png" alt="Haxmas Logo">
<p>Tracking gifts since 2025</p>
<form id="giftForm">
<label for="name">Recipient: </label>
<input id="name" type="text" name="name" placeholder="Who's getting the gift?" required>
<br>
<label for="gift">Gift: </label>
<input id="gift" type="text" name="gift" placeholder="What are you giving?" required>
<br>
<label for="category">Category: </label>
<select id="category" name="category">
<option value="Christmas">Christmas</option>
<option value="Birthday">Birthday</option>
<option value="Valentine">Valentine</option>
<option value="Other" Other</option>
</select>
<br>
<input type="submit" value=" Add Gift">
</form>
<div class="stats">
<span id="totalGifts">0 gifts</span> |
<span id="purchasedGifts">0 purchased</span> |
<span id="deliveredGifts">0 delivered</span>
</div>
<div id="gifts"></div>
<button id="logoutBtn" class="logout-btn">Logout</button>
</div>
</body>
</html>

124
static/main.js Normal file
View file

@ -0,0 +1,124 @@
// Elements
const loginScreen = document.getElementById("loginScreen");
const mainApp = document.getElementById("mainApp");
const loginForm = document.getElementById("loginForm");
const loginError = document.getElementById("loginError");
const giftForm = document.getElementById("giftForm");
const giftsContainer = document.getElementById("gifts");
const logoutBtn = document.getElementById("logoutBtn");
// Stats elements
const totalGiftsEl = document.getElementById("totalGifts");
const purchasedGiftsEl = document.getElementById("purchasedGifts");
const deliveredGiftsEl = document.getElementById("deliveredGifts");
let currentPassword = "";
// Login handling
loginForm.addEventListener("submit", async (event) => {
event.preventDefault();
const password = loginForm.elements.password.value;
const response = await fetch("/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
});
if (response.ok) {
currentPassword = password;
loginScreen.classList.add("hidden");
mainApp.classList.remove("hidden");
loginError.textContent = "";
loadGifts();
} else {
loginError.textContent = "❌ Wrong password! Try again.";
}
});
// Logout
logoutBtn.addEventListener("click", () => {
currentPassword = "";
mainApp.classList.add("hidden");
loginScreen.classList.remove("hidden");
loginForm.reset();
});
// Load gifts from server
async function loadGifts() {
const response = await fetch("/gifts", {
headers: { "X-Password": currentPassword },
});
const gifts = await response.json();
giftsContainer.innerHTML = "";
let purchased = 0,
delivered = 0;
gifts.forEach((gift) => {
if (gift.status === "purchased") purchased++;
if (gift.status === "delivered") delivered++;
const item = document.createElement("div");
item.className = `gift-item ${gift.status}`;
item.innerHTML = `
<div class="gift-info">
<strong>${gift.category}</strong> for <strong>${gift.name}</strong><br>
🎁 ${gift.gift}
${gift.status !== "pending" ? `<br><em>(${gift.status})</em>` : ""}
</div>
<div class="gift-buttons">
${gift.status === "pending" ? `<button class="btn-purchase" onclick="updateStatus(${gift.id}, 'purchased')">✓ Bought</button>` : ""}
${gift.status === "purchased" ? `<button class="btn-deliver" onclick="updateStatus(${gift.id}, 'delivered')">🚚 Delivered</button>` : ""}
<button class="btn-delete" onclick="deleteGift(${gift.id})">🗑</button>
</div>
`;
giftsContainer.appendChild(item);
});
totalGiftsEl.textContent = `${gifts.length} gifts`;
purchasedGiftsEl.textContent = `${purchased} purchased`;
deliveredGiftsEl.textContent = `${delivered} delivered`;
}
giftForm.addEventListener("submit", async (event) => {
event.preventDefault();
const name = giftForm.elements.name.value;
const gift = giftForm.elements.gift.value;
const category = giftForm.elements.category.value;
await fetch("/gifts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Password": currentPassword,
},
body: JSON.stringify({ name, gift, category }),
});
giftForm.reset();
await loadGifts();
});
async function updateStatus(id, status) {
await fetch(`/gifts/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-Password": currentPassword,
},
body: JSON.stringify({ status }),
});
await loadGifts();
}
async function deleteGift(id) {
if (confirm("Are you sure you want to delete this gift?")) {
await fetch(`/gifts/${id}`, {
method: "DELETE",
headers: { "X-Password": currentPassword },
});
await loadGifts();
}
}

81
static/styles.css Normal file
View file

@ -0,0 +1,81 @@
body {
margin: 0;
padding: 20px;
background-color: #2d5a3f;
font-family: Arial, sans-serif;
}
.content {
text-align: center;
max-width: 500px;
margin: 0 auto;
background: white;
padding: 25px;
border-radius: 10px;
}
.hidden {
display: none;
}
h1 {
color: #c41e3a;
}
img {
width: 100px;
}
form {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
}
input[type="text"],
input[type="password"],
select {
padding: 8px;
margin: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
input[type="submit"],
button {
background: #c41e3a;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
}
.stats {
background: #1a472a;
color: white;
padding: 10px;
border-radius: 5px;
margin: 15px 0;
}
.gift-item {
background: #fafafa;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
margin: 8px 0;
text-align: left;
}
.gift-buttons button {
padding: 5px 10px;
margin: 3px;
font-size: 12px;
}
.error {
color: red;
}