Compare commits
2 Commits
9d2d2b0af7
...
94064e66f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94064e66f7 | ||
|
|
4efcacaeba |
33
App.tsx
33
App.tsx
|
|
@ -1,20 +1,29 @@
|
|||
import './global.css';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { PortalHost } from '@rn-primitives/portal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Open up App.tsx to start working on your app!</Text>
|
||||
<View className="flex-1 items-center justify-center gap-6 bg-white p-6">
|
||||
<Text className="text-xl font-bold text-gray-900">
|
||||
Yaltopia Tickets App
|
||||
</Text>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Scan. Send. Reconcile.</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button>
|
||||
<Text>Get started</Text>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<StatusBar style="auto" />
|
||||
<PortalHost />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
31
app.json
31
app.json
|
|
@ -1,30 +1 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "Yaltopia-Tickets-App",
|
||||
"slug": "Yaltopia-Tickets-App",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"predictiveBackGestureEnabled": false
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
{"expo":{"name":"Yaltopia Tickets App","slug":"yaltopia-tickets-app","version":"1.0.0","orientation":"portrait","icon":"./assets/icon.png","userInterfaceStyle":"light","newArchEnabled":true,"splash":{"image":"./assets/splash-icon.png","resizeMode":"contain","backgroundColor":"#ffffff"},"ios":{"supportsTablet":true},"android":{"adaptiveIcon":{"foregroundImage":"./assets/adaptive-icon.png","backgroundColor":"#ffffff"},"edgeToEdgeEnabled":true,"predictiveBackGestureEnabled":false},"web":{"favicon":"./assets/favicon.png","bundler":"metro"},"scheme":"yaltopia-tickets"}}
|
||||
58
app/(tabs)/_layout.tsx
Normal file
58
app/(tabs)/_layout.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { Tabs } from 'expo-router';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const NAV_BG = '#2d2d2d';
|
||||
const ACTIVE_TINT = '#ea580c';
|
||||
const INACTIVE_TINT = '#a1a1aa';
|
||||
|
||||
export default function TabsLayout() {
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: NAV_BG },
|
||||
headerTintColor: '#ffffff',
|
||||
headerTitleStyle: { fontWeight: '600' },
|
||||
tabBarStyle: { backgroundColor: NAV_BG },
|
||||
tabBarActiveTintColor: ACTIVE_TINT,
|
||||
tabBarInactiveTintColor: INACTIVE_TINT,
|
||||
tabBarLabelStyle: { fontSize: 12 },
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Home',
|
||||
tabBarLabel: 'Home',
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="scan"
|
||||
options={{
|
||||
title: 'Scan Invoice',
|
||||
tabBarLabel: 'Scan',
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="proforma"
|
||||
options={{
|
||||
title: 'Proforma',
|
||||
tabBarLabel: 'Proforma',
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="payments"
|
||||
options={{
|
||||
title: 'Payments',
|
||||
tabBarLabel: 'Payments',
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: 'Profile',
|
||||
tabBarLabel: 'Profile',
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
102
app/(tabs)/index.tsx
Normal file
102
app/(tabs)/index.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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 { EARNINGS_SUMMARY, MOCK_INVOICES, MOCK_USER } from '@/lib/mock-data';
|
||||
import { router } from 'expo-router';
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
Waiting: 'bg-amber-500/20 text-amber-700',
|
||||
Paid: 'bg-emerald-500/20 text-emerald-700',
|
||||
Draft: 'bg-gray-200 text-gray-700',
|
||||
Unpaid: 'bg-red-500/20 text-red-700',
|
||||
};
|
||||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-[#f5f5f5]" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<View className="mb-4">
|
||||
<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>
|
||||
</View>
|
||||
|
||||
<Card className="mb-4 overflow-hidden border-0">
|
||||
<View className="bg-primary/10 p-4">
|
||||
<Text className="text-muted-foreground text-sm">Earnings balance</Text>
|
||||
<Text className="text-2xl 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')}>
|
||||
<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>
|
||||
</Pressable>
|
||||
<View className="w-px bg-border" />
|
||||
<Pressable className="flex-1 py-3">
|
||||
<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>
|
||||
</Pressable>
|
||||
</CardContent>
|
||||
</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>
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1" onPress={() => router.push('/(tabs)/proforma')}>
|
||||
<Text className="font-medium">Send proforma</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="mb-2 flex-row gap-2">
|
||||
{['All', 'Draft', 'Waiting', 'Paid', 'Unpaid'].map((filter) => (
|
||||
<View
|
||||
key={filter}
|
||||
className={`rounded-full px-3 py-1.5 ${filter === 'Waiting' ? 'bg-primary' : 'bg-muted'}`}
|
||||
>
|
||||
<Text className={filter === 'Waiting' ? 'text-primary-foreground text-sm font-medium' : 'text-muted-foreground text-sm'}>
|
||||
{filter}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text className="text-muted-foreground mb-2 text-sm">Today</Text>
|
||||
{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">
|
||||
<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>
|
||||
</View>
|
||||
<View className="items-end">
|
||||
<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]}`}>
|
||||
<Text className="text-xs font-medium">{inv.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Text className="text-muted-foreground mb-2 mt-4 text-sm">Yesterday</Text>
|
||||
{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">
|
||||
<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>
|
||||
</View>
|
||||
<View className="items-end">
|
||||
<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]}`}>
|
||||
<Text className="text-xs font-medium">{inv.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
58
app/(tabs)/payments.tsx
Normal file
58
app/(tabs)/payments.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { View, ScrollView } from 'react-native';
|
||||
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';
|
||||
|
||||
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">
|
||||
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>
|
||||
|
||||
<Text className="text-muted-foreground mb-2 text-sm">Pending match</Text>
|
||||
{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>
|
||||
<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">
|
||||
<Text className="font-medium">Match to invoice</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Text className="text-muted-foreground mb-2 mt-4 text-sm">Reconciled</Text>
|
||||
{matched.map((pay) => (
|
||||
<Card key={pay.id} className="mb-2">
|
||||
<CardContent className="py-3">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View>
|
||||
<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">
|
||||
<Text className="text-xs font-medium text-emerald-700">Matched</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
58
app/(tabs)/profile.tsx
Normal file
58
app/(tabs)/profile.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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 { MOCK_USER } from '@/lib/mock-data';
|
||||
|
||||
export default function ProfileScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Account</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-2">
|
||||
<View className="flex-row justify-between py-2">
|
||||
<Text className="text-muted-foreground">Email</Text>
|
||||
<Text className="text-gray-900">{MOCK_USER.email}</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between py-2">
|
||||
<Text className="text-muted-foreground">Language</Text>
|
||||
<Text className="text-gray-900">English</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => router.push('/notifications')} className="flex-row justify-between py-2">
|
||||
<Text className="text-muted-foreground">Notifications</Text>
|
||||
<Text className="text-primary font-medium">Manage</Text>
|
||||
</Pressable>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>About</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="text-muted-foreground text-sm">
|
||||
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>
|
||||
<Button variant="destructive" className="mt-2">
|
||||
<Text className="font-medium">Log out</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
37
app/(tabs)/proforma.tsx
Normal file
37
app/(tabs)/proforma.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { View, ScrollView, Pressable } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
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';
|
||||
|
||||
export default function ProformaScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Text className="text-muted-foreground mb-4">
|
||||
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>
|
||||
|
||||
<Text className="text-muted-foreground mb-2 text-sm">Your proforma requests</Text>
|
||||
{MOCK_PROFORMA.map((pf) => (
|
||||
<Card key={pf.id} 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>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-row justify-between">
|
||||
<Text className="text-muted-foreground text-sm">Sent to {pf.sentCount} contacts</Text>
|
||||
<Button variant="outline" size="sm">
|
||||
<Text className="font-medium">Send to contacts</Text>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
57
app/(tabs)/scan.tsx
Normal file
57
app/(tabs)/scan.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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';
|
||||
|
||||
export default function ScanScreen() {
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Text className="text-muted-foreground mb-4">
|
||||
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>
|
||||
</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">
|
||||
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>
|
||||
</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>
|
||||
<View className="rounded-full bg-amber-500/20 px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-amber-700">Pending</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mb-2">
|
||||
<CardContent className="py-3">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View>
|
||||
<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>
|
||||
</View>
|
||||
<View className="rounded-full bg-emerald-500/20 px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-emerald-700">Saved</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
34
app/_layout.tsx
Normal file
34
app/_layout.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import '../global.css';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { PortalHost } from '@rn-primitives/portal';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { View } from 'react-native';
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<SafeAreaProvider>
|
||||
<View className="flex-1 bg-background">
|
||||
<StatusBar style="light" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: '#2d2d2d' },
|
||||
headerTintColor: '#ffffff',
|
||||
headerTitleStyle: { fontWeight: '600' },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="proforma/[id]" options={{ title: 'Proforma request' }} />
|
||||
<Stack.Screen name="payments/[id]" options={{ title: 'Payment' }} />
|
||||
<Stack.Screen name="notifications/index" options={{ title: 'Notifications' }} />
|
||||
<Stack.Screen name="notifications/settings" options={{ title: 'Notification settings' }} />
|
||||
<Stack.Screen name="login" options={{ title: 'Sign in', headerShown: false }} />
|
||||
</Stack>
|
||||
<PortalHost />
|
||||
</View>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
30
app/login.tsx
Normal file
30
app/login.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { View, ScrollView } 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';
|
||||
|
||||
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">
|
||||
<CardHeader>
|
||||
<CardTitle>Sign in</CardTitle>
|
||||
<CardDescription>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>
|
||||
<Button variant="outline">
|
||||
<Text className="font-medium">Continue with Google</Text>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button variant="ghost" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
33
app/notifications/index.tsx
Normal file
33
app/notifications/index.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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';
|
||||
|
||||
const MOCK_NOTIFICATIONS = [
|
||||
{ id: '1', title: 'Invoice reminder', body: 'Invoice #2 to Robin Murray is due in 2 days.', time: '2h ago', read: false },
|
||||
{ id: '2', title: 'Payment received', body: 'Payment of $500 received for Invoice #4.', time: '1d ago', read: true },
|
||||
{ id: '3', title: 'Proforma submission', body: 'Vendor A submitted a quote for Marketing Landing Page.', time: '2d ago', read: true },
|
||||
];
|
||||
|
||||
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">
|
||||
<Text className="text-xl font-semibold text-gray-900">Notifications</Text>
|
||||
<Pressable onPress={() => router.push('/notifications/settings')}>
|
||||
<Text className="text-primary font-medium">Settings</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{MOCK_NOTIFICATIONS.map((n) => (
|
||||
<Card key={n.id} className={`mb-2 ${!n.read ? 'border-primary/30' : ''}`}>
|
||||
<CardContent className="py-3">
|
||||
<Text className="font-semibold text-gray-900">{n.title}</Text>
|
||||
<Text className="text-muted-foreground mt-1 text-sm">{n.body}</Text>
|
||||
<Text className="text-muted-foreground mt-1 text-xs">{n.time}</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
41
app/notifications/settings.tsx
Normal file
41
app/notifications/settings.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { View, ScrollView, Switch } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export default function NotificationSettingsScreen() {
|
||||
const [invoiceReminders, setInvoiceReminders] = useState(true);
|
||||
const [daysBeforeDue, setDaysBeforeDue] = useState(2);
|
||||
const [newsAlerts, setNewsAlerts] = useState(true);
|
||||
const [reportReady, setReportReady] = useState(true);
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Notification settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-4">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-gray-900">Invoice reminders</Text>
|
||||
<Switch value={invoiceReminders} onValueChange={setInvoiceReminders} />
|
||||
</View>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-gray-900">News & announcements</Text>
|
||||
<Switch value={newsAlerts} onValueChange={setNewsAlerts} />
|
||||
</View>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-gray-900">Report ready</Text>
|
||||
<Switch value={reportReady} onValueChange={setReportReady} />
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button variant="outline" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
44
app/payments/[id].tsx
Normal file
44
app/payments/[id].tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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';
|
||||
|
||||
export default function PaymentDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Payment #{id ?? '—'}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-2">
|
||||
<View className="flex-row justify-between py-2">
|
||||
<Text className="text-muted-foreground">Amount</Text>
|
||||
<Text className="font-semibold text-gray-900">$2,000.00</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between py-2">
|
||||
<Text className="text-muted-foreground">Source</Text>
|
||||
<Text className="text-gray-900">Telebirr</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between py-2">
|
||||
<Text className="text-muted-foreground">Date</Text>
|
||||
<Text className="text-gray-900">Sep 11, 2022</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between py-2">
|
||||
<Text className="text-muted-foreground">Associated invoice</Text>
|
||||
<Text className="text-amber-600">Not linked</Text>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button className="mb-3 bg-primary" onPress={() => {}}>
|
||||
<Text className="text-primary-foreground font-medium">Associate to invoice</Text>
|
||||
</Button>
|
||||
<Button variant="outline" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back to payments</Text>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
66
app/proforma/[id].tsx
Normal file
66
app/proforma/[id].tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
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, CardDescription } from '@/components/ui/card';
|
||||
|
||||
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 },
|
||||
];
|
||||
const MOCK_SUBTOTAL = 1400;
|
||||
const MOCK_TAX = 140;
|
||||
const MOCK_TOTAL = 1540;
|
||||
|
||||
export default function ProformaDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1 bg-background" contentContainerStyle={{ padding: 16, paddingBottom: 32 }}>
|
||||
<Card className="mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Proforma Request #{id ?? '—'}</CardTitle>
|
||||
<CardDescription>Marketing Landing Page Package</CardDescription>
|
||||
<Text className="text-muted-foreground mt-1 text-sm">Deadline: Sep 20, 2022 · OPEN</Text>
|
||||
</CardHeader>
|
||||
<CardContent className="gap-2">
|
||||
{MOCK_ITEMS.map((item, i) => (
|
||||
<View key={i} className="flex-row justify-between py-2">
|
||||
<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-2">
|
||||
<View className="flex-row justify-between">
|
||||
<Text className="text-muted-foreground">Subtotal</Text>
|
||||
<Text className="text-gray-900">${MOCK_SUBTOTAL.toLocaleString()}</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between">
|
||||
<Text className="text-muted-foreground">Tax (10%)</Text>
|
||||
<Text className="text-gray-900">${MOCK_TAX.toLocaleString()}</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between">
|
||||
<Text className="font-semibold text-gray-900">Total</Text>
|
||||
<Text className="font-semibold text-gray-900">${MOCK_TOTAL.toLocaleString()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button className="mb-3 bg-primary" onPress={() => {}}>
|
||||
<Text className="text-primary-foreground font-medium">Send to contacts</Text>
|
||||
</Button>
|
||||
<Button variant="outline" onPress={() => router.back()}>
|
||||
<Text className="font-medium">Back to list</Text>
|
||||
</Button>
|
||||
|
||||
<Text className="text-muted-foreground mt-6 mb-2 text-sm">Submissions (mock)</Text>
|
||||
<Card>
|
||||
<CardContent className="py-3">
|
||||
<Text className="font-medium text-gray-900">Vendor A — $1,450</Text>
|
||||
<Text className="text-muted-foreground text-sm">Submitted Sep 15, 2022</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
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' }],
|
||||
'nativewind/babel',
|
||||
],
|
||||
};
|
||||
};
|
||||
19
components.json
Normal file
19
components.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
108
components/ui/button.tsx
Normal file
108
components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { TextClassContext } from '@/components/ui/text';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Platform, Pressable } from 'react-native';
|
||||
|
||||
const buttonVariants = cva(
|
||||
cn(
|
||||
'group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none',
|
||||
Platform.select({
|
||||
web: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
})
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: cn(
|
||||
'bg-primary active:bg-primary/90 shadow-sm shadow-black/5',
|
||||
Platform.select({ web: 'hover:bg-primary/90' })
|
||||
),
|
||||
destructive: cn(
|
||||
'bg-destructive active:bg-destructive/90 dark:bg-destructive/60 shadow-sm shadow-black/5',
|
||||
Platform.select({
|
||||
web: 'hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
||||
})
|
||||
),
|
||||
outline: cn(
|
||||
'border-border bg-background active:bg-accent dark:bg-input/30 dark:border-input dark:active:bg-input/50 border shadow-sm shadow-black/5',
|
||||
Platform.select({
|
||||
web: 'hover:bg-accent dark:hover:bg-input/50',
|
||||
})
|
||||
),
|
||||
secondary: cn(
|
||||
'bg-secondary active:bg-secondary/80 shadow-sm shadow-black/5',
|
||||
Platform.select({ web: 'hover:bg-secondary/80' })
|
||||
),
|
||||
ghost: cn(
|
||||
'active:bg-accent dark:active:bg-accent/50',
|
||||
Platform.select({ web: 'hover:bg-accent dark:hover:bg-accent/50' })
|
||||
),
|
||||
link: '',
|
||||
},
|
||||
size: {
|
||||
default: cn('h-10 px-4 py-2 sm:h-9', Platform.select({ web: 'has-[>svg]:px-3' })),
|
||||
sm: cn('h-9 gap-1.5 rounded-md px-3 sm:h-8', Platform.select({ web: 'has-[>svg]:px-2.5' })),
|
||||
lg: cn('h-11 rounded-md px-6 sm:h-10', Platform.select({ web: 'has-[>svg]:px-4' })),
|
||||
icon: 'h-10 w-10 sm:h-9 sm:w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const buttonTextVariants = cva(
|
||||
cn(
|
||||
'text-foreground text-sm font-medium',
|
||||
Platform.select({ web: 'pointer-events-none transition-colors' })
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'text-primary-foreground',
|
||||
destructive: 'text-white',
|
||||
outline: cn(
|
||||
'group-active:text-accent-foreground',
|
||||
Platform.select({ web: 'group-hover:text-accent-foreground' })
|
||||
),
|
||||
secondary: 'text-secondary-foreground',
|
||||
ghost: 'group-active:text-accent-foreground',
|
||||
link: cn(
|
||||
'text-primary group-active:underline',
|
||||
Platform.select({ web: 'underline-offset-4 hover:underline group-hover:underline' })
|
||||
),
|
||||
},
|
||||
size: {
|
||||
default: '',
|
||||
sm: '',
|
||||
lg: '',
|
||||
icon: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type ButtonProps = React.ComponentProps<typeof Pressable> &
|
||||
React.RefAttributes<typeof Pressable> &
|
||||
VariantProps<typeof buttonVariants>;
|
||||
|
||||
function Button({ className, variant, size, ...props }: ButtonProps) {
|
||||
return (
|
||||
<TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
|
||||
<Pressable
|
||||
className={cn(props.disabled && 'opacity-50', buttonVariants({ variant, size }), className)}
|
||||
role="button"
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonTextVariants, buttonVariants };
|
||||
export type { ButtonProps };
|
||||
52
components/ui/card.tsx
Normal file
52
components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { Text, TextClassContext } from '@/components/ui/text';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { View, type ViewProps } from 'react-native';
|
||||
|
||||
function Card({ className, ...props }: ViewProps & React.RefAttributes<View>) {
|
||||
return (
|
||||
<TextClassContext.Provider value="text-card-foreground">
|
||||
<View
|
||||
className={cn(
|
||||
'bg-card border-border flex flex-col gap-6 rounded-xl border py-6 shadow-sm shadow-black/5',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: ViewProps & React.RefAttributes<View>) {
|
||||
return <View className={cn('flex flex-col gap-1.5 px-6', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text> & React.RefAttributes<Text>) {
|
||||
return (
|
||||
<Text
|
||||
role="heading"
|
||||
aria-level={3}
|
||||
className={cn('font-semibold leading-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text> & React.RefAttributes<Text>) {
|
||||
return <Text className={cn('text-muted-foreground text-sm', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: ViewProps & React.RefAttributes<View>) {
|
||||
return <View className={cn('px-6', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: ViewProps & React.RefAttributes<View>) {
|
||||
return <View className={cn('flex flex-row items-center px-6', className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||
89
components/ui/text.tsx
Normal file
89
components/ui/text.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import * as Slot from '@rn-primitives/slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
import { Platform, Text as RNText, type Role } from 'react-native';
|
||||
|
||||
const textVariants = cva(
|
||||
cn(
|
||||
'text-foreground text-base',
|
||||
Platform.select({
|
||||
web: 'select-text',
|
||||
})
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: '',
|
||||
h1: cn(
|
||||
'text-center text-4xl font-extrabold tracking-tight',
|
||||
Platform.select({ web: 'scroll-m-20 text-balance' })
|
||||
),
|
||||
h2: cn(
|
||||
'border-border border-b pb-2 text-3xl font-semibold tracking-tight',
|
||||
Platform.select({ web: 'scroll-m-20 first:mt-0' })
|
||||
),
|
||||
h3: cn('text-2xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
|
||||
h4: cn('text-xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
|
||||
p: 'mt-3 leading-7 sm:mt-6',
|
||||
blockquote: 'mt-4 border-l-2 pl-3 italic sm:mt-6 sm:pl-6',
|
||||
code: cn(
|
||||
'bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold'
|
||||
),
|
||||
lead: 'text-muted-foreground text-xl',
|
||||
large: 'text-lg font-semibold',
|
||||
small: 'text-sm font-medium leading-none',
|
||||
muted: 'text-muted-foreground text-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type TextVariantProps = VariantProps<typeof textVariants>;
|
||||
|
||||
type TextVariant = NonNullable<TextVariantProps['variant']>;
|
||||
|
||||
const ROLE: Partial<Record<TextVariant, Role>> = {
|
||||
h1: 'heading',
|
||||
h2: 'heading',
|
||||
h3: 'heading',
|
||||
h4: 'heading',
|
||||
blockquote: Platform.select({ web: 'blockquote' as Role }),
|
||||
code: Platform.select({ web: 'code' as Role }),
|
||||
};
|
||||
|
||||
const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
|
||||
h1: '1',
|
||||
h2: '2',
|
||||
h3: '3',
|
||||
h4: '4',
|
||||
};
|
||||
|
||||
const TextClassContext = React.createContext<string | undefined>(undefined);
|
||||
|
||||
function Text({
|
||||
className,
|
||||
asChild = false,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof RNText> &
|
||||
TextVariantProps &
|
||||
React.RefAttributes<RNText> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const textClass = React.useContext(TextClassContext);
|
||||
const Component = asChild ? Slot.Text : RNText;
|
||||
return (
|
||||
<Component
|
||||
className={cn(textVariants({ variant }), textClass, className)}
|
||||
role={variant ? ROLE[variant] : undefined}
|
||||
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Text, TextClassContext };
|
||||
60
global.css
Normal file
60
global.css
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 24 90% 48%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 63%;
|
||||
--radius: 0.625rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark:root {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 70.9% 59.4%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 300 0% 45%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
94
lib/mock-data.ts
Normal file
94
lib/mock-data.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
export const MOCK_USER = {
|
||||
name: 'Octavia',
|
||||
email: 'octavia@yaltopia.com',
|
||||
};
|
||||
|
||||
export const MOCK_INVOICES = [
|
||||
{
|
||||
id: '1',
|
||||
recipient: 'Robin Murray',
|
||||
recipientEmail: 'robinmurray@email.com',
|
||||
invoiceNumber: '2',
|
||||
dueDate: 'Sep 11, 2022',
|
||||
amount: 1540,
|
||||
status: 'Waiting',
|
||||
createdAt: 'Sep 8, 2022',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
recipient: 'Sophie Shonia',
|
||||
recipientEmail: 'sophie@email.com',
|
||||
invoiceNumber: '4',
|
||||
dueDate: 'Sep 9, 2022',
|
||||
amount: 500,
|
||||
status: 'Paid',
|
||||
createdAt: 'Sep 9, 2022',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
recipient: 'Atlantis Limited',
|
||||
recipientEmail: 'contact@atlantis.com',
|
||||
invoiceNumber: '3',
|
||||
dueDate: 'Sep 9, 2022',
|
||||
amount: 2000,
|
||||
status: 'Paid',
|
||||
createdAt: 'Sep 9, 2022',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_PROFORMA = [
|
||||
{
|
||||
id: 'pf1',
|
||||
title: 'Marketing Landing Page Package',
|
||||
description: 'Landing page design and development',
|
||||
deadline: 'Sep 20, 2022',
|
||||
itemCount: 2,
|
||||
sentCount: 3,
|
||||
},
|
||||
{
|
||||
id: 'pf2',
|
||||
title: 'Q4 Brand Assets',
|
||||
description: 'Logo variants and social templates',
|
||||
deadline: 'Sep 25, 2022',
|
||||
itemCount: 5,
|
||||
sentCount: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_PAYMENTS = [
|
||||
{
|
||||
id: 'pay1',
|
||||
date: 'Sep 12, 2022',
|
||||
amount: 1540,
|
||||
reference: 'INV-002',
|
||||
source: 'Telebirr',
|
||||
matched: true,
|
||||
invoiceId: '1',
|
||||
},
|
||||
{
|
||||
id: 'pay2',
|
||||
date: 'Sep 10, 2022',
|
||||
amount: 500,
|
||||
reference: 'INV-004',
|
||||
source: 'Bank Transfer',
|
||||
matched: true,
|
||||
invoiceId: '2',
|
||||
},
|
||||
{
|
||||
id: 'pay3',
|
||||
date: 'Sep 11, 2022',
|
||||
amount: 2000,
|
||||
reference: null,
|
||||
source: 'Telebirr',
|
||||
matched: false,
|
||||
invoiceId: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const EARNINGS_SUMMARY = {
|
||||
balance: 6432,
|
||||
waitingAmount: 1540,
|
||||
waitingCount: 1,
|
||||
paidThisMonth: 4120,
|
||||
paidCount: 4,
|
||||
};
|
||||
81
lib/theme.ts
Normal file
81
lib/theme.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { DarkTheme, DefaultTheme, type Theme } from '@react-navigation/native';
|
||||
|
||||
export const THEME = {
|
||||
light: {
|
||||
background: 'hsl(0 0% 100%)',
|
||||
foreground: 'hsl(0 0% 3.9%)',
|
||||
card: 'hsl(0 0% 100%)',
|
||||
cardForeground: 'hsl(0 0% 3.9%)',
|
||||
popover: 'hsl(0 0% 100%)',
|
||||
popoverForeground: 'hsl(0 0% 3.9%)',
|
||||
primary: 'hsl(24 90% 48%)',
|
||||
primaryForeground: 'hsl(0 0% 100%)',
|
||||
secondary: 'hsl(0 0% 96.1%)',
|
||||
secondaryForeground: 'hsl(0 0% 9%)',
|
||||
muted: 'hsl(0 0% 96.1%)',
|
||||
mutedForeground: 'hsl(0 0% 45.1%)',
|
||||
accent: 'hsl(0 0% 96.1%)',
|
||||
accentForeground: 'hsl(0 0% 9%)',
|
||||
destructive: 'hsl(0 84.2% 60.2%)',
|
||||
border: 'hsl(0 0% 89.8%)',
|
||||
input: 'hsl(0 0% 89.8%)',
|
||||
ring: 'hsl(0 0% 63%)',
|
||||
radius: '0.625rem',
|
||||
chart1: 'hsl(12 76% 61%)',
|
||||
chart2: 'hsl(173 58% 39%)',
|
||||
chart3: 'hsl(197 37% 24%)',
|
||||
chart4: 'hsl(43 74% 66%)',
|
||||
chart5: 'hsl(27 87% 67%)',
|
||||
},
|
||||
dark: {
|
||||
background: 'hsl(0 0% 3.9%)',
|
||||
foreground: 'hsl(0 0% 98%)',
|
||||
card: 'hsl(0 0% 3.9%)',
|
||||
cardForeground: 'hsl(0 0% 98%)',
|
||||
popover: 'hsl(0 0% 3.9%)',
|
||||
popoverForeground: 'hsl(0 0% 98%)',
|
||||
primary: 'hsl(0 0% 98%)',
|
||||
primaryForeground: 'hsl(0 0% 9%)',
|
||||
secondary: 'hsl(0 0% 14.9%)',
|
||||
secondaryForeground: 'hsl(0 0% 98%)',
|
||||
muted: 'hsl(0 0% 14.9%)',
|
||||
mutedForeground: 'hsl(0 0% 63.9%)',
|
||||
accent: 'hsl(0 0% 14.9%)',
|
||||
accentForeground: 'hsl(0 0% 98%)',
|
||||
destructive: 'hsl(0 70.9% 59.4%)',
|
||||
border: 'hsl(0 0% 14.9%)',
|
||||
input: 'hsl(0 0% 14.9%)',
|
||||
ring: 'hsl(300 0% 45%)',
|
||||
radius: '0.625rem',
|
||||
chart1: 'hsl(220 70% 50%)',
|
||||
chart2: 'hsl(160 60% 45%)',
|
||||
chart3: 'hsl(30 80% 55%)',
|
||||
chart4: 'hsl(280 65% 60%)',
|
||||
chart5: 'hsl(340 75% 55%)',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const NAV_THEME: Record<'light' | 'dark', Theme> = {
|
||||
light: {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
background: THEME.light.background,
|
||||
border: THEME.light.border,
|
||||
card: THEME.light.card,
|
||||
notification: THEME.light.destructive,
|
||||
primary: THEME.light.primary,
|
||||
text: THEME.light.foreground,
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
background: THEME.dark.background,
|
||||
border: THEME.dark.border,
|
||||
card: THEME.dark.card,
|
||||
notification: THEME.dark.destructive,
|
||||
primary: THEME.dark.primary,
|
||||
text: THEME.dark.foreground,
|
||||
},
|
||||
},
|
||||
};
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
6
metro.config.js
Normal file
6
metro.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
const { getDefaultConfig } = require('expo/metro-config');
|
||||
const { withNativeWind } = require('nativewind/metro');
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 });
|
||||
1
nativewind-env.d.ts
vendored
Normal file
1
nativewind-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="nativewind/types" />
|
||||
11405
package-lock.json
generated
Normal file
11405
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "yaltopia-tickets-app",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
|
|
@ -9,13 +9,34 @@
|
|||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "~6.1.2",
|
||||
"@react-navigation/native": "^7.1.28",
|
||||
"@rn-primitives/portal": "^1.3.0",
|
||||
"@rn-primitives/slot": "^1.2.0",
|
||||
"babel-preset-expo": "^54.0.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"expo": "~54.0.33",
|
||||
"expo-constants": "^18.0.13",
|
||||
"expo-linking": "^8.0.11",
|
||||
"expo-router": "^6.0.23",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"nativewind": "^4.2.2",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.5"
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "^2.30.0",
|
||||
"react-native-reanimated": "^4.2.2",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "^4.23.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
|
|
|
|||
11540
swagger.json
Normal file
11540
swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
73
tailwind.config.js
Normal file
73
tailwind.config.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
const { hairlineWidth } = require('nativewind/theme');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
content: ['./App.tsx', './index.ts', './components/**/*.{js,jsx,ts,tsx}', './app/**/*.{js,jsx,ts,tsx}'],
|
||||
presets: [require('nativewind/preset')],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
borderWidth: {
|
||||
hairline: hairlineWidth(),
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
future: {
|
||||
hoverOnlyWhenSupported: true,
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
};
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user