Restructure Organization, Social, and Stats pages with new card-based layouts, improved styling, and enhanced loading states for better readability and UX.

This commit is contained in:
MartinBraquet
2026-05-01 15:14:20 +02:00
parent 02f9ccf561
commit d1720ce644
13 changed files with 953 additions and 347 deletions

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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": "Email",
"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",

View File

@@ -25,7 +25,7 @@ export function ContactComponent() {
const showButton = !!editor?.getText().length
return (
<Col className="mx-2">
<Col className="max-w-3xl mx-auto">
<Title className="!mb-2 text-3xl">{t('contact.title', 'Contact')}</Title>
<p className={'custom-link mb-4'}>
{t('contact.intro_prefix', 'You can also contact us through this ')}

View File

@@ -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<string, number> = {}
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<string, number>, sortedDates: string[]) {
const out: Record<string, number> = {}
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 (
<div
style={{
background: 'rgb(247 244 239)', // canvas-50
border: '1.5px solid rgb(232 213 188)', // canvas-200
borderRadius: '12px',
padding: '12px 16px',
boxShadow: '0 8px 24px rgba(44,36,22,0.12)',
}}
>
<p
style={{
fontSize: '11px',
fontWeight: 700,
color: 'rgb(140 128 112)',
marginBottom: '8px',
textTransform: 'uppercase',
letterSpacing: '0.8px',
}}
>
{date}
</p>
{payload.map((entry: any) => (
<div
key={entry.dataKey}
style={{display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px'}}
>
<div
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
background: entry.color,
flexShrink: 0,
}}
/>
<span style={{fontSize: '12px', color: 'rgb(140 128 112)', fontWeight: 500}}>
{entry.name}
</span>
<span
style={{
fontSize: '14px',
fontWeight: 800,
color: 'rgb(30 26 20)',
marginLeft: 'auto',
paddingLeft: '12px',
}}
>
{entry.value.toLocaleString()}
</span>
</div>
))}
</div>
)
}
// ─── Custom Legend ────────────────────────────────────────────────────────────
function CustomLegend({payload}: any) {
return (
<div style={{display: 'flex', justifyContent: 'center', gap: '24px', marginTop: '8px'}}>
{payload?.map((entry: any) => (
<div key={entry.value} style={{display: 'flex', alignItems: 'center', gap: '7px'}}>
<div
style={{
width: '24px',
height: '3px',
background: entry.color,
borderRadius: '2px',
...(entry.payload?.strokeDasharray
? {
backgroundImage: `repeating-linear-gradient(90deg, ${entry.color} 0, ${entry.color} 4px, transparent 4px, transparent 7px)`,
background: 'none',
}
: {}),
}}
/>
<span style={{fontSize: '12px', fontWeight: 600, color: 'rgb(140 128 112)'}}>
{entry.value}
</span>
</div>
))}
</div>
)
}
// ─── Chart ────────────────────────────────────────────────────────────────────
export default function ChartMembers() {
const [data, setData] = useState<any[]>([])
const [chartHeight, setChartHeight] = useState<number>(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 (
<div>
<ResponsiveContainer width="100%" height={chartHeight}>
<LineChart data={data} margin={{top: 24, right: 16, bottom: 24, left: -20}}>
<text
x="50%"
y="24"
textAnchor="middle"
dominantBaseline="middle"
style={{
fontSize: '16px',
fontWeight: 600,
fill: 'rgb(var(--color-primary-900))',
}}
>
{t('stats.number_members', 'Number of Members')}
</text>
{/*<CartesianGrid strokeDasharray="3 3"/>*/}
<XAxis
dataKey="dateTs"
type="number"
scale="time"
domain={['dataMin', 'dataMax']}
tickFormatter={(ts) => new Date(ts).toISOString().split('T')[0]}
label={{
value: t('charts.date', 'Date'),
position: 'insideBottomRight',
offset: -5,
}}
/>
<YAxis />
<Tooltip
contentStyle={{
backgroundColor: 'rgb(var(--color-canvas-100))',
border: 'none',
borderRadius: '8px',
color: 'rgb(var(--color-primary-900))',
}}
labelStyle={{
color: 'rgb(var(--color-primary-900))',
}}
labelFormatter={(value, payload) =>
(payload && payload[0] && payload[0].payload?.date) ||
new Date(value as number).toISOString().split('T')[0]
}
/>
<Legend />
<Line
type="monotone"
dataKey="profilesCreations"
name={t('stats.total', 'Total')}
stroke="rgb(var(--color-primary-900))"
strokeWidth={2}
dot={false}
/>
<Line
type="monotone"
dataKey="profilesCompletedCreations"
name={t('stats.with_bio', 'Completed')}
stroke="#9ca3af"
strokeDasharray="4 2"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
{/* Loading shimmer */}
{loading && (
<div
style={{
height: `${chartHeight}px`,
background: 'rgb(247 244 239)',
borderRadius: '16px',
border: '1.5px solid rgb(232 213 188)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
className="animate-pulse"
>
<span style={{fontSize: '13px', color: 'rgb(140 128 112)', fontWeight: 500}}>
Loading chart
</span>
</div>
)}
{!loading && (
<ResponsiveContainer width="100%" height={chartHeight}>
<AreaChart data={data} margin={{top: 16, right: 16, bottom: 8, left: -8}}>
{/* Gradient fills */}
<defs>
<linearGradient id="gradAmber" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={AMBER} stopOpacity={0.18} />
<stop offset="95%" stopColor={AMBER} stopOpacity={0} />
</linearGradient>
<linearGradient id="gradSage" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={SAGE} stopOpacity={0.14} />
<stop offset="95%" stopColor={SAGE} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--grid-stroke)" vertical={false} />
<XAxis
dataKey="dateTs"
type="number"
scale="time"
domain={['dataMin', 'dataMax']}
tickFormatter={formatDate}
tick={{fontSize: 11, fill: 'rgb(140 128 112)', fontWeight: 500}}
axisLine={{stroke: 'rgb(222 203 178)'}} // canvas-300
tickLine={{stroke: 'rgb(222 203 178)'}}
tickCount={6}
/>
<YAxis
tick={{fontSize: 11, fill: 'rgb(140 128 112)', fontWeight: 500}}
axisLine={false}
tickLine={false}
tickFormatter={(v) => (v >= 1000 ? `${(v / 1000).toFixed(1)}K` : v)}
width={40}
/>
<Tooltip
content={<CustomTooltip />}
cursor={{stroke: 'rgb(193 127 62)', strokeWidth: 1, strokeDasharray: '4 2'}}
/>
<Legend content={<CustomLegend />} />
{/* Completed (sage, behind) */}
<Area
type="monotone"
dataKey="profilesCompletedCreations"
name={t('stats.with_bio', 'Completed')}
stroke={SAGE}
strokeWidth={2}
strokeDasharray="5 3"
fill="url(#gradSage)"
dot={false}
activeDot={{r: 4, fill: SAGE, stroke: 'rgb(247 244 239)', strokeWidth: 2}}
/>
{/* Total (amber, on top) */}
<Area
type="monotone"
dataKey="profilesCreations"
name={t('stats.total', 'Total')}
stroke={AMBER}
strokeWidth={2.5}
fill="url(#gradAmber)"
dot={false}
activeDot={{r: 5, fill: AMBER, stroke: 'rgb(247 244 239)', strokeWidth: 2}}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
)
}

View File

@@ -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) {
<div className="w-11 h-11 rounded-xl bg-canvas-200 border border-canvas-300 flex items-center justify-center text-xl flex-shrink-0">
{icon}
</div>
<div>
<div className={'min-w-0'}>
<h3 className="text-base font-bold text-ink-900 mb-2">{title}</h3>
<p className="text-sm text-ink-500 leading-relaxed">{text}</p>
</div>
@@ -132,11 +133,11 @@ function ShareStrip({title, text}: {title: string; text: string}) {
const t = useT()
return (
<div className="bg-canvas-950 rounded-2xl px-9 py-8 flex items-center justify-between gap-6 flex-wrap">
<div className={'max-w-[500px]'}>
<div className={'max-w-[450px]'}>
<h3 className="text-white text-lg font-bold mb-1.5">📣 {title}</h3>
<p className="text-primary-500 text-sm leading-relaxed">{text}</p>
</div>
<div className="flex gap-2 flex-wrap flex-shrink-0">
<Row className="flex gap-2 flex-wrap">
{/*// */}
{/*// ${*/}
{/*// 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',
)}
/>
</div>
</Row>
</div>
)
}

View File

@@ -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 (
<div className="max-w-3xl mb-8 mx-4 sm:mx-16 min-w-[200px]">
<h4 className="text-2xl font-bold mb-2">{title}</h4>
<p className="text-ink-600 mb-6">{description}</p>
<div className="flex flex-col gap-4">{children}</div>
<div
className="
group relative overflow-hidden
bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-7
transition-all duration-[120ms] ease-in
hover:shadow-[0_10px_30px_rgba(44,36,22,0.09)]
hover:border-primary-500
"
>
{/* Icon */}
<div className="w-11 h-11 rounded-xl bg-canvas-200 border border-canvas-300 flex items-center justify-center text-xl mb-5 flex-shrink-0">
{icon}
</div>
{/* Title & description */}
<h2 className="text-base font-bold text-ink-900 mb-2">{title}</h2>
<p className="text-sm text-ink-500 leading-relaxed mb-6">{description}</p>
{/* Links */}
<div className="flex flex-wrap gap-2">
{links.map(({url, label, primary}) => (
<GeneralButton
key={url}
url={url}
content={label}
color={
primary
? 'bg-primary-500 hover:bg-primary-600 text-white border-primary-500 shadow-[0_3px_12px_rgba(193,127,62,0.3)] text-sm'
: 'bg-canvas-100 border-canvas-300 text-ink-900 hover:border-primary-500 hover:text-primary-500 text-sm'
}
/>
))}
</div>
</div>
)
}
// ─── Section Label ────────────────────────────────────────────────────────────
function SectionLabel({children}: {children: ReactNode}) {
return (
<div className="flex items-center gap-3 mb-4">
<span className="text-[11px] font-bold tracking-[1.2px] uppercase text-ink-500">
{children}
</span>
<div className="flex-1 h-px bg-canvas-200" />
</div>
)
}
// ─── Divider ──────────────────────────────────────────────────────────────────
function Divider() {
return (
<div className="h-px bg-gradient-to-r from-transparent via-canvas-200 to-transparent my-8" />
)
}
// ─── 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 (
<PageBase trackPageView={'social'}>
<SEO
title={t('organization.seo.title', 'Organization')}
description={t('organization.seo.description', 'Organization')}
url={`/organization`}
url="/organization"
/>
<h3 className="text-4xl font-bold text-center mt-8 mb-8">
{t('organization.title', 'Organization')}
</h3>
{/* ABOUT */}
<Section
title={t('organization.about.title', 'About us')}
description={t(
'organization.about.desc',
'Who we are, our mission, and how the organization works.',
)}
>
<Row className={'flex-wrap'}>
<GeneralButton url={'/about'} content={t('about.seo.description', 'About Compass')} />
<GeneralButton
url={'/constitution'}
content={t('organization.constitution', 'Our constitution')}
/>
</Row>
</Section>
{/* PROOF */}
<Section
title={t('organization.proof.title', 'Proof & transparency')}
description={t(
'organization.proof.desc',
'Key numbers, progress, and what others say about us.',
)}
>
<Row className={'flex-wrap'}>
<GeneralButton url={'/stats'} content={t('organization.stats', 'Key metrics & growth')} />
<GeneralButton url={'/press'} content={t('press.title', 'Press')} />
<GeneralButton
url={'/financials'}
content={t('organization.financials', 'Financial transparency')}
/>
</Row>
</Section>
{/* CONTACT */}
<Section
title={t('organization.contactSection.title', 'Contact & support')}
description={t(
'organization.contactSection.desc',
'Need help or want to reach us? Start here.',
)}
>
<Row className={'flex-wrap'}>
<GeneralButton url={'/contact'} content={t('organization.contact', 'Contact us')} />
<GeneralButton url={'/help'} content={t('organization.help', 'Help & support center')} />
</Row>
</Section>
{/* TRUST / LEGAL */}
<Section
title={t('organization.trust.title', 'Trust & legal')}
description={t(
'organization.trust.desc',
'How we protect your data and the rules that govern the platform.',
)}
>
<Row className={'flex-wrap'}>
<GeneralButton url={'/security'} content={t('organization.security', 'Security')} />
<GeneralButton url={'/terms'} content={t('organization.terms', 'Terms and conditions')} />
<GeneralButton url={'/privacy'} content={t('organization.privacy', 'Privacy policy')} />
</Row>
</Section>
<div className="max-w-4xl mx-auto px-6 py-12 pb-20">
{/* ── Page header ── */}
<div className="mb-10">
<p className="text-xs font-bold tracking-[1.5px] uppercase text-primary-500 mb-3">
{t('organization.eyebrow', 'Compass')}
</p>
<h1 className="text-[clamp(28px,4vw,40px)] font-black text-ink-900 tracking-tight leading-tight mb-3">
{t('organization.title', 'Organization')}
</h1>
<p className="text-lg text-ink-500 max-w-2xl leading-relaxed">
{t(
'organization.subtitle',
'Everything about how Compass is run, governed, and built — transparently.',
)}
</p>
</div>
<Divider />
<SectionLabel>{t('organization.sections.label', 'Explore')}</SectionLabel>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{sections.map((s) => (
<SectionCard key={s.title} {...s} />
))}
</div>
</div>
</PageBase>
)
}

View File

@@ -69,7 +69,7 @@ const pressItems: PressItem[] = [
const PressItem = ({item}: {item: PressItem; locales: Intl.LocalesArgument}) => {
const t = useT()
return (
<div className="mb-8 px-6 pb-4 border border-canvas-200 rounded-lg shadow-md hover:shadow-lg transition-shadow">
<div className="mb-8 px-6 pb-4 border border-canvas-200 rounded-lg shadow-md hover:shadow-lg transition-shadow bg-canvas-50">
<h3 className="text-xl font-semibold mb-2">
<Link href={item.url} target="_blank" rel="noopener noreferrer" className="hover:underline">
{item.title}
@@ -130,7 +130,7 @@ export default function PressPage() {
<div className="mb-12">
<h2 className="text-2xl font-semibold mb-6">{t('press.media_kit', 'Media Kit')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="px-6 pb-4 border border-canvas-200 rounded-lg shadow">
<div className="px-6 pb-4 border border-canvas-200 rounded-lg shadow bg-canvas-50">
<h3 className="text-lg font-medium mb-3">
{t('press.brand_assets', 'Brand Assets')}
</h3>
@@ -139,19 +139,19 @@ export default function PressPage() {
</p>
<a
href="https://github.com/CompassConnections/assets/archive/refs/heads/main.zip"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none transition-all"
>
{t('press.download_assets', 'Download Assets')}
</a>
</div>
<div className="px-6 pb-4 border border-canvas-200 rounded-lg shadow">
<div className="px-6 pb-4 border border-canvas-200 rounded-lg shadow bg-canvas-50">
<h3 className="text-lg font-medium mb-3">{t('press.contact', 'Press Contact')}</h3>
<p className="-300 mb-4">
{t('press.contact_description', 'For press inquiries, please contact our team.')}
</p>
<a
href="mailto:hello@compassmeet.com"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none transition-all"
>
{t('press.contact_us', 'Contact Us')}
</a>

View File

@@ -8,7 +8,7 @@ export default function PrivacyPage() {
const t = useT()
return (
<PageBase trackPageView={'terms'} className="max-w-4xl mx-auto p-8 col-span-8 bg-canvas-50">
<PageBase trackPageView={'terms'} className="max-w-4xl mx-auto p-8 col-span-8">
<SEO
title={t('privacy.seo.title', 'Privacy')}
description={t('privacy.seo.description', 'Privacy Policy for Compass')}

View File

@@ -3,92 +3,200 @@ import {
githubRepo,
instagramLink,
redditLink,
stoatLink,
supportEmail,
xLink,
} from 'common/constants'
import {GeneralButton} from 'web/components/buttons/general-button'
import {Row} from 'web/components/layout/row'
import {ReactNode} from 'react'
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 SocialLink {
url: string
label: string
icon: string
primary?: boolean
}
interface SectionCardProps {
icon: string
title: string
description: string
children: React.ReactNode
}) {
links: SocialLink[]
}
// ─── Social Link Button ───────────────────────────────────────────────────────
function SocialLinkButton({url, label, icon, primary}: SocialLink) {
return (
<div className="max-w-3xl mb-8 mx-4 sm:mx-16 min-w-[200px]">
<h4 className="text-2xl font-bold mb-2">{title}</h4>
<p className="text-ink-600 mb-6">{description}</p>
<div className="flex flex-row gap-4">{children}</div>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className={`
inline-flex items-center gap-2.5 px-4 py-2.5 rounded-xl
border-[1.5px] text-sm font-semibold
transition-all duration-[120ms] ease-in
hover:-translate-y-0.5
${
primary
? 'bg-primary-500 border-primary-500 text-white hover:bg-primary-600 shadow-[0_3px_12px_rgba(193,127,62,0.3)]'
: 'bg-canvas-100 border-canvas-300 text-ink-900 hover:border-primary-500 hover:text-primary-500'
}
`}
>
{icon && <span className="text-base leading-none">{icon}</span>}
{label}
</a>
)
}
// ─── Section Card ─────────────────────────────────────────────────────────────
function SectionCard({icon, title, description, links}: SectionCardProps) {
return (
<div
className="
group relative overflow-hidden
bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-7
transition-all duration-[120ms] ease-in
hover:shadow-[0_10px_30px_rgba(44,36,22,0.09)]
hover:border-primary-500
"
>
{/* Icon */}
<div className="w-11 h-11 rounded-xl bg-canvas-200 border border-canvas-300 flex items-center justify-center text-xl mb-5 flex-shrink-0">
{icon}
</div>
{/* Title & description */}
<h2 className="text-base font-bold text-ink-900 mb-1.5">{title}</h2>
<p className="text-sm text-ink-500 leading-relaxed mb-5">{description}</p>
{/* Links */}
<div className="flex flex-wrap gap-2">
{links.map((link) => (
<SocialLinkButton key={link.url} {...link} />
))}
</div>
</div>
)
}
// ─── Section Label ────────────────────────────────────────────────────────────
function SectionLabel({children}: {children: ReactNode}) {
return (
<div className="flex items-center gap-3 mb-4">
<span className="text-[11px] font-bold tracking-[1.2px] uppercase text-ink-500">
{children}
</span>
<div className="flex-1 h-px bg-canvas-200" />
</div>
)
}
// ─── Divider ──────────────────────────────────────────────────────────────────
function Divider() {
return (
<div className="h-px bg-gradient-to-r from-transparent via-canvas-200 to-transparent my-8" />
)
}
// ─── 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 (
<PageBase trackPageView={'social'}>
<SEO
title={t('social.seo.title', 'Socials')}
description={t('social.seo.description', 'All our social channels and contact info')}
url={`/social`}
url="/social"
/>
<h3 className="text-4xl font-bold text-center mt-8 mb-8">{t('social.title', 'Socials')}</h3>
<div className="max-w-4xl mx-auto px-6 py-12 pb-20">
{/* ── Page header ── */}
<div className="mb-10">
<p className="text-xs font-bold tracking-[1.5px] uppercase text-primary-500 mb-3">
{t('social.eyebrow', 'Connect with us')}
</p>
<h1 className="text-[clamp(28px,4vw,40px)] font-black text-ink-900 tracking-tight leading-tight mb-3">
{t('social.title', 'Socials')}
</h1>
<p className="text-lg text-ink-500 max-w-2xl leading-relaxed">
{t(
'social.subtitle',
'Find us across the web — join the conversation, follow updates, or say hello.',
)}
</p>
</div>
{/* COMMUNITY */}
<Section
title={t('social.community.title', 'Community')}
description={t('social.community.desc', 'Join our community chats and discussions.')}
>
<Row className="flex-wrap">
<GeneralButton url={discordLink} content={t('social.discord', 'Discord')} />
<GeneralButton url={redditLink} content={t('social.reddit', 'Reddit')} />
<GeneralButton url={stoatLink} content={t('social.stoat', 'Revolt / Stoat')} />
</Row>
</Section>
<Divider />
{/* FOLLOW / UPDATES */}
<Section
title={t('social.follow.title', 'Follow & Updates')}
description={t('social.follow.desc', 'Stay informed about announcements and news.')}
>
<Row className="flex-wrap">
<GeneralButton url={xLink} content={t('social.x', 'X')} />
<GeneralButton url={instagramLink} content={t('social.instagram', 'Instagram')} />
</Row>
</Section>
{/* ── Cards ── */}
<SectionLabel>{t('social.sections.label', 'Our channels')}</SectionLabel>
{/* DEVELOPMENT */}
<Section
title={t('social.dev.title', 'Development')}
description={t('social.dev.desc', 'See our source code or contribute.')}
>
<Row className="flex-wrap">
<GeneralButton url={githubRepo} content={t('social.github', 'GitHub')} />
</Row>
</Section>
{/* CONTACT */}
<Section
title={t('social.contact.title', 'Contact')}
description={t('social.contact.desc', 'Reach out to us directly for inquiries.')}
>
<Row className="flex-wrap">
<GeneralButton
url={`mailto:${supportEmail}`}
content={`${t('social.email_button', 'Email')} ${supportEmail}`}
/>
</Row>
</Section>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{sections.map((s) => (
<SectionCard key={s.title} {...s} />
))}
</div>
</div>
</PageBase>
)
}

View File

@@ -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 (
<div
className="
group relative overflow-hidden
bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-6
transition-all duration-[120ms] ease-in
hover:shadow-[0_10px_30px_rgba(44,36,22,0.09)]
hover:border-primary-500
"
>
<div className="flex items-start justify-between mb-3">
<div className="w-9 h-9 rounded-lg bg-canvas-200 border border-canvas-300 flex items-center justify-center text-base flex-shrink-0">
{icon}
</div>
</div>
<div
className={clsx(
'font-black tracking-tight leading-none mb-2',
large ? 'text-4xl' : 'text-3xl',
accentClasses[accent],
)}
>
{formatted}
</div>
<p className="text-xs font-semibold text-ink-500 uppercase tracking-wide leading-tight">
{label}
</p>
</div>
)
}
// ─── Stat Group ───────────────────────────────────────────────────────────────
function StatGroup({icon, title, children}: StatGroupProps) {
return (
<div className="mb-10">
<div className="flex items-center gap-3 mb-4">
<span className="text-base">{icon}</span>
<span className="text-[11px] font-bold tracking-[1.2px] uppercase text-ink-500">
{title}
</span>
<div className="flex-1 h-px bg-canvas-200" />
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">{children}</div>
</div>
)
}
// ─── Chart wrapper ────────────────────────────────────────────────────────────
function ChartCard() {
const t = useT()
return (
<div
className="
bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-6 mb-10
shadow-[0_2px_8px_rgba(44,36,22,0.05)]
"
>
<div className="flex items-center gap-2 mb-1">
<div className="w-8 h-8 rounded-lg bg-canvas-200 border border-canvas-300 flex items-center justify-center text-sm">
📈
</div>
<div>
<h2 className="text-sm font-bold text-ink-900 leading-tight">
{t('stats.chart.title', 'Member Growth')}
</h2>
<p className="text-xs text-ink-500">
{t('stats.chart.subtitle', 'Total & completed profiles over time')}
</p>
</div>
</div>
<ChartMembers />
</div>
)
}
// ─── 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 (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-10">
{items.map((item) => (
<div
key={item.label}
className="
relative overflow-hidden
bg-canvas-950 rounded-2xl p-6
border-[1.5px] border-canvas-900
"
>
<div className="w-9 h-9 rounded-lg bg-canvas-900 flex items-center justify-center text-base mb-4">
{item.icon}
</div>
<div
className={clsx(
'text-4xl font-black tracking-tight leading-none mb-2',
item.accent === 'sage' ? 'text-green-500' : 'text-primary-400',
)}
>
{typeof item.value === 'number' ? formatNumber(item.value) : item.value}
</div>
<p className="text-xs font-semibold text-white/60 uppercase tracking-wide">
{item.label}
</p>
</div>
))}
</div>
)
}
// ─── Page ─────────────────────────────────────────────────────────────────────
export default function Stats() {
const t = useT()
const [data, setData] = useState<Record<string, number | null>>({})
const [statsData, setStatsData] = useState<Stats | undefined>(undefined)
const [loading, setLoading] = useState(true)
useEffect(() => {
async function load() {
@@ -37,22 +220,23 @@ export default function Stats() {
const result: Record<string, number | null> = {}
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 (
<PageBase trackPageView={'stats'}>
<SEO
@@ -61,75 +245,138 @@ export default function Stats() {
'stats.seo.description',
'Explore Compass platform growth metrics, member statistics, active discussions, and community engagement data.',
)}
url={`/stats`}
url="/stats"
/>
<h1 className="text-3xl font-semibold text-center mb-6">
{t('stats.title', 'Growth & Stats')}
</h1>
<Col className={'sm:mx-4 mx-1 mb-8'}>
<ChartMembers />
</Col>
<Col className={'mx-4 mb-8'}>
<Col
className={clsx(
'pb-[58px] lg:pb-0', // bottom bar padding
'text-ink-1000 mx-auto w-full grid grid-cols-1 gap-8 max-w-3xl sm:grid-cols-2 lg:min-h-0 lg:pt-4 mt-4',
)}
>
{!!data.profiles && (
<StatBox value={data.profiles} label={t('stats.members', 'Members')} />
)}
{!!data.active_members && (
<StatBox
value={data.active_members}
label={t('stats.active_members', 'Active Members (last month)')}
<div className="max-w-4xl mx-auto px-6 py-12 pb-20">
{/* ── Page header ── */}
<div className="mb-10">
<p className="text-xs font-bold tracking-[1.5px] uppercase text-primary-500 mb-3">
{t('stats.eyebrow', 'Transparency')}
</p>
<h1 className="text-[clamp(28px,4vw,40px)] font-black text-ink-900 tracking-tight leading-tight mb-3">
{t('stats.title', 'Growth & Stats')}
</h1>
<p className="text-lg text-ink-500 max-w-3xl leading-relaxed">
{t(
'stats.subtitle',
"Real numbers. No spin. Compass is built in the open — here's exactly how we're growing.",
)}
</p>
</div>
{/* ── Loading skeleton ── */}
{loading && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-10">
{Array.from({length: 6}).map((_, i) => (
<div
key={i}
className="bg-canvas-50 border-[1.5px] border-canvas-200 rounded-2xl p-6 animate-pulse"
>
<div className="w-9 h-9 rounded-lg bg-canvas-200 mb-4" />
<div className="h-8 w-20 bg-canvas-200 rounded mb-2" />
<div className="h-3 w-24 bg-canvas-200 rounded" />
</div>
))}
</div>
)}
{!loading && (
<>
{/* ── Hero highlight row ── */}
<HighlightRow
members={data.profiles}
active={data.active_members}
messages={statsData?.messages}
/>
)}
{!!data.private_user_message_channels && (
<StatBox
value={data.private_user_message_channels}
label={t('stats.discussions', 'Discussions')}
/>
)}
{!!statsData?.messages && (
<StatBox value={statsData?.messages} label={t('stats.messages', 'Messages')} />
)}
{!!data.compatibility_prompts && (
<StatBox
value={data.compatibility_prompts}
label={t('stats.compatibility_prompts', 'Compatibility Prompts')}
/>
)}
{!!data.compatibility_answers && (
<StatBox
value={data.compatibility_answers}
label={t('stats.prompts_answered', 'Prompts Answered')}
/>
)}
{!!data.votes && <StatBox value={data.votes} label={t('stats.proposals', 'Proposals')} />}
{!!data.vote_results && (
<StatBox value={data.vote_results} label={t('stats.votes', 'Votes')} />
)}
{!!data.bookmarked_searches && (
<StatBox
value={data.bookmarked_searches}
label={t('stats.searches_bookmarked', 'Searches Bookmarked')}
/>
)}
{!!data.profile_comments && (
<StatBox
value={data.profile_comments}
label={t('stats.endorsements', 'Endorsements')}
/>
)}
{!!statsData?.genderRatio && (
<StatBox
value={`${statsData.genderRatio.male ?? 0} / ${statsData.genderRatio.female ?? 0}`}
label={t('stats.gender_ratio', 'Male / Female Ratio')}
/>
)}
</Col>
</Col>
{/* ── Growth chart ── */}
<ChartCard />
{/* ── Community ── */}
<StatGroup icon="👥" title={t('stats.group.community', 'Community')}>
<StatCard
value={data.profiles}
label={t('stats.members', 'Members')}
icon="👤"
accent="amber"
/>
<StatCard
value={data.active_members}
label={t('stats.active_members', 'Active (last month)')}
icon="⚡"
accent="sage"
/>
<StatCard
value={genderRatioLabel}
label={t('stats.gender_ratio', 'Male / Female')}
icon="⚖️"
accent="muted"
/>
<StatCard
value={data.profile_comments}
label={t('stats.endorsements', 'Endorsements')}
icon="⭐"
accent="amber"
/>
</StatGroup>
{/* ── Conversations ── */}
<StatGroup icon="💬" title={t('stats.group.conversations', 'Conversations')}>
<StatCard
value={data.private_user_message_channels}
label={t('stats.discussions', 'Discussions')}
icon="🗨️"
accent="amber"
/>
<StatCard
value={statsData?.messages}
label={t('stats.messages', 'Messages')}
icon="✉️"
accent="sage"
/>
</StatGroup>
{/* ── Compatibility ── */}
<StatGroup icon="🎯" title={t('stats.group.compatibility', 'Compatibility')}>
<StatCard
value={data.compatibility_prompts}
label={t('stats.compatibility_prompts', 'Prompts Created')}
icon="❓"
accent="amber"
/>
<StatCard
value={data.compatibility_answers}
label={t('stats.prompts_answered', 'Prompts Answered')}
icon="✅"
accent="sage"
/>
</StatGroup>
{/* ── Democracy ── */}
<StatGroup icon="🗳️" title={t('stats.group.democracy', 'Democracy')}>
<StatCard
value={data.votes}
label={t('stats.proposals', 'Proposals')}
icon="📋"
accent="amber"
/>
<StatCard
value={data.vote_results}
label={t('stats.votes', 'Votes Cast')}
icon="🗳️"
accent="sage"
/>
{/*<StatCard*/}
{/* value={data.bookmarked_searches}*/}
{/* label={t('stats.searches_bookmarked', 'Saved Searches')}*/}
{/* icon="🔖"*/}
{/* accent="muted"*/}
{/*/>*/}
</StatGroup>
</>
)}
</div>
</PageBase>
)
}

View File

@@ -9,7 +9,7 @@ export default function TermsPage() {
return (
<PageBase
trackPageView={'terms'}
className="max-w-4xl mx-auto p-8 text-gray-800 dark:text-white col-span-8 bg-canvas-50"
className="max-w-4xl mx-auto p-8 text-gray-800 dark:text-white col-span-8"
>
<SEO
title={t('terms.seo.title', 'Terms & Conditions')}

View File

@@ -98,6 +98,8 @@
:root {
touch-action: pan-y;
--grid-stroke: rgb(232 213 188); /* canvas-200 */
/* Ink - Text Colors */
--color-ink-900: 30 26 20; /* Deep Warm Black (#1E1A14) */
--color-ink-500: 140 128 112; /* Muted Warm Gray (#8C8070) */
@@ -211,6 +213,8 @@
.dark {
color-scheme: dark;
--grid-stroke: rgb(68 52 34); /* canvas-900 */
/* Ink - Text becomes light */
--color-ink-950: 255 255 255; /* Purest highlight */
--color-ink-900: 247 244 239; /* Main Body Text (Old Card color) */