Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
939083e
replaces geist and styled-components with tailwind
michaelwschultz Dec 28, 2025
035c688
increase max-width of pages
michaelwschultz Dec 28, 2025
551c440
improve app structure, replace old chart, move to firebase-lite
michaelwschultz Dec 29, 2025
b15c1da
remove unused deps, replace twitter url with bluesky, fix auth
michaelwschultz Dec 29, 2025
15441e5
adds build:prod script to generate correct firebase rules
michaelwschultz Dec 29, 2025
9f44cb5
sort imports
michaelwschultz Dec 29, 2025
0a1aaa8
rename infusion to treatment
michaelwschultz Dec 29, 2025
3d93c9a
rename factor to medication on table
michaelwschultz Dec 29, 2025
3e099d6
adds firebase-tools back to dev deps
michaelwschultz Dec 29, 2025
5205d4f
fix formatting
michaelwschultz Dec 29, 2025
869f1f4
fix effect deps
michaelwschultz Dec 29, 2025
2ecc2c4
try to fix ci workflow
michaelwschultz Dec 29, 2025
5696b94
fix hydration issues and treatmet dialog
michaelwschultz Jan 10, 2026
9f048bc
update cypress tests
michaelwschultz Jan 10, 2026
b145f2c
add vscode settings with biome defaults
michaelwschultz Jan 10, 2026
062fab7
add cursor rules
michaelwschultz Jan 11, 2026
b2a1849
remove d.ts files from biome
michaelwschultz Jan 11, 2026
807da62
update vscode settings to autoformat on save
michaelwschultz Jan 11, 2026
acc9023
replace modal with silk component
michaelwschultz Jan 11, 2026
34f1d3c
fixes
michaelwschultz Jan 25, 2026
956cdf3
fix biome
michaelwschultz Jan 25, 2026
647e05e
fix lint errors
michaelwschultz Jan 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Hemolog - Cursor Rules

Hemophilia medication tracking app. Next.js 16 + TypeScript + Firebase + TanStack Query.

## Tech Stack
Next.js 16.1.1 (App Router) | TypeScript (strict) | Tailwind 4.0 | TanStack Query v5 | Formik | Firebase Auth + Firestore (lite SDK) | @silk-hq/components | @tabler/icons-react | Recharts | react-hot-toast | Biome | pnpm 10.28 | Node 22.21

## Structure
```
src/
├── app/ # Pages + API routes (src/app/api/)
├── components/ # shared/, home/, emergency/
└── lib/
├── db/ # Client Firestore ops (firestore-lite)
├── admin-db/ # Server Firestore ops (Firebase Admin)
├── hooks/ # React Query hooks
├── types/ # TypeScript types
├── firebase.ts # Client auth (getAuth())
├── firebase-admin.ts # Server admin (adminFirestore, auth)
├── firestore-lite.ts # DB helpers
└── auth.tsx # useAuth() hook
```

## Code Style
- Imports: `@/` prefix for `src/`
- Formatting: 2 spaces, single quotes, semicolons asNeeded, ES5 trailing commas
- Naming: Components=PascalCase, hooks=use*, functions=camelCase, types=PascalCase

## Key Patterns

### Client Component with SSR Safety
```tsx
'use client'
import { useState, useEffect } from 'react'
import { useAuth } from '@/lib/auth'

export default function Component({ userId }: { userId: string }) {
const [mounted, setMounted] = useState(false)
const { user } = useAuth()
useEffect(() => { setMounted(true) }, [])
if (!mounted) return null
return <div>...</div>
}
```

### Firestore Query (Client)
```tsx
import { getDocuments, where } from '@/lib/firestore-lite'
const treatments = await getDocuments<TreatmentType>(
'infusions',
where('user.uid', '==', userUid),
where('deletedAt', '==', null) // Soft delete filter
)
```

### Database Layer (src/lib/db/*.ts)
```tsx
import { createDocument, getDocuments, softDeleteDocument, where } from '@/lib/firestore-lite'

export interface TreatmentType {
uid?: string; date: string; medication: Medication; user: AttachedUserType; deletedAt: string | null
}

export const createTreatment = (data: TreatmentType) => createDocument('infusions', data)
export const fetchTreatments = (userUid: string) => getDocuments<TreatmentType>(
'infusions', where('user.uid', '==', userUid), where('deletedAt', '==', null)
)
```

