This commit is contained in:
elnatansamuel25 2026-03-01 14:43:12 +03:00
parent 837e3f4646
commit 1b41dbd97a
29 changed files with 7834 additions and 4234 deletions

View File

@ -1 +1,38 @@
{"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,
"package": "com.yaltopia.ticketapp"
},
"web": {
"favicon": "./assets/favicon.png",
"bundler": "metro"
},
"scheme": "yaltopia-tickets",
"extra": {
"eas": {
"projectId": "9b79b7de-5639-41ef-a72c-8c226354cd2e"
}
}
}
}

View File

@ -1,62 +1,115 @@
import { Tabs } from 'expo-router';
import { Home, ScanLine, FileText, Wallet, User } from '@/lib/icons';
import { Tabs, router } from "expo-router";
import { Home, ScanLine, FileText, Wallet, History, Scan } from "@/lib/icons";
import { Platform, View, Pressable } from "react-native";
import { ShadowWrapper } from "@/components/ShadowWrapper";
const NAV_BG = '#2d2d2d';
const ACTIVE_TINT = '#ea580c';
const INACTIVE_TINT = '#a1a1aa';
const NAV_BG = "#ffffff";
const ACTIVE_TINT = "#ea580c";
const INACTIVE_TINT = "#94a3b8";
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
headerStyle: { backgroundColor: NAV_BG },
headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '600', fontSize: 18 },
tabBarStyle: { backgroundColor: NAV_BG, paddingTop: 8 },
headerShown: false,
tabBarShowLabel: true,
tabBarActiveTintColor: ACTIVE_TINT,
tabBarInactiveTintColor: INACTIVE_TINT,
tabBarLabelStyle: { fontSize: 11 },
tabBarShowLabel: true,
tabBarLabelStyle: {
fontSize: 9,
fontWeight: "700",
marginBottom: Platform.OS === "ios" ? 0 : 4,
textTransform: "uppercase",
letterSpacing: 0.5,
},
tabBarStyle: {
backgroundColor: NAV_BG,
borderTopWidth: 0,
elevation: 10,
height: Platform.OS === "ios" ? 75 : 75,
paddingBottom: Platform.OS === "ios" ? 30 : 10,
paddingTop: 10,
marginHorizontal: 20,
position: "absolute",
bottom: 25,
left: 20,
right: 20,
borderRadius: 32,
shadowColor: "#000",
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.12,
shadowRadius: 20,
},
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => <Home color={color} size={size ?? 22} strokeWidth={2} />,
}}
/>
<Tabs.Screen
name="scan"
options={{
title: 'Scan Invoice',
tabBarLabel: 'Scan',
tabBarIcon: ({ color, size }) => <ScanLine color={color} size={size ?? 22} strokeWidth={2} />,
}}
/>
<Tabs.Screen
name="proforma"
options={{
title: 'Proforma',
tabBarLabel: 'Proforma',
tabBarIcon: ({ color, size }) => <FileText color={color} size={size ?? 22} strokeWidth={2} />,
tabBarLabel: "Home",
tabBarIcon: ({ color, focused }) => (
<View className={focused ? "bg-primary/5 rounded-2xl p-2" : "p-2"}>
<Home color={color} size={18} strokeWidth={focused ? 2.5 : 2} />
</View>
),
}}
/>
<Tabs.Screen
name="payments"
options={{
title: 'Payments',
tabBarLabel: 'Payments',
tabBarIcon: ({ color, size }) => <Wallet color={color} size={size ?? 22} strokeWidth={2} />,
tabBarLabel: "Payments",
tabBarIcon: ({ color, focused }) => (
<View className={focused ? "bg-primary/5 rounded-2xl p-2" : "p-2"}>
<Wallet color={color} size={18} strokeWidth={focused ? 2.5 : 2} />
</View>
),
}}
/>
<Tabs.Screen
name="profile"
name="scan"
options={{
title: 'Profile',
tabBarLabel: 'Profile',
tabBarIcon: ({ color, size }) => <User color={color} size={size ?? 22} strokeWidth={2} />,
tabBarLabel: "SCAN",
tabBarLabelStyle: {
fontSize: 9,
fontWeight: "700",
color: INACTIVE_TINT,
},
tabBarIcon: ({ focused }) => (
<ShadowWrapper level="lg" className="-mt-12">
<View className="h-16 w-16 rounded-full bg-primary items-center justify-center border-4 border-white">
<ScanLine color="white" size={28} strokeWidth={3} />
</View>
</ShadowWrapper>
),
}}
/>
<Tabs.Screen
name="proforma"
options={{
tabBarLabel: "Proforma",
tabBarIcon: ({ color, focused }) => (
<View className={focused ? "bg-primary/5 rounded-2xl p-2" : "p-2"}>
<FileText
color={color}
size={18}
strokeWidth={focused ? 2.5 : 2}
/>
</View>
),
}}
/>
<Tabs.Screen
name="history"
options={{
tabBarLabel: "History",
tabBarIcon: ({ color, focused }) => (
<View className={focused ? "bg-primary/5 rounded-2xl p-2" : "p-2"}>
<History
color={color}
size={18}
strokeWidth={focused ? 2.5 : 2}
/>
</View>
),
}}
/>
</Tabs>

117
app/(tabs)/history.tsx Normal file
View File

@ -0,0 +1,117 @@
import React from "react";
import { View, ScrollView, Pressable } from "react-native";
import { router } from "expo-router";
import { Text } from "@/components/ui/text";
import { Card } from "@/components/ui/card";
import {
FileText,
Wallet,
ChevronRight,
TrendingUp,
TrendingDown,
Clock,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { ShadowWrapper } from "@/components/ShadowWrapper";
import { StandardHeader } from "@/components/StandardHeader";
import { MOCK_INVOICES, MOCK_PAYMENTS } from "@/lib/mock-data";
export default function HistoryScreen() {
// Combine and sort by date (mocking real activity)
const activity = [
...MOCK_INVOICES.map((inv) => ({
id: `inv-${inv.id}`,
type: "Invoice Sent",
title: inv.recipient,
amount: inv.amount,
date: inv.createdAt,
icon: <FileText size={16} color="#ea580c" />,
})),
...MOCK_PAYMENTS.map((pay) => ({
id: `pay-${pay.id}`,
type: "Payment Received",
title: pay.source,
amount: pay.amount,
date: pay.date,
icon: <Wallet size={16} color="#10b981" />,
})),
].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return (
<ScreenWrapper className="bg-background">
<StandardHeader />
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 150 }}
showsVerticalScrollIndicator={false}
>
<View className="flex-row gap-2 mb-10">
<ShadowWrapper className="flex-1">
<View className="bg-card rounded-[10px] p-3">
<View className="h-8 w-8 bg-emerald-500/10 rounded-[6px] items-center justify-center mb-1">
<TrendingUp color="#10b981" size={16} />
</View>
<Text variant="muted" className="font-semibold">
Inflow
</Text>
<Text variant="h3" className="text-foreground">
$4,120
</Text>
</View>
</ShadowWrapper>
<ShadowWrapper className="flex-1">
<View className="bg-card rounded-[10px] p-3">
<View className="h-8 w-8 bg-amber-500/10 rounded-[6px] items-center justify-center mb-1">
<TrendingDown color="#f59e0b" size={16} />
</View>
<Text variant="muted" className="font-semibold">
Pending
</Text>
<Text variant="h3" className="text-foreground">
$1,540
</Text>
</View>
</ShadowWrapper>
</View>
<Text variant="h4" className="text-foreground mb-2">
Recent Activity
</Text>
<View className="gap-2">
{activity.map((item) => (
<ShadowWrapper key={item.id} level="xs">
<Card className="rounded-[6px] bg-card overflow-hidden">
<View className="flex-row items-center p-3">
<View className="bg-secondary/50 p-1 rounded-[6px] mr-4 border border-border/10">
{item.icon}
</View>
<View className="flex-1 mt-[-10px]">
<Text variant="p" className="text-foreground font-semibold">
{item.title}
</Text>
<Text variant="muted" className="text-xs font-medium">
{item.type} · {item.date}
</Text>
</View>
<View className="items-end mt-[-10px]">
<Text variant="p" className="text-foreground font-semibold">
{item.type.includes("Payment") ? "+" : ""}$
{item.amount.toLocaleString()}
</Text>
<View className="flex-row items-center gap-1">
<Clock color="#000" size={12} />
<Text className="text-[10px] text-foreground font-semibold">
Success
</Text>
</View>
</View>
</View>
</Card>
</ShadowWrapper>
))}
</View>
</ScrollView>
</ScreenWrapper>
);
}

View File

