mirror of
https://github.com/System-End/haxmas-day-5.git
synced 2026-04-19 15:18:17 +00:00
fuck i did it again - STOP DOING EVEYRTHING COMMIT ONCE
This commit is contained in:
parent
42a9b3802f
commit
97b8f1fd7d
7 changed files with 471 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
venv/
|
||||
gifts.db
|
||||
__pycache__/
|
||||
.env
|
||||
*.pyc
|
||||
55
README.md
55
README.md
|
|
@ -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
145
main.py
Normal 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
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
flask
|
||||
Flask-Limiter
|
||||
gunicorn
|
||||
python-dotenv
|
||||
57
static/index.html
Normal file
57
static/index.html
Normal 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
124
static/main.js
Normal 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
81
static/styles.css
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue