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:
INTERNAL_API_BASE: http://backend:3000
API_BASE: ${API_BASE:-http://localhost:3001}
PUBLIC_DEMO_SITE: ${PUBLIC_DEMO_SITE:-false}
depends_on:
- 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 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 publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
const themes = Object.fromEntries(
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
);
@ -84,7 +92,7 @@ const { title } = Astro.props;
</div>
<div class="nav-auth" id="nav-auth">
<a href="/login">Login</a>
<a href="/register" class="btn-register">Register</a>
{!publicDemoSite ? <a href="/register" class="btn-register">Register</a> : null}
</div>
</nav>
</header>

View file

@ -6,8 +6,16 @@ interface Props {
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 publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_SITE);
const themes = Object.fromEntries(
Object.entries(themeRegistry).map(([id, t]) => [id, { isDark: t.isDark, colors: t.colors }]),
);
@ -83,7 +91,7 @@ const defaultTheme = DEFAULT_THEME;
</div>
<div class="nav-actions">
<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>
</nav>
</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 { API_BASE as apiBase } from '../lib/api';
---

View file

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

View file

@ -1,6 +1,15 @@
---
export const prerender = false;
import Layout from '../layouts/Layout.astro';
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">
@ -19,9 +28,11 @@ import { API_BASE as apiBase } from '../lib/api';
<div id="error" class="error"></div>
<button type="submit">Login</button>
</form>
{!publicDemoSite ? (
<p class="alt-action">
Don't have an account? <a href="/register">Register</a>
</p>
) : null}
</div>
</section>
</Layout>
@ -56,7 +67,8 @@ import { API_BASE as apiBase } from '../lib/api';
const result = await res.json();
localStorage.setItem('token', result.token);
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) {
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 { API_BASE as apiBase } from '../lib/api';
---

View file

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