@ -1,147 +1,246 @@
import { View, ScrollView, Pressable } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { EARNINGS_SUMMARY, MOCK_INVOICES, MOCK_USER } from '@/lib/mock-data';
import { router } from 'expo-router';
import { Camera, Send, ChevronRight, Wallet, DollarSign, Clock } from '@/lib/icons';
import React, { useState } from "react";
import { View, ScrollView, Pressable } from "react-native";
import { Text } from "@/components/ui/text";
import { Card, CardContent } from "@/components/ui/card";
import { EARNINGS_SUMMARY, MOCK_INVOICES } from "@/lib/mock-data";
import { router } from "expo-router";
import {
Plus,
Send,
History as HistoryIcon,
BarChart3,
ChevronRight,
Clock,
DollarSign,
FileText,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { ShadowWrapper } from "@/components/ShadowWrapper";
import { StandardHeader } from "@/components/StandardHeader";
const PRIMARY = '#ea580c';
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',
Waiting: "bg-amber-500/30 text-amber-600",
Paid: "bg-emerald-500/30 text-emerald-600",
Draft: "bg-secondary text-muted-foreground",
Unpaid: "bg-red-500/30 text-red-600",
};
export default function HomeScreen() {
const [activeFilter, setActiveFilter] = useState("All");
const filteredInvoices =
activeFilter === "All"
? MOCK_INVOICES
: MOCK_INVOICES.filter((inv) => inv.status === activeFilter);
return (
<ScreenWrapper className="bg-background">
<StandardHeader />
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingHorizontal: 20,
paddingTop: 10,
paddingBottom: 150,
}}
>
<View className="mb-5">
<Text className="text-2xl font-bold text-gray-900">Hi {MOCK_USER.name},</Text>
<Text className="text-muted-foreground mt-1 text-base">Take a look at your last activity.</Text>
{/* Balance Card Section */}
<View className="mb-4">
<ShadowWrapper level="lg">
<Card className="overflow-hidden rounded-[10px] border-0 bg-primary">
<View className="p-4 relative">
<View
className="absolute -top-10 -right-10 w-48 h-48 bg-white/10 rounded-full"
style={{ transform: [{ scale: 1.5 }] }}
/>
<Text className="text-white/60 text-[14px] font-semibold">
Available Balance
</Text>
<View className="mt-2 flex-row items-baseline">
<Text className="text-white text-2xl font-medium">$</Text>
<Text className="ml-1 text-4xl font-bold text-white">
{EARNINGS_SUMMARY.balance.toLocaleString()}
</Text>
</View>
<Card className="mb-5 overflow-hidden rounded-2xl border-0 shadow-sm">
<View className="bg-primary/10 px-5 py-5">
<Text className="text-muted-foreground text-sm">Earnings balance</Text>
<Text className="mt-1 text-3xl font-bold text-gray-900">${EARNINGS_SUMMARY.balance.toLocaleString()}</Text>
<View className="mt-4 flex-row gap-4">
<View className="flex-1 bg-white/15 rounded-[10px] p-2 border border-white/10 backdrop-blur-xl">
<View className="flex-row items-center gap-2">
<View className="p-1.5 bg-white/20 rounded-lg">
<Clock color="white" size={12} strokeWidth={2.5} />
</View>
<View className="flex-row border-t border-border">
<Pressable
className="flex-1 flex-row items-center gap-3 px-5 py-4"
onPress={() => router.push('/(tabs)/payments')}
>
<View className="rounded-xl bg-primary/15 p-2">
<Clock color={PRIMARY} size={20} strokeWidth={2} />
<Text className="text-white text-[12px] font-semibold">
Pending
</Text>
</View>
<View>
<Text className="text-muted-foreground text-xs">Waiting for pay</Text>
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.waitingAmount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.waitingCount} Waiting invoice</Text>
<Text className="text-white font-bold text-xl mt-2">
${EARNINGS_SUMMARY.waitingAmount.toLocaleString()}
</Text>
</View>
</Pressable>
<View className="w-px bg-border" />
<Pressable className="flex-1 flex-row items-center gap-3 px-5 py-4">
<View className="rounded-xl bg-emerald-500/15 p-2">
<DollarSign color="#059669" size={20} strokeWidth={2} />
<View className="flex-1 bg-white/15 rounded-[10px] p-2 border border-white/10 backdrop-blur-xl">
<View className="flex-row items-center gap-2">
<View className="p-1.5 bg-white/20 rounded-lg">
<DollarSign color="white" size={12} strokeWidth={2.5} />
</View>
<Text className="text-white text-[12px] font-semibold">
Income
</Text>
</View>
<Text className="text-white font-bold text-xl mt-2">
${EARNINGS_SUMMARY.paidThisMonth.toLocaleString()}
</Text>
</View>
<View>
<Text className="text-muted-foreground text-xs">Paid this month</Text>
<Text className="font-semibold text-gray-900">${EARNINGS_SUMMARY.paidThisMonth.toLocaleString()}</Text>
<Text className="text-muted-foreground text-xs">{EARNINGS_SUMMARY.paidCount} Paid invoice</Text>
</View>
</Pressable>
</View>
</Card>
<View className="mb-5 flex-row gap-3">
<Button className="min-h-12 flex-1 rounded-xl bg-primary" onPress={() => router.push('/(tabs)/scan')}>
<Camera color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Scan invoice</Text>
</Button>
<Button
variant="outline"
className="min-h-12 flex-1 rounded-xl border-border"
onPress={() => router.push('/(tabs)/proforma')}
>
<Send color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">Send proforma</Text>
</Button>
</ShadowWrapper>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="-mx-1 mb-4">
<View className="flex-row gap-2 px-1">
{['All', 'Draft', 'Waiting', 'Paid', 'Unpaid'].map((filter) => (
{/* Circular Quick Actions Section */}
<View className="mb-4 flex-row justify-around items-center px-2">
<QuickAction
icon={<Plus color="#000" size={20} strokeWidth={1.5} />}
label="Scan"
onPress={() => router.push("/(tabs)/scan")}
/>
<QuickAction
icon={<Send color="#000" size={20} strokeWidth={1.5} />}
label="Send"
onPress={() => router.push("/(tabs)/proforma")}
/>
<QuickAction
icon={<HistoryIcon color="#000" size={20} strokeWidth={1.5} />}
label="History"
onPress={() => router.push("/(tabs)/history")}
/>
<QuickAction
icon={<BarChart3 color="#000" size={20} strokeWidth={1.5} />}
label="Analytics"
/>
</View>
{/* Recent Activity Header */}
<View className="mb-4 flex-row justify-between items-center">
<Text variant="h4" className="text-foreground tracking-tight">
Recent Activity
</Text>
<Pressable className="px-4 py-2 rounded-full">
<Text className="text-primary font-bold text-xs">View all</Text>
</Pressable>
</View>
{/* Filters */}
<View className="mb-6">
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 8 }}
>
{["All", "Paid", "Waiting", "Unpaid"].map((filter) => (
<Pressable
key={filter}
className={`rounded-full px-4 py-2.5 ${filter === 'Waiting' ? 'bg-primary' : 'bg-white'} border border-border`}
onPress={() => setActiveFilter(filter)}
className={`rounded-[4px] px-4 py-1.5 ${activeFilter === filter ? "bg-primary" : "bg-card border border-border"}`}
>
<Text
className={
filter === 'Waiting' ? 'text-primary-foreground text-sm font-medium' : 'text-muted-foreground text-sm'
}
className={`text-xs font-bold ${
activeFilter === filter
? "text-white"
: "text-muted-foreground"
}`}
>
{filter}
</Text>
</Pressable>
))}
</View>
</ScrollView>
</View>
<View className="mb-2 flex-row items-center gap-2">
<View className="h-px flex-1 bg-border" />
<Text className="text-muted-foreground text-xs font-medium">Today</Text>
<View className="h-px flex-1 bg-border" />
{/* Transactions List */}
<View className="gap-2">
{filteredInvoices.length > 0 ? (
filteredInvoices.map((inv) => (
<Pressable
key={inv.id}
onPress={() => router.push(`/invoices/${inv.id}`)}
>
<Card className="overflow-hidden rounded-[6px] bg-card">
<CardContent className="flex-row items-center py-3 px-2">
<View className="bg-secondary/40 rounded-[6px] p-2 mr-2 border border-border/10">
<FileText
className="text-muted-foreground"
size={22}
strokeWidth={2}
/>
</View>
{MOCK_INVOICES.filter((i) => i.status === 'Waiting').map((inv) => (
<Pressable key={inv.id} onPress={() => router.push(`/invoices/${inv.id}`)}>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center justify-between py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Invoice #{inv.invoiceNumber} · Due {inv.dueDate}</Text>
<View className="flex-1 mt-[-20px]">
<Text
variant="p"
className="text-foreground font-semibold"
>
{inv.recipient}
</Text>
<Text
variant="muted"
className="mt-1 text-[11px] font-medium opacity-70"
>
{inv.dueDate} · Proforma
</Text>
</View>
<View className="items-end gap-1">
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
<View className={`rounded-full px-2.5 py-1 ${statusColor[inv.status]}`}>
<Text className="text-xs font-medium">{inv.status}</Text>
<View className="items-end mt-[-20px]">
<Text
variant="p"
className="text-foreground font-semibold"
>
${inv.amount.toLocaleString()}
</Text>
<View
className={`mt-1 rounded-[5px] px-3 py-1 border border-border/50 ${statusColor[inv.status]}`}
>
<Text className="text-[9px] font-semibold uppercase tracking-widest">
{inv.status}
</Text>
</View>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
</Pressable>
))}
<View className="mb-2 mt-6 flex-row items-center gap-2">
<View className="h-px flex-1 bg-border" />
<Text className="text-muted-foreground text-xs font-medium">Yesterday</Text>
<View className="h-px flex-1 bg-border" />
))
) : (
<View className="py-20 items-center">
<Text variant="muted">No transactions found</Text>
</View>
{MOCK_INVOICES.filter((i) => i.status === 'Paid').map((inv) => (
<Pressable key={inv.id} onPress={() => router.push(`/invoices/${inv.id}`)}>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center justify-between py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-semibold text-gray-900">{inv.recipient}</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Invoice #{inv.invoiceNumber} · {inv.dueDate}</Text>
)}
</View>
<View className="items-end gap-1">
<Text className="font-semibold text-gray-900">${inv.amount.toLocaleString()}</Text>
<View className={`rounded-full px-2.5 py-1 ${statusColor[inv.status]}`}>
<Text className="text-xs font-medium">{inv.status}</Text>
</View>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
</Pressable>
))}
</ScrollView>
</ScreenWrapper>
);
}
function QuickAction({
icon,
label,
onPress,
}: {
icon: React.ReactNode;
label: string;
onPress?: () => void;
}) {
return (
<View className="items-center">
<ShadowWrapper>
<Pressable
onPress={onPress}
className="h-12 w-12 rounded-full bg-background items-center justify-center mb-2"
>
{icon}
</Pressable>
</ShadowWrapper>
<Text className="text-foreground text-[12px] font-semibold tracking-tight opacity-90">
{label}
</Text>
</View>
);
}

View File

