ui
This commit is contained in:
parent
837e3f4646
commit
1b41dbd97a
39
app.json
39
app.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
117
app/(tabs)/history.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
321
app/profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
370
app/proforma/create.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
BIN
build-1772357609513.aab
Normal file
Binary file not shown.
BIN
build-1772358682783.apk
Normal file
BIN
build-1772358682783.apk
Normal file
Binary file not shown.
33
components/ScreenWrapper.tsx
Normal file
33
components/ScreenWrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
components/ShadowWrapper.tsx
Normal file
31
components/ShadowWrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
components/StandardHeader.tsx
Normal file
46
components/StandardHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
24
eas.json
Normal 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
73
ios_build_guide.md
Normal 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` |
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
130
lib/theme.ts
130
lib/theme.ts
|
|
@ -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
8753
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
28
patches/react-native-css-interop+0.2.2.patch
Normal file
28
patches/react-native-css-interop+0.2.2.patch
Normal 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",
|
||||
],
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user