mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(app): unified demo mode + public-demo build for Vercel
- Central demo-mode helper (lib/demo-mode.ts): ?demo=1 query flag plus
VITE_PUBLIC_DEMO build flag that makes every route demo for the
public Vercel showcase.
- Single demo fixture source (lib/demo-data.ts): projects, services,
builds, databases, cron, HA, security, host monitoring, templates.
Fixes cross-page ID drift that broke project navigation.
- Demo support on Projects, HA, Security, Usage, Templates, Landing,
Service detail; DemoRestricted gate on People/Settings/Audit/Admin.
- Demo-aware PlatformShell: nav filtering, env:demo, demo account menu,
?demo=1 preserved through internal links.
- TemplatesPage: selection moved into ?template= URL param for deep
links; template fixtures shared via demo-data.
- requestJson returns {} for unparseable 200 bodies (SPA fallback) so
envelope unwraps degrade instead of dereferencing null.
- Recharts ResponsiveContainer initialDimension kills -1 warnings.
- vercel.json: vite framework + SPA rewrites for direct deep links.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
84b102a2c6
commit
5b0a8e0759
20 files changed
+873
-525
No files matched your search
@@ -424,7 +424,10 @@ async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
throw new ApiError(message, response.status);
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
// SPA fallback servers may answer /api/* with index.html — an ok response
|
||||
// whose body is not JSON. Surface an empty object so envelope unwraps
|
||||
// (`payload.x ?? []`) degrade instead of dereferencing null.
|
||||
return (payload ?? {}) as T;
|
||||
}
|
||||
|
||||
async function requestText(path: string, init?: RequestInit): Promise<string> {
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
import type { CronJobEntity, DatabaseEntity, ProjectEntity, ServiceEntity } from '@/lib/api-client';
|
||||
import type {
|
||||
BuildEntity,
|
||||
CronJobEntity,
|
||||
DatabaseEntity,
|
||||
FailoverPolicy,
|
||||
HAAlert,
|
||||
HAHealthResult,
|
||||
HAStatus,
|
||||
HostMonitoring,
|
||||
ProjectEntity,
|
||||
SecurityMetrics,
|
||||
SecurityScan,
|
||||
ServiceEntity,
|
||||
TemplateDetailEntity,
|
||||
TemplateEntity,
|
||||
Vulnerability,
|
||||
} from '@/lib/api-client';
|
||||
import type { ServiceVariable } from '@/features/workspace/auto-connections';
|
||||
|
||||
export const demoProjects: ProjectEntity[] = [
|
||||
@@ -332,3 +348,553 @@ export const demoCronJobsByService: Record<string, CronJobEntity[]> = {
|
||||
export function getDemoCronJobsByService(serviceId: string): CronJobEntity[] {
|
||||
return demoCronJobsByService[serviceId] ?? [];
|
||||
}
|
||||
|
||||
export const demoBuilds: BuildEntity[] = [
|
||||
{
|
||||
id: 'demo-build-api',
|
||||
projectId: 'demo-project-core',
|
||||
serviceId: 'demo-svc-api',
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
startedAt: new Date(Date.now() - 20 * 60_000).toISOString(),
|
||||
completedAt: new Date(Date.now() - 18 * 60_000).toISOString(),
|
||||
imageName: 'ghcr.io/containr/api-gateway',
|
||||
imageTag: 'sha-1f2e3d4',
|
||||
size: 156_000_000,
|
||||
log: '[demo] Build finished successfully.',
|
||||
metadata: { branch: 'main' },
|
||||
},
|
||||
{
|
||||
id: 'demo-build-worker',
|
||||
projectId: 'demo-project-core',
|
||||
serviceId: 'demo-svc-worker',
|
||||
status: 'running',
|
||||
progress: 54,
|
||||
startedAt: new Date(Date.now() - 2 * 60_000).toISOString(),
|
||||
imageName: 'ghcr.io/containr/queue-worker',
|
||||
imageTag: 'sha-9a8b7c6',
|
||||
size: 0,
|
||||
log: '[demo] Building image layers...',
|
||||
metadata: { branch: 'feature/queue' },
|
||||
},
|
||||
{
|
||||
id: 'demo-build-site',
|
||||
projectId: 'demo-project-growth',
|
||||
serviceId: 'demo-growth-site',
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
startedAt: new Date(Date.now() - 3 * 3_600_000).toISOString(),
|
||||
completedAt: new Date(Date.now() - 3 * 3_600_000 + 95_000).toISOString(),
|
||||
imageName: 'ghcr.io/containr/marketing-site',
|
||||
imageTag: 'sha-77aa1c0',
|
||||
size: 89_000_000,
|
||||
log: '[demo] Build finished successfully.',
|
||||
metadata: { branch: 'main' },
|
||||
},
|
||||
{
|
||||
id: 'demo-build-ml',
|
||||
projectId: 'demo-project-ml',
|
||||
serviceId: 'demo-ml-worker',
|
||||
status: 'failed',
|
||||
progress: 71,
|
||||
startedAt: new Date(Date.now() - 26 * 3_600_000).toISOString(),
|
||||
completedAt: new Date(Date.now() - 26 * 3_600_000 + 240_000).toISOString(),
|
||||
imageName: 'ghcr.io/containr/batch-evaluator',
|
||||
imageTag: 'sha-3d4e5f6',
|
||||
size: 0,
|
||||
error: 'pip install failed: could not resolve torch==2.4.0',
|
||||
log: '[demo] ERROR: dependency resolution failed.',
|
||||
metadata: { branch: 'experiment/eval-v2' },
|
||||
},
|
||||
];
|
||||
|
||||
export const demoHostMonitoring: HostMonitoring = {
|
||||
hostname: 'containr-node-1',
|
||||
os: 'linux',
|
||||
architecture: 'amd64',
|
||||
cpu: { cores: 16 },
|
||||
memory: {
|
||||
total: 13.5 * 1024 * 1024 * 1024,
|
||||
used: 9.8 * 1024 * 1024 * 1024,
|
||||
available: 3.7 * 1024 * 1024 * 1024,
|
||||
usagePercent: 72,
|
||||
},
|
||||
storage: {
|
||||
path: '/var/lib/containr',
|
||||
total: 465 * 1024 * 1024 * 1024,
|
||||
used: 338 * 1024 * 1024 * 1024,
|
||||
available: 127 * 1024 * 1024 * 1024,
|
||||
usagePercent: 73,
|
||||
},
|
||||
load: { load1m: 2.2, load5m: 2.6, load15m: 2.4 },
|
||||
uptimeSeconds: 1_828_800,
|
||||
dockerAvailable: true,
|
||||
docker: { containers: 11, images: 34, driver: 'overlay2', server: '27.3.1' },
|
||||
collectedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
export const demoHAStatus: HAStatus = {
|
||||
enabled: true,
|
||||
nodes: { total: 3, healthy: 3, unhealthy: 0 },
|
||||
health_checks: { total: 6, healthy: 5, unhealthy: 1 },
|
||||
alerts: { active: 1 },
|
||||
};
|
||||
|
||||
export const demoHAAlerts: HAAlert[] = [
|
||||
{
|
||||
id: 'demo-alert-worker-heartbeat',
|
||||
rule_id: 'health-check-failing',
|
||||
status: 'firing',
|
||||
severity: 'warning',
|
||||
message: 'Health check failing for demo-svc-worker — 2 consecutive misses',
|
||||
starts_at: new Date(Date.now() - 14 * 60_000).toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const demoHAPolicies: FailoverPolicy[] = [
|
||||
{
|
||||
service_id: 'demo-svc-api',
|
||||
enabled: true,
|
||||
failover_strategy: 'active_passive',
|
||||
min_healthy_nodes: 1,
|
||||
max_failures: 3,
|
||||
},
|
||||
{
|
||||
service_id: 'demo-svc-postgres',
|
||||
enabled: true,
|
||||
failover_strategy: 'graceful',
|
||||
min_healthy_nodes: 1,
|
||||
max_failures: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const demoHAHealthResults: HAHealthResult[] = [
|
||||
{
|
||||
check_id: 'demo-svc-api:http',
|
||||
status: 'healthy',
|
||||
message: 'GET /healthz returned 200',
|
||||
latency: 42_000_000,
|
||||
timestamp: new Date(Date.now() - 30_000).toISOString(),
|
||||
},
|
||||
{
|
||||
check_id: 'demo-svc-postgres:tcp',
|
||||
status: 'healthy',
|
||||
message: 'TCP connect to :5432 succeeded',
|
||||
latency: 8_000_000,
|
||||
timestamp: new Date(Date.now() - 45_000).toISOString(),
|
||||
},
|
||||
{
|
||||
check_id: 'demo-svc-worker:exec',
|
||||
status: 'unhealthy',
|
||||
message: 'Heartbeat file stale — last write 4m ago',
|
||||
latency: 0,
|
||||
timestamp: new Date(Date.now() - 60_000).toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const demoSecurityMetrics: SecurityMetrics = {
|
||||
security_score: 86,
|
||||
vulnerabilities: { total: 5, critical: 0, high: 1, medium: 2, low: 2, open: 3, resolved: 2 },
|
||||
latest_scan: {
|
||||
id: 'demo-scan-latest',
|
||||
score: 86,
|
||||
status: 'completed',
|
||||
scanned_at: new Date(Date.now() - 5 * 3_600_000).toISOString(),
|
||||
},
|
||||
compliance: { overall_status: 'assessed', score: 92, last_assessed: new Date(Date.now() - 2 * 86_400_000).toISOString() },
|
||||
};
|
||||
|
||||
export const demoVulnerabilities: Vulnerability[] = [
|
||||
{
|
||||
id: 'demo-vuln-1',
|
||||
type: 'dependency',
|
||||
severity: 'high',
|
||||
title: 'openssl 3.0.13 — CVE-2024-5535',
|
||||
description: 'API Gateway base image ships an openssl release with a known TLS session-ticket flaw.',
|
||||
service_id: 'demo-svc-api',
|
||||
project_id: 'demo-project-core',
|
||||
status: 'open',
|
||||
found_at: new Date(Date.now() - 5 * 3_600_000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-vuln-2',
|
||||
type: 'configuration',
|
||||
severity: 'medium',
|
||||
title: 'Postgres listens on 0.0.0.0',
|
||||
description: 'Database port is bound on all interfaces; restrict to the internal network.',
|
||||
service_id: 'demo-svc-postgres',
|
||||
project_id: 'demo-project-core',
|
||||
status: 'open',
|
||||
found_at: new Date(Date.now() - 5 * 3_600_000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-vuln-3',
|
||||
type: 'dependency',
|
||||
severity: 'low',
|
||||
title: 'express 4.19.x — outdated minor',
|
||||
description: 'A newer patch release is available for the web frontend dependency.',
|
||||
service_id: 'demo-svc-web',
|
||||
project_id: 'demo-project-core',
|
||||
status: 'open',
|
||||
found_at: new Date(Date.now() - 2 * 86_400_000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-vuln-4',
|
||||
type: 'dependency',
|
||||
severity: 'medium',
|
||||
title: 'redis 7.2.3 — CVE-2024-31227',
|
||||
description: 'Resolved by upgrading the cache image to 7.2.5.',
|
||||
service_id: 'demo-growth-cache',
|
||||
project_id: 'demo-project-growth',
|
||||
status: 'resolved',
|
||||
found_at: new Date(Date.now() - 9 * 86_400_000).toISOString(),
|
||||
resolved_at: new Date(Date.now() - 6 * 86_400_000).toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const demoSecurityScans: SecurityScan[] = [
|
||||
{
|
||||
id: 'demo-scan-1',
|
||||
project_id: 'demo-project-core',
|
||||
scan_type: 'comprehensive',
|
||||
status: 'completed',
|
||||
findings_count: 3,
|
||||
started_at: new Date(Date.now() - 5 * 3_600_000).toISOString(),
|
||||
completed_at: new Date(Date.now() - 5 * 3_600_000 + 48_000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-scan-2',
|
||||
project_id: 'demo-project-core',
|
||||
service_id: 'demo-svc-api',
|
||||
scan_type: 'dependency',
|
||||
status: 'completed',
|
||||
findings_count: 1,
|
||||
started_at: new Date(Date.now() - 2 * 86_400_000).toISOString(),
|
||||
completed_at: new Date(Date.now() - 2 * 86_400_000 + 31_000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'demo-scan-3',
|
||||
project_id: 'demo-project-core',
|
||||
scan_type: 'configuration',
|
||||
status: 'failed',
|
||||
findings_count: 0,
|
||||
started_at: new Date(Date.now() - 4 * 86_400_000).toISOString(),
|
||||
completed_at: new Date(Date.now() - 4 * 86_400_000 + 12_000).toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const demoTemplates: TemplateEntity[] = [
|
||||
{
|
||||
id: 'tpl-react',
|
||||
name: 'React Application',
|
||||
description: 'Single-page frontend with Vite and static serving runtime.',
|
||||
category: 'frontend',
|
||||
logo: 'https://cdn.simpleicons.org/react',
|
||||
configRaw: '{"runtime":"node"}',
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
},
|
||||
{
|
||||
id: 'tpl-go',
|
||||
name: 'Go API Service',
|
||||
description: 'API-ready Go runtime with direct binary startup.',
|
||||
category: 'web',
|
||||
logo: 'https://cdn.simpleicons.org/go',
|
||||
configRaw: '{"runtime":"go"}',
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
},
|
||||
{
|
||||
id: 'tpl-postgres',
|
||||
name: 'PostgreSQL Database',
|
||||
description: 'Managed PostgreSQL service with credential setup variables.',
|
||||
category: 'database',
|
||||
logo: 'https://cdn.simpleicons.org/postgresql',
|
||||
configRaw: '{"runtime":"postgres"}',
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
},
|
||||
{
|
||||
id: 'tpl-mysql',
|
||||
name: 'MySQL Database',
|
||||
description: 'Managed MySQL service for transactional workloads.',
|
||||
category: 'database',
|
||||
logo: 'https://cdn.simpleicons.org/mysql',
|
||||
configRaw: '{"runtime":"mysql"}',
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
},
|
||||
{
|
||||
id: 'tpl-mariadb',
|
||||
name: 'MariaDB Database',
|
||||
description: 'Managed MariaDB service with MySQL compatibility.',
|
||||
category: 'database',
|
||||
logo: 'https://cdn.simpleicons.org/mariadb',
|
||||
configRaw: '{"runtime":"mariadb"}',
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
},
|
||||
{
|
||||
id: 'tpl-clickhouse',
|
||||
name: 'ClickHouse Database',
|
||||
description: 'Columnar analytics database template for high-speed queries.',
|
||||
category: 'database',
|
||||
logo: 'https://cdn.simpleicons.org/clickhouse',
|
||||
configRaw: '{"runtime":"clickhouse"}',
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
},
|
||||
{
|
||||
id: 'tpl-dragonfly',
|
||||
name: 'Dragonfly Database',
|
||||
description: 'Redis-compatible in-memory store powered by Dragonfly.',
|
||||
category: 'database',
|
||||
logo: 'https://cdn.simpleicons.org/redis',
|
||||
configRaw: '{"runtime":"dragonfly"}',
|
||||
variablesRaw: '[]',
|
||||
isOfficial: true,
|
||||
ownerId: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const demoTemplateDetails: Record<string, TemplateDetailEntity> = {
|
||||
'tpl-react': {
|
||||
template: demoTemplates[0],
|
||||
config: {
|
||||
type: 'web',
|
||||
runtime: 'node',
|
||||
buildCommand: 'npm install && npm run build',
|
||||
startCommand: 'npx serve -s dist',
|
||||
port: 3000,
|
||||
healthCheck: '/health',
|
||||
environment: {},
|
||||
nixpacksConfig: {},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
key: 'VITE_API_URL',
|
||||
label: 'API URL',
|
||||
defaultValue: 'https://api.example.com',
|
||||
required: true,
|
||||
secret: false,
|
||||
description: 'Public API endpoint for frontend calls',
|
||||
},
|
||||
],
|
||||
},
|
||||
'tpl-go': {
|
||||
template: demoTemplates[1],
|
||||
config: {
|
||||
type: 'web',
|
||||
runtime: 'go',
|
||||
buildCommand: 'go build -o app .',
|
||||
startCommand: './app',
|
||||
port: 8080,
|
||||
healthCheck: '/health',
|
||||
environment: {},
|
||||
nixpacksConfig: {},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
key: 'GO_ENV',
|
||||
label: 'Go Environment',
|
||||
defaultValue: 'production',
|
||||
required: false,
|
||||
secret: false,
|
||||
description: 'Runtime environment value',
|
||||
},
|
||||
],
|
||||
},
|
||||
'tpl-postgres': {
|
||||
template: demoTemplates[2],
|
||||
config: {
|
||||
type: 'database',
|
||||
runtime: 'postgres',
|
||||
buildCommand: '',
|
||||
startCommand: '',
|
||||
port: 5432,
|
||||
healthCheck: '',
|
||||
environment: {},
|
||||
nixpacksConfig: {},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
key: 'POSTGRES_USER',
|
||||
label: 'Username',
|
||||
defaultValue: 'postgres',
|
||||
required: true,
|
||||
secret: false,
|
||||
description: 'Database user',
|
||||
},
|
||||
{
|
||||
key: 'POSTGRES_PASSWORD',
|
||||
label: 'Password',
|
||||
defaultValue: '',
|
||||
required: true,
|
||||
secret: true,
|
||||
description: 'Database password',
|
||||
},
|
||||
],
|
||||
},
|
||||
'tpl-mysql': {
|
||||
template: demoTemplates[3],
|
||||
config: {
|
||||
type: 'database',
|
||||
runtime: 'mysql',
|
||||
buildCommand: '',
|
||||
startCommand: '',
|
||||
port: 3306,
|
||||
healthCheck: '',
|
||||
environment: {},
|
||||
nixpacksConfig: {},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
key: 'MYSQL_DATABASE',
|
||||
label: 'Database Name',
|
||||
defaultValue: 'app',
|
||||
required: true,
|
||||
secret: false,
|
||||
description: 'Initial database to create',
|
||||
},
|
||||
{
|
||||
key: 'MYSQL_USER',
|
||||
label: 'Username',
|
||||
defaultValue: 'app',
|
||||
required: true,
|
||||
secret: false,
|
||||
description: 'Application DB user',
|
||||
},
|
||||
{
|
||||
key: 'MYSQL_PASSWORD',
|
||||
label: 'User Password',
|
||||
defaultValue: '',
|
||||
required: true,
|
||||
secret: true,
|
||||
description: 'Application DB password',
|
||||
},
|
||||
{
|
||||
key: 'MYSQL_ROOT_PASSWORD',
|
||||
label: 'Root Password',
|
||||
defaultValue: '',
|
||||
required: true,
|
||||
secret: true,
|
||||
description: 'Root account password',
|
||||
},
|
||||
],
|
||||
},
|
||||
'tpl-mariadb': {
|
||||
template: demoTemplates[4],
|
||||
config: {
|
||||
type: 'database',
|
||||
runtime: 'mariadb',
|
||||
buildCommand: '',
|
||||
startCommand: '',
|
||||
port: 3306,
|
||||
healthCheck: '',
|
||||
environment: {},
|
||||
nixpacksConfig: {},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
key: 'MARIADB_DATABASE',
|
||||
label: 'Database Name',
|
||||
defaultValue: 'app',
|
||||
required: true,
|
||||
secret: false,
|
||||
description: 'Initial database to create',
|
||||
},
|
||||
{
|
||||
key: 'MARIADB_USER',
|
||||
label: 'Username',
|
||||
defaultValue: 'app',
|
||||
required: true,
|
||||
secret: false,
|
||||
description: 'Application DB user',
|
||||
},
|
||||
{
|
||||
key: 'MARIADB_PASSWORD',
|
||||
label: 'User Password',
|
||||
defaultValue: '',
|
||||
required: true,
|
||||
secret: true,
|
||||
description: 'Application DB password',
|
||||
},
|
||||
{
|
||||
key: 'MARIADB_ROOT_PASSWORD',
|
||||
label: 'Root Password',
|
||||
defaultValue: '',
|
||||
required: true,
|
||||
secret: true,
|
||||
description: 'Root account password',
|
||||
},
|
||||
],
|
||||
},
|
||||
'tpl-clickhouse': {
|
||||
template: demoTemplates[5],
|
||||
config: {
|
||||
type: 'database',
|
||||
runtime: 'clickhouse',
|
||||
buildCommand: '',
|
||||
startCommand: '',
|
||||
port: 8123,
|
||||
healthCheck: '',
|
||||
environment: {},
|
||||
nixpacksConfig: {},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
key: 'CLICKHOUSE_DB',
|
||||
label: 'Database Name',
|
||||
defaultValue: 'app',
|
||||
required: false,
|
||||
secret: false,
|
||||
description: 'Default database name',
|
||||
},
|
||||
{
|
||||
key: 'CLICKHOUSE_USER',
|
||||
label: 'Username',
|
||||
defaultValue: 'default',
|
||||
required: false,
|
||||
secret: false,
|
||||
description: 'ClickHouse user',
|
||||
},
|
||||
{
|
||||
key: 'CLICKHOUSE_PASSWORD',
|
||||
label: 'Password',
|
||||
defaultValue: '',
|
||||
required: false,
|
||||
secret: true,
|
||||
description: 'ClickHouse password',
|
||||
},
|
||||
],
|
||||
},
|
||||
'tpl-dragonfly': {
|
||||
template: demoTemplates[6],
|
||||
config: {
|
||||
type: 'database',
|
||||
runtime: 'dragonfly',
|
||||
buildCommand: '',
|
||||
startCommand: '',
|
||||
port: 6379,
|
||||
healthCheck: '',
|
||||
environment: {},
|
||||
nixpacksConfig: {},
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
key: 'DRAGONFLY_PASSWORD',
|
||||
label: 'Password',
|
||||
defaultValue: '',
|
||||
required: false,
|
||||
secret: true,
|
||||
description: 'Optional Redis-compatible password',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Build-time flag for public demo deployments that ship without a backend
|
||||
* (e.g. the Vercel showcase). When enabled every route behaves as if
|
||||
* `?demo=1` was passed — fixtures render instead of API calls.
|
||||
*/
|
||||
export const PUBLIC_DEMO = (import.meta.env.VITE_PUBLIC_DEMO ?? '') === '1';
|
||||
|
||||
export function isDemoSearch(search: string): boolean {
|
||||
return PUBLIC_DEMO || new URLSearchParams(search).get('demo') === '1';
|
||||
}
|
||||
|
||||
export function useDemoMode(): boolean {
|
||||
const [searchParams] = useSearchParams();
|
||||
return PUBLIC_DEMO || searchParams.get('demo') === '1';
|
||||
}
|
||||
Reference in new issue
Block a user