demo: add brochure gated mode

This commit is contained in:
Marco Allegretti 2026-01-29 11:38:43 +01:00
parent 593e53a846
commit 3b091653f2
9 changed files with 118 additions and 30 deletions

View file

@ -57,6 +57,7 @@ services:
environment: environment:
INTERNAL_API_BASE: http://backend:3000 INTERNAL_API_BASE: http://backend:3000
API_BASE: ${API_BASE:-http://localhost:3001} API_BASE: ${API_BASE:-http://localhost:3001}
PUBLIC_DEMO_SITE: ${PUBLIC_DEMO_SITE:-false}
depends_on: depends_on:
- backend - backend

View file

@ -7,8 +7,16 @@ import { DEFAULT_THEME, themes as themeRegistry } from '../lib/themes';
import { API_BASE as apiBase } from '../lib/api'; import { API_BASE as apiBase } from '../lib/api';
import VotingIcons from '../components/icons/VotingIcons.astro'; import VotingIcons from '../components/icons/VotingIcons.astro';
function isEnabled(v: string | undefined): boolean {
if (!v) return false;
const n = v.trim().toLowerCase();
return n === '1' || n === 'true' || n === 'yes' || n === 'on';
}
const { title } = Astro.props; const { title } = Astro.props;
const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
const themes = Object.fromEntries( const themes = Object.fromEntries(
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]), Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
); );
@ -84,7 +92,7 @@ const { title } = Astro.props;
</div> </div>
<div class="nav-auth" id="nav-auth"> <div class="nav-auth" id="nav-auth">
<a href="/login">Login</a> <a href="/login">Login</a>
<a href="/register" class="btn-register">Register</a> {!publicDemoSite ? <a href="/register" class="btn-register">Register</a> : null}
</div> </div>
</nav> </nav>
</header> </header>

View file

@ -6,8 +6,16 @@ interface Props {
import { DEFAULT_THEME, themes as themeRegistry } from '../lib/themes'; import { DEFAULT_THEME, themes as themeRegistry } from '../lib/themes';
function isEnabled(v: string | undefined): boolean {
if (!v) return false;
const n = v.trim().toLowerCase();
return n === '1' || n === 'true' || n === 'yes' || n === 'on';
}
const { title, description = "Likwid is a modular governance engine for distributed organizations, open source communities, and civic movements." } = Astro.props; const { title, description = "Likwid is a modular governance engine for distributed organizations, open source communities, and civic movements." } = Astro.props;
const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
const themes = Object.fromEntries( const themes = Object.fromEntries(
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]), Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
); );
@ -83,7 +91,7 @@ const defaultTheme = DEFAULT_THEME;
</div> </div>
<div class="nav-actions"> <div class="nav-actions">
<a href="/demo" class="btn-demo">Explore Demo</a> <a href="/demo" class="btn-demo">Explore Demo</a>
<a href="/login" class="btn-login">Sign In</a> {!publicDemoSite ? <a href="/login" class="btn-login">Sign In</a> : null}
</div> </div>
</nav> </nav>
</header> </header>

View file

@ -0,0 +1,65 @@
import { defineMiddleware } from 'astro:middleware';
function isEnabled(v: string | undefined): boolean {
if (!v) return false;
const n = v.trim().toLowerCase();
return n === '1' || n === 'true' || n === 'yes' || n === 'on';
}
const DEMO_COOKIE = 'likwid_demo_unlocked';
export const onRequest = defineMiddleware(async (context, next) => {
const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
if (!publicDemoSite) {
return next();
}
const url = new URL(context.request.url);
const path = url.pathname;
if (path === '/demo' && url.searchParams.get('enter') === '1') {
let nextPath = url.searchParams.get('next') || '/communities';
if (!nextPath.startsWith('/')) {
nextPath = '/communities';
}
context.cookies.set(DEMO_COOKIE, '1', {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 60 * 12,
});
return context.redirect(nextPath);
}
if (path.startsWith('/_astro') || path.startsWith('/favicon') || path.startsWith('/robots')) {
return next();
}
if (path === '/setup' || path === '/register') {
return context.redirect('/demo');
}
const isProtected =
path === '/login' ||
path === '/dashboard' ||
path === '/delegations' ||
path === '/notifications' ||
path === '/settings' ||
path.startsWith('/admin') ||
path.startsWith('/communities') ||
path.startsWith('/proposals') ||
path.startsWith('/users');
if (!isProtected) {
return next();
}
const unlocked = context.cookies.get(DEMO_COOKIE)?.value === '1';
if (!unlocked) {
return context.redirect('/demo');
}
return next();
});

View file

@ -1,4 +1,5 @@
--- ---
export const prerender = false;
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api'; import { API_BASE as apiBase } from '../lib/api';
--- ---

View file

