feat: finalize app with swagger-based pages, Lucide icons, mobile UI
- Add Lucide React Native icon library and use across tabs and screens
- Mobile-like design: rounded cards (xl/2xl), section dividers, icon chips, chevrons
- New pages from swagger: register, invoices/[id], reports, documents, settings
- Invoice detail: amount, bill to, items, Share/PDF actions (GET /invoices/{id})
- Register screen with link to login (POST /auth/register)
- Reports list with mock data and download (GET /reports)
- Documents list with upload CTA (GET /documents)
- Settings: notifications link, language, about
- Profile: links to Notifications, Reports, Documents, Settings
- Home: invoice rows navigate to /invoices/[id]
- Login ↔ Register navigation
- Keep orange (#ea580c) and dark navbar (#2d2d2d) theme throughout
- README: update screens table with new routes
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b42b3d7602
commit
3b471df8d5
13
README.md
13
README.md
|
|
@ -125,16 +125,21 @@ Use this list to wire the app to the live API. Each item maps to swagger endpoin
|
|||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `/(tabs)` | Bottom tabs: Home, Scan, Proforma, Payments, Profile |
|
||||
| `/(tabs)/index` | Home — earnings summary, quick actions, invoice list |
|
||||
| `/(tabs)/index` | Home — earnings summary, quick actions, invoice list; tap invoice → detail |
|
||||
| `/(tabs)/scan` | Scan invoice — camera placeholder, recent scans |
|
||||
| `/(tabs)/proforma` | Proforma list — create, list requests; tap → detail |
|
||||
| `/(tabs)/payments` | Payments — pending match, reconciled list; tap pending → detail |
|
||||
| `/(tabs)/profile` | Profile — account, notifications link, login, logout |
|
||||
| `/(tabs)/profile` | Profile — account, notifications, reports, documents, settings, login, logout |
|
||||
| `/proforma/[id]` | Proforma request detail — items, send to contacts, submissions |
|
||||
| `/payments/[id]` | Payment detail — associate to invoice |
|
||||
| `/invoices/[id]` | Invoice detail — amount, bill to, items, Share, PDF (swagger: GET /invoices/{id}) |
|
||||
| `/notifications` | Notifications list — link to settings |
|
||||
| `/notifications/settings` | Notification settings — toggles |
|
||||
| `/login` | Sign in — email/password, Google |
|
||||
| `/notifications/settings` | Notification settings — toggles (swagger: GET/PUT /notifications/settings) |
|
||||
| `/reports` | Reports list — monthly reports, download PDF (swagger: GET /reports) |
|
||||
| `/documents` | Documents list — upload, view (swagger: GET /documents) |
|
||||
| `/settings` | App settings — notifications link, language, about |
|
||||
| `/login` | Sign in — email/password, Google; link to register (swagger: POST /auth/login) |
|
||||
| `/register` | Create account (swagger: POST /auth/register) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Tabs } from 'expo-router';
|
||||
import { View } from 'react-native';
|
||||
import { Home, ScanLine, FileText, Wallet, User } from '@/lib/icons';
|
||||
|
||||
const NAV_BG = '#2d2d2d';
|
||||
const ACTIVE_TINT = '#ea580c';
|
||||
|
|
@ -11,11 +11,12 @@ export default function TabsLayout() {
|
|||
screenOptions={{
|
||||
headerStyle: { backgroundColor: NAV_BG },
|
||||
headerTintColor: '#ffffff',
|
||||
headerTitleStyle: { fontWeight: '600' },
|
||||
tabBarStyle: { backgroundColor: NAV_BG },
|
||||
headerTitleStyle: { fontWeight: '600', fontSize: 18 },
|
||||
tabBarStyle: { backgroundColor: NAV_BG, paddingTop: 8 },
|
||||
tabBarActiveTintColor: ACTIVE_TINT,
|
||||
tabBarInactiveTintColor: INACTIVE_TINT,
|
||||
tabBarLabelStyle: { fontSize: 12 },
|
||||
tabBarLabelStyle: { fontSize: 11 },
|
||||
tabBarShowLabel: true,
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
|
|
@ -23,6 +24,7 @@ export default function TabsLayout() {
|
|||
options={{
|
||||
title: 'Home',
|
||||
tabBarLabel: 'Home',
|
||||
tabBarIcon: ({ color, size }) => <Home color={color} size={size ?? 22} strokeWidth={2} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
|
|
@ -30,6 +32,7 @@ export default function TabsLayout() {
|
|||
options={{
|
||||
title: 'Scan Invoice',
|
||||
tabBarLabel: 'Scan',
|
||||
tabBarIcon: ({ color, size }) => <ScanLine color={color} size={size ?? 22} strokeWidth={2} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
|
|
@ -37,6 +40,7 @@ export default function TabsLayout() {
|
|||
options={{
|
||||
title: 'Proforma',
|
||||
tabBarLabel: 'Proforma',
|
||||
tabBarIcon: ({ color, size }) => <FileText color={color} size={size ?? 22} strokeWidth={2} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
|
|
@ -44,6 +48,7 @@ export default function TabsLayout() {
|
|||
options={{
|
||||
title: 'Payments',
|
||||
tabBarLabel: 'Payments',
|
||||
tabBarIcon: ({ color, size }) => <Wallet color={color} size={size ?? 22} strokeWidth={2} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
|
|
@ -51,6 +56,7 @@ export default function TabsLayout() {
|
|||
options={{
|
||||
title: 'Profile',
|
||||
tabBarLabel: 'Profile',
|
||||
tabBarIcon: ({ color, size }) => <User color={color} size={size ?? 22} strokeWidth={2} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { View, ScrollView, Pressable } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { EARNINGS_SUMMARY, MOCK_INVOICES, MOCK_USER } from '@/lib/mock-data';
|
||||
import { router } from 'expo-router';
|
||||
import { Camera, Send, ChevronRight, Wallet, DollarSign, Clock } from '@/lib/icons';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
const statusColor: Record<string, string> = {
|
||||
Waiting: 'bg-amber-500/20 text-amber-700',
|
||||
Paid: 'bg-emerald-500/20 text-emerald-700',
|
||||
|
|
@ -14,88 +16,131 @@ const statusColor: Record<string, string> = {
|
|||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-[#f5f5f5]" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<View className="mb-4">
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="mb-5">
|
||||
<Text className="text-2xl font-bold text-gray-900">Hi {MOCK_USER.name},</Text>
|
||||
<Text className="text-muted-foreground mt-1">Take a look at your last activity.</Text>
|
||||
<Text className="text-muted-foreground mt-1 text-base">Take a look at your last activity.</Text>
|
||||
</View>
|
||||
|
||||
<Card className="mb-4 overflow-hidden border-0">
|
||||
<View className="bg-primary/10 p-4">
|
||||
<Card className="mb-5 overflow-hidden rounded-2xl border-0 shadow-sm">
|
||||
<View className="bg-primary/10 px-5 py-5">
|
||||
<Text className="text-muted-foreground text-sm">Earnings balance</Text>
|
||||
<Text className="text-2xl font-bold text-gray-900">${EARNINGS_SUMMARY.balance.toLocaleString()}</Text>
|
||||
<Text className="mt-1 text-3xl font-bold text-gray-900">${EARNINGS_SUMMARY.balance.toLocaleString()}</Text>
|
||||
</View>
|
||||
<CardContent className="flex-row border-t border-border">
|
||||
<Pressable className="flex-1 py-3" onPress={() => router.push('/(tabs)/payments')}>
|
||||
<View className="flex-row border-t border-border">
|
||||
<Pressable
|
||||
className="flex-1 flex-row items-center gap-3 px-5 py-4"
|
||||
onPress={() => router.push('/(tabs)/payments')}
|
||||
>
|
||||
<View className="rounded-xl bg-primary/15 p-2">
|
||||
<Clock color={PRIMARY} size={20} strokeWidth={2} />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-muted-foreground text-xs">Waiting for pay</Text>
|
||||
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.waitingAmount.toLocaleString()}</Text>
|
||||
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.waitingCount} Waiting invoice</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
<View className="w-px bg-border" />
|
||||
<Pressable className="flex-1 py-3">
|
||||
<Pressable className="flex-1 flex-row items-center gap-3 px-5 py-4">
|
||||
<View className="rounded-xl bg-emerald-500/15 p-2">
|
||||
<DollarSign color="#059669" size={20} strokeWidth={2} />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-muted-foreground text-xs">Paid this month</Text>
|
||||
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.paidThisMonth.toLocaleString()}</Text>
|
||||
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.paidCount} Paid invoice</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
</CardContent>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
<View className="mb-4 flex-row gap-3">
|
||||
<Button className="flex-1 bg-primary" onPress={() => router.push('/(tabs)/scan')}>
|
||||
<Text className="text-primary-foreground font-medium">Scan invoice</Text>
|
||||
<View className="mb-5 flex-row gap-3">
|
||||
<Button className="min-h-12 flex-1 rounded-xl bg-primary" onPress={() => router.push('/(tabs)/scan')}>
|
||||
<Camera color="#ffffff" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 text-primary-foreground font-medium">Scan invoice</Text>
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onPress={() => router.push('/(tabs)/proforma')}>
|
||||
<Text className="font-medium">Send proforma</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="min-h-12 flex-1 rounded-xl border-border"
|
||||
onPress={() => router.push('/(tabs)/proforma')}
|
||||
>
|
||||
<Send color={PRIMARY} size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-gray-700">Send proforma</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="mb-2 flex-row gap-2">
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="-mx-1 mb-4">
|
||||
<View className="flex-row gap-2 px-1">
|
||||
{['All', 'Draft', 'Waiting', 'Paid', 'Unpaid'].map((filter) => (
|
||||
<View
|
||||
<Pressable
|
||||
key={filter}
|
||||
className={`rounded-full px-3 py-1.5 ${filter === 'Waiting' ? 'bg-primary' : 'bg-muted'}`}
|
||||
className={`rounded-full px-4 py-2.5 ${filter === 'Waiting' ? 'bg-primary' : 'bg-white'} border border-border`}
|
||||
>
|
||||
<Text
|
||||
className={
|
||||
filter === 'Waiting' ? 'text-primary-foreground text-sm font-medium' : 'text-muted-foreground text-sm'
|
||||
}
|
||||
>
|
||||
<Text className={filter === 'Waiting' ? 'text-primary-foreground text-sm font-medium' : 'text-muted-foreground text-sm'}>
|
||||
{filter}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<Text className="text-muted-foreground mb-2 text-sm">Today</Text>
|
||||
<View className="mb-2 flex-row items-center gap-2">
|
||||
<View className="h-px flex-1 bg-border" />
|
||||
<Text className="text-muted-foreground text-xs font-medium">Today</Text>
|
||||
<View className="h-px flex-1 bg-border" />
|
||||
</View>
|
||||
{MOCK_INVOICES.filter((i) => i.status === 'Waiting').map((inv) => (
|
||||
<Card key={inv.id} className="mb-2">
|
||||
<CardContent className="flex-row items-center justify-between py-3">
|
||||
<Pressable key={inv.id} onPress={() => router.push(`/invoices/${inv.id}`)}>
|
||||
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardContent className="flex-row items-center justify-between py-4 pl-4 pr-3">
|
||||
<View className="flex-1">
|
||||
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
|
||||
<Text className="text-muted-foreground text-sm">Invoice #{inv.invoiceNumber} - Due {inv.dueDate}</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-sm">Invoice #{inv.invoiceNumber} · Due {inv.dueDate}</Text>
|
||||
</View>
|
||||
<View className="items-end">
|
||||
<View className="items-end gap-1">
|
||||
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
|
||||
<View className={`mt-1 rounded-full px-2 py-0.5 ${statusColor[inv.status]}`}>
|
||||
<View className={`rounded-full px-2.5 py-1 ${statusColor[inv.status]}`}>
|
||||
<Text className="text-xs font-medium">{inv.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Pressable>
|
||||
))}
|
||||
|
||||
<Text className="text-muted-foreground mb-2 mt-4 text-sm">Yesterday</Text>
|
||||
<View className="mb-2 mt-6 flex-row items-center gap-2">
|
||||
<View className="h-px flex-1 bg-border" />
|
||||
<Text className="text-muted-foreground text-xs font-medium">Yesterday</Text>
|
||||
<View className="h-px flex-1 bg-border" />
|
||||
</View>
|
||||
{MOCK_INVOICES.filter((i) => i.status === 'Paid').map((inv) => (
|
||||
<Card key={inv.id} className="mb-2">
|
||||
<CardContent className="flex-row items-center justify-between py-3">
|
||||
<Pressable key={inv.id} onPress={() => router.push(`/invoices/${inv.id}`)}>
|
||||
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardContent className="flex-row items-center justify-between py-4 pl-4 pr-3">
|
||||
<View className="flex-1">
|
||||
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
|
||||
<Text className="text-muted-foreground text-sm">Invoice #{inv.invoiceNumber} - {inv.dueDate}</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-sm">Invoice #{inv.invoiceNumber} · {inv.dueDate}</Text>
|
||||
</View>
|
||||
<View className="items-end">
|
||||
<View className="items-end gap-1">
|
||||
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
|
||||
<View className={`mt-1 rounded-full px-2 py-0.5 ${statusColor[inv.status]}`}>
|
||||
<View className={`rounded-full px-2.5 py-1 ${statusColor[inv.status]}`}>
|
||||
<Text className="text-xs font-medium">{inv.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,53 +4,70 @@ import { Text } from '@/components/ui/text';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { MOCK_PAYMENTS } from '@/lib/mock-data';
|
||||
import { ScanLine, Link2, CheckCircle2, Wallet, ChevronRight } from '@/lib/icons';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
|
||||
export default function PaymentsScreen() {
|
||||
const matched = MOCK_PAYMENTS.filter((p) => p.matched);
|
||||
const pending = MOCK_PAYMENTS.filter((p) => !p.matched);
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Text className="text-muted-foreground mb-4">
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text className="text-muted-foreground mb-5 text-base">
|
||||
Match payment SMS (e.g. bank or Telebirr) to invoices for quick reconciliation.
|
||||
</Text>
|
||||
|
||||
<Button className="mb-4 bg-primary">
|
||||
<Text className="text-primary-foreground font-medium">Scan SMS now</Text>
|
||||
<Button className="mb-5 min-h-12 rounded-xl bg-primary">
|
||||
<ScanLine color="#ffffff" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 text-primary-foreground font-medium">Scan SMS now</Text>
|
||||
</Button>
|
||||
|
||||
<Text className="text-muted-foreground mb-2 text-sm">Pending match</Text>
|
||||
<View className="mb-3 flex-row items-center gap-2">
|
||||
<Link2 color="#71717a" size={18} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground text-sm font-medium">Pending match</Text>
|
||||
</View>
|
||||
{pending.map((pay) => (
|
||||
<Card key={pay.id} className="mb-2 border-amber-500/30">
|
||||
<CardContent className="py-3">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View>
|
||||
<Card key={pay.id} className="mb-2.5 overflow-hidden rounded-xl border-2 border-amber-500/30 bg-white">
|
||||
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
|
||||
<View className="mr-3 rounded-xl bg-primary/10 p-2">
|
||||
<Wallet color={PRIMARY} size={22} strokeWidth={2} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
|
||||
<Text className="text-muted-foreground text-sm">{pay.source} · {pay.date}</Text>
|
||||
</View>
|
||||
<Button variant="outline" size="sm" onPress={() => router.push(`/payments/${pay.id}`)}>
|
||||
<Text className="font-medium">Match to invoice</Text>
|
||||
<Button variant="outline" size="sm" className="rounded-lg" onPress={() => router.push(`/payments/${pay.id}`)}>
|
||||
<Text className="font-medium">Match</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Text className="text-muted-foreground mb-2 mt-4 text-sm">Reconciled</Text>
|
||||
<View className="mb-3 mt-6 flex-row items-center gap-2">
|
||||
<CheckCircle2 color="#059669" size={18} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground text-sm font-medium">Reconciled</Text>
|
||||
</View>
|
||||
{matched.map((pay) => (
|
||||
<Card key={pay.id} className="mb-2">
|
||||
<CardContent className="py-3">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View>
|
||||
<Card key={pay.id} className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
|
||||
<View className="mr-3 rounded-xl bg-emerald-500/15 p-2">
|
||||
<CheckCircle2 color="#059669" size={22} strokeWidth={2} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
|
||||
<Text className="text-muted-foreground text-sm">
|
||||
{pay.source} · {pay.date} {pay.reference && `· ${pay.reference}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="rounded-full bg-emerald-500/20 px-2 py-0.5">
|
||||
<View className="rounded-full bg-emerald-500/20 px-2.5 py-1">
|
||||
<Text className="text-xs font-medium text-emerald-700">Matched</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -4,54 +4,115 @@ import { Text } from '@/components/ui/text';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { MOCK_USER } from '@/lib/mock-data';
|
||||
import { User, Mail, Globe, Bell, ChevronRight, Info, LogOut, LogIn, FileText, FolderOpen, Settings } from '@/lib/icons';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
|
||||
export default function ProfileScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="mb-6 items-center">
|
||||
<View className="mb-3 h-20 w-20 items-center justify-center rounded-full bg-primary">
|
||||
<Text className="text-3xl font-bold text-primary-foreground">{MOCK_USER.name[0]}</Text>
|
||||
</View>
|
||||
<Text className="text-xl font-semibold text-gray-900">{MOCK_USER.name}</Text>
|
||||
<Text className="text-muted-foreground text-sm">{MOCK_USER.email}</Text>
|
||||
<Text className="text-muted-foreground mt-1 text-sm">{MOCK_USER.email}</Text>
|
||||
</View>
|
||||
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Account</CardTitle>
|
||||
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Account</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-2">
|
||||
<View className="flex-row justify-between py-2">
|
||||
<CardContent className="gap-0">
|
||||
<View className="flex-row items-center justify-between border-b border-border py-3">
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Mail color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Email</Text>
|
||||
</View>
|
||||
<Text className="text-gray-900">{MOCK_USER.email}</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between py-2">
|
||||
<View className="flex-row items-center justify-between border-b border-border py-3">
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Globe color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Language</Text>
|
||||
</View>
|
||||
<Text className="text-gray-900">English</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => router.push('/notifications')} className="flex-row justify-between py-2">
|
||||
<Pressable
|
||||
onPress={() => router.push('/notifications')}
|
||||
className="flex-row items-center justify-between border-b border-border py-3"
|
||||
>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Bell color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Notifications</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-1">
|
||||
<Text className="text-primary font-medium">Manage</Text>
|
||||
<ChevronRight color={PRIMARY} size={18} strokeWidth={2} />
|
||||
</View>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => router.push('/reports')}
|
||||
className="flex-row items-center justify-between border-b border-border py-3"
|
||||
>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<FileText color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Reports</Text>
|
||||
</View>
|
||||
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => router.push('/documents')}
|
||||
className="flex-row items-center justify-between border-b border-border py-3"
|
||||
>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<FolderOpen color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Documents</Text>
|
||||
</View>
|
||||
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => router.push('/settings')}
|
||||
className="flex-row items-center justify-between py-3"
|
||||
>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Settings color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Settings</Text>
|
||||
</View>
|
||||
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
|
||||
</Pressable>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>About</CardTitle>
|
||||
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardHeader className="pb-2">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Info color="#71717a" size={18} strokeWidth={2} />
|
||||
<CardTitle className="text-base">About</CardTitle>
|
||||
</View>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="text-muted-foreground text-sm">
|
||||
<Text className="text-muted-foreground text-sm leading-5">
|
||||
Yaltopia Tickets App — Scan. Send. Reconcile. Companion to the Yaltopia Tickets web app.
|
||||
</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button variant="outline" className="mt-2" onPress={() => router.push('/login')}>
|
||||
<Text className="font-medium">Sign in (different account)</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-2 min-h-12 rounded-xl border-border"
|
||||
onPress={() => router.push('/login')}
|
||||
>
|
||||
<LogIn color={PRIMARY} size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-gray-700">Sign in (different account)</Text>
|
||||
</Button>
|
||||
<Button variant="destructive" className="mt-2">
|
||||
<Text className="font-medium">Log out</Text>
|
||||
<Button variant="destructive" className="mt-3 min-h-12 rounded-xl">
|
||||
<LogOut color="#ffffff" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium">Log out</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,32 +4,48 @@ import { Button } from '@/components/ui/button';
|
|||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
|
||||
import { MOCK_PROFORMA } from '@/lib/mock-data';
|
||||
import { router } from 'expo-router';
|
||||
import { Plus, Send, FileText, ChevronRight, Calendar } from '@/lib/icons';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
|
||||
export default function ProformaScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Text className="text-muted-foreground mb-4">
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text className="text-muted-foreground mb-5 text-base">
|
||||
Create or select proforma requests and share with contacts via email or SMS.
|
||||
</Text>
|
||||
|
||||
<Button className="mb-4 bg-primary" onPress={() => {}}>
|
||||
<Text className="text-primary-foreground font-medium">Create new proforma</Text>
|
||||
<Button className="mb-5 min-h-12 rounded-xl bg-primary" onPress={() => {}}>
|
||||
<Plus color="#ffffff" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 text-primary-foreground font-medium">Create new proforma</Text>
|
||||
</Button>
|
||||
|
||||
<Text className="text-muted-foreground mb-2 text-sm">Your proforma requests</Text>
|
||||
<View className="mb-3 flex-row items-center gap-2">
|
||||
<FileText color="#71717a" size={18} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground text-sm font-medium">Your proforma requests</Text>
|
||||
</View>
|
||||
{MOCK_PROFORMA.map((pf) => (
|
||||
<Pressable key={pf.id} onPress={() => router.push(`/proforma/${pf.id}`)}>
|
||||
<Card className="mb-3">
|
||||
<CardHeader>
|
||||
<CardTitle>{pf.title}</CardTitle>
|
||||
<CardDescription>{pf.description}</CardDescription>
|
||||
<Text className="text-muted-foreground mt-1 text-xs">Deadline: {pf.deadline} · {pf.itemCount} items</Text>
|
||||
<Card className="mb-3 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">{pf.title}</CardTitle>
|
||||
<CardDescription className="mt-0.5">{pf.description}</CardDescription>
|
||||
<View className="mt-2 flex-row items-center gap-1.5">
|
||||
<Calendar color="#71717a" size={14} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground text-xs">Deadline {pf.deadline} · {pf.itemCount} items</Text>
|
||||
</View>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-row justify-between">
|
||||
<CardFooter className="flex-row items-center justify-between border-t border-border pt-3">
|
||||
<Text className="text-muted-foreground text-sm">Sent to {pf.sentCount} contacts</Text>
|
||||
<Button variant="outline" size="sm" onPress={() => router.push(`/proforma/${pf.id}`)}>
|
||||
<Text className="font-medium">Send to contacts</Text>
|
||||
</Button>
|
||||
<View className="flex-row items-center gap-1.5">
|
||||
<Send color={PRIMARY} size={16} strokeWidth={2} />
|
||||
<Text className="text-primary font-medium text-sm">Send to contacts</Text>
|
||||
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
|
||||
</View>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Pressable>
|
||||
|
|
|
|||
|
|
@ -1,55 +1,64 @@
|
|||
import { View, ScrollView } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Camera, FileText, ChevronRight } from '@/lib/icons';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
|
||||
export default function ScanScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Text className="text-muted-foreground mb-4">
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text className="text-muted-foreground mb-5 text-base">
|
||||
Capture paper or digital invoices with your camera. We'll extract vendor, amount, date, and line items.
|
||||
</Text>
|
||||
|
||||
<Card className="mb-4 border-2 border-dashed border-border">
|
||||
<CardContent className="items-center justify-center py-16">
|
||||
<View className="mb-4 h-20 w-20 items-center justify-center rounded-full bg-primary/10">
|
||||
<Text className="text-4xl">📷</Text>
|
||||
<Card className="mb-5 overflow-hidden rounded-2xl border-2 border-dashed border-border bg-white">
|
||||
<CardContent className="items-center justify-center py-14">
|
||||
<View className="mb-5 h-24 w-24 items-center justify-center rounded-full bg-primary/10">
|
||||
<Camera color={PRIMARY} size={40} strokeWidth={2} />
|
||||
</View>
|
||||
<Text className="mb-2 text-center text-lg font-semibold text-gray-900">Scan invoice</Text>
|
||||
<Text className="text-muted-foreground mb-4 text-center text-sm">
|
||||
<Text className="text-muted-foreground mb-6 text-center text-sm">
|
||||
Tap below to open camera and capture an invoice
|
||||
</Text>
|
||||
<Button className="bg-primary min-w-[200]">
|
||||
<Text className="text-primary-foreground font-medium">Open camera</Text>
|
||||
<Button className="min-h-12 rounded-xl bg-primary px-8">
|
||||
<Camera color="#ffffff" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 text-primary-foreground font-medium">Open camera</Text>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Text className="text-muted-foreground mb-2 text-sm">Recent scans</Text>
|
||||
<Card className="mb-2">
|
||||
<CardContent className="py-3">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View>
|
||||
<Text className="font-medium text-gray-900">Acme Corp - Invoice #101</Text>
|
||||
<Text className="text-muted-foreground text-sm">Scanned Sep 12, 2022 · $1,240</Text>
|
||||
<View className="mb-3 flex-row items-center gap-2">
|
||||
<FileText color="#71717a" size={18} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground text-sm font-medium">Recent scans</Text>
|
||||
</View>
|
||||
<View className="rounded-full bg-amber-500/20 px-2 py-0.5">
|
||||
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-900">Acme Corp - Invoice #101</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-sm">Scanned Sep 12, 2022 · $1,240</Text>
|
||||
</View>
|
||||
<View className="rounded-full bg-amber-500/20 px-2.5 py-1">
|
||||
<Text className="text-xs font-medium text-amber-700">Pending</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mb-2">
|
||||
<CardContent className="py-3">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View>
|
||||
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-900">Tech Supplies Ltd - Invoice #88</Text>
|
||||
<Text className="text-muted-foreground text-sm">Scanned Sep 11, 2022 · $890</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-sm">Scanned Sep 11, 2022 · $890</Text>
|
||||
</View>
|
||||
<View className="rounded-full bg-emerald-500/20 px-2 py-0.5">
|
||||
<View className="rounded-full bg-emerald-500/20 px-2.5 py-1">
|
||||
<Text className="text-xs font-medium text-emerald-700">Saved</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ export default function RootLayout() {
|
|||
<Stack.Screen name="notifications" options={{ title: 'Notifications' }} />
|
||||
<Stack.Screen name="notifications/settings" options={{ title: 'Notification settings' }} />
|
||||
<Stack.Screen name="login" options={{ title: 'Sign in', headerShown: false }} />
|
||||
<Stack.Screen name="register" options={{ title: 'Create account', headerShown: false }} />
|
||||
<Stack.Screen name="invoices/[id]" options={{ title: 'Invoice' }} />
|
||||
<Stack.Screen name="reports" options={{ title: 'Reports' }} />
|
||||
<Stack.Screen name="documents" options={{ title: 'Documents' }} />
|
||||
<Stack.Screen name="settings" options={{ title: 'Settings' }} />
|
||||
</Stack>
|
||||
<PortalHost />
|
||||
</View>
|
||||
|
|
|
|||
53
app/documents/index.tsx
Normal file
53
app/documents/index.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { View, ScrollView, Pressable } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { FileText, ChevronRight, FolderOpen, Upload } from '@/lib/icons';
|
||||
import { MOCK_DOCUMENTS } from '@/lib/mock-data';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
|
||||
export default function DocumentsScreen() {
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="mb-4 flex-row items-center gap-2">
|
||||
<FolderOpen color="#18181b" size={22} strokeWidth={2} />
|
||||
<Text className="text-xl font-semibold text-gray-900">Documents</Text>
|
||||
</View>
|
||||
<Text className="text-muted-foreground mb-5 text-sm">
|
||||
Uploaded invoices, scans, and attachments. Synced with your account.
|
||||
</Text>
|
||||
|
||||
<Button variant="outline" className="mb-5 min-h-12 rounded-xl border-border" onPress={() => {}}>
|
||||
<Upload color={PRIMARY} size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-gray-700">Upload document</Text>
|
||||
</Button>
|
||||
|
||||
{MOCK_DOCUMENTS.map((d) => (
|
||||
<Card key={d.id} className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<Pressable>
|
||||
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
|
||||
<View className="mr-3 rounded-xl bg-primary/10 p-2">
|
||||
<FileText color={PRIMARY} size={22} strokeWidth={2} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-900" numberOfLines={1}>{d.name}</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-sm">{d.size} · {d.uploadedAt}</Text>
|
||||
</View>
|
||||
<ChevronRight color="#71717a" size={20} strokeWidth={2} />
|
||||
</CardContent>
|
||||
</Pressable>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Button variant="outline" className="mt-4 rounded-xl border-border" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
100
app/invoices/[id].tsx
Normal file
100
app/invoices/[id].tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { View, ScrollView } from 'react-native';
|
||||
import { useLocalSearchParams, router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { FileText, Calendar, User, Share2, Download, ChevronRight } from '@/lib/icons';
|
||||
import { MOCK_INVOICES } from '@/lib/mock-data';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
const MOCK_ITEMS = [
|
||||
{ description: 'Marketing Landing Page Package', qty: 1, unitPrice: 1000, total: 1000 },
|
||||
{ description: 'Instagram Post Initial Design', qty: 4, unitPrice: 100, total: 400 },
|
||||
];
|
||||
|
||||
export default function InvoiceDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const invoice = MOCK_INVOICES.find((i) => i.id === id);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardContent className="p-5">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<FileText color={PRIMARY} size={22} strokeWidth={2} />
|
||||
<Text className="font-semibold text-gray-900">Invoice #{invoice?.invoiceNumber ?? id}</Text>
|
||||
</View>
|
||||
<View className="rounded-full bg-amber-500/20 px-2.5 py-1">
|
||||
<Text className="text-xs font-medium text-amber-700">{invoice?.status ?? 'Waiting'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-muted-foreground mt-2 text-sm">Amount due</Text>
|
||||
<Text className="mt-1 text-2xl font-bold text-gray-900">${invoice?.amount.toLocaleString() ?? '—'}</Text>
|
||||
<View className="mt-3 flex-row gap-4">
|
||||
<View className="flex-row items-center gap-1.5">
|
||||
<Calendar color="#71717a" size={16} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground text-sm">Due {invoice?.dueDate ?? '—'}</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-1.5">
|
||||
<Calendar color="#71717a" size={16} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground text-sm">Issued {invoice?.createdAt ?? '—'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardHeader className="pb-2">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<User color="#71717a" size={18} strokeWidth={2} />
|
||||
<CardTitle className="text-base">Bill to</CardTitle>
|
||||
</View>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="font-medium text-gray-900">{invoice?.recipient ?? '—'}</Text>
|
||||
<Text className="text-muted-foreground text-sm">{invoice?.recipientEmail ?? '—'}</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Items</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-2">
|
||||
{MOCK_ITEMS.map((item, i) => (
|
||||
<View key={i} className="flex-row justify-between border-b border-border py-2 last:border-0">
|
||||
<Text className="text-gray-700">{item.description} × {item.qty}</Text>
|
||||
<Text className="font-medium text-gray-900">${item.total.toLocaleString()}</Text>
|
||||
</View>
|
||||
))}
|
||||
<View className="mt-2 border-t border-border pt-3">
|
||||
<View className="flex-row justify-between">
|
||||
<Text className="font-semibold text-gray-900">Total</Text>
|
||||
<Text className="font-semibold text-gray-900">${invoice?.amount.toLocaleString() ?? '1,540'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<View className="flex-row gap-3">
|
||||
<Button variant="outline" className="min-h-12 flex-1 rounded-xl border-border" onPress={() => {}}>
|
||||
<Share2 color={PRIMARY} size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-gray-700">Share</Text>
|
||||
</Button>
|
||||
<Button variant="outline" className="min-h-12 flex-1 rounded-xl border-border" onPress={() => {}}>
|
||||
<Download color={PRIMARY} size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-gray-700">PDF</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<Button variant="ghost" className="mt-4 rounded-xl" onPress={() => router.back()}>
|
||||
<ChevronRight className="rotate-180" color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-muted-foreground">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,29 +1,39 @@
|
|||
import { View, ScrollView } from 'react-native';
|
||||
import { View, ScrollView, Pressable } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Mail, ArrowLeft } from '@/lib/icons';
|
||||
|
||||
export default function LoginScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingVertical: 48 }}>
|
||||
<Text className="mb-6 text-center text-2xl font-bold text-gray-900">Yaltopia Tickets</Text>
|
||||
<Card className="mb-4">
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 24, paddingVertical: 48 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text className="mb-8 text-center text-2xl font-bold text-gray-900">Yaltopia Tickets</Text>
|
||||
<Card className="mb-5 overflow-hidden rounded-2xl border border-border bg-white">
|
||||
<CardHeader>
|
||||
<CardTitle>Sign in</CardTitle>
|
||||
<CardDescription>Use the same account as the web app.</CardDescription>
|
||||
<CardTitle className="text-lg">Sign in</CardTitle>
|
||||
<CardDescription className="mt-1">Use the same account as the web app.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-3">
|
||||
<Button className="bg-primary">
|
||||
<Text className="text-primary-foreground font-medium">Email & password</Text>
|
||||
<Button className="min-h-12 rounded-xl bg-primary">
|
||||
<Mail color="#ffffff" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 text-primary-foreground font-medium">Email & password</Text>
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Text className="font-medium">Continue with Google</Text>
|
||||
<Button variant="outline" className="min-h-12 rounded-xl border-border">
|
||||
<Text className="font-medium text-gray-700">Continue with Google</Text>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button variant="ghost" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back</Text>
|
||||
<Pressable onPress={() => router.push('/register')} className="mt-4">
|
||||
<Text className="text-center text-primary font-medium">Create account</Text>
|
||||
</Pressable>
|
||||
<Button variant="ghost" className="mt-4 rounded-xl" onPress={() => router.back()}>
|
||||
<ArrowLeft color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-muted-foreground">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { View, ScrollView, Pressable } from 'react-native';
|
|||
import { router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Bell, Settings, ChevronRight } from '@/lib/icons';
|
||||
|
||||
const MOCK_NOTIFICATIONS = [
|
||||
{ id: '1', title: 'Invoice reminder', body: 'Invoice #2 to Robin Murray is due in 2 days.', time: '2h ago', read: false },
|
||||
|
|
@ -13,8 +14,12 @@ export default function NotificationsScreen() {
|
|||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<View className="mb-4 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Bell color="#18181b" size={22} strokeWidth={2} />
|
||||
<Text className="text-xl font-semibold text-gray-900">Notifications</Text>
|
||||
<Pressable onPress={() => router.push('/notifications/settings')}>
|
||||
</View>
|
||||
<Pressable className="flex-row items-center gap-1" onPress={() => router.push('/notifications/settings')}>
|
||||
<Settings color="#ea580c" size={18} strokeWidth={2} />
|
||||
<Text className="text-primary font-medium">Settings</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
|
|
|||
40
app/register.tsx
Normal file
40
app/register.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { View, ScrollView, Pressable } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Mail, ArrowLeft, UserPlus } from '@/lib/icons';
|
||||
|
||||
export default function RegisterScreen() {
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 24, paddingVertical: 48 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text className="mb-8 text-center text-2xl font-bold text-gray-900">Yaltopia Tickets</Text>
|
||||
<Card className="mb-5 overflow-hidden rounded-2xl border border-border bg-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Create account</CardTitle>
|
||||
<CardDescription className="mt-1">Register with the same account format as the web app.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-3">
|
||||
<Button className="min-h-12 rounded-xl bg-primary">
|
||||
<UserPlus color="#ffffff" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 text-primary-foreground font-medium">Email & password</Text>
|
||||
</Button>
|
||||
<Button variant="outline" className="min-h-12 rounded-xl border-border">
|
||||
<Text className="font-medium text-gray-700">Continue with Google</Text>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Pressable onPress={() => router.push('/login')} className="mt-2">
|
||||
<Text className="text-center text-primary font-medium">Already have an account? Sign in</Text>
|
||||
</Pressable>
|
||||
<Button variant="ghost" className="mt-4 rounded-xl" onPress={() => router.back()}>
|
||||
<ArrowLeft color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="ml-2 font-medium text-muted-foreground">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
54
app/reports/index.tsx
Normal file
54
app/reports/index.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { View, ScrollView, Pressable } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { FileText, Download, ChevronRight, BarChart3 } from '@/lib/icons';
|
||||
import { MOCK_REPORTS } from '@/lib/mock-data';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
|
||||
export default function ReportsScreen() {
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="mb-4 flex-row items-center gap-2">
|
||||
<BarChart3 color="#18181b" size={22} strokeWidth={2} />
|
||||
<Text className="text-xl font-semibold text-gray-900">Reports</Text>
|
||||
</View>
|
||||
<Text className="text-muted-foreground mb-5 text-sm">
|
||||
Monthly reports and PDF exports. Generate from the web app or view here.
|
||||
</Text>
|
||||
|
||||
{MOCK_REPORTS.map((r) => (
|
||||
<Card key={r.id} className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<Pressable>
|
||||
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
|
||||
<View className="mr-3 rounded-xl bg-primary/10 p-2">
|
||||
<FileText color={PRIMARY} size={22} strokeWidth={2} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="font-semibold text-gray-900">{r.title}</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-sm">{r.period}</Text>
|
||||
<Text className="text-muted-foreground mt-0.5 text-xs">Generated {r.generatedAt}</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Pressable className="rounded-lg bg-primary/10 p-2">
|
||||
<Download color={PRIMARY} size={18} strokeWidth={2} />
|
||||
</Pressable>
|
||||
<ChevronRight color="#71717a" size={20} strokeWidth={2} />
|
||||
</View>
|
||||
</CardContent>
|
||||
</Pressable>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Button variant="outline" className="mt-4 rounded-xl border-border" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
72
app/settings.tsx
Normal file
72
app/settings.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { View, ScrollView, Pressable } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Settings, Bell, Globe, ChevronRight, Info } from '@/lib/icons';
|
||||
|
||||
const PRIMARY = '#ea580c';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-[#f5f5f5]"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="mb-5 flex-row items-center gap-2">
|
||||
<Settings color="#18181b" size={22} strokeWidth={2} />
|
||||
<Text className="text-xl font-semibold text-gray-900">Settings</Text>
|
||||
</View>
|
||||
|
||||
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Preferences</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-0">
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between border-b border-border py-3"
|
||||
onPress={() => router.push('/notifications/settings')}
|
||||
>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Bell color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Notifications</Text>
|
||||
</View>
|
||||
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
|
||||
</Pressable>
|
||||
<View className="flex-row items-center justify-between py-3">
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Globe color="#71717a" size={20} strokeWidth={2} />
|
||||
<Text className="text-muted-foreground">Language</Text>
|
||||
</View>
|
||||
<Text className="text-gray-900">English</Text>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
|
||||
<CardHeader className="pb-2">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Info color="#71717a" size={18} strokeWidth={2} />
|
||||
<CardTitle className="text-base">About</CardTitle>
|
||||
</View>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="text-muted-foreground text-sm leading-5">
|
||||
Yaltopia Tickets App v1.0 — Scan. Send. Reconcile.
|
||||
</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<View className="rounded-xl border border-border bg-white p-4">
|
||||
<Text className="text-muted-foreground text-xs">
|
||||
API: Invoices, Proforma, Payments, Reports, Documents, Notifications — see swagger.json and README for integration.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button variant="outline" className="mt-6 rounded-xl border-border" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
37
lib/icons.tsx
Normal file
37
lib/icons.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Re-export Lucide icons for consistent use. Use these with color="#ea580c" for primary, "#ffffff" on dark bar.
|
||||
*/
|
||||
export {
|
||||
Home,
|
||||
ScanLine,
|
||||
FileText,
|
||||
Wallet,
|
||||
User,
|
||||
Camera,
|
||||
Send,
|
||||
ChevronRight,
|
||||
Bell,
|
||||
Settings,
|
||||
LogOut,
|
||||
LogIn,
|
||||
Plus,
|
||||
Link2,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Copy,
|
||||
Calendar,
|
||||
Menu,
|
||||
ArrowLeft,
|
||||
MoreVertical,
|
||||
AlertCircle,
|
||||
DollarSign,
|
||||
Mail,
|
||||
Globe,
|
||||
Info,
|
||||
FolderOpen,
|
||||
Share2,
|
||||
Download,
|
||||
BarChart3,
|
||||
Upload,
|
||||
UserPlus,
|
||||
} from 'lucide-react-native';
|
||||
|
|
@ -92,3 +92,13 @@ export const EARNINGS_SUMMARY = {
|
|||
paidThisMonth: 4120,
|
||||
paidCount: 4,
|
||||
};
|
||||
|
||||
export const MOCK_REPORTS = [
|
||||
{ id: 'r1', title: 'September 2022', period: 'Sep 1 – Sep 30, 2022', generatedAt: 'Oct 1, 2022', downloadUrl: '#' },
|
||||
{ id: 'r2', title: 'August 2022', period: 'Aug 1 – Aug 31, 2022', generatedAt: 'Sep 1, 2022', downloadUrl: '#' },
|
||||
];
|
||||
|
||||
export const MOCK_DOCUMENTS = [
|
||||
{ id: 'd1', name: 'Invoice #2 - Robin Murray.pdf', size: '124 KB', uploadedAt: 'Sep 8, 2022' },
|
||||
{ id: 'd2', name: 'Scan - Acme Corp.pdf', size: '89 KB', uploadedAt: 'Sep 12, 2022' },
|
||||
];
|
||||
|
|
|
|||
169
package-lock.json
generated
169
package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
|||
"expo-linking": "^8.0.11",
|
||||
"expo-router": "^6.0.23",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"lucide-react-native": "^0.575.0",
|
||||
"nativewind": "^4.2.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
|
@ -28,6 +29,7 @@
|
|||
"react-native-reanimated": "^4.2.2",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "^4.23.0",
|
||||
"react-native-svg": "^15.15.3",
|
||||
"react-native-web": "^0.21.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
|
|
@ -4143,6 +4145,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/bplist-creator": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||
|
|
@ -4740,6 +4748,56 @@
|
|||
"hyphenate-style-name": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.14",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
|
|
@ -4870,6 +4928,61 @@
|
|||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
|
|
@ -4924,6 +5037,18 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-editor": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||
|
|
@ -7344,6 +7469,17 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react-native": {
|
||||
"version": "0.575.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.575.0.tgz",
|
||||
"integrity": "sha512-kdGcjF4Rm1YKuNs3IaW5lDAqVKn9RBj1Fmjt3JBr08PMIXpVV7iL0ICNF/awiPZQicHlx/v9xgyZZS4TAFxDNg==",
|
||||
"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/makeerror": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
|
||||
|
|
@ -7359,6 +7495,12 @@
|
|||
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
|
|
@ -7977,6 +8119,18 @@
|
|||
"node": "^16.14.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nullthrows": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||
|
|
@ -9171,6 +9325,21 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-svg": {
|
||||
"version": "15.15.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz",
|
||||
"integrity": "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^1.1.3",
|
||||
"warn-once": "0.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"expo-linking": "^8.0.11",
|
||||
"expo-router": "^6.0.23",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"lucide-react-native": "^0.575.0",
|
||||
"nativewind": "^4.2.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
"react-native-reanimated": "^4.2.2",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "^4.23.0",
|
||||
"react-native-svg": "^15.15.3",
|
||||
"react-native-web": "^0.21.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user