From 9bc2c461393620a941dbad20668b9b4d331927eb Mon Sep 17 00:00:00 2001 From: mattrace-gloo Date: Wed, 3 Jun 2026 11:55:25 -0700 Subject: [PATCH 1/6] Build Projects tab with design system and home shell (#32). Add theme tokens, Lucide icons, HomeScreen with page header and tabs, Projects tab with sync indicators and empty state, and My Work placeholder. --- App.tsx | 13 +- jest.config.cjs | 2 +- package-lock.json | 34 ++++ package.json | 1 + src/app/screens/HomeScreen.tsx | 49 ++++++ src/app/tabs/MyWorkTab.tsx | 9 + src/app/tabs/ProjectList.tsx | 106 ------------ src/app/tabs/ProjectsTab.test.tsx | 81 +++++++++ src/app/tabs/ProjectsTab.tsx | 133 +++++++++++++++ src/assets/icons/fluent-logo-white.svg | 66 ++++++++ src/components/layout/PageHeader.tsx | 92 +++++++++++ src/components/layout/ScreenContainer.tsx | 31 ++++ src/components/layout/TabBar.tsx | 74 +++++++++ src/components/ui/EmptyState.tsx | 31 ++++ src/components/ui/ListCard.tsx | 68 ++++++++ src/components/ui/SyncButton.tsx | 193 +++++----------------- src/components/ui/SyncIndicator.tsx | 32 ++++ src/db/queries.ts | 58 +++++++ src/hooks/useSync.ts | 131 +++++++++++++++ src/navigation/AppNavigator.tsx | 4 +- src/theme/colors.ts | 18 ++ src/theme/iconSpecs.ts | 22 +++ src/theme/index.ts | 26 +++ src/theme/radius.ts | 5 + src/theme/spacing.ts | 5 + src/theme/tokens.ts | 94 +++++++++++ src/theme/typography.ts | 5 + src/types/db/types.ts | 13 ++ src/types/navigation/types.ts | 2 +- src/utils/projectSyncState.test.ts | 17 ++ src/utils/projectSyncState.ts | 16 ++ 31 files changed, 1163 insertions(+), 268 deletions(-) create mode 100644 src/app/screens/HomeScreen.tsx create mode 100644 src/app/tabs/MyWorkTab.tsx delete mode 100644 src/app/tabs/ProjectList.tsx create mode 100644 src/app/tabs/ProjectsTab.test.tsx create mode 100644 src/app/tabs/ProjectsTab.tsx create mode 100644 src/assets/icons/fluent-logo-white.svg create mode 100644 src/components/layout/PageHeader.tsx create mode 100644 src/components/layout/ScreenContainer.tsx create mode 100644 src/components/layout/TabBar.tsx create mode 100644 src/components/ui/EmptyState.tsx create mode 100644 src/components/ui/ListCard.tsx create mode 100644 src/components/ui/SyncIndicator.tsx create mode 100644 src/hooks/useSync.ts create mode 100644 src/theme/colors.ts create mode 100644 src/theme/iconSpecs.ts create mode 100644 src/theme/index.ts create mode 100644 src/theme/radius.ts create mode 100644 src/theme/spacing.ts create mode 100644 src/theme/tokens.ts create mode 100644 src/theme/typography.ts create mode 100644 src/utils/projectSyncState.test.ts create mode 100644 src/utils/projectSyncState.ts diff --git a/App.tsx b/App.tsx index fdf62d9..ee6a610 100644 --- a/App.tsx +++ b/App.tsx @@ -7,6 +7,7 @@ import { NavigationContainer } from '@react-navigation/native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { View, ActivityIndicator, Text, StyleSheet } from 'react-native'; import { FLUENT_USER_EMAIL } from '@env'; +import { theme } from './src/theme'; const log = logger.create('App'); @@ -45,7 +46,7 @@ function App() { Error: {error} ) : ( <> - + Initializing... )} @@ -70,14 +71,14 @@ const styles = StyleSheet.create({ alignItems: 'center', }, errorText: { - color: 'red', - fontSize: 14, - padding: 20, + color: theme.colors.destructive, + fontSize: theme.typography.sizes.sm, + padding: theme.spacing.xl, textAlign: 'center', }, loadingText: { - marginTop: 10, - fontSize: 14, + marginTop: theme.spacing.sm, + fontSize: theme.typography.sizes.sm, }, }); diff --git a/jest.config.cjs b/jest.config.cjs index 1c4a260..d6d9485 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,7 +1,7 @@ module.exports = { preset: 'react-native', transformIgnorePatterns: [ - 'node_modules/(?!(@react-native|react-native|@react-navigation|react-native-safe-area-context|@react-native-vector-icons|react-native-screens|react-native-gesture-handler|react-native-svg)/)', + 'node_modules/(?!(@react-native|react-native|@react-navigation|react-native-safe-area-context|@react-native-vector-icons|react-native-screens|react-native-gesture-handler|react-native-svg|lucide-react-native)/)', ], testPathIgnorePatterns: ['/node_modules/', '/android/', '/ios/'], }; diff --git a/package-lock.json b/package-lock.json index 7d9d4ba..9e4b65d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@react-navigation/native": "^7.1.33", "@react-navigation/stack": "^7.8.4", "@tanstack/react-query": "^5.96.1", + "lucide-react-native": "^0.544.0", "react": "19.2.3", "react-native": "0.84.1", "react-native-dotenv": "^3.4.11", @@ -81,6 +82,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1919,6 +1921,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -3391,6 +3394,7 @@ "integrity": "sha512-KlRawK4aXxRLlR3HYVfZKhfQp7sejQefQ/LttUWUkErhKO0AFt+yznoSLq7xwIrH9K3A3YwImHuFVtUtuDmurA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@react-native/js-polyfills": "0.84.1", "@react-native/metro-babel-transformer": "0.84.1", @@ -3503,6 +3507,7 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz", "integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==", "license": "MIT", + "peer": true, "dependencies": { "@react-navigation/core": "^7.17.2", "escape-string-regexp": "^4.0.0", @@ -3760,6 +3765,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4132,6 +4138,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4172,6 +4179,7 @@ "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", @@ -4201,6 +4209,7 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -4469,6 +4478,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5105,6 +5115,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6395,6 +6406,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8300,6 +8312,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9482,6 +9495,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react-native": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.544.0.tgz", + "integrity": "sha512-ylN6TfmVmZVAd82CmTWyKnYv8e5WQLvaaabOHIB5z3OMA2Vg/jtMW5ORDoR2J7Hr8LMSys1v2b2Fr1GVxr3XQA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-native": "*", + "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -10963,6 +10987,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11021,6 +11046,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.84.1.tgz", "integrity": "sha512-0PjxOyXRu3tZ8EobabxSukvhKje2HJbsZikR0U+pvS0pYZza2hXKjcSBiBdFN4h9D0S3v6a8kkrDK6WTRKMwzg==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.84.1", @@ -11091,6 +11117,7 @@ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.31.1.tgz", "integrity": "sha512-wQDlECdEzHhYKTnQXFnSqWUtJ5TS3MGQi7EWvQczTnEVKfk6XVSBecnpWAoI/CqlYQ7IWMJEyutY6BxwEBoxeg==", "license": "MIT", + "peer": true, "dependencies": { "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", @@ -11107,6 +11134,7 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -11117,6 +11145,7 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.24.0.tgz", "integrity": "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==", "license": "MIT", + "peer": true, "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" @@ -11131,6 +11160,7 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.4.tgz", "integrity": "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A==", "license": "MIT", + "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -11215,6 +11245,7 @@ "integrity": "sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "react-is": "^19.2.3", "scheduler": "^0.27.0" @@ -12511,6 +12542,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12716,6 +12748,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13206,6 +13239,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index c850687..c81b012 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@react-navigation/native": "^7.1.33", "@react-navigation/stack": "^7.8.4", "@tanstack/react-query": "^5.96.1", + "lucide-react-native": "^0.544.0", "react": "19.2.3", "react-native": "0.84.1", "react-native-dotenv": "^3.4.11", diff --git a/src/app/screens/HomeScreen.tsx b/src/app/screens/HomeScreen.tsx new file mode 100644 index 0000000..18ad525 --- /dev/null +++ b/src/app/screens/HomeScreen.tsx @@ -0,0 +1,49 @@ +import React, { useState, useCallback } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { PageHeader } from '../../components/layout/PageHeader'; +import { TabBar, HomeTab } from '../../components/layout/TabBar'; +import { ScreenContainer } from '../../components/layout/ScreenContainer'; +import { MyWorkTab } from '../tabs/MyWorkTab'; +import { ProjectsTab } from '../tabs/ProjectsTab'; +import { useSync } from '../../hooks/useSync'; + +export default function HomeScreen() { + const [activeTab, setActiveTab] = useState('myWork'); + const [refreshKey, setRefreshKey] = useState(0); + + const handleSyncComplete = useCallback(() => { + setRefreshKey(key => key + 1); + }, []); + + const { isSyncing, triggerSync } = useSync({ + onSyncComplete: handleSyncComplete, + }); + + const handleSettingsPress = () => { + // TODO(#40): Navigate to Settings screen + }; + + return ( + + + + + {activeTab === 'myWork' ? ( + + ) : ( + + )} + + + ); +} + +const styles = StyleSheet.create({ + content: { + flex: 1, + }, +}); diff --git a/src/app/tabs/MyWorkTab.tsx b/src/app/tabs/MyWorkTab.tsx new file mode 100644 index 0000000..46cd046 --- /dev/null +++ b/src/app/tabs/MyWorkTab.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { EmptyState } from '../../components/ui/EmptyState'; + +const MY_WORK_EMPTY_MESSAGE = + "You don't have any chapters to work on right now. Check the Projects tab to find available work."; + +export function MyWorkTab() { + return ; +} diff --git a/src/app/tabs/ProjectList.tsx b/src/app/tabs/ProjectList.tsx deleted file mode 100644 index 1467292..0000000 --- a/src/app/tabs/ProjectList.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - View, - Text, - FlatList, - TouchableOpacity, - ActivityIndicator, -} from 'react-native'; -import { logger } from '../../utils/logger'; -import { Project } from '../../types/db/types'; -import { getProjects } from '../../db/queries'; -import { appStyles as styles } from '../appStyles'; -import { useNavigation } from '@react-navigation/native'; -import { SyncButton } from '../../components/ui/SyncButton'; -import FluentLogo from '../../assets/icons/fluent-logo.svg'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { Ionicons } from '@react-native-vector-icons/ionicons'; -import { RootStackParamList } from '../../types/navigation/types'; - -const log = logger.create('ProjectList'); -type Nav = StackNavigationProp; - -export default function ProjectList() { - const navigation = useNavigation