### Query Hook (src/lib/hooks/use*Query.ts)
```tsx
import { useQuery } from '@tanstack/react-query'
export const treatmentKeys = {
all: ['treatments'] as const,
list: (uid: string) => [...treatmentKeys.all, 'list', uid] as const,
}
export function useTreatmentsQuery(userUid: string) {
return useQuery({
queryKey: treatmentKeys.list(userUid),
queryFn: () => fetchTreatments(userUid),
enabled: !!userUid,
staleTime: 10_000,
})
}
```

### Mutation Hook with Optimistic Updates (src/lib/hooks/use*Mutations.ts)
```tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'

export function useTreatmentMutations() {
const queryClient = useQueryClient()
const createMutation = useMutation({
mutationFn: createTreatment,
onMutate: async (newTreatment) => {
await queryClient.cancelQueries({ queryKey: treatmentKeys.all })
const previous = queryClient.getQueryData(treatmentKeys.list(newTreatment.user.uid))
queryClient.setQueryData(treatmentKeys.list(newTreatment.user.uid), (old) =>
old ? [{ ...newTreatment, uid: `temp-${Date.now()}` }, ...old] : [newTreatment]
)
return { previous, userUid: newTreatment.user.uid }
},
onError: (err, _, ctx) => {
if (ctx?.previous) queryClient.setQueryData(treatmentKeys.list(ctx.userUid), ctx.previous)
toast.error(`Failed: ${err.message}`)
},
onSuccess: () => toast.success('Treatment logged!'),
onSettled: (_, __, vars) => queryClient.invalidateQueries({ queryKey: treatmentKeys.list(vars.user.uid) }),
})
return { createTreatment: createMutation.mutate, isCreating: createMutation.isPending }
}
```

### API Route (src/app/api/*/route.ts)
```tsx
import type { NextRequest } from 'next/server'
import { getAllTreatmentsByApiKey } from '@/lib/admin-db/treatments'

export async function GET(request: NextRequest) {
try {
const apikey = new URL(request.url).searchParams.get('apikey')
if (!apikey) throw { message: 'Missing api key' }
const { treatments, error } = await getAllTreatmentsByApiKey(apikey)
if (error) throw error
return Response.json(treatments)
} catch (error: unknown) {
const msg = error && typeof error === 'object' && 'message' in error ? String(error.message) : 'Error'
return Response.json({ error: msg }, { status: 500 })
}
}
```

## Data Models

**Collections**: `users`, `infusions` (treatments), `feedback`

**Types** (src/lib/types/):
- `UserType`: Full user (uid, email, name, alertId, apiKey, medication, etc.)
- `Person`: Public user data
- `AttachedUserType`: User reference in documents
- `TreatmentType`: Treatment record with `deletedAt` for soft delete
- `TreatmentTypeEnum`: PROPHY | BLEED | PREVENTATIVE | ANTIBODY

## Firestore Lite Functions
`getDocument<T>(coll, id)` | `getDocuments<T>(coll, ...constraints)` | `createDocument<T>(coll, data)` | `setDocument<T>(coll, id, data, merge?)` | `updateDocument<T>(coll, id, data)` | `softDeleteDocument(coll, id)` | `deleteDocument(coll, id)`

Re-exports: `where`, `limit`, `orderBy`