@ -1,76 +1,106 @@
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 } from '@/components/ui/card';
import { MOCK_PAYMENTS } from '@/lib/mock-data';
import { ScanLine, Link2, CheckCircle2, Wallet, ChevronRight } from '@/lib/icons';
import React from "react";
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 } from "@/components/ui/card";
import { MOCK_PAYMENTS } from "@/lib/mock-data";
import { ScanLine, CheckCircle2, Wallet, ChevronRight } from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { StandardHeader } from "@/components/StandardHeader";
const PRIMARY = '#ea580c';
const PRIMARY = "#ea580c";
export default function PaymentsScreen() {
const matched = MOCK_PAYMENTS.filter((p) => p.matched);
const pending = MOCK_PAYMENTS.filter((p) => !p.matched);
return (
<ScreenWrapper className="bg-background">
<StandardHeader />
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
className="flex-1"
contentContainerStyle={{ padding: 20, paddingBottom: 150 }}
showsVerticalScrollIndicator={false}
>
<Text className="text-muted-foreground mb-5 text-base">
Match payment SMS (e.g. bank or Telebirr) to invoices for quick reconciliation.
<Button className="mb-4 h-10 rounded-[10px] bg-primary shadow-lg shadow-primary/30">
<ScanLine color="#ffffff" size={18} strokeWidth={2.5} />
<Text className=" text-white text-xs font-semibold uppercase tracking-widest">
Scan SMS
</Text>
<Button className="mb-5 min-h-12 rounded-xl bg-primary">
<ScanLine color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Scan SMS now</Text>
</Button>
<View className="mb-3 flex-row items-center gap-2">
<Link2 color="#71717a" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Pending match</Text>
<View className="mb-4 flex-row items-center gap-3">
<Text variant="h4" className="text-foreground">
Pending Match
</Text>
</View>
<View className="gap-2">
{pending.map((pay) => (
<Card key={pay.id} className="mb-2.5 overflow-hidden rounded-xl border-2 border-amber-500/30 bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="mr-3 rounded-xl bg-primary/10 p-2">
<Wallet color={PRIMARY} size={22} strokeWidth={2} />
<Pressable
key={pay.id}
onPress={() => router.push(`/payments/${pay.id}`)}
>
<Card className="rounded-[10px] bg-card overflow-hidden">
<View className="flex-row items-center p-3">
<View className="mr-2 rounded-[6px] bg-primary/10 p-2 border border-primary/5">
<Wallet color={PRIMARY} size={18} strokeWidth={2.5} />
</View>
<View className="flex-1">
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-sm">{pay.source} · {pay.date}</Text>
</View>
<Button variant="outline" size="sm" className="rounded-lg" onPress={() => router.push(`/payments/${pay.id}`)}>
<Text className="font-medium">Match</Text>
</Button>
</CardContent>
</Card>
))}
<View className="mb-3 mt-6 flex-row items-center gap-2">
<CheckCircle2 color="#059669" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Reconciled</Text>
</View>
{matched.map((pay) => (
<Card key={pay.id} className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="mr-3 rounded-xl bg-emerald-500/15 p-2">
<CheckCircle2 color="#059669" size={22} strokeWidth={2} />
</View>
<View className="flex-1">
<Text className="font-semibold text-gray-900">${pay.amount.toLocaleString()}</Text>
<Text className="text-muted-foreground text-sm">
{pay.source} · {pay.date} {pay.reference && `· ${pay.reference}`}
<View className="flex-1 mt-[-15px]">
<Text variant="p" className="text-foreground font-bold">
${pay.amount.toLocaleString()}
</Text>
<Text variant="muted" className="text-xs">
{pay.source} · {pay.date}
</Text>
</View>
<View className="rounded-full bg-emerald-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-emerald-700">Matched</Text>
<View className="bg-amber-500/10 px-4 py-2 rounded-[6px]">
<Text className="text-amber-700 text-[10px] font-semibold">
Match
</Text>
</View>
</View>
</Card>
</Pressable>
))}
</View>
<View className="mb-4 mt-4 flex-row items-center gap-3">
<Text variant="h4" className="text-foreground">
Reconciled
</Text>
</View>
<View className="gap-2">
{matched.map((pay) => (
<Card
key={pay.id}
className="rounded-[10px] bg-card overflow-hidden opacity-80"
>
<View className="flex-row items-center p-3">
<View className="mr-2 rounded-[6px] bg-emerald-500/10 p-2 border border-emerald-500/5">
<CheckCircle2 color="#10b981" size={18} strokeWidth={2.5} />
</View>
<View className="flex-1 mt-[-15px]">
<Text variant="p" className="text-foreground font-bold">
${pay.amount.toLocaleString()}
</Text>
<Text variant="muted" className="text-xs">
{pay.source} · {pay.date}
</Text>
</View>
<ChevronRight
className="text-foreground"
size={18}
strokeWidth={2}
color="#000"
/>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
))}
</View>
</ScrollView>
</ScreenWrapper>
);
}

View File

@ -1,119 +0,0 @@
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';
import { User, Mail, Globe, Bell, ChevronRight, Info, LogOut, LogIn, FileText, FolderOpen, Settings } from '@/lib/icons';
const PRIMARY = '#ea580c';
export default function ProfileScreen() {
return (
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
<View className="mb-6 items-center">
<View className="mb-3 h-20 w-20 items-center justify-center rounded-full bg-primary">
<Text className="text-3xl font-bold text-primary-foreground">{MOCK_USER.name[0]}</Text>
</View>
<Text className="text-xl font-semibold text-gray-900">{MOCK_USER.name}</Text>
<Text className="text-muted-foreground mt-1 text-sm">{MOCK_USER.email}</Text>
</View>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<CardTitle className="text-base">Account</CardTitle>
</CardHeader>
<CardContent className="gap-0">
<View className="flex-row items-center justify-between border-b border-border py-3">
<View className="flex-row items-center gap-3">
<Mail color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Email</Text>
</View>
<Text className="text-gray-900">{MOCK_USER.email}</Text>
</View>
<View className="flex-row items-center justify-between border-b border-border py-3">
<View className="flex-row items-center gap-3">
<Globe color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Language</Text>
</View>
<Text className="text-gray-900">English</Text>
</View>
<Pressable
onPress={() => router.push('/notifications')}
className="flex-row items-center justify-between border-b border-border py-3"
>
<View className="flex-row items-center gap-3">
<Bell color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Notifications</Text>
</View>
<View className="flex-row items-center gap-1">
<Text className="text-primary font-medium">Manage</Text>
<ChevronRight color={PRIMARY} size={18} strokeWidth={2} />
</View>
</Pressable>
<Pressable
onPress={() => router.push('/reports')}
className="flex-row items-center justify-between border-b border-border py-3"
>
<View className="flex-row items-center gap-3">
<FileText color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Reports</Text>
</View>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</Pressable>
<Pressable
onPress={() => router.push('/documents')}
className="flex-row items-center justify-between border-b border-border py-3"
>
<View className="flex-row items-center gap-3">
<FolderOpen color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Documents</Text>
</View>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</Pressable>
<Pressable
onPress={() => router.push('/settings')}
className="flex-row items-center justify-between py-3"
>
<View className="flex-row items-center gap-3">
<Settings color="#71717a" size={20} strokeWidth={2} />
<Text className="text-muted-foreground">Settings</Text>
</View>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
</Pressable>
</CardContent>
</Card>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<View className="flex-row items-center gap-2">
<Info color="#71717a" size={18} strokeWidth={2} />
<CardTitle className="text-base">About</CardTitle>
</View>
</CardHeader>
<CardContent>
<Text className="text-muted-foreground text-sm leading-5">
Yaltopia Tickets App Scan. Send. Reconcile. Companion to the Yaltopia Tickets web app.
</Text>
</CardContent>
</Card>
<Button
variant="outline"
className="mt-2 min-h-12 rounded-xl border-border"
onPress={() => router.push('/login')}
>
<LogIn color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">Sign in (different account)</Text>
</Button>
<Button variant="destructive" className="mt-3 min-h-12 rounded-xl">
<LogOut color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 font-medium">Log out</Text>
</Button>
</ScrollView>
);
}

View File

