Storage and basic website implementation

This commit is contained in:
Maximilian Giller 2024-10-10 17:01:50 +02:00
parent 7f47c8571c
commit b4f24abae0
8 changed files with 300 additions and 2 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.env/
**/__pycache__/

View file

@ -1,2 +1,3 @@
"id","name","description","seller","image","price" "id","name","description","shop","image","price"
"d3e203ba-9aab-4283-8c46-f4a46a5d1f62","Dune: Part 2 - 4K HDR Blu-Ray","Der zweite Film der Dune Reihe in bester Qualität auf Blu-Ray. Die Qualität ist für den Film wichtig, weshalb auf 4K HDR geachtet werden sollte. Das ist in der Regel gut auf der Verpackung gekennzeichnet.",,,29.99 "d3e203ba-9aab-4283-8c46-f4a46a5d1f62","Dune: Part 2 - 4K HDR Blu-Ray","Der zweite Film der Dune Reihe in bester Qualität auf Blu-Ray. Die Qualität ist für den Film wichtig, weshalb auf 4K HDR geachtet werden sollte. Das ist in der Regel gut auf der Verpackung gekennzeichnet.","https://www.mediamarkt.de/de/product/_dune-part-two-blu-ray-2923536.html","https://assets.mmsrg.com/isr/166325/c1/-/ASSET_MMS_138728291?x=536&y=402&format=jpg&quality=80&sp=yes&strip=yes&trim&ex=536&ey=402&align=center&resizesource&unsharp=1.5x1+0.7+0.02&cox=0&coy=0&cdx=536&cdy=402",29.99
"77b3c3a0-303b-482f-a1e0-67c29030ec69","Ex-Machina","","","",14.99
1 id name description seller shop image price
2 d3e203ba-9aab-4283-8c46-f4a46a5d1f62 Dune: Part 2 - 4K HDR Blu-Ray Der zweite Film der Dune Reihe in bester Qualität auf Blu-Ray. Die Qualität ist für den Film wichtig, weshalb auf 4K HDR geachtet werden sollte. Das ist in der Regel gut auf der Verpackung gekennzeichnet. https://www.mediamarkt.de/de/product/_dune-part-two-blu-ray-2923536.html https://assets.mmsrg.com/isr/166325/c1/-/ASSET_MMS_138728291?x=536&y=402&format=jpg&quality=80&sp=yes&strip=yes&trim&ex=536&ey=402&align=center&resizesource&unsharp=1.5x1+0.7+0.02&cox=0&coy=0&cdx=536&cdy=402 29.99
3 77b3c3a0-303b-482f-a1e0-67c29030ec69 Ex-Machina 14.99

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
fastapi[standard]

22
src/main.py Normal file
View file

@ -0,0 +1,22 @@
import uuid
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from wishlist import read_all_wishlists
WISHLISTS_DIR = "../data"
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/{id}/view", response_class=HTMLResponse)
async def read_item(request: Request, id: uuid.UUID):
wishlists = read_all_wishlists(WISHLISTS_DIR)
if str(id) not in wishlists.keys():
return Response(status_code=404)
return templates.TemplateResponse(
request=request, name="wishlist_view.html", context={"wishlist": wishlists[str(id)]}
)

View file

