Skip to content

Commit b29196a

Browse files
Replace the entry form with a cinematic intro
The /app entry was a one-tap profile form that read as a signup wall and leaked most first-time visitors. Replace it with a short, skippable cosmic cinematic (the portal circles, the Seeker arrives, the figures fade one by one, then the Paradiso rose), and move profile creation, legal consent, and the entry/signup/conversion beacons into the welcome step where they belong. - Profile defaults to the Seeker, changeable later in settings. - The welcome modal is now header, tagline, trust line, Campbell, and the AI disclaimer; the carousel and the extra prose are gone. - Scope the AI disclaimer to the figures' generated content, live or prepared in advance, so it stays accurate. - Logout returns to the homepage instead of dropping back into the intro. The profile_created conversion and its gclid capture are unchanged; only the call site moved, and it still ships in the bundle.
1 parent 776cbea commit b29196a

12 files changed

Lines changed: 327 additions & 420 deletions

File tree

client/src/App.tsx

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import NutzungsbedingungenPage from './pages/NutzungsbedingungenPage';
2121
// CF Pages serves the prerendered HTML for /figures/*, /themes/*, /about,
2222
// /contact directly; this React SPA only handles `/`, `/de/`, and the legal
2323
// pages (Impressum, Datenschutz, Cookie Policy, Nutzungsbedingungen).
24-
import { sendConversion } from './utils/public/gclidCapture';
25-
import { sendEntryBeacon } from './utils/entryBeacon';
26-
import { sendSignupBeacon } from './utils/signupBeacon';
24+
// Entry/signup beacons + the profile_created conversion now fire from the
25+
// WelcomeDisclosureModal "Begin" (where profile + consent complete), not here.
26+
// captureGclid() still runs in index.tsx; sendConversion reads sessionStorage.
2727
// Dev/test pages — only imported in dev mode so Vite excludes them from production build.
2828
// Kept lean: contributor-facing diagnostics and benchmarks only. Internal A/B harnesses
2929
// and visual-design experiments live in the private workspace.
@@ -335,64 +335,35 @@ function App(): React.ReactElement {
335335
document.body.style.pointerEvents = 'auto';
336336
};
337337

338-
// Check if this is a first-time login
339-
const hasAnyHistory = LocalStorageAdapter.keys().some((key) => key.startsWith('history_'));
340-
const hasSelectedFigure = LocalStorageAdapter.getString('selectedFigure');
341-
const { complete: onboardingCompletedFlag, skipped: onboardingSkippedFlag } = useDomainStore.getState().onboarding;
342-
const onboardingCompleted = onboardingCompletedFlag ? 'true' : null;
343-
const onboardingSkipped = onboardingSkippedFlag ? 'true' : null;
344-
345-
const isFirstLogin = !hasAnyHistory && !hasSelectedFigure && !onboardingCompleted && !onboardingSkipped;
338+
// The cinematic only plays for visitors without a stored profile, so they
339+
// all need the welcome step next. Profile creation, consent, the entry /
340+
// signup beacons, and the profile_created conversion now live there (the
341+
// WelcomeDisclosureModal "Begin"); this just opens it.
342+
sessionStorage.setItem('showOnboarding', 'true');
346343

347-
// If this is the first login, force the onboarding to show
348-
if (isFirstLogin) {
349-
sessionStorage.setItem('showOnboarding', 'true');
350-
}
351-
352344
// Clean up before setting logged in state
353345
cleanupOverlays();
354-
346+
355347
// Set login state (persist hint for page refresh)
356348
LocalStorageAdapter.setString('isLoggedIn', 'true');
357349
setIsLoggedIn(true);
358350

359-
// Anonymous entry beacon — closes the bounce stage between page-load
360-
// beacon (arrival) and session row (first Turnstile-gated interaction).
361-
sendEntryBeacon();
362-
363-
// Anonymous signup beacon — fires only for new accounts (isFirstLogin),
364-
// so the dashboard can show total signups including organic users (the
365-
// gclid-gated profile_created conversion shows ad-attributed ones only).
366-
if (isFirstLogin) {
367-
sendSignupBeacon();
368-
}
369-
370-
// Send conversion event if user came from a Google Ad
371-
sendConversion('profile_created');
372-
373351
// Clean up again after a short delay (in case React rerenders add new elements)
374352
setTimeout(cleanupOverlays, 100);
375353
};
376354