@ -1,55 +1,132 @@
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';
import { Plus, Send, FileText, ChevronRight, Calendar } from '@/lib/icons';
const PRIMARY = '#ea580c';
import React, { useState } from "react";
import { View, ScrollView, Pressable } from "react-native";
import { Text } from "@/components/ui/text";
import { Card } from "@/components/ui/card";
import { MOCK_PROFORMA } from "@/lib/mock-data";
import { router } from "expo-router";
import {
Plus,
Send,
FileText,
ChevronRight,
Clock,
History,
DraftingCompass,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { ShadowWrapper } from "@/components/ShadowWrapper";
import { StandardHeader } from "@/components/StandardHeader";
import { Button } from "@/components/ui/button";
export default function ProformaScreen() {
const [activeTab, setActiveTab] = React.useState("All");
return (
<ScreenWrapper className="bg-background">
<StandardHeader />
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
className="flex-1"
contentContainerStyle={{ padding: 20, paddingBottom: 150 }}
showsVerticalScrollIndicator={false}
>
<Text className="text-muted-foreground mb-5 text-base">
Create or select proforma requests and share with contacts via email or SMS.
<Button
className="mb-4 h-10 rounded-[10px] bg-primary shadow-lg shadow-primary/30"
onPress={() => router.push("/proforma/create")}
>
<Plus color="white" size={18} strokeWidth={2.5} />
<Text className=" text-white text-sm font-semibold uppercase tracking-widest">
Create Proforma
</Text>
<Button className="mb-5 min-h-12 rounded-xl bg-primary" onPress={() => {}}>
<Plus color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Create new proforma</Text>
</Button>
<View className="mb-3 flex-row items-center gap-2">
<FileText color="#71717a" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Your proforma requests</Text>
{/* <View className="flex-row gap-4 mb-8">
<Pressable
onPress={() => setActiveTab("All")}
className={`flex-1 py-3 rounded-[10px] items-center border ${activeTab === "All" ? "bg-primary border-primary" : "bg-card border-border"}`}
>
<DraftingCompass
color={activeTab === "All" ? "white" : "#94a3b8"}
size={20}
/>
<Text
className={`mt-1 text-[10px] font-black uppercase tracking-widest ${activeTab === "All" ? "text-white" : "text-muted-foreground"}`}
>
All
</Text>
</Pressable>
<Pressable
onPress={() => setActiveTab("Pending")}
className={`flex-1 py-3 rounded-[10px] items-center border ${activeTab === "Pending" ? "bg-primary border-primary" : "bg-card border-border"}`}
>
<History
color={activeTab === "Pending" ? "white" : "#94a3b8"}
size={20}
/>
<Text
className={`mt-1 text-[10px] font-black uppercase tracking-widest ${activeTab === "Pending" ? "text-white" : "text-muted-foreground"}`}
>
Pending
</Text>
</Pressable>
</View> */}
<View className="gap-3">
{MOCK_PROFORMA.map((item) => (
<Pressable
key={item.id}
onPress={() => router.push(`/proforma/${item.id}`)}
>
<Card className="rounded-[6px] bg-card overflow-hidden">
<View className="p-3">
<View className="flex-row justify-between items-start">
<View className="bg-secondary/50 p-2 rounded-[10px]">
<FileText color="#000" size={18} />
</View>
{MOCK_PROFORMA.map((pf) => (
<Pressable key={pf.id} onPress={() => router.push(`/proforma/${pf.id}`)}>
<Card className="mb-3 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<CardTitle className="text-base">{pf.title}</CardTitle>
<CardDescription className="mt-0.5">{pf.description}</CardDescription>
<View className="mt-2 flex-row items-center gap-1.5">
<Calendar color="#71717a" size={14} strokeWidth={2} />
<Text className="text-muted-foreground text-xs">Deadline {pf.deadline} · {pf.itemCount} items</Text>
<View className="bg-emerald-500/10 px-3 py-1 rounded-[6px] border border-emerald-500/20">
<Text className="text-emerald-600 text-[10px] font-bold uppercase tracking-tighter">
{item.sentCount} Shared
</Text>
</View>
</CardHeader>
<CardFooter className="flex-row items-center justify-between border-t border-border pt-3">
<Text className="text-muted-foreground text-sm">Sent to {pf.sentCount} contacts</Text>
</View>
<Text variant="p" className="text-foreground font-semibold">
{item.title}
</Text>
<Text variant="muted" className="mb-4 line-clamp-2 text-xs">
{item.description}
</Text>
<View className="h-[1px] bg-border mb-4 opacity-50" />
<View className="flex-row justify-between items-center">
<View className="flex-row gap-4">
<View className="flex-row items-center gap-1.5">
<Send color={PRIMARY} size={16} strokeWidth={2} />
<Text className="text-primary font-medium text-sm">Send to contacts</Text>
<ChevronRight color="#a1a1aa" size={18} strokeWidth={2} />
<Clock
className="text-muted-foreground"
color="#000"
size={12}
/>
<Text variant="muted" className="text-xs">
{item.deadline}
</Text>
</View>
</View>
<View className="flex-row items-center gap-3">
<Pressable className="bg-secondary px-2 py-1 rounded-[6px] border border-border/50 flex-row items-center gap-1">
<Send color="#000" size={12} />
<Text variant="muted" className="text-xs">
Share
</Text>
</Pressable>
</View>
</View>
</View>
</CardFooter>
</Card>
</Pressable>
))}
</View>
</ScrollView>
</ScreenWrapper>
);
}

View File

@ -1,66 +1,140 @@
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 { Camera, FileText, ChevronRight } from '@/lib/icons';
import React, { useState, useEffect } from "react";
import {
View,
ScrollView,
Pressable,
Platform,
Dimensions,
StyleSheet,
} from "react-native";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { X, Zap, Camera as CameraIcon } from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { CameraView, useCameraPermissions } from "expo-camera";
import { router, useNavigation } from "expo-router";
const PRIMARY = '#ea580c';
const { width } = Dimensions.get("window");
export default function ScanScreen() {
const [permission, requestPermission] = useCameraPermissions();
const [torch, setTorch] = useState(false);
const navigation = useNavigation();
const NAV_BG = "#ffffff";
// Hide tab bar when on this screen (since it's a dedicated camera view)
useEffect(() => {
navigation.setOptions({
tabBarStyle: {
display: "none",
},
});
return () => {
navigation.setOptions({
tabBarStyle: {
display: "flex",
backgroundColor: NAV_BG,
borderTopWidth: 0,
elevation: 10,
height: 75,
paddingBottom: Platform.OS === "ios" ? 30 : 10,
paddingTop: 10,
marginHorizontal: 20,
position: "absolute",
bottom: 25,
left: 20,
right: 20,
borderRadius: 32,
shadowColor: "#000",
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.12,
shadowRadius: 20,
},
});
};
}, [navigation]);
if (!permission) {
// Camera permissions are still loading.
return <View className="flex-1 bg-black" />;
}
if (!permission.granted) {
// Camera permissions are not granted yet.
return (
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
<ScreenWrapper className="bg-background items-center justify-center p-10">
<View className="bg-primary/10 p-6 rounded-[24px] mb-6">
<CameraIcon className="text-primary" size={48} strokeWidth={1.5} />
</View>
<Text variant="h2" className="text-center mb-2">
Camera Access
</Text>
<Text variant="muted" className="text-center mb-10 leading-6">
We need your permission to use the camera to scan invoices and
receipts automatically.
</Text>
<Button
className="w-full h-14 rounded-[12px] bg-primary"
onPress={requestPermission}
>
<Text className="text-muted-foreground mb-5 text-base">
Capture paper or digital invoices with your camera. We'll extract vendor, amount, date, and line items.
<Text className="text-white font-bold uppercase tracking-widest">
Enable Camera
</Text>
<Card className="mb-5 overflow-hidden rounded-2xl border-2 border-dashed border-border bg-white">
<CardContent className="items-center justify-center py-14">
<View className="mb-5 h-24 w-24 items-center justify-center rounded-full bg-primary/10">
<Camera color={PRIMARY} size={40} strokeWidth={2} />
</View>
<Text className="mb-2 text-center text-lg font-semibold text-gray-900">Scan invoice</Text>
<Text className="text-muted-foreground mb-6 text-center text-sm">
Tap below to open camera and capture an invoice
</Text>
<Button className="min-h-12 rounded-xl bg-primary px-8">
<Camera color="#ffffff" size={20} strokeWidth={2} />
<Text className="ml-2 text-primary-foreground font-medium">Open camera</Text>
</Button>
</CardContent>
</Card>
<Pressable onPress={() => router.back()} className="mt-6">
<Text className="text-muted-foreground font-bold">Go Back</Text>
</Pressable>
</ScreenWrapper>
);
}
<View className="mb-3 flex-row items-center gap-2">
<FileText color="#71717a" size={18} strokeWidth={2} />
<Text className="text-muted-foreground text-sm font-medium">Recent scans</Text>
return (
<View className="flex-1 bg-black">
<CameraView
style={StyleSheet.absoluteFill}
facing="back"
enableTorch={torch}
>
<View className="flex-1 justify-between p-10 pt-16">
<View className="flex-row justify-between items-center">
<Pressable
onPress={() => setTorch(!torch)}
className={`h-12 w-12 rounded-full items-center justify-center border border-white/20 ${torch ? "bg-primary" : "bg-black/40"}`}
>
<Zap
color="white"
size={20}
fill={torch ? "white" : "transparent"}
/>
</Pressable>
<Pressable
onPress={() => navigation.goBack()}
className="h-12 w-12 rounded-full bg-black/40 items-center justify-center border border-white/20"
>
<X color="white" size={24} />
</Pressable>
</View>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-medium text-gray-900">Acme Corp - Invoice #101</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Scanned Sep 12, 2022 · $1,240</Text>
<View className="items-center">
{/* Scanning Frame */}
<View className="w-72 h-72 border-2 border-primary rounded-3xl border-dashed opacity-50 items-center justify-center">
<View className="w-64 h-64 border border-white/10 rounded-2xl" />
</View>
<View className="rounded-full bg-amber-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-amber-700">Pending</Text>
<Text className="text-white font-bold mt-8 uppercase tracking-[3px] text-xs">
Align Invoice
</Text>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
<Card className="mb-2.5 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="flex-row items-center py-4 pl-4 pr-3">
<View className="flex-1">
<Text className="font-medium text-gray-900">Tech Supplies Ltd - Invoice #88</Text>
<Text className="text-muted-foreground mt-0.5 text-sm">Scanned Sep 11, 2022 · $890</Text>
<View className="items-center pb-10">
<View className="bg-black/40 px-6 py-3 rounded-full border border-white/10">
<Text className="text-white/60 text-[10px] font-black uppercase tracking-widest">
AI Auto-detecting...
</Text>
</View>
<View className="rounded-full bg-emerald-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-emerald-700">Saved</Text>
</View>
<ChevronRight className="ml-2 text-muted-foreground" color="#71717a" size={20} strokeWidth={2} />
</CardContent>
</Card>
</ScrollView>
</View>
</CameraView>
</View>
);
}

View File

@ -1,12 +1,23 @@
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';
import React, { useEffect, useState } from "react";
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";
import { useRestoreTheme } from "@/lib/theme";
export default function RootLayout() {
useRestoreTheme();
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) return null;
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
@ -14,22 +25,41 @@ export default function RootLayout() {
<StatusBar style="light" />
<Stack
screenOptions={{
headerStyle: { backgroundColor: '#2d2d2d' },
headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '600' },
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" options={{ title: 'Notifications' }} />
<Stack.Screen name="notifications/settings" options={{ title: 'Notification settings' }} />
<Stack.Screen name="login" options={{ title: 'Sign in', headerShown: false }} />
<Stack.Screen name="register" options={{ title: 'Create account', headerShown: false }} />
<Stack.Screen name="invoices/[id]" options={{ title: 'Invoice' }} />
<Stack.Screen name="reports" options={{ title: 'Reports' }} />
<Stack.Screen name="documents" options={{ title: 'Documents' }} />
<Stack.Screen name="settings" options={{ title: 'Settings' }} />
<Stack.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.Screen
name="register"
options={{ title: "Create account", headerShown: false }}
/>
<Stack.Screen name="invoices/[id]" options={{ title: "Invoice" }} />
<Stack.Screen name="reports/index" options={{ title: "Reports" }} />
<Stack.Screen
name="documents/index"
options={{ title: "Documents" }}
/>
<Stack.Screen name="settings" options={{ title: "Settings" }} />
<Stack.Screen name="profile" options={{ headerShown: false }} />
</Stack>
<PortalHost />
</View>

View File

@ -1,15 +1,37 @@
import { View, ScrollView } from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { FileText, Calendar, User, Share2, Download, ChevronRight } from '@/lib/icons';
import { MOCK_INVOICES } from '@/lib/mock-data';
import React from "react";
import { View, ScrollView, Pressable } from "react-native";
import { useLocalSearchParams, router, Stack } from "expo-router";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
FileText,
Calendar,
Share2,
Download,
ArrowLeft,
Tag,
CreditCard,
Building2,
ExternalLink,
} from "@/lib/icons";
import { MOCK_INVOICES } from "@/lib/mock-data";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { ShadowWrapper } from "@/components/ShadowWrapper";
const PRIMARY = '#ea580c';
const MOCK_ITEMS = [
{ description: 'Marketing Landing Page Package', qty: 1, unitPrice: 1000, total: 1000 },
{ description: 'Instagram Post Initial Design', qty: 4, unitPrice: 100, total: 400 },
{
description: "Marketing Landing Page Package",
qty: 1,
unitPrice: 1000,
total: 1000,
},
{
description: "Instagram Post Initial Design",
qty: 4,
unitPrice: 100,
total: 400,
},
];
export default function InvoiceDetailScreen() {
@ -17,84 +39,169 @@ export default function InvoiceDetailScreen() {
const invoice = MOCK_INVOICES.find((i) => i.id === id);
return (
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<View className="px-6 pt-4 flex-row justify-between items-center">
<Pressable
onPress={() => router.back()}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<ArrowLeft color="#0f172a" size={20} />
</Pressable>
<Text variant="h4" className="text-foreground font-semibold">
Invoice Details
</Text>
<Pressable className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border">
<ExternalLink className="text-foreground" color="#000" size={18} />
</Pressable>
</View>
<ScrollView
className="flex-1 bg-[#f5f5f5]"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
showsVerticalScrollIndicator={false}
>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardContent className="p-5">
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-2">
<FileText color={PRIMARY} size={22} strokeWidth={2} />
<Text className="font-semibold text-gray-900">Invoice #{invoice?.invoiceNumber ?? id}</Text>
{/* Status Hero Card */}
<Card className="mb-4 overflow-hidden rounded-[6px] border-0 bg-primary">
<View className="p-5">
<View className="flex-row items-center justify-between mb-3">
<View className="bg-white/20 p-1.5 rounded-[6px]">
<FileText color="white" size={16} strokeWidth={2.5} />
</View>
<View className="rounded-full bg-amber-500/20 px-2.5 py-1">
<Text className="text-xs font-medium text-amber-700">{invoice?.status ?? 'Waiting'}</Text>
<View
className={`rounded-[6px] px-3 py-1 ${invoice?.status === "Paid" ? "bg-emerald-500/20" : "bg-white/15"}`}
>
<Text
className={`text-[10px] font-bold ${invoice?.status === "Paid" ? "text-emerald-400" : "text-white"}`}
>
{invoice?.status || "Pending"}
</Text>
</View>
</View>
<Text className="text-muted-foreground mt-2 text-sm">Amount due</Text>
<Text className="mt-1 text-2xl font-bold text-gray-900">${invoice?.amount.toLocaleString() ?? '—'}</Text>
<View className="mt-3 flex-row gap-4">
<Text variant="small" className="text-white/70 mb-0.5">
Total Amount
</Text>
<Text variant="h3" className="text-white font-bold mb-3">
${invoice?.amount.toLocaleString() ?? "—"}
</Text>
<View className="flex-row items-center gap-3 border-t border-white/40 pt-3">
<View className="flex-row items-center gap-1.5">
<Calendar color="#71717a" size={16} strokeWidth={2} />
<Text className="text-muted-foreground text-sm">Due {invoice?.dueDate ?? '—'}</Text>
<Calendar color="rgba(255,255,255,0.9)" size={12} />
<Text className="text-white/90 text-xs font-semibold">
Due {invoice?.dueDate || "—"}
</Text>
</View>
<View className="flex-row items-center gap-1.5">
<Calendar color="#71717a" size={16} strokeWidth={2} />
<Text className="text-muted-foreground text-sm">Issued {invoice?.createdAt ?? '—'}</Text>
<View className="h-3 w-[1px] bg-white/60" />
<Text className="text-white/90 text-xs font-semibold">
#{invoice?.invoiceNumber || id}
</Text>
</View>
</View>
</CardContent>
</Card>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<View className="flex-row items-center gap-2">
<User color="#71717a" size={18} strokeWidth={2} />
<CardTitle className="text-base">Bill to</CardTitle>
{/* Recipient & Category — inline info strip */}
<Card className="bg-card rounded-[6px] mb-4">
<View className="flex-row px-4 py-2">
<View className="flex-1 flex-row items-center">
<View className="flex-col">
<Text className="text-foreground text-xs">Recipient</Text>
<Text
variant="p"
className="text-foreground font-semibold"
numberOfLines={1}
>
{invoice?.recipient || "—"}
</Text>
</View>
</View>
<View className="w-[1px] bg-border/70 mx-3" />
<View className="flex-1 flex-row items-center">
<View className="flex-col">
<Text className="text-foreground text-xs">Category</Text>
<Text
variant="p"
className="text-foreground font-semibold"
numberOfLines={1}
>
Subscription
</Text>
</View>
</View>
</View>
</CardHeader>
<CardContent>
<Text className="font-medium text-gray-900">{invoice?.recipient ?? '—'}</Text>
<Text className="text-muted-foreground text-sm">{invoice?.recipientEmail ?? '—'}</Text>
</CardContent>
</Card>
<Card className="mb-4 overflow-hidden rounded-xl border border-border bg-white">
<CardHeader className="pb-2">
<CardTitle className="text-base">Items</CardTitle>
</CardHeader>
<CardContent className="gap-2">
{/* Items / Billing Summary */}
<Card className="mb-4 bg-card rounded-[6px]">
<View className="p-4">
<View className="flex-row items-center gap-2">
<Text variant="small" className="">
Billing Summary
</Text>
</View>
{MOCK_ITEMS.map((item, i) => (
<View key={i} className="flex-row justify-between border-b border-border py-2 last:border-0">
<Text className="text-gray-700">{item.description} × {item.qty}</Text>
<Text className="font-medium text-gray-900">${item.total.toLocaleString()}</Text>
<View
key={i}
className={`flex-row justify-between py-3 ${i < MOCK_ITEMS.length - 1 ? "border-b border-border/70" : ""}`}
>
<View className="flex-1 pr-4">
<Text
variant="p"
className="text-foreground font-semibold text-sm"
>
{item.description}
</Text>
<Text variant="muted" className="text-[10px] mt-0.5">
QTY: {item.qty} · ${item.unitPrice}/unit
</Text>
</View>
<Text variant="p" className="text-foreground font-bold text-sm">
${item.total.toLocaleString()}
</Text>
</View>
))}
<View className="mt-2 border-t border-border pt-3">
<View className="flex-row justify-between">
<Text className="font-semibold text-gray-900">Total</Text>
<Text className="font-semibold text-gray-900">${invoice?.amount.toLocaleString() ?? '1,540'}</Text>
<View className="mt-3 pt-3 flex-row justify-between items-center border-t border-border/70">
<Text variant="muted" className="font-semibold text-sm">
Total Balance
</Text>
<Text
variant="h3"
className="text-foreground font-semibold text-xl tracking-tight"
>
${invoice?.amount.toLocaleString() || "0"}
</Text>
</View>
</View>
</CardContent>
</Card>
{/* Actions */}
<View className="flex-row gap-3">
<Button variant="outline" className="min-h-12 flex-1 rounded-xl border-border" onPress={() => {}}>
<Share2 color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">Share</Text>
<Button
className=" flex-1 mb-4 h-10 rounded-[6px] bg-primary shadow-lg shadow-primary/30"
onPress={() => {}}
>
<Share2 color="#ffffff" size={14} strokeWidth={2.5} />
<Text className=" text-white text-xs font-semibold uppercase tracking-widest">
Share
</Text>
</Button>
<Button variant="outline" className="min-h-12 flex-1 rounded-xl border-border" onPress={() => {}}>
<Download color={PRIMARY} size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-gray-700">PDF</Text>
<ShadowWrapper>
<Button
className=" flex-1 mb-4 h-10 rounded-[10px] bg-card"
onPress={() => {}}
>
<Download color="#000" size={14} strokeWidth={2.5} />
<Text className="text-black text-xs font-semibold uppercase tracking-widest">
Download PDF
</Text>
</Button>
</ShadowWrapper>
</View>
<Button variant="ghost" className="mt-4 rounded-xl" onPress={() => router.back()}>
<ChevronRight className="rotate-180" color="#71717a" size={20} strokeWidth={2} />
<Text className="ml-2 font-medium text-muted-foreground">Back</Text>
</Button>
</ScrollView>
</ScreenWrapper>
);
}

View File

@ -1,44 +1,128 @@
import { View, ScrollView } from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { View, ScrollView, Pressable } from "react-native";
import { useLocalSearchParams, router, Stack } from "expo-router";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ArrowLeft, Wallet, Link2, Clock } from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
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>
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<View className="px-6 pt-4 flex-row justify-between items-center">
<Pressable
onPress={() => router.back()}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<ArrowLeft color="#0f172a" size={20} />
</Pressable>
<Text variant="h4" className="text-foreground font-semibold">
Payment Match
</Text>
<View className="w-9" />
</View>
<View className="flex-row justify-between py-2">
<Text className="text-muted-foreground">Source</Text>
<Text className="text-gray-900">Telebirr</Text>
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
showsVerticalScrollIndicator={false}
>
<Card className=" overflow-hidden rounded-[6px] border-0 bg-primary">
<View className="p-5">
<View className="flex-row items-center justify-between mb-3">
<View className="bg-white/20 p-1.5 rounded-[6px]">
<Wallet color="white" size={18} strokeWidth={2.5} />
</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 className="bg-amber-500/20 px-3 py-1 rounded-[6px] border border-white/10">
<Text className={`text-[10px] font-bold text-white`}>
Pending Match
</Text>
</View>
</View>
<Text variant="small" className="text-white/70 mb-0.5">
Received Amount
</Text>
<Text variant="h3" className="text-white font-bold mb-3">
$2,000.00
</Text>
<View className="flex-row items-center gap-3 border-t border-white/40 pt-3">
<View className="flex-row items-center gap-1.5">
<Text className="text-white/90 text-xs font-semibold">
TXN-9982734
</Text>
</View>
<View className="h-3 w-[1px] bg-white/60" />
<Text className="text-white/90 text-xs font-semibold">
Telebirr SMS
</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>
{/* Transaction Details */}
<Text variant="h4" className="text-foreground mt-4 mb-2">
Transaction Details
</Text>
<Card className="bg-card rounded-[6px] mb-3">
<View className="p-4">
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-2">
<Clock color="#000" size={13} />
<Text variant="muted" className="text-sm">
Received On
</Text>
</View>
<Text variant="p" className="text-foreground text-sm">
Sep 11, 2022 · 14:30
</Text>
</View>
<View className="h-[1px] bg-border/70 my-3" />
<View className="flex-row items-center justify-between py-1">
<View className="flex-row items-center gap-2">
<Link2 color="#000" size={13} />
<Text variant="muted" className="text-sm">
Status
</Text>
</View>
<View className="bg-amber-500/10 px-2.5 py-1 rounded-[4px]">
<Text className="text-amber-600 text-xs font-semibold">
Awaiting Link
</Text>
</View>
</View>
</View>
</Card>
{/* SMS Message */}
<Card className="bg-card rounded-[6px] mb-6">
<View className="p-4">
<Text variant="muted" className="mb-3 font-semibold">
Original SMS
</Text>
<Text className="text-foreground/70 font-medium leading-6 text-sm">
"Payment received from Elnatan Jansen for order #2322 via
Telebirr. Amount: $2,000. Ref: B88-22X7."
</Text>
</View>
</Card>
{/* Action */}
<Button className="mb-4 h-10 rounded-[10px] bg-primary shadow-lg shadow-primary/30">
<Link2 color="white" size={18} strokeWidth={2.5} />
<Text className=" text-white text-xs font-semibold uppercase tracking-widest">
Associate to Invoice
</Text>
</Button>
</ScrollView>
</ScreenWrapper>
);
}

321
app/profile.tsx Normal file
View File

@ -0,0 +1,321 @@
import React, { useState } from "react";
import {
View,
ScrollView,
Pressable,
Image,
Switch,
Modal,
TouchableOpacity,
TouchableWithoutFeedback,
} from "react-native";
import { router } from "expo-router";
import { Text } from "@/components/ui/text";
import {
ArrowLeft,
Settings,
ChevronRight,
CreditCard,
ShieldCheck,
FileText,
HelpCircle,
History,
Bell,
LogOut,
User,
Lock,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { ShadowWrapper } from "@/components/ShadowWrapper";
import { useColorScheme } from "nativewind";
import { saveTheme, AppTheme } from "@/lib/theme";
// ── Theme bottom sheet ────────────────────────────────────────────
const THEME_OPTIONS = [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "system", label: "System Default" },
] as const;
type ThemeOption = (typeof THEME_OPTIONS)[number]["value"];
function ThemeSheet({
visible,
current,
onSelect,
onClose,
}: {
visible: boolean;
current: ThemeOption;
onSelect: (v: ThemeOption) => void;
onClose: () => void;
}) {
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<TouchableWithoutFeedback onPress={onClose}>
<View className="flex-1 bg-black/40 justify-end" />
</TouchableWithoutFeedback>
<View className="bg-card rounded-t-[16px] pb-10 px-4 pt-4">
{/* Handle */}
<View className="w-10 h-1 rounded-full bg-border self-center mb-5" />
<Text variant="p" className="text-foreground font-bold mb-4 px-1">
Appearance
</Text>
{THEME_OPTIONS.map((opt, i) => {
const selected = current === opt.value;
const isLast = i === THEME_OPTIONS.length - 1;
return (
<TouchableOpacity
key={opt.value}
activeOpacity={0.7}
onPress={() => {
onSelect(opt.value);
onClose();
}}
className={`flex-row items-center justify-between py-3.5 px-1 ${!isLast ? "border-b border-border/40" : ""}`}
>
<Text
variant="p"
className={
selected ? "text-primary font-semibold" : "text-foreground"
}
>
{opt.label}
</Text>
{selected && <View className="h-2 w-2 rounded-full bg-primary" />}
</TouchableOpacity>
);
})}
</View>
</Modal>
);
}
// ── Shared menu components ────────────────────────────────────────
function MenuGroup({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<View className="mb-5">
<Text variant="muted" className="text-xs font-semibold mb-2 px-1">
{label}
</Text>
<ShadowWrapper>
<View className="bg-card rounded-[10px] overflow-hidden">
{children}
</View>
</ShadowWrapper>
</View>
);
}
function MenuItem({
icon,
label,
sublabel,
onPress,
right,
destructive = false,
isLast = false,
}: {
icon: React.ReactNode;
label: string;
sublabel?: string;
onPress?: () => void;
right?: React.ReactNode;
destructive?: boolean;
isLast?: boolean;
}) {
return (
<Pressable
onPress={onPress}
className={`flex-row items-center px-4 py-3 ${
!isLast ? "border-b border-border/40" : ""
}`}
>
<View className="h-9 w-9 rounded-[8px] items-center justify-center mr-3">
{icon}
</View>
<View className="flex-1 mt-[-10px]">
<Text
variant="p"
className={`font-medium ${destructive ? "text-red-500" : "text-foreground"}`}
>
{label}
</Text>
{sublabel ? (
<Text variant="muted" className="text-xs mt-0.5">
{sublabel}
</Text>
) : null}
</View>
{right !== undefined ? right : <ChevronRight color="#000" size={17} />}
</Pressable>
);
}
// ── Screen ────────────────────────────────────────────────────────
export default function ProfileScreen() {
const { setColorScheme, colorScheme } = useColorScheme();
const [notifications, setNotifications] = useState(true);
const [themeSheetVisible, setThemeSheetVisible] = useState(false);
const currentTheme: ThemeOption = (colorScheme as ThemeOption) ?? "system";
const handleThemeSelect = (val: AppTheme) => {
setColorScheme(val === "system" ? "system" : val);
saveTheme(val); // persist across restarts
};
return (
<ScreenWrapper className="bg-background">
{/* Header */}
<View className="px-6 pt-4 flex-row justify-between items-center">
<Pressable
onPress={() => router.back()}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<ArrowLeft color="#0f172a" size={20} />
</Pressable>
<Text variant="h4" className="text-foreground font-semibold">
Profile
</Text>
{/* Edit Profile shortcut */}
<Pressable
onPress={() => {}}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<User className="text-foreground" size={18} />
</Pressable>
</View>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingHorizontal: 16,
paddingTop: 24,
paddingBottom: 80,
}}
>
{/* Avatar */}
<View className="items-center mb-8">
<View className="h-20 w-20 rounded-full border-2 border-border overflow-hidden bg-muted mb-3">
<Image
source={{
uri: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=300&h=300",
}}
className="h-full w-full"
/>
</View>
<Text variant="h4" className="text-foreground font-bold">
Ms. Charlotte
</Text>
<Text variant="muted" className="text-sm mt-0.5">
charlotte@example.com
</Text>
</View>
{/* Account */}
<MenuGroup label="Account">
<MenuItem
icon={<CreditCard className="text-foreground" size={17} />}
label="Subscription"
sublabel="Pro Plan — active"
onPress={() => {}}
/>
<MenuItem
icon={<History className="text-foreground" size={17} />}
label="Transaction History"
onPress={() => {}}
isLast
/>
</MenuGroup>
{/* Preferences */}
<MenuGroup label="Preferences">
<MenuItem
icon={<Bell className="text-foreground" size={17} />}
label="Push Notifications"
right={
<Switch
value={notifications}
onValueChange={setNotifications}
trackColor={{ true: "#ea580c" }}
/>
}
/>
<MenuItem
icon={<Settings className="text-foreground" size={17} />}
label="Appearance"
sublabel={
THEME_OPTIONS.find((o) => o.value === currentTheme)?.label ??
"System Default"
}
onPress={() => setThemeSheetVisible(true)}
/>
<MenuItem
icon={<Lock className="text-foreground" size={17} />}
label="Security"
sublabel="PIN & Biometrics"
onPress={() => {}}
isLast
/>
</MenuGroup>
{/* Support & Legal */}
<MenuGroup label="Support & Legal">
<MenuItem
icon={<HelpCircle className="text-foreground" size={17} />}
label="Help & Support"
onPress={() => {}}
/>
<MenuItem
icon={<ShieldCheck className="text-foreground" size={17} />}
label="Privacy Policy"
onPress={() => {}}
/>
<MenuItem
icon={<FileText className="text-foreground" size={17} />}
label="Terms of Use"
onPress={() => {}}
isLast
/>
</MenuGroup>
{/* Logout */}
<ShadowWrapper>
<View className="bg-card rounded-[10px] overflow-hidden">
<MenuItem
icon={<LogOut color="#ef4444" size={17} />}
label="Log Out"
destructive
onPress={() => {}}
right={null}
isLast
/>
</View>
</ShadowWrapper>
</ScrollView>
{/* Theme sheet */}
<ThemeSheet
visible={themeSheetVisible}
current={currentTheme}
onSelect={handleThemeSelect}
onClose={() => setThemeSheetVisible(false)}
/>
</ScreenWrapper>
);
}

