mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-05-08 23:25:01 -04:00
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:
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ')}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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) */
|
||||
|
||||
Reference in New Issue
Block a user