377355
const handleLogout = (): void => {
378-
// For production, we want to preserve user state between sessions
379-
// Remove profile from IndexedDB when logging out
380-
preferencesIndexedDbAdapter.deleteUserProfile().catch(err => {
381-
console.error('Failed to delete profile:', err);
382-
});
383-
384-
// Update app state (clear localStorage hint)
356+
// Clear the session, then send the user back to the public homepage.
357+
// The entry page is now a cinematic (no profile form), so dropping back
358+
// into it on logout would loop oddly — agoracosmica.org is the right place
359+
// to land. Delete the profile first, then navigate.
385360
LocalStorageAdapter.remove('isLoggedIn');
386-
setIsLoggedIn(false);
387-
388-
// Note: We intentionally preserve the following between sessions:
389-
// - Conversation history
390-
// - Selected figure and seeds
391-
// - Completed onboarding status
392-
// - User preferences and settings
393-
394-
// This ensures returning users see their previous conversations
395-
// and don't have to redo onboarding when they log back in
361+
preferencesIndexedDbAdapter.deleteUserProfile()
362+
.catch((err) => console.error('Failed to delete profile:', err))
363+
.finally(() => { window.location.href = '/'; });
364+
365+
// Conversation history, selected figure, onboarding status, and preferences
366+
// are intentionally preserved between sessions.
396367
};
397368

398369
// Create router with future flags — memoized to prevent recreation on unrelated re-renders