View File

@ -1,12 +1,34 @@
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';
import React from "react";
import { View, ScrollView, Pressable } from "react-native";
import { useLocalSearchParams, router, Stack } from "expo-router";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
ArrowLeft,
DraftingCompass,
Clock,
Tag,
Send,
ExternalLink,
ChevronRight,
CheckCircle2,
} from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
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 },
{
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;
@ -16,51 +38,174 @@ 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">
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<View className="px-6 pt-4 flex-row justify-between items-center">
<Pressable
onPress={() => router.back()}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<ArrowLeft color="#0f172a" size={20} />
</Pressable>
<Text variant="h4" className="text-foreground font-semibold">
Proforma
</Text>
<Pressable className="h-9 w-9 rounded-[6px] bg-card items-center justify-center border border-border">
<ExternalLink className="text-foreground" color="#000" size={17} />
</Pressable>
</View>
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
showsVerticalScrollIndicator={false}
>
<Card className=" overflow-hidden rounded-[6px] border-0 bg-primary">
<View className="p-5">
<View className="flex-row items-center justify-between mb-3">
<View className="bg-white/20 p-1.5 rounded-[6px]">
<DraftingCompass color="white" size={16} strokeWidth={2.5} />
</View>
<View className="bg-amber-500/20 px-3 py-1 rounded-[6px] border border-white/10">
<Text className={`text-[10px] font-bold text-white`}>
Open Request
</Text>
</View>
</View>
<Text variant="small" className="text-white/70 mb-0.5">
Target Package
</Text>
<Text variant="h3" className="text-white font-bold mb-3">
Marketing Landing Page
</Text>
<View className="flex-row items-center gap-3 border-t border-white/40 pt-3">
<View className="flex-row items-center gap-1.5">
<Text className="text-white/90 text-xs font-semibold">
Expires in 5 days
</Text>
</View>
<View className="h-3 w-[1px] bg-white/60" />
<Text className="text-white/90 text-xs font-semibold">
REQ-{id || "002"}
</Text>
</View>
</View>
</Card>
<Card className="bg-card rounded-[6px] mb-4">
<View className="p-4">
<View className="flex-row items-center gap-2">
<Text variant="small" className="font-semibold">
Line Items
</Text>
</View>
{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
key={i}
className={`flex-row justify-between py-3 ${i < MOCK_ITEMS.length - 1 ? "border-b border-border/40" : ""}`}
>
<View className="flex-1 pr-4">
<Text
variant="p"
className="text-foreground font-semibold text-sm"
>
{item.description}
</Text>
<Text variant="muted" className="text-[10px] mt-0.5">
{item.qty} × ${item.unitPrice.toLocaleString()}
</Text>
</View>
<Text variant="p" className="text-foreground font-bold text-sm">
${item.total.toLocaleString()}
</Text>
</View>
))}
<View className="mt-2 border-t border-border pt-2">
<View className="mt-3 pt-3 border-t border-border/40 gap-2">
<View className="flex-row justify-between">
<Text className="text-muted-foreground">Subtotal</Text>
<Text className="text-gray-900">${MOCK_SUBTOTAL.toLocaleString()}</Text>
<Text
variant="p"
className="text-foreground font-semibold text-sm"
>
Subtotal
</Text>
<Text variant="p" className="text-foreground font-bold text-sm">
${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>
<Text
variant="p"
className="text-foreground font-semibold text-sm"
>
Tax (10%)
</Text>
<Text variant="p" className="text-foreground font-bold text-sm">
${MOCK_TAX.toLocaleString()}
</Text>
</View>
<View className="flex-row justify-between items-center mt-1">
<Text variant="p" className="text-foreground font-semibold">
Estimated Total
</Text>
<Text
variant="h4"
className="text-foreground font-bold tracking-tight"
>
${MOCK_TOTAL.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 variant="h4" className="text-foreground mb-2">
Recent Submissions
</Text>
<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 className="bg-card rounded-[6px] mb-6">
<Pressable className="flex-row items-center p-3">
<View className="bg-secondary h-9 w-9 rounded-[6px] items-center justify-center mr-3 border border-border/50">
<CheckCircle2 className="text-muted-foreground" size={16} />
</View>
<View className="flex-1 mt-[-10px]">
<Text
variant="p"
className="text-foreground font-semibold text-sm"
>
Vendor A $1,450
</Text>
<Text variant="muted" className="text-xs mt-0.5">
Submitted 2 hours ago
</Text>
</View>
<ChevronRight className="text-muted-foreground/50" size={16} />
</Pressable>
</Card>
<View className="flex-row gap-3">
<Button
className="flex-1 h-11 rounded-[6px] bg-primary"
onPress={() => {}}
>
<Send color="#ffffff" size={14} strokeWidth={2.5} />
<Text className="ml-2 text-white font-bold text-[11px] uppercase tracking-widest">
Share
</Text>
</Button>
<Button
className="flex-1 h-11 rounded-[6px] bg-card border border-border"
onPress={() => router.back()}
>
<Text className="text-foreground font-semibold text-[11px] uppercase tracking-widest">
Back
</Text>
</Button>
</View>
</ScrollView>
</ScreenWrapper>
);
}

