From 8f66a4d4ed1981f0ec81378d9b4c4037978cf1ab Mon Sep 17 00:00:00 2001 From: root Date: Wed, 18 Feb 2026 06:21:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Sprint=200=20=E2=80=94=20project=20scaf?= =?UTF-8?q?fold,=20DB=20schema,=20Zustand=20stores,=20base=20UI=20componen?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 6 +++ .gitignore | 12 +++++ .prettierrc | 6 +++ app.json | 53 ++++++++++++++++++ app/(tabs)/_layout.tsx | 43 +++++++++++++++ app/(tabs)/contacts.tsx | 10 ++++ app/(tabs)/index.tsx | 10 ++++ app/(tabs)/map.tsx | 10 ++++ app/(tabs)/settings.tsx | 10 ++++ app/_layout.tsx | 11 ++++ assets/images/.gitkeep | 0 babel.config.js | 9 ++++ components/ui/Button.tsx | 37 +++++++++++++ components/ui/Card.tsx | 9 ++++ components/ui/Input.tsx | 22 ++++++++ global.css | 3 ++ lib/database.ts | 114 +++++++++++++++++++++++++++++++++++++++ package.json | 43 +++++++++++++++ store/useContactStore.ts | 42 +++++++++++++++ store/useUserStore.ts | 19 +++++++ tailwind.config.js | 19 +++++++ tsconfig.json | 9 ++++ 22 files changed, 497 insertions(+) create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 app.json create mode 100644 app/(tabs)/_layout.tsx create mode 100644 app/(tabs)/contacts.tsx create mode 100644 app/(tabs)/index.tsx create mode 100644 app/(tabs)/map.tsx create mode 100644 app/(tabs)/settings.tsx create mode 100644 app/_layout.tsx create mode 100644 assets/images/.gitkeep create mode 100644 babel.config.js create mode 100644 components/ui/Button.tsx create mode 100644 components/ui/Card.tsx create mode 100644 components/ui/Input.tsx create mode 100644 global.css create mode 100644 lib/database.ts create mode 100644 package.json create mode 100644 store/useContactStore.ts create mode 100644 store/useUserStore.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c80a3af --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['expo', 'prettier'], + rules: { + 'no-unused-vars': 'warn', + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..094d863 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +.expo/ +dist/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ +.env diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..92f97e7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/app.json b/app.json new file mode 100644 index 0000000..3c2022d --- /dev/null +++ b/app.json @@ -0,0 +1,53 @@ +{ + "expo": { + "name": "TerritoryLog", + "slug": "territory-log", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "territorylog", + "userInterfaceStyle": "light", + "splash": { + "resizeMode": "contain", + "backgroundColor": "#1A6B72" + }, + "ios": { + "supportsTablet": false, + "bundleIdentifier": "space.juankibin.territorylog" + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#1A6B72" + }, + "package": "space.juankibin.territorylog" + }, + "web": { + "bundler": "metro" + }, + "plugins": [ + "expo-router", + "expo-sqlite", + [ + "expo-location", + { + "locationAlwaysAndWhenInUsePermission": "Allow TerritoryLog to use your location to tag ministry contacts." + } + ], + [ + "expo-local-authentication", + { + "faceIDPermission": "Allow TerritoryLog to use Face ID for app lock." + } + ], + [ + "expo-camera", + { + "cameraPermission": "Allow TerritoryLog to use the camera to scan QR codes." + } + ] + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..dc8d944 --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,43 @@ +import { Tabs } from 'expo-router'; +import { Home, Users, Map, Settings } from 'lucide-react-native'; + +export default function TabLayout() { + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ); +} diff --git a/app/(tabs)/contacts.tsx b/app/(tabs)/contacts.tsx new file mode 100644 index 0000000..4c84da1 --- /dev/null +++ b/app/(tabs)/contacts.tsx @@ -0,0 +1,10 @@ +import { View, Text } from 'react-native'; + +export default function ContactsScreen() { + return ( + + Contacts + Coming in Sprint 1 + + ); +} diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..68f7810 --- /dev/null +++ b/app/(tabs)/index.tsx @@ -0,0 +1,10 @@ +import { View, Text } from 'react-native'; + +export default function HomeScreen() { + return ( + + TerritoryLog + Dashboard coming in Sprint 2 + + ); +} diff --git a/app/(tabs)/map.tsx b/app/(tabs)/map.tsx new file mode 100644 index 0000000..bf68c76 --- /dev/null +++ b/app/(tabs)/map.tsx @@ -0,0 +1,10 @@ +import { View, Text } from 'react-native'; + +export default function MapScreen() { + return ( + + Map + Coming in Sprint 4 + + ); +} diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx new file mode 100644 index 0000000..5087d12 --- /dev/null +++ b/app/(tabs)/settings.tsx @@ -0,0 +1,10 @@ +import { View, Text } from 'react-native'; + +export default function SettingsScreen() { + return ( + + Settings + Coming in Sprint 6 + + ); +} diff --git a/app/_layout.tsx b/app/_layout.tsx new file mode 100644 index 0000000..3ba7271 --- /dev/null +++ b/app/_layout.tsx @@ -0,0 +1,11 @@ +import { Stack } from 'expo-router'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import '../global.css'; + +export default function RootLayout() { + return ( + + + + ); +} diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..a8c8326 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,9 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: [ + ['babel-preset-expo', { jsxImportSource: 'nativewind' }], + ], + plugins: ['react-native-reanimated/plugin'], + }; +}; diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx new file mode 100644 index 0000000..ff35e50 --- /dev/null +++ b/components/ui/Button.tsx @@ -0,0 +1,37 @@ +import { TouchableOpacity, Text, ActivityIndicator } from 'react-native'; + +interface ButtonProps { + label: string; + onPress: () => void; + variant?: 'primary' | 'secondary' | 'danger'; + loading?: boolean; + disabled?: boolean; +} + +export function Button({ label, onPress, variant = 'primary', loading, disabled }: ButtonProps) { + const base = 'rounded-xl py-4 px-6 items-center justify-center'; + const variants = { + primary: 'bg-primary', + secondary: 'bg-secondary border border-primary', + danger: 'bg-danger', + }; + const textColors = { + primary: 'text-white', + secondary: 'text-primary', + danger: 'text-white', + }; + + return ( + + {loading ? ( + + ) : ( + {label} + )} + + ); +} diff --git a/components/ui/Card.tsx b/components/ui/Card.tsx new file mode 100644 index 0000000..620b527 --- /dev/null +++ b/components/ui/Card.tsx @@ -0,0 +1,9 @@ +import { View, ViewProps } from 'react-native'; + +export function Card({ children, className = '', ...props }: ViewProps & { className?: string }) { + return ( + + {children} + + ); +} diff --git a/components/ui/Input.tsx b/components/ui/Input.tsx new file mode 100644 index 0000000..a641ec9 --- /dev/null +++ b/components/ui/Input.tsx @@ -0,0 +1,22 @@ +import { View, Text, TextInput, TextInputProps } from 'react-native'; + +interface InputProps extends TextInputProps { + label?: string; + error?: string; +} + +export function Input({ label, error, ...props }: InputProps) { + return ( + + {label && {label}} + + {error && {error}} + + ); +} diff --git a/global.css b/global.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/lib/database.ts b/lib/database.ts new file mode 100644 index 0000000..80f626a --- /dev/null +++ b/lib/database.ts @@ -0,0 +1,114 @@ +import * as SQLite from 'expo-sqlite'; + +let db: SQLite.SQLiteDatabase | null = null; + +export async function getDatabase(): Promise { + if (!db) { + db = await SQLite.openDatabaseAsync('territorylog.db'); + await initSchema(db); + } + return db; +} + +async function initSchema(database: SQLite.SQLiteDatabase): Promise { + await database.execAsync(` + PRAGMA journal_mode = WAL; + PRAGMA foreign_keys = ON; + + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + share_id TEXT UNIQUE NOT NULL, + is_self INTEGER DEFAULT 1, + created_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS territories ( + id TEXT PRIMARY KEY, + territory_code TEXT UNIQUE NOT NULL, + municipality TEXT, + barangay TEXT, + area TEXT, + block TEXT, + assigned_to TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS contacts ( + id TEXT PRIMARY KEY, + owner_id TEXT NOT NULL, + full_name TEXT NOT NULL, + address TEXT, + household_count INTEGER DEFAULT 1, + gender TEXT, + category TEXT NOT NULL, + status TEXT DEFAULT 'Active', + tags TEXT DEFAULT '[]', + notes TEXT, + territory_code TEXT REFERENCES territories(territory_code), + latitude REAL, + longitude REAL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + deleted_at INTEGER + ); + + CREATE TABLE IF NOT EXISTS visits ( + id TEXT PRIMARY KEY, + contact_id TEXT NOT NULL REFERENCES contacts(id), + visited_by_name TEXT NOT NULL, + visited_by_id TEXT NOT NULL, + visit_date INTEGER NOT NULL, + topic TEXT, + response TEXT, + remarks TEXT, + next_visit_date INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS topics ( + id TEXT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + is_default INTEGER DEFAULT 0, + created_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS sync_log ( + id TEXT PRIMARY KEY, + partner_id TEXT NOT NULL, + partner_name TEXT, + synced_at INTEGER NOT NULL, + sent_count INTEGER DEFAULT 0, + received_count INTEGER DEFAULT 0 + ); + + CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(full_name); + CREATE INDEX IF NOT EXISTS idx_contacts_status ON contacts(status); + CREATE INDEX IF NOT EXISTS idx_contacts_territory ON contacts(territory_code); + CREATE INDEX IF NOT EXISTS idx_visits_contact ON visits(contact_id); + CREATE INDEX IF NOT EXISTS idx_visits_date ON visits(visit_date); + `); + + // Seed default topics + const now = Math.floor(Date.now() / 1000); + const defaultTopics = [ + { id: 't1', name: 'The Kingdom of God' }, + { id: 't2', name: 'Paradise Earth' }, + { id: 't3', name: 'Life After Death' }, + { id: 't4', name: "The Bible's Reliability" }, + { id: 't5', name: "God's Name (Jehovah)" }, + { id: 't6', name: 'Why Bad Things Happen' }, + { id: 't7', name: 'Jesus Christ' }, + { id: 't8', name: 'The Resurrection' }, + { id: 't9', name: 'Family Happiness' }, + ]; + + for (const topic of defaultTopics) { + await database.runAsync( + 'INSERT OR IGNORE INTO topics (id, name, is_default, created_at) VALUES (?, ?, 1, ?)', + [topic.id, topic.name, now] + ); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..eadf87f --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "territory-log", + "version": "1.0.0", + "main": "expo-router/entry", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web", + "lint": "eslint . --ext .ts,.tsx" + }, + "dependencies": { + "expo": "~52.0.0", + "expo-router": "~4.0.0", + "expo-sqlite": "~15.0.0", + "expo-location": "~18.0.0", + "expo-local-authentication": "~14.0.0", + "expo-camera": "~16.0.0", + "expo-sharing": "~12.0.0", + "expo-document-picker": "~12.0.0", + "expo-file-system": "~18.0.0", + "react": "18.3.2", + "react-native": "0.76.0", + "react-native-safe-area-context": "4.12.0", + "react-native-screens": "~4.0.0", + "react-native-gesture-handler": "~2.20.0", + "react-native-reanimated": "~3.16.0", + "@gorhom/bottom-sheet": "^5.0.0", + "zustand": "^5.0.0", + "nativewind": "^4.0.0", + "lucide-react-native": "^0.460.0", + "react-native-svg": "15.8.0" + }, + "devDependencies": { + "@babel/core": "^7.25.0", + "@types/react": "~18.3.12", + "typescript": "^5.3.0", + "tailwindcss": "^3.4.0", + "eslint": "^8.57.0", + "eslint-config-expo": "~8.0.0", + "prettier": "^3.3.0" + } +} diff --git a/store/useContactStore.ts b/store/useContactStore.ts new file mode 100644 index 0000000..d94998d --- /dev/null +++ b/store/useContactStore.ts @@ -0,0 +1,42 @@ +import { create } from 'zustand'; + +export type ContactCategory = 'Adult' | 'Teenager' | 'Kid'; +export type ContactStatus = 'Active' | 'Return Visit' | 'Bible Study' | 'Not Interested' | 'Do Not Call'; + +export interface Contact { + id: string; + ownerId: string; + fullName: string; + address?: string; + householdCount: number; + gender?: string; + category: ContactCategory; + status: ContactStatus; + tags: string[]; + notes?: string; + territoryCode?: string; + latitude?: number; + longitude?: number; + createdAt: number; + updatedAt: number; +} + +interface ContactStore { + contacts: Contact[]; + setContacts: (contacts: Contact[]) => void; + addContact: (contact: Contact) => void; + updateContact: (id: string, updates: Partial) => void; + removeContact: (id: string) => void; +} + +export const useContactStore = create((set) => ({ + contacts: [], + setContacts: (contacts) => set({ contacts }), + addContact: (contact) => set((state) => ({ contacts: [...state.contacts, contact] })), + updateContact: (id, updates) => + set((state) => ({ + contacts: state.contacts.map((c) => (c.id === id ? { ...c, ...updates } : c)), + })), + removeContact: (id) => + set((state) => ({ contacts: state.contacts.filter((c) => c.id !== id) })), +})); diff --git a/store/useUserStore.ts b/store/useUserStore.ts new file mode 100644 index 0000000..21d8394 --- /dev/null +++ b/store/useUserStore.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; + +export interface User { + id: string; + displayName: string; + shareId: string; +} + +interface UserStore { + user: User | null; + setUser: (user: User) => void; + clearUser: () => void; +} + +export const useUserStore = create((set) => ({ + user: null, + setUser: (user) => set({ user }), + clearUser: () => set({ user: null }), +})); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..511c172 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,19 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'], + presets: [require('nativewind/preset')], + theme: { + extend: { + colors: { + primary: '#1A6B72', + 'primary-light': '#2A8B94', + secondary: '#F8F4EF', + accent: '#D4A843', + success: '#4CAF7D', + danger: '#C0392B', + charcoal: '#2C3E50', + }, + }, + }, + plugins: [], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..140c044 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@/*": ["./*"] + } + } +}