client/src/assets/translations/ui-de.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
"explainer": "Lerne von 30 großen Persönlichkeiten der Geschichte.",
8080
"access": "Ohne Anmeldung. 30 kostenlose Nachrichten pro Tag.",
8181
"trustLine": "Gemeinnützig · Open Source · Kein Nutzer-Tracking",
82-
"identityLabel": "Tritt ein, wie du bist.",
82+
"identityLabel": "Nur ein Name, dann los.",
8383
"motto": {
8484
"dante": "Durch dunkle Wälder, ans Licht der Sterne.",
8585
"beatrice": "Erkenntnis, wie Licht, erhellt den Weg.",
@@ -98,7 +98,11 @@
9898
"formReady": "Das Profilformular ist jetzt verfügbar",
9999
"reassure": "Dein Name bleibt auf diesem Gerät. Kein Konto, keine E-Mail.",
100100
"formAria": "Wähle, wie du erscheinst",
101-
"switchLang": "Zu Englisch wechseln"
101+
"cinematic": {
102+
"card1": "Jede Frage beginnt im Dunkeln.",
103+
"card2": "Vor dir haben viele gefragt.",
104+
"card3": "Sie erheben sich, um dir zu begegnen."
105+
}
102106
},
103107
"login": {
104108
"title": "Willkommen zurück",
@@ -1275,7 +1279,7 @@
12751279
},
12761280
"consent": {
12771281
"heading": "Bevor du loslegst",
1278-
"aiNotice": "Diese Anwendung wird vollständig von künstlicher Intelligenz (KI) betrieben. Du interagierst NICHT mit echten historischen Persönlichkeiten. Alle Inhalte werden automatisch von einem KI-System generiert und können sachlich unrichtig, unvollständig oder irreführend sein. Überprüfe wichtige Informationen immer mit Primärquellen.",
1282+
"aiNotice": "Die Persönlichkeiten hier sind KI-Interpretationen, nicht die echten historischen Menschen. Alles, was sie sagen, ob im Gespräch oder vorab erstellt, wird von künstlicher Intelligenz (KI) erzeugt und kann ungenau, unvollständig oder irreführend sein. Überprüfe wichtige Informationen immer anhand vertrauenswürdiger Primärquellen.",
12791283
"ageCheckbox": "Ich bestätige, dass ich mindestens 16 Jahre alt bin oder die Zustimmung eines Erziehungsberechtigten habe.",
12801284
"termsCheckbox": "Ich habe die",
12811285
"termsLink": "Nutzungsbedingungen",

client/src/assets/translations/ui-en.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"explainer": "Learn from 30 remarkable people from history.",
8282
"access": "No signup. 30 free messages a day.",
8383
"trustLine": "Nonprofit · Open Source · No User Tracking",
84-
"identityLabel": "Step in as you are.",
84+
"identityLabel": "Just a name to begin.",
8585
"motto": {
8686
"dante": "Through dark woods, into the light of the stars.",
8787
"beatrice": "Knowledge, like light, illuminates the way.",
@@ -100,7 +100,11 @@
100100
"formReady": "Profile form is now available",
101101
"reassure": "Your name stays on this device. No account, no email.",
102102
"formAria": "Choose how you appear",
103-
"switchLang": "Switch to German"
103+
"cinematic": {
104+
"card1": "Every question begins in the dark.",
105+
"card2": "You are not the first to ask.",
106+
"card3": "Those who came before rise to meet you."
107+
}
104108
},
105109
"login": {
106110
"title": "Welcome Back",
@@ -1273,7 +1277,7 @@
12731277
},
12741278
"consent": {
12751279
"heading": "Before you begin",
1276-
"aiNotice": "This application is fully operated by artificial intelligence (AI). You are NOT interacting with real historical figures. All content is generated automatically by an AI system and may be inaccurate, incomplete, or misleading. Always verify important information with primary sources.",
1280+
"aiNotice": "The figures here are AI interpretations, not the real historical people. Everything they say, whether in live conversation or prepared in advance, is generated by artificial intelligence (AI) and can be inaccurate, incomplete, or misleading. Always verify anything important against trusted primary sources.",
12771281
"ageCheckbox": "I confirm that I am at least 16 years old or have the consent of a parent or guardian.",
12781282
"termsCheckbox": "I have read and accept the",
12791283
"termsLink": "Terms of Service",
@@ -1652,7 +1656,7 @@
16521656
},
16531657
"quote": "Follow your bliss and the universe will open doors where there were only walls.",
16541658
"quoteAttribution": "— Joseph Campbell",
1655-
"trustSignals": "Non-profit · Open Source · No User Tracking",
1659+
"trustSignals": "Nonprofit · Open Source · No User Tracking",
16561660
"button": "Ignite Your Stars",
16571661
"seekerText": "Questions, like stars, shine within you.",
16581662
"seekerAlt": "The Seeker - your cosmic self with infinite potential within"

client/src/components/CharacterLoginForm.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,6 @@ const CharacterLoginForm: FC<CharacterLoginFormProps> = ({ onComplete, beatriceI
145145
margin: 0 0 0.45rem;
146146
}
147147
148-
.cs-reassure {
149-
text-align: center;
150-
color: color-mix(in srgb, var(--text-primary) 42%, transparent);
151-
font-size: 0.62rem;
152-
line-height: 1.35;
153-
letter-spacing: 0.02em;
154-
margin: 0.3rem auto 0;
155-
max-width: 22rem;
156-
}
157-
158148
.cs-selector {
159149
display: flex;
160150
align-items: center;
@@ -341,11 +331,6 @@ const CharacterLoginForm: FC<CharacterLoginFormProps> = ({ onComplete, beatriceI
341331
/>
342332
</div>
343333

344-
{/* On-device reassurance, right where the "is this a signup?" doubt is
345-
born. Resolves the tension with the No User Tracking promise instead
346-
of contradicting it. */}
347-
<p className="cs-reassure">{tNode('entry.reassure')}</p>
348-
349334
{/* Submit */}
350335
<div className="form-actions">
351336
<button type="submit" className="enter-button" disabled={!displayName.trim()}>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* CinematicCards — the parallel verse story for the entry "Vorspann".
3+
*
4+
* Sits above the ring and unfolds line by line across the whole sequence:
5+
* card 1 while the portal circles, card 2 as the Seeker arrives, card 3 while
6+
* the figures fade — holding until the rose takes over. Each line floats up and
7+
* glows gold, matching the Paradiso rose's Dante quote. The Dante line itself
8+
* lives in the rose, so this layer owns cards 1–3.
9+
*
10+
* `step` is driven by the page timeline so the words sync to the visuals.
11+
*/
12+
import { FC } from 'react';
13+
import { useTranslation } from '../hooks/useTranslation';
14+
15+
interface CinematicCardsProps {
16+
active: boolean;
17+
step: number; // 0 = portal, 1 = Seeker, 2 = figures fading
18+
}
19+
20+
const CARD_KEYS = ['entry.cinematic.card1', 'entry.cinematic.card2', 'entry.cinematic.card3'];
21+
22+
const CinematicCards: FC<CinematicCardsProps> = ({ active, step }) => {
23+
const { tString } = useTranslation();
24+
if (!active) return null;
25+
26+
const text = tString(CARD_KEYS[step] || CARD_KEYS[0], '');
27+
28+
return (
29+
<div className="cine-stage" aria-hidden="true">
30+
<style>{`
31+
.cine-stage {
32+
position: absolute;
33+
left: 50%;
34+
top: 13%;
35+
transform: translateX(-50%);
36+
width: min(90vw, 44rem);
37+
text-align: center;
38+
z-index: 6;
39+
pointer-events: none;
40+
}
41+
.cine-card {
42+
margin: 0;
43+
font-family: 'Libre Caslon Text', Georgia, serif;
44+
font-style: italic;
45+
font-weight: 400;
46+
font-size: clamp(1.1rem, 3.4vw, 1.7rem);
47+
line-height: 1.45;
48+
/* Golden + glow, matching the Paradiso rose quote */
49+
color: rgba(245, 210, 120, 0.92);
50+
text-shadow: 0 0 28px rgba(245, 210, 120, 0.4);
51+
/* Float up like the rose's quote */
52+
animation: cine-rise 1.6s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
53+
}
54+
@keyframes cine-rise {
55+
from { opacity: 0; transform: translateY(30px); }
56+
to { opacity: 1; transform: translateY(0); }
57+
}
58+
@media (prefers-reduced-motion: reduce) {
59+
.cine-card { animation: none; opacity: 1; transform: none; }
60+
}
61+
`}</style>
62+
{/* key remounts on step change so each new line floats up fresh */}
63+
<p className="cine-card" key={step}>{text}</p>
64+
</div>
65+
);
66+
};
67+
68+
export default CinematicCards;

client/src/components/LoginContainer.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,15 @@
365365
color: color-mix(in srgb, var(--gold-subtle) 70%, transparent);
366366
}
367367

368+
.intro-reassure {
369+
margin: 0.3rem 0 0;
370+
font-family: var(--font-ui);
371+
font-size: clamp(0.7rem, 1.6vw, 0.86rem);
372+
font-weight: 400;
373+
letter-spacing: 0.015em;
374+
color: color-mix(in srgb, var(--gold-subtle) 58%, transparent);
375+
}
376+
368377
.intro-trust {
369378
margin: 0.3rem 0 0;
370379
font-family: var(--font-ui);
Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// src/components/LoginContainer.tsx
2+
//
3+
// Cinematic entry: no brand header anymore (wordmark, tagline, trust band all
4+
// live on the homepage + the welcome step). The container just frames the
5+
// portal, which carries the whole "Vorspann".
26
import { forwardRef, ReactNode } from 'react';
37
import './LoginContainer.css';
4-
import CosmicLogo from './CosmicLogo';
58
import PerfectPortal from './PerfectPortal';
6-
import { useTranslation } from '../hooks/useTranslation';
7-
import { isSelfHost } from '../config/deployment';
89

910
interface LoginContainerProps {
1011
isFormVisible: boolean;
@@ -15,31 +16,8 @@ interface LoginContainerProps {
1516

1617
const LoginContainer = forwardRef<HTMLDivElement, LoginContainerProps>(
1718
({ isFormVisible, handlePortalClick, children, loginSuccessful = false }, ref) => {
18-
const { tNode } = useTranslation();
19-
2019
return (
2120
<div className="login-content">
22-
{/* Org credit — tiny top line, pairs with the wordmark as a lockup */}
23-
<p className={`brand-credit${loginSuccessful ? ' header-to-quote' : ''}`}>{tNode('app.attribution')}</p>
24-
25-
{/* Brand wordmark — docks just below the credit after the brand beat */}
26-
<div className={`login-header-group${loginSuccessful ? ' header-to-quote' : ''}`}>
27-
<h1 className="headline app-title">
28-
<span className="headline-text">{tNode('app.name')}</span>
29-
{/* Logo as child of text for bulletproof positioning */}
30-
<CosmicLogo className="headline-logo" />
31-
</h1>
32-
<p className="brand-tagline">{tNode('app.brandTagline')}</p>
33-
</div>
34-
35-
{/* Working-state value statement — fills the band the wordmark vacates */}
36-
<div className={`intro-clarity${loginSuccessful ? ' header-to-quote' : ''}`}>
37-
<p className="intro-explainer">{tNode('entry.explainer')}</p>
38-
{!isSelfHost && <p className="intro-access">{tNode('entry.access')}</p>}
39-
<p className="intro-trust">{tNode('entry.trustLine')}</p>
40-
</div>
41-
42-
{/* Perfect Portal with login form — unmount once rose takes over */}
4321
{!loginSuccessful && (
4422
<PerfectPortal
4523
ref={ref}
@@ -57,4 +35,4 @@ const LoginContainer = forwardRef<HTMLDivElement, LoginContainerProps>(
5735

5836
LoginContainer.displayName = 'LoginContainer';
5937

60-
export default LoginContainer;
38+
export default LoginContainer;

client/src/components/PerfectPortal.css

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -422,18 +422,15 @@
422422
/* Login form styling */
423423
.login-form {
424424
position: absolute;
425-
width: 72%; /* Slightly smaller to ensure fit */
425+
width: 72%;
426426
max-width: 240px;
427427
opacity: 0;
428428
visibility: hidden;
429429
transition: opacity 0.3s ease 0.5s, visibility 0.3s ease 0.5s, transform 0.3s ease 0.5s;
430430
z-index: var(--z-base); /* 10 */
431-
background-color: rgba(20, 25, 60, 0.6);
432-
backdrop-filter: blur(var(--glass-blur-heavy));
433-
padding: 1.1rem; /* Slightly less padding */
434-
border-radius: 10px;
435-
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
436-
border: 1px solid color-mix(in srgb, var(--gold-subtle) 25%, transparent);
431+
/* Cinematic entry: the form card is gone. The portal now frames the
432+
frameless Seeker portrait, so no box (no background, blur, padding,
433+
border, or shadow). */
437434
transform: translateY(20px);
438435
}
439436

0 commit comments

Comments
 (0)