import logging
from typing import Annotated
from datetime import datetime, timezone
from fastapi import APIRouter, Request, HTTPException, Form
from devplacepy.models import ProjectForm
from fastapi.responses import HTMLResponse, RedirectResponse
from devplacepy.database import get_table, get_vote_counts, load_comments, resolve_by_slug
from devplacepy.attachments import link_attachments, get_attachments, delete_target_attachments
from devplacepy.templating import templates
from devplacepy.utils import generate_uid, get_current_user, require_user, time_ago, make_combined_slug, create_mention_notifications
from devplacepy.seo import base_seo_context, site_url, website_schema, breadcrumb_schema, combine, software_application_schema
logger = logging.getLogger(__name__)
router = APIRouter()
def get_projects_list(tab: str = "recent", search: str = "", user_uid: str = None, project_type: str = None):
projects = get_table("projects")
all_projects = list(projects.all())
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 = [
p for p in all_projects
if search_lower in p.get("title", "").lower()
or search_lower in p.get("description", "").lower()
]
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"]
elif tab == "popular":
all_projects.sort(key=lambda p: int(p.get("stars", 0)), reverse=True)
elif tab == "new":
all_projects.sort(key=lambda p: p.get("created_at", ""), reverse=True)
else:
all_projects.sort(key=lambda p: p.get("created_at", ""), reverse=True)
return all_projects
@router.get("", response_class=HTMLResponse)
async def projects_page(
request: Request,
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, project_type)
users = get_table("users")
total_members = len(list(users.all()))
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="Projects",
description=f"Explore {len(projects)} developer projects on DevPlace. Games, software, mobile apps and more.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Projects", "url": "/projects"},
],
schemas=[website_schema(base)],
)
return templates.TemplateResponse(request, "projects.html", {
**seo_ctx,
"request": request,
"user": user,
"projects": projects,
"current_tab": tab,
"search": search,
"project_type": project_type,
"total_count": len(projects),
"total_members": total_members,
})
@router.get("/{project_slug}", response_class=HTMLResponse)
async def project_detail(request: Request, project_slug: str):
user = get_current_user(request)
projects = get_table("projects")
project = resolve_by_slug(projects, project_slug)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
from devplacepy.database import get_users_by_uids
users_map = get_users_by_uids([project["user_uid"]])
author = users_map.get(project["user_uid"])
is_owner = user and user["uid"] == project["user_uid"]
ups, downs = get_vote_counts([project["uid"]])
star_count = ups.get(project["uid"], 0) - downs.get(project["uid"], 0)
comments = load_comments("project", project["uid"])
project_attachments = get_attachments("project", project["uid"])
base = site_url(request)
seo_ctx = base_seo_context(
request,
title=project.get("title", "Project"),
description=project.get("description", "")[:160],
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Projects", "url": "/projects"},
{"name": project.get("title", "Project"), "url": f"/projects/{project['slug'] or project['uid']}"},
],
schemas=[website_schema(base), software_application_schema(project, base)],
)
return templates.TemplateResponse(request, "project_detail.html", {
**seo_ctx,
"request": request,
"user": user,
"project": project,
"author": author,
"is_owner": is_owner,
"star_count": star_count,
"platforms": project.get("platforms", "").split(",") if project.get("platforms") else [],
"comments": comments,
"attachments": project_attachments,
})
@router.post("/delete/{project_slug}")
async def delete_project(request: Request, project_slug: str):
user = require_user(request)
projects = get_table("projects")
project = resolve_by_slug(projects, project_slug)
if project and project["user_uid"] == user["uid"]:
delete_target_attachments("project", project["uid"])
projects.delete(id=project["id"])
logger.info(f"Project {project['uid']} deleted by {user['username']}")
return RedirectResponse(url="/projects", status_code=302)
@router.post("/create")
async def create_project(request: Request, data: Annotated[ProjectForm, Form()]):
user = require_user(request)
title = data.title.strip()
description = data.description.strip()
projects = get_table("projects")
uid = generate_uid()
project_slug = make_combined_slug(title, uid)
projects.insert({
"uid": uid,
"user_uid": user["uid"],
"title": title,
"slug": project_slug,
"description": description,
"release_date": data.release_date or None,
"demo_date": data.demo_date or None,
"project_type": data.project_type,
"platforms": data.platforms.strip(),
"status": data.status,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
})
if data.attachment_uids:
link_attachments(data.attachment_uids, "project", uid)
create_mention_notifications(description, user["uid"], f"/projects/{project_slug}")
logger.info(f"Project {uid} created by {user['username']}")
return RedirectResponse(url=f"/projects/{project_slug}", status_code=302)