mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 17:59:13 -04:00
Add structured FAQ page with search, categories, and deep links
- Implemented `parseFaq` to transform markdown into structured data for improved rendering and SEO. - Introduced `react-markdown` with custom styling for better alignment with the application design. - Added search functionality with query matching and automatic expansion of relevant answers. - Designed a sticky navigation rail for easier category navigation. - Enabled deep linking to questions and categories for external reference. - Created reusable components for FAQ items and markdown rendering. - Updated markdown file handling to exclude FAQ from generic loader (`MD_PATHS`).
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"version": "1.50.0",
|
||||
"version": "1.51.0",
|
||||
"private": true,
|
||||
"description": "Backend API endpoints",
|
||||
"main": "src/serve.ts",
|
||||
|
||||
@@ -1679,5 +1679,25 @@
|
||||
"share_profile.x_share_profile": "Ich bin auf einer Plattform, die Menschen nach Werten, nicht nach Fotos zusammenbringt.\nKeine Werbung. Keine Algorithmen. Kein Wischen. Nur Tiefe.",
|
||||
"email.new_message.daily_limit": "Um deine Inbox nicht zu überladen, erhältst du höchstens eine E-Mail pro Tag pro Konversation. Wenn {creatorName} heute weitere Nachrichten sendet, wirst du erst morgen wieder per E-Mail benachrichtigt.",
|
||||
"email.new_message.app_prompt": "Willst du Echtzeit-Benachrichtigungen? Lade die Compass App für Android herunter:",
|
||||
"email.new_message.app_link": "App herunterladen"
|
||||
"email.new_message.app_link": "App herunterladen",
|
||||
"faq.empty.cta": "Frage stellen →",
|
||||
"faq.empty.text": "Versuche ein allgemeineres Wort — oder frag uns direkt, wir beantworten jede Nachricht.",
|
||||
"faq.empty.title": "Dazu passt hier nichts.",
|
||||
"faq.eyebrow": "Hilfebereich",
|
||||
"faq.nav.label": "FAQ-Kategorien",
|
||||
"faq.nav.title": "Themen",
|
||||
"faq.search.clear": "Suche zurücksetzen",
|
||||
"faq.search.count": "{count} passende Fragen",
|
||||
"faq.search.label": "FAQ durchsuchen",
|
||||
"faq.search.none": "Keine Frage passt — versuche ein anderes Wort.",
|
||||
"faq.search.placeholder": "FAQ durchsuchen…",
|
||||
"faq.seo.description": "Häufige Fragen zu Compass",
|
||||
"faq.seo.title": "FAQ",
|
||||
"faq.stuck.contact": "Kontakt aufnehmen",
|
||||
"faq.stuck.discord": "Auf Discord fragen",
|
||||
"faq.stuck.label": "Noch offen",
|
||||
"faq.stuck.text": "Compass wird von den Menschen gebaut, die es nutzen — frag alles, und die Antwort landet meist auf dieser Seite für die nächste Person.",
|
||||
"faq.stuck.title": "Antwort nicht gefunden?",
|
||||
"faq.title": "Häufig gestellte Fragen",
|
||||
"faq.search.count_one": "1 passende Frage"
|
||||
}
|
||||
|
||||
@@ -1678,5 +1678,25 @@
|
||||
"share_profile.x_share_profile": "Je suis sur une plateforme qui met les gens en relation selon leurs valeurs, pas leurs photos.\nPas de pubs. Pas d'algorithmes. Pas de swipe. Juste de la profondeur.",
|
||||
"email.new_message.daily_limit": "Pour éviter de surcharger votre boîte de réception, vous recevrez au plus un e-mail par jour par conversation. Si {creatorName} envoie d'autres messages aujourd'hui, vous ne serez pas notifié par e-mail.",
|
||||
"email.new_message.app_prompt": "Vous voulez des notifications en temps réel ? Téléchargez l'application Compass pour Android :",
|
||||
"email.new_message.app_link": "Obtenir l'app"
|
||||
"email.new_message.app_link": "Obtenir l'app",
|
||||
"faq.empty.cta": "Poser une question →",
|
||||
"faq.empty.text": "Essayez un mot plus général — ou posez-nous directement la question, nous répondons à chaque message.",
|
||||
"faq.empty.title": "Aucun résultat pour cette recherche.",
|
||||
"faq.eyebrow": "Centre d’aide",
|
||||
"faq.nav.label": "Catégories de la FAQ",
|
||||
"faq.nav.title": "Thèmes",
|
||||
"faq.search.clear": "Effacer la recherche",
|
||||
"faq.search.count": "{count} questions correspondantes",
|
||||
"faq.search.label": "Rechercher dans la FAQ",
|
||||
"faq.search.none": "Aucune question ne correspond — essayez un autre mot.",
|
||||
"faq.search.placeholder": "Rechercher dans la FAQ…",
|
||||
"faq.seo.description": "Questions fréquentes sur Compass",
|
||||
"faq.seo.title": "FAQ",
|
||||
"faq.stuck.contact": "Nous contacter",
|
||||
"faq.stuck.discord": "Demander sur Discord",
|
||||
"faq.stuck.label": "Toujours bloqué",
|
||||
"faq.stuck.text": "Compass est construit par celles et ceux qui l’utilisent — posez n’importe quelle question, la réponse finit généralement sur cette page pour la personne suivante.",
|
||||
"faq.stuck.title": "Vous n’avez pas trouvé votre réponse ?",
|
||||
"faq.title": "Questions fréquentes",
|
||||
"faq.search.count_one": "1 question correspondante"
|
||||
}
|
||||
|
||||
367
web/components/faq/faq-content.tsx
Normal file
367
web/components/faq/faq-content.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
ChatBubbleLeftRightIcon,
|
||||
LifebuoyIcon,
|
||||
MagnifyingGlassIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {discordLink} from 'common/constants'
|
||||
import Link from 'next/link'
|
||||
import {useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {eyebrow, surface} from 'web/components/widgets/surface'
|
||||
import {FaqDoc, FaqQuestion, normalizeForSearch, toPlainText} from 'web/lib/faq'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
import {FaqItem} from './faq-item'
|
||||
import {FaqMarkdown} from './faq-markdown'
|
||||
|
||||
/**
|
||||
* The FAQ page body.
|
||||
*
|
||||
* Three things carry it, in order of how much they matter for a 22-question page:
|
||||
*
|
||||
* 1. **Search.** Collapsing the answers is what makes the page scannable, but it also breaks the
|
||||
* browser's own find-in-page — so the search box is not a nicety here, it is the replacement for
|
||||
* the thing collapsing took away. It matches question *and* answer text, and expands its hits.
|
||||
* 2. **Categories.** Twenty-two flat questions is a wall; five labelled groups with a sticky rail is
|
||||
* a table of contents. The groups come from `##` headings in the markdown, so translators get them
|
||||
* as ordinary text.
|
||||
* 3. **Deep links.** Every question has a slug id and the URL updates as you open one, so a support
|
||||
* answer can be linked to directly instead of described.
|
||||
*/
|
||||
export function FaqContent({doc}: {doc: FaqDoc}) {
|
||||
const t = useT()
|
||||
const [query, setQuery] = useState('')
|
||||
const [open, setOpen] = useState<Set<string>>(() => new Set())
|
||||
const [activeCategory, setActiveCategory] = useState<string | undefined>(doc.categories[0]?.id)
|
||||
const sectionRefs = useRef<Record<string, HTMLElement | null>>({})
|
||||
|
||||
// Built once per document rather than per keystroke: `toPlainText` runs over every answer, which is
|
||||
// wasted work to repeat on each of the ~20 renders a typed query produces.
|
||||
//
|
||||
// Question and answer only — deliberately *not* the category title. Folding the title in meant every
|
||||
// question in "Money & governance" answered a search for "governance", so a query for one topic word
|
||||
// returned four unrelated questions alongside the real hit. Navigating by topic is the rail's job.
|
||||
const index = useMemo(
|
||||
() =>
|
||||
doc.categories.flatMap((c) =>
|
||||
c.questions.map((q) => ({
|
||||
id: q.id,
|
||||
haystack: normalizeForSearch(`${q.question} ${toPlainText(q.answer)}`),
|
||||
})),
|
||||
),
|
||||
[doc],
|
||||
)
|
||||
|
||||
const matches = useMemo(() => {
|
||||
const terms = normalizeForSearch(query).split(/\s+/).filter(Boolean)
|
||||
if (!terms.length) return undefined
|
||||
// Every term must appear somewhere in the entry, in any order — "data safe" and "safe data" find
|
||||
// the same question. Substring rather than word-boundary matching, so "encrypt" finds
|
||||
// "encryption".
|
||||
return new Set(
|
||||
index.filter((e) => terms.every((term) => e.haystack.includes(term))).map((e) => e.id),
|
||||
)
|
||||
}, [index, query])
|
||||
|
||||
const visibleCategories = useMemo(
|
||||
() =>
|
||||
doc.categories
|
||||
.map((c) => ({...c, questions: c.questions.filter((q) => !matches || matches.has(q.id))}))
|
||||
.filter((c) => c.questions.length > 0),
|
||||
[doc, matches],
|
||||
)
|
||||
|
||||
const resultCount = visibleCategories.reduce((n, c) => n + c.questions.length, 0)
|
||||
|
||||
const toggle = (q: FaqQuestion) => {
|
||||
setOpen((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(q.id)) next.delete(q.id)
|
||||
else next.add(q.id)
|
||||
return next
|
||||
})
|
||||
// `replaceState` rather than a router push: opening a question should be linkable, but it is not
|
||||
// a navigation, and pushing would make Back walk through every card the reader expanded.
|
||||
if (typeof window !== 'undefined' && !open.has(q.id)) {
|
||||
window.history.replaceState(null, '', `#${q.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// A search that returns three questions and leaves them all shut has answered nothing — the reader
|
||||
// then has to click each one. Any hit set small enough to read at once is opened outright.
|
||||
useEffect(() => {
|
||||
if (!matches || matches.size === 0 || matches.size > 6) return
|
||||
setOpen((prev) => new Set([...prev, ...matches]))
|
||||
}, [matches])
|
||||
|
||||
// Deep links, on mount *and* on `hashchange`.
|
||||
//
|
||||
// The mount case is the shared link opened cold. The `hashchange` case is the same link followed by
|
||||
// someone already on /faq — a same-document navigation, so the component never remounts and the
|
||||
// browser scrolls to a question that is still collapsed. Back/forward between two question anchors
|
||||
// has the same shape. The rail's own `#cat-` links land here too and are ignored, since no question
|
||||
// carries that id.
|
||||
useEffect(() => {
|
||||
const openFromHash = () => {
|
||||
const id = decodeURIComponent(window.location.hash.replace('#', ''))
|
||||
if (!id) return
|
||||
setOpen((prev) => new Set([...prev, id]))
|
||||
// Deferred a frame: the panel has to be expanded before its position is worth measuring, or the
|
||||
// scroll lands short by the height of the answer.
|
||||
requestAnimationFrame(() =>
|
||||
document.getElementById(id)?.scrollIntoView({block: 'start', behavior: 'smooth'}),
|
||||
)
|
||||
}
|
||||
openFromHash()
|
||||
window.addEventListener('hashchange', openFromHash)
|
||||
return () => window.removeEventListener('hashchange', openFromHash)
|
||||
}, [])
|
||||
|
||||
// Scroll-spy for the rail: the active category is the last one whose heading has crossed a line
|
||||
// near the top of the viewport.
|
||||
//
|
||||
// Not an IntersectionObserver, which is the reflex here and is wrong for it. An observer only fires
|
||||
// on a *crossing*, so once a section taller than the trigger band has scrolled past, nothing
|
||||
// intersects the band and the highlight sticks on whatever last entered it — which on this page
|
||||
// means the rail says "The basics" while you are reading "Vision & roadmap". Answers expand and
|
||||
// collapse under the reader too, moving every section below them, and a position check re-derives
|
||||
// the answer from scratch on the next scroll frame where an observer would need re-observing.
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
const line = 140
|
||||
let active: string | undefined
|
||||
for (const [id, el] of Object.entries(sectionRefs.current)) {
|
||||
if (el && el.getBoundingClientRect().top <= line) active = id
|
||||
}
|
||||
setActiveCategory(active ?? visibleCategories[0]?.id)
|
||||
}
|
||||
update()
|
||||
window.addEventListener('scroll', update, {passive: true})
|
||||
window.addEventListener('resize', update)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', update)
|
||||
window.removeEventListener('resize', update)
|
||||
}
|
||||
}, [visibleCategories])
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-5 sm:px-8 pt-12 pb-24">
|
||||
{/* ── Header ── */}
|
||||
<div className="mb-10">
|
||||
<p className={clsx(eyebrow, 'text-primary-700 mb-4')}>{t('faq.eyebrow', 'Help center')}</p>
|
||||
<h1 className="text-[clamp(34px,5vw,56px)] text-ink-900 tracking-tight leading-[1.08] mb-5 max-w-3xl text-balance">
|
||||
{t('faq.title', 'Frequently asked questions')}
|
||||
</h1>
|
||||
{doc.intro && (
|
||||
<FaqMarkdown className="max-w-2xl !text-lg text-ink-700">{doc.intro}</FaqMarkdown>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Search ── */}
|
||||
<div className="relative max-w-xl">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-ink-400"
|
||||
strokeWidth={2}
|
||||
aria-hidden
|
||||
/>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t('faq.search.placeholder', 'Search the FAQ…')}
|
||||
aria-label={t('faq.search.label', 'Search the FAQ')}
|
||||
className={clsx(
|
||||
'w-full rounded-2xl bg-canvas-50 py-3.5 pl-12 pr-11 text-base text-ink-900 placeholder:text-ink-400',
|
||||
// `appearance-none border-0` before the ring: `type="search"` keeps a UA `1px solid grey`
|
||||
// border that Tailwind's preflight does not zero out, so the field drew two edges — a cool
|
||||
// grey one inside our warm `canvas-200` ring. The ring is the only edge we want.
|
||||
'appearance-none border-0 ring-1 ring-canvas-200 transition-shadow',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary-500/50',
|
||||
// The native clear affordance is Chrome-only and sits where our own button goes.
|
||||
'[&::-webkit-search-cancel-button]:appearance-none',
|
||||
)}
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuery('')}
|
||||
aria-label={t('faq.search.clear', 'Clear search')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full p-1.5 text-ink-500 transition-colors hover:bg-canvas-100 hover:text-ink-800"
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" strokeWidth={2.2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* Announced, not just shown: filtering happens with focus still in the input, so a screen
|
||||
reader user gets no signal that the page below them changed. */}
|
||||
<p aria-live="polite" className="mt-3 min-h-5 text-sm text-ink-500">
|
||||
{query &&
|
||||
(resultCount === 0
|
||||
? t('faq.search.none', 'No questions match — try another word.')
|
||||
: // A separate key for one hit rather than one string with `{count}`. `t()` has no
|
||||
// pluralisation, so the single template renders "1 passende Fragen" in German and
|
||||
// "1 questions correspondantes" in French — wrong in both, and a search for a specific
|
||||
// term lands on exactly one hit often enough to be the common case.
|
||||
resultCount === 1
|
||||
? t('faq.search.count_one', '1 matching question')
|
||||
: t('faq.search.count', '{count} matching questions', {count: resultCount}))}
|
||||
</p>
|
||||
|
||||
{/* ── Body ── */}
|
||||
<div className="mt-8 grid gap-10 lg:grid-cols-[190px_minmax(0,1fr)] lg:gap-14">
|
||||
{/* The rail. Hidden below `lg` — at tablet width it either steals the column the answers need
|
||||
or wraps into a chip row that duplicates the section headings two lines below it. */}
|
||||
<nav aria-label={t('faq.nav.label', 'FAQ categories')} className="hidden lg:block">
|
||||
<div className="sticky top-24">
|
||||
<p className={clsx(eyebrow, 'text-ink-500 mb-4')}>{t('faq.nav.title', 'Topics')}</p>
|
||||
{/* `list-none pl-0 mt-0` — globals.css puts a disc and a 1.25rem indent on every `ul`. */}
|
||||
<ul className="mt-0 list-none space-y-1 pl-0">
|
||||
{doc.categories.map((c) => {
|
||||
const active = c.id === activeCategory
|
||||
const empty = !visibleCategories.some((v) => v.id === c.id)
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<a
|
||||
href={`#cat-${c.id}`}
|
||||
className={clsx(
|
||||
'block border-l-2 py-1.5 pl-3 text-sm transition-colors',
|
||||
active
|
||||
? 'border-primary-500 font-semibold text-primary-800'
|
||||
: 'border-canvas-200 text-ink-600 hover:border-ink-400 hover:text-ink-900',
|
||||
// Dimmed rather than removed while searching: a rail whose items disappear as
|
||||
// you type reads as breakage, and the layout jumping is worse than a grey row.
|
||||
empty && 'opacity-40',
|
||||
)}
|
||||
>
|
||||
{c.title}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0">
|
||||
{visibleCategories.map((category, i) => (
|
||||
<section
|
||||
key={category.id}
|
||||
id={`cat-${category.id}`}
|
||||
ref={(el) => {
|
||||
sectionRefs.current[category.id] = el
|
||||
}}
|
||||
className={clsx('scroll-mt-24', i > 0 && 'mt-12')}
|
||||
>
|
||||
{category.title && (
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
{/* A real `h2` so the categories are in the document outline, but styled as the
|
||||
same eyebrow /about uses — hence `m-0 font-figtree`, which undoes the global
|
||||
heading margins and the serif face that would otherwise land on an 11px cap. */}
|
||||
<h2 className={clsx(eyebrow, 'm-0 shrink-0 font-figtree text-ink-700')}>
|
||||
{category.title}
|
||||
</h2>
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-canvas-200 to-transparent" />
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{category.questions.map((q) => (
|
||||
<FaqItem key={q.id} item={q} open={open.has(q.id)} onToggle={() => toggle(q)} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
{resultCount === 0 && (
|
||||
<div className={clsx(surface, 'px-6 py-12 text-center')}>
|
||||
<MagnifyingGlassIcon
|
||||
className="mx-auto mb-4 h-8 w-8 text-ink-300"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden
|
||||
/>
|
||||
<p className="font-semibold text-ink-900">
|
||||
{t('faq.empty.title', 'Nothing here matches that.')}
|
||||
</p>
|
||||
<p className="mx-auto mt-2 max-w-sm text-sm text-ink-600">
|
||||
{t(
|
||||
'faq.empty.text',
|
||||
'Try a broader word — or ask us directly, we answer every message.',
|
||||
)}
|
||||
</p>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="mt-5 inline-flex items-center gap-2 rounded-xl border border-cta bg-cta px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-cta-hover"
|
||||
>
|
||||
{t('faq.empty.cta', 'Ask a question →')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outside the grid, not inside the answer column: the answer column is ~520px once the rail
|
||||
and the app sidebar are subtracted, which is narrow enough that a two-column closing block
|
||||
placed in it wraps its own headline to three words a line. The finale spans the page, the
|
||||
way the closing strip on /about does. */}
|
||||
<StillStuck />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The closing block.
|
||||
*
|
||||
* An FAQ that ends on its last answer leaves the reader whose question was not on the list with
|
||||
* nowhere to go, and that reader is the one most worth catching. Dark, like the closing strip on
|
||||
* `/about`, so the page ends on something rather than trailing off.
|
||||
*/
|
||||
function StillStuck() {
|
||||
const t = useT()
|
||||
|
||||
return (
|
||||
<div className="relative mt-14 overflow-hidden rounded-3xl bg-canvas-950 px-7 py-10 sm:px-10">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-16 -top-24 h-[360px] w-[360px] rounded-full bg-primary-500/20 blur-3xl"
|
||||
/>
|
||||
<div className="relative flex flex-col gap-7 md:flex-row md:items-center md:justify-between">
|
||||
<div className="max-w-lg">
|
||||
<div className="mb-4 flex items-center gap-2.5">
|
||||
<LifebuoyIcon className="h-5 w-5 flex-shrink-0 text-primary-500" strokeWidth={1.8} />
|
||||
<span className={clsx(eyebrow, 'text-primary-500')}>
|
||||
{t('faq.stuck.label', 'Still stuck')}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-heading mt-0 mb-3 text-[clamp(22px,2.4vw,30px)] font-bold leading-tight tracking-tight text-white text-balance">
|
||||
{t('faq.stuck.title', "Didn't find your answer?")}
|
||||
</h2>
|
||||
<p className="text-base leading-relaxed text-white/70">
|
||||
{t(
|
||||
'faq.stuck.text',
|
||||
'Compass is built by the people who use it — ask anything, and the answer usually ends up on this page for the next person.',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 flex-wrap gap-3">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-cta bg-cta px-5 py-2.5 text-sm font-semibold text-white shadow-[0_6px_20px_-6px_rgba(193,127,62,0.6)] transition-colors hover:bg-cta-hover"
|
||||
>
|
||||
{t('faq.stuck.contact', 'Contact us')}
|
||||
</Link>
|
||||
<a
|
||||
href={discordLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-white/20 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-white/10"
|
||||
>
|
||||
<ChatBubbleLeftRightIcon className="h-[1.05rem] w-[1.05rem]" strokeWidth={2} />
|
||||
{t('faq.stuck.discord', 'Ask on Discord')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
96
web/components/faq/faq-item.tsx
Normal file
96
web/components/faq/faq-item.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import {ChevronDownIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {surface, surfaceHover} from 'web/components/widgets/surface'
|
||||
import {FaqQuestion} from 'web/lib/faq'
|
||||
|
||||
import {FaqMarkdown} from './faq-markdown'
|
||||
|
||||
/**
|
||||
* One question, as a disclosure card.
|
||||
*
|
||||
* **The answer stays in the DOM when collapsed.** It is hidden by a `grid-template-rows` collapse, not
|
||||
* by unmounting — which is what keeps 22 answers' worth of text in the served HTML for crawlers, and
|
||||
* what lets a `#hash` deep link scroll to a question that has not been opened yet. Conditionally
|
||||
* rendering the body would have quietly traded away the SEO the build-time parse was for.
|
||||
*
|
||||
* The `0fr → 1fr` grid trick rather than a `max-height` guess: answers here range from two lines to
|
||||
* roughly thirty, so any single `max-height` is either a clipped answer or a long dead pause on the
|
||||
* short ones. Grid animates to the content's real height with no measurement.
|
||||
*/
|
||||
export function FaqItem({
|
||||
item,
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
item: FaqQuestion
|
||||
open: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
id={item.id}
|
||||
className={clsx(
|
||||
surface,
|
||||
!open && surfaceHover,
|
||||
// `scroll-mt` clears the sticky header when a deep link or a nav click scrolls here — without
|
||||
// it the question lands underneath the chrome.
|
||||
'scroll-mt-28 overflow-hidden',
|
||||
open && 'ring-primary-500/30',
|
||||
)}
|
||||
>
|
||||
{/* `m-0` because globals.css gives every heading `margin: 1.5rem 0 0.5rem` — inside a card whose
|
||||
padding already sets the spacing, that is 32px of dead height per question, times 22. */}
|
||||
<h3 className="m-0 text-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
aria-expanded={open}
|
||||
aria-controls={`${item.id}-panel`}
|
||||
className="flex w-full items-start gap-4 px-5 py-4 text-left sm:px-6 sm:py-5"
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
'flex-1 font-semibold leading-snug transition-colors',
|
||||
open ? 'text-primary-800' : 'text-ink-900',
|
||||
)}
|
||||
>
|
||||
{item.question}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className={clsx(
|
||||
'mt-0.5 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full transition-all duration-200 ease-out',
|
||||
open ? 'rotate-180 bg-primary-100 text-primary-700' : 'bg-canvas-100 text-ink-500',
|
||||
)}
|
||||
>
|
||||
<ChevronDownIcon className="h-4 w-4" strokeWidth={2.2} />
|
||||
</span>
|
||||
</button>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
id={`${item.id}-panel`}
|
||||
role="region"
|
||||
className={clsx(
|
||||
'grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none',
|
||||
open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
|
||||
)}
|
||||
>
|
||||
{/* The clipper. The row collapses to zero height, so this has to hide the overflow or the
|
||||
answer stays painted over the card below it. */}
|
||||
<div className="overflow-hidden">
|
||||
{/* `inert` rather than `hidden` or unmounting: a zero-height `overflow-hidden` box still
|
||||
holds focusable links, so without this, tabbing through the page walks into every
|
||||
collapsed answer. `inert` takes the subtree out of the tab order and the accessibility
|
||||
tree while leaving it in the DOM, which is exactly the split this needs — invisible to
|
||||
a keyboard, still present for crawlers. */}
|
||||
<div className="px-5 pb-5 sm:px-6 sm:pb-6" inert={!open}>
|
||||
<div className="border-t border-canvas-200/70 pt-4">
|
||||
<FaqMarkdown>{item.answer}</FaqMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
web/components/faq/faq-markdown.tsx
Normal file
93
web/components/faq/faq-markdown.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import clsx from 'clsx'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import {CustomLink} from 'web/components/links'
|
||||
|
||||
/**
|
||||
* The answer body renderer.
|
||||
*
|
||||
* The old FAQ handed the entire file to `react-markdown` inside one `prose prose-neutral` div, which
|
||||
* is why it read as a document rather than as part of the product: `prose` is a *reset for unstyled
|
||||
* HTML*, not a design, so none of the tokens `/about` and `/home` are built on (`text-ink-600`,
|
||||
* `primary-*`, the leading scale) ever reached it. This maps the handful of node types an FAQ answer
|
||||
* actually uses onto those tokens instead.
|
||||
*
|
||||
* `a` goes through `CustomLink` — the FAQ links to `/news`, `/vote`, `/support`, `/stats` and a dozen
|
||||
* other internal routes, and `CustomLink` is what keeps those as client-side navigations while still
|
||||
* opening external links in a new tab.
|
||||
*/
|
||||
export function FaqMarkdown({children, className}: {children: string; className?: string}) {
|
||||
return (
|
||||
<div className={clsx('text-[15px] leading-relaxed text-ink-600', className)}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
p: ({node: _node, children, ...props}) => (
|
||||
<p className="mb-4 last:mb-0" {...props}>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
a: ({node: _node, children, ...props}) => (
|
||||
<CustomLink
|
||||
className="font-medium text-primary-700 underline decoration-primary-500/35 underline-offset-2 transition-colors hover:decoration-primary-500"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</CustomLink>
|
||||
),
|
||||
strong: ({node: _node, children, ...props}) => (
|
||||
<strong className="font-semibold text-ink-900" {...props}>
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
// A drawn marker rather than a native bullet: the bulleted answers are the page's densest
|
||||
// content, and a small primary dot pinned to the first line's optical centre is both calmer
|
||||
// than a default disc and consistent with the flow steps on /about. Done as a `before:` on
|
||||
// the children so the `li` renderer stays shared with ordered lists, which keep their real
|
||||
// numbers.
|
||||
//
|
||||
// `list-none pl-0 mt-0` undoes globals.css's blanket `ul { list-style: disc; padding-left:
|
||||
// 1.25rem; margin-top: 0.5rem }` — without it every bullet is drawn twice, once by the
|
||||
// browser and once by us, at two different indents.
|
||||
ul: ({node: _node, children, ...props}) => (
|
||||
<ul
|
||||
className={clsx(
|
||||
'mb-4 mt-0 list-none space-y-2.5 pl-0 last:mb-0',
|
||||
"[&>li]:relative [&>li]:pl-5 [&>li]:before:absolute [&>li]:before:left-0 [&>li]:before:top-[0.6em] [&>li]:before:h-1.5 [&>li]:before:w-1.5 [&>li]:before:rounded-full [&>li]:before:bg-primary-500/70 [&>li]:before:content-['']",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
// `list-outside` because the global `ol` rule sets `list-style-position: inside`, which puts
|
||||
// the number in the text flow and kills the hanging indent on wrapped lines.
|
||||
ol: ({node: _node, children, ...props}) => (
|
||||
<ol
|
||||
className="mb-4 mt-0 list-decimal list-outside space-y-2.5 pl-5 last:mb-0 marker:text-ink-400"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
code: ({node: _node, children, ...props}) => (
|
||||
<code
|
||||
className="rounded bg-canvas-100 px-1.5 py-0.5 font-mono text-[0.9em] text-ink-800"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
blockquote: ({node: _node, children, ...props}) => (
|
||||
<blockquote
|
||||
className="mb-4 border-l-2 border-primary-300 pl-4 italic last:mb-0"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,14 +7,10 @@ import {CustomLink} from 'web/components/links'
|
||||
import {PageBase} from 'web/components/page-base'
|
||||
import {SEO} from 'web/components/SEO'
|
||||
|
||||
export const MD_PATHS = [
|
||||
'constitution',
|
||||
'faq',
|
||||
'financials',
|
||||
'members',
|
||||
'support',
|
||||
'tips-bio',
|
||||
] as const
|
||||
// `faq` is deliberately absent: /faq no longer renders through this generic loader. It parses its
|
||||
// markdown at build time (see web/lib/faq.ts) so the questions reach the served HTML and the page can
|
||||
// group, search, and deep-link them — none of which is possible with the file as one opaque blob.
|
||||
export const MD_PATHS = ['constitution', 'financials', 'members', 'support', 'tips-bio'] as const
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
|
||||
176
web/lib/faq.ts
Normal file
176
web/lib/faq.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Turns an FAQ markdown file into the structure the FAQ page renders.
|
||||
*
|
||||
* The markdown already carries every distinction the UI needs — `##` is a category, `###` is a
|
||||
* question, everything until the next heading is that question's answer — so the source files stay
|
||||
* plain prose that a translator can work on without touching any markup. All this does is stop
|
||||
* treating the file as one opaque blob, which is what forced the old page to render it as a single
|
||||
* undifferentiated `prose` column.
|
||||
*
|
||||
* Answers stay as *markdown strings*, not HTML: the page renders them with `react-markdown` and a
|
||||
* component map, so the links keep going through `CustomLink` (client-side routing for internal
|
||||
* hrefs) and the lists pick up the design tokens. Pre-rendering to an HTML string here would trade
|
||||
* both away for nothing.
|
||||
*
|
||||
* Runs in `getStaticProps`, so the result is in the served HTML — which is what makes the FAQPage
|
||||
* JSON-LD possible and removes the empty-page flash the old client-side `fetch` had.
|
||||
*/
|
||||
|
||||
export type FaqQuestion = {
|
||||
/** Slug used as the element id, so `/faq#is-my-data-safe` deep-links to a question. */
|
||||
id: string
|
||||
question: string
|
||||
/** Raw markdown, rendered by the page. */
|
||||
answer: string
|
||||
}
|
||||
|
||||
export type FaqCategory = {
|
||||
id: string
|
||||
title: string
|
||||
questions: FaqQuestion[]
|
||||
}
|
||||
|
||||
export type FaqDoc = {
|
||||
title: string
|
||||
/** Everything between the `#` title and the first category — rendered as the page's lede. */
|
||||
intro: string
|
||||
categories: FaqCategory[]
|
||||
}
|
||||
|
||||
const HEADING = /^(#{1,6})\s+(.*?)\s*$/
|
||||
|
||||
/**
|
||||
* Slugs come from the question text rather than its position, so an anchor survives questions being
|
||||
* reordered or inserted above it — the thing that actually happens to an FAQ. The cost is that the
|
||||
* anchors are per-locale, since the text they derive from is translated; a shared cross-locale anchor
|
||||
* would need an explicit id in the markdown, which is exactly the markup the source files are meant
|
||||
* to stay free of.
|
||||
*/
|
||||
export function slugify(text: string) {
|
||||
return (
|
||||
text
|
||||
.normalize('NFD')
|
||||
// Strip diacritics so `Confidentialité` and `Für` produce clean ASCII anchors.
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 60)
|
||||
.replace(/-+$/g, '')
|
||||
)
|
||||
}
|
||||
|
||||
export function parseFaq(markdown: string): FaqDoc {
|
||||
const lines = markdown.split('\n')
|
||||
|
||||
let title = ''
|
||||
const intro: string[] = []
|
||||
const categories: FaqCategory[] = []
|
||||
// Tracks fenced blocks so a `### ` inside one is never mistaken for a question. No FAQ answer has
|
||||
// a code fence today, but a parser that silently shatters the first one that shows up is a trap.
|
||||
let inFence = false
|
||||
let current: FaqCategory | undefined
|
||||
let question: {question: string; answer: string[]} | undefined
|
||||
const seen = new Set<string>()
|
||||
|
||||
const flushQuestion = () => {
|
||||
if (!question || !current) return
|
||||
let id = slugify(question.question) || `q${current.questions.length + 1}`
|
||||
// Two questions can legitimately slugify the same way across a long file; the suffix keeps the
|
||||
// ids unique so `getElementById` and the deep link stay unambiguous.
|
||||
if (seen.has(id)) {
|
||||
let n = 2
|
||||
while (seen.has(`${id}-${n}`)) n++
|
||||
id = `${id}-${n}`
|
||||
}
|
||||
seen.add(id)
|
||||
current.questions.push({id, question: question.question, answer: trim(question.answer)})
|
||||
question = undefined
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^\s*(```|~~~)/.test(line)) inFence = !inFence
|
||||
|
||||
const m = inFence ? null : HEADING.exec(line)
|
||||
const depth = m ? m[1].length : 0
|
||||
|
||||
if (m && depth === 1 && !title) {
|
||||
title = m[2]
|
||||
continue
|
||||
}
|
||||
|
||||
if (m && depth === 2) {
|
||||
flushQuestion()
|
||||
current = {id: slugify(m[2]), title: m[2], questions: []}
|
||||
categories.push(current)
|
||||
continue
|
||||
}
|
||||
|
||||
if (m && depth >= 3) {
|
||||
flushQuestion()
|
||||
// A `###` before any `##` still has to land somewhere; an untitled category renders without a
|
||||
// heading, so a file that never groups its questions degrades to one flat list rather than
|
||||
// dropping content.
|
||||
if (!current) {
|
||||
current = {id: 'faq', title: '', questions: []}
|
||||
categories.push(current)
|
||||
}
|
||||
question = {question: m[2], answer: []}
|
||||
continue
|
||||
}
|
||||
|
||||
if (question) question.answer.push(line)
|
||||
else if (!current) intro.push(line)
|
||||
// Prose sitting directly under a `##` with no `###` after it has nowhere to go in this model.
|
||||
// Dropping it is deliberate — the alternative is inventing a heading-less pseudo-question.
|
||||
}
|
||||
|
||||
flushQuestion()
|
||||
|
||||
return {
|
||||
title,
|
||||
intro: trim(intro),
|
||||
// A category whose questions all failed to parse would render as a heading over nothing.
|
||||
categories: categories.filter((c) => c.questions.length > 0),
|
||||
}
|
||||
}
|
||||
|
||||
function trim(lines: string[]) {
|
||||
const out = [...lines]
|
||||
while (out.length && !out[0].trim()) out.shift()
|
||||
while (out.length && !out[out.length - 1].trim()) out.pop()
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
/** Flattened, in document order — for search and for the JSON-LD. */
|
||||
export function allQuestions(doc: FaqDoc): FaqQuestion[] {
|
||||
return doc.categories.flatMap((c) => c.questions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer markdown reduced to plain text, for the search index and the JSON-LD `acceptedAnswer`.
|
||||
*
|
||||
* Deliberately a regex pass rather than a real render: search wants the words, and Google wants the
|
||||
* text of the answer. Neither needs the tree, and pulling a second markdown pipeline in for this
|
||||
* would be far more machinery than the job.
|
||||
*/
|
||||
export function toPlainText(markdown: string) {
|
||||
return markdown
|
||||
.replace(/^\[\/\/\]:.*$/gm, '') // markdown comments (`[//]: # '...'`)
|
||||
.replace(/```[\s\S]*?```/g, ' ')
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
|
||||
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // keep link text, drop the href
|
||||
.replace(/[*_`>#]/g, '')
|
||||
.replace(/^\s*[-+*]\s+/gm, '')
|
||||
.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
/** Accent- and case-insensitive, so `donnees` matches `données` and `fur` matches `für`. */
|
||||
export function normalizeForSearch(text: string) {
|
||||
return text
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
@@ -1,5 +1,86 @@
|
||||
import {MarkdownPageLoader} from 'web/components/MarkdownPageLoader'
|
||||
import {defaultLocale, OG_DESCRIPTION, supportedLocales} from 'common/constants'
|
||||
import fs from 'fs'
|
||||
import Head from 'next/head'
|
||||
import path from 'path'
|
||||
import {FaqContent} from 'web/components/faq/faq-content'
|
||||
import {PageBase} from 'web/components/page-base'
|
||||
import {SEO} from 'web/components/SEO'
|
||||
import {allQuestions, FaqDoc, parseFaq, toPlainText} from 'web/lib/faq'
|
||||
import {useLocale, useT} from 'web/lib/locale'
|
||||
|
||||
export default function Page() {
|
||||
return <MarkdownPageLoader filename="faq" />
|
||||
type Props = {
|
||||
/** Every locale's parsed FAQ, keyed by locale code. */
|
||||
docs: Record<string, FaqDoc>
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parsed at build time rather than fetched at runtime.
|
||||
*
|
||||
* The old page fetched `/md/{locale}/faq.md` from a `useEffect` and rendered it client-side, which
|
||||
* meant the HTML a crawler received contained no questions at all — on the one page in the app that
|
||||
* is *made of* the thing Google shows rich results for. Parsing here puts the Q&A in the served
|
||||
* markup and makes the `FAQPage` JSON-LD below possible.
|
||||
*
|
||||
* All locales ship in the props (roughly 30KB of text across three) because locale here is a cookie
|
||||
* read by `I18nContext`, not a route segment — there is no per-locale URL to statically render
|
||||
* separately, so the client has to be able to switch without another round trip. This still works
|
||||
* under `output: 'export'` for the Android build, since `getStaticProps` runs at build time.
|
||||
*/
|
||||
export const getStaticProps = async () => {
|
||||
const dir = path.join(process.cwd(), 'public', 'md')
|
||||
const docs: Record<string, FaqDoc> = {}
|
||||
|
||||
for (const locale of supportedLocales) {
|
||||
const file =
|
||||
locale === defaultLocale ? path.join(dir, 'faq.md') : path.join(dir, locale, 'faq.md')
|
||||
// A locale without its own translation simply falls back to English at render time rather than
|
||||
// failing the build — the same behaviour the old runtime fetch had.
|
||||
if (!fs.existsSync(file)) continue
|
||||
docs[locale] = parseFaq(fs.readFileSync(file, 'utf-8'))
|
||||
}
|
||||
|
||||
if (!docs[defaultLocale]) throw new Error(`Missing ${path.join(dir, 'faq.md')}`)
|
||||
|
||||
return {props: {docs}}
|
||||
}
|
||||
|
||||
export default function Faq({docs}: Props) {
|
||||
const t = useT()
|
||||
const {locale} = useLocale()
|
||||
const doc = docs[locale] ?? docs[defaultLocale]
|
||||
|
||||
return (
|
||||
<PageBase trackPageView="faq">
|
||||
<SEO
|
||||
title={t('faq.seo.title', 'FAQ')}
|
||||
description={t('faq.seo.description', OG_DESCRIPTION)}
|
||||
url="/faq"
|
||||
/>
|
||||
{/* Structured data for rich results. Always the default-locale document: the pre-rendered HTML
|
||||
is English (locale is a cookie, resolved after hydration), so emitting the visitor's
|
||||
translated questions here would describe markup the crawler never saw. */}
|
||||
<Head>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{__html: faqJsonLd(docs[defaultLocale])}}
|
||||
/>
|
||||
</Head>
|
||||
<FaqContent doc={doc} />
|
||||
</PageBase>
|
||||
)
|
||||
}
|
||||
|
||||
function faqJsonLd(doc: FaqDoc) {
|
||||
const json = JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: allQuestions(doc).map((q) => ({
|
||||
'@type': 'Question',
|
||||
name: q.question,
|
||||
acceptedAnswer: {'@type': 'Answer', text: toPlainText(q.answer)},
|
||||
})),
|
||||
})
|
||||
// `</script>` inside the JSON would close the tag early; escaping the slash is the standard fix and
|
||||
// stays valid JSON.
|
||||
return json.replace(/</g, '\\u003c')
|
||||
}
|
||||
|
||||
@@ -4,13 +4,7 @@ Compass ist ein **Geschenk der Community** — aufgebaut von seinen Mitgliedern
|
||||
Verbindungen wichtig sind. Deine Teilnahme, dein Feedback und deine Beiträge helfen dabei, Compass für alle frei,
|
||||
transparent und werbefrei zu halten.
|
||||
|
||||
## Was gibt es Neues?
|
||||
|
||||
Bleib über die neuesten Releases, Funktionen und Verbesserungen [hier](/news) auf dem Laufenden.
|
||||
|
||||
### Wird Compass in der Presse erwähnt?
|
||||
|
||||
Ja. Berichterstattung, Media Kit, Markenrichtlinien und Pressekontakt findest du auf der [Presse-Seite](/press).
|
||||
## Grundlagen
|
||||
|
||||
### Was ist Compass?
|
||||
|
||||
@@ -44,6 +38,10 @@ und du Beziehungen suchst, die auf **geteilten Werten, Vertrauen und Verständni
|
||||
- **Demokratisch & Open Source**: Von der Community für die Community — keine Werbung, keine versteckte Monetarisierung.
|
||||
- **Events**: Erstelle und nimm an realen oder virtuellen Treffen teil, um tiefere Verbindungen zu knüpfen.
|
||||
|
||||
### Ist Compass für Dating oder Freundschaften?
|
||||
|
||||
Beides — und mehr. Du kannst angeben, ob du **platonische, romantische oder kollaborative Verbindungen** suchst.
|
||||
|
||||
### Was sind Events und warum sind sie wichtig?
|
||||
|
||||
**Events** stehen im Mittelpunkt von Compass' Mission, tiefe und bedeutungsvolle Verbindungen zu fördern. Während
|
||||
@@ -76,40 +74,22 @@ Jeder kann [ein Event erstellen](/events) — ob du eine Wanderung organisieren,
|
||||
Diskussion über effektiven Altruismus hosten möchtest. Events sind es, wie wir gemeinsame Werte in gemeinsames Leben
|
||||
verwandeln.
|
||||
|
||||
### Ist Compass für Dating oder Freundschaften?
|
||||
### Wie wird der Kompatibilitätsscore berechnet?
|
||||
|
||||
Beides — und mehr. Du kannst angeben, ob du **platonische, romantische oder kollaborative Verbindungen** suchst.
|
||||
Der **Kompatibilitätsscore** basiert auf Antworten auf **Kompatibilitätsfragen**. Jede Person gibt an:
|
||||
|
||||
### Wer hat Compass gegründet?
|
||||
- **Ihre eigene Antwort**
|
||||
- **Antworten, die sie von anderen akzeptieren würde**
|
||||
- **Den Wichtigkeitsgrad** jeder Frage
|
||||
|
||||
Compass wurde von [Martin Braquet](https://www.martinbraquet.com) gegründet, einem Ingenieur und Forscher, der sich
|
||||
leidenschaftlich mit den drängendsten Herausforderungen der Menschheit befasst — von Klimawandel und KI-Sicherheit bis
|
||||
hin zu Tierschutz.
|
||||
Übereinstimmungen werden danach bewertet, wie gut die Antworten und akzeptierten Antworten zweier Personen
|
||||
zusammenpassen, gewichtet nach Wichtigkeit. Dieses [Video](https://www.youtube.com/watch?v=m9PiPlRuy6E) erklärt die
|
||||
zugrunde liegende Mathematik.
|
||||
|
||||
Martin hat in ganz Europa, den USA, Indien und Indonesien gelebt und dabei vielfältige Praktiken kennengelernt, von
|
||||
Meditationsretreats bis zu nachhaltig ausgerichteten Waldkooperativen. Diese Erfahrungen prägten seine Überzeugung, dass
|
||||
tiefe Eins-zu-eins-Verbindungen zwischen Menschen zu den bedeutendsten Quellen von Wohlbefinden und positivem Wandel
|
||||
gehören.
|
||||
Die [vollständige Implementierung](https://github.com/CompassConnections/Compass/blob/main/common/src/profiles/compatibility-score.ts)
|
||||
ist **Open Source** und offen für Überprüfung, Feedback und Verbesserungen durch die Community.
|
||||
|
||||
Compass ist aus dieser Überzeugung heraus entstanden. Während Martin sich seit Langem dafür einsetzt, globale Risiken
|
||||
und Leid zu verringern, erkannte er zugleich, dass sein eigenes Leben — ebenso wie das vieler anderer — durch tiefere,
|
||||
engere und unterstützendere Beziehungen erheblich bereichert würde. Obwohl er das Projekt initiiert hat, ist die
|
||||
Plattform ein Geschenk der Community — gestaltet von Mitwirkenden, Spendern und Unterstützern, die ihr Gedeihen
|
||||
ermöglichen. Compass ist der Versuch vieler Menschen, eine offene, transparente und gemeinschaftsgetragene Plattform zu
|
||||
schaffen, auf der sich Menschen rund um gemeinsame Werte, Neugier und Fürsorge verbinden können — ohne die Ablenkungen
|
||||
von Swipes, Werbung oder Oberflächlichkeit.
|
||||
|
||||
Martin wirkt weiterhin als Initiator und Hüter von Compass, doch die Ausrichtung des Projekts liegt bewusst in den
|
||||
Händen der Community, geregelt durch die Compass-Verfassung (wie im nächsten Abschnitt beschrieben).
|
||||
|
||||
### Wie funktioniert die Governance?
|
||||
|
||||
Compass wird demokratisch auf Grundlage einer [Verfassung](/constitution) geführt, die zentrale Kontrolle verhindert und
|
||||
eine langfristige Ausrichtung an der Mission sicherstellt.
|
||||
|
||||
- Wichtige Entscheidungen (Umfang, Finanzierung, Regeln) werden von **aktiven Mitwirkenden** [abgestimmt](/vote).
|
||||
- Die vollständige Verfassung ist **öffentlich und transparent**.
|
||||
- Keine Vereinnahmung durch Unternehmen — Compass bleibt dauerhaft ein gemeinschaftseigenes Projekt.
|
||||
## Finanzierung & Governance
|
||||
|
||||
### Ist Compass wirklich kostenlos?
|
||||
|
||||
@@ -137,6 +117,39 @@ Jeder Beitrag — egal wie groß oder klein, von einer kleinen Code-Änderung ü
|
||||
hin zum Weitersagen — ist Teil des Geschenks, das Compass frei, werbefrei und gemeinschaftseigen hält. Alle Einnahmen
|
||||
und Ausgaben sind **öffentlich dokumentiert** [hier](/financials).
|
||||
|
||||
### Wie funktioniert die Governance?
|
||||
|
||||
Compass wird demokratisch auf Grundlage einer [Verfassung](/constitution) geführt, die zentrale Kontrolle verhindert und
|
||||
eine langfristige Ausrichtung an der Mission sicherstellt.
|
||||
|
||||
- Wichtige Entscheidungen (Umfang, Finanzierung, Regeln) werden von **aktiven Mitwirkenden** [abgestimmt](/vote).
|
||||
- Die vollständige Verfassung ist **öffentlich und transparent**.
|
||||
- Keine Vereinnahmung durch Unternehmen — Compass bleibt dauerhaft ein gemeinschaftseigenes Projekt.
|
||||
|
||||
### Wer hat Compass gegründet?
|
||||
|
||||
Compass wurde von [Martin Braquet](https://www.martinbraquet.com) gegründet, einem Ingenieur und Forscher, der sich
|
||||
leidenschaftlich mit den drängendsten Herausforderungen der Menschheit befasst — von Klimawandel und KI-Sicherheit bis
|
||||
hin zu Tierschutz.
|
||||
|
||||
Martin hat in ganz Europa, den USA, Indien und Indonesien gelebt und dabei vielfältige Praktiken kennengelernt, von
|
||||
Meditationsretreats bis zu nachhaltig ausgerichteten Waldkooperativen. Diese Erfahrungen prägten seine Überzeugung, dass
|
||||
tiefe Eins-zu-eins-Verbindungen zwischen Menschen zu den bedeutendsten Quellen von Wohlbefinden und positivem Wandel
|
||||
gehören.
|
||||
|
||||
Compass ist aus dieser Überzeugung heraus entstanden. Während Martin sich seit Langem dafür einsetzt, globale Risiken
|
||||
und Leid zu verringern, erkannte er zugleich, dass sein eigenes Leben — ebenso wie das vieler anderer — durch tiefere,
|
||||
engere und unterstützendere Beziehungen erheblich bereichert würde. Obwohl er das Projekt initiiert hat, ist die
|
||||
Plattform ein Geschenk der Community — gestaltet von Mitwirkenden, Spendern und Unterstützern, die ihr Gedeihen
|
||||
ermöglichen. Compass ist der Versuch vieler Menschen, eine offene, transparente und gemeinschaftsgetragene Plattform zu
|
||||
schaffen, auf der sich Menschen rund um gemeinsame Werte, Neugier und Fürsorge verbinden können — ohne die Ablenkungen
|
||||
von Swipes, Werbung oder Oberflächlichkeit.
|
||||
|
||||
Martin wirkt weiterhin als Initiator und Hüter von Compass, doch die Ausrichtung des Projekts liegt bewusst in den
|
||||
Händen der Community, geregelt durch die Compass-Verfassung (wie im nächsten Abschnitt beschrieben).
|
||||
|
||||
## Datenschutz & Technik
|
||||
|
||||
### Sind meine Daten sicher?
|
||||
|
||||
Ja.
|
||||
@@ -147,21 +160,6 @@ Ja.
|
||||
\*Ende-zu-Ende-Verschlüsselung\*\* ist für zukünftige Versionen geplant).
|
||||
- Speicherung in sicheren Supabase- und Firebase-Datenbanken.
|
||||
|
||||
### Wie wird der Kompatibilitätsscore berechnet?
|
||||
|
||||
Der **Kompatibilitätsscore** basiert auf Antworten auf **Kompatibilitätsfragen**. Jede Person gibt an:
|
||||
|
||||
- **Ihre eigene Antwort**
|
||||
- **Antworten, die sie von anderen akzeptieren würde**
|
||||
- **Den Wichtigkeitsgrad** jeder Frage
|
||||
|
||||
Übereinstimmungen werden danach bewertet, wie gut die Antworten und akzeptierten Antworten zweier Personen
|
||||
zusammenpassen, gewichtet nach Wichtigkeit. Dieses [Video](https://www.youtube.com/watch?v=m9PiPlRuy6E) erklärt die
|
||||
zugrunde liegende Mathematik.
|
||||
|
||||
Die [vollständige Implementierung](https://github.com/CompassConnections/Compass/blob/main/common/src/profiles/compatibility-score.ts)
|
||||
ist **Open Source** und offen für Überprüfung, Feedback und Verbesserungen durch die Community.
|
||||
|
||||
### Auf welchen Plattformen läuft Compass?
|
||||
|
||||
Derzeit ist Compass überwiegend **webbasiert**. Die gute Nachricht: Die Plattform ist **vollständig responsiv**
|
||||
@@ -206,6 +204,23 @@ Transparenzprinzipien wie das gesamte Projekt:
|
||||
Du kannst die API unter [api.compassmeet.com](https://api.compassmeet.com) erkunden oder testen. Entwickler:innen können
|
||||
neue Endpunkte beitragen oder Verbesserungen vorschlagen.
|
||||
|
||||
## Community & Neuigkeiten
|
||||
|
||||
### Wie kann ich helfen?
|
||||
|
||||
- **Feedback geben**: [Vorschlagsformular ausfüllen](https://forms.gle/tKnXUMAbEreMK6FC6)
|
||||
- **Diskussionen beitreten**: [Discord-Community](https://discord.gg/8Vd7jzqjun)
|
||||
- **Vorschläge machen und abstimmen**: [hier abstimmen](/vote)
|
||||
- **Zur Entwicklung beitragen**: [Code auf GitHub ansehen](https://github.com/CompassConnections/Compass)
|
||||
- **Spenden**: [Infrastruktur unterstützen](/support)
|
||||
- **Weitersagen**: Erzähle Freund:innen und Familie davon, die Tiefe und echte Verbindung schätzen. Teile den Link oder
|
||||
diesen [QR-Code](/referrals).
|
||||
|
||||
### Wie kann ich die Community kontaktieren?
|
||||
|
||||
Du erreichst uns über das [Kontaktformular](/contact), das [Feedback-Formular](https://forms.gle/tKnXUMAbEreMK6FC6) oder
|
||||
über unsere [Social-Media-Kanäle](/social).
|
||||
|
||||
### Wie schnell wächst Compass?
|
||||
|
||||
Compass ist im **Oktober 2025 offiziell gestartet** und wächst schnell. Du kannst Echtzeit-Statistiken und transparente
|
||||
@@ -227,6 +242,16 @@ Da Compass vollständig transparent und gemeinschaftseigen ist, kannst du beobac
|
||||
nicht nur in Zahlen, sondern auch darin, wie Menschen sich verbinden, zusammenarbeiten und die Plattform gemeinsam
|
||||
gestalten.
|
||||
|
||||
### Was gibt es Neues?
|
||||
|
||||
Bleib über die neuesten Releases, Funktionen und Verbesserungen [hier](/news) auf dem Laufenden.
|
||||
|
||||
### Wird Compass in der Presse erwähnt?
|
||||
|
||||
Ja. Berichterstattung, Media Kit, Markenrichtlinien und Pressekontakt findest du auf der [Presse-Seite](/press).
|
||||
|
||||
## Vision & Ausblick
|
||||
|
||||
### Was ist die langfristige Vision?
|
||||
|
||||
Unser Ziel ist es, dass Compass für menschliche Verbindung das wird, was Linux für Software, Wikipedia für Wissen oder
|
||||
@@ -241,18 +266,3 @@ Unser Fokus liegt nun auf dem **Sammeln von Feedback**, dem **Wachstum der Commu
|
||||
|
||||
Jede Handlung — Teilen, Spenden oder Beitragen — hilft direkt dabei, Compass werbefrei, abonnementfrei und
|
||||
gemeinschaftseigen zu halten.
|
||||
|
||||
### Wie kann ich helfen?
|
||||
|
||||
- **Feedback geben**: [Vorschlagsformular ausfüllen](https://forms.gle/tKnXUMAbEreMK6FC6)
|
||||
- **Diskussionen beitreten**: [Discord-Community](https://discord.gg/8Vd7jzqjun)
|
||||
- **Vorschläge machen und abstimmen**: [hier abstimmen](/vote)
|
||||
- **Zur Entwicklung beitragen**: [Code auf GitHub ansehen](https://github.com/CompassConnections/Compass)
|
||||
- **Spenden**: [Infrastruktur unterstützen](/support)
|
||||
- **Weitersagen**: Erzähle Freund:innen und Familie davon, die Tiefe und echte Verbindung schätzen. Teile den Link oder
|
||||
diesen [QR-Code](/referrals).
|
||||
|
||||
### Wie kann ich die Community kontaktieren?
|
||||
|
||||
Du erreichst uns über das [Kontaktformular](/contact), das [Feedback-Formular](https://forms.gle/tKnXUMAbEreMK6FC6) oder
|
||||
über unsere [Social-Media-Kanäle](/social).
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
# FAQ – Compass
|
||||
|
||||
Compass is a **free, open-source platform to help people form deep, meaningful, and lasting connections** — whether
|
||||
platonic, romantic, or collaborative. It’s made possible by contributions from the community, including code, ideas,
|
||||
feedback, and donations. Unlike typical apps, Compass prioritizes **values, interests, and personality over swipes and
|
||||
ads**, giving you full control over who you discover and how you connect.
|
||||
platonic, romantic, or collaborative. Unlike typical apps, Compass is a transparent directory that prioritizes **values,
|
||||
interests,
|
||||
and personality without any swipes and
|
||||
ads**, giving you full control over who you discover and how you connect. It’s made possible by contributions from the
|
||||
community, including code, ideas,
|
||||
feedback, and donations.
|
||||
|
||||
## What’s new?
|
||||
|
||||
Stay up to date with the latest releases, features, and improvements [here](/news).
|
||||
|
||||
### Is Compass mentioned in the press?
|
||||
|
||||
Yes. You can find media coverage, our media kit, brand assets, and press contact details on the [Press page](/press).
|
||||
## The basics
|
||||
|
||||
### Why is the platform called Compass?
|
||||
|
||||
@@ -41,89 +38,6 @@ grounded in **shared values, trust, and understanding**, Compass is for you.
|
||||
Both, and others things. You can specify whether you’re looking for **platonic, romantic, or collaborative
|
||||
connections**.
|
||||
|
||||
### Who started Compass?
|
||||
|
||||
Compass was founded by [Martin Braquet](https://www.martinbraquet.com), an engineer and researcher passionate about
|
||||
tackling humanity’s most pressing challenges — from climate change and AI safety to animal welfare.
|
||||
|
||||
Martin has lived across Europe, the U.S., India, and Indonesia, immersing himself in diverse practices ranging from
|
||||
meditation retreats to sustainability-focused forest co-ops. These experiences shaped his conviction that deep
|
||||
one-to-one human connections are among the most meaningful drivers of well-being and positive change.
|
||||
|
||||
Compass grew out of that conviction. While Martin has long been driven to reduce global risks and suffering, he also
|
||||
recognized that his own life — and the lives of many others — would be greatly enriched by more profound, close, and
|
||||
supportive relationships. Although he initiated the project, the platform is a gift from the community — shaped by
|
||||
contributors, donors, and supporters who help it thrive. Compass is many people's attempt to build an open, transparent,
|
||||
and community-driven platform where people can connect around shared values, curiosity, and care, without the
|
||||
distractions of swipes, ads, or superficiality.
|
||||
|
||||
Martin continues to serve as an initiator and steward of Compass, but its direction is intentionally placed in the hands
|
||||
of the community through the Compass Constitution (as detailed in the next section).
|
||||
|
||||
### Who keeps Compass alive and thriving?
|
||||
|
||||
Compass is a **gift from the community** — built by its members like you who care about deep, meaningful connections.
|
||||
Your participation, feedback, and contributions help keep it free, transparent, and ad-free for everyone.
|
||||
|
||||
### How does governance work?
|
||||
|
||||
Compass is run democratically under a [constitution](/constitution) that prevents central control and ensures long-term
|
||||
alignment with its mission.
|
||||
|
||||
- Major decisions (scope, funding, rules) are [voted](/vote) on by **active contributors**.
|
||||
- The full constitution is **public and transparent**.
|
||||
- No corporate capture — Compass will always remain a community-owned project.
|
||||
|
||||
### Is Compass really free?
|
||||
|
||||
Yes. Compass will always be:
|
||||
|
||||
- **Ad-free**
|
||||
- **Subscription-free**
|
||||
- **Open-source**
|
||||
|
||||
Supported entirely by **donations**, not by selling your data or attention.
|
||||
|
||||
### How do you sustain Compass without ads or subscriptions?
|
||||
|
||||
Through **[donations](/support) and contributions from the community**. Options include:
|
||||
|
||||
- Open Collective
|
||||
- Liberapay
|
||||
- Ko-fi
|
||||
- GitHub Sponsors
|
||||
- Patreon
|
||||
- PayPal
|
||||
- Sharing ideas, feedback, or code
|
||||
|
||||
Every contribution, no matter the size — from a small code tweak to helping refine the interface, or simply spreading
|
||||
the word — is part of the gift that keeps Compass free, ad-free, and community-owned. All funding and expenses are \*
|
||||
\*publicly documented\*\* [here](/financials).
|
||||
|
||||
### Is my data safe?
|
||||
|
||||
Yes.
|
||||
|
||||
- Your data will **never be sold**.
|
||||
- You can **control what is visible publicly**.
|
||||
- Chat messages are stored in the database with AES-256 encryption (it may move toward **end-to-end encryption** in
|
||||
future versions).
|
||||
- Stored in secure Supabase and Firebase databases.
|
||||
|
||||
### How is the compatibility score calculated?
|
||||
|
||||
The **compatibility score** comes from answers to **compatibility prompts**. Each user provides:
|
||||
|
||||
- **Their answer**
|
||||
- **Answers they would accept from others**
|
||||
- **A degree of importance** for each question
|
||||
|
||||
Matches are scored based on how well two people’s responses and accepted answers align, weighted by importance. See
|
||||
this [video](https://www.youtube.com/watch?v=m9PiPlRuy6E) for the math behind it.
|
||||
|
||||
The [full implementation](https://github.com/CompassConnections/Compass/blob/main/common/src/profiles/compatibility-score.ts)
|
||||
is **open source** and open to review, feedback, and improvement by the community.
|
||||
|
||||
### What are Events and why do they matter?
|
||||
|
||||
**Events** are at the core of Compass's mission to foster deep, meaningful connections. While profiles and compatibility
|
||||
@@ -154,6 +68,93 @@ Examples of events you might find or create:
|
||||
Anyone can [create an event](/events) — whether you're looking to find a hiking buddy, start a writers' circle, or host
|
||||
a discussion about effective altruism. Events are how we turn shared values into shared lives.
|
||||
|
||||
### How is the compatibility score calculated?
|
||||
|
||||
The **compatibility score** comes from answers to **compatibility prompts**. Each user provides:
|
||||
|
||||
- **Their answer**
|
||||
- **Answers they would accept from others**
|
||||
- **A degree of importance** for each question
|
||||
|
||||
Matches are scored based on how well two people’s responses and accepted answers align, weighted by importance. See
|
||||
this [video](https://www.youtube.com/watch?v=m9PiPlRuy6E) for the math behind it.
|
||||
|
||||
The [full implementation](https://github.com/CompassConnections/Compass/blob/main/common/src/profiles/compatibility-score.ts)
|
||||
is **open source** and open to review, feedback, and improvement by the community.
|
||||
|
||||
## Money & governance
|
||||
|
||||
### Is Compass really free?
|
||||
|
||||
Yes. Compass will always be:
|
||||
|
||||
- **Ad-free**
|
||||
- **Subscription-free**
|
||||
- **Open-source**
|
||||
|
||||
Supported entirely by **donations**, not by selling your data or attention.
|
||||
|
||||
### How do you sustain Compass without ads or subscriptions?
|
||||
|
||||
Through **[donations](/support) and contributions from the community**. Options include:
|
||||
|
||||
- Open Collective
|
||||
- Liberapay
|
||||
- Ko-fi
|
||||
- GitHub Sponsors
|
||||
- Patreon
|
||||
- PayPal
|
||||
- Sharing ideas, feedback, or code
|
||||
|
||||
Every contribution, no matter the size — from a small code tweak to helping refine the interface, or simply spreading
|
||||
the word — is part of the gift that keeps Compass free, ad-free, and community-owned. All funding and expenses are \*
|
||||
\*publicly documented\*\* [here](/financials).
|
||||
|
||||
### How does governance work?
|
||||
|
||||
Compass is run democratically under a [constitution](/constitution) that prevents central control and ensures long-term
|
||||
alignment with its mission.
|
||||
|
||||
- Major decisions (scope, funding, rules) are [voted](/vote) on by **active contributors**.
|
||||
- The full constitution is **public and transparent**.
|
||||
- No corporate capture — Compass will always remain a community-owned project.
|
||||
|
||||
### Who started Compass?
|
||||
|
||||
Compass was founded by [Martin Braquet](https://www.martinbraquet.com), an engineer and researcher passionate about
|
||||
tackling humanity’s most pressing challenges — from climate change and AI safety to animal welfare.
|
||||
|
||||
Martin has lived across Europe, the U.S., India, and Indonesia, immersing himself in diverse practices ranging from
|
||||
meditation retreats to sustainability-focused forest co-ops. These experiences shaped his conviction that deep
|
||||
one-to-one human connections are among the most meaningful drivers of well-being and positive change.
|
||||
|
||||
Compass grew out of that conviction. While Martin has long been driven to reduce global risks and suffering, he also
|
||||
recognized that his own life — and the lives of many others — would be greatly enriched by more profound, close, and
|
||||
supportive relationships. Although he initiated the project, the platform is a gift from the community — shaped by
|
||||
contributors, donors, and supporters who help it thrive. Compass is many people's attempt to build an open, transparent,
|
||||
and community-driven platform where people can connect around shared values, curiosity, and care, without the
|
||||
distractions of swipes, ads, or superficiality.
|
||||
|
||||
Martin continues to serve as an initiator and steward of Compass, but its direction is intentionally placed in the hands
|
||||
of the community through the Compass Constitution (as detailed in the next section).
|
||||
|
||||
### Who keeps Compass alive and thriving?
|
||||
|
||||
Compass is a **gift from the community** — built by its members like you who care about deep, meaningful connections.
|
||||
Your participation, feedback, and contributions help keep it free, transparent, and ad-free for everyone.
|
||||
|
||||
## Privacy & technology
|
||||
|
||||
### Is my data safe?
|
||||
|
||||
Yes.
|
||||
|
||||
- Your data will **never be sold**.
|
||||
- You can **control what is visible publicly**.
|
||||
- Chat messages are stored in the database with AES-256 encryption (it may move toward **end-to-end encryption** in
|
||||
future versions).
|
||||
- Stored in secure Supabase and Firebase databases.
|
||||
|
||||
### What platforms does Compass run on?
|
||||
|
||||
Compass is mostly both as [website](https://www.compassmeet.com/)
|
||||
@@ -191,10 +192,27 @@ of the project:
|
||||
You can explore or test the API at [api.compassmeet.com](https://api.compassmeet.com). Developers can contribute new
|
||||
endpoints or suggest improvements.
|
||||
|
||||
## Community & updates
|
||||
|
||||
### How can I help?
|
||||
|
||||
- **Spread the Word**: By far the best way to help is to grow it! Tell friends and family. Post about it on forums,
|
||||
social media, etc. Share the URL or this [QR code](/referrals).
|
||||
- **Give Feedback**: [Fill out the suggestion form](https://forms.gle/tKnXUMAbEreMK6FC6)
|
||||
- **Join the Discussion**: [Discord Community](https://discord.gg/8Vd7jzqjun)
|
||||
- **Make and vote on proposals**: [vote here](/vote)
|
||||
- **Contribute to Development**: [View the code on GitHub](https://github.com/CompassConnections/Compass)
|
||||
- **Donate**: [Support the infrastructure](/support)
|
||||
|
||||
### How can I contact the community?
|
||||
|
||||
You can reach us through the [contact form](/contact), the [feedback form](https://forms.gle/tKnXUMAbEreMK6FC6), or any
|
||||
of our [socials](/social).
|
||||
|
||||
### How fast is Compass growing?
|
||||
|
||||
Compass has officially **launched** in October 2025 and is growing fast. You can explore real-time stats and transparent
|
||||
community data on our [**Growth & Stats page**](/stats). It includes information such as:
|
||||
community data on our [Growth & Stats page](/stats). It includes information such as:
|
||||
|
||||
- Community growth over time
|
||||
- Number of active users
|
||||
@@ -211,6 +229,16 @@ community data on our [**Growth & Stats page**](/stats). It includes information
|
||||
Because Compass is fully transparent and community-owned, you can see how the ecosystem evolves — not just in numbers,
|
||||
but in how people connect, collaborate, and help shape the platform together.
|
||||
|
||||
### What’s new?
|
||||
|
||||
Stay up to date with the latest releases, features, and improvements [here](/news).
|
||||
|
||||
### Is Compass mentioned in the press?
|
||||
|
||||
Yes. You can find media coverage, our media kit, brand assets, and press contact details on the [Press page](/press).
|
||||
|
||||
## Vision & roadmap
|
||||
|
||||
### What’s the long-term vision?
|
||||
|
||||
Our goal is for Compass to become what Linux is for software, Wikipedia is for knowledge, or Firefox is for browsing — a
|
||||
@@ -224,18 +252,3 @@ expand the platform.
|
||||
|
||||
Every action, whether sharing, donating, or contributing, directly helps Compass remain ad-free, subscription-free, and
|
||||
community-owned.
|
||||
|
||||
### How can I help?
|
||||
|
||||
- **Give Feedback**: [Fill out the suggestion form](https://forms.gle/tKnXUMAbEreMK6FC6)
|
||||
- **Join the Discussion**: [Discord Community](https://discord.gg/8Vd7jzqjun)
|
||||
- **Make and vote on proposals**: [vote here](/vote)
|
||||
- **Contribute to Development**: [View the code on GitHub](https://github.com/CompassConnections/Compass)
|
||||
- **Donate**: [Support the infrastructure](/support)
|
||||
- **Spread the Word**: Tell friends and family who value depth and real connection. Post about it on forums, social
|
||||
media, etc. Share the URL or this [QR code](/referrals).
|
||||
|
||||
### How can I contact the community?
|
||||
|
||||
You can reach us through the [contact form](/contact), the [feedback form](https://forms.gle/tKnXUMAbEreMK6FC6), or any
|
||||
of our [socials](/social).
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
|
||||
Compass est un **cadeau de la communauté** — construit par ses membres comme vous, qui valorisent des connexions profondes et significatives. Votre participation, vos retours et vos contributions permettent de le maintenir gratuit, transparent et sans publicité pour tous.
|
||||
|
||||
## Quoi de neuf ?
|
||||
|
||||
Restez informé des dernières versions, fonctionnalités et améliorations [ici](/news).
|
||||
|
||||
### Compass est-il mentionné dans la presse ?
|
||||
|
||||
Oui. Vous trouverez la couverture médiatique, notre kit média, nos ressources de marque et les coordonnées presse sur
|
||||
la [page Presse](/press).
|
||||
## Les bases
|
||||
|
||||
### Qu’est-ce que Compass ?
|
||||
|
||||
@@ -36,6 +29,10 @@ Pour toute personne qui souhaite autre chose que des discussions superficielles
|
||||
monétisation cachée.
|
||||
- **Événements** : Créez et participez à des rencontres réelles ou virtuelles pour tisser des liens plus profonds.
|
||||
|
||||
### Compass est-il destiné à la rencontre ou à l’amitié ?
|
||||
|
||||
Aux deux, et à d’autres formes de connexion. Vous pouvez préciser si vous recherchez des relations **platoniques, romantiques ou collaboratives**.
|
||||
|
||||
### Qu'est-ce que les Événements et pourquoi sont-ils importants ?
|
||||
|
||||
**Les Événements** sont au cœur de la mission de Compass : favoriser des connexions profondes et significatives. Si les
|
||||
@@ -68,27 +65,19 @@ Chacun peut [créer un événement](/events) — que vous cherchiez un compagnon
|
||||
cercle d'écriture, ou organiser une discussion sur l'altruisme efficace. Les événements sont le moyen par lequel nous
|
||||
transformons les valeurs partagées en vies partagées.
|
||||
|
||||
### Compass est-il destiné à la rencontre ou à l’amitié ?
|
||||
### Comment le score de compatibilité est-il calculé ?
|
||||
|
||||
Aux deux, et à d’autres formes de connexion. Vous pouvez préciser si vous recherchez des relations **platoniques, romantiques ou collaboratives**.
|
||||
Le **score de compatibilité** provient des réponses aux **questions de compatibilité**. Chaque utilisateur fournit :
|
||||
|
||||
### Qui a créé Compass ?
|
||||
- **Sa réponse**
|
||||
- **Les réponses qu’il accepterait chez les autres**
|
||||
- **Un degré d’importance** pour chaque question
|
||||
|
||||
Compass a été fondé par [Martin Braquet](https://www.martinbraquet.com), ingénieur et chercheur passionné par les défis majeurs de l’humanité — du changement climatique à la sécurité de l’IA, en passant par le bien-être animal.
|
||||
Les correspondances sont évaluées selon l’alignement des réponses et des réponses acceptées, pondéré par l’importance. Voir cette [vidéo](https://www.youtube.com/watch?v=m9PiPlRuy6E) pour les calculs.
|
||||
|
||||
Martin a vécu en Europe, aux États-Unis, en Inde et en Indonésie, s’immergeant dans diverses pratiques allant des retraites de méditation aux coopératives forestières durables. Ces expériences ont façonné sa conviction que des relations humaines profondes et individuelles sont parmi les moteurs les plus significatifs du bien-être et du changement positif.
|
||||
L’[implémentation complète](https://github.com/CompassConnections/Compass/blob/main/common/src/profiles/compatibility-score.ts) est **open source** et ouverte à la révision, aux retours et aux améliorations de la communauté.
|
||||
|
||||
Compass est né de cette conviction. Bien que Martin ait été motivé par la réduction des risques et des souffrances à l’échelle mondiale, il a également reconnu que sa propre vie — et celle de beaucoup d’autres — serait enrichie par des relations plus profondes, proches et solidaires. Bien qu’il ait initié le projet, la plateforme est un cadeau de la communauté — façonnée par les contributeurs, donateurs et soutiens qui permettent son développement. Compass représente la tentative de nombreux utilisateurs de créer une plateforme ouverte, transparente et communautaire, où les gens peuvent se connecter autour de valeurs partagées, de curiosité et de bienveillance, sans distractions liées aux swipes, publicités ou superficialité.
|
||||
|
||||
Martin continue d’agir comme initiateur et gardien de Compass, mais la direction est volontairement confiée à la communauté via la Constitution de Compass (détaillée dans la section suivante).
|
||||
|
||||
### Comment fonctionne la gouvernance ?
|
||||
|
||||
Compass est géré démocratiquement selon une [constitution](/constitution) qui empêche le contrôle centralisé et garantit l’alignement à long terme avec sa mission.
|
||||
|
||||
- Les décisions majeures (portée, financement, règles) sont [votées](/vote) par les **contributeurs actifs**.
|
||||
- La constitution complète est **publique et transparente**.
|
||||
- Pas de capture par des entreprises — Compass restera toujours un projet appartenant à la communauté.
|
||||
## Financement et gouvernance
|
||||
|
||||
### Compass est-il vraiment gratuit ?
|
||||
|
||||
@@ -114,6 +103,26 @@ Grâce aux **[dons](/support) et contributions de la communauté**. Options disp
|
||||
|
||||
Chaque contribution, quelle que soit sa taille — d’un petit correctif de code à l’aide à l’amélioration de l’interface, ou simplement en partageant le projet — fait partie du cadeau qui maintient Compass gratuit, sans publicité et communautaire. Tous les financements et dépenses sont **publiquement documentés** [ici](/financials).
|
||||
|
||||
### Comment fonctionne la gouvernance ?
|
||||
|
||||
Compass est géré démocratiquement selon une [constitution](/constitution) qui empêche le contrôle centralisé et garantit l’alignement à long terme avec sa mission.
|
||||
|
||||
- Les décisions majeures (portée, financement, règles) sont [votées](/vote) par les **contributeurs actifs**.
|
||||
- La constitution complète est **publique et transparente**.
|
||||
- Pas de capture par des entreprises — Compass restera toujours un projet appartenant à la communauté.
|
||||
|
||||
### Qui a créé Compass ?
|
||||
|
||||
Compass a été fondé par [Martin Braquet](https://www.martinbraquet.com), ingénieur et chercheur passionné par les défis majeurs de l’humanité — du changement climatique à la sécurité de l’IA, en passant par le bien-être animal.
|
||||
|
||||
Martin a vécu en Europe, aux États-Unis, en Inde et en Indonésie, s’immergeant dans diverses pratiques allant des retraites de méditation aux coopératives forestières durables. Ces expériences ont façonné sa conviction que des relations humaines profondes et individuelles sont parmi les moteurs les plus significatifs du bien-être et du changement positif.
|
||||
|
||||
Compass est né de cette conviction. Bien que Martin ait été motivé par la réduction des risques et des souffrances à l’échelle mondiale, il a également reconnu que sa propre vie — et celle de beaucoup d’autres — serait enrichie par des relations plus profondes, proches et solidaires. Bien qu’il ait initié le projet, la plateforme est un cadeau de la communauté — façonnée par les contributeurs, donateurs et soutiens qui permettent son développement. Compass représente la tentative de nombreux utilisateurs de créer une plateforme ouverte, transparente et communautaire, où les gens peuvent se connecter autour de valeurs partagées, de curiosité et de bienveillance, sans distractions liées aux swipes, publicités ou superficialité.
|
||||
|
||||
Martin continue d’agir comme initiateur et gardien de Compass, mais la direction est volontairement confiée à la communauté via la Constitution de Compass (détaillée dans la section suivante).
|
||||
|
||||
## Confidentialité et technique
|
||||
|
||||
### Mes données sont-elles sécurisées ?
|
||||
|
||||
Oui.
|
||||
@@ -124,18 +133,6 @@ Oui.
|
||||
pourrait être implémentée dans de futures versions).
|
||||
- Stockage sécurisé dans les bases de données Supabase et Firebase.
|
||||
|
||||
### Comment le score de compatibilité est-il calculé ?
|
||||
|
||||
Le **score de compatibilité** provient des réponses aux **questions de compatibilité**. Chaque utilisateur fournit :
|
||||
|
||||
- **Sa réponse**
|
||||
- **Les réponses qu’il accepterait chez les autres**
|
||||
- **Un degré d’importance** pour chaque question
|
||||
|
||||
Les correspondances sont évaluées selon l’alignement des réponses et des réponses acceptées, pondéré par l’importance. Voir cette [vidéo](https://www.youtube.com/watch?v=m9PiPlRuy6E) pour les calculs.
|
||||
|
||||
L’[implémentation complète](https://github.com/CompassConnections/Compass/blob/main/common/src/profiles/compatibility-score.ts) est **open source** et ouverte à la révision, aux retours et aux améliorations de la communauté.
|
||||
|
||||
### Sur quelles plateformes Compass fonctionne-t-il ?
|
||||
|
||||
Compass est disponible à la fois sous forme de [site web](https://www.compassmeet.com/) et
|
||||
@@ -168,6 +165,22 @@ Oui. Compass propose une **API publique** pour les développeurs souhaitant cré
|
||||
|
||||
Vous pouvez explorer ou tester l’API sur [api.compassmeet.com](https://api.compassmeet.com).
|
||||
|
||||
## Communauté et actualités
|
||||
|
||||
### Comment puis-je aider ?
|
||||
|
||||
- **Donner des retours** : [Remplir le formulaire de suggestion](https://forms.gle/tKnXUMAbEreMK6FC6)
|
||||
- **Participer à la discussion** : [Communauté Discord](https://discord.gg/8Vd7jzqjun)
|
||||
- **Faire et voter des propositions** : [voter ici](/vote)
|
||||
- **Contribuer au développement** : [Voir le code sur GitHub](https://github.com/CompassConnections/Compass)
|
||||
- **Faire un don** : [Soutenir l’infrastructure](/support)
|
||||
- **Faire connaître** : Parlez-en à vos amis et votre famille, partagez sur les forums et réseaux sociaux, ou utilisez
|
||||
ce [QR code](/referrals).
|
||||
|
||||
### Comment puis-je contacter la communauté ?
|
||||
|
||||
Vous pouvez nous contacter via le [formulaire de contact](/contact), le [formulaire de retours](https://forms.gle/tKnXUMAbEreMK6FC6), ou nos [réseaux sociaux](/social).
|
||||
|
||||
### Quelle est la vitesse de croissance de Compass ?
|
||||
|
||||
Compass a officiellement **lancé en octobre 2025** et croît rapidement. Vous pouvez consulter les statistiques en temps réel sur notre [page Croissance & Statistiques](/stats). Cela inclut :
|
||||
@@ -184,6 +197,17 @@ Compass a officiellement **lancé en octobre 2025** et croît rapidement. Vous p
|
||||
|
||||
Comme Compass est entièrement transparent et communautaire, vous pouvez suivre l’évolution de l’écosystème — pas seulement en chiffres, mais aussi dans la manière dont les gens se connectent et collaborent.
|
||||
|
||||
### Quoi de neuf ?
|
||||
|
||||
Restez informé des dernières versions, fonctionnalités et améliorations [ici](/news).
|
||||
|
||||
### Compass est-il mentionné dans la presse ?
|
||||
|
||||
Oui. Vous trouverez la couverture médiatique, notre kit média, nos ressources de marque et les coordonnées presse sur
|
||||
la [page Presse](/press).
|
||||
|
||||
## Vision et perspectives
|
||||
|
||||
### Quelle est la vision à long terme ?
|
||||
|
||||
Notre objectif est que Compass devienne à la connexion humaine ce que Linux est au logiciel, Wikipédia au savoir, ou Firefox à la navigation — une infrastructure publique, open-source, utilisable et fiable par tous. Nous pensons que la connexion humaine significative mérite le même traitement : gratuite, transparente, communautaire et protégée de la captation par les entreprises.
|
||||
@@ -193,17 +217,3 @@ Notre objectif est que Compass devienne à la connexion humaine ce que Linux est
|
||||
Nous nous concentrons sur **la collecte de retours**, **la croissance de la communauté** et **la sécurisation de dons** pour soutenir et étendre la plateforme.
|
||||
|
||||
Chaque action — partage, don ou contribution — aide directement Compass à rester sans publicité, gratuit et communautaire.
|
||||
|
||||
### Comment puis-je aider ?
|
||||
|
||||
- **Donner des retours** : [Remplir le formulaire de suggestion](https://forms.gle/tKnXUMAbEreMK6FC6)
|
||||
- **Participer à la discussion** : [Communauté Discord](https://discord.gg/8Vd7jzqjun)
|
||||
- **Faire et voter des propositions** : [voter ici](/vote)
|
||||
- **Contribuer au développement** : [Voir le code sur GitHub](https://github.com/CompassConnections/Compass)
|
||||
- **Faire un don** : [Soutenir l’infrastructure](/support)
|
||||
- **Faire connaître** : Parlez-en à vos amis et votre famille, partagez sur les forums et réseaux sociaux, ou utilisez
|
||||
ce [QR code](/referrals).
|
||||
|
||||
### Comment puis-je contacter la communauté ?
|
||||
|
||||
Vous pouvez nous contacter via le [formulaire de contact](/contact), le [formulaire de retours](https://forms.gle/tKnXUMAbEreMK6FC6), ou nos [réseaux sociaux](/social).
|
||||
|
||||
Reference in New Issue
Block a user