mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-25 01:51:37 -04:00
6.1 KiB
6.1 KiB
trigger, description, globs
| trigger | description | globs |
|---|---|---|
| always_on |
Project Structure
Compass (compassmeet.com) is a transparent dating platform for forming deep, authentic 1-on-1 connections.
- Next.js React frontend
/web- Pages, components, hooks, lib
- Express Node API server
/backend/api - Shared backend utilities
/backend/shared - Email functions
/backend/email - Database schema
/backend/supabase- Supabase-generated types in
/backend/supabase/schema.ts
- Supabase-generated types in
- Files shared between frontend and backend
/common- Types (User, Profile, etc.) and utilities
- Try not to add package dependencies to common
- Android app
/android
Deployment
- Both dev and prod environments
- Backend on GCP (Google Cloud Platform)
- Frontend on Vercel
- Database on Supabase (PostgreSQL)
- Firebase for authentication and storage
Code Guidelines
Component Example
import clsx from 'clsx'
import Link from 'next/link'
import {User} from 'common/user'
import {ProfileRow} from 'common/profiles/profile'
import {useUser} from 'web/hooks/use-user'
import {useT} from 'web/lib/locale'
interface ProfileCardProps {
user: User
profile: ProfileRow
}
export function ProfileCard({user, profile}: ProfileCardProps) {
const t = useT()
return (
<div className={clsx('bg-canvas-50 rounded-lg p-4')}>
<img src={user.avatarUrl} alt={user.name} />
<h3>{user.name}</h3>
<p>{profile.bio}</p>
</div>
)
}
We prefer many smaller components that each represent one logical unit, rather than one large component.
Export the main component at the top of the file. Name the component the same as the file (e.g., profile-card.tsx →
ProfileCard).
API Calls
Server-side (getStaticProps):
import {api} from 'web/lib/api'
export async function getStaticProps() {
const profiles = await api('get-profiles', {})
return {
props: {profiles},
revalidate: 30 * 60, // 30 minutes
}
}
Client-side - use hooks:
import {useAPIGetter} from 'web/hooks/use-api-getter'
function ProfileList() {
const {data, refresh} = useAPIGetter('get-profiles', {})
if (!data) return <CompassLoadingIndicator / >
return (
<div>
{
data.profiles.map((profile) => (
<ProfileCard key = {profile.id} user = {profile.user} profile = {profile}
/>
))
}
<button onClick = {refresh} > Refresh < /button>
< /div>
)
}
Database Access
Backend (pg-promise):
import {createSupabaseDirectClient} from 'shared/supabase/init'
const pg = createSupabaseDirectClient()
const user = await pg.oneOrNone<User>('SELECT * FROM users WHERE username = $1', [username])
Frontend (Supabase client):
import {db} from 'web/lib/supabase/db'
const {data} = await db.from('profiles').select('*').eq('user_id', userId)
Translation
import {useT} from 'web/lib/locale'
function MyComponent() {
const t = useT()
return <h1>{t('welcome', 'Welcome to Compass')}</h1>
}
Translation files are in common/messages/ (en.json, fr.json, de.json).
Backend Endpoints
- Define schema in
common/src/api/schema.ts:
'get-user-and-profile': {
method: 'GET',
authed: false,
rateLimited: true,
props: z.object({
username: z.string().min(1),
}),
returns: {} as {user: User; profile: ProfileRow | null},
summary: 'Get user and profile data by username',
tag: 'Users',
},
- Create handler in
backend/api/src/:
import {APIError, APIHandler} from './helpers/endpoint'
export const getUserAndProfile: APIHandler<'get-user-and-profile'> = async ({username}, _auth) => {
const user = await getUserByUsername(username)
if (!user) {
throw new APIError(404, 'User not found')
}
return {user, profile}
}
- Register in
backend/api/src/app.ts:
import {getUserAndProfile} from './get-user-and-profile'
const handlers = {
'get-user-and-profile': getUserAndProfile,
// ...
}
Profile Options (Interests, Causes, Work)
Options are stored in separate tables with many-to-many relationships:
interests,causes,work- option valuesprofile_interests,profile_causes,profile_work- junction tables
Fetch in parallel:
const [interestsRes, causesRes, workRes] = await Promise.all([
db.from('profile_interests').select('interests(name, id)').eq('profile_id', profile.id),
db.from('profile_causes').select('causes(name, id)').eq('profile_id', profile.id),
db.from('profile_work').select('work(name, id)').eq('profile_id', profile.id),
])
Testing
Running Tests
# Jest (unit + integration)
yarn test
# Playwright (E2E)
yarn test:e2e
Test Structure
- Unit tests:
*.unit.test.tsintests/unit/ - Integration tests:
*.integration.test.tsintests/integration/ - E2E tests:
*.e2e.spec.tsintests/e2e/
Mocking Example
jest.mock('shared/supabase/init')
import {createSupabaseDirectClient} from 'shared/supabase/init'
const mockPg = {
oneOrNone: jest.fn(),
tx: jest.fn(async (cb) => cb(mockTx)),
}
;(createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg)
Important Patterns
User Registration
- Create user + profile + options in single database transaction
- Return full profile data from creation API
- Don't use sleep() hacks - rely on transactional integrity
API Errors
import {APIError} from './helpers/endpoint'
throw new APIError(404, 'User not found')
throw new APIError(400, 'Invalid input', {field: 'email'})
Logging
- Use
debug()fromcommon/loggerfor development - Use
logfromshared/utilsfor production
Things to Avoid
- Don't use string concatenation for SQL queries
- Don't add sleep() delays for "eventual consistency"
- Don't create separate API calls when data can be batched in one transaction
- Don't use console.log - use
debug()orlog()
Key Dependencies
- Node.js 20+
- React 19
- Next.js 16
- Supabase (PostgreSQL)
- Firebase (Auth, Storage)
- Tailwind CSS
- Jest (testing)
- Playwright (E2E testing)