diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..3e87d06 --- /dev/null +++ b/.cursorrules @@ -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
...
+} +``` + +### Firestore Query (Client) +```tsx +import { getDocuments, where } from '@/lib/firestore-lite' +const treatments = await getDocuments( + '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( + '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(coll, id)` | `getDocuments(coll, ...constraints)` | `createDocument(coll, data)` | `setDocument(coll, id, data, merge?)` | `updateDocument(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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e09c54e..6cec2e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 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 }} diff --git a/.gitignore b/.gitignore index 4e92021..963aa8c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,8 @@ firebase-debug.log* cypress/downloads/ cypress/videos/ cypress/screenshots/ + +# Kilo Code agent worktrees +.kilocode/worktrees/ +.plans/ +.env*.local diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d7ca641 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "[javascript, typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json, jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "explicit", + "source.fixAll.biome": "explicit" + } +} diff --git a/biome.json b/biome.json index 4afb514..e1bf3bb 100644 --- a/biome.json +++ b/biome.json @@ -12,7 +12,10 @@ "**/*.tsx", "**/*.js", "**/*.jsx", + "**/*.css", + "**/*.cy.ts", "!**/*.min.js", + "!**/*.d.ts", "!**/node_modules", "!**/dist", "!**/build", @@ -22,8 +25,7 @@ "!**/.husky", "!**/.vscode", "!**/.env", - "!**/.env.local", - "!**/cypress" + "!**/.env.local" ] }, "formatter": { @@ -34,7 +36,16 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "performance": { + "noImgElement": "off" + }, + "security": { + "noDangerouslySetInnerHtml": "off" + }, + "correctness": { + "useExhaustiveDependencies": "warn" + } } }, "javascript": { @@ -49,8 +60,13 @@ "enabled": true, "actions": { "source": { - "organizeImports": "off" + "organizeImports": "on" } } + }, + "css": { + "parser": { + "tailwindDirectives": true + } } } diff --git a/components/blog/blogFooter.tsx b/components/blog/blogFooter.tsx deleted file mode 100644 index f7f25a0..0000000 --- a/components/blog/blogFooter.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Text, Image, Spacer, Grid, Button } from '@geist-ui/react' -import { useRouter } from 'next/router' - -import { useAuth } from 'lib/auth' - -export default function BlogFooter(): JSX.Element { - const { user, loading } = useAuth() - - const router = useRouter() - return ( - - - - Designed and developed by Michael Schultz in Oakland, California. - - {user ? ( - Thanks for being part of the Hemolog community! - ) : ( - <> - Start using Hemolog for free. - - - )} - - - - Michael Schultz - - - ) -} diff --git a/components/blog/postFooter.tsx b/components/blog/postFooter.tsx deleted file mode 100644 index c69c834..0000000 --- a/components/blog/postFooter.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { - Spacer, - Grid, - User, - Divider, - useClipboard, - useToasts, -} from '@geist-ui/react' -import { Share } from '@geist-ui/react-icons' - -export default function PostFooter({ postId }: { postId: string }) { - const [, setToast] = useToasts() - const { copy } = useClipboard() - const handleCopy = (postId: string) => { - copy(`https://hemolog.com/changelog#${postId}`) - setToast({ type: 'success', text: 'Link copied!' }) - } - - return ( - <> - - - - - - @michaelschultz - - - - -
- handleCopy(postId)} /> -
-
-
- - - - ) -} diff --git a/components/chart.tsx b/components/chart.tsx deleted file mode 100644 index 58ac1c7..0000000 --- a/components/chart.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { - FlexibleWidthXYPlot, - VerticalBarSeries, - HorizontalGridLines, - XAxis, - YAxis, -} from 'react-vis' -import useInfusions from 'lib/hooks/useInfusions' -import { filterInfusions } from 'lib/helpers' -import { TreatmentTypeEnum } from 'lib/db/infusions' - -// Dynamically load react-vis CSS only when Chart component is used -if (typeof window !== 'undefined') { - const link = document.createElement('link') - link.rel = 'stylesheet' - link.href = 'https://unpkg.com/react-vis/dist/style.css' - if (!document.querySelector(`link[href="${link.href}"]`)) { - document.head.appendChild(link) - } -} - -type ChartEntry = { - x: string - y: number -} - -interface ChartProps { - filterYear: string -} - -export default function Chart(props: ChartProps): JSX.Element | null { - const { filterYear } = props - const { data } = useInfusions() - - if (!data) { - return null - } - - const filteredInfusions = filterInfusions(data, filterYear) - - const bleeds = filteredInfusions - .filter((entry) => entry.type === TreatmentTypeEnum.BLEED) - .map((bleed) => bleed.date) - - const preventative = filteredInfusions - .filter((entry) => entry.type === TreatmentTypeEnum.PREVENTATIVE) - .map((preventitive) => preventitive.date) - - const prophy = filteredInfusions - .filter((entry) => entry.type === TreatmentTypeEnum.PROPHY) - .map((prophy) => prophy.date) - - const antibody = filteredInfusions - .filter((entry) => entry.type === TreatmentTypeEnum.ANTIBODY) - .map((antibody) => antibody.date) - - const chartSchema: ChartEntry[] = [ - { x: 'Jan', y: 0 }, - { x: 'Feb', y: 0 }, - { x: 'Mar', y: 0 }, - { x: 'Apr', y: 0 }, - { x: 'May', y: 0 }, - { x: 'Jun', y: 0 }, - { x: 'Jul', y: 0 }, - { x: 'Aug', y: 0 }, - { x: 'Sep', y: 0 }, - { x: 'Oct', y: 0 }, - { x: 'Nov', y: 0 }, - { x: 'Dec', y: 0 }, - ] - - // clones array using value rather than reference - const bleedData = JSON.parse(JSON.stringify(chartSchema)) - const preventativeData = JSON.parse(JSON.stringify(chartSchema)) - const prophyData = JSON.parse(JSON.stringify(chartSchema)) - const antibodyData = JSON.parse(JSON.stringify(chartSchema)) - - // distribute infusions into months - const distributeInfusions = (infusions: string[], data: ChartEntry[]) => { - for (const infusion of infusions) { - // Extract month from YYYY-MM-DD format (zero-based index) - const monthIndex = Number.parseInt(infusion.split('-')[1], 10) - 1 - data[monthIndex].y = data[monthIndex].y + 1 - } - } - - distributeInfusions(bleeds, bleedData) - distributeInfusions(preventative, preventativeData) - distributeInfusions(prophy, prophyData) - distributeInfusions(antibody, antibodyData) - - // determine the highest number of grouped infunsions to - // create a max value used to set the height of the chart - const bleedNumbers = bleedData.map((infusion: ChartEntry) => infusion.y) - const preventativeNumbers = preventativeData.map( - (infusion: ChartEntry) => infusion.y - ) - const prophyNumbers = prophyData.map((infusion: ChartEntry) => infusion.y) - const antibodyNumbers = antibodyData.map((infusion: ChartEntry) => infusion.y) - - const largestNumberOfBleeds = Math.max(...bleedNumbers) - const largestNumberOfPreventative = Math.max(...preventativeNumbers) - const largestNumberOfProphy = Math.max(...prophyNumbers) - const largestNumberOfAntibody = Math.max(...antibodyNumbers) - - const maxY = - largestNumberOfBleeds + - largestNumberOfPreventative + - largestNumberOfProphy + - largestNumberOfAntibody - - return ( -
- - - - - - - - - -
- ) -} diff --git a/components/descriptionCards.tsx b/components/descriptionCards.tsx deleted file mode 100644 index 857c4ac..0000000 --- a/components/descriptionCards.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Text, Grid, Link, Card } from '@geist-ui/react' -import Image from 'next/image' - -export default function DescrtipionCards(): JSX.Element { - return ( - - - - - - Free forever - - - No sponsorships, pharma companies, or ads. - - - - - - - - Didn’t Hemolog die? - - - Yep, but it’s back. Just wait till you see what this reincarnation - can do! - - - - - - - - Safe and secure - - - Your data is stored in Firebase, a trused database owned by Google. - - - - - - - - Open source - - - Check out the code on{' '} - - Github - - . - - - - - ) -} diff --git a/components/emergencyCard.tsx b/components/emergencyCard.tsx deleted file mode 100644 index 0ff88da..0000000 --- a/components/emergencyCard.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useContext } from 'react' -import Link from 'next/link' -import { - Grid, - Spacer, - Loading, - useTheme, - Tooltip, - Text, - useMediaQuery, -} from '@geist-ui/react' -import styled, { ThemeContext } from 'styled-components' -import QRCode from 'react-qr-code' - -import { useAuth } from 'lib/auth' -import useDbUser from 'lib/hooks/useDbUser' - -interface Props { - forPrint?: boolean -} - -export default function EmergencyCard({ forPrint }: Props): JSX.Element { - const { user } = useAuth() - const { person } = useDbUser(user?.uid || '') - const theme = useTheme() - // biome-ignore lint/suspicious/noExplicitAny: TODO: fix when moving to tailwind - const themeContext = useContext(ThemeContext) as any - const isMobile = useMediaQuery('xs', { match: 'down' }) - - if (isMobile) { - forPrint = true - } - - const alertUrl = `hemolog.com/emergency/${person?.alertId}` - - return ( - - - - -
- Bleeding disorder -
-

Emergency

-
- - {user?.photoUrl && ( - - - - )} -
-
- - - - - {person ? ( - - ) : ( - - )} - - - - - {person ? ( - -

{person?.name}

-
- {person?.severity} Hemophilia {person?.hemophiliaType} -
- {person?.factor &&
Treat with factor {person.factor}
} - - Scan or visit for treatment history - - - -

- {alertUrl} -

-
- -
-
-
- ) : ( - - )} -
-
-
- ) -} - -const StyledEmergencyCard = styled.div<{ forPrint?: boolean }>` - position: relative; - width: ${(props) => (props.forPrint ? '308px' : '525px')}; - height: ${(props) => (props.forPrint ? '192px' : '300px')}; - border-radius: 20px; - overflow: hidden; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - - h2, - h3, - h4, - h5, - h6 { - padding: 0; - margin: 0; - line-height: ${(props) => (props.forPrint ? '15px' : '24px')}; - font-size: ${(props) => (props.forPrint ? '75%' : '100%')}; - } - - h5 { - font-weight: 400; - } -` - -const StyledHeader = styled.div<{ forPrint?: boolean; accentColor: string }>` - background-color: ${(props) => props.accentColor}; - height: ${(props) => (props.forPrint ? '56px' : '90px')}; - width: 100%; - padding: ${(props) => (props.forPrint ? '16px' : '24px')}; -` - -const StyledPersonalInfo = styled.div<{ forPrint?: boolean }>` - padding-left: ${(props) => (props.forPrint ? '8px' : '16px')}; -` - -const StyledQRCode = styled.div<{ forPrint?: boolean; accentColor: string }>` - position: relative; - width: ${(props) => (props.forPrint ? '96px' : '148px')}; - height: ${(props) => (props.forPrint ? '96px' : '148px')}; - padding: ${(props) => (props.forPrint ? '5px' : '8px')}; - border-radius: 8px; - border: ${(props) => (props.forPrint ? '3px' : '4px')} solid - ${(props) => props.accentColor}; -` - -const StyledScanLink = styled.div<{ forPrint?: boolean }>` - padding-top: ${(props) => (props.forPrint ? '8px' : '16px')}; -` - -const StyledBloodDrop = styled.img<{ forPrint?: boolean }>` - position: absolute; - left: ${(props) => (props.forPrint ? '34px' : '56px')}; - top: ${(props) => (props.forPrint ? '-18px' : '-24px')}; - width: ${(props) => (props.forPrint ? '24px' : '32px')}; - height: ${(props) => (props.forPrint ? '24px' : '32px')}; - border: none !important; -` - -const StyledAvatar = styled.img<{ forPrint?: boolean }>` - width: ${(props) => (props.forPrint ? '60px' : '100px')}; - height: ${(props) => (props.forPrint ? '60px' : '100px')}; - border-radius: 50%; - border: ${(props) => (props.forPrint ? '4px' : '8px')} solid white; -` diff --git a/components/emergencyInfo.tsx b/components/emergencyInfo.tsx deleted file mode 100644 index 8a8d8f6..0000000 --- a/components/emergencyInfo.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - Grid, - Avatar, - Note, - Spacer, - Text, - useMediaQuery, -} from '@geist-ui/react' -import styled from 'styled-components' - -import InfusionTable from 'components/infusionTable' -import type { Person } from 'lib/types/person' -import { useAuth } from 'lib/auth' - -interface Props { - person: Person -} - -export default function EmergencyInfo(props: Props): JSX.Element { - const { person } = props - const { user } = useAuth() - const smallerThanSmall = useMediaQuery('xs', { match: 'down' }) - - if (person) { - return ( - <> - - - -
- {person.name} - - {person.severity} Hemophilia {person.hemophiliaType}, treat with - factor {person.factor} - -
-
- - - - Most recent treatments - {smallerThanSmall && Swipe →} - - - - - Pay attention to the date on each of these logs. We’re only showing - you the 3 most recent logs. If you want to see more,{' '} - {person.name?.split(' ')[0]} will have to give you - permission. - - - - - {user && ( - <> - Emergency contacts (coming soon) - - Soon you’ll be able to add these from your settings page. - - - )} - - {/* NOTE(michael) remember when you implement this that you remember - to update the example logic on /emergency/alertId as to not - leak my actual emergency contact's info */} - - {/* - - - - Jenifer Schultz - - - 555-555-5555 - - - - - - Mike Schultz - - - 555-555-5555 - - - */} - - ) - } - - return ( - - This person’s information could not be found. - - ) -} - -const StyledRow = styled.div` - display: flex; - align-items: center; - flex-shrink: 0; - - h3, - h5 { - margin: 0; - } - - div { - display: flex; - flex-direction: column; - padding-left: 16px; - } -` diff --git a/components/emergencySnippet.tsx b/components/emergencySnippet.tsx deleted file mode 100644 index 7e6bcb3..0000000 --- a/components/emergencySnippet.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Grid, Snippet } from '@geist-ui/react' - -interface Props { - alertId: string - style?: React.CSSProperties -} - -export default function EmergencySnippet(props: Props): JSX.Element { - const { alertId = 'example', style } = props - const env = process.env.NODE_ENV - const domain = env === 'development' ? 'localhost:3000' : 'hemolog.com' - - return ( - - - - ) -} diff --git a/components/feedbackFishFooter.tsx b/components/feedbackFishFooter.tsx deleted file mode 100644 index 3a63bab..0000000 --- a/components/feedbackFishFooter.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Text, Page, Grid, Link } from '@geist-ui/react' -// import { FeedbackFish } from '@feedback-fish/react' - -export default function Footer(): JSX.Element { - // NOTE(michael): testing out https://feedback.fish. - // const PROJECT_ID = process.env.FEEDBACK_FISH_PROJECT_ID - - return ( - - - {/* - - */} - - - Built by{' '} - - Michael Schultz - - - - - ) -} diff --git a/components/feedbackModal.tsx b/components/feedbackModal.tsx deleted file mode 100644 index cda2d93..0000000 --- a/components/feedbackModal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Modal, Textarea, Text, Spacer, useToasts } from '@geist-ui/react' -import { useFormik } from 'formik' - -import { useAuth } from 'lib/auth' -import { createFeedback, type FeedbackType } from 'lib/db/feedback' -import type { AttachedUserType } from 'lib/types/users' - -interface FeedbackValues { - message: string -} - -interface FeedbackModalProps { - visible: boolean - setVisible: (flag: boolean) => void - bindings: Record -} - -export default function FeedbackModal(props: FeedbackModalProps): JSX.Element { - const { visible, setVisible, bindings } = props - const [, setToast] = useToasts() - const { user } = useAuth() - - const handleCreateFeedback = async (feedback: FeedbackValues) => { - const feedbackUser: AttachedUserType = { - email: user?.email || '', - name: user?.name || '', - photoUrl: user?.photoUrl || '', - uid: user?.uid || '', - } - - const feedbackPayload: FeedbackType = { - ...feedback, - createdAt: new Date().toISOString(), - user: feedbackUser, - } - - createFeedback(feedbackPayload) - .then(() => { - setToast({ - text: "Feedback submitted! We'll respond soon via email.", - type: 'success', - delay: 5000, - }) - closeModal() - }) - .catch((error: unknown) => - setToast({ - text: `Something went wrong: ${error instanceof Error ? error.message : String(error)}`, - type: 'error', - delay: 10000, - }) - ) - } - - const closeModal = () => { - setVisible(false) - formik.resetForm() - } - - const formik = useFormik({ - initialValues: { - message: '', - }, - onSubmit: async (values) => { - await handleCreateFeedback(values) - }, - }) - - return ( - - Feedback - Hemolog.com - -

- If you’ve run into a bug or have an idea for how Hemolog could work - better for you, let me know. -

- -
- {/* Name - */} - - Your feedback -