370
app/proforma/create.tsx Normal file
View File

@ -0,0 +1,370 @@
import React, { useState } from "react";
import {
View,
ScrollView,
Pressable,
TextInput,
StyleSheet,
Platform,
} from "react-native";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Trash2, Send, Plus } from "@/lib/icons";
import { ScreenWrapper } from "@/components/ScreenWrapper";
import { router, Stack } from "expo-router";
import { useColorScheme } from "nativewind";
import { ShadowWrapper } from "@/components/ShadowWrapper";
type Item = { id: number; description: string; qty: string; price: string };
// All TextInput styles are native StyleSheet — NO className on TextInput
// NativeWind className on TextInput causes focus loop because it re-processes
// styles each render and resets the responder chain.
const S = StyleSheet.create({
input: {
height: 44,
paddingHorizontal: 12,
fontSize: 12,
fontWeight: "500",
borderRadius: 6,
borderWidth: 1,
},
inputCenter: {
height: 44,
paddingHorizontal: 12,
fontSize: 14,
fontWeight: "500",
borderRadius: 6,
borderWidth: 1,
textAlign: "center",
},
});
function useInputColors() {
const { colorScheme } = useColorScheme();
const dark = colorScheme === "dark";
return {
bg: dark ? "rgba(30,30,30,0.8)" : "rgba(241,245,249,0.2)",
border: dark ? "rgba(255,255,255,0.08)" : "rgba(203,213,225,0.6)",
text: dark ? "#f1f5f9" : "#0f172a",
placeholder: "rgba(100,116,139,0.45)",
};
}
function Field({
label,
value,
onChangeText,
placeholder,
numeric = false,
center = false,
flex,
}: {
label: string;
value: string;
onChangeText: (v: string) => void;
placeholder: string;
numeric?: boolean;
center?: boolean;
flex?: number;
}) {
const c = useInputColors();
return (
<View style={flex != null ? { flex } : undefined}>
<Text variant="muted" className="font-semibold text-xs mb-1.5">
{label}
</Text>
<TextInput
style={[
center ? S.inputCenter : S.input,
{ backgroundColor: c.bg, borderColor: c.border, color: c.text },
]}
placeholder={placeholder}
placeholderTextColor={c.placeholder}
value={value}
onChangeText={onChangeText}
keyboardType={numeric ? "numeric" : "default"}
autoCorrect={false}
autoCapitalize="none"
returnKeyType="next"
/>
</View>
);
}
export default function CreateProformaScreen() {
const [company, setCompany] = useState("");
const [project, setProject] = useState("");
const [validity, setValidity] = useState("");
const [terms, setTerms] = useState("");
const [items, setItems] = useState<Item[]>([
{ id: 1, description: "", qty: "1", price: "" },
]);
const c = useInputColors();
const updateField = (id: number, field: keyof Item, value: string) =>
setItems((prev) =>
prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)),
);
const addItem = () =>
setItems((prev) => [
...prev,
{ id: Date.now(), description: "", qty: "1", price: "" },
]);
const removeItem = (id: number) => {
if (items.length > 1)
setItems((prev) => prev.filter((item) => item.id !== id));
};
const total = items.reduce(
(sum, item) =>
sum + (parseFloat(item.qty) || 0) * (parseFloat(item.price) || 0),
0,
);
return (
<ScreenWrapper className="bg-background">
<Stack.Screen options={{ headerShown: false }} />
<View className="px-6 pt-4 flex-row justify-between items-center">
<Pressable
onPress={() => router.back()}
className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border"
>
<ArrowLeft color="#0f172a" size={20} />
</Pressable>
<Text variant="h4" className="text-foreground font-semibold">
New Proforma
</Text>
<View className="w-9" />
</View>
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 140 }}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Recipient */}
<Label>Recipient</Label>
<ShadowWrapper>
<View className="bg-card rounded-[6px] p-4 mb-5 gap-4">
<Field
label="Company / Name"
value={company}
onChangeText={setCompany}
placeholder="e.g. Acme Corp"
/>
<Field
label="Project Title"
value={project}
onChangeText={setProject}
placeholder="e.g. Website Redesign"
/>
</View>
</ShadowWrapper>
{/* Terms */}
<Label>Terms & Validity</Label>
<ShadowWrapper>
<View className="bg-card rounded-[6px] p-4 mb-5">
<View className="flex-row gap-4">
<Field
label="Validity (days)"
value={validity}
onChangeText={setValidity}
placeholder="30"
numeric
flex={1}
/>
<Field
label="Payment Terms"
value={terms}
onChangeText={setTerms}
placeholder="e.g. 50% upfront"
flex={2}
/>
</View>
</View>
</ShadowWrapper>
{/* Items */}
<View className="flex-row items-center justify-between mb-3">
<Label noMargin>Billable Items</Label>
<Pressable
onPress={addItem}
className="flex-row items-center gap-1 px-3 py-1 rounded-[6px] bg-primary/10 border border-primary/20"
>
<Plus color="#ea580c" size={10} strokeWidth={2.5} />
<Text className="text-primary text-[8px] font-bold uppercase tracking-widest">
Add Item
</Text>
</Pressable>
</View>
<View className="gap-3 mb-5">
{items.map((item, index) => (
<ShadowWrapper>
<View key={item.id} className="bg-card rounded-[6px] p-4">
<View className="flex-row justify-between items-center mb-3">
<Text
variant="muted"
className="text-[12px] font-bold uppercase tracking-wide"
>
Item {index + 1}
</Text>
{items.length > 1 && (
<Pressable onPress={() => removeItem(item.id)} hitSlop={8}>
<Trash2 color="#ef4444" size={13} />
</Pressable>
)}
</View>
<Text
variant="muted"
className="text-[11px] font-semibold mb-1.5"
>
Description
</Text>
<TextInput
style={[
S.input,
{
backgroundColor: c.bg,
borderColor: c.border,
color: c.text,
marginBottom: 12,
},
]}
placeholder="e.g. Web Design Package"
placeholderTextColor={c.placeholder}
value={item.description}
onChangeText={(v) => updateField(item.id, "description", v)}
autoCorrect={false}
autoCapitalize="none"
returnKeyType="next"
/>
<View className="flex-row gap-3">
<View className="flex-1">
<Text
variant="muted"
className="text-[11px] font-semibold mb-1.5"
>
Qty
</Text>
<TextInput
style={[
S.inputCenter,
{
backgroundColor: c.bg,
borderColor: c.border,
color: c.text,
},
]}
placeholder="1"
placeholderTextColor={c.placeholder}
keyboardType="numeric"
value={item.qty}
onChangeText={(v) => updateField(item.id, "qty", v)}
returnKeyType="next"
/>
</View>
<View className="flex-[2]">
<Text
variant="muted"
className="text-[11px] font-semibold mb-1.5"
>
Unit Price ($)
</Text>
<TextInput
style={[
S.input,
{
backgroundColor: c.bg,
borderColor: c.border,
color: c.text,
},
]}
placeholder="0.00"
placeholderTextColor={c.placeholder}
keyboardType="numeric"
value={item.price}
onChangeText={(v) => updateField(item.id, "price", v)}
returnKeyType="done"
/>
</View>
<View className="flex-1 items-end justify-end pb-1">
<Text variant="muted" className="text-[10px]">
Total
</Text>
<Text
variant="p"
className="text-foreground font-bold text-sm"
>
$
{(
(parseFloat(item.qty) || 0) *
(parseFloat(item.price) || 0)
).toFixed(2)}
</Text>
</View>
</View>
</View>
</ShadowWrapper>
))}
</View>
{/* Summary */}
<View className="border border-border/60 rounded-[6px] p-4 bg-secondary/10 mb-6">
<View className="flex-row justify-between items-center mb-4">
<Text variant="muted" className="font-semibold text-sm">
Estimated Total
</Text>
<Text variant="h4" className="text-foreground font-semibold">
$
{total.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</Text>
</View>
<View className="flex-row gap-3">
<Button
variant="outline"
className="flex-1 h-11 rounded-[6px] border-border bg-card"
onPress={() => router.back()}
>
<Text className="text-foreground font-semibold text-[11px] uppercase tracking-widest">
Cancel
</Text>
</Button>
<Button className="flex-1 h-11 rounded-[6px] bg-primary">
<Send color="white" size={14} strokeWidth={2.5} />
<Text className=" text-white font-bold text-[11px] uppercase tracking-widest">
Create & Share
</Text>
</Button>
</View>
</View>
</ScrollView>
</ScreenWrapper>
);
}
function Label({
children,
noMargin,
}: {
children: string;
noMargin?: boolean;
}) {
return (
<Text variant="muted" className={`font-semibold ${noMargin ? "" : "mb-3"}`}>
{children}
</Text>
);
}

