Compare commits

..

2 Commits

Author SHA1 Message Date
“kirukib”
94064e66f7 - 2026-02-22 22:45:45 +03:00
“kirukib”
4efcacaeba feat: add suggested screens with orange/black theme and mock data
- Add Expo Router with bottom tabs (Home, Scan, Proforma, Payments, Profile)
- Home: earnings summary, quick actions, invoice list with Waiting/Paid filters
- Scan: placeholder for camera flow, recent scans list
- Proforma: list of proforma requests with send-to-contacts CTA
- Payments: pending match and reconciled list
- Profile: account info, about, logout
- Apply Yaltopia theme: primary orange (#ea580c), dark navbar/tabs (#2d2d2d)
- Add mock data (invoices, proforma, payments, user) in lib/mock-data.ts
- Root layout: GestureHandler, SafeArea, PortalHost; tab bar dark with orange active

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 22:43:30 +03:00
30 changed files with 24213 additions and 46 deletions

33
App.tsx
View File

@ -1,20 +1,29 @@
import './global.css';
import { StatusBar } from 'expo-status-bar'; 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() { export default function App() {
return ( return (
<View style={styles.container}> <View className="flex-1 items-center justify-center gap-6 bg-white p-6">
<Text>Open up App.tsx to start working on your app!</Text> <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" /> <StatusBar style="auto" />
<PortalHost />
</View> </View>
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@ -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","bundler":"metro"},"scheme":"yaltopia-tickets"}}
"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"
}
}
}

58
app/(tabs)/_layout.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="nativewind/types" />

11405
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "yaltopia-tickets-app", "name": "yaltopia-tickets-app",
"version": "1.0.0", "version": "1.0.0",
"main": "index.ts", "main": "expo-router/entry",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
@ -9,13 +9,34 @@
"web": "expo start --web" "web": "expo start --web"
}, },
"dependencies": { "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": "~54.0.33",
"expo-constants": "^18.0.13",
"expo-linking": "^8.0.11",
"expo-router": "^6.0.23",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"nativewind": "^4.2.2",
"react": "19.1.0", "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": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"prettier-plugin-tailwindcss": "^0.5.14",
"tailwindcss": "^3.4.19",
"typescript": "~5.9.2" "typescript": "~5.9.2"
}, },
"private": true "private": true

11540
swagger.json Normal file

File diff suppressed because it is too large Load Diff

73
tailwind.config.js Normal file
View 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')],
};

View File

@ -1,6 +1,11 @@
{ {
"extends": "expo/tsconfig.base", "extends": "expo/tsconfig.base",
"compilerOptions": { "compilerOptions": {
"strict": true "strict": true,
} "baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx", "nativewind-env.d.ts"]
} }