Ajout : - e2e/csp_preview.spec.js — apercu CSP strict SANS deployer : le build officiel @alpinejs/csp (e2e/fixtures/alpine.csp.js, 0 eval) est servi a la place de alpine.min.js par interception Playwright ; toute expression que le parseur maison ne digere pas = pageerror (filet). Premiere surface VERTE : library (composant lie, icones SVG via Alpine.effect, recherche ouverte + focalisee, 0 erreur). Changed : - 16 composants x-data="fn()" enregistres via Alpine.data (registre = seule resolution du build CSP, probe « Undefined variable » ; scripts classiques executes pendant le parsing => alpine:init toujours joint) : appState, libraryPage, workspacesPage, editorState, board x4, settings/import/table_view/team_load/trash/workspace/welcome/accounts/ card_detail. - base.html (shell) migre : x-effect document.* -> syncSidebarClass(), $nextTick(arrow) -> initSidebarSort(), window.FlowDeck.* -> fdCreatePage/fdCreateFolder/fdGwRefresh, Object.keys/Math.min/ window.innerWidth dans x-for et :style -> sidebarSections()/ sectionMenuPos() — tout = simple appel de methode. - x-html restants du shell -> x-init + Alpine.effect : icone agent, carte projet, library x3 ; recherche library -> toggleSearch() (Alpine.nextTick) ; openMoveSelected() pour Object.keys en expression. - eslint : 70 warnings -> 0/0 (globals getCsrf depuis A38 ph1, /* exported openCardDetail */ + /* global owner, repo */, 3 ;; residuels). suite **1093/1093** · ruff OK · eslint 0/0 · E2E 3/3 (csp_preview + smoke x2) · docs a jour (ROADMAP ph3 LOT 1, CHANGELOG, WORKLOAD, VERSION)
193 lines
10 KiB
HTML
193 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Bienvenue sur FlowDeck</title>
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
|
<script src="/static/js/alpine.min.js" defer data-cfasync="false"></script>
|
|
<style>
|
|
:root{--bg:#191919;--bg2:#1F1F1F;--bg3:#2D2D2D;--border:#333;--text:#fff;--dim:#9B9A97;--accent:#2383E2;--accent-h:#1a6bc0;--success:#0F7B6C;--danger:#E03E3E;}
|
|
*{margin:0;padding:0;box-sizing:border-box;}
|
|
body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;}
|
|
.wizard{width:560px;max-width:94vw;background:var(--bg2);border:1px solid var(--border);border-radius:16px;padding:32px;box-shadow:0 12px 40px rgba(0,0,0,.35);}
|
|
.wizard .brand{font-size:15px;font-weight:700;letter-spacing:.3px;margin-bottom:24px;display:flex;align-items:center;gap:8px;}
|
|
.wizard .brand svg{width:20px;height:20px;}
|
|
h1{font-size:22px;font-weight:700;margin-bottom:6px;}
|
|
.sub{color:var(--dim);font-size:14px;margin-bottom:26px;line-height:1.5;}
|
|
.steps{display:flex;gap:8px;margin-bottom:26px;}
|
|
.step{flex:1;height:4px;border-radius:2px;background:var(--bg3);transition:background .3s;}
|
|
.step.active{background:var(--accent);}
|
|
.step.done{background:var(--success);}
|
|
.step-label{font-size:12px;color:var(--dim);margin-bottom:8px;}
|
|
.card{background:var(--bg3);border:1px solid var(--border);border-radius:12px;padding:18px;margin-bottom:14px;cursor:pointer;transition:border-color .15s, transform .1s;}
|
|
.card:hover{border-color:var(--accent);transform:translateY(-1px);}
|
|
.card.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(35,131,226,.25);}
|
|
.card .card-icon{font-size:22px;margin-bottom:6px;}
|
|
.card .card-title{font-size:15px;font-weight:600;margin-bottom:2px;}
|
|
.card .card-desc{font-size:13px;color:var(--dim);}
|
|
.input{width:100%;padding:11px 12px;background:var(--bg);border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:14px;outline:none;margin-bottom:14px;}
|
|
.input:focus{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);}
|
|
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 18px;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;transition:background .15s;}
|
|
.btn-primary{background:var(--accent);color:#fff;}
|
|
.btn-primary:hover{background:var(--accent-h);}
|
|
.btn-ghost{background:transparent;color:var(--dim);}
|
|
.btn-ghost:hover{color:var(--text);}
|
|
.btn:disabled{opacity:.5;cursor:not-allowed;}
|
|
.actions{display:flex;justify-content:space-between;align-items:center;margin-top:8px;}
|
|
.skip{color:var(--dim);font-size:13px;cursor:pointer;text-decoration:none;}
|
|
.skip:hover{color:var(--text);}
|
|
.forge-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);}
|
|
.forge-row:last-child{border-bottom:none;}
|
|
.forge-logo{width:34px;height:34px;border-radius:9px;background:var(--bg);display:flex;align-items:center;justify-content:center;font-size:17px;}
|
|
.forge-name{font-size:14px;font-weight:600;}
|
|
.forge-status{font-size:12px;color:var(--dim);}
|
|
.badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:99px;background:rgba(46,160,67,.16);color:#2ea043;}
|
|
.badge.off{background:var(--bg3);color:var(--dim);}
|
|
.link{margin-left:auto;font-size:12px;color:var(--accent);text-decoration:none;}
|
|
.link:hover{text-decoration:underline;}
|
|
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#0F7B6C;color:#fff;padding:10px 18px;border-radius:10px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:50;display:none;}
|
|
.toast.error{background:var(--danger);}
|
|
.avatar{width:40px;height:40px;border-radius:50%;background:var(--accent);display:inline-flex;align-items:center;justify-content:center;font-weight:700;margin-right:10px;}
|
|
</style>
|
|
</head>
|
|
<body x-data="onboarding()" x-init="init()">
|
|
<div class="toast" id="toast" x-show.transition="toastMsg" x-text="toastMsg" :class="toastError ? 'error':''" x-cloak></div>
|
|
|
|
<div class="wizard">
|
|
<div class="brand">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="#2383E2" stroke-width="2"><path d="M13 2L3 14h7l-1 8 10-12h-7l1-8z"/></svg>
|
|
FlowDeck
|
|
</div>
|
|
|
|
<!-- Stepper -->
|
|
<div class="steps">
|
|
<template x-for="(s,i) in steps" :key="i">
|
|
<div class="step" :class="{ active: step===i, done: step>i }"></div>
|
|
</template>
|
|
</div>
|
|
<div class="step-label" x-text="steps[step]"></div>
|
|
|
|
<!-- Step 0: welcome -->
|
|
<section x-show="step===0">
|
|
<h1>Bienvenue 👋</h1>
|
|
<p class="sub">Configurons votre espace de travail en 3 étapes. Vous pourrez tout changer plus tard.</p>
|
|
<button class="btn btn-primary" @click="step=1">Commencer</button>
|
|
</section>
|
|
|
|
<!-- Step 1: workspace -->
|
|
<section x-show="step===1">
|
|
<h1>Créez votre espace</h1>
|
|
<p class="sub">Un workspace regroupe vos pages, bases de données et projets.</p>
|
|
<input class="input" x-model="wsName" @keydown.enter="createWorkspace()" placeholder="Ex : Équipe Frelon" maxlength="120">
|
|
<div class="actions">
|
|
<button class="btn btn-primary" @click="createWorkspace()" :disabled="!wsName.trim() || saving">Créer l'espace</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Step 2: connect forge -->
|
|
<section x-show="step===2">
|
|
<h1>Connectez vos dépôts</h1>
|
|
<p class="sub">Optionnel — syncronisez Gitea ou GitHub. Vous pouvez le faire plus tard dans Préférences → Intégrations.</p>
|
|
<div class="card" :class="{selected: forgeChoice==='gitea'}" @click="forgeChoice='gitea'">
|
|
<div style="display:flex;align-items:center;gap:14px;">
|
|
<div class="forge-logo">🔗</div>
|
|
<div style="flex:1;">
|
|
<div class="forge-name">Gitea</div>
|
|
<div class="forge-status">
|
|
<template x-if="!{{ 'true' if gitea_url_configured else 'false' }}">Non configuré par l'administrateur</template>
|
|
<template x-if="{{ 'true' if gitea_url_configured else 'false' }}">Disponible — Issues, Kanban, fichiers en sync</template>
|
|
</div>
|
|
</div>
|
|
<span class="badge" :class="giteaLinking ? 'off':''" x-text="giteaLinking ? 'Connexion…' : 'Choisir'"></span>
|
|
</div>
|
|
</div>
|
|
<div class="card" :class="{selected: forgeChoice==='github'}" @click="forgeChoice='github'">
|
|
<div style="display:flex;align-items:center;gap:14px;">
|
|
<div class="forge-logo">🐙</div>
|
|
<div style="flex:1;">
|
|
<div class="forge-name">GitHub</div>
|
|
<div class="forge-status">
|
|
<template x-if="!{{ 'true' if github_url_configured else 'false' }}">Non configuré par l'administrateur</template>
|
|
<template x-if="{{ 'true' if github_url_configured else 'false' }}">Disponible — dépôts privés & publics</template>
|
|
</div>
|
|
</div>
|
|
<span class="badge" :class="githubLinking ? 'off':''" x-text="githubLinking ? 'Connexion…' : 'Choisir'"></span>
|
|
</div>
|
|
</div>
|
|
<div class="actions">
|
|
<button class="btn btn-ghost" @click="skipForge()">Passer</button>
|
|
<button class="btn btn-primary" @click="connectForge()" :disabled="!forgeChoice">Connecter</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Step 3: first project -->
|
|
<section x-show="step===3">
|
|
<h1>Créez votre premier projet</h1>
|
|
<p class="sub">Une page d'accueil avec quelques conseils pour démarrer.</p>
|
|
<input class="input" x-model="projectTitle" @keydown.enter="createProject()" placeholder="Ex : Welcome to FlowDeck">
|
|
<div class="actions">
|
|
<button class="btn btn-ghost" @click="finish()">Passer</button>
|
|
<button class="btn btn-primary" @click="createProject()" :disabled="saving">Créer</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: onboarding »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('onboarding', onboarding); });
|
|
function onboarding() {
|
|
return {
|
|
steps: ['Bienvenue', 'Espace de travail', 'Connecter une forge', 'Premier projet'],
|
|
step: 0,
|
|
wsName: '',
|
|
forgeChoice: '',
|
|
projectTitle: 'Welcome to FlowDeck',
|
|
workspaceId: null,
|
|
saving: false,
|
|
giteaLinking: false,
|
|
githubLinking: false,
|
|
toastMsg: '',
|
|
toastError: false,
|
|
init() {
|
|
const p = new URLSearchParams(location.search);
|
|
if (p.get('linked') === '1') { this.step = 2; this.toastMsg = 'Forge connectée ✅'; setTimeout(()=>this.toastMsg='', 2500); }
|
|
},
|
|
toast(msg, err) { this.toastMsg = msg; this.toastError = !!err; setTimeout(()=>this.toastMsg='', 3000); },
|
|
async createWorkspace() {
|
|
this.saving = true;
|
|
try {
|
|
const r = await fetch('/api/onboarding/workspace', {method:'POST', headers:{'Content-Type':'application/json', 'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, body: JSON.stringify({name:this.wsName.trim()})});
|
|
const d = await r.json();
|
|
if (!r.ok) { this.toast(d.detail || 'Erreur', true); return; }
|
|
this.workspaceId = d.id;
|
|
this.step = 2;
|
|
} catch(e) { this.toast('Erreur réseau', true); }
|
|
finally { this.saving = false; }
|
|
},
|
|
connectForge() {
|
|
if (this.forgeChoice === 'gitea') { this.giteaLinking = true; window.location = '/auth/login?provider=gitea&mode=link&next=/welcome?linked=1'; }
|
|
else if (this.forgeChoice === 'github') { this.githubLinking = true; window.location = '/auth/login?provider=github&mode=link&next=/welcome?linked=1'; }
|
|
else this.skipForge();
|
|
},
|
|
skipForge() { this.forgeChoice = ''; this.step = 3; },
|
|
async createProject() {
|
|
this.saving = true;
|
|
try {
|
|
const r = await fetch('/api/onboarding/project', {method:'POST', headers:{'Content-Type':'application/json', 'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, body: JSON.stringify({title:this.projectTitle.trim(), workspace_id:this.workspaceId})});
|
|
const d = await r.json();
|
|
if (!r.ok) { this.toast(d.detail || 'Erreur', true); return; }
|
|
this.toast('Projet créé 🎉');
|
|
setTimeout(()=> window.location = '/local-workspace?ws=' + (this.workspaceId || ''), 700);
|
|
} catch(e) { this.toast('Erreur réseau', true); }
|
|
finally { this.saving = false; }
|
|
},
|
|
finish() { window.location = '/workspaces'; }
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |