Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ public-hoist-pattern[]=@astrojs/check
public-hoist-pattern[]=eslint-plugin-astro
public-hoist-pattern[]=astro-eslint-parser
public-hoist-pattern[]=@typescript-eslint/*
public-hoist-pattern[]=metro
public-hoist-pattern[]=metro-*
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"cSpell.words": [
"midwifes"
"midwifes",
"Pressable"
],
"eslint.useFlatConfig": true,
"eslint.workingDirectories": [
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ The visual design language for the mobile app. Covers:

---

### [`documentation/CONVENTIONS.md`](documentation/CONVENTIONS.md)
Coding conventions for the repository. Covers:
- File naming: PascalCase for components, camelCase for hooks, kebab-case for lib/util files
- JSDoc: when to write one, what to include (params, returns, examples), and line length

**Update when:** establishing or changing any coding convention that should be applied consistently across the codebase.

---

## Mockups

Visual mockups live in `documentation/mockups/` and are for reference only — do not modify them unless producing new design artifacts.
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ yarn-error.*
# generated native folders
/ios
/android

# test coverage
coverage/
47 changes: 35 additions & 12 deletions apps/mobile/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { View } from "react-native";
// import { useMigrations } from "drizzle-orm/expo-sqlite/migrator";
// import { db } from "./src/db";
// import migrations from "./drizzle/migrations";
import { ClientsTest } from "./src/components/ClientsTest";
import { useAppFonts } from "./src/hooks/useAppFonts";
import { ThemeProvider, useTheme } from "./src/lib/theme-context";
import { makeStyles } from "./src/lib/make-styles";

export default function App() {
const useStyles = makeStyles((theme) => ({
container: {
flex: 1,
backgroundColor: theme.background,
paddingTop: 30,
},
}));

function AppContent() {
const styles = useStyles();
const theme = useTheme();
return (
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
<StatusBar style="auto" />
<ClientsTest />
<StatusBar style={theme.statusBarIcons} />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
export default function App() {
const fontsLoaded = useAppFonts();
// const { success, error } = useMigrations(db, migrations);

if (!fontsLoaded) return null;
// if (error) throw error;
// if (!success) return null;

return (
<ThemeProvider>
<AppContent />
</ThemeProvider>
);
}
4 changes: 4 additions & 0 deletions apps/mobile/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
presets: ["babel-preset-expo"],
plugins: [["inline-import", { extensions: [".sql"] }]],
};
8 changes: 8 additions & 0 deletions apps/mobile/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "sqlite",
driver: "expo",
});
7 changes: 7 additions & 0 deletions apps/mobile/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { getDefaultConfig } = require("expo/metro-config");

const config = getDefaultConfig(__dirname);

config.resolver.assetExts.push("sql");

module.exports = config;
19 changes: 16 additions & 3 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@
"ios": "expo start --ios",
"web": "expo start --web",
"check:lint": "eslint --max-warnings 0",
"check:types": "tsc --noEmit"
"check:types": "tsc --noEmit",
"check:format": "prettier --check .",
"check": "pnpm check:format && pnpm run check:lint && pnpm run check:types",
"test": "jest",
"test:watch": "jest --watch",
"db:generate": "drizzle-kit generate"
},
"dependencies": {
"@expo-google-fonts/inter": "^0.4.2",
"@expo-google-fonts/newsreader": "^0.4.1",
"@midwifes-notebook/server": "workspace:*",
"drizzle-orm": "^0.45.1",
"expo": "~55.0.6",
Expand All @@ -20,17 +27,23 @@
"expo-splash-screen": "^55.0.10",
"expo-sqlite": "^55.0.10",
"expo-status-bar": "~55.0.4",
"nativewind": "^4.2.2",
"react": "19.2.0",
"react-native": "0.83.2"
},
"devDependencies": {
"@testing-library/react-native": "^13.3.3",
"@types/jest": "29.5.14",
"@types/react": "~19.2.2",
"babel-plugin-inline-import": "^3.0.0",
"drizzle-kit": "^0.31.9",
"eslint": "^9.39.4",
"eslint-config-expo": "^55.0.0",
"tailwindcss": "^4.2.1",
"jest": "~29.7.0",
"jest-expo": "~55.0.18",
"typescript": "~5.9.2"
},
"jest": {
"preset": "jest-expo"
},
"private": true
}
71 changes: 71 additions & 0 deletions apps/mobile/src/components/ClientsTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from "react";
import { FlatList, StyleSheet, View } from "react-native";
import { useLiveQuery } from "drizzle-orm/expo-sqlite";

import { db } from "../db";
import { clients } from "../db/schema";
import { useToggleTheme } from "../lib/theme-context";

import { Text } from "./ui/Text";
import { Button } from "./ui/Button";

const NAMES = [
"Alice",
"Betty",
"Clara",
"Diana",
"Elena",
"Fiona",
"Grace",
"Hannah",
"Iris",
"Julia",
];

function randomName() {
return (
NAMES[Math.floor(Math.random() * NAMES.length)] +
" " +
Math.floor(Math.random() * 9000 + 1000)
);
}

export function ClientsTest() {
const { data } = useLiveQuery(db.select().from(clients));
const [inserting, setInserting] = useState(false);
const toggleTheme = useToggleTheme();

async function insertClient() {
setInserting(true);
await db.insert(clients).values({ name: randomName() });
setInserting(false);
}

return (
<View style={styles.container}>
<Button
title={inserting ? "Inserting…" : "Add test client"}
onPress={insertClient}
disabled={inserting}
variant="primary"
/>
<Button title="Toggle theme" onPress={toggleTheme} variant="secondary" />
<FlatList
data={data}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<Text style={styles.row}>
Found one! {item.id}. {item.name}
</Text>
)}
ListEmptyComponent={<Text style={styles.empty}>No clients yet.</Text>}
/>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, width: "100%", padding: 16, gap: 12 },
row: { paddingVertical: 6, fontSize: 16 },
empty: { color: "#888", fontStyle: "italic" },
});
67 changes: 67 additions & 0 deletions apps/mobile/src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Pressable } from "react-native";
import { makeStyles } from "../../lib/make-styles";
import { Text } from "./Text";

type ButtonProps = {
title: string;
onPress?: () => void;
disabled?: boolean;
variant?: "primary" | "secondary";
};

export function Button({
title,
onPress,
disabled,
variant = "primary",
}: ButtonProps) {
const styles = useStyles();

return (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.base,
styles[variant],
pressed && styles.pressed,
disabled && styles.disabled,
]}
>
<Text style={[styles.label, styles[`${variant}Label`]]}>{title}</Text>
</Pressable>
);
}
const useStyles = makeStyles((theme) => ({
base: {
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 20,
alignItems: "center",
justifyContent: "center",
},
primary: {
backgroundColor: theme.primary,
},
secondary: {
backgroundColor: "transparent",
borderWidth: 1,
borderColor: theme.primary,
},
pressed: {
opacity: 0.8,
},
disabled: {
opacity: 0.4,
},
label: {
fontSize: 16,
fontWeight: "600",
},
primaryLabel: {
color: theme.primaryForeground,
},
secondaryLabel: {
color: theme.primary,
},
}));
17 changes: 17 additions & 0 deletions apps/mobile/src/components/ui/Text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Text as RNText, TextProps as RNTextProps } from "react-native";
import { fontFamilies } from "../../lib/themes";
import { makeStyles } from "../../lib/make-styles";

type TextProps = RNTextProps & {
header?: boolean;
};

const useStyles = makeStyles((theme) => ({
base: { color: theme.foreground },
}));

export function Text({ header: heading, style, ...props }: TextProps) {
const styles = useStyles();
const fontFamily = heading ? fontFamilies.heading : fontFamilies.base;
return <RNText style={[styles.base, { fontFamily }, style]} {...props} />;
}
29 changes: 29 additions & 0 deletions apps/mobile/src/components/ui/__tests__/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";
import { fireEvent, renderWithTheme, screen } from "../../../test-utils";
import { Button } from "../Button";

describe("Button", () => {
it("renders the title", () => {
renderWithTheme(<Button title="Save" />);
expect(screen.getByText("Save")).toBeTruthy();
});

it("calls onPress when pressed", () => {
const onPress = jest.fn();
renderWithTheme(<Button title="Save" onPress={onPress} />);
fireEvent.press(screen.getByText("Save"));
expect(onPress).toHaveBeenCalledTimes(1);
});

it("does not call onPress when disabled", () => {
const onPress = jest.fn();
renderWithTheme(<Button title="Save" onPress={onPress} disabled />);
fireEvent.press(screen.getByText("Save"));
expect(onPress).not.toHaveBeenCalled();
});

it("renders secondary variant without crashing", () => {
renderWithTheme(<Button title="Cancel" variant="secondary" />);
expect(screen.getByText("Cancel")).toBeTruthy();
});
});
15 changes: 15 additions & 0 deletions apps/mobile/src/db/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { drizzle } from "drizzle-orm/expo-sqlite";
import { deleteDatabaseSync, openDatabaseSync } from "expo-sqlite";
import * as schema from "./schema";

const DB_NAME = "midwifes_notebook.db";

let expo = openDatabaseSync(DB_NAME, { enableChangeListener: true });
// Delete the database in development mode to ensure a clean slate on each reload. Stop using this after shipping 1 migration.
if (__DEV__) {
expo.closeSync();
deleteDatabaseSync(DB_NAME);
expo = openDatabaseSync(DB_NAME, { enableChangeListener: true });
}

export const db = drizzle(expo, { schema });
6 changes: 6 additions & 0 deletions apps/mobile/src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const clients = sqliteTable("clients", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
});
Loading
Loading