feat: Sprint 0 — project scaffold, DB schema, Zustand stores, base UI components
This commit is contained in:
6
.eslintrc.js
Normal file
6
.eslintrc.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['expo', 'prettier'],
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'warn',
|
||||||
|
},
|
||||||
|
};
|
||||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules/
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
npm-debug.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
*.orig.*
|
||||||
|
web-build/
|
||||||
|
.env
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
53
app.json
Normal file
53
app.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/(tabs)/_layout.tsx
Normal file
43
app/(tabs)/_layout.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Tabs } from 'expo-router';
|
||||||
|
import { Home, Users, Map, Settings } from 'lucide-react-native';
|
||||||
|
|
||||||
|
export default function TabLayout() {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
screenOptions={{
|
||||||
|
tabBarActiveTintColor: '#1A6B72',
|
||||||
|
tabBarInactiveTintColor: '#9CA3AF',
|
||||||
|
headerShown: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: 'Home',
|
||||||
|
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="contacts"
|
||||||
|
options={{
|
||||||
|
title: 'Contacts',
|
||||||
|
tabBarIcon: ({ color, size }) => <Users size={size} color={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="map"
|
||||||
|
options={{
|
||||||
|
title: 'Map',
|
||||||
|
tabBarIcon: ({ color, size }) => <Map size={size} color={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="settings"
|
||||||
|
options={{
|
||||||
|
title: 'Settings',
|
||||||
|
tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
app/(tabs)/contacts.tsx
Normal file
10
app/(tabs)/contacts.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
export default function ContactsScreen() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1 items-center justify-center bg-secondary">
|
||||||
|
<Text className="text-xl text-charcoal">Contacts</Text>
|
||||||
|
<Text className="text-gray-500 mt-2">Coming in Sprint 1</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
app/(tabs)/index.tsx
Normal file
10
app/(tabs)/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
export default function HomeScreen() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1 items-center justify-center bg-secondary">
|
||||||
|
<Text className="text-2xl font-bold text-primary">TerritoryLog</Text>
|
||||||
|
<Text className="text-charcoal mt-2">Dashboard coming in Sprint 2</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
app/(tabs)/map.tsx
Normal file
10
app/(tabs)/map.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
export default function MapScreen() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1 items-center justify-center bg-secondary">
|
||||||
|
<Text className="text-xl text-charcoal">Map</Text>
|
||||||
|
<Text className="text-gray-500 mt-2">Coming in Sprint 4</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
app/(tabs)/settings.tsx
Normal file
10
app/(tabs)/settings.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
export default function SettingsScreen() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1 items-center justify-center bg-secondary">
|
||||||
|
<Text className="text-xl text-charcoal">Settings</Text>
|
||||||
|
<Text className="text-gray-500 mt-2">Coming in Sprint 6</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
app/_layout.tsx
Normal file
11
app/_layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Stack } from 'expo-router';
|
||||||
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
|
import '../global.css';
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
return (
|
||||||
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
|
<Stack screenOptions={{ headerShown: false }} />
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
assets/images/.gitkeep
Normal file
0
assets/images/.gitkeep
Normal file
9
babel.config.js
Normal file
9
babel.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
|
||||||
|
],
|
||||||
|
plugins: ['react-native-reanimated/plugin'],
|
||||||
|
};
|
||||||
|
};
|
||||||
37
components/ui/Button.tsx
Normal file
37
components/ui/Button.tsx
Normal file
@@ -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 (
|
||||||
|
<TouchableOpacity
|
||||||
|
className={`${base} ${variants[variant]} ${disabled || loading ? 'opacity-50' : ''}`}
|
||||||
|
onPress={onPress}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator color={variant === 'secondary' ? '#1A6B72' : 'white'} />
|
||||||
|
) : (
|
||||||
|
<Text className={`font-semibold text-base ${textColors[variant]}`}>{label}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
components/ui/Card.tsx
Normal file
9
components/ui/Card.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { View, ViewProps } from 'react-native';
|
||||||
|
|
||||||
|
export function Card({ children, className = '', ...props }: ViewProps & { className?: string }) {
|
||||||
|
return (
|
||||||
|
<View className={`bg-white rounded-2xl p-4 shadow-sm ${className}`} {...props}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
components/ui/Input.tsx
Normal file
22
components/ui/Input.tsx
Normal file
@@ -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 (
|
||||||
|
<View className="mb-4">
|
||||||
|
{label && <Text className="text-charcoal font-medium mb-1">{label}</Text>}
|
||||||
|
<TextInput
|
||||||
|
className={`border rounded-xl px-4 py-3 text-charcoal bg-white ${
|
||||||
|
error ? 'border-danger' : 'border-gray-200'
|
||||||
|
}`}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && <Text className="text-danger text-sm mt-1">{error}</Text>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
global.css
Normal file
3
global.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
114
lib/database.ts
Normal file
114
lib/database.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import * as SQLite from 'expo-sqlite';
|
||||||
|
|
||||||
|
let db: SQLite.SQLiteDatabase | null = null;
|
||||||
|
|
||||||
|
export async function getDatabase(): Promise<SQLite.SQLiteDatabase> {
|
||||||
|
if (!db) {
|
||||||
|
db = await SQLite.openDatabaseAsync('territorylog.db');
|
||||||
|
await initSchema(db);
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initSchema(database: SQLite.SQLiteDatabase): Promise<void> {
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
package.json
Normal file
43
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
store/useContactStore.ts
Normal file
42
store/useContactStore.ts
Normal file
@@ -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<Contact>) => void;
|
||||||
|
removeContact: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useContactStore = create<ContactStore>((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) })),
|
||||||
|
}));
|
||||||
19
store/useUserStore.ts
Normal file
19
store/useUserStore.ts
Normal file
@@ -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<UserStore>((set) => ({
|
||||||
|
user: null,
|
||||||
|
setUser: (user) => set({ user }),
|
||||||
|
clearUser: () => set({ user: null }),
|
||||||
|
}));
|
||||||
19
tailwind.config.js
Normal file
19
tailwind.config.js
Normal file
@@ -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: [],
|
||||||
|
};
|
||||||
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "expo/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user