mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 06:51:45 -04:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277c6a444f | ||
|
|
f344800fd6 | ||
|
|
39a6fba33f | ||
|
|
8e11657bd2 | ||
|
|
dfbeaa4edf | ||
|
|
e90dc3b7f4 | ||
|
|
dba89e611a | ||
|
|
1a3fecc89e | ||
|
|
407e6a3d06 | ||
|
|
6ee19d5359 | ||
|
|
2df424dbac | ||
|
|
9874be6bf1 | ||
|
|
a3d4199d1d | ||
|
|
247fa146a9 | ||
|
|
f2b2c02cd6 | ||
|
|
a915f27f00 | ||
|
|
e14a488934 | ||
|
|
e82a8d9bc3 | ||
|
|
4527a0d12b | ||
|
|
01be202484 | ||
|
|
d1fe99edc3 | ||
|
|
fa629591e9 | ||
|
|
4ab3edc97b | ||
|
|
f1bfc6bf55 | ||
|
|
3283843ef3 | ||
|
|
4cb14ec8cc | ||
|
|
41535a68be | ||
|
|
d62447a12a | ||
|
|
802367c914 | ||
|
|
ff9b2c6ee8 | ||
|
|
a0e25c941a | ||
|
|
091c99e784 | ||
|
|
e264bb407b | ||
|
|
16625210fc | ||
|
|
2550453ee4 | ||
|
|
d1c480f23f | ||
|
|
b4b0397589 | ||
|
|
ab6b34e84c | ||
|
|
87af9d5078 | ||
|
|
95fab7c395 | ||
|
|
90825925ff | ||
|
|
7036cf9e49 | ||
|
|
53123eb0ee | ||
|
|
3c5407dd51 | ||
|
|
1ffe81f740 | ||
|
|
6bb35d61e1 | ||
|
|
f36ccf7bdc | ||
|
|
4632e68a00 | ||
|
|
09858d0783 | ||
|
|
9d1423c41b | ||
|
|
1a4b7786dd | ||
|
|
77c0a21ad0 | ||
|
|
7cedf14121 | ||
|
|
235346f3dd | ||
|
|
34c36b7c3a | ||
|
|
3e0f788ec3 | ||
|
|
867bb8a072 | ||
|
|
31a400158a | ||
|
|
8106ff6489 | ||
|
|
de3508993c | ||
|
|
fd3e7a6f8a | ||
|
|
4cf97a6054 | ||
|
|
75036e3ec7 |
@@ -4,6 +4,7 @@
|
||||
|
||||
# For database connection. A 16-character password with digits and letters.
|
||||
SUPABASE_DB_PASSWORD=
|
||||
NEXT_PUBLIC_SUPABASE_KEY=
|
||||
|
||||
# For authentication.
|
||||
# Ask the project admin. Should start with "AIza".
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -46,10 +46,12 @@ jobs:
|
||||
# npx playwright install
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
NEXT_PUBLIC_API_URL: localhost:8088
|
||||
NEXT_PUBLIC_FIREBASE_ENV: PROD
|
||||
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
|
||||
run: |
|
||||
NEXT_PUBLIC_API_URL=localhost:8088 \
|
||||
NEXT_PUBLIC_FIREBASE_ENV=PROD \
|
||||
NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} \
|
||||
yarn --cwd=web serve &
|
||||
npx wait-on http://localhost:3000
|
||||
npx playwright test tests/playwright
|
||||
|
||||
@@ -29,7 +29,7 @@ A detailed description of the vision is available [here](https://martinbraquet.c
|
||||
- [x] Ask for detailed info upon registration (location, desired type of connection, prompt answers, gender, etc.)
|
||||
- [x] Set up page listing all the profiles
|
||||
- [x] Search through most profile variables
|
||||
- [x] (Set up chat / direct messaging)
|
||||
- [x] Set up chat / direct messaging
|
||||
- [x] Set up domain name (https://compassmeet.com)
|
||||
|
||||
#### Secondary To Do
|
||||
@@ -39,8 +39,9 @@ Any action item is open to anyone for collaboration, but the following ones are
|
||||
- [ ] Add profile features (intellectual interests, cause areas, personality type, conflict style, etc.)
|
||||
- [ ] Add filters to search through remaining profile features (politics, religion, education level, etc.)
|
||||
- [ ] Cover with tests (very important, just the test template and framework are ready)
|
||||
- [ ] Clean up terms and conditions
|
||||
- [ ] Clean up privacy notice
|
||||
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
||||
- [ ] Clean up terms and conditions (convert to Markdown)
|
||||
- [ ] Clean up privacy notice (convert to Markdown)
|
||||
- [x] Clean up learn more page
|
||||
- [x] Add dark theme
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
source ../../.env
|
||||
|
||||
ENV=${1:-prod}
|
||||
@@ -28,7 +30,6 @@ IMAGE_TAG="${TIMESTAMP}-${GIT_REVISION}"
|
||||
IMAGE_URL="${REGION}-docker.pkg.dev/${PROJECT}/builds/${SERVICE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
echo "🚀 Deploying ${SERVICE_NAME} to ${ENV} ($(date "+%Y-%m-%d %I:%M:%S %p"))"
|
||||
yarn add tsconfig-paths
|
||||
yarn build
|
||||
|
||||
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin us-west1-docker.pkg.dev
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"description": "Backend API endpoints",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"watch:compile": "npx concurrently \"tsc -b --watch --preserveWatchOutput\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\" \"tsc-alias --watch\"",
|
||||
@@ -51,8 +51,8 @@
|
||||
"lodash": "4.17.21",
|
||||
"pg-promise": "11.4.1",
|
||||
"posthog-node": "4.11.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "3.0.7",
|
||||
"resend": "4.1.2",
|
||||
"string-similarity": "4.0.4",
|
||||
@@ -63,6 +63,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "2.8.17",
|
||||
"@types/ws": "8.5.10"
|
||||
"@types/ws": "8.5.10",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export const getLovers: APIHandler<'get-lovers'> = async (props, _auth) => {
|
||||
where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
|
||||
|
||||
name &&
|
||||
where(`lower(users.name) ilike '%' || lower($(name)) || '%'`, { name }),
|
||||
where(`lower(users.name) ilike '%' || lower($(name)) || '%' or lower(bio::text) ilike '%' || lower($(name)) || '%'`, { name }),
|
||||
|
||||
genders?.length && where(`gender = ANY($(gender))`, { gender: genders }),
|
||||
|
||||
|
||||
@@ -1,36 +1,8 @@
|
||||
import { APIHandler } from './helpers/endpoint'
|
||||
import {APIHandler} from './helpers/endpoint'
|
||||
import {geodbFetch} from "common/geodb";
|
||||
|
||||
export const searchLocation: APIHandler<'search-location'> = async (body) => {
|
||||
const { term, limit } = body
|
||||
const apiKey = process.env.GEODB_API_KEY
|
||||
console.log('GEODB_API_KEY', apiKey)
|
||||
|
||||
if (!apiKey) {
|
||||
return { status: 'failure', data: 'Missing GEODB API key' }
|
||||
}
|
||||
const host = 'wft-geo-db.p.rapidapi.com'
|
||||
const baseUrl = `https://${host}/v1/geo`
|
||||
const url = `${baseUrl}/cities?namePrefix=${term}&limit=${
|
||||
limit ?? 10
|
||||
}&offset=0&sort=-population`
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-RapidAPI-Key': apiKey,
|
||||
'X-RapidAPI-Host': host,
|
||||
},
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! Status: ${res.status} ${await res.text()}`)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
// console.log('GEO DB', data)
|
||||
return { status: 'success', data: data }
|
||||
} catch (error: any) {
|
||||
console.log('failure', error)
|
||||
return { status: 'failure', data: error.message }
|
||||
}
|
||||
const {term, limit} = body
|
||||
const endpoint = `/cities?namePrefix=${term}&limit=${limit ?? 10}&offset=0&sort=-population`
|
||||
return await geodbFetch(endpoint)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,17 @@
|
||||
import { APIHandler } from './helpers/endpoint'
|
||||
import {APIHandler} from './helpers/endpoint'
|
||||
import {geodbFetch} from "common/geodb";
|
||||
|
||||
const searchNearCityMain = async (cityId: string, radius: number) => {
|
||||
// Limit to 10 cities for now for free plan, was 100 before (may need to buy plan)
|
||||
const endpoint = `/cities/${cityId}/nearbyCities?radius=${radius}&offset=0&sort=-population&limit=10`
|
||||
return await geodbFetch(endpoint)
|
||||
}
|
||||
|
||||
export const searchNearCity: APIHandler<'search-near-city'> = async (body) => {
|
||||
const { cityId, radius } = body
|
||||
return await searchNearCityMain(cityId, radius)
|
||||
}
|
||||
|
||||
const searchNearCityMain = async (cityId: string, radius: number) => {
|
||||
const apiKey = process.env.GEODB_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
return { status: 'failure', data: 'Missing GEODB API key' }
|
||||
}
|
||||
const host = 'wft-geo-db.p.rapidapi.com'
|
||||
const baseUrl = `https://${host}/v1/geo`
|
||||
const url = `${baseUrl}/cities/${cityId}/nearbyCities?radius=${radius}&offset=0&sort=-population&limit=100`
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-RapidAPI-Key': apiKey,
|
||||
'X-RapidAPI-Host': host,
|
||||
},
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! Status: ${res.status}`)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
return { status: 'success', data: data }
|
||||
} catch (error) {
|
||||
return { status: 'failure', data: error }
|
||||
}
|
||||
}
|
||||
|
||||
export const getNearbyCities = async (cityId: string, radius: number) => {
|
||||
const result = await searchNearCityMain(cityId, radius)
|
||||
const cityIds = (result.data.data as any[]).map(
|
||||
|
||||
@@ -29,6 +29,7 @@ export const sendNewMatchEmail = async (
|
||||
react: (
|
||||
<NewMatchEmail
|
||||
onUser={lover.user}
|
||||
email={privateUser.email}
|
||||
matchedWithUser={matchedWithUser}
|
||||
matchedLover={lover}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
@@ -67,6 +68,7 @@ export const sendNewMessageEmail = async (
|
||||
toUser={toUser}
|
||||
channelId={channelId}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
email={privateUser.email}
|
||||
/>
|
||||
),
|
||||
})
|
||||
@@ -82,6 +84,7 @@ export const sendNewMessageEmail = async (
|
||||
toUser={toUser}
|
||||
channelId={channelId}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
email={privateUser.email}
|
||||
/>
|
||||
),
|
||||
})
|
||||
@@ -109,6 +112,7 @@ export const sendNewEndorsementEmail = async (
|
||||
onUser={onUser}
|
||||
endorsementText={text}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
email={privateUser.email}
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ const getResend = () => {
|
||||
if (resend) return resend
|
||||
|
||||
const apiKey = process.env.RESEND_KEY as string
|
||||
console.log(`RESEND_KEY: ${apiKey}`)
|
||||
// console.log(`RESEND_KEY: ${apiKey}`)
|
||||
resend = new Resend(apiKey)
|
||||
return resend
|
||||
}
|
||||
|
||||
@@ -1,41 +1,31 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Column,
|
||||
Head,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { type User } from 'common/user'
|
||||
import { DOMAIN } from 'common/envs/constants'
|
||||
import { jamesUser, sinclairUser } from './functions/mock'
|
||||
import {Body, Button, Column, Container, Head, Html, Preview, Row, Section, Text,} from '@react-email/components'
|
||||
import {type User} from 'common/user'
|
||||
import {DOMAIN} from 'common/envs/constants'
|
||||
import {jamesUser, sinclairUser} from './functions/mock'
|
||||
import {button, container, content, Footer, main, paragraph} from "email/utils";
|
||||
|
||||
interface NewEndorsementEmailProps {
|
||||
fromUser: User
|
||||
onUser: User
|
||||
endorsementText: string
|
||||
unsubscribeUrl: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export const NewEndorsementEmail = ({
|
||||
fromUser,
|
||||
onUser,
|
||||
endorsementText,
|
||||
unsubscribeUrl,
|
||||
}: NewEndorsementEmailProps) => {
|
||||
fromUser,
|
||||
onUser,
|
||||
endorsementText,
|
||||
unsubscribeUrl,
|
||||
email,
|
||||
}: NewEndorsementEmailProps) => {
|
||||
const name = onUser.name.split(' ')[0]
|
||||
|
||||
const endorsementUrl = `https://${DOMAIN}/${onUser.username}`
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Head/>
|
||||
<Preview>New endorsement from {fromUser.name}</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
@@ -55,15 +45,15 @@ export const NewEndorsementEmail = ({
|
||||
|
||||
<Section style={endorsementContainer}>
|
||||
<Row>
|
||||
<Column>
|
||||
<Img
|
||||
src={fromUser.avatarUrl}
|
||||
width="50"
|
||||
height="50"
|
||||
alt=""
|
||||
style={avatarImage}
|
||||
/>
|
||||
</Column>
|
||||
{/*<Column>*/}
|
||||
{/* <Img*/}
|
||||
{/* src={fromUser.avatarUrl}*/}
|
||||
{/* width="50"*/}
|
||||
{/* height="50"*/}
|
||||
{/* alt=""*/}
|
||||
{/* style={avatarImage}*/}
|
||||
{/* />*/}
|
||||
{/*</Column>*/}
|
||||
<Column>
|
||||
<Text style={endorsementTextStyle}>"{endorsementText}"</Text>
|
||||
</Column>
|
||||
@@ -75,15 +65,7 @@ export const NewEndorsementEmail = ({
|
||||
</Section>
|
||||
</Section>
|
||||
|
||||
<Section style={footer}>
|
||||
<Text style={footerText}>
|
||||
This e-mail has been sent to {name},{' '}
|
||||
{/* <Link href={unsubscribeUrl} style={footerLink}>
|
||||
click here to unsubscribe from this type of notification
|
||||
</Link>
|
||||
. */}
|
||||
</Text>
|
||||
</Section>
|
||||
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
@@ -96,37 +78,9 @@ NewEndorsementEmail.PreviewProps = {
|
||||
endorsementText:
|
||||
"Sinclair is someone you want to have around because she injects creativity and humor into every conversation, and her laugh is infectious! Not to mention that she's a great employee, treats everyone with respect, and is even-tempered.",
|
||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||
email: 'someone@gmail.com',
|
||||
} as NewEndorsementEmailProps
|
||||
|
||||
const main = {
|
||||
backgroundColor: '#f4f4f4',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
wordSpacing: 'normal',
|
||||
}
|
||||
|
||||
const container = {
|
||||
margin: '0 auto',
|
||||
maxWidth: '600px',
|
||||
}
|
||||
|
||||
const logoContainer = {
|
||||
padding: '20px 0px 5px 0px',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#ffffff',
|
||||
}
|
||||
|
||||
const content = {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: '20px 25px',
|
||||
}
|
||||
|
||||
const paragraph = {
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
margin: '10px 0',
|
||||
color: '#000000',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
}
|
||||
|
||||
const endorsementContainer = {
|
||||
margin: '20px 0',
|
||||
@@ -135,10 +89,6 @@ const endorsementContainer = {
|
||||
borderRadius: '8px',
|
||||
}
|
||||
|
||||
const avatarImage = {
|
||||
borderRadius: '50%',
|
||||
}
|
||||
|
||||
const endorsementTextStyle = {
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
@@ -146,35 +96,4 @@ const endorsementTextStyle = {
|
||||
color: '#333333',
|
||||
}
|
||||
|
||||
const button = {
|
||||
backgroundColor: '#4887ec',
|
||||
borderRadius: '12px',
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'semibold',
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center' as const,
|
||||
display: 'inline-block',
|
||||
padding: '6px 10px',
|
||||
margin: '10px 0',
|
||||
}
|
||||
|
||||
const footer = {
|
||||
margin: '20px 0',
|
||||
textAlign: 'center' as const,
|
||||
}
|
||||
|
||||
const footerText = {
|
||||
fontSize: '11px',
|
||||
lineHeight: '22px',
|
||||
color: '#000000',
|
||||
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
||||
}
|
||||
|
||||
const footerLink = {
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}
|
||||
|
||||
export default NewEndorsementEmail
|
||||
|
||||
@@ -1,51 +1,43 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { DOMAIN } from 'common/envs/constants'
|
||||
import { type LoverRow } from 'common/love/lover'
|
||||
import { getLoveOgImageUrl } from 'common/love/og-image'
|
||||
import { type User } from 'common/user'
|
||||
import { jamesLover, jamesUser, sinclairUser } from './functions/mock'
|
||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||
import {DOMAIN} from 'common/envs/constants'
|
||||
import {type LoverRow} from 'common/love/lover'
|
||||
import {type User} from 'common/user'
|
||||
import {jamesLover, jamesUser, sinclairUser} from './functions/mock'
|
||||
import {Footer} from "email/utils";
|
||||
|
||||
interface NewMatchEmailProps {
|
||||
onUser: User
|
||||
matchedWithUser: User
|
||||
matchedLover: LoverRow
|
||||
unsubscribeUrl: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export const NewMatchEmail = ({
|
||||
onUser,
|
||||
matchedWithUser,
|
||||
matchedLover,
|
||||
unsubscribeUrl,
|
||||
}: NewMatchEmailProps) => {
|
||||
onUser,
|
||||
matchedWithUser,
|
||||
matchedLover,
|
||||
unsubscribeUrl,
|
||||
email
|
||||
}: NewMatchEmailProps) => {
|
||||
const name = onUser.name.split(' ')[0]
|
||||
const userImgSrc = getLoveOgImageUrl(matchedWithUser, matchedLover)
|
||||
// const userImgSrc = getLoveOgImageUrl(matchedWithUser, matchedLover)
|
||||
const userUrl = `https://${DOMAIN}/${matchedWithUser.username}`
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Head/>
|
||||
<Preview>You have a new match!</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
|
||||
{/*<Section style={logoContainer}>*/}
|
||||
{/* <Img*/}
|
||||
{/* src="..."*/}
|
||||
{/* width="550"*/}
|
||||
{/* height="auto"*/}
|
||||
{/* alt="compassmeet.com"*/}
|
||||
{/* />*/}
|
||||
{/*<Img*/}
|
||||
{/* src="..."*/}
|
||||
{/* width="550"*/}
|
||||
{/* height="auto"*/}
|
||||
{/* alt="compassmeet.com"*/}
|
||||
{/*/>*/}
|
||||
{/*</Section>*/}
|
||||
|
||||
<Section style={content}>
|
||||
@@ -56,31 +48,21 @@ export const NewMatchEmail = ({
|
||||
</Text>
|
||||
|
||||
<Section style={imageContainer}>
|
||||
<Link href={userUrl}>
|
||||
<Img
|
||||
src={userImgSrc}
|
||||
width="375"
|
||||
height="200"
|
||||
alt=""
|
||||
style={profileImage}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/*<Link href={userUrl}>*/}
|
||||
{/* <Img*/}
|
||||
{/* src={userImgSrc}*/}
|
||||
{/* width="375"*/}
|
||||
{/* height="200"*/}
|
||||
{/* alt=""*/}
|
||||
{/* style={profileImage}*/}
|
||||
{/* />*/}
|
||||
{/*</Link>*/}
|
||||
<Button href={userUrl} style={button}>
|
||||
View profile
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
|
||||
<Section style={footer}>
|
||||
<Text style={footerText}>
|
||||
This e-mail has been sent to {name},{' '}
|
||||
{/* <Link href={unsubscribeUrl} style={footerLink}>
|
||||
click here to unsubscribe from this type of notification
|
||||
</Link>
|
||||
. */}
|
||||
</Text>
|
||||
</Section>
|
||||
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
@@ -91,11 +73,12 @@ NewMatchEmail.PreviewProps = {
|
||||
onUser: sinclairUser,
|
||||
matchedWithUser: jamesUser,
|
||||
matchedLover: jamesLover,
|
||||
email: 'someone@gmail.com',
|
||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||
} as NewMatchEmailProps
|
||||
|
||||
const main = {
|
||||
backgroundColor: '#f4f4f4',
|
||||
// backgroundColor: '#f4f4f4',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
wordSpacing: 'normal',
|
||||
}
|
||||
@@ -147,21 +130,4 @@ const button = {
|
||||
margin: '10px 0',
|
||||
}
|
||||
|
||||
const footer = {
|
||||
margin: '20px 0',
|
||||
textAlign: 'center' as const,
|
||||
}
|
||||
|
||||
const footerText = {
|
||||
fontSize: '11px',
|
||||
lineHeight: '22px',
|
||||
color: '#000000',
|
||||
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
||||
}
|
||||
|
||||
const footerLink = {
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}
|
||||
|
||||
export default NewMatchEmail
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from './functions/mock'
|
||||
import { DOMAIN } from 'common/envs/constants'
|
||||
import { getLoveOgImageUrl } from 'common/love/og-image'
|
||||
import {button, container, content, Footer, imageContainer, main, paragraph, profileImage} from "email/utils";
|
||||
|
||||
interface NewMessageEmailProps {
|
||||
fromUser: User
|
||||
@@ -27,6 +28,7 @@ interface NewMessageEmailProps {
|
||||
toUser: User
|
||||
channelId: number
|
||||
unsubscribeUrl: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export const NewMessageEmail = ({
|
||||
@@ -35,6 +37,7 @@ export const NewMessageEmail = ({
|
||||
toUser,
|
||||
channelId,
|
||||
unsubscribeUrl,
|
||||
email,
|
||||
}: NewMessageEmailProps) => {
|
||||
const name = toUser.name.split(' ')[0]
|
||||
const creatorName = fromUser.name
|
||||
@@ -62,15 +65,15 @@ export const NewMessageEmail = ({
|
||||
<Text style={paragraph}>{creatorName} just messaged you!</Text>
|
||||
|
||||
<Section style={imageContainer}>
|
||||
<Link href={messagesUrl}>
|
||||
<Img
|
||||
src={userImgSrc}
|
||||
width="375"
|
||||
height="200"
|
||||
alt={`${creatorName}'s profile`}
|
||||
style={profileImage}
|
||||
/>
|
||||
</Link>
|
||||
{/*<Link href={messagesUrl}>*/}
|
||||
{/* <Img*/}
|
||||
{/* src={userImgSrc}*/}
|
||||
{/* width="375"*/}
|
||||
{/* height="200"*/}
|
||||
{/* alt={`${creatorName}'s profile`}*/}
|
||||
{/* style={profileImage}*/}
|
||||
{/* />*/}
|
||||
{/*</Link>*/}
|
||||
|
||||
<Button href={messagesUrl} style={button}>
|
||||
View message
|
||||
@@ -78,15 +81,7 @@ export const NewMessageEmail = ({
|
||||
</Section>
|
||||
</Section>
|
||||
|
||||
<Section style={footer}>
|
||||
<Text style={footerText}>
|
||||
This e-mail has been sent to {name},{' '}
|
||||
{/* <Link href={unsubscribeUrl} style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
click here to unsubscribe from this type of notification
|
||||
</Link>
|
||||
. */}
|
||||
</Text>
|
||||
</Section>
|
||||
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
@@ -98,72 +93,9 @@ NewMessageEmail.PreviewProps = {
|
||||
fromUserLover: jamesLover,
|
||||
toUser: sinclairUser,
|
||||
channelId: 1,
|
||||
email: 'someone@gmail.com',
|
||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||
} as NewMessageEmailProps
|
||||
|
||||
const main = {
|
||||
backgroundColor: '#f4f4f4',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
wordSpacing: 'normal',
|
||||
}
|
||||
|
||||
const container = {
|
||||
margin: '0 auto',
|
||||
maxWidth: '600px',
|
||||
}
|
||||
|
||||
const logoContainer = {
|
||||
padding: '20px 0px 5px 0px',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#ffffff',
|
||||
}
|
||||
|
||||
const content = {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: '20px 25px',
|
||||
}
|
||||
|
||||
const paragraph = {
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
margin: '10px 0',
|
||||
color: '#000000',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
}
|
||||
|
||||
const imageContainer = {
|
||||
textAlign: 'center' as const,
|
||||
margin: '20px 0',
|
||||
}
|
||||
|
||||
const profileImage = {
|
||||
// border: '1px solid #ec489a',
|
||||
}
|
||||
|
||||
const button = {
|
||||
backgroundColor: '#4887ec',
|
||||
borderRadius: '12px',
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'semibold',
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center' as const,
|
||||
display: 'inline-block',
|
||||
padding: '6px 10px',
|
||||
margin: '10px 0',
|
||||
}
|
||||
|
||||
const footer = {
|
||||
margin: '20px 0',
|
||||
textAlign: 'center' as const,
|
||||
}
|
||||
|
||||
const footerText = {
|
||||
fontSize: '11px',
|
||||
lineHeight: '22px',
|
||||
color: '#000000',
|
||||
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
||||
}
|
||||
|
||||
export default NewMessageEmail
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Test = (props: { name: string }) => {
|
||||
}
|
||||
|
||||
Test.PreviewProps = {
|
||||
name: 'Clarity',
|
||||
name: 'Friend',
|
||||
}
|
||||
|
||||
export default Test
|
||||
|
||||
117
backend/email/emails/utils.tsx
Normal file
117
backend/email/emails/utils.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import {Link, Row, Section, Text} from "@react-email/components";
|
||||
|
||||
interface Props {
|
||||
email?: string
|
||||
unsubscribeUrl: string
|
||||
}
|
||||
|
||||
export const Footer = ({
|
||||
email,
|
||||
unsubscribeUrl,
|
||||
}: Props) => {
|
||||
return <Section style={footer}>
|
||||
<hr style={{border: 'none', borderTop: '1px solid #e0e0e0', margin: '20px 0'}}/>
|
||||
<Row>
|
||||
<Text style={footerText}>
|
||||
Compass © {new Date().getFullYear()}
|
||||
</Text>
|
||||
{/*<Row>*/}
|
||||
{/* <Link href="https://github.com/CompassMeet/Compass">*/}
|
||||
{/* <TbBrandGithub size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/* <Link href="https://discord.gg/8Vd7jzqjun">*/}
|
||||
{/* <TbBrandDiscord size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/* <Link href="https://patreon.com/CompassMeet">*/}
|
||||
{/* <TbBrandPatreon size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/* <Link href="https://www.paypal.com/paypalme/MartinBraquet">*/}
|
||||
{/* <TbBrandPaypal size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/*</Row>*/}
|
||||
<Text style={footerText}>
|
||||
The email was sent to {email}. To no longer receive these emails, unsubscribe {' '}
|
||||
<Link href={unsubscribeUrl}>
|
||||
here
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Row>
|
||||
</Section>
|
||||
}
|
||||
|
||||
export const footer = {
|
||||
margin: '20px 0',
|
||||
textAlign: 'center' as const,
|
||||
}
|
||||
|
||||
export const footerText = {
|
||||
fontSize: '11px',
|
||||
lineHeight: '22px',
|
||||
color: '#000000',
|
||||
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
||||
}
|
||||
|
||||
export const blackLinks = {
|
||||
color: 'black'
|
||||
}
|
||||
|
||||
|
||||
// const footerLink = {
|
||||
// color: 'inherit',
|
||||
// textDecoration: 'none',
|
||||
// }
|
||||
|
||||
|
||||
export const main = {
|
||||
// backgroundColor: '#f4f4f4',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
wordSpacing: 'normal',
|
||||
}
|
||||
|
||||
export const container = {
|
||||
margin: '0 auto',
|
||||
maxWidth: '600px',
|
||||
}
|
||||
|
||||
export const logoContainer = {
|
||||
padding: '20px 0px 5px 0px',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#ffffff',
|
||||
}
|
||||
|
||||
export const content = {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: '20px 25px',
|
||||
}
|
||||
|
||||
export const paragraph = {
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
margin: '10px 0',
|
||||
color: '#000000',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
}
|
||||
|
||||
export const imageContainer = {
|
||||
textAlign: 'center' as const,
|
||||
margin: '20px 0',
|
||||
}
|
||||
|
||||
export const profileImage = {
|
||||
// border: '1px solid #ec489a',
|
||||
}
|
||||
|
||||
export const button = {
|
||||
backgroundColor: '#4887ec',
|
||||
borderRadius: '12px',
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'semibold',
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center' as const,
|
||||
display: 'inline-block',
|
||||
padding: '6px 10px',
|
||||
margin: '10px 0',
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"name": "react-email-starter",
|
||||
"version": "0.1.9",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "email dev",
|
||||
"dev": "email dev --port 3001",
|
||||
"build": "tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.0.33",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "3.0.7",
|
||||
"react-icons": "5.5.0",
|
||||
"resend": "4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "9.0.4",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4"
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,7 +65,7 @@ const newClient = (
|
||||
...settings,
|
||||
}
|
||||
|
||||
console.log(config)
|
||||
// console.log(config)
|
||||
|
||||
return pgp(config)
|
||||
}
|
||||
|
||||
@@ -19,4 +19,8 @@ BEGIN;
|
||||
\i backend/supabase/user_events.sql
|
||||
\i backend/supabase/user_notifications.sql
|
||||
\i backend/supabase/functions_others.sql
|
||||
\i backend/supabase/reports.sql
|
||||
\i backend/supabase/migrations/20250910_bio_to_jsonb.sql
|
||||
\i backend/supabase/migrations/20250910_add_bio_text.sql
|
||||
\i backend/supabase/migrations/20250910_pg_trgm.sql
|
||||
COMMIT;
|
||||
@@ -1,23 +0,0 @@
|
||||
-- This file is copied from https://github.com/manifoldmarkets/manifold/blob/main/backend/supabase/reports.sql
|
||||
create table if not exists
|
||||
reports (
|
||||
content_id text not null,
|
||||
content_owner_id text not null,
|
||||
content_type text not null,
|
||||
created_time timestamp with time zone default now(),
|
||||
description text,
|
||||
id text default uuid_generate_v4 () not null,
|
||||
parent_id text,
|
||||
parent_type text,
|
||||
user_id text not null
|
||||
);
|
||||
|
||||
-- Foreign Keys
|
||||
alter table reports
|
||||
add constraint reports_content_owner_id_fkey foreign key (content_owner_id) references users (id);
|
||||
|
||||
alter table reports
|
||||
add constraint reports_user_id_fkey foreign key (user_id) references users (id);
|
||||
|
||||
-- Row Level Security
|
||||
alter table reports enable row level security;
|
||||
22
backend/supabase/migrations/20250910_add_bio_text.sql
Normal file
22
backend/supabase/migrations/20250910_add_bio_text.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
ALTER TABLE lovers ADD COLUMN bio_text tsvector;
|
||||
|
||||
CREATE OR REPLACE FUNCTION lovers_bio_tsvector_update()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
new.bio_text := to_tsvector(
|
||||
'english',
|
||||
(
|
||||
SELECT string_agg(trim(both '"' from x::text), ' ')
|
||||
FROM jsonb_path_query(new.bio, '$.**.text'::jsonpath) AS x
|
||||
)
|
||||
);
|
||||
RETURN new;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER lovers_bio_tsvector_trigger
|
||||
BEFORE INSERT OR UPDATE OF bio ON lovers
|
||||
FOR EACH ROW EXECUTE FUNCTION lovers_bio_tsvector_update();
|
||||
|
||||
|
||||
create index on lovers using gin(bio_text);
|
||||
5
backend/supabase/migrations/20250910_bio_to_jsonb.sql
Normal file
5
backend/supabase/migrations/20250910_bio_to_jsonb.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- 1. Drop the old column
|
||||
alter table lovers drop column if exists bio;
|
||||
|
||||
-- 2. Add the new column as jsonb
|
||||
alter table lovers add column bio jsonb;
|
||||
4
backend/supabase/migrations/20250910_pg_trgm.sql
Normal file
4
backend/supabase/migrations/20250910_pg_trgm.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
create extension if not exists pg_trgm;
|
||||
|
||||
CREATE INDEX lovers_bio_trgm_idx
|
||||
ON lovers USING gin ((bio::text) gin_trgm_ops);
|
||||
@@ -51,10 +51,9 @@ export const baseLoversSchema = z.object({
|
||||
pref_age_max: z.number().min(18).max(1000),
|
||||
pref_relation_styles: z.array(
|
||||
z.union([
|
||||
z.literal('mono'),
|
||||
z.literal('poly'),
|
||||
z.literal('open'),
|
||||
z.literal('other'),
|
||||
z.literal('collaboration'),
|
||||
z.literal('friendship'),
|
||||
z.literal('relationship'),
|
||||
])
|
||||
),
|
||||
wants_kids_strength: z.number(),
|
||||
|
||||
@@ -46,7 +46,7 @@ export const PROD_CONFIG: EnvConfig = {
|
||||
cloudRunId: 'w3txbmd3ba',
|
||||
cloudRunRegion: 'uc',
|
||||
supabaseInstanceId: 'ltzepxnhhnrnvovqblfr',
|
||||
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx0emVweG5oaG5ybnZvdnFibGZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU5NjczNjgsImV4cCI6MjA3MTU0MzM2OH0.pbazcrVOG7Kh_IgblRu2VAfoBe3-xheNfRzAto7xvzY',
|
||||
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_KEY || '',
|
||||
apiEndpoint: 'api.compassmeet.com',
|
||||
adminIds: [
|
||||
'0vaZsIJk9zLVOWY4gb61gTrRIU73', // Martin
|
||||
|
||||
32
common/src/geodb.ts
Normal file
32
common/src/geodb.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
export const geodbHost = 'wft-geo-db.p.rapidapi.com'
|
||||
|
||||
export const geodbFetch = async (endpoint: string) => {
|
||||
const apiKey = process.env.GEODB_API_KEY
|
||||
if (!apiKey) {
|
||||
return {status: 'failure', data: 'Missing GEODB API key'}
|
||||
}
|
||||
const baseUrl = `https://${geodbHost}/v1/geo`
|
||||
const url = `${baseUrl}${endpoint}`
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-RapidAPI-Key': apiKey,
|
||||
'X-RapidAPI-Host': geodbHost,
|
||||
},
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! Status: ${res.status} ${await res.text()}`)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
console.log('geodbFetch', endpoint, data)
|
||||
return {status: 'success', data}
|
||||
} catch (error) {
|
||||
console.log('geodbFetch', endpoint, error)
|
||||
return {status: 'failure', data: error}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ const isPreferredGender = (
|
||||
preferredGenders: string[] | undefined,
|
||||
gender: string | undefined
|
||||
) => {
|
||||
if (preferredGenders === undefined || gender === undefined) return true
|
||||
// console.log('isPreferredGender', preferredGenders, gender)
|
||||
if (preferredGenders === undefined || preferredGenders.length === 0 || gender === undefined) return true
|
||||
|
||||
// If simple gender preference, don't include non-binary.
|
||||
if (
|
||||
@@ -17,6 +18,7 @@ const isPreferredGender = (
|
||||
}
|
||||
|
||||
export const areGenderCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
||||
// console.log('areGenderCompatible', isPreferredGender(lover1.pref_gender, lover2.gender), isPreferredGender(lover2.pref_gender, lover1.gender))
|
||||
return (
|
||||
isPreferredGender(lover1.pref_gender, lover2.gender) &&
|
||||
isPreferredGender(lover2.pref_gender, lover1.gender)
|
||||
|
||||
@@ -5,11 +5,12 @@ export const SITE_ORDER = [
|
||||
'bluesky',
|
||||
'mastodon',
|
||||
'substack',
|
||||
// 'onlyfans',
|
||||
'paypal',
|
||||
'instagram',
|
||||
'github',
|
||||
'linkedin',
|
||||
'facebook',
|
||||
'patreon',
|
||||
'spotify',
|
||||
] as const
|
||||
|
||||
@@ -29,6 +30,8 @@ const stripper: { [key in Site]: (input: string) => string } = {
|
||||
.replace(/^@/, '')
|
||||
.replace(/\/$/, ''),
|
||||
discord: (s) => s,
|
||||
paypal: (s) => s,
|
||||
patreon: (s) => s,
|
||||
bluesky: (s) =>
|
||||
s
|
||||
.replace(/^(https?:\/\/)?(www\.)?bsky\.app\/profile\//, '')
|
||||
@@ -83,6 +86,8 @@ const urler: { [key in Site]: (handle: string) => string } = {
|
||||
linkedin: (s) => `https://linkedin.com/in/${s}`,
|
||||
facebook: (s) => `https://facebook.com/${s}`,
|
||||
spotify: (s) => `https://open.spotify.com/user/${s}`,
|
||||
paypal: (s) => `https://paypal.com/user/${s}`,
|
||||
patreon: (s) => `https://patreon.com/user/${s}`,
|
||||
}
|
||||
|
||||
export const PLATFORM_LABELS: { [key in Site]: string } = {
|
||||
@@ -98,4 +103,6 @@ export const PLATFORM_LABELS: { [key in Site]: string } = {
|
||||
linkedin: 'LinkedIn',
|
||||
facebook: 'Facebook',
|
||||
spotify: 'Spotify',
|
||||
paypal: 'Paypal',
|
||||
patreon: 'Patreon',
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function createClient(
|
||||
opts?: SupabaseClientOptionsGeneric<'public'>
|
||||
) {
|
||||
const url = `https://${instanceId}.supabase.co`
|
||||
console.log('createClient', instanceId, key, opts)
|
||||
// console.log('createClient', instanceId, key, opts)
|
||||
return createClientGeneric(
|
||||
url,
|
||||
key,
|
||||
|
||||
@@ -68,7 +68,7 @@ export const getNotificationDestinationsForUser = (
|
||||
destinations.includes('browser') && !opt_out.includes('browser'),
|
||||
sendToMobile:
|
||||
destinations.includes('mobile') && !opt_out.includes('mobile'),
|
||||
unsubscribeUrl: 'TODO',
|
||||
unsubscribeUrl: 'https://compassmeet.com/notifications',
|
||||
urlToManageThisNotification: '/notifications',
|
||||
}
|
||||
}
|
||||
|
||||
2704
common/yarn.lock
2704
common/yarn.lock
File diff suppressed because it is too large
Load Diff
10
install.sh
Executable file
10
install.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
rm -rf node_modules web/node_modules backend/api/node_modules backend/email/node_modules command/node_modules backend/shared/node_modules
|
||||
|
||||
yarn install
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
psql \
|
||||
-h db.ltzepxnhhnrnvovqblfr.supabase.co \
|
||||
-p 5432 \
|
||||
-d postgres \
|
||||
-U postgres \
|
||||
-f migration.sql
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "compass",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"common",
|
||||
@@ -22,22 +22,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@tiptap/core": "2.3.2",
|
||||
"@tiptap/extension-blockquote": "2.3.2",
|
||||
"@tiptap/extension-bold": "2.3.2",
|
||||
"@tiptap/extension-bubble-menu": "2.3.2",
|
||||
"@tiptap/extension-floating-menu": "2.3.2",
|
||||
"@tiptap/extension-image": "2.3.2",
|
||||
"@tiptap/extension-link": "2.3.2",
|
||||
"@tiptap/extension-mention": "2.3.2",
|
||||
"@tiptap/html": "2.3.2",
|
||||
"@tiptap/starter-kit": "2.3.2",
|
||||
"@tiptap/suggestion": "2.3.2",
|
||||
"colorette": "^2.0.20",
|
||||
"react-markdown": "*",
|
||||
"prismjs": "^1.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -67,6 +57,13 @@
|
||||
"@tiptap/extension-mention": "2.3.2",
|
||||
"@tiptap/html": "2.3.2",
|
||||
"@tiptap/starter-kit": "2.3.2",
|
||||
"@tiptap/pm": "2.3.2",
|
||||
"@tiptap/suggestion": "2.3.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
ENV=${1:-prod}
|
||||
PROJECT=$2
|
||||
case $ENV in
|
||||
|
||||
15
scripts/migrate.sh
Normal file
15
scripts/migrate.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
source .env
|
||||
|
||||
export PGPASSWORD=$SUPABASE_DB_PASSWORD
|
||||
|
||||
psql \
|
||||
-h db.ltzepxnhhnrnvovqblfr.supabase.co \
|
||||
-p 5432 \
|
||||
-d postgres \
|
||||
-U postgres \
|
||||
-f backend/supabase/migration.sql
|
||||
@@ -41,6 +41,8 @@ export function CopyLinkOrShareButton(props: {
|
||||
setTimeout(() => setIsSuccess(false), 2000) // Reset after 2 seconds
|
||||
}
|
||||
|
||||
const content = isSuccess ? 'Copied!' : children
|
||||
|
||||
return (
|
||||
<ToolTipOrDiv
|
||||
hasChildren={!!children}
|
||||
@@ -72,7 +74,7 @@ export function CopyLinkOrShareButton(props: {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{content}
|
||||
</Button>
|
||||
</ToolTipOrDiv>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||
import { Button } from './button'
|
||||
import { Col } from '../layout/col'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import {firebaseLogin} from 'web/lib/firebase/users'
|
||||
import {Button} from './button'
|
||||
import {Col} from '../layout/col'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
|
||||
import {ButtonHTMLAttributes} from "react"
|
||||
import {FcGoogle} from "react-icons/fc"
|
||||
|
||||
export const SidebarSignUpButton = (props: { className?: string }) => {
|
||||
const { className } = props
|
||||
const {className} = props
|
||||
|
||||
return (
|
||||
<Col className={clsx('mt-4', className)}>
|
||||
@@ -43,3 +46,29 @@ export const GoogleSignInButton = (props: { onClick: () => any }) => {
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
type GoogleButtonProps = {
|
||||
onClick: () => void
|
||||
isLoading?: boolean
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>
|
||||
|
||||
export function GoogleButton({onClick, isLoading = false, ...props}: GoogleButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={isLoading}
|
||||
className={clsx(
|
||||
"w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300",
|
||||
"rounded-full shadow-sm text-sm font-medium",
|
||||
"hover:bg-canvas-25 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
|
||||
"disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<FcGoogle className="w-5 h-5"/>
|
||||
{isLoading ? "Loading..." : "Google"}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
13
web/components/filters/choices.tsx
Normal file
13
web/components/filters/choices.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export const RELATIONSHIP_CHOICES = {
|
||||
// Monogamous: 'mono',
|
||||
// Polyamorous: 'poly',
|
||||
// 'Open Relationship': 'open',
|
||||
// Other: 'other',
|
||||
Collaboration: 'collaboration',
|
||||
Friendship: 'friendship',
|
||||
Relationship: 'relationship',
|
||||
};
|
||||
|
||||
export const REVERTED_RELATIONSHIP_CHOICES = Object.fromEntries(
|
||||
Object.entries(RELATIONSHIP_CHOICES).map(([key, value]) => [value, key])
|
||||
);
|
||||
@@ -72,25 +72,25 @@ export function DesktopFilters(props: {
|
||||
popoverClassName="bg-canvas-50"
|
||||
/>
|
||||
{/* PREFERRED GENDER */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open: boolean) => (
|
||||
<DropdownButton
|
||||
content={
|
||||
<PrefGenderFilterText
|
||||
pref_gender={filters.pref_gender as Gender[]}
|
||||
highlightedClass={open ? 'text-primary-500' : undefined}
|
||||
/>
|
||||
}
|
||||
open={open}
|
||||
/>
|
||||
)}
|
||||
dropdownMenuContent={
|
||||
<Col>
|
||||
<PrefGenderFilter filters={filters} updateFilter={updateFilter} />
|
||||
</Col>
|
||||
}
|
||||
popoverClassName="bg-canvas-50"
|
||||
/>
|
||||
{/*<CustomizeableDropdown*/}
|
||||
{/* buttonContent={(open: boolean) => (*/}
|
||||
{/* <DropdownButton*/}
|
||||
{/* content={*/}
|
||||
{/* <PrefGenderFilterText*/}
|
||||
{/* pref_gender={filters.pref_gender as Gender[]}*/}
|
||||
{/* highlightedClass={open ? 'text-primary-500' : undefined}*/}
|
||||
{/* />*/}
|
||||
{/* }*/}
|
||||
{/* open={open}*/}
|
||||
{/* />*/}
|
||||
{/* )}*/}
|
||||
{/* dropdownMenuContent={*/}
|
||||
{/* <Col>*/}
|
||||
{/* <PrefGenderFilter filters={filters} updateFilter={updateFilter} />*/}
|
||||
{/* </Col>*/}
|
||||
{/* }*/}
|
||||
{/* popoverClassName="bg-canvas-50"*/}
|
||||
{/*/>*/}
|
||||
{/* AGE RANGE */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open: boolean) => (
|
||||
|
||||
@@ -45,9 +45,7 @@ export function GenderFilter(props: {
|
||||
choices={{
|
||||
Women: 'female',
|
||||
Men: 'male',
|
||||
'Non-binary': 'non-binary',
|
||||
'Trans women': 'trans-female',
|
||||
'Trans men': 'trans-male',
|
||||
'Other': 'other',
|
||||
}}
|
||||
onChange={(c) => {
|
||||
updateFilter({ genders: c })
|
||||
|
||||
@@ -86,22 +86,22 @@ export function MobileFilters(props: {
|
||||
<GenderFilter filters={filters} updateFilter={updateFilter} />
|
||||
</MobileFilterSection>
|
||||
{/* PREFERRED GENDER */}
|
||||
<MobileFilterSection
|
||||
title="Interested in"
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny(filters.pref_gender)}
|
||||
selection={
|
||||
<PrefGenderFilterText
|
||||
pref_gender={filters.pref_gender as Gender[]}
|
||||
highlightedClass={
|
||||
hasAny(filters.pref_gender) ? 'text-primary-600' : 'text-ink-400'
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PrefGenderFilter filters={filters} updateFilter={updateFilter} />
|
||||
</MobileFilterSection>
|
||||
{/*<MobileFilterSection*/}
|
||||
{/* title="Interested in"*/}
|
||||
{/* openFilter={openFilter}*/}
|
||||
{/* setOpenFilter={setOpenFilter}*/}
|
||||
{/* isActive={hasAny(filters.pref_gender)}*/}
|
||||
{/* selection={*/}
|
||||
{/* <PrefGenderFilterText*/}
|
||||
{/* pref_gender={filters.pref_gender as Gender[]}*/}
|
||||
{/* highlightedClass={*/}
|
||||
{/* hasAny(filters.pref_gender) ? 'text-primary-600' : 'text-ink-400'*/}
|
||||
{/* }*/}
|
||||
{/* />*/}
|
||||
{/* }*/}
|
||||
{/*>*/}
|
||||
{/* <PrefGenderFilter filters={filters} updateFilter={updateFilter} />*/}
|
||||
{/*</MobileFilterSection>*/}
|
||||
{/* AGE RANGE */}
|
||||
<MobileFilterSection
|
||||
title="Age"
|
||||
|
||||
@@ -54,9 +54,7 @@ export function PrefGenderFilter(props: {
|
||||
choices={{
|
||||
Women: 'female',
|
||||
Men: 'male',
|
||||
'Non-binary': 'non-binary',
|
||||
'Trans women': 'trans-female',
|
||||
'Trans men': 'trans-male',
|
||||
Other: 'other',
|
||||
}}
|
||||
onChange={(c) => {
|
||||
updateFilter({ pref_gender: c })
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
RelationshipType,
|
||||
convertRelationshipType,
|
||||
} from 'web/lib/util/convert-relationship-type'
|
||||
import {convertRelationshipType, RelationshipType,} from 'web/lib/util/convert-relationship-type'
|
||||
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
|
||||
import { FilterFields } from './search'
|
||||
import { MultiCheckbox } from 'web/components/multi-checkbox'
|
||||
import {FilterFields} from './search'
|
||||
import {MultiCheckbox} from 'web/components/multi-checkbox'
|
||||
|
||||
import {RELATIONSHIP_CHOICES} from "web/components/filters/choices";
|
||||
|
||||
export function RelationshipFilterText(props: {
|
||||
relationship: RelationshipType[] | undefined
|
||||
highlightedClass?: string
|
||||
}) {
|
||||
const { relationship, highlightedClass } = props
|
||||
const {relationship, highlightedClass} = props
|
||||
const relationshipLength = (relationship ?? []).length
|
||||
|
||||
if (!relationship || relationshipLength < 1) {
|
||||
return (
|
||||
<span className={clsx('text-semibold', highlightedClass)}>Any style</span>
|
||||
<span className={clsx('text-semibold', highlightedClass)}>Any connection</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,20 +48,13 @@ export function RelationshipFilter(props: {
|
||||
filters: Partial<FilterFields>
|
||||
updateFilter: (newState: Partial<FilterFields>) => void
|
||||
}) {
|
||||
const { filters, updateFilter } = props
|
||||
const {filters, updateFilter} = props
|
||||
return (
|
||||
<MultiCheckbox
|
||||
selected={filters.pref_relation_styles ?? []}
|
||||
choices={
|
||||
{
|
||||
Monogamous: 'mono',
|
||||
Polyamorous: 'poly',
|
||||
'Open Relationship': 'open',
|
||||
Other: 'other',
|
||||
} as any
|
||||
}
|
||||
choices={RELATIONSHIP_CHOICES as any}
|
||||
onChange={(c) => {
|
||||
updateFilter({ pref_relation_styles: c })
|
||||
updateFilter({pref_relation_styles: c})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ export const Search = (props: {
|
||||
<Row className={'mb-2 justify-between gap-2'}>
|
||||
<Input
|
||||
value={filters.name ?? ''}
|
||||
placeholder={'Search name'}
|
||||
placeholder={'Search anything...'}
|
||||
className={'w-full max-w-xs'}
|
||||
onChange={(e) => {
|
||||
updateFilter({ name: e.target.value })
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useEffect} from "react";
|
||||
import {Col} from "web/components/layout/col";
|
||||
import {Button} from "web/components/buttons/button";
|
||||
import {signupRedirect} from "web/lib/util/signup";
|
||||
import {SignUpButton} from "web/components/nav/love-sidebar";
|
||||
|
||||
export function AboutBox(props: {
|
||||
title: string
|
||||
@@ -48,18 +47,15 @@ export function LoggedOutHome() {
|
||||
return (
|
||||
<>
|
||||
<Col className="mb-4 gap-2 lg:hidden">
|
||||
<Button
|
||||
className="flex-1 fixed top-4 left-1/2 transform -translate-x-1/2 w-full"
|
||||
color="gradient"
|
||||
<SignUpButton
|
||||
className="mt-4 flex-1 fixed bottom-[55px] w-full left-0 right-0 z-10 mx-auto px-4"
|
||||
size="xl"
|
||||
onClick={signupRedirect}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
text="Sign up"
|
||||
/>
|
||||
{/*<SignUpAsMatchmaker className="flex-1"/>*/}
|
||||
</Col>
|
||||
<h1
|
||||
className="pt-12 pb-2 text-7xl md:text-8xl xs:text-6xl font-extrabold max-w-4xl leading-tight xl:whitespace-nowrap md:whitespace-nowrap text-center">
|
||||
className="pt-12 pb-2 text-7xl md:text-8xl xs:text-6xl font-extrabold leading-tight xl:whitespace-nowrap md:whitespace-nowrap text-center">
|
||||
Don't Swipe.<br/>
|
||||
<span id="typewriter"></span>
|
||||
<span id="cursor" className="animate-pulse">|</span>
|
||||
@@ -67,9 +63,9 @@ export function LoggedOutHome() {
|
||||
<div className="py-8"></div>
|
||||
<h3
|
||||
className="text-2xl font-bold text-center">
|
||||
A focused platform for real connections—built with purpose and speed.
|
||||
Find people who share your values, not just your photos.
|
||||
</h3>
|
||||
<div className="w-full bg-gray-50 dark:bg-gray-900 py-8 mt-20">
|
||||
<div className="w-full py-8 mt-20">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="grid md:grid-cols-3 gap-8 text-center">
|
||||
<AboutBox title="Radically Transparent" text="No algorithms. Every profile searchable."/>
|
||||
@@ -78,6 +74,7 @@ export function LoggedOutHome() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="block lg:hidden h-12"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function LovePage(props: {
|
||||
<Col
|
||||
className={clsx(
|
||||
'pb-[58px] lg:pb-0', // bottom bar padding
|
||||
'text-ink-1000 mx-auto min-h-screen w-full max-w-[1440px] lg:grid lg:grid-cols-12'
|
||||
'text-ink-1000 mx-auto min-h-screen w-full lg:grid lg:grid-cols-12'
|
||||
)}
|
||||
>
|
||||
<Toaster
|
||||
@@ -74,13 +74,13 @@ export function LovePage(props: {
|
||||
) : (
|
||||
<Sidebar
|
||||
navigationOptions={desktopSidebarOptions}
|
||||
className="sticky top-0 hidden self-start px-2 lg:col-span-2 lg:flex"
|
||||
/>
|
||||
className="sticky top-0 hidden self-start px-2 lg:col-span-2 lg:flex sidebar-nav bg-canvas-25"
|
||||
/>
|
||||
)}
|
||||
<main
|
||||
className={clsx(
|
||||
'flex flex-1 flex-col lg:mt-6 xl:px-2',
|
||||
'col-span-10',
|
||||
'col-span-8',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -97,17 +97,23 @@ export function LovePage(props: {
|
||||
)
|
||||
}
|
||||
|
||||
const Profiles = { name: 'Profiles', href: '/', icon: SolidHomeIcon };
|
||||
const ProfilesHome = { name: 'Profiles', href: '/', icon: HomeIcon };
|
||||
const faq = { name: 'FAQ', href: '/faq', icon: SolidQuestionIcon };
|
||||
const About = { name: 'About', href: '/about', icon: QuestionMarkCircleIcon };
|
||||
const Signin = { name: 'Sign in', href: '/signin', icon: UserCircleIcon };
|
||||
const Notifs = {name: 'Notifs', href: `/notifications`, icon: NotificationsIcon};
|
||||
const NotifsSolid = {name: 'Notifs', href: `/notifications`, icon: SolidNotificationsIcon};
|
||||
const Messages = {name: 'Messages', href: '/messages', icon: PrivateMessagesIcon};
|
||||
|
||||
function getBottomNavigation(user: User, lover: Lover | null | undefined) {
|
||||
return buildArray(
|
||||
{ name: 'Profiles', href: '/', icon: SolidHomeIcon },
|
||||
{
|
||||
name: 'Notifs',
|
||||
href: `/notifications`,
|
||||
icon: SolidNotificationsIcon,
|
||||
},
|
||||
Profiles,
|
||||
NotifsSolid,
|
||||
{
|
||||
name: 'Profile',
|
||||
href: lover === null ? '/signup' : `/${user.username}`,
|
||||
icon: SolidHomeIcon,
|
||||
},
|
||||
{
|
||||
name: 'Messages',
|
||||
@@ -120,36 +126,32 @@ function getBottomNavigation(user: User, lover: Lover | null | undefined) {
|
||||
}
|
||||
|
||||
const signedOutNavigation = () => [
|
||||
{ name: 'Profiles', href: '/', icon: SolidHomeIcon },
|
||||
{ name: 'About', href: '/about', icon: SolidQuestionIcon },
|
||||
{ name: 'Sign in', href: '/signin', icon: UserCircleIcon },
|
||||
Profiles,
|
||||
About,
|
||||
faq,
|
||||
Signin,
|
||||
]
|
||||
const getDesktopNav = (user: User | null | undefined) => {
|
||||
if (user)
|
||||
return buildArray(
|
||||
{ name: 'Profiles', href: '/', icon: HomeIcon },
|
||||
{
|
||||
name: 'Notifs',
|
||||
href: `/notifications`,
|
||||
icon: NotificationsIcon,
|
||||
},
|
||||
{
|
||||
name: 'Messages',
|
||||
href: '/messages',
|
||||
icon: PrivateMessagesIcon,
|
||||
},
|
||||
{ name: 'About', href: '/about', icon: QuestionMarkCircleIcon }
|
||||
ProfilesHome,
|
||||
Notifs,
|
||||
Messages,
|
||||
About,
|
||||
faq
|
||||
)
|
||||
|
||||
return buildArray(
|
||||
// { name: 'Profiles', href: '/', icon: HomeIcon },
|
||||
{ name: 'About', href: '/about', icon: QuestionMarkCircleIcon }
|
||||
About,
|
||||
faq
|
||||
)
|
||||
}
|
||||
|
||||
// No sidebar when signed out
|
||||
const getSidebarNavigation = (_toggleModal: () => void) => {
|
||||
return buildArray(
|
||||
{ name: 'About', href: '/about', icon: QuestionMarkCircleIcon }
|
||||
About,
|
||||
faq
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,13 +131,13 @@ function RelationshipType(props: { lover: Lover }) {
|
||||
const relationshipTypes = lover.pref_relation_styles
|
||||
const seekingGenderText = stringOrStringArrayToText({
|
||||
text: relationshipTypes.map((rel) =>
|
||||
convertRelationshipType(rel as RelationshipType)
|
||||
),
|
||||
convertRelationshipType(rel as RelationshipType).toLowerCase()
|
||||
).sort(),
|
||||
preText: 'Seeking',
|
||||
postText:
|
||||
relationshipTypes.length == 1 && relationshipTypes[0] == 'mono'
|
||||
? 'relationship'
|
||||
: 'relationships',
|
||||
// postText:
|
||||
// relationshipTypes.length == 1 && relationshipTypes[0] == 'mono'
|
||||
// ? 'relationship'
|
||||
// : 'relationships',
|
||||
asSentence: true,
|
||||
capitalizeFirstLetterOption: false,
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function ManifoldLoveLogo(props: {
|
||||
const inner = (
|
||||
<>
|
||||
<FavIcon className="dark:invert"/>
|
||||
<div className={clsx('my-auto text-xl font-thin')}>
|
||||
<div className={clsx('my-auto text-xl font-thin logo')}>
|
||||
{ENV == 'DEV' ? 'Compass dev' : 'Compass'}
|
||||
</div>
|
||||
</>
|
||||
|
||||
43
web/components/markdown.tsx
Normal file
43
web/components/markdown.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {LovePage} from "web/components/love-page";
|
||||
import {Col} from "web/components/layout/col";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
const MarkdownLink = ({href, children}: { href?: string; children: React.ReactNode }) => {
|
||||
if (!href) return <>{children}</>
|
||||
|
||||
// If href is internal, use Next.js Link
|
||||
if (href.startsWith('/')) {
|
||||
return <Link href={href}>{children}</Link>
|
||||
}
|
||||
|
||||
// For external links, fall back to <a>
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MarkdownPage({content, filename}: Props) {
|
||||
return (
|
||||
<LovePage trackPageView={filename} className={'col-span-8'}>
|
||||
<Col className="items-center">
|
||||
<Col className='w-full rounded px-3 py-4 sm:px-6 space-y-4 customlink'>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({node, children, ...props}) => <MarkdownLink {...props}>{children}</MarkdownLink>
|
||||
}}
|
||||
>{content}
|
||||
</ReactMarkdown>
|
||||
</Col>
|
||||
</Col>
|
||||
</LovePage>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function BottomNavBar(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="border-ink-200 dark:border-ink-300 text-ink-700 bg-canvas-0 fixed inset-x-0 bottom-0 z-50 flex select-none items-center justify-between border-t-2 text-xs lg:hidden">
|
||||
<nav className="border-ink-200 dark:border-ink-300 text-ink-700 bg-canvas-50 fixed inset-x-0 bottom-0 z-50 flex select-none items-center justify-between border-t-2 text-xs lg:hidden sidebar-nav">
|
||||
{navigationOptions.map((item) => (
|
||||
<NavBarItem
|
||||
key={item.name}
|
||||
@@ -149,8 +149,7 @@ function NavBarItem(props: {
|
||||
}
|
||||
|
||||
const currentBasePath = '/' + (currentPage?.split('/')[1] ?? '')
|
||||
const isCurrentPage =
|
||||
item.href != null && currentBasePath === item.href.split('?')[0]
|
||||
const isCurrentPage = currentBasePath === item.href.split('?')[0]
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -207,7 +206,7 @@ export function MobileSidebar(props: {
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<div className="bg-canvas-0 relative flex w-full max-w-xs flex-1 flex-col">
|
||||
<div className="bg-canvas-25 relative flex w-full max-w-[200px] flex-1 flex-col">
|
||||
<div className="mx-2 h-0 flex-1 overflow-y-auto">
|
||||
<Sidebar
|
||||
navigationOptions={sidebarNavigationOptions}
|
||||
|
||||
@@ -28,6 +28,8 @@ import {City, CityRow, loverToCity, useCitySearch} from "web/components/search-l
|
||||
import {AddPhotosWidget} from './widgets/add-photos'
|
||||
import {RadioToggleGroup} from "web/components/widgets/radio-toggle-group";
|
||||
import {MultipleChoiceOptions} from "common/love/multiple-choice";
|
||||
import {RELATIONSHIP_CHOICES} from "web/components/filters/choices";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const OptionalLoveUserForm = (props: {
|
||||
lover: LoverRow
|
||||
@@ -68,6 +70,10 @@ export const OptionalLoveUserForm = (props: {
|
||||
)
|
||||
if (error) {
|
||||
console.error(error)
|
||||
toast.error(
|
||||
`We ran into an issue saving your profile. Please try again or contact us if the issue persists.`
|
||||
)
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
if (!isEqual(newLinks, user.link)) {
|
||||
@@ -225,7 +231,7 @@ export const OptionalLoveUserForm = (props: {
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Aged between</label>
|
||||
<label className={clsx(labelClassName)}>Who are aged between</label>
|
||||
<Row className={'gap-2'}>
|
||||
<Col>
|
||||
<span>Min</span>
|
||||
@@ -262,6 +268,17 @@ export const OptionalLoveUserForm = (props: {
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Connection type</label>
|
||||
<MultiCheckbox
|
||||
choices={RELATIONSHIP_CHOICES}
|
||||
selected={lover['pref_relation_styles']}
|
||||
onChange={(selected) =>
|
||||
setLover('pref_relation_styles', selected)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName, 'pb-4')}>
|
||||
<label className={clsx(labelClassName)}>Socials</label>
|
||||
|
||||
@@ -522,7 +539,7 @@ export const OptionalLoveUserForm = (props: {
|
||||
{lookingRelationship && <>
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
You want to have kids
|
||||
I would like to have kids
|
||||
</label>
|
||||
<RadioToggleGroup
|
||||
className={'w-44'}
|
||||
@@ -533,22 +550,6 @@ export const OptionalLoveUserForm = (props: {
|
||||
currentChoice={lover.wants_kids_strength ?? -1}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Relationship style</label>
|
||||
<MultiCheckbox
|
||||
choices={{
|
||||
Monogamous: 'mono',
|
||||
Polyamorous: 'poly',
|
||||
'Open Relationship': 'open',
|
||||
Other: 'other',
|
||||
}}
|
||||
selected={lover['pref_relation_styles']}
|
||||
onChange={(selected) =>
|
||||
setLover('pref_relation_styles', selected)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</>}
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
|
||||
@@ -2,14 +2,16 @@ import {Lover} from 'common/love/lover'
|
||||
import {CompatibilityScore} from 'common/love/compatibility-score'
|
||||
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
import {LoadMoreUntilNotVisible} from 'web/components/widgets/visibility-observer'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {track} from 'web/lib/service/analytics'
|
||||
import {Col} from './layout/col'
|
||||
import clsx from 'clsx'
|
||||
import {JSONContent} from "@tiptap/core";
|
||||
import {Content} from "web/components/widgets/editor";
|
||||
import React from "react";
|
||||
import Router from "next/router";
|
||||
import Link from "next/link";
|
||||
import {Row} from "web/components/layout/row";
|
||||
import {CompatibleBadge} from "web/components/widgets/compatible-badge";
|
||||
import {useUser} from "web/hooks/use-user";
|
||||
|
||||
export const ProfileGrid = (props: {
|
||||
lovers: Lover[]
|
||||
@@ -30,6 +32,8 @@ export const ProfileGrid = (props: {
|
||||
refreshStars,
|
||||
} = props
|
||||
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
@@ -38,7 +42,9 @@ export const ProfileGrid = (props: {
|
||||
isReloading && 'animate-pulse opacity-80'
|
||||
)}
|
||||
>
|
||||
{lovers.map((lover) => (
|
||||
{lovers
|
||||
.filter((lover) => lover.user_id !== user?.id)
|
||||
.map((lover) => (
|
||||
<ProfilePreview
|
||||
key={lover.id}
|
||||
lover={lover}
|
||||
@@ -75,15 +81,13 @@ function ProfilePreview(props: {
|
||||
// const currentUser = useUser()
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
track('click love profile preview')
|
||||
Router.push(`/${user.username}`)
|
||||
}}
|
||||
className="cursor-pointer group block dark:bg-gray-800 rounded-lg overflow-hidden shadow hover:shadow-md transition-shadow duration-200 h-full"
|
||||
<Link
|
||||
onClick={() => track('click love profile preview')}
|
||||
href={`/${user.username}`}
|
||||
className="cursor-pointer group block bg-canvas-100 rounded-lg overflow-hidden shadow hover:shadow-md transition-shadow duration-200 h-full"
|
||||
>
|
||||
<Col
|
||||
className="relative h-40 w-full overflow-hidden rounded text-white transition-all hover:scale-y-105 hover:drop-shadow">
|
||||
className="relative h-40 w-full overflow-hidden rounded transition-all hover:scale-y-105 hover:drop-shadow">
|
||||
{/*{pinned_url ? (*/}
|
||||
{/* <Image*/}
|
||||
{/* src={pinned_url}*/}
|
||||
@@ -100,22 +104,23 @@ function ProfilePreview(props: {
|
||||
{/* </Col>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{/*<Row className="absolute inset-x-0 right-0 top-0 items-start justify-between bg-gradient-to-b from-black/70 via-black/70 to-transparent px-2 pb-3 pt-2">*/}
|
||||
{/* {currentUser ? (*/}
|
||||
{/* <StarButton*/}
|
||||
{/* className="!pt-0"*/}
|
||||
{/* isStarred={hasStar}*/}
|
||||
{/* refresh={refreshStars}*/}
|
||||
{/* targetLover={lover}*/}
|
||||
{/* hideTooltip*/}
|
||||
{/* />*/}
|
||||
{/* ) : (*/}
|
||||
{/* <div />*/}
|
||||
{/* )}*/}
|
||||
{/* {compatibilityScore && (*/}
|
||||
{/* <CompatibleBadge compatibility={compatibilityScore} />*/}
|
||||
{/* )}*/}
|
||||
{/*</Row>*/}
|
||||
<Row
|
||||
className="absolute top-2 right-2 items-start justify-end px-2 pb-3">
|
||||
{/* {currentUser ? (*/}
|
||||
{/* <StarButton*/}
|
||||
{/* className="!pt-0"*/}
|
||||
{/* isStarred={hasStar}*/}
|
||||
{/* refresh={refreshStars}*/}
|
||||
{/* targetLover={lover}*/}
|
||||
{/* hideTooltip*/}
|
||||
{/* />*/}
|
||||
{/* ) : (*/}
|
||||
{/* <div />*/}
|
||||
{/* )}*/}
|
||||
{compatibilityScore && (
|
||||
<CompatibleBadge compatibility={compatibilityScore}/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Col className="absolute inset-x-0 bottom-0 bg-gradient-to-t to-transparent px-4 pb-2 pt-6">
|
||||
<div>
|
||||
@@ -125,6 +130,7 @@ function ProfilePreview(props: {
|
||||
{user.name}
|
||||
</h3>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{/*TODO: fix nested <a> links warning (one from Link above, one from link in bio below)*/}
|
||||
<Content className="w-full line-clamp-4" content={lover.bio as JSONContent}/>
|
||||
</div>
|
||||
{/*{age}*/}
|
||||
@@ -135,6 +141,6 @@ function ProfilePreview(props: {
|
||||
{/*</Row>*/}
|
||||
</Col>
|
||||
</Col>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import { PencilIcon, EyeIcon, LockClosedIcon } from '@heroicons/react/outline'
|
||||
import { DotsHorizontalIcon } from '@heroicons/react/outline'
|
||||
import {DotsHorizontalIcon, EyeIcon, LockClosedIcon, PencilIcon} from '@heroicons/react/outline'
|
||||
import clsx from 'clsx'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { User } from 'common/user'
|
||||
import { Button } from 'web/components/buttons/button'
|
||||
import { MoreOptionsUserButton } from 'web/components/buttons/more-options-user-button'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { SendMessageButton } from 'web/components/messaging/send-message-button'
|
||||
import {User} from 'common/user'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {MoreOptionsUserButton} from 'web/components/buttons/more-options-user-button'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {SendMessageButton} from 'web/components/messaging/send-message-button'
|
||||
import LoverPrimaryInfo from './lover-primary-info'
|
||||
import { OnlineIcon } from '../online-icon'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import {OnlineIcon} from '../online-icon'
|
||||
import {track} from 'web/lib/service/analytics'
|
||||
import DropdownMenu from 'web/components/comments/dropdown-menu'
|
||||
import { ShareProfileButton } from '../widgets/share-profile-button'
|
||||
import { Lover } from 'common/love/lover'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { linkClass } from 'web/components/widgets/site-link'
|
||||
import { StarButton } from '../widgets/star-button'
|
||||
import { api, updateLover } from 'web/lib/api'
|
||||
import { useState } from 'react'
|
||||
import { VisibilityConfirmationModal } from './visibility-confirmation-modal'
|
||||
import {ShareProfileButton} from '../widgets/share-profile-button'
|
||||
import {Lover} from 'common/love/lover'
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {linkClass} from 'web/components/widgets/site-link'
|
||||
import {StarButton} from '../widgets/star-button'
|
||||
import {api, updateLover} from 'web/lib/api'
|
||||
import React, {useState} from 'react'
|
||||
import {VisibilityConfirmationModal} from './visibility-confirmation-modal'
|
||||
|
||||
export default function LoverProfileHeader(props: {
|
||||
user: User
|
||||
@@ -44,7 +43,7 @@ export default function LoverProfileHeader(props: {
|
||||
const isCurrentUser = currentUser?.id === user.id
|
||||
const [showVisibilityModal, setShowVisibilityModal] = useState(false)
|
||||
|
||||
console.log('LoverProfileHeader', { user, lover, currentUser })
|
||||
console.log('LoverProfileHeader', {user, lover, currentUser})
|
||||
|
||||
return (
|
||||
<Col className="w-full">
|
||||
@@ -52,7 +51,7 @@ export default function LoverProfileHeader(props: {
|
||||
<Row className="items-center gap-1">
|
||||
<Col className="gap-1">
|
||||
<Row className="items-center gap-1 text-xl">
|
||||
<OnlineIcon last_online_time={lover.last_online_time} />
|
||||
<OnlineIcon last_online_time={lover.last_online_time}/>
|
||||
<span>
|
||||
{simpleView ? (
|
||||
<Link className={linkClass} href={`/${user.username}`}>
|
||||
@@ -64,7 +63,7 @@ export default function LoverProfileHeader(props: {
|
||||
, {lover.age}
|
||||
</span>
|
||||
</Row>
|
||||
<LoverPrimaryInfo lover={lover} />
|
||||
<LoverPrimaryInfo lover={lover}/>
|
||||
</Col>
|
||||
</Row>
|
||||
{currentUser && isCurrentUser ? (
|
||||
@@ -81,13 +80,13 @@ export default function LoverProfileHeader(props: {
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
<PencilIcon className=" h-4 w-4" />
|
||||
<PencilIcon className=" h-4 w-4"/>
|
||||
</Button>
|
||||
|
||||
<DropdownMenu
|
||||
menuWidth={'w-52'}
|
||||
icon={
|
||||
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true"/>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
@@ -97,9 +96,9 @@ export default function LoverProfileHeader(props: {
|
||||
: 'Limit to Members Only',
|
||||
icon:
|
||||
lover.visibility === 'member' ? (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
<EyeIcon className="h-4 w-4"/>
|
||||
) : (
|
||||
<LockClosedIcon className="h-4 w-4" />
|
||||
<LockClosedIcon className="h-4 w-4"/>
|
||||
),
|
||||
onClick: () => setShowVisibilityModal(true),
|
||||
},
|
||||
@@ -112,7 +111,7 @@ export default function LoverProfileHeader(props: {
|
||||
)
|
||||
if (confirmed) {
|
||||
track('delete love profile')
|
||||
await api('me/delete', { username: user.username })
|
||||
await api('me/delete', {username: user.username})
|
||||
window.location.reload()
|
||||
}
|
||||
},
|
||||
@@ -122,8 +121,10 @@ export default function LoverProfileHeader(props: {
|
||||
</Row>
|
||||
) : (
|
||||
<Row className="items-center gap-1 sm:gap-2">
|
||||
{/*TODO: Add score to profile page once we can efficiently compute it (i.e., not recomputing it for every profile)*/}
|
||||
{/*<CompatibleBadge compatibility={compatibilityScore}/>*/}
|
||||
<ShareProfileButton
|
||||
className="hidden sm:flex"
|
||||
className="sm:flex"
|
||||
username={user.username}
|
||||
/>
|
||||
{currentUser && (
|
||||
@@ -134,16 +135,16 @@ export default function LoverProfileHeader(props: {
|
||||
/>
|
||||
)}
|
||||
{currentUser && showMessageButton && (
|
||||
<SendMessageButton toUser={user} currentUser={currentUser} />
|
||||
<SendMessageButton toUser={user} currentUser={currentUser}/>
|
||||
)}
|
||||
<MoreOptionsUserButton user={user} />
|
||||
<MoreOptionsUserButton user={user}/>
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Row className="justify-end sm:hidden">
|
||||
<ShareProfileButton username={user.username} />
|
||||
</Row>
|
||||
{/*<Row className="justify-end sm:hidden">*/}
|
||||
{/* <ShareProfileButton username={user.username} />*/}
|
||||
{/*</Row>*/}
|
||||
|
||||
<VisibilityConfirmationModal
|
||||
open={showVisibilityModal}
|
||||
@@ -152,7 +153,7 @@ export default function LoverProfileHeader(props: {
|
||||
onConfirm={async () => {
|
||||
const newVisibility =
|
||||
lover.visibility === 'member' ? 'public' : 'member'
|
||||
await updateLover({ visibility: newVisibility })
|
||||
await updateLover({visibility: newVisibility})
|
||||
refreshLover()
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,9 @@ import { LikeData, ShipData } from 'common/api/love-types'
|
||||
import { useAPIGetter } from 'web/hooks/use-api-getter'
|
||||
import { useGetter } from 'web/hooks/use-getter'
|
||||
import { getStars } from 'web/lib/supabase/stars'
|
||||
import {Content} from "web/components/widgets/editor";
|
||||
import {JSONContent} from "@tiptap/core";
|
||||
import React from "react";
|
||||
|
||||
export function LoverProfile(props: {
|
||||
lover: Lover
|
||||
@@ -63,6 +66,8 @@ export function LoverProfile(props: {
|
||||
|
||||
const showMessageButton = liked || likedBack || !areCompatible
|
||||
|
||||
const isProfileVisible = currentUser || lover.visibility === 'public'
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoverProfileHeader
|
||||
@@ -74,7 +79,7 @@ export function LoverProfile(props: {
|
||||
showMessageButton={showMessageButton}
|
||||
refreshLover={refreshLover}
|
||||
/>
|
||||
{currentUser || lover.visibility === 'public' ? (
|
||||
{isProfileVisible ? (
|
||||
<LoverContent
|
||||
user={user}
|
||||
lover={lover}
|
||||
@@ -88,6 +93,9 @@ export function LoverProfile(props: {
|
||||
/>
|
||||
) : (
|
||||
<Col className="bg-canvas-0 w-full gap-4 rounded p-4">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<Content className="w-full line-clamp-6" content={lover.bio as JSONContent}/>
|
||||
</div>
|
||||
<Col className="relative gap-4">
|
||||
<div className="bg-ink-200 dark:bg-ink-400 h-4 w-2/5" />
|
||||
<div className="bg-ink-200 dark:bg-ink-400 h-4 w-3/5" />
|
||||
@@ -102,7 +110,7 @@ export function LoverProfile(props: {
|
||||
{areCompatible &&
|
||||
((!fromLoverPage && !isCurrentUser) ||
|
||||
(fromLoverPage && fromLoverPage.user_id === currentUser?.id)) && (
|
||||
<Row className="sticky bottom-[70px] right-0 mr-1 self-end lg:bottom-6">
|
||||
<Row className="right-0 mr-1 self-end lg:bottom-6">
|
||||
<LikeButton targetLover={lover} liked={liked} refresh={refresh} />
|
||||
</Row>
|
||||
)}
|
||||
@@ -118,7 +126,7 @@ export function LoverProfile(props: {
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{lover.photo_urls && <ProfileCarousel lover={lover} />}
|
||||
{isProfileVisible && lover.photo_urls && <ProfileCarousel lover={lover} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ export function ProfilesHome() {
|
||||
if (!user) return;
|
||||
setIsReloading(true);
|
||||
const current = ++id.current;
|
||||
api('get-lovers', removeNullOrUndefinedProps({limit: 20, compatibleWithUserId: user?.id, ...filters}) as any)
|
||||
api('get-lovers', removeNullOrUndefinedProps({
|
||||
limit: 20,
|
||||
compatibleWithUserId: user?.id,
|
||||
...filters
|
||||
}) as any)
|
||||
.then(({lovers}) => {
|
||||
if (current === id.current) setLovers(lovers);
|
||||
})
|
||||
@@ -74,7 +78,8 @@ export function ProfilesHome() {
|
||||
const result = await api('get-lovers', removeNullOrUndefinedProps({
|
||||
limit: 20,
|
||||
compatibleWithUserId: user?.id,
|
||||
after: lastLover?.id.toString(), ...filters
|
||||
after: lastLover?.id.toString(),
|
||||
...filters
|
||||
}) as any);
|
||||
if (result.lovers.length === 0) return false;
|
||||
setLovers((prev) => (prev ? [...prev, ...result.lovers] : result.lovers));
|
||||
|
||||
@@ -71,7 +71,7 @@ export function SelectUsers(props: {
|
||||
id="user name"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="e.g. Ian Philips"
|
||||
placeholder="Search users..."
|
||||
/>
|
||||
</Col>
|
||||
{queryReady && (
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
TbBrandInstagram,
|
||||
TbBrandLinkedin,
|
||||
TbBrandMastodon,
|
||||
// TbBrandOnlyfans,
|
||||
TbBrandPatreon,
|
||||
TbBrandPaypal,
|
||||
TbBrandSpotify,
|
||||
TbBrandX,
|
||||
} from 'react-icons/tb'
|
||||
import Foldy from 'web/public/manifold-logo.svg'
|
||||
|
||||
export const PLATFORM_ICONS: {
|
||||
[key in Site]: (props: { className?: string }) => ReactNode
|
||||
@@ -25,15 +25,21 @@ export const PLATFORM_ICONS: {
|
||||
bluesky: TbBrandBluesky,
|
||||
mastodon: TbBrandMastodon,
|
||||
substack: LuBookmark,
|
||||
// onlyfans: TbBrandOnlyfans,
|
||||
instagram: TbBrandInstagram,
|
||||
github: TbBrandGithub,
|
||||
linkedin: TbBrandLinkedin,
|
||||
facebook: TbBrandFacebook,
|
||||
spotify: TbBrandSpotify,
|
||||
patreon: TbBrandPatreon,
|
||||
paypal: TbBrandPaypal,
|
||||
}
|
||||
|
||||
export const SocialIcon = (props: { site: string; className?: string }) => {
|
||||
export const SocialIcon = (props: {
|
||||
site: string;
|
||||
className?: string;
|
||||
size?: number;
|
||||
color?: string;
|
||||
}) => {
|
||||
const { site, ...rest } = props
|
||||
const Icon = PLATFORM_ICONS[site as Site] || PLATFORM_ICONS.site
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export const LikeButton = (props: {
|
||||
<div className="p-2 pb-0 pt-0">{liked ? <>Liked!</> : <>Like</>}</div>
|
||||
</Col>
|
||||
</button>
|
||||
<LikeConfimationDialog
|
||||
<LikeConfirmationDialog
|
||||
targetLover={targetLover}
|
||||
hasFreeLike={hasFreeLike}
|
||||
submit={like}
|
||||
@@ -86,7 +86,7 @@ export const LikeButton = (props: {
|
||||
)
|
||||
}
|
||||
|
||||
const LikeConfimationDialog = (props: {
|
||||
const LikeConfirmationDialog = (props: {
|
||||
targetLover: Lover
|
||||
hasFreeLike: boolean
|
||||
open: boolean
|
||||
|
||||
@@ -19,6 +19,7 @@ export function useNearbyCities(
|
||||
cityId: referenceCityId,
|
||||
radius,
|
||||
}).then((result) => {
|
||||
console.log('search-near-city', result)
|
||||
if (thisSearchCount == searchCount.current) {
|
||||
if (result.status === 'failure') {
|
||||
setNearbyCities(null)
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
export type RelationshipType = 'mono' | 'poly' | 'open' | 'other'
|
||||
import {REVERTED_RELATIONSHIP_CHOICES} from "web/components/filters/choices";
|
||||
|
||||
export type RelationshipType = keyof typeof REVERTED_RELATIONSHIP_CHOICES
|
||||
|
||||
export function convertRelationshipType(relationshipType: RelationshipType) {
|
||||
if (relationshipType == 'mono') {
|
||||
return 'monogamous'
|
||||
}
|
||||
if (relationshipType == 'poly') {
|
||||
return 'polyamorous'
|
||||
}
|
||||
return relationshipType
|
||||
return REVERTED_RELATIONSHIP_CHOICES[relationshipType]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "next dev -p 3000",
|
||||
|
||||
@@ -117,7 +117,7 @@ function UserPageInner(props: ActiveUserPageProps) {
|
||||
const {lover: clientLover, refreshLover} = useLoverByUser(user)
|
||||
// Show previous profile while loading another one
|
||||
const lover = clientLover ?? staticLover
|
||||
console.log('lover:', user?.username, lover, clientLover, staticLover)
|
||||
// console.log('lover:', user?.username, lover, clientLover, staticLover)
|
||||
|
||||
return (
|
||||
<LovePage
|
||||
|
||||
@@ -100,11 +100,15 @@ function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) {
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1,maximum-scale=1, user-scalable=no"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<PostHogProvider client={posthog}>
|
||||
<div
|
||||
className={clsx(
|
||||
'font-figtree contents font-normal',
|
||||
'contents font-normal',
|
||||
logoFont.variable,
|
||||
mainFont.variable
|
||||
)}
|
||||
@@ -118,7 +122,7 @@ function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) {
|
||||
</div>
|
||||
</div>
|
||||
</PostHogProvider>
|
||||
{/* LOVE TODO: Reenable one tap setup */}
|
||||
{/* TODO: Reenable one tap setup */}
|
||||
{/* <GoogleOneTapSetup /> */}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function Document() {
|
||||
<link rel="icon" href={ENV_CONFIG.faviconPath} />
|
||||
<Script src="/init-theme.js" strategy="beforeInteractive" />
|
||||
</Head>
|
||||
<body className="bg-canvas-50 text-ink-1000">
|
||||
<body className="body-bg text-ink-1000">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const AboutBlock = (props: {
|
||||
const {text, title} = props
|
||||
return <section className="mb-12">
|
||||
<h2 className="text-3xl font-bold mb-4">{title}</h2>
|
||||
<p className="text-lg text-gray-500">{text}</p>
|
||||
<p className="text-lg">{text}</p>
|
||||
</section>;
|
||||
}
|
||||
|
||||
@@ -59,8 +59,7 @@ export default function About() {
|
||||
|
||||
<AboutBlock
|
||||
title="Democratic"
|
||||
text={<span>Governed by the community, while ensuring no drift through our <Link href="/constitution"
|
||||
className="text-blue-600 dark:text-blue-400">constitution</Link>.</span>}
|
||||
text={<span className="customlink">Governed by the community, while ensuring no drift through our <Link href="/constitution">constitution</Link>.</span>}
|
||||
/>
|
||||
|
||||
<AboutBlock
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import {LovePage} from "web/components/love-page";
|
||||
import {Col} from "web/components/layout/col";
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import MarkdownPage from "web/components/markdown";
|
||||
|
||||
const FILENAME = 'constitution';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const filePath = path.join(process.cwd(), 'public', 'md', 'constitution.md');
|
||||
const filePath = path.join(process.cwd(), 'public', 'md', FILENAME + '.md');
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return { props: { content } };
|
||||
return {props: {content}};
|
||||
}
|
||||
|
||||
type Props = { content: string };
|
||||
|
||||
export default function Constitution({ content }: Props) {
|
||||
return (
|
||||
<LovePage trackPageView={'test'}>
|
||||
<Col className="items-center">
|
||||
<Col className={'bg-canvas-0 w-full rounded px-3 py-4 sm:px-6 space-y-4'}>
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
</Col>
|
||||
</Col>
|
||||
</LovePage>
|
||||
);
|
||||
export default function Faq({content}: Props) {
|
||||
return <MarkdownPage content={content} filename={FILENAME}></MarkdownPage>
|
||||
}
|
||||
|
||||
19
web/pages/faq.tsx
Normal file
19
web/pages/faq.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import MarkdownPage from "web/components/markdown";
|
||||
|
||||
const FILENAME = 'faq';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const filePath = path.join(process.cwd(), 'public', 'md', FILENAME + '.md');
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return {props: {content}};
|
||||
}
|
||||
|
||||
type Props = { content: string };
|
||||
|
||||
export default function Faq({content}: Props) {
|
||||
return <MarkdownPage content={content} filename={FILENAME}></MarkdownPage>
|
||||
}
|
||||
19
web/pages/financials.tsx
Normal file
19
web/pages/financials.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import MarkdownPage from "web/components/markdown";
|
||||
|
||||
const FILENAME = 'financials';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const filePath = path.join(process.cwd(), 'public', 'md', FILENAME + '.md');
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return {props: {content}};
|
||||
}
|
||||
|
||||
type Props = { content: string };
|
||||
|
||||
export default function Faq({content}: Props) {
|
||||
return <MarkdownPage content={content} filename={FILENAME}></MarkdownPage>
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default function ProfilesPage() {
|
||||
return (
|
||||
<LovePage trackPageView={'user profiles'}>
|
||||
<Col className="items-center">
|
||||
<Col className={'bg-canvas-0 w-full rounded px-3 py-4 sm:px-6'}>
|
||||
<Col className={'w-full rounded px-3 py-4 sm:px-6'}>
|
||||
{user ? <ProfilesHome/> : <LoggedOutHome/>}
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
29
web/pages/md/[filename].tsx
Normal file
29
web/pages/md/[filename].tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import MarkdownPage from "web/components/markdown";
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
filename: string;
|
||||
};
|
||||
|
||||
export default function Markdown({content, filename}: Props) {
|
||||
return <MarkdownPage content={content} filename={filename}></MarkdownPage>
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const mdDir = path.join(process.cwd(), 'public', 'md');
|
||||
const files = fs.readdirSync(mdDir);
|
||||
const paths = files.map((file) => ({
|
||||
params: {filename: file.replace(/\.md$/, '')},
|
||||
}));
|
||||
|
||||
return {paths, fallback: false};
|
||||
}
|
||||
|
||||
export async function getStaticProps({params}: { params: { filename: string } }) {
|
||||
const filePath = path.join(process.cwd(), 'public', 'md', `${params.filename}.md`);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return {props: {content, filename: params.filename}};
|
||||
}
|
||||
19
web/pages/members.tsx
Normal file
19
web/pages/members.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import MarkdownPage from "web/components/markdown";
|
||||
|
||||
const FILENAME = 'members';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const filePath = path.join(process.cwd(), 'public', 'md', FILENAME + '.md');
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return {props: {content}};
|
||||
}
|
||||
|
||||
type Props = { content: string };
|
||||
|
||||
export default function Faq({content}: Props) {
|
||||
return <MarkdownPage content={content} filename={FILENAME}></MarkdownPage>
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export default function PrivacyPage() {
|
||||
return (
|
||||
<LovePage
|
||||
trackPageView={'terms'}
|
||||
className="max-w-4xl mx-auto p-8"
|
||||
className="max-w-4xl mx-auto p-8 col-span-8 bg-canvas-0"
|
||||
>
|
||||
<h1 className="text-3xl font-semibold text-center mb-6">Privacy Policy</h1>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {getLoverRow} from "common/love/lover";
|
||||
import {db} from "web/lib/supabase/db";
|
||||
import Router from "next/router";
|
||||
import {useUser} from "web/hooks/use-user";
|
||||
import {GoogleButton} from "web/components/buttons/sign-up-button";
|
||||
|
||||
|
||||
export default function RegisterPage() {
|
||||
@@ -109,7 +110,7 @@ function RegisterComponent() {
|
||||
|
||||
return (
|
||||
<LovePage trackPageView={'register'}>
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
{registrationSuccess ? (
|
||||
<div className="text-center">
|
||||
@@ -164,7 +165,7 @@ function RegisterComponent() {
|
||||
<div className="flex justify-center mb-6">
|
||||
<FavIcon className="dark:invert"/>
|
||||
</div>
|
||||
<h2 className="text-center text-3xl font-extrabold ">
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold ">
|
||||
Get Started
|
||||
</h2>
|
||||
</div>
|
||||
@@ -179,7 +180,7 @@ function RegisterComponent() {
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="bg-canvas-50 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-t-md border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Email"
|
||||
/>
|
||||
</div>
|
||||
@@ -192,20 +193,20 @@ function RegisterComponent() {
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="bg-canvas-50 bg-input appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs mt-2 text-center">
|
||||
<p className="text-sm mt-2 text-center customlink">
|
||||
By signing up, I agree to the{" "}
|
||||
<Link href="/terms" className="underline hover:text-blue-600">
|
||||
<Link href="/terms">
|
||||
Terms and Conditions
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="/privacy" className="underline hover:text-blue-600">
|
||||
<Link href="/privacy">
|
||||
Privacy Policy
|
||||
</Link>.
|
||||
</p>
|
||||
@@ -215,7 +216,7 @@ function RegisterComponent() {
|
||||
<div className="text-red-500 text-sm text-center">{error}</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
@@ -229,25 +230,17 @@ function RegisterComponent() {
|
||||
<div className="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500">Or sign up with</span>
|
||||
<span className="px-2 body-bg text-gray-500">Or sign up with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={signupThenMaybeRedirectToSignup}
|
||||
disabled={isLoading}
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium hover: focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FcGoogle className="w-5 h-5"/>
|
||||
Google
|
||||
</button>
|
||||
<GoogleButton onClick={signupThenMaybeRedirectToSignup} isLoading={isLoading} />
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm mt-2">
|
||||
<div className="my-8" />
|
||||
<div className="text-center customlink">
|
||||
<p className="">
|
||||
Already have an account?{' '}
|
||||
<Link href="/signin" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
<Link href="/signin">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {useSearchParams} from "next/navigation";
|
||||
import {Suspense, useEffect, useState} from "react";
|
||||
import Link from "next/link";
|
||||
import {FcGoogle} from "react-icons/fc";
|
||||
import {auth, firebaseLogin} from "web/lib/firebase/users";
|
||||
import FavIcon from "web/public/FavIcon";
|
||||
|
||||
@@ -13,6 +12,7 @@ import {db} from "web/lib/supabase/db";
|
||||
import Router from "next/router";
|
||||
import {LovePage} from "web/components/love-page";
|
||||
import {useUser} from "web/hooks/use-user";
|
||||
import {GoogleButton} from "web/components/buttons/sign-up-button";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
@@ -64,6 +64,10 @@ function RegisterComponent() {
|
||||
setError(null);
|
||||
try {
|
||||
const creds = await firebaseLogin();
|
||||
if (creds){
|
||||
setIsLoading(true)
|
||||
setIsLoadingGoogle(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error signing in:", error);
|
||||
const message = 'Failed to sign in with Google';
|
||||
@@ -134,7 +138,7 @@ function RegisterComponent() {
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="bg-canvas-50 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-t-md border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Email"
|
||||
/>
|
||||
</div>
|
||||
@@ -147,7 +151,7 @@ function RegisterComponent() {
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="bg-canvas-50 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-b-md border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
@@ -171,25 +175,19 @@ function RegisterComponent() {
|
||||
<div className="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500">Or continue with</span>
|
||||
<span className="px-2 body-bg text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignIn}
|
||||
disabled={isLoading}
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium hover: focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FcGoogle className="w-5 h-5"/>
|
||||
Google
|
||||
</button>
|
||||
<GoogleButton onClick={handleGoogleSignIn} isLoading={isLoading}/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center">
|
||||
<Link href="/register" className="text-blue-600 hover:underline">
|
||||
No account? Register.
|
||||
</Link>
|
||||
<div className="text-center customlink">
|
||||
<p className="">
|
||||
No account?{' '}
|
||||
<Link href="/register">
|
||||
Register
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function TermsPage() {
|
||||
return (
|
||||
<LovePage
|
||||
trackPageView={'terms'}
|
||||
className="max-w-4xl mx-auto p-8 text-gray-800 dark:text-white"
|
||||
className="max-w-4xl mx-auto p-8 text-gray-800 dark:text-white col-span-8 bg-canvas-0"
|
||||
>
|
||||
<h1 className="text-3xl font-semibold text-center mb-6">Terms & Conditions</h1>
|
||||
|
||||
|
||||
@@ -27,12 +27,13 @@ We, the community of Compass, commit to building and maintaining this project in
|
||||
- Sustained inactivity (e.g., less than 10 hours of contribution for 12 months).
|
||||
- Proven bad-faith conduct (vote manipulation, harassment, sabotage). Removal requires a **2/3 vote** of Members.
|
||||
|
||||
[Current members and administrators](/members)
|
||||
|
||||
## Article III: Governance Structure
|
||||
|
||||
### Section 1: Interim Governance
|
||||
- Until the community reaches **5 voting members**, governance decisions shall be made by **unanimous agreement** of the *Founding Maintainers*.
|
||||
- The Founding Maintainers may appoint temporary coordinators for specific tasks.
|
||||
- Until the community reaches **5 voting members**, governance decisions shall be made by the *Founding Maintainer*.
|
||||
- The Founding Maintainer may appoint temporary coordinators for specific tasks.
|
||||
- Once the community reaches **5 voting members**, leadership positions will be filled via community election as described below.
|
||||
|
||||
### Section 2: Democratic Governance
|
||||
@@ -81,4 +82,4 @@ If the project is dissolved, the platform will be shut down and made unavailable
|
||||
|
||||
|
||||
*Adopted on: August 11, 2025*
|
||||
*Founding Maintainers: Martin, Emily*
|
||||
*Founding Maintainer: Martin Braquet*
|
||||
96
web/public/md/faq.md
Normal file
96
web/public/md/faq.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# FAQ – Compass
|
||||
|
||||
### What is Compass?
|
||||
|
||||
Compass is a **free, open-source platform to help people form deep, meaningful, and lasting connections** — whether platonic, romantic, or collaborative.
|
||||
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.
|
||||
|
||||
### Who is Compass for?
|
||||
|
||||
Anyone who wants more than small talk or casual networking. If you value **depth over quantity** and want relationships grounded in **shared values, trust, and understanding**, Compass is for you.
|
||||
|
||||
### Why is Compass different from other meeting apps?
|
||||
|
||||
* **Keyword Search**: Find people who share your niche interests (e.g., “Minimalism”, “Thinking, Fast and Slow”, “Indie film”).
|
||||
* **Transparent Database**: See all profiles, apply filters, and search freely — no hidden algorithms.
|
||||
* **Notification System**: Get alerts when new people match your searches — no endless scrolling required.
|
||||
* **Personality-Centered**: Values and ideas first. Photos stay secondary.
|
||||
* **Democratic & Open Source**: Built by the community, for the community — no ads, no hidden monetization.
|
||||
|
||||
### Is Compass for dating or friendship?
|
||||
|
||||
Both. 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. Compass is his 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).
|
||||
|
||||
### 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 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 from the community**. Options include:
|
||||
|
||||
* Patreon
|
||||
* PayPal
|
||||
* GitHub Sponsors
|
||||
|
||||
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**.
|
||||
* Messaging may move toward **end-to-end encryption** in future versions.
|
||||
|
||||
### 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.
|
||||
|
||||
The [full implementation](https://github.com/CompassMeet/Compass/blob/main/common/src/love/compatibility-score.ts) is **open source** and open to review, feedback, and improvement by the community.
|
||||
|
||||
|
||||
|
||||
### How can I help?
|
||||
|
||||
* **Give Feedback**: [Fill out the suggestion form](https://forms.gle/tKnXUMAbEreMK6FC6)
|
||||
* **Join the Discussion**: [Discord Community](https://discord.gg/8Vd7jzqjun)
|
||||
* **Contribute to Development**: [View the code on GitHub](https://github.com/CompassMeet/Compass)
|
||||
* **Donate**: [Support infrastructure](/about)
|
||||
* **Spread the Word**: Tell friends and family who value depth and real connection.
|
||||
|
||||
### What’s next?
|
||||
|
||||
Compass has officially **launched**. The platform is now open for everyone who values meaningful, values-driven connections. Our focus has shifted toward **growing the community** and **securing donations** to sustain and expand the platform.
|
||||
|
||||
Every action, whether sharing, donating, or contributing, directly helps Compass remain **ad-free, subscription-free, and community-owned**.
|
||||
20
web/public/md/financials.md
Normal file
20
web/public/md/financials.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Financials
|
||||
|
||||
See [this spreadsheet](https://docs.google.com/spreadsheets/d/18GJr-xSi_ypkgQIxfwPTMaKgQsfLLTjrZBtYd-TeGbc/edit?usp=sharing) for the most updated information.
|
||||
|
||||
### Expenses
|
||||
|
||||
- Hosting & Infrastructure: $100
|
||||
- Marketing: $0
|
||||
- Miscellaneous: $0
|
||||
|
||||
### Funding Sources
|
||||
|
||||
- Donations: $0
|
||||
- Grants: $0
|
||||
|
||||
### Financial Summary
|
||||
|
||||
- Total Income: $0
|
||||
- Total Expenses: $100
|
||||
- Net Surplus: -$100
|
||||
11
web/public/md/members.md
Normal file
11
web/public/md/members.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Current Members and Administrators
|
||||
|
||||
See the [full constitution](/constitution) for details on membership criteria and governance structure.
|
||||
|
||||
### Members
|
||||
|
||||
- Martin Braquet
|
||||
|
||||
### Administrators
|
||||
|
||||
- Martin Braquet
|
||||
@@ -2,153 +2,168 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.logo {
|
||||
font-family: "Crimson Pro", Georgia, "Times New Roman", Times, serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
:root {
|
||||
/* Text / Ink Grey Scale */
|
||||
--color-ink-0: 255 255 255; /* white */
|
||||
--color-ink-50: 245 245 245;
|
||||
--color-ink-100: 230 230 230;
|
||||
--color-ink-200: 200 200 200;
|
||||
--color-ink-300: 170 170 170;
|
||||
--color-ink-400: 140 140 140;
|
||||
--color-ink-500: 110 110 110;
|
||||
--color-ink-600: 85 85 85;
|
||||
--color-ink-700: 60 60 60;
|
||||
--color-ink-800: 40 40 40;
|
||||
--color-ink-900: 25 25 25;
|
||||
--color-ink-950: 12 12 12;
|
||||
--color-ink-1000: 0 0 0; /* black */
|
||||
body {
|
||||
font-family: "Crimson Pro", Georgia, "Times New Roman", Times, serif;
|
||||
}
|
||||
|
||||
/* Background / Canvas Grey Scale */
|
||||
--color-canvas-0: 255 255 255; /* white */
|
||||
--color-canvas-50: 245 245 245;
|
||||
--color-canvas-100: 230 230 230;
|
||||
--color-canvas-200: 210 210 210;
|
||||
--color-canvas-300: 190 190 190;
|
||||
--color-canvas-400: 160 160 160;
|
||||
--color-canvas-500: 130 130 130;
|
||||
--color-canvas-600: 100 100 100;
|
||||
--color-canvas-700: 75 75 75;
|
||||
--color-canvas-800: 50 50 50;
|
||||
--color-canvas-900: 30 30 30;
|
||||
--color-canvas-950: 15 15 15;
|
||||
--color-canvas-1000: 0 0 0; /* black */
|
||||
button, input, label {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
}
|
||||
|
||||
--color-primary-950: 23 37 84; /* very dark navy */
|
||||
--color-primary-900: 30 58 138;
|
||||
--color-primary-800: 30 64 175;
|
||||
--color-primary-700: 29 78 216;
|
||||
--color-primary-600: 37 99 235;
|
||||
--color-primary-500: 59 130 246; /* standard blue */
|
||||
--color-primary-400: 96 165 250;
|
||||
--color-primary-300: 147 197 253;
|
||||
--color-primary-200: 191 219 254;
|
||||
--color-primary-100: 219 234 254;
|
||||
--color-primary-50: 239 246 255; /* very light blue */
|
||||
html {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
--color-yes-0: 255 255 255;
|
||||
--color-yes-50: 249 249 249;
|
||||
--color-yes-100: 242 242 242;
|
||||
--color-yes-200: 217 217 217;
|
||||
--color-yes-300: 191 191 191;
|
||||
--color-yes-400: 166 166 166;
|
||||
--color-yes-500: 140 140 140;
|
||||
--color-yes-600: 115 115 115;
|
||||
--color-yes-700: 89 89 89;
|
||||
--color-yes-800: 64 64 64;
|
||||
--color-yes-900: 34 34 34;
|
||||
--color-yes-950: 17 17 17;
|
||||
--color-yes-1000: 0 0 0;
|
||||
:root {
|
||||
/* Text / Ink Grey Scale */
|
||||
--color-ink-0: 255 255 255; /* white */
|
||||
--color-ink-50: 245 245 245;
|
||||
--color-ink-100: 230 230 230;
|
||||
--color-ink-200: 200 200 200;
|
||||
--color-ink-300: 170 170 170;
|
||||
--color-ink-400: 140 140 140;
|
||||
--color-ink-500: 110 110 110;
|
||||
--color-ink-600: 85 85 85;
|
||||
--color-ink-700: 60 60 60;
|
||||
--color-ink-800: 40 40 40;
|
||||
--color-ink-900: 25 25 25;
|
||||
--color-ink-950: 12 12 12;
|
||||
--color-ink-1000: 0 0 0; /* black */
|
||||
|
||||
--color-no-0: 255 255 255;
|
||||
--color-no-50: 249 249 249;
|
||||
--color-no-100: 242 242 242;
|
||||
--color-no-200: 217 217 217;
|
||||
--color-no-300: 191 191 191;
|
||||
--color-no-400: 166 166 166;
|
||||
--color-no-500: 140 140 140;
|
||||
--color-no-600: 115 115 115;
|
||||
--color-no-700: 89 89 89;
|
||||
--color-no-800: 64 64 64;
|
||||
--color-no-900: 34 34 34;
|
||||
--color-no-950: 17 17 17;
|
||||
--color-no-1000: 0 0 0;
|
||||
/* Background / Canvas Grey Scale */
|
||||
--color-canvas-0: 255 255 255; /* white */
|
||||
--color-canvas-25: 245 245 245;
|
||||
--color-canvas-50: 245 245 245;
|
||||
--color-canvas-100: 245 245 245;
|
||||
--color-canvas-200: 210 210 210;
|
||||
--color-canvas-300: 190 190 190;
|
||||
--color-canvas-400: 160 160 160;
|
||||
--color-canvas-500: 130 130 130;
|
||||
--color-canvas-600: 100 100 100;
|
||||
--color-canvas-700: 75 75 75;
|
||||
--color-canvas-800: 50 50 50;
|
||||
--color-canvas-900: 30 30 30;
|
||||
--color-canvas-950: 15 15 15;
|
||||
--color-canvas-1000: 0 0 0; /* black */
|
||||
|
||||
}
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
--color-primary-950: 23 37 84; /* very dark navy */
|
||||
--color-primary-900: 30 58 138;
|
||||
--color-primary-800: 30 64 175;
|
||||
--color-primary-700: 29 78 216;
|
||||
--color-primary-600: 37 99 235;
|
||||
--color-primary-500: 59 130 246; /* standard blue */
|
||||
--color-primary-400: 96 165 250;
|
||||
--color-primary-300: 147 197 253;
|
||||
--color-primary-200: 191 219 254;
|
||||
--color-primary-100: 219 234 254;
|
||||
--color-primary-50: 239 246 255; /* very light blue */
|
||||
|
||||
--color-ink-1000: 255 255 255; /* white */
|
||||
--color-ink-950: 250 250 250; /* #FAFAFA */
|
||||
--color-ink-900: 242 242 242; /* #F2F2F2 */
|
||||
--color-ink-800: 217 217 217; /* #D9D9D9 */
|
||||
--color-ink-700: 191 191 191; /* #BFBFBF */
|
||||
--color-ink-600: 166 166 166; /* #A6A6A6 */
|
||||
--color-ink-500: 140 140 140; /* #8C8C8C */
|
||||
--color-ink-400: 115 115 115; /* #737373 */
|
||||
--color-ink-300: 89 89 89; /* #595959 */
|
||||
--color-ink-200: 64 64 64; /* #404040 */
|
||||
--color-ink-100: 34 34 34; /* #222222 */
|
||||
--color-ink-50: 17 17 17; /* #111111 */
|
||||
--color-ink-0: 0 0 0; /* black */
|
||||
--color-yes-0: 255 255 255;
|
||||
--color-yes-50: 249 249 249;
|
||||
--color-yes-100: 242 242 242;
|
||||
--color-yes-200: 217 217 217;
|
||||
--color-yes-300: 191 191 191;
|
||||
--color-yes-400: 166 166 166;
|
||||
--color-yes-500: 140 140 140;
|
||||
--color-yes-600: 115 115 115;
|
||||
--color-yes-700: 89 89 89;
|
||||
--color-yes-800: 64 64 64;
|
||||
--color-yes-900: 34 34 34;
|
||||
--color-yes-950: 17 17 17;
|
||||
--color-yes-1000: 0 0 0;
|
||||
|
||||
--color-canvas-0: 32 30 37;
|
||||
--color-canvas-50: 22 20 25;
|
||||
--color-canvas-100: 46 41 51;
|
||||
--color-no-0: 255 255 255;
|
||||
--color-no-50: 249 249 249;
|
||||
--color-no-100: 242 242 242;
|
||||
--color-no-200: 217 217 217;
|
||||
--color-no-300: 191 191 191;
|
||||
--color-no-400: 166 166 166;
|
||||
--color-no-500: 140 140 140;
|
||||
--color-no-600: 115 115 115;
|
||||
--color-no-700: 89 89 89;
|
||||
--color-no-800: 64 64 64;
|
||||
--color-no-900: 34 34 34;
|
||||
--color-no-950: 17 17 17;
|
||||
--color-no-1000: 0 0 0;
|
||||
|
||||
--color-primary-950: 239 246 255; /* very light blue */
|
||||
--color-primary-900: 219 234 254; /* light blue */
|
||||
--color-primary-800: 191 219 254;
|
||||
--color-primary-700: 147 197 253;
|
||||
--color-primary-600: 96 165 250;
|
||||
--color-primary-500: 59 130 246; /* standard blue */
|
||||
--color-primary-400: 37 99 235;
|
||||
--color-primary-300: 29 78 216;
|
||||
--color-primary-200: 30 64 175;
|
||||
--color-primary-100: 30 58 138;
|
||||
--color-primary-50: 23 37 84; /* very dark navy */
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-ink-1000: 255 255 255; /* white */
|
||||
--color-ink-950: 250 250 250; /* #FAFAFA */
|
||||
--color-ink-900: 242 242 242; /* #F2F2F2 */
|
||||
--color-ink-800: 217 217 217; /* #D9D9D9 */
|
||||
--color-ink-700: 191 191 191; /* #BFBFBF */
|
||||
--color-ink-600: 166 166 166; /* #A6A6A6 */
|
||||
--color-ink-500: 140 140 140; /* #8C8C8C */
|
||||
--color-ink-400: 115 115 115; /* #737373 */
|
||||
--color-ink-300: 89 89 89; /* #595959 */
|
||||
--color-ink-200: 64 64 64; /* #404040 */
|
||||
--color-ink-100: 34 34 34; /* #222222 */
|
||||
--color-ink-50: 17 17 17; /* #111111 */
|
||||
--color-ink-0: 0 0 0; /* black */
|
||||
|
||||
--color-canvas-25: 0 0 0;
|
||||
--color-canvas-0: 20 20 20;
|
||||
--color-canvas-50: 20 20 20;
|
||||
--color-canvas-100: 40 40 40;
|
||||
|
||||
--color-primary-950: 239 246 255; /* very light blue */
|
||||
--color-primary-900: 219 234 254; /* light blue */
|
||||
--color-primary-800: 191 219 254;
|
||||
--color-primary-700: 147 197 253;
|
||||
--color-primary-600: 96 165 250;
|
||||
--color-primary-500: 59 130 246; /* standard blue */
|
||||
--color-primary-400: 37 99 235;
|
||||
--color-primary-300: 29 78 216;
|
||||
--color-primary-200: 30 64 175;
|
||||
--color-primary-100: 30 58 138;
|
||||
--color-primary-50: 23 37 84; /* very dark navy */
|
||||
|
||||
|
||||
--color-no-950: 255 255 255; /* white */
|
||||
--color-no-900: 242 242 242;
|
||||
--color-no-800: 217 217 217;
|
||||
--color-no-700: 191 191 191;
|
||||
--color-no-600: 166 166 166;
|
||||
--color-no-500: 140 140 140;
|
||||
--color-no-400: 115 115 115;
|
||||
--color-no-300: 89 89 89;
|
||||
--color-no-200: 64 64 64;
|
||||
--color-no-100: 34 34 34;
|
||||
--color-no-50: 17 17 17;
|
||||
--color-no-0: 0 0 0; /* black */
|
||||
--color-no-950: 255 255 255; /* white */
|
||||
--color-no-900: 242 242 242;
|
||||
--color-no-800: 217 217 217;
|
||||
--color-no-700: 191 191 191;
|
||||
--color-no-600: 166 166 166;
|
||||
--color-no-500: 140 140 140;
|
||||
--color-no-400: 115 115 115;
|
||||
--color-no-300: 89 89 89;
|
||||
--color-no-200: 64 64 64;
|
||||
--color-no-100: 34 34 34;
|
||||
--color-no-50: 17 17 17;
|
||||
--color-no-0: 0 0 0; /* black */
|
||||
|
||||
|
||||
--color-yes-950: 255 255 255; /* white */
|
||||
--color-yes-900: 242 242 242;
|
||||
--color-yes-800: 217 217 217;
|
||||
--color-yes-700: 191 191 191;
|
||||
--color-yes-600: 166 166 166;
|
||||
--color-yes-500: 140 140 140;
|
||||
--color-yes-400: 115 115 115;
|
||||
--color-yes-300: 89 89 89;
|
||||
--color-yes-200: 64 64 64;
|
||||
--color-yes-100: 34 34 34;
|
||||
--color-yes-50: 17 17 17;
|
||||
--color-yes-0: 0 0 0; /* black */
|
||||
--color-yes-950: 255 255 255; /* white */
|
||||
--color-yes-900: 242 242 242;
|
||||
--color-yes-800: 217 217 217;
|
||||
--color-yes-700: 191 191 191;
|
||||
--color-yes-600: 166 166 166;
|
||||
--color-yes-500: 140 140 140;
|
||||
--color-yes-400: 115 115 115;
|
||||
--color-yes-300: 89 89 89;
|
||||
--color-yes-200: 64 64 64;
|
||||
--color-yes-100: 34 34 34;
|
||||
--color-yes-50: 17 17 17;
|
||||
--color-yes-0: 0 0 0; /* black */
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'emoji';
|
||||
src: local('AppleColorEmoji') local('Segoe UI Emoji'),
|
||||
local('Noto Color Emoji');
|
||||
/* from official unicode range for emoji: https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D%0D%0A&abb=on&esc=on&g=&i= */
|
||||
/* but include zero width joiner and variant block selector, like 🏳️🌈 */
|
||||
unicode-range: U+200D, U+FE0?, U+203C, U+2049, U+2122, U+2139, U+2194-2199,
|
||||
font-family: 'emoji';
|
||||
src: local('AppleColorEmoji'), local('Segoe UI Emoji'), local('Noto Color Emoji');
|
||||
/* from official unicode range for emoji: https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D%0D%0A&abb=on&esc=on&g=&i= */
|
||||
/* but include zero width joiner and variant block selector, like 🏳️🌈 */
|
||||
unicode-range: U+200D, U+FE0?, U+203C, U+2049, U+2122, U+2139, U+2194-2199,
|
||||
U+21A9, U+21AA, U+231A, U+231B, U+2328, U+23CF, U+23E9-23F3, U+23F8-23FA,
|
||||
U+24C2, U+25AA, U+25AB, U+25B6, U+25C0, U+25FB-25FE, U+2600-2604, U+260E,
|
||||
U+2611, U+2614, U+2615, U+2618, U+261D, U+2620, U+2622, U+2623, U+2626,
|
||||
@@ -176,57 +191,56 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('../public/fonts/icomoon.eot?v49ui9#iefix')
|
||||
format('embedded-opentype'),
|
||||
font-family: 'icomoon';
|
||||
src: url('../public/fonts/icomoon.eot?v49ui9#iefix') format('embedded-opentype'),
|
||||
url('../public/fonts/icomoon.ttf?v49ui9') format('truetype'),
|
||||
url('../public/fonts/icomoon.woff?v49ui9') format('woff'),
|
||||
url('../public/fonts/icomoon.svg?v49ui9#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+1E40;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
unicode-range: U+1E40;
|
||||
}
|
||||
|
||||
[class^='icon-'],
|
||||
[class*=' icon-'] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-mana_3:before {
|
||||
content: '\1e40';
|
||||
content: '\1e40';
|
||||
}
|
||||
|
||||
/* For Webkit-inkd browsers (Chrome, Safari and Opera) */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* For IE, Edge and Firefox */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
text {
|
||||
font-family: icomoon, var(--font-main), emoji, sans-serif;
|
||||
font-family: icomoon, var(--font-main), emoji, sans-serif;
|
||||
}
|
||||
|
||||
/* Style all headings globally */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Inter', sans-serif; /* Clean modern font */
|
||||
font-weight: 600; /* Semi-bold for clarity */
|
||||
/*font-family: 'Inter', sans-serif; !* Clean modern font *!*/
|
||||
font-weight: 600; /* Semi-bold for clarity */
|
||||
/*color: #111827; !* Near-black text for readability *!*/
|
||||
line-height: 1.25;
|
||||
margin-top: 1.5rem;
|
||||
@@ -235,27 +249,53 @@ h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
/* Size scaling */
|
||||
h1 {
|
||||
font-size: 2rem; /* ~32px */
|
||||
font-size: 2rem; /* ~32px */
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem; /* ~24px */
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem; /* ~20px */
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.125rem; /* ~18px */
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem; /* ~16px */
|
||||
font-size: 1rem; /* ~16px */
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.875rem; /* ~14px */
|
||||
color: #374151; /* Slightly lighter for subheadings */
|
||||
color: #374151; /* Slightly lighter for subheadings */
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
padding-left: 1.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.customlink a {
|
||||
color: rgb(var(--color-primary-500));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dark .customlink a {
|
||||
color: rgb(var(--color-primary-700));
|
||||
}
|
||||
|
||||
.customlink a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
font-family: "Inter", sans-serif; /* clean and legible */
|
||||
}
|
||||
|
||||
.body-bg {
|
||||
background-color: rgb(var(--color-canvas-0));
|
||||
}
|
||||
@@ -310,6 +310,7 @@ module.exports = {
|
||||
},
|
||||
canvas: {
|
||||
0: 'rgb(var(--color-canvas-0) / <alpha-value>)',
|
||||
25: 'rgb(var(--color-canvas-25) / <alpha-value>)',
|
||||
50: 'rgb(var(--color-canvas-50) / <alpha-value>)',
|
||||
100: 'rgb(var(--color-canvas-100) / <alpha-value>)',
|
||||
},
|
||||
@@ -328,17 +329,17 @@ module.exports = {
|
||||
950: 'rgb(var(--color-primary-950) / <alpha-value>)',
|
||||
},
|
||||
gray: {
|
||||
50: 'hsl(240, 40%, 98%)',
|
||||
100: 'hsl(235, 46%, 95%)',
|
||||
200: 'hsl(236, 33%, 90%)',
|
||||
300: 'hsl(238, 26%, 81%)',
|
||||
400: 'hsl(238, 19%, 68%)',
|
||||
500: 'hsl(240, 12%, 52%)',
|
||||
600: 'hsl(240, 16%, 40%)',
|
||||
700: 'hsl(240, 20%, 30%)',
|
||||
800: 'hsl(240, 30%, 22%)',
|
||||
900: 'hsl(242, 45%, 15%)',
|
||||
950: 'hsl(243, 69%, 10%)',
|
||||
50: 'hsl(0, 0%, 95%)',
|
||||
100: 'hsl(0, 0%, 90%)',
|
||||
200: 'hsl(0, 0%, 80%)',
|
||||
300: 'hsl(0, 0%, 70%)',
|
||||
400: 'hsl(0, 0%, 60%)',
|
||||
500: 'hsl(0, 0%, 50%)',
|
||||
600: 'hsl(0, 0%, 40%)',
|
||||
700: 'hsl(0, 0%, 30%)',
|
||||
800: 'hsl(0, 0%, 20%)',
|
||||
900: 'hsl(0,0%,10%)',
|
||||
950: 'hsl(0,0%,5%)',
|
||||
},
|
||||
warning: '#F0D630',
|
||||
error: '#E70D3D',
|
||||
|
||||
7186
web/yarn.lock
7186
web/yarn.lock
File diff suppressed because it is too large
Load Diff
108
yarn.lock
108
yarn.lock
@@ -28,6 +28,15 @@
|
||||
"@babel/highlight" "^7.24.2"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/code-frame@^7.10.4":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
|
||||
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4":
|
||||
version "7.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a"
|
||||
@@ -230,6 +239,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62"
|
||||
integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
|
||||
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
||||
|
||||
"@babel/helper-validator-option@^7.23.5":
|
||||
version "7.23.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
|
||||
@@ -3029,6 +3043,20 @@
|
||||
lodash.merge "^4.6.2"
|
||||
postcss-selector-parser "6.0.10"
|
||||
|
||||
"@testing-library/dom@^10.0.0":
|
||||
version "10.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95"
|
||||
integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/aria-query" "^5.0.1"
|
||||
aria-query "5.3.0"
|
||||
dom-accessibility-api "^0.5.9"
|
||||
lz-string "^1.5.0"
|
||||
picocolors "1.1.1"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/jest-dom@^6.6.4":
|
||||
version "6.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz#697db9424f0d21d8216f1958fa0b1b69b5f43923"
|
||||
@@ -3288,6 +3316,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||
|
||||
"@types/aria-query@^5.0.1":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
|
||||
integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
|
||||
|
||||
"@types/babel__core@^7.1.14":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
|
||||
@@ -3786,11 +3819,6 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@19.0.4":
|
||||
version "19.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89"
|
||||
integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==
|
||||
|
||||
"@types/react@*", "@types/react@18.3.5":
|
||||
version "18.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f"
|
||||
@@ -3799,13 +3827,6 @@
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@19.0.10":
|
||||
version "19.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
|
||||
integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/request@^2.48.8":
|
||||
version "2.48.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30"
|
||||
@@ -4193,18 +4214,18 @@ aria-hidden@^1.1.3:
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
aria-query@^5.0.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
|
||||
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
|
||||
|
||||
aria-query@^5.3.0:
|
||||
aria-query@5.3.0, aria-query@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
|
||||
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
aria-query@^5.0.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
|
||||
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
|
||||
|
||||
array-buffer-byte-length@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f"
|
||||
@@ -5417,6 +5438,11 @@ doctrine@^3.0.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||
|
||||
dom-accessibility-api@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
|
||||
@@ -8149,6 +8175,11 @@ lru-memoizer@^2.2.0:
|
||||
lodash.clonedeep "^4.5.0"
|
||||
lru-cache "6.0.0"
|
||||
|
||||
lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||
|
||||
make-dir@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
|
||||
@@ -9265,16 +9296,16 @@ pgpass@1.x:
|
||||
dependencies:
|
||||
split2 "^4.1.0"
|
||||
|
||||
picocolors@1.1.1, picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
@@ -9471,6 +9502,15 @@ prettier@3.4.2:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
|
||||
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
|
||||
|
||||
pretty-format@^27.0.2:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
pretty-format@^29.0.0, pretty-format@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
|
||||
@@ -9860,13 +9900,6 @@ react-dom@18.2.0:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-dom@19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57"
|
||||
integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
|
||||
dependencies:
|
||||
scheduler "^0.25.0"
|
||||
|
||||
react-email@3.0.7:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-email/-/react-email-3.0.7.tgz#8b52684f157c5d5e6200bc201590827aaf9dc9ec"
|
||||
@@ -9923,6 +9956,11 @@ react-is@^16.13.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-is@^18.0.0:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
@@ -9979,11 +10017,6 @@ react@18.2.0:
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
react@19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd"
|
||||
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
|
||||
|
||||
read-cache@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
|
||||
@@ -10320,11 +10353,6 @@ scheduler@^0.23.0:
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
scheduler@^0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015"
|
||||
integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
|
||||
|
||||
selderee@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a"
|
||||
|
||||
Reference in New Issue
Block a user