View File

@ -2,8 +2,9 @@ module.exports = function (api) {
api.cache(true);
return {
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
plugins: ["react-native-reanimated/plugin"],
};
};

BIN
build-1772357609513.aab Normal file

Binary file not shown.

BIN
build-1772358682783.apk Normal file

Binary file not shown.

View File

@ -0,0 +1,33 @@
import React from "react";
import {
View,
ViewProps,
SafeAreaView,
Platform,
StatusBar,
} from "react-native";
import { cn } from "@/lib/utils";
interface ScreenWrapperProps extends ViewProps {
children: React.ReactNode;
withSafeArea?: boolean;
fixedHeader?: boolean;
}
export function ScreenWrapper({
children,
className,
containerClassName,
withSafeArea = true,
fixedHeader = false,
...props
}: ScreenWrapperProps & { containerClassName?: string }) {
const Container = withSafeArea ? SafeAreaView : View;
return (
<View className={cn("flex-1 bg-background", containerClassName)} {...props}>
<StatusBar barStyle="dark-content" />
<Container className={cn("flex-1", className)}>{children}</Container>
</View>
);
}

View File

@ -0,0 +1,31 @@
import React from "react";
import { View, ViewProps, Platform } from "react-native";
import { cn } from "@/lib/utils";
interface ShadowWrapperProps extends ViewProps {
level?: "none" | "xs" | "sm" | "md" | "lg" | "xl";
children: React.ReactNode;
className?: string;
}
export function ShadowWrapper({
level = "md",
className,
children,
...props
}: ShadowWrapperProps) {
const shadowClasses = {
none: "",
xs: "shadow-sm shadow-slate-200/30",
sm: "shadow-sm shadow-slate-200/50",
md: "shadow-md shadow-slate-200/60",
lg: "shadow-xl shadow-slate-200/70",
xl: "shadow-2xl shadow-slate-300/40",
};
return (
<View className={cn(shadowClasses[level], className)} {...props}>
{children}
</View>
);
}

