forked from retoor/devplacepy
feat: scaffold initial DevPlace project with FastAPI, SQLite auth, and SSR routing
Add complete project skeleton including .gitignore, Makefile, AGENTS.md, config, database init with indexes, main app with router mounting for auth/feed/posts/comments/projects/profile/messages/notifications/votes, Pydantic models for signup/login/post/comment/project/message/profile, and auth router with signup/login page rendering and form handling.
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
{% extends "base.html" %}
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="projects-layout">
|
||||
<aside class="projects-sidebar">
|
||||
<h3>Filter</h3>
|
||||
<div class="projects-search">
|
||||
<form method="GET" action="/projects">
|
||||
<input type="text" name="search" placeholder="Search projects..." value="{{ search }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h3 style="margin-bottom: 0.5rem;">Type</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
||||
<a href="/projects?tab={{ current_tab }}" class="feed-category" style="display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; color: var(--text-secondary); transition: all 0.2s;">All</a>
|
||||
<a href="/projects?tab={{ current_tab }}&type=game" class="feed-category" style="display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; color: var(--text-secondary); transition: all 0.2s;">Game</a>
|
||||
<a href="/projects?tab={{ current_tab }}&type=software" class="feed-category" style="display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; color: var(--text-secondary); transition: all 0.2s;">Software</a>
|
||||
<a href="/projects?tab={{ current_tab }}&type=mobile_app" class="feed-category" style="display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; color: var(--text-secondary); transition: all 0.2s;">Mobile App</a>
|
||||
<a href="/projects?tab={{ current_tab }}&type=website" class="feed-category" style="display: flex; align-items: center; gap: 0.625rem; padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.875rem; color: var(--text-secondary); transition: all 0.2s;">Website</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h3 style="margin-bottom: 0.5rem;">My Projects</h3>
|
||||
<a href="/projects?user_uid={{ user['uid'] }}" class="btn btn-secondary btn-sm" style="width: 100%; justify-content: center;">View My Projects</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
|
||||
<div class="projects-main">
|
||||
<div class="projects-header">
|
||||
<div>
|
||||
<h2>Projects</h2>
|
||||
<span class="subtitle">Discover amazing projects from the DevPlace community</span>
|
||||
</div>
|
||||
<div class="projects-count">Showing {{ total_count }} of {{ total_count }} projects</div>
|
||||
</div>
|
||||
|
||||
<div class="projects-tabs">
|
||||
<a href="/projects?tab=recent" class="projects-tab {% if current_tab == 'recent' or not current_tab %}active{% endif %}">Recently Released</a>
|
||||
<a href="/projects?tab=popular" class="projects-tab {% if current_tab == 'popular' %}active{% endif %}">Most Popular</a>
|
||||
<a href="/projects?tab=new" class="projects-tab {% if current_tab == 'new' %}active{% endif %}">New This Week</a>
|
||||
{% if user %}
|
||||
<a href="/projects?user_uid={{ user['uid'] }}" class="projects-tab {% if request.query_params.get('user_uid') %}active{% endif %}">My Projects</a>
|
||||
{% endif %}
|
||||
<button class="btn-ghost btn-icon" style="margin-left: auto;">⚙</button>
|
||||
</div>
|
||||
|
||||
<div class="projects-grid">
|
||||
{% for project in projects %}
|
||||
<div class="project-card fade-in">
|
||||
<div class="project-card-header">
|
||||
<h3 class="project-card-title">{{ project['title'] }}</h3>
|
||||
<button class="project-card-star">☆</button>
|
||||
</div>
|
||||
|
||||
<div class="project-card-meta">
|
||||
<span>By {{ project.get('author_name', 'Unknown') }}</span>
|
||||
{% if project.get('release_date') %}
|
||||
<span>📅 {{ project['release_date'] }}</span>
|
||||
{% endif %}
|
||||
<span class="project-status {% if project.get('status') == 'Released' %}released{% else %}dev{% endif %}">
|
||||
● {{ project.get('status', 'In Development') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="project-card-desc">{{ project.get('description', '')[:200] }}</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="badge" style="background: var(--border); color: var(--text-muted); text-transform: capitalize;">{{ project.get('project_type', 'software').replace('_', ' ') }}</span>
|
||||
{% if project.get('platforms') %}
|
||||
<div class="project-card-platforms">
|
||||
{% for p in project['platforms'].split(',') %}
|
||||
<span class="platform-tag">{{ p.strip() }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card" style="text-align: center; padding: 3rem 1rem; grid-column: 1 / -1;">
|
||||
<p style="color: var(--text-muted); font-size: 0.9375rem;">No projects found. Create one!</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="feed-fab" id="create-project-btn" title="Add Project" style="position: fixed; bottom: 2rem; right: 2rem; width: 56px; height: 56px; border-radius: 50%; background: var(--accent); color: #fff; font-size: 1.5rem; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-lg); border: none; cursor: pointer; z-index: 100;">+</button>
|
||||
|
||||
<div class="modal-overlay" id="create-project-modal" style="position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 2000; align-items: center; justify-content: center;">
|
||||
<div class="card" style="width: 100%; max-width: 560px; margin: 1rem; max-height: 90vh; overflow-y: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="font-size: 1.125rem; font-weight: 700;">Create Project</h3>
|
||||
<button type="button" class="modal-close btn-ghost btn-icon" style="font-size: 1.25rem;">×</button>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/projects/create">
|
||||
<div class="auth-field" style="margin-bottom: 0.75rem;">
|
||||
<label for="title">Title</label>
|
||||
<input type="text" id="title" name="title" required maxlength="200" placeholder="Project name">
|
||||
</div>
|
||||
|
||||
<div class="auth-field" style="margin-bottom: 0.75rem;">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" required maxlength="5000" placeholder="Describe your project" style="min-height: 100px;"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 0.75rem;">
|
||||
<div class="auth-field">
|
||||
<label for="release_date">Release Date</label>
|
||||
<input type="date" id="release_date" name="release_date">
|
||||
</div>
|
||||
<div class="auth-field">
|
||||
<label for="demo_date">Demo Date</label>
|
||||
<input type="date" id="demo_date" name="demo_date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-field" style="margin-bottom: 0.75rem;">
|
||||
<label>Type</label>
|
||||
<div style="display: flex; gap: 0.375rem; flex-wrap: wrap;">
|
||||
{% for t in [('game', 'Game'), ('game_asset', 'Game Asset'), ('software', 'Software'), ('mobile_app', 'Mobile App'), ('website', 'Website')] %}
|
||||
<label style="display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; cursor: pointer; background: var(--bg-card-hover);">
|
||||
<input type="radio" name="project_type" value="{{ t[0] }}" {% if t[0] == 'software' %}checked{% endif %} style="width: auto; accent-color: var(--accent);">
|
||||
{{ t[1] }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-field" style="margin-bottom: 0.75rem;">
|
||||
<label>Status</label>
|
||||
<div style="display: flex; gap: 0.375rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; cursor: pointer; background: var(--bg-card-hover);">
|
||||
<input type="radio" name="status" value="In Development" checked style="width: auto; accent-color: var(--accent);"> In Development
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; cursor: pointer; background: var(--bg-card-hover);">
|
||||
<input type="radio" name="status" value="Released" style="width: auto; accent-color: var(--accent);"> Released
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-field" style="margin-bottom: 1rem;">
|
||||
<label>Platforms</label>
|
||||
<div id="platforms-tags" style="display: flex; flex-wrap: wrap; gap: 0.375rem; margin-bottom: 0.5rem;"></div>
|
||||
<input type="text" id="platforms-input" placeholder="Add platform (press Enter)" style="font-size: 0.8125rem;">
|
||||
<input type="hidden" id="platforms" name="platforms" value="">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.5rem;">
|
||||
{% for p in ['PC', 'Windows', 'Mac', 'Linux', 'iOS', 'Android', 'Web', 'Console'] %}
|
||||
<button type="button" class="platform-preset" data-platform="{{ p }}" style="padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.6875rem; font-weight: 600; background: var(--border); color: var(--text-muted); cursor: pointer; border: none;">{{ p }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
|
||||
<button type="button" class="modal-close btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Project</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById("create-project-btn").addEventListener("click", function() {
|
||||
document.getElementById("create-project-modal").classList.add("visible");
|
||||
});
|
||||
document.querySelectorAll("#create-project-modal .modal-close").forEach(function(btn) {
|
||||
btn.addEventListener("click", function() {
|
||||
document.getElementById("create-project-modal").classList.remove("visible");
|
||||
});
|
||||
});
|
||||
document.getElementById("create-project-modal").addEventListener("click", function(e) {
|
||||
if (e.target === this) this.classList.remove("visible");
|
||||
});
|
||||
|
||||
(function() {
|
||||
var platformsInput = document.getElementById("platforms-input");
|
||||
var hiddenInput = document.getElementById("platforms");
|
||||
var tagsContainer = document.getElementById("platforms-tags");
|
||||
|
||||
function addPlatform(val) {
|
||||
val = val.trim();
|
||||
if (!val) return;
|
||||
var tag = document.createElement("span");
|
||||
tag.className = "platform-tag";
|
||||
tag.textContent = val;
|
||||
var remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "x";
|
||||
remove.style.marginLeft = "4px";
|
||||
remove.style.fontSize = "0.75rem";
|
||||
remove.style.padding = "0";
|
||||
remove.style.background = "none";
|
||||
remove.style.border = "none";
|
||||
remove.style.color = "inherit";
|
||||
remove.style.cursor = "pointer";
|
||||
remove.addEventListener("click", function() {
|
||||
tag.remove();
|
||||
updatePlatforms();
|
||||
});
|
||||
tag.appendChild(remove);
|
||||
tagsContainer.appendChild(tag);
|
||||
platformsInput.value = "";
|
||||
updatePlatforms();
|
||||
}
|
||||
|
||||
function updatePlatforms() {
|
||||
var tags = tagsContainer.querySelectorAll(".platform-tag");
|
||||
var values = [];
|
||||
tags.forEach(function(t) {
|
||||
values.push(t.textContent.replace("x", "").trim());
|
||||
});
|
||||
hiddenInput.value = values.join(",");
|
||||
}
|
||||
|
||||
platformsInput.addEventListener("keydown", function(e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addPlatform(platformsInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll(".platform-preset").forEach(function(btn) {
|
||||
btn.addEventListener("click", function() {
|
||||
addPlatform(this.dataset.platform);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user