- local_workspace.html: supprimé Alpine.data dupliqué + ajout hamburger - workspaces.html, workspace.html, settings.html, page_editor.html: +hamburger - auth.py: +path="/" sur tous les set_cookie de session (Chrome compat) - csrf.py: +path="/" sur cookie CSRF Root cause des bugs: 1. Chrome: cookie sans path="/" → non envoyé sur certaines routes 2. Firefox: les templates écrasaient le block topbar → pas de hamburger → sidebar inaccessible sur mobile (overlay + slide-in ne fonctionnaient pas)
153 lines
6.2 KiB
HTML
153 lines
6.2 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_title %}Workspaces{% endblock %}
|
|
{% block page_icon %}🏠{% endblock %}
|
|
|
|
{% block topbar %}
|
|
<header class="topbar">
|
|
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
</button>
|
|
<div class="topbar-left">
|
|
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Home</span>
|
|
<span class="page-title-topbar">Workspaces</span>
|
|
</div>
|
|
<div class="topbar-right">
|
|
<button class="page-action-btn" onclick="window.location='/accounts/settings'" title="Settings">⚙</button>
|
|
</div>
|
|
</header>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
.ws-page{max-width:800px;margin:0 auto;padding:40px 24px;}
|
|
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;}
|
|
.ws-header h1{font-size:24px;}
|
|
|
|
.ws-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px;}
|
|
.ws-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:20px;cursor:pointer;transition:background 150ms;}
|
|
.ws-card:hover{background:var(--bg-tertiary);border-color:var(--border-strong);}
|
|
.ws-card.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);}
|
|
.ws-card-name{font-size:16px;font-weight:500;margin-bottom:4px;}
|
|
.ws-card-count{font-size:12px;color:var(--text-tertiary);}
|
|
.ws-card-actions{display:flex;gap:4px;margin-top:8px;opacity:0;transition:opacity 150ms;}
|
|
.ws-card:hover .ws-card-actions{opacity:1;}
|
|
|
|
.dialog-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:500;}
|
|
.dialog-box{background:#1F1F1F;border:1px solid var(--border);border-radius:18px;padding:24px;width:400px;max-width:90vw;}
|
|
.dialog-box h3{margin-bottom:16px;}
|
|
.dialog-input{width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;margin-bottom:12px;outline:none;}
|
|
.dialog-input:focus{border-color:var(--accent);}
|
|
.dialog-actions{display:flex;gap:8px;justify-content:flex-end;}
|
|
.btn{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;}
|
|
.btn-primary{background:var(--accent);color:#fff;}
|
|
.btn-primary:hover{background:var(--accent-hover);}
|
|
.btn-secondary{background:var(--bg-tertiary);color:var(--text);}
|
|
.btn-secondary:hover{background:var(--bg-hover);}
|
|
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);}
|
|
.btn-danger:hover{background:rgba(255,80,80,.25);}
|
|
.btn-new{border:1px dashed var(--border-strong);background:transparent;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;min-height:120px;}
|
|
.btn-new:hover{background:var(--bg-tertiary);color:var(--text);}
|
|
</style>
|
|
|
|
<div class="ws-page" x-data="workspacesPage()">
|
|
|
|
<div class="ws-header">
|
|
<h1>🏠 Workspaces</h1>
|
|
<button class="btn btn-primary" @click="showCreate=true">+ New Workspace</button>
|
|
</div>
|
|
|
|
<div class="ws-grid">
|
|
<template x-for="ws in workspaces" :key="ws.id">
|
|
<div class="ws-card" :class="{active:ws.id===activeId}" @click="select(ws)">
|
|
<div class="ws-card-name" x-text="ws.name"></div>
|
|
<div class="ws-card-count" x-text="ws.page_count+' page(s)'"></div>
|
|
<div class="ws-card-actions" @click.stop>
|
|
<button class="btn btn-secondary" style="padding:4px 8px;font-size:11px" @click="renameWs(ws)">✏️</button>
|
|
<button class="btn btn-danger" style="padding:4px 8px;font-size:11px" @click="deleteWs(ws)">🗑</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div class="ws-card btn-new" @click="showCreate=true">
|
|
<span style="font-size:32px">+</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create/Rename dialog -->
|
|
<div class="dialog-overlay" x-show="showCreate||showRename" @click.away="showCreate=false;showRename=false">
|
|
<div class="dialog-box">
|
|
<h3 x-text="showRename?'Rename Workspace':'New Workspace'"></h3>
|
|
<input class="dialog-input" x-model="wsName" placeholder="Workspace name" @keydown.enter="showRename?doRename():doCreate()">
|
|
<div class="dialog-actions">
|
|
<button class="btn btn-secondary" @click="showCreate=false;showRename=false">Cancel</button>
|
|
<button class="btn btn-primary" @click="showRename?doRename():doCreate()" x-text="showRename?'Rename':'Create'"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
function workspacesPage() {
|
|
return {
|
|
workspaces: [],
|
|
activeId: null,
|
|
showCreate: false,
|
|
showRename: false,
|
|
wsName: '',
|
|
renameTarget: null,
|
|
|
|
async init() {
|
|
await this.load();
|
|
},
|
|
async load() {
|
|
const r = await fetch('/api/workspaces');
|
|
const d = await r.json();
|
|
this.workspaces = d.workspaces || [];
|
|
this.activeId = d.active_id;
|
|
},
|
|
async select(ws) {
|
|
await fetch(`/api/workspaces/${ws.id}/select`, {method:'POST'});
|
|
if (ws.page_count > 0) {
|
|
window.location = '/local-workspace';
|
|
} else {
|
|
window.location = '/local-workspace';
|
|
}
|
|
},
|
|
async doCreate() {
|
|
if (!this.wsName.trim()) return;
|
|
await fetch('/api/workspaces', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({name:this.wsName.trim()})
|
|
});
|
|
this.wsName = '';
|
|
this.showCreate = false;
|
|
await this.load();
|
|
},
|
|
renameWs(ws) {
|
|
this.renameTarget = ws;
|
|
this.wsName = ws.name;
|
|
this.showRename = true;
|
|
},
|
|
async doRename() {
|
|
if (!this.wsName.trim()||!this.renameTarget) return;
|
|
await fetch(`/api/workspaces/${this.renameTarget.id}`, {
|
|
method:'PUT',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({name:this.wsName.trim()})
|
|
});
|
|
this.wsName = '';
|
|
this.showRename = false;
|
|
this.renameTarget = null;
|
|
await this.load();
|
|
},
|
|
async deleteWs(ws) {
|
|
if (!confirm(`Delete workspace "${ws.name}" and all its pages?`)) return;
|
|
await fetch(`/api/workspaces/${ws.id}`, {method:'DELETE'});
|
|
await this.load();
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|