Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/landing-page/components/footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ function Footer() {
<div className="meta text-ink-400 text-center">
PoliHack v19 · BMW · AppDev with AI · "Applications that encourage AI adoption"
</div>
<div className="meta text-ink-400">© 2026 LearnLoop</div>
<div className="meta text-ink-400 flex items-center gap-3">
<a href={REPO_URL} className="hover:text-ink-900 transition-colors">GitHub</a>
<span aria-hidden>·</span>
<a href={`${REPO_URL}/blob/main/LICENSE`} className="hover:text-ink-900 transition-colors">MIT</a>
<span aria-hidden>·</span>
<span>© 2026 LearnLoop</span>
</div>
</Container>
</footer>
);
Expand Down
154 changes: 24 additions & 130 deletions apps/landing-page/components/hero.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function Hero() {
<span className="absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping" style={{background:'#1FA29A'}} />
<span className="relative inline-flex rounded-full h-1.5 w-1.5" style={{background:'#1FA29A'}} />
</span>
Closed beta · 2026
Open source · MIT · self-hosted
</span>
</div>

Expand All @@ -34,8 +34,8 @@ function Hero() {
</p>

{/* CTA */}
<div className="mt-9 md:mt-11 flex justify-center">
<WaitlistButton />
<div className="mt-9 md:mt-11 flex flex-col sm:flex-row items-center justify-center gap-3">
<CtaLinks />
</div>

{/* credential cards */}
Expand All @@ -61,139 +61,33 @@ function Hero() {
);
}

function WaitlistButton() {
const [open, setOpen] = useState(false);
// There is no hosted product or waitlist: LearnLoop is open source and
// self-hosted. The CTAs point at the code, the self-hosting guide and the
// walkthrough video.

function CtaLinks() {
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
<a
href={REPO_URL}
className="rounded-md px-7 py-4 font-medium text-base tracking-wide transition lift inline-flex items-center gap-2"
style={{background:'#0E1A1F', color:'#FAFBFB'}}
>
Join the waitlist <span aria-hidden>→</span>
</button>
{open && <WaitlistModal onClose={() => setOpen(false)} />}
</>
);
}

function WaitlistModal({ onClose }) {
const [email, setEmail] = useState('');
const [state, setState] = useState('idle'); // idle | submitting | done | error
const [error, setError] = useState('');
const inputRef = useRef(null);

const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());

useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
setTimeout(() => inputRef.current?.focus(), 50);
return () => {
window.removeEventListener('keydown', onKey);
document.body.style.overflow = '';
};
}, [onClose]);

function onSubmit(e) {
e.preventDefault();
if (!valid || state === 'submitting') return;
setState('submitting');
setError('');
try {
const list = JSON.parse(localStorage.getItem('learnloop_waitlist') || '[]');
if (!list.includes(email.trim())) list.push(email.trim());
localStorage.setItem('learnloop_waitlist', JSON.stringify(list));
setTimeout(() => setState('done'), 600);
} catch (err) {
setState('error');
setError('Something went wrong. Try again.');
}
}

return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4"
style={{background:'rgba(14,26,31,0.45)', backdropFilter:'blur(6px)'}}
onClick={onClose}
>
<div
className="paper-card w-full max-w-md p-6 md:p-7 relative"
style={{boxShadow:'0 24px 60px -20px rgba(14,26,31,0.35)'}}
onClick={(e) => e.stopPropagation()}
View on GitHub <span aria-hidden>→</span>
</a>
<a
href={`${REPO_URL}/blob/main/SELFHOSTING.md`}
className="rounded-md px-6 py-4 font-medium text-base tracking-wide transition lift inline-flex items-center gap-2 border hairline-strong bg-white/70 text-ink-900"
>
<button
type="button"
aria-label="Close"
onClick={onClose}
className="absolute top-3 right-3 w-8 h-8 rounded-md flex items-center justify-center text-ink-400 hover:text-ink-900 hover:bg-paper-100 transition"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
</svg>
</button>

{state === 'done' ? (
<div className="text-center py-2">
<div className="mx-auto w-10 h-10 rounded-full flex items-center justify-center"
style={{background:'#E8F6F2', color:'#0E7B7A'}}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M4 10.5l3.5 3.5L16 5.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div className="mt-4 font-serif text-2xl text-ink-900">You're on the list.</div>
<p className="mt-2 text-ink-500 text-sm">
We'll email <span className="text-ink-900">{email.trim()}</span> when closed beta opens up a spot.
</p>
<button
type="button"
onClick={onClose}
className="meta mt-5 inline-flex items-center gap-2"
style={{color:'#0E7B7A'}}
>
Close <span aria-hidden>→</span>
</button>
</div>
) : (
<form onSubmit={onSubmit}>
<div className="meta mb-2 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full" style={{background:'#1FA29A'}} />
Closed beta
</div>
<div className="font-serif text-2xl md:text-[26px] text-ink-900 leading-tight">
Join the waitlist
</div>
<p className="mt-2 text-ink-500 text-sm">
We'll email you when your team's invite is ready.
</p>

<input
ref={inputRef}
type="email"
required
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="you@team.com"
className="mt-5 w-full rounded-md border hairline-strong bg-paper-50 px-3.5 py-3 text-ink-900 placeholder:text-ink-400 font-mono text-[13px] focus:outline-none focus:ring-2 focus:ring-[#1FA29A]/30 focus:border-[#1FA29A]"
/>
<button
type="submit"
disabled={!valid || state === 'submitting'}
className="mt-3 w-full rounded-md px-5 py-3 font-medium text-sm tracking-wide transition disabled:opacity-50 disabled:cursor-not-allowed"
style={{background:'#0E1A1F', color:'#FAFBFB'}}
>
{state === 'submitting' ? 'Joining…' : 'Submit'}
</button>
{error && <div className="mt-2 text-[12px]" style={{color:'#E26F95'}}>{error}</div>}
<div className="mt-3 meta text-[10px] text-ink-400 text-center">
No spam. One email when your invite is ready.
</div>
</form>
)}
</div>
</div>
Self-host it
</a>
<a
href="#demo"
className="rounded-md px-6 py-4 font-medium text-base tracking-wide transition inline-flex items-center gap-2 text-ink-700 hover:text-ink-900"
>
Watch the 3-min demo
</a>
</>
);
}

Expand Down
1 change: 1 addition & 0 deletions apps/landing-page/components/nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function Nav() {
['Problem', '#problem'],
['Solution', '#features'],
['Demo', '#demo'],
['GitHub', REPO_URL],
];

return (
Expand Down
4 changes: 3 additions & 1 deletion apps/landing-page/components/primitives.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* Shared primitives */
const { useState, useEffect, useRef, useMemo, useCallback } = React;

const REPO_URL = 'https://github.com/Bogzx/LearnLoop';

function Logomark({ size = 32, className = '' }) {
const w = size * 1.6;
return (
Expand Down Expand Up @@ -96,4 +98,4 @@ function Container({ children, className = '' }) {
return <div className={`max-w-[1200px] mx-auto px-5 md:px-8 ${className}`}>{children}</div>;
}

Object.assign(window, { Logomark, Wordmark, SectionKicker, SectionTitle, Reveal, LiveDot, Orbs, Container });
Object.assign(window, { REPO_URL, Logomark, Wordmark, SectionKicker, SectionTitle, Reveal, LiveDot, Orbs, Container });
Loading
Loading