Storage and basic website implementation
This commit is contained in:
parent
7f47c8571c
commit
b4f24abae0
8 changed files with 300 additions and 2 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.env/
|
||||
**/__pycache__/
|
|
@ -1,2 +1,3 @@
|
|||
"id","name","description","seller","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
|
||||
"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.","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
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
fastapi[standard]
|
22
src/main.py
Normal file
22
src/main.py
Normal 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)]}
|
||||
)
|
177
src/templates/wishlist_view.html
Normal file
177
src/templates/wishlist_view.html
Normal 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
2
src/wishlist/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .models import Wishlist, WishlistConfig, WishlistItem
|
||||
from .storage import read_all_wishlists
|
33
src/wishlist/models.py
Normal file
33
src/wishlist/models.py
Normal 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
60
src/wishlist/storage.py
Normal 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
|
Loading…
Reference in a new issue