130 lines
4.0 KiB
TypeScript
130 lines
4.0 KiB
TypeScript
import React from 'react';
|
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { Tabs, useRouter } from 'expo-router';
|
|
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
|
import { useT } from '@/i18n';
|
|
import { colors, fontSize, shadow, spacing } from '@/theme';
|
|
|
|
const FAB_SIZE = 58;
|
|
/** Width reserved for the centre action button, flanked by two tabs a side. */
|
|
const FAB_SLOT = 84;
|
|
|
|
const TAB_ICONS: Record<string, keyof typeof Ionicons.glyphMap> = {
|
|
index: 'home',
|
|
search: 'search',
|
|
expiring: 'time',
|
|
profile: 'person',
|
|
};
|
|
|
|
const TAB_LABEL_KEYS: Record<string, string> = {
|
|
index: 'tabs.home',
|
|
search: 'tabs.search',
|
|
expiring: 'tabs.expiring',
|
|
profile: 'tabs.profile',
|
|
};
|
|
|
|
function TabBar({ state, navigation }: BottomTabBarProps) {
|
|
const insets = useSafeAreaInsets();
|
|
const router = useRouter();
|
|
const t = useT();
|
|
|
|
const renderTab = (routeName: string) => {
|
|
const index = state.routes.findIndex((r) => r.name === routeName);
|
|
if (index < 0) return <View style={styles.tab} key={routeName} />;
|
|
const route = state.routes[index];
|
|
const focused = state.index === index;
|
|
return (
|
|
<Pressable
|
|
key={routeName}
|
|
style={styles.tab}
|
|
onPress={() => {
|
|
const event = navigation.emit({
|
|
type: 'tabPress',
|
|
target: route.key,
|
|
canPreventDefault: true,
|
|
});
|
|
if (!focused && !event.defaultPrevented) navigation.navigate(route.name);
|
|
}}
|
|
>
|
|
<Ionicons
|
|
name={TAB_ICONS[routeName]}
|
|
size={22}
|
|
color={focused ? colors.teal600 : colors.slate400}
|
|
/>
|
|
<Text style={[styles.tabLabel, focused && { color: colors.teal600 }]} numberOfLines={1}>
|
|
{t(TAB_LABEL_KEYS[routeName])}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
};
|
|
|
|
// Two tabs, action, two tabs: symmetric, so the button sits on the midpoint.
|
|
return (
|
|
<View style={[styles.bar, { paddingBottom: insets.bottom || spacing[3] }]}>
|
|
<View style={styles.row}>
|
|
<View style={styles.group}>
|
|
{renderTab('index')}
|
|
{renderTab('search')}
|
|
</View>
|
|
|
|
<View style={styles.fabSlot}>
|
|
<Pressable
|
|
style={({ pressed }) => [styles.fab, pressed && { opacity: 0.9 }]}
|
|
onPress={() => router.push('/upload')}
|
|
accessibilityLabel={t('tabs.upload')}
|
|
>
|
|
<Ionicons name="cloud-upload-outline" size={26} color={colors.onDark} />
|
|
</Pressable>
|
|
<Text style={styles.fabLabel} numberOfLines={1}>
|
|
{t('tabs.upload')}
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.group}>
|
|
{renderTab('expiring')}
|
|
{renderTab('profile')}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export default function TabsLayout() {
|
|
return (
|
|
<Tabs screenOptions={{ headerShown: false }} tabBar={(props) => <TabBar {...props} />}>
|
|
<Tabs.Screen name="index" />
|
|
<Tabs.Screen name="search" />
|
|
<Tabs.Screen name="expiring" />
|
|
<Tabs.Screen name="profile" />
|
|
</Tabs>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
bar: {
|
|
backgroundColor: colors.surface,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.border,
|
|
paddingTop: spacing[2],
|
|
...shadow.sm,
|
|
},
|
|
row: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: spacing[1] },
|
|
group: { flex: 1, flexDirection: 'row' },
|
|
tab: { flex: 1, alignItems: 'center', gap: 3, paddingVertical: 4 },
|
|
tabLabel: { fontSize: fontSize['2xs'], fontWeight: '600', color: colors.slate400 },
|
|
fabSlot: { width: FAB_SLOT, alignItems: 'center' },
|
|
fab: {
|
|
width: FAB_SIZE,
|
|
height: FAB_SIZE,
|
|
borderRadius: FAB_SIZE / 2,
|
|
backgroundColor: colors.teal600,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginTop: -FAB_SIZE / 2,
|
|
...shadow.md,
|
|
},
|
|
fabLabel: { fontSize: fontSize['2xs'], fontWeight: '600', color: colors.teal700, marginTop: 3 },
|
|
});
|