View File

@ -0,0 +1,46 @@
import React from "react";
import { View, Image, Pressable } from "react-native";
import { Text } from "@/components/ui/text";
import { Bell } from "@/lib/icons";
import { ShadowWrapper } from "@/components/ShadowWrapper";
import { MOCK_USER } from "@/lib/mock-data";
import { router } from "expo-router";
export function StandardHeader() {
return (
<View className="px-5 pt-4 pb-2 flex-row justify-between items-center bg-background">
<View className="flex-row items-center gap-3">
<ShadowWrapper level="xs">
<Pressable
onPress={() => router.push("/profile")}
className="h-[40px] w-[40px] rounded-full border-2 border-primary/20 overflow-hidden"
>
<Image
source={{
uri: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&q=80&w=200&h=200",
}}
className="h-full w-full"
/>
</Pressable>
</ShadowWrapper>
<View>
<Text
variant="muted"
className="text-[10px] uppercase tracking-widest font-bold"
>
Welcome back,
</Text>
<Text variant="h4" className="text-foreground leading-tight">
{MOCK_USER.name}
</Text>
</View>
</View>
<ShadowWrapper level="xs">
<Pressable className="rounded-full p-2.5 border border-border">
<Bell color="#000" size={20} strokeWidth={2} />
</Pressable>
</ShadowWrapper>
</View>
);
}

View File

@ -1,23 +1,28 @@
import { Text, TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import { View, type ViewProps } from 'react-native';
import { Text, TextClassContext } from "@/components/ui/text";
import { cn } from "@/lib/utils";
import { View, type ViewProps } from "react-native";
import { ShadowWrapper } from "../ShadowWrapper";
function Card({ className, ...props }: ViewProps & React.RefAttributes<View>) {
return (
<TextClassContext.Provider value="text-card-foreground">
<ShadowWrapper>
<View
className={cn(
'bg-card border-border flex flex-col gap-6 rounded-xl border py-6 shadow-sm shadow-black/5',
className
)}
className={cn("bg-card flex flex-col gap-4 rounded-xl ", className)}
{...props}
/>
</ShadowWrapper>
</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 CardHeader({
className,
...props
}: ViewProps & React.RefAttributes<View>) {
return (
<View className={cn("flex flex-col gap-1.5 px-6", className)} {...props} />
);
}
function CardTitle({
@ -28,7 +33,7 @@ function CardTitle({
<Text
role="heading"
aria-level={3}
className={cn('font-semibold leading-none', className)}
className={cn("font-semibold leading-none", className)}
{...props}
/>
);
@ -38,15 +43,38 @@ function CardDescription({
className,
...props
}: React.ComponentProps<typeof Text> & React.RefAttributes<Text>) {
return <Text className={cn('text-muted-foreground text-sm', className)} {...props} />;
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 CardContent({
className,
...props
}: ViewProps & React.RefAttributes<View>) {
return <View className={cn("px-4", className)} {...props} />;
}
function CardFooter({ className, ...props }: ViewProps & React.RefAttributes<View>) {
return <View className={cn('flex flex-row items-center 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 };
export {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
};

View File

@ -1,65 +1,71 @@
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';
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',
"text-foreground text-base",
Platform.select({
web: 'select-text',
})
web: "select-text",
}),
),
{
variants: {
variant: {
default: '',
default: "",
h1: cn(
'text-center text-4xl font-extrabold tracking-tight',
Platform.select({ web: 'scroll-m-20 text-balance' })
"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' })
"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',
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'
"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',
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',
variant: "default",
},
},
}
);
type TextVariantProps = VariantProps<typeof textVariants>;
type TextVariant = NonNullable<TextVariantProps['variant']>;
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 }),
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',
h1: "1",
h2: "2",
h3: "3",
h4: "4",
};
const TextClassContext = React.createContext<string | undefined>(undefined);
@ -67,7 +73,7 @@ const TextClassContext = React.createContext<string | undefined>(undefined);
function Text({
className,
asChild = false,
variant = 'default',
variant = "default",
...props
}: React.ComponentProps<typeof RNText> &
TextVariantProps &

24
eas.json Normal file
View File

@ -0,0 +1,24 @@
{
"cli": {
"version": ">= 18.0.5",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

73
ios_build_guide.md Normal file
View File

@ -0,0 +1,73 @@
# iOS Build Guide for Expo
This project uses the **Expo Managed Workflow**, which means the `ios` and `android` native directories are generated automatically via **Continuous Native Generation (CNG)**. You should not see or manually edit an `ios` folder in your project root.
---
## 1. Development (Expo Go)
The easiest way to build/run for iOS during development is using the **Expo Go** app on your iPhone.
1. Install **Expo Go** from the App Store.
2. Run the development server:
```bash
npx expo start
```
3. Scan the QR code with your Camera app to open the project in Expo Go.
---
## 2. Local Native Development (Prebuild)
If you need to test native modules, use a custom dev client, or specifically need the `ios` folder for debugging in Xcode:
1. Generate the native directories:
```bash
npx expo prebuild
```
_This will create the `ios` and `android` folders based on your `app.json` configuration._
2. Run on the iOS Simulator (requires macOS + Xcode):
```bash
npx expo run:ios
```
> [!WARNING]
> The `ios` folder is typically gitignored. In the managed workflow, any changes you make manually in the `ios` folder may be overwritten the next time you run `prebuild`. Always use `app.json` or config plugins for permanent configuration.
---
## 3. Production Builds (EAS Build)
To build a `.ipa` file for TestFlight or the App Store, the recommended way is using **Expo Application Services (EAS)**.
1. Install EAS CLI:
```bash
npm install -g eas-cli
```
2. Log in to your Expo account:
```bash
eas login
```
3. Configure the project (run once):
```bash
eas build:configure
```
4. Run a build for iOS:
```bash
eas build --platform ios
```
_EAS will handle certificates, provisioning profiles, and building on their servers (no macOS/Xcode required locally)._
---
## Summary of Commands
| Goal | Command |
| :----------------------- | :------------------------- |
| **Start Dev Server** | `npx expo start` |
| **Generate iOS Folder** | `npx expo prebuild` |
| **Run on iOS Simulator** | `npx expo run:ios` |
| **Build for Production** | `eas build --platform ios` |

View File

@ -34,4 +34,24 @@ export {
BarChart3,
Upload,
UserPlus,
} from 'lucide-react-native';
Briefcase,
Layout,
Hash,
Star,
Trash2,
X,
History,
DraftingCompass,
Zap,
Tag,
CreditCard,
Building2,
ExternalLink,
Scan,
TrendingUp,
TrendingDown,
ShieldCheck,
HelpCircle,
ArrowUpRight,
Lock,
} from "lucide-react-native";

View File

@ -1,61 +1,61 @@
import { DarkTheme, DefaultTheme, type Theme } from '@react-navigation/native';
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%)',
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%)',
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> = {
export const NAV_THEME: Record<"light" | "dark", Theme> = {
light: {
...DefaultTheme,
colors: {
@ -79,3 +79,33 @@ export const NAV_THEME: Record<'light' | 'dark', Theme> = {
},
},
};
// ── Persistent theme helpers ──────────────────────────────────────
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useEffect } from "react";
import { useColorScheme } from "nativewind";
export type AppTheme = "light" | "dark" | "system";
const THEME_KEY = "app_theme_preference";
export async function saveTheme(theme: AppTheme): Promise<void> {
try {
await AsyncStorage.setItem(THEME_KEY, theme);
} catch {}
}
export async function loadTheme(): Promise<AppTheme> {
try {
const v = await AsyncStorage.getItem(THEME_KEY);
if (v === "light" || v === "dark" || v === "system") return v;
} catch {}
return "system";
}
/** Drop this in the root _layout to restore the saved theme on every app launch. */
export function useRestoreTheme() {
const { setColorScheme } = useColorScheme();
useEffect(() => {
loadTheme().then((t) => setColorScheme(t));
}, []);
}

8753
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,40 +6,46 @@
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
"web": "expo start --web",
"postinstall": "patch-package"
},
"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",
"@expo/metro-runtime": "~4.0.1",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/native": "^7.0.14",
"@rn-primitives/portal": "^1.1.0",
"@rn-primitives/slot": "^1.1.0",
"babel-preset-expo": "~11.0.15",
"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",
"lucide-react-native": "^0.575.0",
"nativewind": "^4.2.2",
"react": "19.1.0",
"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-svg": "^15.15.3",
"react-native-web": "^0.21.0",
"tailwind-merge": "^3.5.0",
"expo": "~52.0.35",
"expo-camera": "~16.0.18",
"expo-constants": "~17.0.7",
"expo-linear-gradient": "~14.0.2",
"expo-linking": "~7.0.5",
"expo-router": "~4.0.17",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.9",
"lucide-react-native": "^0.471.0",
"nativewind": "^4.1.23",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/react": "~19.1.0",
"@types/react": "~18.3.12",
"patch-package": "^8.0.1",
"prettier-plugin-tailwindcss": "^0.5.14",
"tailwindcss": "^3.4.19",
"typescript": "~5.9.2"
"tailwindcss": "^3.4.17",
"typescript": "^5.3.3"
},
"private": true
}

View File

@ -0,0 +1,28 @@
diff --git a/node_modules/react-native-css-interop/.cache/android.js b/node_modules/react-native-css-interop/.cache/android.js
new file mode 100644
index 0000000..e69de29
diff --git a/node_modules/react-native-css-interop/.cache/ios.js b/node_modules/react-native-css-interop/.cache/ios.js
new file mode 100644
index 0000000..e69de29
diff --git a/node_modules/react-native-css-interop/.cache/macos.js b/node_modules/react-native-css-interop/.cache/macos.js
new file mode 100644
index 0000000..e69de29
diff --git a/node_modules/react-native-css-interop/.cache/native.js b/node_modules/react-native-css-interop/.cache/native.js
new file mode 100644
index 0000000..e69de29
diff --git a/node_modules/react-native-css-interop/.cache/windows.js b/node_modules/react-native-css-interop/.cache/windows.js
new file mode 100644
index 0000000..e69de29
diff --git a/node_modules/react-native-css-interop/babel.js b/node_modules/react-native-css-interop/babel.js
index d84e52b..6e6fd21 100644
--- a/node_modules/react-native-css-interop/babel.js
+++ b/node_modules/react-native-css-interop/babel.js
@@ -10,7 +10,7 @@ module.exports = function () {
},
],
// Use this plugin in reanimated 4 and later
- "react-native-worklets/plugin",
+ // "react-native-worklets/plugin",
],
};
};