diff --git a/web/components/home/home.tsx b/web/components/home/home.tsx index 3eaee77f..712d8175 100644 --- a/web/components/home/home.tsx +++ b/web/components/home/home.tsx @@ -1,53 +1,237 @@ -import {useEffect} from 'react' +import {discordLink, githubRepo} from 'common/constants' +import Link from 'next/link' +import {useEffect, useRef} from 'react' import {Col} from 'web/components/layout/col' import {SignUpButton} from 'web/components/nav/sidebar' import {useUser} from 'web/hooks/use-user' import {useT} from 'web/lib/locale' -export function AboutBox(props: {title: string; text: string}) { - const {title, text} = props +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface FeatureCardProps { + icon: string + title: string + text: string +} + +interface SocialAvatarProps { + letter: string + gradient: string +} + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +function EyebrowBadge({children}: {children: React.ReactNode}) { return ( -
-

{title}

-

{text}

+
+ + {children}
) } +function FeatureCard({icon, title, text}: FeatureCardProps) { + return ( +
+ {/* Left accent bar */} +
+ +
+ {icon} +
+

{title}

+

{text}

+
+ ) +} + +function SocialAvatar({letter, gradient}: SocialAvatarProps) { + return ( +
+ {letter} +
+ ) +} + +function SocialProof({label}: {label: React.ReactNode}) { + const avatars: SocialAvatarProps[] = [ + {letter: 'S', gradient: 'linear-gradient(135deg, #C17F3E, #8B5E3C)'}, + {letter: 'R', gradient: 'linear-gradient(135deg, #6B8F71, #4A7055)'}, + {letter: 'T', gradient: 'linear-gradient(135deg, #8B5E3C, #6B3E22)'}, + {letter: 'L', gradient: 'linear-gradient(135deg, #C17F3E, #D4955A)'}, + ] + + return ( +
+
+ {avatars.map((av) => ( + + ))} +
+ {label} +
+ ) +} + +function QuoteBlock({children}: {children: React.ReactNode}) { + return ( +
+ + " + +

{children}

+
+ ) +} + +function OpenSourceStrip({ + title, + description, + badges, +}: { + title: string + description: string + badges: {label: string; url: string; primary?: boolean}[] +}) { + return ( +
+
+

{title}

+

{description}

+
+
+ {badges.map((b) => ( + + {b.label} + + ))} +
+
+ ) +} + +// ─── Typewriter hook ────────────────────────────────────────────────────────── + +function useTypewriter(words: string[]) { + const elRef = useRef(null) + + useEffect(() => { + const el = elRef.current + if (!el) return + + let wordIndex = 0 + let charIndex = 0 + let deleting = false + let timeoutId: ReturnType + + function tick() { + const word = words[wordIndex] + + if (!el) return + + if (!deleting) { + el.textContent = word.substring(0, charIndex + 1) + charIndex++ + if (charIndex === word.length) { + deleting = true + timeoutId = setTimeout(tick, 1800) + return + } + } else { + el.textContent = word.substring(0, charIndex - 1) + charIndex-- + if (charIndex === 0) { + deleting = false + wordIndex = (wordIndex + 1) % words.length + } + } + + timeoutId = setTimeout(tick, deleting ? 60 : 120) + } + + timeoutId = setTimeout(tick, 600) + return () => clearTimeout(timeoutId) + }, [words]) + + return elRef +} + +// ─── Main component ─────────────────────────────────────────────────────────── + export function LoggedOutHome() { const user = useUser() const t = useT() - const typewriterText = t('home.typewriter', 'Search.') + const typewriterWords = [ + t('home.typewriter.search', 'Search.'), + t('home.typewriter.connect', 'Connect.'), + // t('home.typewriter.belong', 'Belong.'), + ] - useEffect(() => { - const text = typewriterText - const el = document.getElementById('typewriter') - if (!el) return + const typewriterRef = useTypewriter(typewriterWords) - let i = 0 - let timeoutId: any - el.textContent = '' + const features: FeatureCardProps[] = [ + { + icon: '🔍', + title: t('home.feature1.title', 'Radically Transparent'), + text: t( + 'home.feature1.text', + 'No algorithms. Every profile fully searchable. You decide who to discover — not a black box.', + ), + }, + { + icon: '🎯', + title: t('home.feature2.title', 'Built for Depth'), + text: t( + 'home.feature2.text', + 'Filter by values, interests, goals, and keywords — from "stoicism" to "sustainable living." Surface connections that truly matter.', + ), + }, + { + icon: '🌍', + title: t('home.feature3.title', 'Community Owned'), + text: t( + 'home.feature3.text', + 'Free forever. No ads, no subscriptions. Built by the people who use it, for the benefit of everyone.', + ), + }, + ] - function typeWriter() { - if (i < text.length && el) { - el.textContent = text.substring(0, i + 1) - i++ - timeoutId = setTimeout(typeWriter, 150) - } - } - - const startId = setTimeout(typeWriter, 500) - - return () => { - clearTimeout(timeoutId) - clearTimeout(startId) - if (el) el.textContent = text - } - }, [typewriterText]) + const openSourceBadges = [ + {label: t('home.strip.github', '⭐ GitHub'), url: githubRepo}, + {label: t('home.strip.discord', '📖 Discord'), url: discordLink}, + {label: t('home.strip.join', 'Join Now →'), url: '/register', primary: true}, + ] return ( <> + {/* Mobile sign-up CTA */} {user === null && ( )} -

- {t('home.title', "Don't Swipe.")} -
- - - | - -

-
-

- {t( - 'home.subtitle', - 'Find people who share your values, ideas, and intentions — not just your photos.', - )} -

-
-
-
- - - + +
+ {/* ── Hero ── */} +
+ + {t('home.eyebrow', 'Free forever · Open source · No matching algorithms')} + + +

+ {t('home.title', "Don't Swipe.")} +

+ + {/* Typewriter line */} +
+ + |
-
-

- {t( - 'home.bottom', - 'Compass is to human connection what Linux is to software, Wikipedia is to knowledge, and Firefox is to browsing — a public digital good designed to serve people, not profit.', - )} -

+ +

+ {t('home.subtitle', 'Find people who share your ')} + {t('home.subtitle.values', 'values')} + {', '} + {t('home.subtitle.ideas', 'ideas')} + {t('home.subtitle.and', ', and ')} + {t('home.subtitle.intentions', 'intentions')} + {t('home.subtitle.end', ' — not just your photos.')} +

+ + {/* CTAs */} +
+ {/**/} + + {t('home.cta.secondary', 'Learn how it works')} +
-
+ + {/* Social proof */} + + {t('home.proof.prefix', 'Joined by ')} + {t('home.proof.count', '600+')} + {t('home.proof.suffix', ' real people worldwide')} + + } + /> +
+ + {/* Divider */} +
+ + {/* ── Features ── */} +
+