@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ wishlist.config.title }}</title>
<style>
/* Same base styles from the previous version */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
h1, h3 {
text-align: center;
color: #333;
}
ul {
list-style: none;
padding: 0;
}
li {
background-color: #fff;
margin: 10px 0;
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
position: relative;
}
h2 {
margin: 0 0 10px;
color: #2c3e50;
}
p {
color: #7f8c8d;
font-size: 14px;
}
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
margin-top: 10px;
border-radius: 5px;
max-width: 150px;
display: block;
}
.price {
font-weight: bold;
color: #27ae60;
}
hr {
border: none;
border-top: 1px solid #eee;
margin: 20px 0;
}
.reserve-button {
background-color: #3498db;
color: white;
border: none;
padding: 10px;
cursor: pointer;
border-radius: 5px;
}
.reserve-button:hover {
background-color: #2980b9;
}
.reserved-item {
background-color: #f9e79f;
}
form {
margin-top: 10px;
}
input[type="text"] {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
@media (min-width: 768px) {
li {
display: flex;
align-items: center;
justify-content: space-between;
}
img {
max-width: 100px;
margin-left: 20px;
}
}
</style>
</head>
<body>
<h1>{{ wishlist.config.title }}</h1>
<ul>
<!-- Loop through unreserved items -->
{% for item_id, item in wishlist.items.items() if not item.is_reserved %}
<li>
<div>
<h2>{{ item.name }}</h2>
<p>{{ item.description }}</p>
<p class="price">Price: €{{ "%.2f"|format(item.price) }}</p>
<a href="{{ item.shop }}" target="_blank">Shop Link</a>
<!-- Form for reserving the item -->
<form method="POST" action="/reserve-item">
<input type="hidden" name="wishlist_id" value="{{ wishlist.config.id }}">
<input type="hidden" name="item_id" value="{{ item.id }}">
<input type="hidden" name="reserved" value="true">
<label for="name_{{ item_id }}">Reserve for:</label>
<input type="text" id="name_{{ item_id }}" name="reserver_name" placeholder="Your name">
<button class="reserve-button" type="submit">Reserve</button>
</form>
</div>
<img src="{{ item.image }}" alt="{{ item.name }}">
</li>
<hr>
{% endfor %}
</ul>
<!-- Collapsible section for reserved items -->
<h3>Reserved Items (click to expand/collapse)</h3>
<ul id="reserved-items" style="display:none;">
{% for item_id, item in wishlist.items.items() if item.is_reserved %}
<li class="reserved-item">
<div>
<h2>{{ item.name }} (Reserved)</h2>
<p>{{ item.description }}</p>
<p class="price">Price: €{{ "%.2f"|format(item.price) }}</p>
<a href="{{ item.shop }}" target="_blank">Shop Link</a>
<!-- Form for unreserving the item -->
<form method="POST" action="/reserve-item">
<input type="hidden" name="wishlist_id" value="{{ wishlist.config.id }}">
<input type="hidden" name="item_id" value="{{ item.id }}">
<input type="hidden" name="reserved" value="false">
<label for="name_{{ item_id }}">Reserved by:</label>
<input type="text" id="name_{{ item_id }}" name="reserver_name" value="{{ item.reservation.name }}" readonly>
<button class="reserve-button" type="submit">Unreserve</button>
</form>
</div>
<img src="{{ item.image }}" alt="{{ item.name }}">
</li>
<hr>
{% endfor %}
</ul>
<script>
// Toggle display of reserved items
document.querySelector('h3').addEventListener('click', function() {
const reservedItems = document.getElementById('reserved-items');
reservedItems.style.display = (reservedItems.style.display === 'none') ? 'block' : 'none';
});
</script>
</body>
</html>

2
src/wishlist/__init__.py Normal file
View file

@ -0,0 +1,2 @@
from .models import Wishlist, WishlistConfig, WishlistItem
from .storage import read_all_wishlists

33
src/wishlist/models.py Normal file
View file

@ -0,0 +1,33 @@
import uuid
from datetime import date
class ItemReservation:
def __init__(self) -> None:
self.name: str = ""
class WishlistItem:
def __init__(self) -> None:
self.id: uuid = uuid.uuid4()
self.name: str = ""
self.description: str = ""
self.shop: str = ""
self.image: str = ""
self.price: float = 0
self.reservation: ItemReservation | None = None
@property
def is_reserved(self) -> bool:
return self.reservation is not None
class WishlistConfig:
def __init__(self) -> None:
self.id: uuid = uuid.uuid4()
self.title: str = ""
self.deadlines: dict[str, date] = {}
self.deadline_offset_days: int = 0
class Wishlist:
def __init__(self) -> None:
self.directory: str = ""
self.items: dict[str, WishlistItem] = {} # Mapping Item ID to item
self.config: WishlistConfig = None

60
src/wishlist/storage.py Normal file
View file

@ -0,0 +1,60 @@
from .models import Wishlist, WishlistConfig, WishlistItem
from datetime import date
import uuid
import json
import csv
import os
def read_wishlist_config(wishlist_dir: str) -> WishlistConfig:
path: str = f"{wishlist_dir}/config.json"
with open(path, "r", encoding="UTF-8") as fp:
json_config = json.load(fp)
config = WishlistConfig()
config.id = uuid.UUID(json_config["id"])
config.title = json_config["title"]
config.deadline_offset_days = json_config["deadlineOffsetDays"]
for label, timestamp in json_config["deadlines"].items():
config.deadlines[label] = date.fromisoformat(timestamp)
return config
def read_wishlist_items(wishlist_dir: str) -> dict[str, WishlistItem]:
items: dict[str, WishlistItem] = {}
path: str = f"{wishlist_dir}/wishlist.csv"
with open(path, "r", encoding = "UTF-8") as fp:
csvreader = csv.DictReader(fp)
for row in csvreader:
item: WishlistItem = WishlistItem()
item.id = uuid.UUID(row.get("id"))
item.name = row.get("name")
item.description = row.get("description")
item.shop = row.get("shop")
item.image = row.get("image")
item.price = float(row.get("price"))
items[str(item.id)] = item
return items
def read_wishlist(wishlist_dir: str) -> Wishlist:
wishlist = Wishlist()
wishlist.directory = wishlist_dir
wishlist.items = read_wishlist_items(wishlist.directory)
wishlist.config = read_wishlist_config(wishlist.directory)
return wishlist
def read_all_wishlists(directory: str) -> dict[str, Wishlist]:
parent_dir, wishlist_dirs, _ = list(os.walk(directory))[0]
wishlists: dict[str, Wishlist] = {}
for dir in wishlist_dirs:
wishlist: Wishlist = read_wishlist(f"{parent_dir}/{dir}")
wishlists[str(wishlist.config.id)] = wishlist
return wishlists