feat: implement basic version of the application with core functionality

This commit is contained in:
2026-05-10 07:08:12 +00:00
commit 97689f86d7
54 changed files with 5906 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevPlace - The Developer Social Network</title>
<link rel="stylesheet" href="/static/css/variables.css">
<link rel="stylesheet" href="/static/css/base.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>&#x1f4bb;</text></svg>">
{% block extra_head %}{% endblock %}
</head>
<body>
{% if user %}
<nav class="topnav">
<div class="topnav-inner">
<a href="/feed" class="topnav-logo">Dev<span>Place</span></a>
<div class="topnav-links">
<a href="/feed" class="topnav-link {% if request.url.path == '/feed' %}active{% endif %}">Home</a>
<a href="/projects" class="topnav-link {% if 'projects' in request.url.path %}active{% endif %}">Projects</a>
<a href="/messages" class="topnav-link {% if 'messages' in request.url.path %}active{% endif %}">Messages</a>
</div>
<div class="topnav-right">
<a href="/notifications" class="topnav-icon">
<span class="nav-bell">&#x1F514;</span>
{% set unread_count = get_unread_count(user["uid"]) %}
{% if unread_count > 0 %}
<span class="nav-badge">{{ unread_count }}</span>
{% endif %}
</a>
<a href="/profile/{{ user['username'] }}" class="topnav-user">
<div class="avatar avatar-sm">{{ user['username'][:1].upper() }}</div>
<div class="topnav-user-info">
<span class="topnav-user-name">{{ user['username'] }}</span>
<span class="topnav-user-role">{{ user.get('role', 'Member') }}</span>
</div>
</a>
</div>
</div>
</nav>
{% endif %}
<main class="page">
{% block content %}{% endblock %}
</main>
<script type="module" src="/static/js/Application.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+318
View File
@@ -0,0 +1,318 @@
{% 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">
<aside class="feed-sidebar">
<h3>Topics</h3>
<div class="feed-categories">
<a href="/feed" class="feed-category {% if not current_topic %}active{% endif %}">
<span class="dot" style="background: var(--text-muted);"></span>
All
</a>
<a href="/feed?topic=devlog" class="feed-category {% if current_topic == 'devlog' %}active{% endif %}">
<span class="dot" style="background: #7c4dff;"></span>
Devlog
</a>
<a href="/feed?topic=showcase" class="feed-category {% if current_topic == 'showcase' %}active{% endif %}">
<span class="dot" style="background: #00bfa5;"></span>
Showcase
</a>
<a href="/feed?topic=question" class="feed-category {% if current_topic == 'question' %}active{% endif %}">
<span class="dot" style="background: #448aff;"></span>
Question
</a>
<a href="/feed?topic=rant" class="feed-category {% if current_topic == 'rant' %}active{% endif %}">
<span class="dot" style="background: #ff5252;"></span>
Rant
</a>
<a href="/feed?topic=fun" class="feed-category {% if current_topic == 'fun' %}active{% endif %}">
<span class="dot" style="background: #ffab00;"></span>
Fun
</a>
</div>
</aside>
<div class="feed-main">
<div class="feed-nav">
<a href="/feed?tab=all" class="feed-nav-btn {% if current_tab == 'all' or not current_tab %}active{% endif %}">All</a>
<a href="/feed?tab=trending" class="feed-nav-btn {% if current_tab == 'trending' %}active{% endif %}">Trending</a>
<a href="/feed?tab=recent" class="feed-nav-btn {% if current_tab == 'recent' %}active{% endif %}">Recent</a>
<a href="/feed?tab=following" class="feed-nav-btn {% if current_tab == 'following' %}active{% endif %}">Following</a>
<div class="feed-nav-actions">
<button class="btn-ghost btn-icon" title="Refresh">&#x21BB;</button>
<button class="btn-ghost btn-icon" title="Notifications">&#x1F514;</button>
</div>
</div>
<div class="feed-posts">
{% for item in posts %}
<article class="post-card fade-in">
<div class="post-header">
<div class="avatar avatar-sm">{{ item.author['username'][:1].upper() if item.author else '?' }}</div>
<div>
<div class="post-author">{{ item.author['username'] if item.author else 'Unknown' }}
{% if item.author and item.author.get('role') %}
<span class="post-author-role">&middot; {{ item.author['role'] }}</span>
{% endif %}
</div>
</div>
<span class="post-time">{{ item.time_ago }}</span>
</div>
<div class="post-topic">
<span class="badge badge-{{ item.post['topic'] }}">{{ item.post['topic'] }}</span>
</div>
{% if item.post.get('title') %}
<h3 class="post-title">{{ item.post['title'] }}</h3>
{% endif %}
<p class="post-content">{{ item.post['content'][:300] }}{% if item.post['content']|length > 300 %}...{% endif %}</p>
<div class="post-actions">
<form method="POST" action="/votes/post/{{ item.post['uid'] }}" style="display:inline;">
<input type="hidden" name="value" value="1">
<button type="submit" class="post-action-btn" data-vote="1" data-target="{{ item.post['uid'] }}" data-type="post">
+{{ item.post.get('stars', 0) }}
</button>
</form>
<a href="/posts/{{ item.post['uid'] }}" class="post-action-btn">
&#x1F4AC; {{ item.comment_count }}
</a>
<a href="/posts/{{ item.post['uid'] }}" class="post-action-btn" style="margin-left: auto;">
&#x2197;&#xFE0E; Open
</a>
<button class="post-action-btn">&#x1F517; Share</button>
</div>
</article>
{% else %}
<div class="card" style="text-align: center; padding: 3rem 1rem;">
<p style="color: var(--text-muted); font-size: 0.9375rem;">No posts yet. Be the first!</p>
</div>
{% endfor %}
</div>
</div>
<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>
<div class="topic-links">
<a href="#">Read More</a>
<a href="#">Source</a>
</div>
</div>
<div class="community-stats">
<h3>Community</h3>
<div class="stat-row">
<span class="label">Total Members</span>
<span class="value">{{ total_members }}</span>
</div>
<div class="stat-row">
<span class="label">Posts Today</span>
<span class="value">{{ posts_today }}</span>
</div>
<div class="stat-row">
<span class="label">Total Projects</span>
<span class="value">{{ total_projects }}</span>
</div>
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border);">
<h3 style="font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.5rem;">Top Authors</h3>
{% for author in top_authors %}
<div class="stat-row">
<span class="label">
<span class="avatar avatar-sm" style="display: inline-flex; width: 20px; height: 20px; font-size: 0.625rem; vertical-align: middle; margin-right: 0.375rem;">{{ author['username'][:1].upper() }}</span>
{{ author['username'] }}
</span>
<span class="value">{{ author.get('stars', 0) }}</span>
</div>
{% endfor %}
</div>
</div>
</aside>
</div>
<a href="#" class="feed-fab" data-modal="create-post-modal" title="Create New Post">+</a>
<div class="modal-overlay" id="create-post-modal" style="display: none; 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 New Post</h3>
<button type="button" class="modal-close btn-ghost btn-icon" style="font-size: 1.25rem;">&times;</button>
</div>
<form id="create-post-form" method="POST" action="/posts/create" enctype="multipart/form-data">
<div style="display: flex; gap: 0.375rem; flex-wrap: wrap; margin-bottom: 1rem;">
{% for t in ['devlog', 'showcase', 'question', 'rant', 'fun', 'random'] %}
<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); transition: all 0.2s;">
<input type="radio" name="topic" value="{{ t }}" {% if t == 'random' %}checked{% endif %} style="width: auto; accent-color: var(--accent);">
{{ t|capitalize }}
</label>
{% endfor %}
</div>
<div class="auth-field" style="margin-bottom: 0.75rem;">
<label for="post-content">What are you sharing?</label>
<textarea id="post-content" name="content" required maxlength="2000" placeholder="What's on your mind?" style="min-height: 120px;"></textarea>
<small style="color: var(--text-muted); font-size: 0.75rem;"><span id="post-content-count">0/2000</span></small>
</div>
<div class="auth-field" style="margin-bottom: 0.75rem;">
<label for="post-title">Title (optional)</label>
<input type="text" id="post-title" name="title" maxlength="500" placeholder="Post title">
<small style="color: var(--text-muted); font-size: 0.75rem;"><span id="post-title-count">0/500</span></small>
</div>
<div class="auth-field" style="margin-bottom: 1rem;">
<label for="project_uid">Link to Project (Optional)</label>
<select id="project_uid" name="project_uid">
<option value="">No project</option>
{% for p in get_user_projects(user['uid']) %}
<option value="{{ p['uid'] }}">{{ p['title'] }}</option>
{% endfor %}
</select>
</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>
<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">Post</button>
</div>
</form>
</div>
</div>
<script>
document.querySelector("[data-modal]").addEventListener("click", function() {
document.getElementById("create-post-modal").style.display = "flex";
});
document.querySelectorAll(".modal-close").forEach(function(btn) {
btn.addEventListener("click", function() {
var modal = this.closest(".modal-overlay");
if (modal) modal.style.display = "none";
});
});
document.getElementById("create-post-modal").addEventListener("click", function(e) {
if (e.target === this) this.style.display = "none";
});
</script>
{% endblock %}
+56
View File
@@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/landing.css">
{% endblock %}
{% block content %}
<div class="landing">
<nav class="landing-nav">
<div class="landing-logo">Dev<span>Place</span></div>
<div class="landing-nav-links">
<a href="/">Home</a>
<a href="/projects">Projects</a>
<a href="/auth/login">Login</a>
<a href="/auth/signup" class="btn btn-primary btn-sm">Sign Up</a>
</div>
</nav>
<section class="landing-hero">
<h1>Devplace.net &mdash; The Developer <span>Social Network</span></h1>
<p>Track industry shifts. Discover bold releases. Share what you're building in an open, uncensored environment.</p>
<a href="/auth/signup" class="landing-cta">Join DevPlace Free</a>
<div class="landing-features">
<div class="landing-feature">
<div class="landing-feature-icon">&#x1F511;</div>
<h3>100% Free Forever</h3>
</div>
<div class="landing-feature">
<div class="landing-feature-icon">&#x1F6AB;</div>
<h3>Zero Ads</h3>
</div>
<div class="landing-feature">
<div class="landing-feature-icon">&#x1F4AD;</div>
<h3>No Censorship</h3>
</div>
<div class="landing-feature">
<div class="landing-feature-icon">&#x1F4B3;</div>
<h3>No Payments</h3>
</div>
</div>
<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>
<div class="landing-daily-topic-links">
<a href="#">Read More</a>
<a href="#">Source</a>
</div>
</div>
<div class="landing-auth-link">
Already have an account? <a href="/auth/login">Log In</a>
</div>
</section>
</div>
{% endblock %}
+51
View File
@@ -0,0 +1,51 @@
{% 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="/" class="auth-back">&larr; Back</a>
<h2>Welcome Back</h2>
<p class="subtitle">Sign in to continue to DevPlace</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/login">
<div class="auth-field">
<label for="email">Email address</label>
<input type="email" id="email" name="email" value="{{ email or '' }}" required maxlength="255" placeholder="you@example.com">
</div>
<div class="auth-field">
<label for="password">Password</label>
<div class="input-wrap">
<input type="password" id="password" name="password" required minlength="6" maxlength="128" placeholder="Your password">
<button type="button" class="auth-toggle-pw" aria-label="Toggle password visibility">&#x1F441;</button>
</div>
</div>
<div class="auth-options">
<label>
<input type="checkbox" name="remember_me"> Remember me
</label>
<a href="#">Forgot your password?</a>
</div>
<button type="submit" class="auth-submit">Sign in</button>
</form>
<div class="auth-footer">
Don't have an account? <a href="/auth/signup">Create new account</a>
</div>
</div>
</div>
{% endblock %}
+98
View File
@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% 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>
{% endblock %}
{% block content %}
<div class="messages-layout">
<div class="messages-list">
<div class="messages-list-header">
<input type="text" id="message-search" placeholder="Search conversations..." value="{{ search }}">
</div>
<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 %}">
<div class="avatar avatar-sm">{{ conv.other_user['username'][:1].upper() }}</div>
<div class="conversation-info">
<div class="conversation-name">{{ conv.other_user['username'] }}</div>
<div class="conversation-preview">{{ conv.last_message[:60] }}</div>
</div>
<span class="conversation-time">{{ conv.last_message_at[:10] }}</span>
</a>
{% else %}
<div style="padding: 2rem 1rem; text-align: center; color: var(--text-muted); font-size: 0.875rem;">
No conversations yet
</div>
{% endfor %}
</div>
</div>
<div class="messages-main">
{% if other_user %}
<div class="messages-main-header">
<div class="avatar avatar-sm">{{ other_user['username'][:1].upper() }}</div>
<h3>{{ other_user['username'] }}</h3>
</div>
<div class="messages-thread">
{% for msg in messages %}
<div class="message-bubble {% if msg.is_mine %}mine{% else %}theirs{% endif %}">
{{ msg.message['content'] }}
<span class="message-time">{{ msg.time_ago }}</span>
</div>
{% endfor %}
</div>
<form class="messages-input-area" method="POST" action="/messages/send">
<input type="hidden" name="receiver_uid" value="{{ other_user['uid'] }}">
<input type="text" name="content" placeholder="Type a message..." required maxlength="2000" autocomplete="off">
<button type="submit" class="messages-send-btn">&#x27A4;</button>
</form>
{% else %}
<div class="messages-empty">
<p>Select a conversation to start chatting</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
+71
View File
@@ -0,0 +1,71 @@
{% 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">
<div class="notifications-header">
<h2>Notifications</h2>
<form method="POST" action="/notifications/mark-all-read" style="display:inline;">
<button type="submit" class="btn btn-ghost btn-sm">Mark all read</button>
</form>
</div>
<div class="notifications-list">
{% for item in notifications %}
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}">
<div class="avatar avatar-sm">{{ item.actor['username'][:1].upper() if item.actor else '?' }}</div>
<div class="notification-body">
<div class="notification-text">{{ item.notification['message'] }}</div>
<div class="notification-time">{{ item.time_ago }}</div>
</div>
<form method="POST" action="/notifications/mark-read/{{ item.notification['uid'] }}" style="display:inline;">
<button type="submit" class="notification-dismiss" data-uid="{{ item.notification['uid'] }}">&times;</button>
</form>
</div>
{% else %}
<div class="notifications-empty">
<p>No notifications yet</p>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
+149
View File
@@ -0,0 +1,149 @@
{% 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">
<a href="/feed" style="display: inline-flex; align-items: center; gap: 0.375rem; color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;">&larr; Back to Feed</a>
<article class="post-detail">
<div class="post-detail-header">
<div class="avatar">{{ author['username'][:1].upper() if author else '?' }}</div>
<div>
<div class="post-detail-author">{{ author['username'] if author else 'Unknown' }}
{% if author and author.get('role') %}
<span class="post-detail-role">&middot; {{ author['role'] }}</span>
{% endif %}
</div>
</div>
<span class="post-detail-time">{{ time_ago }}</span>
</div>
<div style="margin-bottom: 0.75rem;">
<span class="badge badge-{{ post['topic'] }}">{{ post['topic'] }}</span>
</div>
{% if post.get('title') %}
<h1 class="post-detail-title">{{ post['title'] }}</h1>
{% endif %}
<div class="post-detail-content">{{ post['content'] }}</div>
<div class="post-detail-actions">
<form method="POST" action="/votes/post/{{ post['uid'] }}" style="display:inline;">
<input type="hidden" name="value" value="1">
<button type="submit" class="post-action-btn" style="display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.625rem; border-radius: var(--radius); font-size: 0.8125rem; color: var(--text-muted); background: none; cursor: pointer;">
+{{ post.get('stars', 0) }}
</button>
</form>
<button class="post-action-btn" style="display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.625rem; border-radius: var(--radius); font-size: 0.8125rem; color: var(--text-muted); background: none;">&#x1F517; Share</button>
</div>
</article>
<section class="comments-section">
<h3>Comments</h3>
{% for item in comments %}
<div class="comment">
<div class="comment-votes">
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
<input type="hidden" name="value" value="1">
<button type="submit" class="comment-vote-btn">+</button>
</form>
<span class="comment-vote-count">{{ item.votes.up - item.votes.down }}</span>
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
<input type="hidden" name="value" value="-1">
<button type="submit" class="comment-vote-btn">-</button>
</form>
</div>
<div class="comment-body">
<div class="comment-header">
<div class="avatar avatar-sm" style="width: 24px; height: 24px; font-size: 0.625rem;">{{ item.author['username'][:1].upper() if item.author else '?' }}</div>
<span class="comment-author">{{ item.author['username'] if item.author else 'Unknown' }}</span>
<span class="comment-time">{{ item.time_ago }}</span>
</div>
<div class="comment-text">{{ item.comment['content'] }}</div>
<div class="comment-actions">
<button class="comment-action-btn">Reply</button>
{% if user and item.comment['user_uid'] == user['uid'] %}
<form method="POST" action="/comments/delete/{{ item.comment['uid'] }}" style="display:inline;">
<button type="submit" class="comment-action-btn">Delete</button>
</form>
{% endif %}
</div>
</div>
</div>
{% else %}
<p style="color: var(--text-muted); font-size: 0.875rem; text-align: center; padding: 2rem 0;">No comments yet. Start the discussion.</p>
{% endfor %}
{% if user %}
<form class="comment-form" method="POST" action="/comments/create">
<input type="hidden" name="post_uid" value="{{ post['uid'] }}">
<div class="avatar avatar-sm">{{ user['username'][:1].upper() }}</div>
<textarea name="content" placeholder="Your opinion goes here..." required maxlength="1000"></textarea>
<div class="comment-form-actions">
<button type="button" title="Add emoji">&#x1F600;</button>
<button type="button" title="Add image">&#x1F5BC;</button>
<button type="submit" class="comment-form-submit">Post</button>
</div>
</form>
{% endif %}
</section>
</div>
{% endblock %}
+228
View File
@@ -0,0 +1,228 @@
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/profile.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 %}
{% block content %}
<div class="profile-layout">
<aside class="profile-sidebar">
<div class="profile-card">
<div class="profile-avatar-wrap">
<div class="avatar avatar-lg">{{ profile_user['username'][:1].upper() }}</div>
</div>
<div class="profile-name">{{ profile_user['username'] }}</div>
{% if profile_user.get('role') %}
<div class="profile-role">{{ profile_user['role'] }}</div>
{% endif %}
<div class="profile-stats">
<div class="profile-stat">
<span class="profile-stat-value">{{ profile_user.get('posts_count', 0) }}</span>
<span class="profile-stat-label">Posts</span>
</div>
<div class="profile-stat">
<span class="profile-stat-value">{{ profile_user.get('level', 1) }}</span>
<span class="profile-stat-label">Level</span>
</div>
<div class="profile-stat">
<span class="profile-stat-value">{{ profile_user.get('stars', 0) }}</span>
<span class="profile-stat-label">Stars</span>
</div>
</div>
<div class="profile-level-bar">
<div class="level-label">
<span>Progress to next level</span>
<span>{{ profile_user.get('xp', 0) % 100 }}%</span>
</div>
<div class="bar">
<div class="bar-fill" style="width: {{ profile_user.get('xp', 0) % 100 }}%"></div>
</div>
</div>
<div class="profile-badges">
{% for badge in badges %}
<span class="profile-badge">{{ badge['badge_name'] }}</span>
{% else %}
<span class="profile-badge">Member</span>
{% endfor %}
</div>
</div>
{% if user and user['uid'] == profile_user['uid'] %}
<div class="profile-info">
<form method="POST" action="/profile/update">
<div class="profile-info-row">
<span class="profile-info-label">Bio</span>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="profile-info-value bio-display" id="display-bio">{{ profile_user.get('bio', '') or 'Not set' }}</span>
<button type="button" class="profile-info-edit" data-edit-field="bio">&#x270E;</button>
</div>
</div>
<div class="auth-field hidden" id="input-bio" style="margin-bottom: 0.5rem;">
<textarea name="bio" maxlength="500" rows="2" style="font-size: 0.8125rem;">{{ profile_user.get('bio', '') }}</textarea>
</div>
<div class="profile-info-row">
<span class="profile-info-label">Location</span>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="profile-info-value" id="display-location">{{ profile_user.get('location', '') or 'Not set' }}</span>
<button type="button" class="profile-info-edit" data-edit-field="location">&#x270E;</button>
</div>
</div>
<div class="auth-field hidden" id="input-location" style="margin-bottom: 0.5rem;">
<input type="text" name="location" maxlength="200" value="{{ profile_user.get('location', '') }}" style="font-size: 0.8125rem;">
</div>
<div class="profile-info-row">
<span class="profile-info-label">Git Link</span>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="profile-info-value" id="display-git_link">{{ profile_user.get('git_link', '') or 'Not set' }}</span>
<button type="button" class="profile-info-edit" data-edit-field="git_link">&#x270E;</button>
</div>
</div>
<div class="auth-field hidden" id="input-git_link" style="margin-bottom: 0.5rem;">
<input type="url" name="git_link" maxlength="500" value="{{ profile_user.get('git_link', '') }}" style="font-size: 0.8125rem;">
</div>
<div class="profile-info-row">
<span class="profile-info-label">Website</span>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="profile-info-value" id="display-website">{{ profile_user.get('website', '') or 'Not set' }}</span>
<button type="button" class="profile-info-edit" data-edit-field="website">&#x270E;</button>
</div>
</div>
<div class="auth-field hidden" id="input-website" style="margin-bottom: 0.5rem;">
<input type="url" name="website" maxlength="500" value="{{ profile_user.get('website', '') }}" style="font-size: 0.8125rem;">
</div>
<button type="submit" class="btn btn-primary btn-sm" style="width: 100%; margin-top: 0.5rem;">Save Changes</button>
</form>
</div>
{% else %}
<div class="profile-info">
<div class="profile-info-row">
<span class="profile-info-label">Bio</span>
<span class="profile-info-value">{{ profile_user.get('bio', '') or 'Not set' }}</span>
</div>
<div class="profile-info-row">
<span class="profile-info-label">Location</span>
<span class="profile-info-value">{{ profile_user.get('location', '') or 'Not set' }}</span>
</div>
<div class="profile-info-row">
<span class="profile-info-label">Git Link</span>
<span class="profile-info-value">{{ profile_user.get('git_link', '') or 'Not set' }}</span>
</div>
<div class="profile-info-row">
<span class="profile-info-label">Website</span>
<span class="profile-info-value">{{ profile_user.get('website', '') or 'Not set' }}</span>
</div>
</div>
{% endif %}
</aside>
<div class="profile-content">
<a href="/feed" style="display: inline-flex; align-items: center; gap: 0.375rem; color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;">&larr; Back</a>
<div class="profile-tabs">
<a href="/profile/{{ profile_user['username'] }}?tab=posts" class="profile-tab {% if current_tab == 'posts' %}active{% endif %}">Posts</a>
<a href="/profile/{{ profile_user['username'] }}?tab=projects" class="profile-tab {% if current_tab == 'projects' %}active{% endif %}">Projects</a>
<a href="/profile/{{ profile_user['username'] }}?tab=activity" class="profile-tab {% if current_tab == 'activity' %}active{% endif %}">Activity</a>
<button class="btn-ghost btn-icon" style="margin-left: auto; font-size: 0.8125rem;">&#x25B3;</button>
</div>
<div class="profile-posts">
{% if current_tab == 'posts' %}
{% for item in posts %}
<article class="post-card fade-in">
<div class="post-header">
<div class="avatar avatar-sm">{{ profile_user['username'][:1].upper() }}</div>
<div>
<div class="post-author">{{ profile_user['username'] }}
{% if profile_user.get('role') %}
<span class="post-author-role">&middot; {{ profile_user['role'] }}</span>
{% endif %}
</div>
</div>
<span class="post-time">{{ item.time_ago }}</span>
</div>
<div class="post-topic">
<span class="badge badge-{{ item.post['topic'] }}">{{ item.post['topic'] }}</span>
</div>
{% if item.post.get('title') %}
<h3 class="post-title">{{ item.post['title'] }}</h3>
{% endif %}
<p class="post-content">{{ item.post['content'][:200] }}{% if item.post['content']|length > 200 %}...{% endif %}</p>
<div class="post-actions">
<span class="post-action-btn" style="cursor: default;">+{{ item.post.get('stars', 0) }}</span>
<span class="post-action-btn" style="cursor: default;">&#x1F4AC; {{ item.comment_count }}</span>
<a href="/posts/{{ item.post['uid'] }}" class="post-action-btn" style="margin-left: auto;">&#x2197;&#xFE0E; Open</a>
</div>
</article>
{% else %}
<div class="card" style="text-align: center; padding: 3rem 1rem;">
<p style="color: var(--text-muted);">No posts yet.</p>
</div>
{% endfor %}
{% elif current_tab == 'projects' %}
{% for p in projects %}
<div class="card">
<h3 style="font-size: 1rem; font-weight: 700; margin-bottom: 0.25rem;">{{ p['title'] }}</h3>
<p style="font-size: 0.8125rem; color: var(--text-secondary);">{{ p.get('description', '')[:200] }}</p>
<div style="margin-top: 0.5rem; display: flex; gap: 0.375rem;">
<span class="badge" style="background: var(--border); color: var(--text-muted);">{{ p.get('project_type', 'software') }}</span>
<span class="badge" style="background: var(--border); color: var(--text-muted);">{{ p.get('status', 'In Development') }}</span>
</div>
</div>
{% else %}
<div class="card" style="text-align: center; padding: 3rem 1rem;">
<p style="color: var(--text-muted);">No projects yet.</p>
</div>
{% endfor %}
{% else %}
<div class="card" style="text-align: center; padding: 3rem 1rem;">
<p style="color: var(--text-muted);">Activity coming soon.</p>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+275
View File
@@ -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;">&#x2699;</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">&#x2606;</button>
</div>
<div class="project-card-meta">
<span>By {{ project.get('author_name', 'Unknown') }}</span>
{% if project.get('release_date') %}
<span>&#x1F4C5; {{ project['release_date'] }}</span>
{% endif %}
<span class="project-status {% if project.get('status') == 'Released' %}released{% else %}dev{% endif %}">
&#x25CF; {{ 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;">&times;</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 %}
+58
View File
@@ -0,0 +1,58 @@
{% 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="/" class="auth-back">&larr; Back</a>
<h2>Join the Community</h2>
<p class="subtitle">Create your DevPlace account and start connecting with developers</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/signup">
<div class="auth-field">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{ username or '' }}" required maxlength="32" placeholder="cooldev42">
<small style="color: var(--text-muted); font-size: 0.75rem;">Letters, numbers, hyphens, and underscores only</small>
</div>
<div class="auth-field">
<label for="email">Email address</label>
<input type="email" id="email" name="email" value="{{ email or '' }}" required maxlength="255" placeholder="you@example.com">
</div>
<div class="auth-field">
<label for="password">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">Create account</button>
</form>
<div class="auth-footer">
Already have an account? <a href="/auth/login">Sign in instead</a>
</div>
</div>
</div>
{% endblock %}