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) => (
+
+ ))}
+
+ )
+}
+
+// ─── 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}
-
+
@@ -132,11 +133,11 @@ function ShareStrip({title, text}: {title: string; text: string}) {
const t = useT()
return (
-
+
-
+
{/*// */}
{/*// ${*/}
{/*// 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 (
+
+ )
+}
+
+// ─── 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.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 (
+
+ )
+}
+
+// ─── 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 (
+
+
+
+
+ {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 (