## Tailwind Theme (src/app/globals.css)
Primary: `primary-500` (#ff062c) | Success: `success-500` (#48bb78) | Warning: `warning-500` (#0070f3)

## Scripts
```bash
pnpm dev # Dev server
pnpm firebase # Emulators (Auth:9099, Firestore:8082, UI:8081)
pnpm lint:fix # Biome fix
pnpm build # Production build
pnpm seed # Seed test data
```

## Environment
```
NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY, NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, NEXT_PUBLIC_FIREBASE_PROJECT_ID
FIREBASE_PRIVATE_KEY, FIREBASE_CLIENT_EMAIL
NEXT_PUBLIC_USE_EMULATORS=true # Dev mode
```

## Critical Rules
1. Use `firebase/firestore/lite` (not full SDK) - smaller bundle
2. All mutations need optimistic updates + rollback
3. Soft delete via `deletedAt` field, filter with `where('deletedAt', '==', null)`
4. Client DB ops → `src/lib/db/`, Server → `src/lib/admin-db/`
5. SSR safety: check `typeof window === 'undefined'` or use mounted pattern
6. Toast feedback: `react-hot-toast` for all user actions
7. Run `pnpm lint:fix` before commits
107 changes: 88 additions & 19 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,119 @@ on:
workflow_dispatch:

jobs:
test-app:
setup-environment:
name: Setup environment
runs-on: ubuntu-latest
env:
FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress

steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pnpm version mismatch between CI and package.json

Medium Severity

The package.json was updated to use pnpm@10.28.0 but the GitHub Actions workflow still specifies version: 10.13.1 in both the setup-environment and run-tests jobs. This version mismatch between local development and CI could cause different dependency resolution behavior, lockfile format incompatibilities, or feature differences between environments.

Additional Locations (1)

Fix in Cursor Fix in Web

with:
version: 10.13.1
run_install: false

- name: Set up Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'

- run: pnpm install --frozen-lockfile # optional, --immutable
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Start firebase in background
run: pnpm firebase &
- name: Determine pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

# - name: sleep for 30 seconds
# run: sleep 30
# shell: bash
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-

# - name: Cache firebase emulators
# uses: actions/cache@v3
# with:
# path: ${{ env.FIREBASE_EMULATORS_PATH }}
# key:
# ${{ runner.os }}-firebase-emulators-${{ hashFiles('emulator-cache/**') }}
# continue-on-error: true
- name: Cache Cypress binary
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: cypress-binary-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
cypress-binary-${{ runner.os }}-

- name: Install dependencies
run: pnpm install --frozen-lockfile # optional, --immutable

run-tests:
name: Run E2E tests
runs-on: ubuntu-latest
needs: setup-environment
env:
FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.13.1
run_install: false

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Determine pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-

- name: Cache Cypress binary
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: cypress-binary-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
cypress-binary-${{ runner.os }}-

- name: Install dependencies
run: pnpm install --frozen-lockfile # optional, --immutable

- name: Start firebase in background
run: pnpm firebase &

- name: Start app and run tests
uses: cypress-io/github-action@v4
with:
build: pnpm build
start: pnpm start
wait-on: 'http://localhost:8082, http://localhost:8081'
wait-on: 'http://localhost:8082, http://localhost:8081, http://localhost:3000'
install: false
env:
FIREBASE_PRIVATE_KEY: ${{ secrets.FIREBASE_PRIVATE_KEY }}
FIREBASE_CLIENT_EMAIL: ${{ secrets.FIREBASE_CLIENT_EMAIL }}
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ firebase-debug.log*
cypress/downloads/
cypress/videos/
cypress/screenshots/

# Kilo Code agent worktrees
.kilocode/worktrees/
.plans/
.env*.local
12 changes: 12 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"[javascript, typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json, jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VSCode settings use invalid language-specific override syntax

Low Severity

The VSCode settings file uses [javascript, typescript] and [json, jsonc] as keys for language-specific overrides, but this syntax is invalid. VSCode expects individual language identifiers like [javascript] and [typescript] as separate entries. The comma-separated format won't be recognized as a language selector, so the editor.defaultFormatter setting will not be applied to these file types as intended.

Fix in Cursor Fix in Web

"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
}
}
24 changes: 20 additions & 4 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.css",
"**/*.cy.ts",
"!**/*.min.js",
"!**/*.d.ts",
"!**/node_modules",
"!**/dist",
"!**/build",
Expand All @@ -22,8 +25,7 @@
"!**/.husky",
"!**/.vscode",
"!**/.env",
"!**/.env.local",
"!**/cypress"
"!**/.env.local"
]
},
"formatter": {
Expand All @@ -34,7 +36,16 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true
"recommended": true,
"performance": {
"noImgElement": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"correctness": {
"useExhaustiveDependencies": "warn"
}
}
},
"javascript": {
Expand All @@ -49,8 +60,13 @@
"enabled": true,
"actions": {
"source": {
"organizeImports": "off"
"organizeImports": "on"
}
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}
Loading
Loading