Pre locust

This commit is contained in:
2026-05-11 00:41:41 +02:00
parent e340a8dc42
commit 120eb740aa
31 changed files with 598 additions and 473 deletions
+20
View File
@@ -1,5 +1,6 @@
import dataset
import logging
from datetime import datetime
from devplacepy.config import DATABASE_URL
logger = logging.getLogger(__name__)
@@ -35,8 +36,27 @@ def init_db():
if "follows" in tables:
db.query("CREATE INDEX IF NOT EXISTS idx_follows_follower ON follows (follower_uid)")
db.query("CREATE INDEX IF NOT EXISTS idx_follows_following ON follows (following_uid)")
if "daily_topics" in tables:
existing = db["daily_topics"].find_one()
if not existing:
db["daily_topics"].insert({
"uid": "default",
"title": "Anthropic details how it improved Claude's safety...",
"summary": "New techniques in AI safety research show promising results for alignment of large language models with human values.",
"updated_at": datetime.utcnow().isoformat(),
})
if "password_resets" in tables:
db.query("CREATE INDEX IF NOT EXISTS idx_password_resets_token ON password_resets (token)")
logger.info("Database initialized")
def get_table(name):
return db[name]
def get_daily_topic():
topics = db["daily_topics"]
topic = topics.find_one()
if topic:
return topic
return {"title": "Anthropic details how it improved Claude's safety...", "summary": "New techniques in AI safety research show promising results."}
+5 -3
View File
@@ -3,10 +3,10 @@ from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from devplacepy.config import STATIC_DIR
from devplacepy.database import init_db
from devplacepy.database import init_db, get_daily_topic
from devplacepy.templating import templates
from devplacepy.utils import get_current_user
from devplacepy.routers import auth, feed, posts, comments, projects, profile, messages, notifications, votes, avatar
from devplacepy.routers import auth, feed, posts, comments, projects, profile, messages, notifications, votes, avatar, follow
logging.basicConfig(
level=logging.INFO,
@@ -27,6 +27,7 @@ app.include_router(messages.router, prefix="/messages")
app.include_router(notifications.router, prefix="/notifications")
app.include_router(votes.router, prefix="/votes")
app.include_router(avatar.router, prefix="/avatar")
app.include_router(follow.router, prefix="/follow")
@app.on_event("startup")
@@ -40,4 +41,5 @@ async def landing(request: Request):
user = get_current_user(request)
if user:
return RedirectResponse(url="/feed", status_code=302)
return templates.TemplateResponse("landing.html", {"request": request})
daily_topic = get_daily_topic()
return templates.TemplateResponse("landing.html", {"request": request, "daily_topic": daily_topic})
+94 -1
View File
@@ -1,5 +1,6 @@
import secrets
import logging
from datetime import datetime
from datetime import datetime, timedelta
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse, HTMLResponse
from devplacepy.database import get_table
@@ -79,6 +80,14 @@ async def signup(request: Request):
"created_at": datetime.utcnow().isoformat(),
})
badges = get_table("badges")
badges.insert({
"uid": generate_uid(),
"user_uid": uid,
"badge_name": "Member",
"created_at": datetime.utcnow().isoformat(),
})
token = create_session(uid)
response = RedirectResponse(url="/feed", status_code=302)
response.set_cookie(key="session", value=token, max_age=86400 * 7, httponly=True)
@@ -117,6 +126,90 @@ async def login(request: Request):
return response
@router.get("/forgot-password", response_class=HTMLResponse)
async def forgot_password_page(request: Request):
return templates.TemplateResponse("forgot_password.html", {"request": request})
@router.post("/forgot-password")
async def forgot_password(request: Request):
form = await request.form()
email = form.get("email", "").strip().lower()
errors = []
if not email or "@" not in email:
errors.append("Valid email is required")
if errors:
return templates.TemplateResponse("forgot_password.html", {
"request": request, "errors": errors,
})
users = get_table("users")
user = users.find_one(email=email)
if user:
token = secrets.token_hex(32)
resets = get_table("password_resets")
resets.insert({
"uid": generate_uid(),
"user_uid": user["uid"],
"token": hash_password(token),
"expires_at": (datetime.utcnow() + timedelta(hours=1)).isoformat(),
"used": False,
"created_at": datetime.utcnow().isoformat(),
})
logger.info(f"Password reset requested for {email}: /auth/reset-password/{token}")
return templates.TemplateResponse("forgot_password.html", {
"request": request, "sent": True,
})
@router.get("/reset-password/{token}", response_class=HTMLResponse)
async def reset_password_page(request: Request, token: str):
return templates.TemplateResponse("reset_password.html", {
"request": request, "token": token,
})
@router.post("/reset-password/{token}")
async def reset_password(request: Request, token: str):
form = await request.form()
password = form.get("password", "")
confirm = form.get("confirm_password", "")
errors = []
if len(password) < 6:
errors.append("Password must be at least 6 characters")
if password != confirm:
errors.append("Passwords do not match")
if errors:
return templates.TemplateResponse("reset_password.html", {
"request": request, "token": token, "errors": errors,
})
resets = get_table("password_resets")
users = get_table("users")
all_resets = list(resets.find(used=False))
matched = None
for r in all_resets:
expires = datetime.fromisoformat(r["expires_at"])
if expires > datetime.utcnow() and verify_password(token, r["token"]):
matched = r
break
if not matched:
errors.append("Invalid or expired reset token")
return templates.TemplateResponse("reset_password.html", {
"request": request, "token": token, "errors": errors,
})
users.update({"uid": matched["user_uid"], "password_hash": hash_password(password)}, ["uid"])
resets.update({"id": matched["id"], "used": True}, ["id"])
logger.info(f"Password reset completed for user {matched['user_uid']}")
return RedirectResponse(url="/auth/login", status_code=302)
@router.get("/logout")
async def logout(request: Request):
token = request.cookies.get("session")
+10
View File
@@ -32,6 +32,16 @@ async def create_comment(request: Request):
"created_at": datetime.utcnow().isoformat(),
})
badges = get_table("badges")
existing = badges.find_one(user_uid=user["uid"], badge_name="First Comment")
if not existing:
badges.insert({
"uid": generate_uid(),
"user_uid": user["uid"],
"badge_name": "First Comment",
"created_at": datetime.utcnow().isoformat(),
})
posts = get_table("posts")
post = posts.find_one(uid=post_uid)
if post and post["user_uid"] != user["uid"]:
+7 -2
View File
@@ -1,7 +1,8 @@
import logging
from datetime import datetime, timedelta
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from devplacepy.database import get_table
from devplacepy.database import get_table, get_daily_topic
from devplacepy.templating import templates
from devplacepy.utils import get_current_user, require_user, time_ago
@@ -59,9 +60,12 @@ async def feed_page(request: Request, tab: str = "all", topic: str = None):
posts = get_feed_posts(user, tab, topic)
users_table = get_table("users")
total_members = len(list(users_table.all()))
posts_today = len(list(get_table("posts").find()))
posts_table = get_table("posts")
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
posts_today = len(list(posts_table.find(created_at={">=": today_start})))
total_projects = len(list(get_table("projects").all()))
top_authors = list(users_table.find(order_by=["-stars"], _limit=5))
daily_topic = get_daily_topic()
return templates.TemplateResponse("feed.html", {
"request": request,
@@ -73,4 +77,5 @@ async def feed_page(request: Request, tab: str = "all", topic: str = None):
"posts_today": posts_today,
"total_projects": total_projects,
"top_authors": top_authors,
"daily_topic": daily_topic,
})
+61
View File
@@ -0,0 +1,61 @@
import logging
from datetime import datetime
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse
from devplacepy.database import get_table
from devplacepy.utils import generate_uid, require_user
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/{username}")
async def follow_user(request: Request, username: str):
user = require_user(request)
users = get_table("users")
target = users.find_one(username=username)
if not target or target["uid"] == user["uid"]:
return RedirectResponse(url=f"/profile/{username}", status_code=302)
follows = get_table("follows")
existing = follows.find_one(follower_uid=user["uid"], following_uid=target["uid"])
if existing:
return RedirectResponse(url=f"/profile/{username}", status_code=302)
follows.insert({
"uid": generate_uid(),
"follower_uid": user["uid"],
"following_uid": target["uid"],
"created_at": datetime.utcnow().isoformat(),
})
notifications = get_table("notifications")
notifications.insert({
"uid": generate_uid(),
"user_uid": target["uid"],
"type": "follow",
"message": f"{user['username']} started following you",
"related_uid": user["uid"],
"read": False,
"created_at": datetime.utcnow().isoformat(),
})
logger.info(f"{user['username']} followed {username}")
return RedirectResponse(url=f"/profile/{username}", status_code=302)
@router.post("/unfollow/{username}")
async def unfollow_user(request: Request, username: str):
user = require_user(request)
users = get_table("users")
target = users.find_one(username=username)
if not target:
return RedirectResponse(url=f"/profile/{username}", status_code=302)
follows = get_table("follows")
existing = follows.find_one(follower_uid=user["uid"], following_uid=target["uid"])
if existing:
follows.delete(id=existing["id"])
logger.info(f"{user['username']} unfollowed {username}")
return RedirectResponse(url=f"/profile/{username}", status_code=302)
+32 -2
View File
@@ -1,9 +1,13 @@
import os
import logging
import aiofiles
from datetime import datetime
from fastapi import APIRouter, Request, HTTPException, status
from pathlib import Path
from fastapi import APIRouter, Request, HTTPException, status, UploadFile, Form
from fastapi.responses import RedirectResponse, HTMLResponse
from devplacepy.database import get_table
from devplacepy.templating import templates
from devplacepy.config import STATIC_DIR
from devplacepy.utils import generate_uid, get_current_user, require_user, time_ago
logger = logging.getLogger(__name__)
@@ -30,6 +34,22 @@ async def create_post(request: Request):
if errors:
return RedirectResponse(url="/feed", status_code=302)
image_filename = None
image_file = form.get("image")
if image_file and hasattr(image_file, "filename") and image_file.filename:
try:
upload_dir = STATIC_DIR / "uploads"
upload_dir.mkdir(parents=True, exist_ok=True)
ext = Path(image_file.filename).suffix or ".png"
image_filename = f"{generate_uid()}{ext}"
file_path = upload_dir / image_filename
content_bytes = await image_file.read()
async with aiofiles.open(str(file_path), "wb") as f:
await f.write(content_bytes)
logger.info(f"Image saved: {image_filename}")
except Exception as e:
logger.warning(f"Image upload failed: {e}")
posts = get_table("posts")
uid = generate_uid()
posts.insert({
@@ -39,11 +59,21 @@ async def create_post(request: Request):
"content": content,
"topic": topic,
"project_uid": project_uid or None,
"image": None,
"image": image_filename,
"stars": 0,
"created_at": datetime.utcnow().isoformat(),
})
badges = get_table("badges")
existing = badges.find_one(user_uid=user["uid"], badge_name="First Post")
if not existing:
badges.insert({
"uid": generate_uid(),
"user_uid": user["uid"],
"badge_name": "First Post",
"created_at": datetime.utcnow().isoformat(),
})
logger.info(f"Post {uid} created by {user['username']}")
return RedirectResponse(url=f"/posts/{uid}", status_code=302)
+19
View File
@@ -32,6 +32,22 @@ async def profile_page(request: Request, username: str, tab: str = "posts"):
badges = list(get_table("badges").find(user_uid=profile_user["uid"]))
projects = list(get_table("projects").find(user_uid=profile_user["uid"]))
posts_count = len(list(get_table("posts").find(user_uid=profile_user["uid"])))
activities = []
if tab == "activity":
posts_table = get_table("posts")
for p in posts_table.find(user_uid=profile_user["uid"], order_by=["-created_at"], _limit=10):
activities.append({"type": "post", "content": p.get("title") or p["content"][:80], "time_ago": time_ago(p["created_at"]), "uid": p["uid"]})
comments_table = get_table("comments")
for c in comments_table.find(user_uid=profile_user["uid"], order_by=["-created_at"], _limit=10):
activities.append({"type": "comment", "content": c["content"][:80], "time_ago": time_ago(c["created_at"]), "uid": c["post_uid"]})
activities.sort(key=lambda a: a["time_ago"], reverse=True)
is_following = False
if current_user:
follows = get_table("follows")
is_following = bool(follows.find_one(follower_uid=current_user["uid"], following_uid=profile_user["uid"]))
return templates.TemplateResponse("profile.html", {
"request": request,
@@ -41,6 +57,9 @@ async def profile_page(request: Request, username: str, tab: str = "posts"):
"badges": badges,
"projects": projects,
"current_tab": tab,
"posts_count": posts_count,
"is_following": is_following,
"activities": activities,
})
+6 -2
View File
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None):
def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None, project_type: str = None):
projects = get_table("projects")
users = get_table("users")
all_projects = list(projects.all())
@@ -18,6 +18,9 @@ def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = Non
if user_uid:
all_projects = [p for p in all_projects if p["user_uid"] == user_uid]
if project_type:
all_projects = [p for p in all_projects if p.get("project_type") == project_type]
if search:
search_lower = search.lower()
all_projects = [
@@ -48,9 +51,10 @@ async def projects_page(
tab: str = "recent",
search: str = "",
user_uid: str = None,
project_type: str = None,
):
user = get_current_user(request)
projects = get_projects_list(tab, search, user_uid)
projects = get_projects_list(tab, search, user_uid, project_type)
users = get_table("users")
total_members = len(list(users.all()))
+3
View File
@@ -41,6 +41,9 @@ async def vote(request: Request, target_type: str, target_uid: str):
if target_type == "post":
posts = get_table("posts")
posts.update({"uid": target_uid, "stars": up - down}, ["uid"])
elif target_type == "project":
projects = get_table("projects")
projects.update({"uid": target_uid, "stars": up - down}, ["uid"])
referer = request.headers.get("Referer", "/feed")
return RedirectResponse(url=referer, status_code=302)
+54
View File
@@ -175,6 +175,60 @@ img {
.avatar-lg { width: 80px; height: 80px; font-size: 2rem; }
.avatar-img { border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
}
.topnav-inner {
max-width: var(--max-content);
margin: 0 auto;
padding: 0 1rem;
width: 100%;
display: flex;
align-items: center;
gap: 2rem;
}
.topnav-logo {
font-size: 1.25rem;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.topnav-logo span { color: var(--accent); }
.topnav-links { display: flex; gap: 0.25rem; }
.topnav-link {
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.topnav-link:hover { color: var(--text-primary); background: var(--bg-card); }
.topnav-link.active { color: var(--accent); background: var(--accent-light); }
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; transition: color 0.2s; }
.topnav-icon:hover { color: var(--text-primary); }
.nav-badge {
position: absolute; top: 0; right: 0; min-width: 16px; height: 16px;
padding: 0 4px; border-radius: 8px; background: var(--accent); color: #fff;
font-size: 0.6875rem; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.topnav-user { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem; border-radius: var(--radius); transition: background 0.2s; }
.topnav-user:hover { background: var(--bg-card); }
.topnav-user-info { display: flex; flex-direction: column; line-height: 1.2; }
.topnav-user-name { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
.topnav-user-role { font-size: 0.6875rem; color: var(--text-muted); }
.card {
background: var(--bg-card);
border: 1px solid var(--border);
+2 -111
View File
@@ -1,115 +1,6 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/feed.css">
<style>
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
}
.topnav-inner {
max-width: var(--max-content);
margin: 0 auto;
padding: 0 1rem;
width: 100%;
display: flex;
align-items: center;
gap: 2rem;
}
.topnav-logo {
font-size: 1.25rem;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.topnav-logo span {
color: var(--accent);
}
.topnav-links {
display: flex;
gap: 0.25rem;
}
.topnav-link {
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.topnav-link:hover {
color: var(--text-primary);
background: var(--bg-card);
}
.topnav-link.active {
color: var(--accent);
background: var(--accent-light);
}
.topnav-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 1rem;
}
.topnav-icon {
position: relative;
padding: 0.375rem;
color: var(--text-secondary);
font-size: 1.25rem;
transition: color 0.2s;
}
.topnav-icon:hover {
color: var(--text-primary);
}
.nav-badge {
position: absolute;
top: 0;
right: 0;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: var(--accent);
color: #fff;
font-size: 0.6875rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.topnav-user {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem;
border-radius: var(--radius);
transition: background 0.2s;
}
.topnav-user:hover {
background: var(--bg-card);
}
.topnav-user-info {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.topnav-user-name {
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-primary);
}
.topnav-user-role {
font-size: 0.6875rem;
color: var(--text-muted);
}
</style>
{% endblock %}
{% block content %}
<div class="feed-layout">
@@ -204,8 +95,8 @@
<aside class="feed-right">
<div class="daily-topic-card">
<div class="daily-topic-label">Daily Topic</div>
<h4>Anthropic details how it improved Claude's safety...</h4>
<p>New techniques in AI safety research show promising results.</p>
<h4>{{ daily_topic.get('title', '') }}</h4>
<p>{{ daily_topic.get('summary', '') }}</p>
<div class="topic-links">
<a href="#">Read More</a>
<a href="#">Source</a>
+41
View File
@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/auth.css">
{% endblock %}
{% block content %}
<div class="auth-page">
<div class="auth-card">
<a href="/auth/login" class="auth-back">&larr; Back to Login</a>
<h2>Reset Password</h2>
<p class="subtitle">Enter your email and we will send you a reset link</p>
{% if sent %}
<div class="auth-success" style="background: rgba(76, 175, 80, 0.1); border: 1px solid var(--success); border-radius: var(--radius); padding: 0.75rem; font-size: 0.8125rem; color: var(--success); margin-bottom: 0.5rem;">
If an account with that email exists, a reset link has been sent. Check your email or server logs.
</div>
{% endif %}
{% if errors %}
<div class="auth-error">
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form class="auth-form" method="POST" action="/auth/forgot-password">
<div class="auth-field">
<label for="email">Email address</label>
<input type="email" id="email" name="email" required maxlength="255" placeholder="you@example.com">
</div>
<button type="submit" class="auth-submit">Send Reset Link</button>
</form>
<div class="auth-footer">
Remember your password? <a href="/auth/login">Sign in</a>
</div>
</div>
</div>
{% endblock %}
+2 -2
View File
@@ -40,8 +40,8 @@
<div class="landing-daily-topic">
<div class="landing-daily-topic-label">Daily Topic</div>
<h3>Anthropic details how it improved Claude's safety...</h3>
<p>New techniques in AI safety research show promising results for alignment of large language models with human values.</p>
<h3>{{ daily_topic.get('title', '') }}</h3>
<p>{{ daily_topic.get('summary', '') }}</p>
<div class="landing-daily-topic-links">
<a href="#">Read More</a>
<a href="#">Source</a>
-36
View File
@@ -2,42 +2,6 @@
{% block extra_head %}
<link rel="stylesheet" href="/static/css/messages.css">
<style>
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
}
.topnav-inner {
max-width: var(--max-content);
margin: 0 auto;
padding: 0 1rem;
width: 100%;
display: flex;
align-items: center;
gap: 2rem;
}
.topnav-logo { font-size: 1.25rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.02em; }
.topnav-logo span { color: var(--accent); }
.topnav-links { display: flex; gap: 0.25rem; }
.topnav-link { padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; }
.topnav-link:hover { color: var(--text-primary); background: var(--bg-card); }
.topnav-link.active { color: var(--accent); background: var(--accent-light); }
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; transition: color 0.2s; }
.topnav-icon:hover { color: var(--text-primary); }
.nav-badge { position: absolute; top: 0; right: 0; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: #fff; font-size: 0.6875rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.topnav-user { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem; border-radius: var(--radius); transition: background 0.2s; }
.topnav-user:hover { background: var(--bg-card); }
.topnav-user-info { display: flex; flex-direction: column; line-height: 1.2; }
.topnav-user-name { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
.topnav-user-role { font-size: 0.6875rem; color: var(--text-muted); }
.page { padding-top: 0; }
.page > .messages-layout { margin-top: calc(var(--nav-height) + 1rem); }
</style>
-38
View File
@@ -1,44 +1,6 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/notifications.css">
<style>
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
}
.topnav-inner {
max-width: var(--max-content);
margin: 0 auto;
padding: 0 1rem;
width: 100%;
display: flex;
align-items: center;
gap: 2rem;
}
.topnav-logo { font-size: 1.25rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.02em; }
.topnav-logo span { color: var(--accent); }
.topnav-links { display: flex; gap: 0.25rem; }
.topnav-link { padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; }
.topnav-link:hover { color: var(--text-primary); background: var(--bg-card); }
.topnav-link.active { color: var(--accent); background: var(--accent-light); }
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; transition: color 0.2s; }
.topnav-icon:hover { color: var(--text-primary); }
.nav-badge { position: absolute; top: 0; right: 0; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: #fff; font-size: 0.6875rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.topnav-user { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem; border-radius: var(--radius); transition: background 0.2s; }
.topnav-user:hover { background: var(--bg-card); }
.topnav-user-info { display: flex; flex-direction: column; line-height: 1.2; }
.topnav-user-name { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
.topnav-user-role { font-size: 0.6875rem; color: var(--text-muted); }
</style>
{% endblock %}
{% block content %}
<div class="notifications-page">
-54
View File
@@ -1,60 +1,6 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/post.css">
<style>
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
}
.topnav-inner {
max-width: var(--max-content);
margin: 0 auto;
padding: 0 1rem;
width: 100%;
display: flex;
align-items: center;
gap: 2rem;
}
.topnav-logo {
font-size: 1.25rem;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.topnav-logo span { color: var(--accent); }
.topnav-links { display: flex; gap: 0.25rem; }
.topnav-link {
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.topnav-link:hover { color: var(--text-primary); background: var(--bg-card); }
.topnav-link.active { color: var(--accent); background: var(--accent-light); }
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; transition: color 0.2s; }
.topnav-icon:hover { color: var(--text-primary); }
.nav-badge {
position: absolute; top: 0; right: 0; min-width: 16px; height: 16px; padding: 0 4px;
border-radius: 8px; background: var(--accent); color: #fff; font-size: 0.6875rem;
font-weight: 700; display: flex; align-items: center; justify-content: center;
}
.topnav-user { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem; border-radius: var(--radius); transition: background 0.2s; }
.topnav-user:hover { background: var(--bg-card); }
.topnav-user-info { display: flex; flex-direction: column; line-height: 1.2; }
.topnav-user-name { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
.topnav-user-role { font-size: 0.6875rem; color: var(--text-muted); }
</style>
{% endblock %}
{% block content %}
<div class="post-page">
+32 -39
View File
@@ -3,42 +3,6 @@
<link rel="stylesheet" href="/static/css/profile.css">
<link rel="stylesheet" href="/static/css/projects.css">
<style>
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
}
.topnav-inner {
max-width: var(--max-content);
margin: 0 auto;
padding: 0 1rem;
width: 100%;
display: flex;
align-items: center;
gap: 2rem;
}
.topnav-logo { font-size: 1.25rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.02em; }
.topnav-logo span { color: var(--accent); }
.topnav-links { display: flex; gap: 0.25rem; }
.topnav-link { padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; }
.topnav-link:hover { color: var(--text-primary); background: var(--bg-card); }
.topnav-link.active { color: var(--accent); background: var(--accent-light); }
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; transition: color 0.2s; }
.topnav-icon:hover { color: var(--text-primary); }
.nav-badge { position: absolute; top: 0; right: 0; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: #fff; font-size: 0.6875rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.topnav-user { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem; border-radius: var(--radius); transition: background 0.2s; }
.topnav-user:hover { background: var(--bg-card); }
.topnav-user-info { display: flex; flex-direction: column; line-height: 1.2; }
.topnav-user-name { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
.topnav-user-role { font-size: 0.6875rem; color: var(--text-muted); }
.hidden { display: none !important; }
</style>
{% endblock %}
@@ -56,7 +20,7 @@
<div class="profile-stats">
<div class="profile-stat">
<span class="profile-stat-value">{{ profile_user.get('posts_count', 0) }}</span>
<span class="profile-stat-value">{{ posts_count }}</span>
<span class="profile-stat-label">Posts</span>
</div>
<div class="profile-stat">
@@ -88,7 +52,21 @@
</div>
</div>
{% if user and user['uid'] == profile_user['uid'] %}
{% if user and user['uid'] != profile_user['uid'] %}
<div style="margin-top: 1rem;">
{% if is_following %}
<form method="POST" action="/follow/unfollow/{{ profile_user['username'] }}">
<button type="submit" class="btn btn-secondary btn-sm" style="width: 100%;">Unfollow</button>
</form>
{% else %}
<form method="POST" action="/follow/{{ profile_user['username'] }}">
<button type="submit" class="btn btn-primary btn-sm" style="width: 100%;">Follow</button>
</form>
{% endif %}
</div>
{% endif %}
{% if user and user['uid'] == profile_user['uid'] %}
<div class="profile-info">
<form method="POST" action="/profile/update">
<div class="profile-info-row">
@@ -242,7 +220,22 @@
<div class="empty-state">No projects yet.</div>
{% endfor %}
{% else %}
<div class="empty-state">Activity coming soon.</div>
{% for act in activities %}
<div class="post-card fade-in" style="padding: 0.75rem 1rem;">
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem;">
<span style="color: var(--text-muted);">
{% if act['type'] == 'post' %}&#x1F4DD;{% else %}&#x1F4AC;{% endif %}
</span>
<span style="color: var(--text-secondary);">
{% if act['type'] == 'post' %}Posted{% else %}Commented{% endif %}
</span>
<span style="color: var(--text-muted);">{{ act['time_ago'] }}</span>
</div>
<p style="font-size: 0.8125rem; color: var(--text-muted); margin-top: 0.25rem;">{{ act['content'] }}</p>
</div>
{% else %}
<div class="empty-state">No activity yet.</div>
{% endfor %}
{% endif %}
</div>
</div>
+4 -37
View File
@@ -2,42 +2,6 @@
{% block extra_head %}
<link rel="stylesheet" href="/static/css/projects.css">
<style>
.topnav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
}
.topnav-inner {
max-width: var(--max-content);
margin: 0 auto;
padding: 0 1rem;
width: 100%;
display: flex;
align-items: center;
gap: 2rem;
}
.topnav-logo { font-size: 1.25rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.02em; }
.topnav-logo span { color: var(--accent); }
.topnav-links { display: flex; gap: 0.25rem; }
.topnav-link { padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; }
.topnav-link:hover { color: var(--text-primary); background: var(--bg-card); }
.topnav-link.active { color: var(--accent); background: var(--accent-light); }
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; transition: color 0.2s; }
.topnav-icon:hover { color: var(--text-primary); }
.nav-badge { position: absolute; top: 0; right: 0; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: #fff; font-size: 0.6875rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.topnav-user { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem; border-radius: var(--radius); transition: background 0.2s; }
.topnav-user:hover { background: var(--bg-card); }
.topnav-user-info { display: flex; flex-direction: column; line-height: 1.2; }
.topnav-user-name { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
.topnav-user-role { font-size: 0.6875rem; color: var(--text-muted); }
.modal-overlay { display: none; }
.modal-overlay.visible { display: flex; }
</style>
@@ -95,7 +59,10 @@
<div class="project-card fade-in">
<div class="project-card-header">
<h3 class="project-card-title">{{ project['title'] }}</h3>
<button class="project-card-star">&#x2606;</button>
<form method="POST" action="/votes/project/{{ project['uid'] }}" style="display:inline;">
<input type="hidden" name="value" value="1">
<button type="submit" class="project-card-star">&#x2606;</button>
</form>
</div>
<div class="project-card-meta">
+41
View File
@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/auth.css">
{% endblock %}
{% block content %}
<div class="auth-page">
<div class="auth-card">
<a href="/auth/login" class="auth-back">&larr; Back to Login</a>
<h2>Set New Password</h2>
<p class="subtitle">Choose a new password for your account</p>
{% if errors %}
<div class="auth-error">
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form class="auth-form" method="POST" action="/auth/reset-password/{{ token }}">
<div class="auth-field">
<label for="password">New Password</label>
<div class="input-wrap">
<input type="password" id="password" name="password" required maxlength="128" placeholder="At least 6 characters">
<button type="button" class="auth-toggle-pw" aria-label="Toggle password visibility">&#x1F441;</button>
</div>
</div>
<div class="auth-field">
<label for="confirm_password">Confirm Password</label>
<div class="input-wrap">
<input type="password" id="confirm_password" name="confirm_password" required maxlength="128" placeholder="Repeat your password">
<button type="button" class="auth-toggle-pw" aria-label="Toggle password visibility">&#x1F441;</button>
</div>
</div>
<button type="submit" class="auth-submit">Reset Password</button>
</form>
</div>
</div>
{% endblock %}