forked from retoor/devplacepy
Super fast..
This commit is contained in:
+14
-31
@@ -2,39 +2,22 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AVATAR_STYLES = [
|
||||
"adventurer",
|
||||
"adventurer-neutral",
|
||||
"avataaars",
|
||||
"big-ears",
|
||||
"big-smile",
|
||||
"bottts",
|
||||
"croodles",
|
||||
"fun-emoji",
|
||||
"identicon",
|
||||
"initials",
|
||||
"lorelei",
|
||||
"micah",
|
||||
"miniavs",
|
||||
"notionists",
|
||||
"open-peeps",
|
||||
"personas",
|
||||
"pixel-art",
|
||||
"shapes",
|
||||
"thumbs",
|
||||
]
|
||||
|
||||
DICEBEAR_BASE = "https://api.dicebear.com/9.x"
|
||||
|
||||
|
||||
def avatar_url(style: str, seed: str, size: int = 128) -> str:
|
||||
style = style or "initials"
|
||||
return f"/avatar/{style}/{seed}?size={size}"
|
||||
|
||||
|
||||
def dicebear_proxy_url(style: str, seed: str, size: int = 128) -> str:
|
||||
return f"{DICEBEAR_BASE}/{style}/svg?seed={seed}&size={size}"
|
||||
|
||||
|
||||
def avatar_styles():
|
||||
return AVATAR_STYLES
|
||||
def generate_avatar_svg(seed: str) -> str:
|
||||
try:
|
||||
from multiavatar import multiavatar
|
||||
svg = multiavatar(seed)
|
||||
if svg and svg.strip().startswith("<svg"):
|
||||
return svg
|
||||
except Exception as e:
|
||||
logger.warning(f"Avatar generation failed for {seed}: {e}")
|
||||
initial = seed[:1].upper() if seed else "?"
|
||||
return (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
|
||||
f'<rect width="100" height="100" rx="50" fill="#ff6b35"/>'
|
||||
f'<text x="50" y="65" text-anchor="middle" fill="white" font-size="40" font-weight="700" font-family="sans-serif">{initial}</text></svg>'
|
||||
)
|
||||
|
||||
+77
-29
@@ -5,37 +5,55 @@ from devplacepy.config import DATABASE_URL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
db = dataset.connect(DATABASE_URL)
|
||||
db = dataset.connect(
|
||||
DATABASE_URL,
|
||||
engine_kwargs={
|
||||
"connect_args": {
|
||||
"timeout": 30,
|
||||
"check_same_thread": False,
|
||||
},
|
||||
},
|
||||
on_connect_statements=[
|
||||
"PRAGMA journal_mode=WAL",
|
||||
"PRAGMA synchronous=NORMAL",
|
||||
"PRAGMA busy_timeout=30000",
|
||||
"PRAGMA cache_size=-8000",
|
||||
"PRAGMA temp_store=MEMORY",
|
||||
"PRAGMA mmap_size=268435456",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _index(db, table, name, columns):
|
||||
try:
|
||||
if table in db.tables:
|
||||
cols = ", ".join(columns)
|
||||
db.query(f"CREATE INDEX IF NOT EXISTS {name} ON {table} ({cols})")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not create index {name} on {table}: {e}")
|
||||
|
||||
|
||||
def init_db():
|
||||
tables = db.tables
|
||||
if "users" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_users_username ON users (username)")
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_users_email ON users (email)")
|
||||
if "posts" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_posts_user_uid ON posts (user_uid)")
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts (created_at)")
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_posts_topic ON posts (topic)")
|
||||
if "comments" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_comments_post_uid ON comments (post_uid)")
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_comments_user_uid ON comments (user_uid)")
|
||||
if "votes" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_votes_target ON votes (target_uid, target_type)")
|
||||
if "messages" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages (sender_uid)")
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages (receiver_uid)")
|
||||
if "notifications" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_uid)")
|
||||
if "sessions" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions (session_token)")
|
||||
if "projects" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_projects_user ON projects (user_uid)")
|
||||
if "badges" in tables:
|
||||
db.query("CREATE INDEX IF NOT EXISTS idx_badges_user ON badges (user_uid)")
|
||||
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)")
|
||||
_index(db, "users", "idx_users_username", ["username"])
|
||||
_index(db, "users", "idx_users_email", ["email"])
|
||||
_index(db, "posts", "idx_posts_user_uid", ["user_uid"])
|
||||
_index(db, "posts", "idx_posts_created_at", ["created_at"])
|
||||
_index(db, "posts", "idx_posts_topic", ["topic"])
|
||||
_index(db, "comments", "idx_comments_post_uid", ["post_uid"])
|
||||
_index(db, "comments", "idx_comments_user_uid", ["user_uid"])
|
||||
_index(db, "votes", "idx_votes_target", ["target_uid", "target_type"])
|
||||
_index(db, "messages", "idx_messages_sender", ["sender_uid"])
|
||||
_index(db, "messages", "idx_messages_receiver", ["receiver_uid"])
|
||||
_index(db, "notifications", "idx_notifications_user", ["user_uid"])
|
||||
_index(db, "notifications", "idx_notifications_user_read", ["user_uid", "read"])
|
||||
_index(db, "sessions", "idx_sessions_token", ["session_token"])
|
||||
_index(db, "projects", "idx_projects_user", ["user_uid"])
|
||||
_index(db, "badges", "idx_badges_user", ["user_uid"])
|
||||
_index(db, "follows", "idx_follows_follower", ["follower_uid"])
|
||||
_index(db, "follows", "idx_follows_following", ["following_uid"])
|
||||
_index(db, "password_resets", "idx_password_resets_token", ["token"])
|
||||
|
||||
if "daily_topics" in tables:
|
||||
existing = db["daily_topics"].find_one()
|
||||
if not existing:
|
||||
@@ -45,8 +63,7 @@ def init_db():
|
||||
"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")
|
||||
|
||||
|
||||
@@ -54,6 +71,37 @@ def get_table(name):
|
||||
return db[name]
|
||||
|
||||
|
||||
def get_users_by_uids(uids):
|
||||
if not uids:
|
||||
return {}
|
||||
seen = set()
|
||||
unique = [u for u in uids if u not in seen and not seen.add(u)]
|
||||
return {u["uid"]: u for u in db["users"].find(db["users"].table.columns.uid.in_(unique))}
|
||||
|
||||
|
||||
def get_comment_counts_by_post_uids(post_uids):
|
||||
if not post_uids or "comments" not in db.tables:
|
||||
return {}
|
||||
placeholders = ",".join(f"'{u}'" for u in post_uids)
|
||||
rows = db.query(f"SELECT post_uid, COUNT(*) as c FROM comments WHERE post_uid IN ({placeholders}) GROUP BY post_uid")
|
||||
return {r["post_uid"]: r["c"] for r in rows}
|
||||
|
||||
|
||||
def get_vote_counts(target_uids):
|
||||
if not target_uids or "votes" not in db.tables:
|
||||
return {}, {}
|
||||
placeholders = ",".join(f"'{u}'" for u in target_uids)
|
||||
rows = db.query(f"SELECT target_uid, value, COUNT(*) as c FROM votes WHERE target_uid IN ({placeholders}) GROUP BY target_uid, value")
|
||||
ups = {}
|
||||
downs = {}
|
||||
for r in rows:
|
||||
if r["value"] == 1:
|
||||
ups[r["target_uid"]] = r["c"]
|
||||
else:
|
||||
downs[r["target_uid"]] = r["c"]
|
||||
return ups, downs
|
||||
|
||||
|
||||
def get_daily_topic():
|
||||
topics = db["daily_topics"]
|
||||
topic = topics.find_one()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
|
||||
|
||||
@@ -60,9 +60,6 @@ async def signup(request: Request):
|
||||
)
|
||||
|
||||
uid = generate_uid()
|
||||
avatar_style = form.get("avatar_style", "initials")
|
||||
if avatar_style not in ("adventurer", "adventurer-neutral", "avataaars", "big-ears", "big-smile", "bottts", "croodles", "fun-emoji", "identicon", "initials", "lorelei", "micah", "miniavs", "notionists", "open-peeps", "personas", "pixel-art", "shapes", "thumbs"):
|
||||
avatar_style = "initials"
|
||||
users.insert({
|
||||
"uid": uid,
|
||||
"username": username,
|
||||
@@ -72,7 +69,6 @@ async def signup(request: Request):
|
||||
"location": "",
|
||||
"git_link": "",
|
||||
"website": "",
|
||||
"avatar_style": avatar_style,
|
||||
"role": "Member",
|
||||
"level": 1,
|
||||
"xp": 0,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
import httpx
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import Response
|
||||
from devplacepy.avatar import dicebear_proxy_url
|
||||
from devplacepy.avatar import generate_avatar_svg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -10,36 +9,12 @@ router = APIRouter()
|
||||
_cache = {}
|
||||
|
||||
|
||||
def _initial_svg(seed: str) -> str:
|
||||
initial = seed[:1].upper() if seed else "?"
|
||||
return (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
|
||||
f'<rect width="100" height="100" rx="50" fill="#ff6b35"/>'
|
||||
f'<text x="50" y="65" text-anchor="middle" fill="white" font-size="40" font-weight="700" font-family="sans-serif">{initial}</text></svg>'
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{style}/{seed}")
|
||||
async def avatar_proxy(request: Request, style: str, seed: str, size: int = 128):
|
||||
cache_key = f"{style}:{seed}:{size}"
|
||||
cache_key = f"{seed}:{size}"
|
||||
if cache_key in _cache:
|
||||
return Response(content=_cache[cache_key], media_type="image/svg+xml")
|
||||
|
||||
if style == "initials" or not style:
|
||||
svg = _initial_svg(seed)
|
||||
_cache[cache_key] = svg
|
||||
return Response(content=svg, media_type="image/svg+xml")
|
||||
|
||||
url = dicebear_proxy_url(style, seed, size)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=3.0) as client:
|
||||
resp = await client.get(url, follow_redirects=True)
|
||||
content_type = resp.headers.get("content-type", "image/svg+xml")
|
||||
body = resp.content
|
||||
_cache[cache_key] = body
|
||||
return Response(content=body, media_type=content_type)
|
||||
except Exception as e:
|
||||
logger.warning(f"Avatar proxy failed for {style}/{seed}: {e}")
|
||||
svg = _initial_svg(seed)
|
||||
_cache[cache_key] = svg
|
||||
return Response(content=svg, media_type="image/svg+xml")
|
||||
svg = generate_avatar_svg(seed)
|
||||
_cache[cache_key] = svg
|
||||
return Response(content=svg, media_type="image/svg+xml")
|
||||
|
||||
@@ -51,7 +51,7 @@ async def create_comment(request: Request):
|
||||
"user_uid": post["user_uid"],
|
||||
"type": "comment",
|
||||
"message": f"{user['username']} commented on your post",
|
||||
"related_uid": post_uid,
|
||||
"related_uid": user["uid"],
|
||||
"read": False,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
})
|
||||
@@ -65,6 +65,12 @@ async def delete_comment(request: Request, comment_uid: str):
|
||||
user = require_user(request)
|
||||
comments = get_table("comments")
|
||||
comment = comments.find_one(uid=comment_uid)
|
||||
if comment and comment["user_uid"] == user["uid"]:
|
||||
comments.delete(id=comment["id"])
|
||||
return RedirectResponse(url=f"/posts/{comment['post_uid']}", status_code=302)
|
||||
post_uid = None
|
||||
if comment:
|
||||
post_uid = comment.get("post_uid")
|
||||
if comment["user_uid"] == user["uid"]:
|
||||
comments.delete(id=comment["id"])
|
||||
logger.info(f"Comment {comment_uid} deleted by {user['username']}")
|
||||
if post_uid:
|
||||
return RedirectResponse(url=f"/posts/{post_uid}", status_code=302)
|
||||
return RedirectResponse(url="/feed", status_code=302)
|
||||
|
||||
+39
-31
@@ -1,63 +1,70 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from devplacepy.database import get_table, get_daily_topic
|
||||
from fastapi.responses import HTMLResponse
|
||||
from devplacepy.database import get_table, get_daily_topic, get_users_by_uids, get_comment_counts_by_post_uids
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import get_current_user, require_user, time_ago
|
||||
from devplacepy.utils import require_user, time_ago
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
PAGE_SIZE = 25
|
||||
|
||||
def get_feed_posts(user, tab: str = "all", topic: str = None):
|
||||
|
||||
def get_feed_posts(user, tab: str = "all", topic: str = None, before: str = None):
|
||||
posts_table = get_table("posts")
|
||||
users_table = get_table("users")
|
||||
|
||||
query = "1=1"
|
||||
params = {}
|
||||
|
||||
filters = {}
|
||||
if topic:
|
||||
query += " AND topic = :topic"
|
||||
params["topic"] = topic
|
||||
filters["topic"] = topic
|
||||
|
||||
if tab == "following":
|
||||
follows = get_table("follows")
|
||||
following = [f["following_uid"] for f in follows.find(follower_uid=user["uid"])]
|
||||
if following:
|
||||
placeholders = ",".join(f":uid_{i}" for i in range(len(following)))
|
||||
query += f" AND user_uid IN ({placeholders})"
|
||||
for i, uid in enumerate(following):
|
||||
params[f"uid_{i}"] = uid
|
||||
if not following:
|
||||
return [], None
|
||||
posts = list(posts_table.find(posts_table.table.columns.user_uid.in_(following), order_by=["-created_at"], _limit=PAGE_SIZE))
|
||||
else:
|
||||
order = ["-created_at"]
|
||||
if tab == "trending":
|
||||
order = ["-stars", "-created_at"]
|
||||
if before:
|
||||
posts = list(posts_table.find(**filters, order_by=order, _limit=PAGE_SIZE + 1))
|
||||
posts = [p for p in posts if p["created_at"] < before][:PAGE_SIZE]
|
||||
else:
|
||||
return []
|
||||
posts = list(posts_table.find(**filters, order_by=order, _limit=PAGE_SIZE + 1))
|
||||
|
||||
order = "created_at DESC"
|
||||
if tab == "trending":
|
||||
order = "stars DESC, created_at DESC"
|
||||
has_more = len(posts) > PAGE_SIZE
|
||||
posts = posts[:PAGE_SIZE]
|
||||
|
||||
posts = list(posts_table.find(**params, _limit=50))
|
||||
posts.sort(key=lambda p: p.get("created_at", ""), reverse=True)
|
||||
if tab == "trending":
|
||||
posts.sort(key=lambda p: int(p.get("stars", 0)), reverse=True)
|
||||
next_cursor = None
|
||||
if has_more and posts:
|
||||
next_cursor = posts[-1]["created_at"]
|
||||
|
||||
if not posts:
|
||||
return [], next_cursor
|
||||
|
||||
uids = [p["user_uid"] for p in posts]
|
||||
post_uids = [p["uid"] for p in posts]
|
||||
authors = get_users_by_uids(uids)
|
||||
counts = get_comment_counts_by_post_uids(post_uids)
|
||||
|
||||
result = []
|
||||
for post in posts:
|
||||
author = users_table.find_one(uid=post["user_uid"])
|
||||
comment_count = len(list(get_table("comments").find(post_uid=post["uid"])))
|
||||
result.append({
|
||||
"post": post,
|
||||
"author": author,
|
||||
"author": authors.get(post["user_uid"]),
|
||||
"time_ago": time_ago(post["created_at"]),
|
||||
"comment_count": comment_count,
|
||||
"comment_count": counts.get(post["uid"], 0),
|
||||
})
|
||||
return result
|
||||
return result, next_cursor
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
async def feed_page(request: Request, tab: str = "all", topic: str = None):
|
||||
async def feed_page(request: Request, tab: str = "all", topic: str = None, before: str = None):
|
||||
user = require_user(request)
|
||||
posts = get_feed_posts(user, tab, topic)
|
||||
posts, next_cursor = get_feed_posts(user, tab, topic, before)
|
||||
users_table = get_table("users")
|
||||
total_members = len(list(users_table.all()))
|
||||
posts_table = get_table("posts")
|
||||
@@ -78,4 +85,5 @@ async def feed_page(request: Request, tab: str = "all", topic: str = None):
|
||||
"total_projects": total_projects,
|
||||
"top_authors": top_authors,
|
||||
"daily_topic": daily_topic,
|
||||
"next_cursor": next_cursor,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import generate_uid, get_current_user, require_user, time_ago
|
||||
from devplacepy.utils import generate_uid, require_user, time_ago
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -12,24 +12,30 @@ router = APIRouter()
|
||||
|
||||
def get_conversations(user_uid: str):
|
||||
messages_table = get_table("messages")
|
||||
users_table = get_table("users")
|
||||
all_messages = list(messages_table.find(
|
||||
"sender_uid = :uid OR receiver_uid = :uid",
|
||||
{"uid": user_uid},
|
||||
))
|
||||
all_messages = (
|
||||
list(messages_table.find(sender_uid=user_uid)) +
|
||||
list(messages_table.find(receiver_uid=user_uid))
|
||||
)
|
||||
|
||||
conversation_map = {}
|
||||
other_uids = set()
|
||||
for msg in all_messages:
|
||||
other_uid = msg["receiver_uid"] if msg["sender_uid"] == user_uid else msg["sender_uid"]
|
||||
other_uids.add(other_uid)
|
||||
if other_uid not in conversation_map or msg["created_at"] > conversation_map[other_uid]["last_message_at"]:
|
||||
other_user = users_table.find_one(uid=other_uid)
|
||||
conversation_map[other_uid] = {
|
||||
"other_user": other_user,
|
||||
"other_user": None,
|
||||
"last_message": msg["content"],
|
||||
"last_message_at": msg["created_at"],
|
||||
"unread": msg["receiver_uid"] == user_uid and not msg["read"],
|
||||
}
|
||||
|
||||
if other_uids:
|
||||
from devplacepy.database import get_users_by_uids
|
||||
users_map = get_users_by_uids(list(other_uids))
|
||||
for uid, conv in conversation_map.items():
|
||||
conv["other_user"] = users_map.get(uid)
|
||||
|
||||
conversations = sorted(
|
||||
conversation_map.values(),
|
||||
key=lambda c: c["last_message_at"],
|
||||
@@ -40,25 +46,26 @@ def get_conversations(user_uid: str):
|
||||
|
||||
def get_conversation_messages(user_uid: str, other_uid: str):
|
||||
messages_table = get_table("messages")
|
||||
msgs = list(messages_table.find(
|
||||
"(sender_uid = :me AND receiver_uid = :other) OR (sender_uid = :other AND receiver_uid = :me)",
|
||||
{"me": user_uid, "other": other_uid},
|
||||
))
|
||||
msgs = (
|
||||
list(messages_table.find(sender_uid=user_uid, receiver_uid=other_uid)) +
|
||||
list(messages_table.find(sender_uid=other_uid, receiver_uid=user_uid))
|
||||
)
|
||||
msgs.sort(key=lambda m: m["created_at"])
|
||||
|
||||
for msg in msgs:
|
||||
if msg["receiver_uid"] == user_uid and not msg["read"]:
|
||||
messages_table.update({"id": msg["id"], "read": True}, ["id"])
|
||||
|
||||
users_table = get_table("users")
|
||||
other_user = users_table.find_one(uid=other_uid)
|
||||
from devplacepy.database import get_users_by_uids
|
||||
user_ids = list({m["sender_uid"] for m in msgs} | {other_uid})
|
||||
users_map = get_users_by_uids(user_ids)
|
||||
other_user = users_map.get(other_uid)
|
||||
|
||||
result = []
|
||||
for m in msgs:
|
||||
sender = users_table.find_one(uid=m["sender_uid"])
|
||||
result.append({
|
||||
"message": m,
|
||||
"sender": sender,
|
||||
"sender": users_map.get(m["sender_uid"]),
|
||||
"is_mine": m["sender_uid"] == user_uid,
|
||||
"time_ago": time_ago(m["created_at"]),
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import get_current_user, require_user, time_ago
|
||||
from devplacepy.utils import require_user, time_ago
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -17,13 +17,17 @@ async def notifications_page(request: Request):
|
||||
notifications_table.find(user_uid=user["uid"], order_by=["-created_at"])
|
||||
)
|
||||
|
||||
users_table = get_table("users")
|
||||
notifications = []
|
||||
if raw_notifications:
|
||||
from devplacepy.database import get_users_by_uids
|
||||
actor_uids = [n.get("related_uid") for n in raw_notifications if n.get("related_uid")]
|
||||
actors = get_users_by_uids(actor_uids)
|
||||
else:
|
||||
actors = {}
|
||||
for n in raw_notifications:
|
||||
actor = users_table.find_one(uid=n.get("related_uid")) if n.get("related_uid") else None
|
||||
notifications.append({
|
||||
"notification": n,
|
||||
"actor": actor,
|
||||
"actor": actors.get(n.get("related_uid")),
|
||||
"time_ago": time_ago(n["created_at"]),
|
||||
})
|
||||
|
||||
|
||||
+29
-14
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
import logging
|
||||
import aiofiles
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Request, HTTPException, status, UploadFile, Form
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.templating import templates
|
||||
@@ -38,15 +37,23 @@ async def create_post(request: Request):
|
||||
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}")
|
||||
if len(content_bytes) > 5 * 1024 * 1024:
|
||||
logger.warning(f"Image too large: {image_file.filename}")
|
||||
else:
|
||||
import imghdr
|
||||
ext = Path(image_file.filename).suffix.lower()
|
||||
allowed = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"}
|
||||
if ext not in allowed:
|
||||
logger.warning(f"Unsupported image type: {ext}")
|
||||
else:
|
||||
upload_dir = STATIC_DIR / "uploads"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
image_filename = f"{generate_uid()}{ext}"
|
||||
file_path = upload_dir / image_filename
|
||||
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}")
|
||||
|
||||
@@ -91,16 +98,24 @@ async def view_post(request: Request, post_uid: str):
|
||||
|
||||
comments_table = get_table("comments")
|
||||
raw_comments = list(comments_table.find(post_uid=post_uid, order_by=["created_at"]))
|
||||
if raw_comments:
|
||||
uids = [c["user_uid"] for c in raw_comments]
|
||||
cids = [c["uid"] for c in raw_comments]
|
||||
from devplacepy.database import get_users_by_uids, get_vote_counts
|
||||
comment_users = get_users_by_uids(uids)
|
||||
ups, downs = get_vote_counts(cids)
|
||||
else:
|
||||
comment_users, ups, downs = {}, {}, {}
|
||||
|
||||
comment_map = {}
|
||||
for c in raw_comments:
|
||||
commenter = users_table.find_one(uid=c["user_uid"])
|
||||
comment_map[c["uid"]] = {
|
||||
"comment": c,
|
||||
"author": commenter,
|
||||
"author": comment_users.get(c["user_uid"]),
|
||||
"time_ago": time_ago(c["created_at"]),
|
||||
"votes": {
|
||||
"up": len(list(get_table("votes").find(target_uid=c["uid"], value=1))),
|
||||
"down": len(list(get_table("votes").find(target_uid=c["uid"], value=-1))),
|
||||
"up": ups.get(c["uid"], 0),
|
||||
"down": downs.get(c["uid"], 0),
|
||||
},
|
||||
"children": [],
|
||||
}
|
||||
|
||||
@@ -20,29 +20,32 @@ async def profile_page(request: Request, username: str, tab: str = "posts"):
|
||||
posts = []
|
||||
if tab == "posts":
|
||||
posts_table = get_table("posts")
|
||||
raw_posts = list(posts_table.find(user_uid=profile_user["uid"]))
|
||||
raw_posts = list(posts_table.find(user_uid=profile_user["uid"], order_by=["-created_at"]))
|
||||
if raw_posts:
|
||||
from devplacepy.database import get_comment_counts_by_post_uids
|
||||
counts = get_comment_counts_by_post_uids([p["uid"] for p in raw_posts])
|
||||
else:
|
||||
counts = {}
|
||||
for p in raw_posts:
|
||||
comments_count = len(list(get_table("comments").find(post_uid=p["uid"])))
|
||||
posts.append({
|
||||
"post": p,
|
||||
"time_ago": time_ago(p["created_at"]),
|
||||
"comment_count": comments_count,
|
||||
"comment_count": counts.get(p["uid"], 0),
|
||||
})
|
||||
posts.sort(key=lambda x: x["post"]["created_at"], reverse=True)
|
||||
|
||||
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"])))
|
||||
posts_count = len(posts) or 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"]})
|
||||
activities.append({"type": "post", "content": p.get("title") or p["content"][:80], "time_ago": time_ago(p["created_at"]), "created_at": 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)
|
||||
activities.append({"type": "comment", "content": c["content"][:80], "time_ago": time_ago(c["created_at"]), "created_at": c["created_at"], "uid": c["post_uid"]})
|
||||
activities.sort(key=lambda a: a["created_at"], reverse=True)
|
||||
|
||||
is_following = False
|
||||
if current_user:
|
||||
@@ -71,21 +74,14 @@ async def update_profile(request: Request):
|
||||
location = form.get("location", "").strip()
|
||||
git_link = form.get("git_link", "").strip()
|
||||
website = form.get("website", "").strip()
|
||||
avatar_style = form.get("avatar_style", "").strip()
|
||||
|
||||
users = get_table("users")
|
||||
update_data = {
|
||||
users.update({
|
||||
"uid": user["uid"],
|
||||
"bio": bio,
|
||||
"location": location,
|
||||
"git_link": git_link,
|
||||
"website": website,
|
||||
}
|
||||
valid_styles = ("adventurer", "adventurer-neutral", "avataaars", "big-ears", "big-smile", "bottts", "croodles", "fun-emoji", "identicon", "initials", "lorelei", "micah", "miniavs", "notionists", "open-peeps", "personas", "pixel-art", "shapes", "thumbs")
|
||||
if avatar_style in valid_styles:
|
||||
update_data["avatar_style"] = avatar_style
|
||||
|
||||
users.update(update_data, ["uid"])
|
||||
}, ["uid"])
|
||||
|
||||
logger.info(f"Profile updated for {user['username']}")
|
||||
return RedirectResponse(url=f"/profile/{user['username']}", status_code=302)
|
||||
|
||||
@@ -12,7 +12,6 @@ router = APIRouter()
|
||||
|
||||
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())
|
||||
|
||||
if user_uid:
|
||||
@@ -29,9 +28,13 @@ def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = Non
|
||||
or search_lower in p.get("description", "").lower()
|
||||
]
|
||||
|
||||
for p in all_projects:
|
||||
author = users.find_one(uid=p["user_uid"])
|
||||
p["author_name"] = author["username"] if author else "Unknown"
|
||||
if all_projects:
|
||||
from devplacepy.database import get_users_by_uids
|
||||
uids = [p["user_uid"] for p in all_projects]
|
||||
users_map = get_users_by_uids(uids)
|
||||
for p in all_projects:
|
||||
author = users_map.get(p["user_uid"])
|
||||
p["author_name"] = author["username"] if author else "Unknown"
|
||||
|
||||
if tab == "released":
|
||||
all_projects = [p for p in all_projects if p.get("status") == "Released"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from devplacepy.database import get_table
|
||||
@@ -32,7 +33,7 @@ async def vote(request: Request, target_type: str, target_uid: str):
|
||||
"target_uid": target_uid,
|
||||
"target_type": target_type,
|
||||
"value": value,
|
||||
"created_at": __import__("datetime").datetime.utcnow().isoformat(),
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
})
|
||||
|
||||
up = len(list(votes.find(target_uid=target_uid, value=1)))
|
||||
|
||||
@@ -9,8 +9,7 @@ class Application {
|
||||
this.initNotificationDismiss();
|
||||
this.initProfileEdit();
|
||||
this.initProjectForm();
|
||||
this.loadCSS("/static/css/variables.css");
|
||||
this.loadCSS("/static/css/base.css");
|
||||
this.initFormDisable();
|
||||
}
|
||||
|
||||
loadCSS(href) {
|
||||
@@ -83,6 +82,25 @@ class Application {
|
||||
titleCount.textContent = `${title.value.length}/500`;
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener("submit", () => {
|
||||
const btn = form.querySelector("button[type='submit']");
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Posting...";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initFormDisable() {
|
||||
document.querySelectorAll("form").forEach((form) => {
|
||||
form.addEventListener("submit", () => {
|
||||
const btn = form.querySelector("button[type='submit']");
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initCommentForms() {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/profile/{{ user['username'] }}" class="topnav-user">
|
||||
<img src="{{ avatar_url(user.get('avatar_style', 'initials'), user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}">
|
||||
<div class="topnav-user-info">
|
||||
<span class="topnav-user-name">{{ user['username'] }}</span>
|
||||
<span class="topnav-user-role">{{ user.get('role', 'Member') }}</span>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{% for item in posts %}
|
||||
<article class="post-card fade-in">
|
||||
<div class="post-header">
|
||||
<img src="{{ avatar_url(item.author.get('avatar_style', 'initials') if item.author else 'initials', item.author['username'] if item.author else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.author['username'] if item.author else '?' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.author['username'] if item.author else '?' }}">
|
||||
<div class="post-author-wrap">
|
||||
<span class="post-author">{{ item.author['username'] if item.author else 'Unknown' }}</span>
|
||||
{% if item.author and item.author.get('role') %}
|
||||
@@ -90,6 +90,12 @@
|
||||
<div class="empty-state">No posts yet. Be the first!</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if next_cursor %}
|
||||
<div style="text-align: center; margin-top: 1rem;">
|
||||
<a href="/feed?before={{ next_cursor }}{% if current_tab %}&tab={{ current_tab }}{% endif %}{% if current_topic %}&topic={{ current_topic }}{% endif %}" class="btn btn-secondary btn-sm">Load More</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<aside class="feed-right">
|
||||
@@ -122,7 +128,7 @@
|
||||
{% for author in top_authors %}
|
||||
<div class="stat-row">
|
||||
<span class="label">
|
||||
<img src="{{ avatar_url(author.get('avatar_style', 'initials'), author['username'], 20) }}" class="avatar-img" style="width: 20px; height: 20px; vertical-align: middle; margin-right: 0.375rem;" alt="{{ author['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', author['username'], 20) }}" class="avatar-img" style="width: 20px; height: 20px; vertical-align: middle; margin-right: 0.375rem;" alt="{{ author['username'] }}">
|
||||
{{ author['username'] }}
|
||||
</span>
|
||||
<span class="value">{{ author.get('stars', 0) }}</span>
|
||||
@@ -175,11 +181,9 @@
|
||||
</div>
|
||||
|
||||
<div class="auth-field" style="margin-bottom: 1rem;">
|
||||
<label>Add an image (optional)</label>
|
||||
<div style="border: 1px dashed var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; color: var(--text-muted); font-size: 0.8125rem;">
|
||||
Drop image here or click to browse
|
||||
</div>
|
||||
</div>
|
||||
<label for="post-image">Add an image (optional)</label>
|
||||
<input type="file" id="post-image" name="image" accept="image/*" style="font-size: 0.8125rem; color: var(--text-muted); padding: 0.5rem 0;">
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
|
||||
<button type="button" class="modal-close btn btn-secondary">Cancel</button>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="messages-conversations">
|
||||
{% for conv in conversations %}
|
||||
<a href="/messages?with_uid={{ conv.other_user['uid'] }}" class="conversation-item {% if current_conversation == conv.other_user['uid'] %}active{% endif %}">
|
||||
<img src="{{ avatar_url(conv.other_user.get('avatar_style', 'initials'), conv.other_user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ conv.other_user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', conv.other_user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ conv.other_user['username'] }}">
|
||||
<div class="conversation-info">
|
||||
<div class="conversation-name">{{ conv.other_user['username'] }}</div>
|
||||
<div class="conversation-preview">{{ conv.last_message[:60] }}</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="messages-main">
|
||||
{% if other_user %}
|
||||
<div class="messages-main-header">
|
||||
<img src="{{ avatar_url(other_user.get('avatar_style', 'initials'), other_user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ other_user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', other_user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ other_user['username'] }}">
|
||||
<h3>{{ other_user['username'] }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="notifications-list">
|
||||
{% for item in notifications %}
|
||||
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}">
|
||||
<img src="{{ avatar_url(item.actor.get('avatar_style', 'initials') if item.actor else 'initials', item.actor['username'] if item.actor else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.actor['username'] if item.actor else '?' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.actor['username'] if item.actor else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.actor['username'] if item.actor else '?' }}">
|
||||
<div class="notification-body">
|
||||
<div class="notification-text">{{ item.notification['message'] }}</div>
|
||||
<div class="notification-time">{{ item.time_ago }}</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<article class="post-detail">
|
||||
<div class="post-detail-header">
|
||||
<img src="{{ avatar_url(author.get('avatar_style', 'initials') if author else 'initials', author['username'] if author else '?', 40) }}" class="avatar-img" style="width: 40px; height: 40px; border-radius: 50%;" alt="{{ author['username'] if author else '?' }}">
|
||||
<img src="{{ avatar_url('multiavatar', author['username'] if author else '?', 40) }}" class="avatar-img" style="width: 40px; height: 40px; border-radius: 50%;" alt="{{ author['username'] if author else '?' }}">
|
||||
<div>
|
||||
<div class="post-detail-author">{{ author['username'] if author else 'Unknown' }}
|
||||
{% if author and author.get('role') %}
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<div class="comment-body">
|
||||
<div class="comment-header">
|
||||
<img src="{{ avatar_url(item.author.get('avatar_style', 'initials') if item.author else 'initials', item.author['username'] if item.author else '?', 24) }}" class="avatar-img" style="width: 24px; height: 24px; border-radius: 50%;" alt="{{ item.author['username'] if item.author else '?' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 24) }}" class="avatar-img" style="width: 24px; height: 24px; border-radius: 50%;" alt="{{ item.author['username'] if item.author else '?' }}">
|
||||
<span class="comment-author">{{ item.author['username'] if item.author else 'Unknown' }}</span>
|
||||
<span class="comment-time">{{ item.time_ago }}</span>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@
|
||||
{% if user %}
|
||||
<form class="comment-form" method="POST" action="/comments/create">
|
||||
<input type="hidden" name="post_uid" value="{{ post['uid'] }}">
|
||||
<img src="{{ avatar_url(user.get('avatar_style', 'initials'), user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ user['username'] }}">
|
||||
<textarea name="content" placeholder="Your opinion goes here..." required maxlength="1000"></textarea>
|
||||
<div class="comment-form-actions">
|
||||
<button type="button" title="Add emoji">😀</button>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<div class="profile-layout">
|
||||
<aside class="profile-sidebar">
|
||||
<div class="profile-card">
|
||||
<div class="profile-avatar-wrap" style="position: relative;">
|
||||
<img src="{{ avatar_url(profile_user.get('avatar_style', 'initials'), profile_user['username'], 80) }}" class="avatar-img avatar-lg" alt="{{ profile_user['username'] }}" id="profile-avatar-preview">
|
||||
<div class="profile-avatar-wrap">
|
||||
<img src="{{ avatar_url('multiavatar', profile_user['username'], 80) }}" class="avatar-img avatar-lg" alt="{{ profile_user['username'] }}" id="profile-avatar-preview">
|
||||
</div>
|
||||
<div class="profile-name">{{ profile_user['username'] }}</div>
|
||||
{% if profile_user.get('role') %}
|
||||
@@ -113,23 +113,6 @@
|
||||
<input type="url" name="website" maxlength="500" value="{{ profile_user.get('website', '') }}" style="font-size: 0.8125rem;">
|
||||
</div>
|
||||
|
||||
<div class="profile-info-row">
|
||||
<span class="profile-info-label">Avatar Style</span>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<img src="{{ avatar_url(profile_user.get('avatar_style', 'initials'), profile_user['username'], 32) }}" class="avatar-img avatar-sm" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.375rem; margin: 0.5rem 0;">
|
||||
{% for style in avatar_styles() %}
|
||||
<label style="display: flex; flex-direction: column; align-items: center; gap: 0.125rem; padding: 0.25rem; border-radius: var(--radius); border: 2px solid var(--border); cursor: pointer; transition: border-color 0.2s; {% if profile_user.get('avatar_style', 'initials') == style %}border-color: var(--accent);{% endif %}">
|
||||
<input type="radio" name="avatar_style" value="{{ style }}" {% if profile_user.get('avatar_style', 'initials') == style %}checked{% endif %} style="display: none;" onchange="this.closest('label').parentElement.querySelectorAll('label').forEach(l => l.style.borderColor = 'var(--border)'); this.closest('label').style.borderColor = 'var(--accent)'; document.getElementById('profile-avatar-preview').src = '{{ avatar_url(style, profile_user['username'], 80) }}';">
|
||||
<img src="{{ avatar_url(style, profile_user['username'], 36) }}" class="avatar-img" style="width: 28px; height: 28px; border-radius: 50%;" alt="{{ style }}">
|
||||
<span style="font-size: 0.5rem; color: var(--text-muted); text-align: center; line-height: 1.1;">{{ style[:8] }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small style="color: var(--text-muted); font-size: 0.75rem; display: block; margin-bottom: 0.5rem;">Avatars by DiceBear</small>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="width: 100%; margin-top: 0.5rem;">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -170,7 +153,7 @@
|
||||
{% for item in posts %}
|
||||
<article class="post-card fade-in">
|
||||
<div class="post-header">
|
||||
<img src="{{ avatar_url(profile_user.get('avatar_style', 'initials'), profile_user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ profile_user['username'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', profile_user['username'], 32) }}" class="avatar-img avatar-sm" alt="{{ profile_user['username'] }}">
|
||||
<div class="post-author-wrap">
|
||||
<span class="post-author">{{ profile_user['username'] }}</span>
|
||||
{% if profile_user.get('role') %}
|
||||
|
||||
@@ -47,26 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label>Avatar Style</label>
|
||||
<div class="avatar-picker" style="display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.5rem; margin-top: 0.25rem;">
|
||||
{% for style in avatar_styles() %}
|
||||
<label class="avatar-option" style="display: flex; flex-direction: column; align-items: center; gap: 0.25rem; padding: 0.375rem; border-radius: var(--radius); border: 2px solid var(--border); cursor: pointer; transition: border-color 0.2s;">
|
||||
<input type="radio" name="avatar_style" value="{{ style }}" {% if loop.first %}checked{% endif %} style="display: none;">
|
||||
<img src="{{ avatar_url(style, 'demo', 48) }}" class="avatar-img" style="width: 36px; height: 36px; border-radius: 50%;" alt="{{ style }}">
|
||||
<span style="font-size: 0.5625rem; color: var(--text-muted); text-align: center; line-height: 1.2;">{{ style.replace('-', ' ')|capitalize }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<small style="color: var(--text-muted); font-size: 0.75rem; margin-top: 0.375rem; display: block;">Avatars by DiceBear — choose your style</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-submit">Create account</button>
|
||||
|
||||
<style>
|
||||
.avatar-option:hover { border-color: var(--accent); }
|
||||
.avatar-option input[type="radio"]:checked + img { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 50%; }
|
||||
</style>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from devplacepy.config import TEMPLATES_DIR
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.avatar import avatar_url, avatar_styles
|
||||
from devplacepy.avatar import avatar_url
|
||||
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
|
||||
_unread_cache = {}
|
||||
|
||||
def jinja_unread_count(user_uid: str) -> int:
|
||||
if user_uid in _unread_cache:
|
||||
return _unread_cache[user_uid]
|
||||
notifs = get_table("notifications")
|
||||
return len(list(notifs.find(user_uid=user_uid, read=False)))
|
||||
count = len(list(notifs.find(user_uid=user_uid, read=False)))
|
||||
_unread_cache[user_uid] = count
|
||||
return count
|
||||
|
||||
|
||||
def jinja_user_projects(user_uid: str) -> list:
|
||||
@@ -19,4 +25,3 @@ def jinja_user_projects(user_uid: str) -> list:
|
||||
templates.env.globals["get_unread_count"] = jinja_unread_count
|
||||
templates.env.globals["get_user_projects"] = jinja_user_projects
|
||||
templates.env.globals["avatar_url"] = avatar_url
|
||||
templates.env.globals["avatar_styles"] = avatar_styles
|
||||
|
||||
@@ -30,10 +30,18 @@ def create_session(user_uid: str) -> str:
|
||||
return token
|
||||
|
||||
|
||||
_user_cache = {}
|
||||
|
||||
|
||||
def get_current_user(request: Request):
|
||||
token = request.cookies.get("session")
|
||||
if not token:
|
||||
return None
|
||||
|
||||
cached = _user_cache.get(token)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
sessions = get_table("sessions")
|
||||
session = sessions.find_one(session_token=token)
|
||||
if not session:
|
||||
@@ -44,6 +52,8 @@ def get_current_user(request: Request):
|
||||
return None
|
||||
users = get_table("users")
|
||||
user = users.find_one(uid=session["user_uid"])
|
||||
if user:
|
||||
_user_cache[token] = user
|
||||
return user
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user