Add login screen: user picker with initials, new user join, cookie-based session

This commit is contained in:
Richard Sauer 2026-04-08 13:16:09 +10:00
parent 82678f9c58
commit f756de43c5
3 changed files with 88 additions and 29 deletions

38
app.py
View File

@ -3,7 +3,7 @@ import os
import json
import time
import uuid
from flask import Flask, render_template, request, jsonify, send_from_directory
from flask import Flask, render_template, request, jsonify, redirect, make_response
app = Flask(__name__)
LAYOUT_DIR = os.path.join(os.path.dirname(__file__), "layouts")
@ -17,7 +17,6 @@ def get_layout():
if os.path.exists(CURRENT_LAYOUT):
with open(CURRENT_LAYOUT) as f:
return json.load(f)
# First run — copy default
default = os.path.join(LAYOUT_DIR, "default.json")
with open(default) as f:
layout = json.load(f)
@ -28,9 +27,36 @@ def save_layout(layout):
with open(CURRENT_LAYOUT, "w") as f:
json.dump(layout, f, indent=2)
def get_user():
return request.cookies.get("bfa_user", "")
@app.route("/")
def index():
return render_template("editor.html")
if not get_user():
return redirect("login")
return render_template("editor.html", user=get_user())
@app.route("/login", methods=["GET", "POST"])
def login():
layout = get_layout()
users = layout.get("users", ["Richard", "Rob", "Guido"])
if request.method == "POST":
name = request.form.get("user", "").strip()
if name:
resp = make_response(redirect("./"))
resp.set_cookie("bfa_user", name, max_age=86400*30)
# Add user to layout if new
if name not in users:
layout.setdefault("users", []).append(name)
save_layout(layout)
return resp
return render_template("login.html", users=users)
@app.route("/logout")
def logout():
resp = make_response(redirect("login"))
resp.delete_cookie("bfa_user")
return resp
@app.route("/api/layout", methods=["GET"])
def api_get_layout():
@ -49,7 +75,7 @@ def api_add_comment():
comment = {
"id": str(uuid.uuid4())[:8],
"target": data["target"],
"user": data["user"],
"user": data.get("user", get_user()),
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
"text": data["text"]
}
@ -75,12 +101,12 @@ def api_upload_photo():
f = request.files["file"]
fname = f"{int(time.time())}_{f.filename}"
f.save(os.path.join(PHOTO_DIR, fname))
return jsonify({"filename": fname, "url": f"/static/photos/{fname}"})
return jsonify({"filename": fname, "url": f"static/photos/{fname}"})
@app.route("/api/photos", methods=["GET"])
def api_list_photos():
photos = sorted(os.listdir(PHOTO_DIR)) if os.path.exists(PHOTO_DIR) else []
return jsonify([{"filename": p, "url": f"/static/photos/{p}"} for p in photos if not p.startswith(".")])
return jsonify([{"filename": p, "url": f"static/photos/{p}"} for p in photos if not p.startswith(".")])
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001, debug=True)

View File

@ -170,8 +170,8 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
<span class="sub">HMI Design Tool</span>
</div>
<div class="toolbar-right">
<select id="user-select"></select>
<button onclick="addUser()">+ Add Person</button>
<span style="color:var(--blue);font-weight:600">{{ user }}</span>
<a href="logout" style="color:var(--dim);text-decoration:none;font-size:12px">Logout</a>
<span style="color:var(--dim)">|</span>
<button class="mode-btn active" id="btn-edit" onclick="setMode('edit')">EDIT</button>
<button class="mode-btn" id="btn-preview" onclick="setMode('preview')">PREVIEW</button>
@ -284,7 +284,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
let layout = null;
let currentPage = 0;
let mode = 'edit'; // 'edit' or 'preview'
let currentUser = localStorage.getItem('bfa-user') || '';
let currentUser = '{{ user }}';
let simState = {}; // id -> {on:bool, value:number}
let commentTarget = null;
let sortableInstance = null;
@ -303,17 +303,7 @@ async function init() {
renderSchematic();
}
function initUsers() {
const sel = document.getElementById('user-select');
sel.innerHTML = '';
(layout.users || []).forEach(u => {
const o = document.createElement('option');
o.value = u; o.textContent = u;
if (u === currentUser) o.selected = true;
sel.appendChild(o);
});
sel.onchange = () => { currentUser = sel.value; localStorage.setItem('bfa-user', currentUser); };
}
function initUsers() { /* handled by login page */ }
function initSimState() {
layout.pages.forEach(p => p.cards.forEach(c => {
@ -324,15 +314,7 @@ function initSimState() {
}));
}
async function addUser() {
const name = prompt('Enter name:');
if (!name) return;
await fetch('api/users', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name})});
layout.users.push(name);
currentUser = name;
localStorage.setItem('bfa-user', name);
initUsers();
}
function addUser() { window.location.href = 'logout'; }
// ══════════════════════════════════════════════════════
// MODE

51
templates/login.html Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>BFA Banana Dryer — Login</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0e17;--card:#131a2b;--border:#1e2a45;--text:#e8ecf4;--text2:#7a8baa;--dim:#4a5670;--blue:#2d7ff9;--amber:#ffab00}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;align-items:center;justify-content:center}
.login-box{width:400px;background:var(--card);border:1px solid var(--border);border-radius:16px;padding:40px;display:flex;flex-direction:column;align-items:center;gap:24px}
.login-box h1{font-size:24px;font-weight:700}
.login-box h2{font-size:16px;color:var(--text2);font-weight:400}
.login-box .divider{width:100%;height:1px;background:var(--border)}
.user-list{width:100%;display:flex;flex-direction:column;gap:8px}
.user-btn{width:100%;padding:16px;border:2px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text);font-size:18px;font-weight:600;cursor:pointer;text-align:left;transition:all 0.15s}
.user-btn:hover{border-color:var(--blue);background:rgba(45,127,249,0.1)}
.user-btn .initials{display:inline-flex;width:36px;height:36px;border-radius:50%;background:var(--blue);color:#fff;align-items:center;justify-content:center;font-size:14px;font-weight:700;margin-right:12px}
.add-section{width:100%;display:flex;gap:8px}
.add-section input{flex:1;padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);font-size:16px}
.add-section input::placeholder{color:var(--dim)}
.add-section button{padding:12px 20px;border:none;border-radius:8px;background:var(--blue);color:#fff;font-size:14px;font-weight:600;cursor:pointer}
.add-section button:hover{opacity:0.9}
.footer{font-size:12px;color:var(--dim)}
</style>
</head>
<body>
<form method="POST" class="login-box">
<h1>BFA Banana Dryer</h1>
<h2>HMI Design Tool — Who are you?</h2>
<div class="divider"></div>
<div class="user-list">
{% for u in users %}
<button type="submit" name="user" value="{{ u }}" class="user-btn">
<span class="initials">{{ u[0] }}</span>{{ u }}
</button>
{% endfor %}
</div>
<div class="divider"></div>
<div class="add-section">
<input type="text" name="user" placeholder="Or type your name...">
<button type="submit">Join</button>
</div>
<div class="footer">SAE Engineering — Design Collaboration</div>
</form>
</body>
</html>