+ {t('home.features.label', 'Why Compass')} +

+

+ {t('home.features.title', 'Built different. On purpose.')} +

+
+ {features.map((f) => ( + + ))} +
+
+ + {/* ── Quote ── */} + + {t('home.quote.prefix', 'Compass is to human connection what ')} + {t('home.quote.linux', 'Linux')} + {t('home.quote.linux_suffix', ' is to software, ')} + {t('home.quote.wikipedia', 'Wikipedia')} + {t('home.quote.wikipedia_suffix', ' is to knowledge, and ')} + {t('home.quote.firefox', 'Firefox')} + {t('home.quote.end', ' is to browsing — a public digital good designed to ')} + + {t('home.quote.mission', 'serve people, not profit.')} + + + + {/* ── Open source strip ── */} +
-
+ + {/* Mobile bottom spacing */} +
) } diff --git a/web/pages/about.tsx b/web/pages/about.tsx index 42ece596..a02bda5c 100644 --- a/web/pages/about.tsx +++ b/web/pages/about.tsx @@ -9,7 +9,7 @@ import {useT} from 'web/lib/locale' export const AboutBlock = (props: {text: ReactNode; title: string}) => { const {text, title} = props return ( -
+

{title}

{text}

@@ -131,7 +131,7 @@ export default function About() { {t('about.help.title', 'Help Compass')}
-
+
-
+
{t('about.dev.title', 'Develop the App')}
@@ -161,7 +161,7 @@ export default function About() {

-
+
{t('about.join.title', 'Join the Community')}
@@ -182,7 +182,7 @@ export default function About() { {/**/}
-
+
@@ -227,7 +227,7 @@ export default function About() { {/**/}
-
+
{t('about.final.title', 'Tell Your Friends and Family')}
diff --git a/web/styles/globals.css b/web/styles/globals.css index 14455674..2b16af44 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -135,7 +135,7 @@ --color-canvas-800: 50 50 50; --color-canvas-900: 68 52 34; /* Sidebar pressed */ --color-canvas-950: 44 36 22; /* Sidebar - Dark Espresso (#2C2416) */ - --color-canvas-1000: 0 0 0; /* black */ + --color-canvas-1000: 44 36 22; /* black */ /* Primary - Warm Amber */ --color-primary-50: 250 243 233; /* #FAF3E9 */ @@ -249,6 +249,7 @@ --color-canvas-200: 68 52 34; /* UI Elements/Tags */ --color-canvas-300: 78 62 44; --color-canvas-950: 15 13 10; /* Deepest depth (Sidebar/Bottom) */ + --color-canvas-1000: 247 244 239; /* Deepest depth (Sidebar/Bottom) */ /* Dark Mode Primary - Luminous Amber */ --color-primary-50: 43 21 8; /* Deepest Shadow (Old 950) */ diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 10a9bf9c..904e8bd8 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -332,6 +332,7 @@ module.exports = { 800: 'rgb(var(--color-canvas-800) / )', 900: 'rgb(var(--color-canvas-900) / )', 950: 'rgb(var(--color-canvas-950) / )', + 1000: 'rgb(var(--color-canvas-1000) / )', }, primary: {