diff --git a/android/app/build.gradle b/android/app/build.gradle index 0e3c36f4..9114d7e9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -11,7 +11,7 @@ android { applicationId "com.compassconnections.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 99 + versionCode 100 versionName "1.21.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { diff --git a/common/messages/de.json b/common/messages/de.json index 15f70a19..ccff5281 100644 --- a/common/messages/de.json +++ b/common/messages/de.json @@ -643,6 +643,7 @@ "organization.contact": "Kontakt", "organization.contactSection.desc": "Brauchen Sie Hilfe oder möchten Sie uns erreichen? Hier geht es los.", "organization.contactSection.title": "Kontakt & Support", + "organization.eyebrow": "Compass", "organization.financials": "Finanzen", "organization.help": "Hilfe", "organization.privacy": "Datenschutzrichtlinie", @@ -651,7 +652,9 @@ "organization.security": "Sicherheit", "organization.seo.description": "Organisation", "organization.seo.title": "Organisation", + "organization.sections.label": "Entdecken", "organization.stats": "Wachstum & Statistiken", + "organization.subtitle": "Alles über die transparente Verwaltung, Leitung und Entwicklung von Compass.", "organization.support": "Support", "organization.terms": "Allgemeine Geschäftsbedingungen", "organization.title": "Organisation", @@ -1277,30 +1280,45 @@ "social.dev.title": "Entwicklung", "social.discord": "Discord", "social.email_button": "E-Mail", + "social.eyebrow": "Bleiben Sie verbunden", "social.follow.desc": "Bleiben Sie über Ankündigungen und Nachrichten informiert.", "social.follow.title": "Folgen & Updates", "social.github": "GitHub", + "social.instagram": "Instagram", "social.reddit": "Reddit", "social.seo.description": "Soziale Netzwerke", "social.seo.title": "Soziale Netzwerke", + "social.sections.label": "Unsere Kanäle", "social.stoat": "Revolt / Stoat", + "social.subtitle": "Finden Sie uns im Internet — nehmen Sie an Diskussionen teil, verfolgen Sie Updates oder sagen Sie einfach Hallo.", "social.title": "Soziale Netzwerke", "social.x": "X", "star_button.save": "Profil speichern", "star_button.unsave": "Profil nicht mehr speichern", "stats.active_members": "Aktive Mitglieder (letzter Monat)", + "stats.chart.subtitle": "Gesamt- und vollständige Profile im Zeitverlauf", + "stats.chart.title": "Mitgliederwachstum", "stats.compatibility_prompts": "Kompatibilitätsfragen", "stats.discussions": "Diskussionen", "stats.endorsements": "Empfehlungen", + "stats.eyebrow": "Transparenz", "stats.gender_ratio": "Geschlechterverteilung", + "stats.group.community": "Community", + "stats.group.compatibility": "Kompatibilität", + "stats.group.conversations": "Gespräche", + "stats.group.democracy": "Demokratie", "stats.members": "Mitglieder", "stats.messages": "Nachrichten", "stats.number_members": "Anzahl der Mitglieder", "stats.prompts_answered": "Beantwortete Fragen", "stats.proposals": "Vorschläge", + "stats.highlight.active": "Aktiv (letzter Monat)", + "stats.highlight.members": "Mitglieder gesamt", + "stats.highlight.messages": "Nachrichten gesendet", "stats.searches_bookmarked": "Gespeicherte Suchen", "stats.seo.description": "Statistiken", "stats.seo.title": "Statistiken", + "stats.subtitle": "Echte Zahlen. Kein Spin. Compass wird transparent aufgebaut — hier ist genau, wie wir wachsen.", "stats.title": "Wachstum und Statistiken", "stats.total": "Gesamt", "stats.votes": "Stimmen", diff --git a/common/messages/fr.json b/common/messages/fr.json index b8fcc44c..c4f22372 100644 --- a/common/messages/fr.json +++ b/common/messages/fr.json @@ -14,8 +14,8 @@ "about.block.keyword.title": "Recherche par mots-clés", "about.block.mission.text": "Notre unique mission est de créer plus de relations humaines authentiques, et chaque décision doit servir cet objectif.", "about.block.mission.title": "Une mission", - "about.block.notify.text": "Plus besoin de vérifier constamment l'application ! Nous vous contacterons lorsque de nouveaux utilisateurs correspondent à vos recherches.", - "about.block.notify.title": "Recevez des notifications de recherches", + "about.block.notify.text": "Plus besoin de vérifier constamment l'application ! Nous to contacterons lorsque de nouveaux utilisateurs correspondent à tes recherches.", + "about.block.notify.title": "Reçois des notifications de recherches", "about.block.personality.text": "Valeurs et centres d'intérêt d'abord, les photos sont secondaires.", "about.block.personality.title": "Centré sur la personnalité", "about.block.press.link": "ici", @@ -642,6 +642,7 @@ "organization.contact": "Contact", "organization.contactSection.desc": "Besoin d'aide ou de nous contacter ? Commencez ici.", "organization.contactSection.title": "Contact & support", + "organization.eyebrow": "Compass", "organization.financials": "Finances", "organization.help": "Aide", "organization.privacy": "Politique de confidentialité", @@ -650,7 +651,9 @@ "organization.security": "Sécurité", "organization.seo.description": "Organisation", "organization.seo.title": "Organisation", + "organization.sections.label": "Explorer", "organization.stats": "Croissance & Statistiques", + "organization.subtitle": "Tout sur la manière dont Compass est géré, dirigé et construit — de façon transparente.", "organization.support": "Support", "organization.terms": "Conditions générales", "organization.title": "Organisation", @@ -1276,30 +1279,45 @@ "social.dev.title": "Développement", "social.discord": "Discord", "social.email_button": "E‑mail", + "social.eyebrow": "Rejoignez-nous", "social.follow.desc": "Restez informé des annonces et actualités.", "social.follow.title": "Annonces & Mises à jour", "social.github": "GitHub", + "social.instagram": "Instagram", "social.reddit": "Reddit", "social.seo.description": "Réseaux sociaux", "social.seo.title": "Réseaux sociaux", + "social.sections.label": "Nos chaînes", "social.stoat": "Revolt / Stoat", + "social.subtitle": "Retrouvez-nous sur le web — rejoignez la conversation, suivez les actualités ou dites bonjour.", "social.title": "Réseaux sociaux", "social.x": "X", "star_button.save": "Enregistrer le profil", "star_button.unsave": "Ne plus enregistrer le profil", "stats.active_members": "Membres actifs (le mois dernier)", + "stats.chart.subtitle": "Profils partiels et complétés au fil du temps", + "stats.chart.title": "Croissance des membres", "stats.compatibility_prompts": "Questions de compatibilité", "stats.discussions": "Discussions", "stats.endorsements": "Recommandations", - "stats.gender_ratio": "Répartition Hommes / Femmes", + "stats.eyebrow": "Transparence", + "stats.gender_ratio": "Hommes / Femmes", + "stats.group.community": "Communauté", + "stats.group.compatibility": "Compatibilité", + "stats.group.conversations": "Conversations", + "stats.group.democracy": "Démocratie", "stats.members": "Membres", "stats.messages": "Messages", "stats.number_members": "Nombre de membres", "stats.prompts_answered": "Questions répondues", "stats.proposals": "Propositions", + "stats.highlight.active": "Actifs (dernier mois)", + "stats.highlight.members": "Membres", + "stats.highlight.messages": "Messages envoyés", "stats.searches_bookmarked": "Recherches mises en favoris", "stats.seo.description": "Statistiques", "stats.seo.title": "Statistiques", + "stats.subtitle": "Des chiffres réels. Sans filtre. Compass est construit en toute transparence — voici exactement comment nous grandissons.", "stats.title": "Croissance et statistiques", "stats.total": "Total", "stats.votes": "Votes", diff --git a/web/components/contact.tsx b/web/components/contact.tsx index a08f7a18..bb6c6728 100644 --- a/web/components/contact.tsx +++ b/web/components/contact.tsx @@ -25,7 +25,7 @@ export function ContactComponent() { const showButton = !!editor?.getText().length return ( - + {t('contact.title', 'Contact')}

{t('contact.intro_prefix', 'You can also contact us through this ')} diff --git a/web/components/widgets/charts.tsx b/web/components/widgets/charts.tsx index d197ee16..e8bba0db 100644 --- a/web/components/widgets/charts.tsx +++ b/web/components/widgets/charts.tsx @@ -1,9 +1,19 @@ import {useEffect, useState} from 'react' -import {Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts' +import { + Area, + AreaChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' import {useT} from 'web/lib/locale' import {getCompletedProfilesCreations, getProfilesCreations} from 'web/lib/supabase/users' -// Helper to convert rows into date -> count map +// ─── Helpers ────────────────────────────────────────────────────────────────── + function buildCounts(rows: any[]) { const counts: Record = {} for (const r of rows) { @@ -13,33 +23,155 @@ function buildCounts(rows: any[]) { return counts } -// Helper to turn count map into cumulative by sorted date array function cumulativeFromCounts(counts: Record, sortedDates: string[]) { const out: Record = {} let prev = 0 for (const d of sortedDates) { - const v = counts[d] || 0 - prev += v + prev += counts[d] || 0 out[d] = prev } return out } +function toISODate(d: Date) { + return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())) + .toISOString() + .split('T')[0] +} + +function addDays(d: Date, days: number) { + const nd = new Date(d) + nd.setUTCDate(nd.getUTCDate() + days) + return nd +} + +function buildDailyRange(startStr: string, endStr: string) { + const out: string[] = [] + const start = new Date(startStr + 'T00:00:00.000Z') + const end = new Date(endStr + 'T00:00:00.000Z') + for (let d = start; d <= end; d = addDays(d, 1)) out.push(toISODate(d)) + return out +} + +function formatDate(ts: number) { + return new Date(ts).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: '2-digit', + }) +} + +// ─── Custom Tooltip ─────────────────────────────────────────────────────────── + +function CustomTooltip({active, payload, label}: any) { + if (!active || !payload?.length) return null + + const date = payload[0]?.payload?.date + ? new Date(payload[0].payload.date).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + }) + : formatDate(label) + + return ( +

+

+ {date} +

+ {payload.map((entry: any) => ( +
+
+ + {entry.name} + + + {entry.value.toLocaleString()} + +
+ ))} +
+ ) +} + +// ─── Custom Legend ──────────────────────────────────────────────────────────── + +function CustomLegend({payload}: any) { + return ( +
+ {payload?.map((entry: any) => ( +
+
+ + {entry.value} + +
+ ))} +
+ ) +} + +// ─── Chart ──────────────────────────────────────────────────────────────────── + export default function ChartMembers() { const [data, setData] = useState([]) - - const [chartHeight, setChartHeight] = useState(400) + const [chartHeight, setChartHeight] = useState(380) + const [loading, setLoading] = useState(true) const t = useT() useEffect(() => { - // Set responsive chart height: 300px on small widths, 400px otherwise function applyHeight() { - if (typeof window !== 'undefined') { - const isSmall = window.innerWidth < 420 - setChartHeight(isSmall ? 320 : 400) - } + setChartHeight(window.innerWidth < 420 ? 280 : 380) } - applyHeight() window.addEventListener('resize', applyHeight) return () => window.removeEventListener('resize', applyHeight) @@ -47,127 +179,134 @@ export default function ChartMembers() { useEffect(() => { async function load() { - const [allProfiles, complatedProfiles] = await Promise.all([ + const [allProfiles, completedProfiles] = await Promise.all([ getProfilesCreations(), getCompletedProfilesCreations(), ]) const countsAll = buildCounts(allProfiles) - const countsCompleted = buildCounts(complatedProfiles) + const countsCompleted = buildCounts(completedProfiles) - // Build a full daily date range from min to max date for equidistant time axis const allDates = Object.keys(countsAll) const completedDates = Object.keys(countsCompleted) - const minDateStr = [...allDates, ...completedDates].sort((a, b) => a.localeCompare(b))[0] - const maxDateStr = [...allDates, ...completedDates].sort((a, b) => b.localeCompare(a))[0] - - function toISODate(d: Date) { - return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())) - .toISOString() - .split('T')[0] - } - - function addDays(d: Date, days: number) { - const nd = new Date(d) - nd.setUTCDate(nd.getUTCDate() + days) - return nd - } - - function buildDailyRange(startStr: string, endStr: string) { - const out: string[] = [] - const start = new Date(startStr + 'T00:00:00.000Z') - const end = new Date(endStr + 'T00:00:00.000Z') - for (let d = start; d <= end; d = addDays(d, 1)) { - out.push(toISODate(d)) - } - return out - } + const sorted = [...allDates, ...completedDates].sort((a, b) => a.localeCompare(b)) + const minDateStr = sorted[0] + const maxDateStr = sorted[sorted.length - 1] const dates = buildDailyRange(minDateStr, maxDateStr) - const cumAll = cumulativeFromCounts(countsAll, dates) const cumCompleted = cumulativeFromCounts(countsCompleted, dates) - const merged = dates.map((date) => ({ - date, - dateTs: new Date(date + 'T00:00:00.000Z').getTime(), - profilesCreations: cumAll[date] || 0, - profilesCompletedCreations: cumCompleted[date] || 0, - })) - - setData(merged) + setData( + dates.map((date) => ({ + date, + dateTs: new Date(date + 'T00:00:00.000Z').getTime(), + profilesCreations: cumAll[date] || 0, + profilesCompletedCreations: cumCompleted[date] || 0, + })), + ) + setLoading(false) } - void load() }, []) - // One LineChart with two Line series sharing the same data array + // Colors from palette + const AMBER = 'rgb(193 127 62)' // primary-500 + const SAGE = 'rgb(107 143 113)' // green-500 + return (
- - - - {t('stats.number_members', 'Number of Members')} - - {/**/} - new Date(ts).toISOString().split('T')[0]} - label={{ - value: t('charts.date', 'Date'), - position: 'insideBottomRight', - offset: -5, - }} - /> - - - (payload && payload[0] && payload[0].payload?.date) || - new Date(value as number).toISOString().split('T')[0] - } - /> - - - - - + {/* Loading shimmer */} + {loading && ( +
+ + Loading chart… + +
+ )} + + {!loading && ( + + + {/* Gradient fills */} + + + + + + + + + + + + + + + + (v >= 1000 ? `${(v / 1000).toFixed(1)}K` : v)} + width={40} + /> + + } + cursor={{stroke: 'rgb(193 127 62)', strokeWidth: 1, strokeDasharray: '4 2'}} + /> + + } /> + + {/* Completed (sage, behind) */} + + + {/* Total (amber, on top) */} + + + + )}
) } diff --git a/web/pages/about.tsx b/web/pages/about.tsx index d521decd..89a8f6ff 100644 --- a/web/pages/about.tsx +++ b/web/pages/about.tsx @@ -5,6 +5,7 @@ import Link from 'next/link' import {ReactNode} from 'react' import {CopyLinkOrShareButton, ShareProfileOnXButton} from 'web/components/buttons/copy-link-button' import {GeneralButton} from 'web/components/buttons/general-button' +import {Row} from 'web/components/layout/row' import {PageBase} from 'web/components/page-base' import {SEO} from 'web/components/SEO' import {useT} from 'web/lib/locale' @@ -57,7 +58,7 @@ function FeatureCardWide({icon, title, text}: FeatureCardProps) { className=" group relative overflow-hidden col-span-1 md:col-span-2 bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-7 - flex items-center gap-6 + flex flex-col sm:flex-row items-start sm:items-center gap-0 sm:gap-6 transition-all duration-[120ms] ease-in hover:shadow-[0_10px_30px_rgba(44,36,22,0.09)] hover:border-primary-500 @@ -66,7 +67,7 @@ function FeatureCardWide({icon, title, text}: FeatureCardProps) {
{icon}
-
+

{title}

{text}

@@ -132,11 +133,11 @@ function ShareStrip({title, text}: {title: string; text: string}) { const t = useT() return (
-
+

📣 {title}

{text}

-
+ {/*// */} {/*// ${*/} {/*// primary*/} @@ -157,7 +158,7 @@ function ShareStrip({title, text}: {title: string; text: string}) { 'bg-primary-500 text-white hover:text-white border-primary-500 hover:bg-primary-600', )} /> -
+
) } diff --git a/web/pages/organization.tsx b/web/pages/organization.tsx index 1a343b29..db22d950 100644 --- a/web/pages/organization.tsx +++ b/web/pages/organization.tsx @@ -1,105 +1,176 @@ +import {ReactNode} from 'react' import {GeneralButton} from 'web/components/buttons/general-button' -import {Row} from 'web/components/layout/row' import {PageBase} from 'web/components/page-base' import {SEO} from 'web/components/SEO' import {useT} from 'web/lib/locale' -function Section({ - title, - description, - children, -}: { +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface LinkItem { + url: string + label: string + primary?: boolean +} + +interface SectionCardProps { + icon: string title: string description: string - children: React.ReactNode -}) { + links: LinkItem[] +} + +// ─── Section Card ───────────────────────────────────────────────────────────── + +function SectionCard({icon, title, description, links}: SectionCardProps) { return ( -
-

{title}

-

{description}

-
{children}
+
+ {/* Icon */} +
+ {icon} +
+ + {/* Title & description */} +

{title}

+

{description}

+ + {/* Links */} +
+ {links.map(({url, label, primary}) => ( + + ))} +
) } +// ─── Section Label ──────────────────────────────────────────────────────────── + +function SectionLabel({children}: {children: ReactNode}) { + return ( +
+ + {children} + +
+
+ ) +} + +// ─── Divider ────────────────────────────────────────────────────────────────── + +function Divider() { + return ( +
+ ) +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + export default function Organization() { const t = useT() + const sections: SectionCardProps[] = [ + { + icon: 'ℹ️', + title: t('organization.about.title', 'About us'), + description: t( + 'organization.about.desc', + 'Who we are, our mission, and how the organization works.', + ), + links: [ + {url: '/about', label: t('about.seo.description', 'About Compass'), primary: true}, + {url: '/constitution', label: t('organization.constitution', 'Our constitution')}, + ], + }, + { + icon: '📊', + title: t('organization.proof.title', 'Proof & transparency'), + description: t( + 'organization.proof.desc', + 'Key numbers, progress, and what others say about us.', + ), + links: [ + {url: '/stats', label: t('organization.stats', 'Key metrics & growth'), primary: true}, + {url: '/press', label: t('press.title', 'Press')}, + {url: '/financials', label: t('organization.financials', 'Financial transparency')}, + ], + }, + { + icon: '💬', + title: t('organization.contactSection.title', 'Contact & support'), + description: t( + 'organization.contactSection.desc', + 'Need help or want to reach us? Start here.', + ), + links: [ + {url: '/contact', label: t('organization.contact', 'Contact us'), primary: true}, + {url: '/help', label: t('organization.help', 'Help & support center')}, + ], + }, + { + icon: '🔒', + title: t('organization.trust.title', 'Trust & legal'), + description: t( + 'organization.trust.desc', + 'How we protect your data and the rules that govern the platform.', + ), + links: [ + {url: '/security', label: t('organization.security', 'Security'), primary: true}, + {url: '/terms', label: t('organization.terms', 'Terms and conditions')}, + {url: '/privacy', label: t('organization.privacy', 'Privacy policy')}, + ], + }, + ] + return ( -

- {t('organization.title', 'Organization')} -

- - {/* ABOUT */} -
- - - - -
- - {/* PROOF */} -
- - - - - -
- - {/* CONTACT */} -
- - - - -
- - {/* TRUST / LEGAL */} -
- - - - - -
+
+ {/* ── Page header ── */} +
+

+ {t('organization.eyebrow', 'Compass')} +

+

+ {t('organization.title', 'Organization')} +

+

+ {t( + 'organization.subtitle', + 'Everything about how Compass is run, governed, and built — transparently.', + )} +

+
+ + {t('organization.sections.label', 'Explore')} +
+ {sections.map((s) => ( + + ))} +
+
) } diff --git a/web/pages/press.tsx b/web/pages/press.tsx index c8bcb843..d4e9e280 100644 --- a/web/pages/press.tsx +++ b/web/pages/press.tsx @@ -69,7 +69,7 @@ const pressItems: PressItem[] = [ const PressItem = ({item}: {item: PressItem; locales: Intl.LocalesArgument}) => { const t = useT() return ( -
+

{item.title} @@ -130,7 +130,7 @@ export default function PressPage() {

{t('press.media_kit', 'Media Kit')}

-
+

{t('press.brand_assets', 'Brand Assets')}

@@ -139,19 +139,19 @@ export default function PressPage() {

{t('press.download_assets', 'Download Assets')}
-
+

{t('press.contact', 'Press Contact')}

{t('press.contact_description', 'For press inquiries, please contact our team.')}

{t('press.contact_us', 'Contact Us')} diff --git a/web/pages/privacy.tsx b/web/pages/privacy.tsx index d062bd2e..8de6b2a8 100644 --- a/web/pages/privacy.tsx +++ b/web/pages/privacy.tsx @@ -8,7 +8,7 @@ export default function PrivacyPage() { const t = useT() return ( - + -

{title}

-

{description}

-
{children}
+ + {icon && {icon}} + {label} + + ) +} + +// ─── Section Card ───────────────────────────────────────────────────────────── + +function SectionCard({icon, title, description, links}: SectionCardProps) { + return ( +
+ {/* Icon */} +
+ {icon} +
+ + {/* Title & description */} +

{title}

+

{description}

+ + {/* Links */} +
+ {links.map((link) => ( + + ))} +
) } +// ─── Section Label ──────────────────────────────────────────────────────────── + +function SectionLabel({children}: {children: ReactNode}) { + return ( +
+ + {children} + +
+
+ ) +} + +// ─── Divider ────────────────────────────────────────────────────────────────── + +function Divider() { + return ( +
+ ) +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + export default function Social() { const t = useT() + const sections: SectionCardProps[] = [ + { + icon: '💬', + title: t('social.community.title', 'Community'), + description: t( + 'social.community.desc', + 'Join our community chats and shape the platform with us.', + ), + links: [ + {url: discordLink, label: t('social.discord', 'Discord'), icon: '🎮', primary: true}, + {url: redditLink, label: t('social.reddit', 'Reddit'), icon: ''}, + // {url: stoatLink, label: t('social.stoat', 'Revolt / Stoat'), icon: '💬'}, + ], + }, + { + icon: '📣', + title: t('social.follow.title', 'Follow & Updates'), + description: t( + 'social.follow.desc', + 'Stay informed about announcements, releases, and news.', + ), + links: [ + {url: xLink, label: t('social.x', 'X / Twitter'), icon: '𝕏', primary: true}, + {url: instagramLink, label: t('social.instagram', 'Instagram'), icon: '📸'}, + ], + }, + { + icon: '💻', + title: t('social.dev.title', 'Development'), + description: t( + 'social.dev.desc', + 'Explore our source code, open issues, or contribute a PR.', + ), + links: [{url: githubRepo, label: t('social.github', 'GitHub'), icon: '⭐', primary: true}], + }, + { + icon: '✉️', + title: t('social.contact.title', 'Contact'), + description: t('social.contact.desc', 'Reach out to us directly for inquiries or support.'), + links: [ + { + url: `mailto:${supportEmail}`, + label: `${t('social.email_button', 'Email us')}`, + icon: '📧', + primary: true, + }, + ], + }, + ] + return ( -

{t('social.title', 'Socials')}

+
+ {/* ── Page header ── */} +
+

+ {t('social.eyebrow', 'Connect with us')} +

+

+ {t('social.title', 'Socials')} +

+

+ {t( + 'social.subtitle', + 'Find us across the web — join the conversation, follow updates, or say hello.', + )} +

+
- {/* COMMUNITY */} -
- - - - - -
+ - {/* FOLLOW / UPDATES */} -
- - - - -
+ {/* ── Cards ── */} + {t('social.sections.label', 'Our channels')} - {/* DEVELOPMENT */} -
- - - -
- - {/* CONTACT */} -
- - - -
+
+ {sections.map((s) => ( + + ))} +
+
) } diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index 325dbe26..41d8f679 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -1,19 +1,202 @@ import clsx from 'clsx' import {type Stats} from 'common/stats' -import {useEffect, useState} from 'react' -import {Col} from 'web/components/layout/col' +import {ReactNode, useEffect, useState} from 'react' import {PageBase} from 'web/components/page-base' import {SEO} from 'web/components/SEO' import ChartMembers from 'web/components/widgets/charts' -import StatBox from 'web/components/widgets/stat-box' import {api} from 'web/lib/api' import {useT} from 'web/lib/locale' import {getCount} from 'web/lib/supabase/users' +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface StatCardProps { + value: string | number | null | undefined + label: string + icon: string + accent?: 'amber' | 'sage' | 'muted' + large?: boolean +} + +interface StatGroupProps { + icon: string + title: string + children: ReactNode +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function formatNumber(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` + return n.toLocaleString() +} + +// ─── Stat Card ──────────────────────────────────────────────────────────────── + +function StatCard({value, label, icon, accent = 'amber', large}: StatCardProps) { + if (value === null || value === undefined || value === 0) return null + + const formatted = typeof value === 'number' ? formatNumber(value) : value + + const accentClasses = { + amber: 'text-primary-500', + sage: 'text-green-500', + muted: 'text-ink-500', + } + + return ( +
+
+
+ {icon} +
+
+ +
+ {formatted} +
+ +

+ {label} +

+
+ ) +} + +// ─── Stat Group ─────────────────────────────────────────────────────────────── + +function StatGroup({icon, title, children}: StatGroupProps) { + return ( +
+
+ {icon} + + {title} + +
+
+
{children}
+
+ ) +} + +// ─── Chart wrapper ──────────────────────────────────────────────────────────── + +function ChartCard() { + const t = useT() + return ( +
+
+
+ 📈 +
+
+

+ {t('stats.chart.title', 'Member Growth')} +

+

+ {t('stats.chart.subtitle', 'Total & completed profiles over time')} +

+
+
+ +
+ ) +} + +// ─── Highlight Row ──────────────────────────────────────────────────────────── + +function HighlightRow({ + members, + active, + messages, +}: { + members: number | null + active: number | null + messages: number | null | undefined +}) { + const t = useT() + const items = [ + { + value: members, + label: t('stats.highlight.members', 'Total Members'), + icon: '👥', + accent: 'amber' as const, + }, + { + value: active, + label: t('stats.highlight.active', 'Active (last month)'), + icon: '⚡', + accent: 'sage' as const, + }, + { + value: messages, + label: t('stats.highlight.messages', 'Messages sent'), + icon: '✉️', + accent: 'amber' as const, + }, + ].filter((i) => !!i.value) + + if (!items.length) return null + + return ( +
+ {items.map((item) => ( +
+
+ {item.icon} +
+
+ {typeof item.value === 'number' ? formatNumber(item.value) : item.value} +
+

+ {item.label} +

+
+ ))} +
+ ) +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + export default function Stats() { const t = useT() const [data, setData] = useState>({}) const [statsData, setStatsData] = useState(undefined) + const [loading, setLoading] = useState(true) useEffect(() => { async function load() { @@ -37,22 +220,23 @@ export default function Stats() { const result: Record = {} if (settled.status === 'fulfilled') { settled.value.forEach((res, i) => { - const key = tables[i] - if (res.status === 'fulfilled') result[key] = res.value - else result[key] = null + result[tables[i]] = res.status === 'fulfilled' ? res.value : null }) } - if (statsResult.status === 'fulfilled') { - setStatsData(statsResult.value) - } + if (statsResult.status === 'fulfilled') setStatsData(statsResult.value) setData(result) + setLoading(false) } void load() }, []) + const genderRatioLabel = statsData?.genderRatio + ? `${statsData.genderRatio.male ?? 0} / ${statsData.genderRatio.female ?? 0}` + : null + return ( -

- {t('stats.title', 'Growth & Stats')} -

- - - - - - {!!data.profiles && ( - - )} - {!!data.active_members && ( - + {/* ── Page header ── */} +
+

+ {t('stats.eyebrow', 'Transparency')} +

+

+ {t('stats.title', 'Growth & Stats')} +

+

+ {t( + 'stats.subtitle', + "Real numbers. No spin. Compass is built in the open — here's exactly how we're growing.", + )} +

+
+ + {/* ── Loading skeleton ── */} + {loading && ( +
+ {Array.from({length: 6}).map((_, i) => ( +
+
+
+
+
+ ))} +
+ )} + + {!loading && ( + <> + {/* ── Hero highlight row ── */} + - )} - {!!data.private_user_message_channels && ( - - )} - {!!statsData?.messages && ( - - )} - {!!data.compatibility_prompts && ( - - )} - {!!data.compatibility_answers && ( - - )} - {!!data.votes && } - {!!data.vote_results && ( - - )} - {!!data.bookmarked_searches && ( - - )} - {!!data.profile_comments && ( - - )} - {!!statsData?.genderRatio && ( - - )} - - + + {/* ── Growth chart ── */} + + + {/* ── Community ── */} + + + + + + + + {/* ── Conversations ── */} + + + + + + {/* ── Compatibility ── */} + + + + + + {/* ── Democracy ── */} + + + + {/**/} + + + )} +
) } diff --git a/web/pages/terms.tsx b/web/pages/terms.tsx index 1ea05904..363cbd64 100644 --- a/web/pages/terms.tsx +++ b/web/pages/terms.tsx @@ -9,7 +9,7 @@ export default function TermsPage() { return (