@ -1,4 +1,5 @@
--- ---
export const prerender = false;
import PublicLayout from '../layouts/PublicLayout.astro'; import PublicLayout from '../layouts/PublicLayout.astro';
import { API_BASE } from '../lib/api'; import { API_BASE } from '../lib/api';
--- ---
@ -55,7 +56,7 @@ import { API_BASE } from '../lib/api';
<div class="card-stats" id="stats-aurora"> <div class="card-stats" id="stats-aurora">
<span class="loading-text">Loading...</span> <span class="loading-text">Loading...</span>
</div> </div>
<a href="/communities/aurora" class="card-link">Explore Aurora →</a> <a href="/demo?enter=1&next=/communities/aurora" class="card-link">Explore Aurora →</a>
</div> </div>
<div class="community-card"> <div class="community-card">
@ -71,7 +72,7 @@ import { API_BASE } from '../lib/api';
<div class="card-stats" id="stats-civic-commons"> <div class="card-stats" id="stats-civic-commons">
<span class="loading-text">Loading...</span> <span class="loading-text">Loading...</span>
</div> </div>
<a href="/communities/civic-commons" class="card-link">Explore Civic Commons →</a> <a href="/demo?enter=1&next=/communities/civic-commons" class="card-link">Explore Civic Commons →</a>
</div> </div>
<div class="community-card"> <div class="community-card">
@ -87,7 +88,7 @@ import { API_BASE } from '../lib/api';
<div class="card-stats" id="stats-makers"> <div class="card-stats" id="stats-makers">
<span class="loading-text">Loading...</span> <span class="loading-text">Loading...</span>
</div> </div>
<a href="/communities/makers" class="card-link">Explore Makers →</a> <a href="/demo?enter=1&next=/communities/makers" class="card-link">Explore Makers →</a>
</div> </div>
</div> </div>
</section> </section>
@ -103,7 +104,7 @@ import { API_BASE } from '../lib/api';
Navigate communities, read proposals, view voting results, and explore Navigate communities, read proposals, view voting results, and explore
delegation networks without any account. Full read access is public. delegation networks without any account. Full read access is public.
</p> </p>
<a href="/communities" class="mode-link">Browse Communities →</a> <a href="/demo?enter=1&next=/communities" class="mode-link">Browse Communities →</a>
</div> </div>
<div class="access-mode"> <div class="access-mode">
@ -199,8 +200,8 @@ import { API_BASE } from '../lib/api';
Use demo accounts to participate. Use demo accounts to participate.
</p> </p>
<div class="cta-buttons"> <div class="cta-buttons">
<a href="/communities" class="btn-primary btn-large">Enter the Demo</a> <a href="/demo?enter=1&next=/communities" class="btn-primary btn-large">Enter the Demo</a>
<a href="/login" class="btn-secondary">Sign In with Demo Account</a> <a href="/demo?enter=1&next=/login" class="btn-secondary">Sign In with Demo Account</a>
</div> </div>
</section> </section>
@ -787,7 +788,7 @@ import { API_BASE } from '../lib/api';
} }
// Demo login handler // Demo login handler
async function handleDemoLogin(username) { async function loginDemoUser(username) {
const btn = document.querySelector('[data-username="' + username + '"]'); const btn = document.querySelector('[data-username="' + username + '"]');
if (btn) { if (btn) {
btn.classList.add('loading'); btn.classList.add('loading');
@ -810,22 +811,10 @@ import { API_BASE } from '../lib/api';
throw new Error('Login failed'); throw new Error('Login failed');
} }
const data = await response.json(); const result = await response.json();
localStorage.setItem('token', result.token);
// Store token in localStorage (use 'token' to match rest of app) localStorage.setItem('user', JSON.stringify(result.user));
localStorage.setItem('token', data.token); window.location.href = '/demo?enter=1&next=/communities';
localStorage.setItem('user', JSON.stringify(data.user));
// Update button to show success
if (btn) {
btn.querySelector('.btn-info span').textContent = 'Success! Redirecting...';
}
// Redirect to dashboard or communities
setTimeout(() => {
window.location.href = '/communities';
}, 500);
} catch (error) { } catch (error) {
console.error('Demo login failed:', error); console.error('Demo login failed:', error);
if (btn) { if (btn) {
@ -841,6 +830,8 @@ import { API_BASE } from '../lib/api';
btn.querySelector('.btn-info span').textContent = texts[username] || ''; btn.querySelector('.btn-info span').textContent = texts[username] || '';
}, 2000); }, 2000);
} }
alert('Demo login failed. Please try again.');
} }
} }
@ -852,7 +843,7 @@ import { API_BASE } from '../lib/api';
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
var username = btn.dataset.username; var username = btn.dataset.username;
if (username) { if (username) {
handleDemoLogin(username); loginDemoUser(username);
} }
}); });
})(buttons[i]); })(buttons[i]);

View file

@ -1,6 +1,15 @@
--- ---
export const prerender = false;
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api'; import { API_BASE as apiBase } from '../lib/api';
function isEnabled(v: string | undefined): boolean {
if (!v) return false;
const n = v.trim().toLowerCase();
return n === '1' || n === 'true' || n === 'yes' || n === 'on';
}
const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
--- ---
<Layout title="Login"> <Layout title="Login">
@ -19,9 +28,11 @@ import { API_BASE as apiBase } from '../lib/api';
<div id="error" class="error"></div> <div id="error" class="error"></div>
<button type="submit">Login</button> <button type="submit">Login</button>
</form> </form>
{!publicDemoSite ? (
<p class="alt-action"> <p class="alt-action">
Don't have an account? <a href="/register">Register</a> Don't have an account? <a href="/register">Register</a>
</p> </p>
) : null}
</div> </div>
</section> </section>
</Layout> </Layout>
@ -56,7 +67,8 @@ import { API_BASE as apiBase } from '../lib/api';
const result = await res.json(); const result = await res.json();
localStorage.setItem('token', result.token); localStorage.setItem('token', result.token);
localStorage.setItem('user', JSON.stringify(result.user)); localStorage.setItem('user', JSON.stringify(result.user));
window.location.href = '/'; const next = new URLSearchParams(window.location.search).get('next') || '/communities';
window.location.href = next;
} catch (err) { } catch (err) {
errorEl.textContent = 'Connection error. Is the backend running?'; errorEl.textContent = 'Connection error. Is the backend running?';
} }

View file

@ -1,4 +1,5 @@
--- ---
export const prerender = false;
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api'; import { API_BASE as apiBase } from '../lib/api';
--- ---

View file

@ -1,4 +1,5 @@
--- ---
export const prerender = false;
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import { API_BASE as apiBase } from '../lib/api'; import { API_BASE as apiBase } from '../lib/api';
--- ---