From d1394506abcd49965e7e8cbbd9cd1198c4e95c90 Mon Sep 17 00:00:00 2001 From: retoor Date: Tue, 5 Aug 2025 03:07:46 +0200 Subject: [PATCH] Update, backend rendering. --- classic.html | 1976 ++++++++++++++++++++++++++ locustfile.py | 256 ++++ main.py | 541 ++++++- templates/base.html | 794 +++++++++++ templates/components/modals.html | 133 ++ templates/components/navigation.html | 23 + templates/components/rant_card.html | 35 + templates/feed.html | 16 + templates/login.html | 26 + templates/notifications.html | 15 + templates/profile.html | 74 + templates/rant_detail.html | 103 ++ templates/search.html | 23 + 13 files changed, 3983 insertions(+), 32 deletions(-) create mode 100644 classic.html create mode 100644 locustfile.py create mode 100644 templates/base.html create mode 100644 templates/components/modals.html create mode 100644 templates/components/navigation.html create mode 100644 templates/components/rant_card.html create mode 100644 templates/feed.html create mode 100644 templates/login.html create mode 100644 templates/notifications.html create mode 100644 templates/profile.html create mode 100644 templates/rant_detail.html create mode 100644 templates/search.html diff --git a/classic.html b/classic.html new file mode 100644 index 0000000..5dcc382 --- /dev/null +++ b/classic.html @@ -0,0 +1,1976 @@ + + + + + + Rant Community + + + + + + + +
+ + + + + + + + + + diff --git a/locustfile.py b/locustfile.py new file mode 100644 index 0000000..e7b8688 --- /dev/null +++ b/locustfile.py @@ -0,0 +1,256 @@ +from locust import HttpUser, task, between +import random +import uuid + +class RantCommunityUser(HttpUser): + wait_time = between(1, 5) + host = "http://127.0.0.1:8111" + + def on_start(self): + self.token_id = random.randint(1, 1000) + self.token_key = str(uuid.uuid4()) + self.user_id = random.randint(1, 1000) + self.username = f"user_{self.user_id}" + self.password = "testpassword123" + + @task(2) + def home_page(self): + self.client.get(f"/?sort=recent") + + @task(2) + def rant_detail(self): + rant_id = random.randint(1, 100) + self.client.get(f"/rant/{rant_id}") + + @task(2) + def profile_page(self): + user_id = random.randint(1, 100) + tab = random.choice(["rants", "comments", "favorites"]) + self.client.get(f"/profile/{user_id}?tab={tab}") + + @task(1) + def search_page(self): + term = random.choice(["test", "rant", "community"]) + self.client.get(f"/search?term={term}") + + @task(1) + def notifications(self): + self.client.get("/notifications") + + @task(1) + def classic_page(self): + self.client.get("/classic") + + @task(1) + def login(self): + self.client.post("/login", data={ + "username": self.username, + "password": self.password + }) + + @task(1) + def logout(self): + self.client.get("/logout") + + @task(1) + def register_user(self): + self.client.post("/api/users", data={ + "email": f"{self.username}@example.com", + "username": self.username, + "password": self.password, + "type": 1, + "app": 3 + }) + + @task(1) + def auth_token(self): + self.client.post("/api/users/auth-token", data={ + "username": self.username, + "password": self.password, + "app": 3 + }) + + @task(3) + def get_rants(self): + self.client.get(f"/api/rant/rants?sort=recent&limit=20&skip=0&app=3&token_id={self.token_id}&token_key={self.token_key}&user_id={self.user_id}") + + @task(2) + def create_rant(self): + self.client.post("/api/rant/rants", data={ + "rant": "This is a test rant", + "tags": "test,example", + "type": 1, + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(2) + def get_rant(self): + rant_id = random.randint(1, 100) + self.client.get(f"/api/rant/rants/{rant_id}?app=3&token_id={self.token_id}&token_key={self.token_key}&user_id={self.user_id}") + + @task(1) + def update_rant(self): + rant_id = random.randint(1, 100) + self.client.post(f"/api/rant/rants/{rant_id}", data={ + "rant": "Updated test rant", + "tags": "test,updated", + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def delete_rant(self): + rant_id = random.randint(1, 100) + self.client.delete(f"/api/rant/rants/{rant_id}?app=3&token_id={self.token_id}&token_key={self.token_key}&user_id={self.user_id}") + + @task(2) + def vote_rant(self): + rant_id = random.randint(1, 100) + self.client.post(f"/api/rant/rants/{rant_id}/vote", data={ + "vote": random.choice([1, -1]), + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def favorite_rant(self): + rant_id = random.randint(1, 100) + self.client.post(f"/api/rant/rants/{rant_id}/favorite", data={ + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def unfavorite_rant(self): + rant_id = random.randint(1, 100) + self.client.post(f"/api/rant/rants/{rant_id}/unfavorite", data={ + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(2) + def create_comment(self): + rant_id = random.randint(1, 100) + self.client.post(f"/api/rant/rants/{rant_id}/comments", data={ + "comment": "Test comment", + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def get_comment(self): + comment_id = random.randint(1, 100) + self.client.get(f"/api/comments/{comment_id}?app=3&token_id={self.token_id}&token_key={self.token_key}&user_id={self.user_id}") + + @task(1) + def update_comment(self): + comment_id = random.randint(1, 100) + self.client.post(f"/api/comments/{comment_id}", data={ + "comment": "Updated test comment", + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def delete_comment(self): + comment_id = random.randint(1, 100) + self.client.delete(f"/api/comments/{comment_id}?app=3&token_id={self.token_id}&token_key={self.token_key}&user_id={self.user_id}") + + @task(1) + def vote_comment(self): + comment_id = random.randint(1, 100) + self.client.post(f"/api/comments/{comment_id}/vote", data={ + "vote": random.choice([1, -1]), + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def get_profile(self): + user_id = random.randint(1, 100) + self.client.get(f"/api/users/{user_id}?app=3&token_id={self.token_id}&token_key={self.token_key}&auth_user_id={self.user_id}") + + @task(1) + def get_user_id(self): + self.client.get(f"/api/get-user-id?username={self.username}&app=3") + + @task(1) + def search(self): + term = random.choice(["test", "rant", "community"]) + self.client.get(f"/api/rant/search?term={term}&app=3&token_id={self.token_id}&token_key={self.token_key}&user_id={self.user_id}") + + @task(1) + def get_notifications(self): + self.client.get(f"/api/users/me/notif-feed?ext_prof=1&app=3&token_id={self.token_id}&token_key={self.token_key}&user_id={self.user_id}") + + @task(1) + def clear_notifications(self): + self.client.delete(f"/api/users/me/notif-feed", data={ + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def edit_profile(self): + self.client.post("/api/users/me/edit-profile", data={ + "profile_about": "Test about", + "profile_skills": "Python,Testing", + "profile_location": "Test City", + "profile_website": "http://example.com", + "profile_github": "http://github.com/test", + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def forgot_password(self): + self.client.post("/api/users/forgot-password", data={ + "username": self.username, + "app": 3 + }) + + @task(1) + def resend_confirmation(self): + self.client.post("/api/users/me/resend-confirm", data={ + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def mark_news_read(self): + news_id = str(uuid.uuid4()) + self.client.post("/api/users/me/mark-news-read", data={ + "news_id": news_id, + "app": 3, + "token_id": self.token_id, + "token_key": self.token_key, + "user_id": self.user_id + }) + + @task(1) + def get_upload(self): + filename = f"test_{random.randint(1, 100)}.jpg" + self.client.get(f"/uploads/{filename}") diff --git a/main.py b/main.py index 32334eb..6710c76 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ -from fastapi import FastAPI, HTTPException, Depends, Form, File, UploadFile +from fastapi import FastAPI, HTTPException, Depends, Form, File, UploadFile, Request, Response from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates from pydantic import BaseModel, EmailStr from typing import Optional, List, Dict, Any, Literal from datetime import datetime, timedelta @@ -28,6 +30,10 @@ DB_PATH = "rant_community.db" UPLOAD_DIR = Path("uploads") UPLOAD_DIR.mkdir(exist_ok=True) +# Template setup +templates = Jinja2Templates(directory="templates") + + # Initialize AsyncDataSet db = AsyncDataSet(DB_PATH) @@ -115,6 +121,10 @@ async def init_db(): @app.on_event("startup") async def startup_event(): await init_db() + # Create necessary directories + Path("templates").mkdir(exist_ok=True) + Path("templates/components").mkdir(exist_ok=True) + Path("static").mkdir(exist_ok=True) # Pydantic models class UserRegister(BaseModel): @@ -145,9 +155,59 @@ def hash_password(password: str) -> str: def generate_token() -> str: return secrets.token_urlsafe(32) -async def DELETE_get_current_user(token_id: Optional[int] = Form(None), - token_key: Optional[str] = Form(None), - user_id: Optional[int] = Form(None)): +def format_time(timestamp): + date = datetime.fromtimestamp(timestamp) + now = datetime.now() + diff = (now - date).total_seconds() + + if diff < 60: return 'just now' + if diff < 3600: return f'{int(diff / 60)}m ago' + if diff < 86400: return f'{int(diff / 3600)}h ago' + if diff < 604800: return f'{int(diff / 86400)}d ago' + + return date.strftime('%Y-%m-%d') + + +templates.env.globals['format_time'] = format_time + +# Add template context processors +def escape_html(text): + return text.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') + + +templates.env.globals['escape_html'] = escape_html + +# Authentication helpers +async def get_current_user_from_cookie(request: Request): + """Get current user from session cookie""" + auth_token = request.cookies.get("auth_token") + if not auth_token: + return None + + try: + token_data = json.loads(auth_token) + token = await db.get("auth_tokens", { + "id": token_data['id'], + "token_key": token_data['key'], + "user_id": token_data['user_id'] + }) + + if not token or token['expire_time'] <= int(datetime.now().timestamp()): + return None + + user = await db.get("users", {"id": token_data['user_id']}) + if user: + # Add token info to user object for client-side API calls + user['token_id'] = token_data['id'] + user['token_key'] = token_data['key'] + return user + except: + return None + +async def authenticate_user(token_id: Optional[int] = None, + token_key: Optional[str] = None, + user_id: Optional[int] = None): + """Generic authentication function that works with any parameter source""" if not all([token_id, token_key, user_id]): return None @@ -232,7 +292,446 @@ async def format_comment(comment_row, user_row, current_user_id=None): } } -# Endpoints +# SSR Routes +@app.get("/", response_class=HTMLResponse) +async def home(request: Request, sort: str = "recent"): + current_user = await get_current_user_from_cookie(request) + current_user_id = current_user['id'] if current_user else None + + # Get rants + order_by = "r.created_time DESC" if sort == "recent" else "r.score DESC" + rows = await db.query_raw( + f"""SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + ORDER BY {order_by} + LIMIT 50 OFFSET 0""", + () + ) + + rants = [] + for row in rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } + + rants.append(await format_rant(rant_data, user_data, current_user_id)) + + # Get notification count + notif_count = 0 + if current_user: + notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0}) + + return templates.TemplateResponse("feed.html", { + "request": request, + "current_user": current_user, + "notif_count": notif_count, + "rants": rants, + "current_sort": sort, + "escape_html": escape_html, + "format_time": format_time + }) + +@app.get("/rant/{rant_id}", response_class=HTMLResponse) +async def rant_detail(request: Request, rant_id: int): + current_user = await get_current_user_from_cookie(request) + current_user_id = current_user['id'] if current_user else None + + # Get rant with user info + rant_row = await db.query_one( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + WHERE r.id = ?""", + (rant_id,) + ) + + if not rant_row: + raise HTTPException(status_code=404, detail="Rant not found") + + rant_data = { + 'id': rant_row['id'], + 'text': rant_row['text'], + 'score': rant_row['score'], + 'created_time': rant_row['created_time'], + 'attached_image': rant_row['attached_image'], + 'tags': rant_row['tags'], + 'edited': rant_row['edited'], + 'type': rant_row['type'] + } + user_data = { + 'id': rant_row['user_id'], + 'username': rant_row['username'], + 'score': rant_row['user_score'], + 'avatar_b': rant_row['avatar_b'], + 'avatar_i': rant_row['avatar_i'] + } + + rant = await format_rant(rant_data, user_data, current_user_id) + + # Get comments + comment_rows = await db.query_raw( + """SELECT c.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM comments c + JOIN users u ON c.user_id = u.id + WHERE c.rant_id = ? + ORDER BY c.created_time ASC""", + (rant_id,) + ) + + comments = [] + for row in comment_rows: + comment_data = { + 'id': row['id'], + 'rant_id': row['rant_id'], + 'body': row['body'], + 'score': row['score'], + 'created_time': row['created_time'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } + + comments.append(await format_comment(comment_data, user_data, current_user_id)) + + # Check if subscribed (favorited) + subscribed = 0 + if current_user_id: + if await db.exists("favorites", {"user_id": current_user_id, "rant_id": rant_id}): + subscribed = 1 + + # Get notification count + notif_count = 0 + if current_user: + notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0}) + + return templates.TemplateResponse("rant_detail.html", { + "request": request, + "current_user": current_user, + "notif_count": notif_count, + "rant": rant, + "comments": comments, + "subscribed": subscribed, + "escape_html": escape_html, + "format_time": format_time + }) + +@app.get("/profile/{user_id}", response_class=HTMLResponse) +async def profile(request: Request, user_id: int, tab: str = "rants"): + current_user = await get_current_user_from_cookie(request) + current_user_id = current_user['id'] if current_user else None + + # Get user + user = await db.get("users", {"id": user_id}) + + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Get user's rants + rant_rows = await db.query_raw( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + WHERE r.user_id = ? + ORDER BY r.created_time DESC + LIMIT 50""", + (user_id,) + ) + + rants = [] + for row in rant_rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } + + rants.append(await format_rant(rant_data, user_data, current_user_id)) + + # Get user's comments + comment_rows = await db.query_raw( + """SELECT c.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM comments c + JOIN users u ON c.user_id = u.id + WHERE c.user_id = ? + ORDER BY c.created_time DESC + LIMIT 50""", + (user_id,) + ) + + comments = [] + for row in comment_rows: + comment_data = { + 'id': row['id'], + 'rant_id': row['rant_id'], + 'body': row['body'], + 'score': row['score'], + 'created_time': row['created_time'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } + + comments.append(await format_comment(comment_data, user_data, current_user_id)) + + # Get favorited rants + favorite_rows = await db.query_raw( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + JOIN favorites f ON f.rant_id = r.id + WHERE f.user_id = ? + ORDER BY f.id DESC + LIMIT 50""", + (user_id,) + ) + + favorites = [] + for row in favorite_rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } + + favorites.append(await format_rant(rant_data, user_data, current_user_id)) + + # Get notification count + notif_count = 0 + if current_user: + notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0}) + + profile_data = { + "username": user['username'], + "score": user['score'], + "about": user['about'], + "location": user['location'], + "created_time": user['created_time'], + "skills": user['skills'], + "github": user['github'], + "website": user['website'], + "avatar": { + "b": user['avatar_b'], + "i": user['avatar_i'] or "" + } + } + + return templates.TemplateResponse("profile.html", { + "request": request, + "current_user": current_user, + "notif_count": notif_count, + "profile": profile_data, + "profile_user_id": user_id, + "rants": rants, + "comments": comments, + "favorites": favorites, + "active_tab": tab, + "escape_html": escape_html, + "format_time": format_time + }) + +@app.get("/search", response_class=HTMLResponse) +async def search_page(request: Request, term: str = None): + current_user = await get_current_user_from_cookie(request) + current_user_id = current_user['id'] if current_user else None + + results = [] + if term: + # Search rants + rows = await db.query_raw( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + WHERE r.text LIKE ? OR r.tags LIKE ? + ORDER BY r.score DESC + LIMIT 50""", + (f'%{term}%', f'%{term}%') + ) + + for row in rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } + + results.append(await format_rant(rant_data, user_data, current_user_id)) + + # Get notification count + notif_count = 0 + if current_user: + notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0}) + + return templates.TemplateResponse("search.html", { + "request": request, + "current_user": current_user, + "notif_count": notif_count, + "search_term": term, + "results": results, + "escape_html": escape_html, + "format_time": format_time + }) + +@app.get("/notifications", response_class=HTMLResponse) +async def notifications(request: Request): + current_user = await get_current_user_from_cookie(request) + if not current_user: + return RedirectResponse("/", status_code=302) + + # Get notifications + rows = await db.query_raw( + """SELECT n.*, u.username + FROM notifications n + LEFT JOIN users u ON n.from_user_id = u.id + WHERE n.user_id = ? + ORDER BY n.created_time DESC + LIMIT 50""", + (current_user['id'],) + ) + + items = [] + for row in rows: + item = { + "type": row['type'], + "rant_id": row['rant_id'], + "comment_id": row['comment_id'], + "created_time": row['created_time'], + "read": row['read'], + "uid": row['from_user_id'], + "username": row['username'] or "" + } + items.append(item) + + # Mark notifications as read + if rows: + await db.update("notifications", {"read": 1}, {"user_id": current_user['id']}) + + return templates.TemplateResponse("notifications.html", { + "request": request, + "current_user": current_user, + "notif_count": 0, # We just marked them as read + "items": items, + "format_time": format_time + }) + +@app.get("/classic", response_class=HTMLResponse) +async def classic(): + return FileResponse("classic.html") + +# Authentication endpoints with cookie support +@app.post("/login") +async def login_form( + request: Request, + response: Response, + username: str = Form(...), + password: str = Form(...) +): + # Find user by username or email + user = await db.query_one( + "SELECT * FROM users WHERE username = ? OR email = ?", + (username, username) + ) + + if not user or user['password_hash'] != hash_password(password): + return templates.TemplateResponse("login.html", { + "request": request, + "error": "Invalid login credentials entered. Please try again." + }) + + # Create auth token + token_key = generate_token() + expire_time = int((datetime.now() + timedelta(days=30)).timestamp()) + + token_id = await db.insert("auth_tokens", { + "user_id": user['id'], + "token_key": token_key, + "expire_time": expire_time + }, return_id=True) + + # Set cookie + auth_token = { + "id": token_id, + "key": token_key, + "expire_time": expire_time, + "user_id": user['id'] + } + response = RedirectResponse("/", status_code=302) + response.set_cookie("auth_token", json.dumps(auth_token), max_age=30*24*60*60) + + return response + +@app.get("/logout") +async def logout(response: Response): + response = RedirectResponse("/", status_code=302) + response.delete_cookie("auth_token") + return response + +# REST API Endpoints (keeping all existing endpoints) @app.post("/api/users") async def register_user( email: str = Form(...), @@ -1208,25 +1707,6 @@ async def get_notifications( } } -async def authenticate_user(token_id: Optional[int] = None, - token_key: Optional[str] = None, - user_id: Optional[int] = None): - """Generic authentication function that works with any parameter source""" - if not all([token_id, token_key, user_id]): - return None - - token = await db.get("auth_tokens", { - "id": token_id, - "token_key": token_key, - "user_id": user_id - }) - - if not token or token['expire_time'] <= int(datetime.now().timestamp()): - return None - - return user_id - - @app.delete("/api/users/me/notif-feed") async def clear_notifications( app: int = Form(3), @@ -1313,14 +1793,11 @@ async def get_upload(filename: str): return FileResponse(file_path) raise HTTPException(status_code=404, detail="File not found") -# Serve static files (for frontend) -from fastapi.staticfiles import StaticFiles -app.mount("/static", StaticFiles(directory="static"), name="static") +# Create static directory if it doesn't exist +Path("static").mkdir(exist_ok=True) -# Root endpoint serves the main HTML -@app.get("/") -async def root(): - return FileResponse("static/index.html") +# Serve static files (for frontend) +app.mount("/static", StaticFiles(directory="static"), name="static") if __name__ == "__main__": import uvicorn diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..060679e --- /dev/null +++ b/templates/base.html @@ -0,0 +1,794 @@ + + + + + + {% block title %}Rant Community{% endblock %} + + + + + {% include 'components/navigation.html' %} + + +
+ {% block content %}{% endblock %} +
+ + + {% if current_user %} + + {% endif %} + + + {% include 'components/modals.html' %} + + + + {% block scripts %}{% endblock %} + + diff --git a/templates/components/modals.html b/templates/components/modals.html new file mode 100644 index 0000000..e38b7fc --- /dev/null +++ b/templates/components/modals.html @@ -0,0 +1,133 @@ + + + diff --git a/templates/components/navigation.html b/templates/components/navigation.html new file mode 100644 index 0000000..c68713f --- /dev/null +++ b/templates/components/navigation.html @@ -0,0 +1,23 @@ + diff --git a/templates/components/rant_card.html b/templates/components/rant_card.html new file mode 100644 index 0000000..efe2488 --- /dev/null +++ b/templates/components/rant_card.html @@ -0,0 +1,35 @@ +{% macro render_rant_card(rant, clickable=True) %} +
+
+
+ {{ rant.user_username[0]|upper }} +
+ +
+
{{ escape_html(rant.text) }}
+ {% if rant.attached_image %} + Rant image + {% endif %} + {% if rant.tags %} +
+ {% for tag in rant.tags %} + {{ tag }} + {% endfor %} +
+ {% endif %} + +
+{% endmacro %} diff --git a/templates/feed.html b/templates/feed.html new file mode 100644 index 0000000..4dc2c82 --- /dev/null +++ b/templates/feed.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% from 'components/rant_card.html' import render_rant_card %} + +{% block content %} +
+ Recent + Top + Algorithm +
+ +
+ {% for rant in rants %} + {{ render_rant_card(rant) }} + {% endfor %} +
+{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..94f4f10 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Login

+
+ {% if error %} +
{{ error }}
+ {% endif %} +
+ + +
+
+ + +
+ +

+ Don't have an account? Sign up +

+
+
+
+{% endblock %} diff --git a/templates/notifications.html b/templates/notifications.html new file mode 100644 index 0000000..3aa7a92 --- /dev/null +++ b/templates/notifications.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block content %} +

Notifications

+{% if items %} + {% for notif in items %} +
+

{{ notif.username }} {% if notif.type == 'comment' %}commented on your rant{% else %}mentioned you{% endif %}

+

{{ format_time(notif.created_time) }}

+
+ {% endfor %} +{% else %} +

No notifications

+{% endif %} +{% endblock %} diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..6e4949d --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,74 @@ +{% extends 'base.html' %} +{% from 'components/rant_card.html' import render_rant_card %} + +{% block content %} +← Back to Feed +
+
+ {{ profile.username[0]|upper }} +
+

{{ profile.username }}

+ {% if profile.about %} +

{{ escape_html(profile.about) }}

+ {% endif %} + {% if profile.skills %} +

Skills: {{ escape_html(profile.skills) }}

+ {% endif %} + {% if profile.location %} +

Location: {{ escape_html(profile.location) }}

+ {% endif %} + {% if profile.github %} +

GitHub: {{ profile.github }}

+ {% endif %} + {% if profile.website %} +

Website: {{ profile.website }}

+ {% endif %} +
+
+
{{ profile.score }}
+
Score
+
+
+
{{ rants|length }}
+
Rants
+
+
+
{{ comments|length }}
+
Comments
+
+
+ {% if current_user and current_user.id == profile_user_id %} + + {% endif %} +
+ +
+ Rants + Comments + Favorites +
+ +
+ {% if active_tab == 'rants' %} + {% for rant in rants %} + {{ render_rant_card(rant) }} + {% endfor %} + {% elif active_tab == 'comments' %} + {% for comment in comments %} +
+
{{ escape_html(comment.body) }}
+ +
+ {% endfor %} + {% elif active_tab == 'favorites' %} + {% for rant in favorites %} + {{ render_rant_card(rant) }} + {% endfor %} + {% endif %} +
+{% endblock %} diff --git a/templates/rant_detail.html b/templates/rant_detail.html new file mode 100644 index 0000000..a6839cc --- /dev/null +++ b/templates/rant_detail.html @@ -0,0 +1,103 @@ +{% extends 'base.html' %} + +{% block content %} +← Back to Feed +
+
+
+ {{ rant.user_username[0]|upper }} +
+ + {% if current_user and current_user.id == rant.user_id %} + + + {% endif %} +
+
{{ escape_html(rant.text) }}
+ {% if rant.attached_image %} + Rant image + {% endif %} + {% if rant.tags %} +
+ {% for tag in rant.tags %} + {{ tag }} + {% endfor %} +
+ {% endif %} + +
+ +
+

Comments ({{ comments|length }})

+
+ {% for comment in comments %} +
+
+
+ {{ comment.user_username[0]|upper }} +
+ + {% if current_user and current_user.id == comment.user_id %} + + {% endif %} +
+
{{ escape_html(comment.body) }}
+ +
+ {% endfor %} +
+ {% if current_user %} +
+

Add a comment

+
+ +
+ +
+ {% else %} +

Login to comment

+ {% endif %} +
+ + +{% endblock %} diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..faece59 --- /dev/null +++ b/templates/search.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% from 'components/rant_card.html' import render_rant_card %} + +{% block content %} + + +
+ {% if search_term %} + {% if results %} + {% for rant in results %} + {{ render_rant_card(rant) }} + {% endfor %} + {% else %} +

No results found

+ {% endif %} + {% endif %} +
+{% endblock %}