15 files changed
+178
-91
No files matched your search
+16
-13
@@ -5,6 +5,7 @@ import * as Notifications from 'expo-notifications';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import React from 'react';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { initialWindowMetrics, SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import 'expo-dev-client';
|
||||
|
||||
import { AuthProvider } from '@/auth/AuthContext';
|
||||
@@ -27,19 +28,21 @@ export default function App() {
|
||||
useDeepLinking();
|
||||
return (
|
||||
<GestureHandlerRootView>
|
||||
<StatusBar style="light" />
|
||||
<PersistQueryClientProvider client={appQueryClient} persistOptions={persistOptions}>
|
||||
<AuthProvider>
|
||||
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
|
||||
<StripeProvider
|
||||
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
|
||||
urlScheme={Constants.expoConfig?.scheme as string}
|
||||
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
|
||||
>
|
||||
<AppInner />
|
||||
</StripeProvider>
|
||||
</AuthProvider>
|
||||
</PersistQueryClientProvider>
|
||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||
<StatusBar style="light" />
|
||||
<PersistQueryClientProvider client={appQueryClient} persistOptions={persistOptions}>
|
||||
<AuthProvider>
|
||||
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
|
||||
<StripeProvider
|
||||
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
|
||||
urlScheme={Constants.expoConfig?.scheme as string}
|
||||
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
|
||||
>
|
||||
<AppInner />
|
||||
</StripeProvider>
|
||||
</AuthProvider>
|
||||
</PersistQueryClientProvider>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
+63
-40
@@ -2,8 +2,11 @@ import { BottomTabNavigationOptions, createBottomTabNavigator } from '@react-nav
|
||||
import { DefaultTheme, NavigationContainer } from '@react-navigation/native';
|
||||
import { createNativeStackNavigator, NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import TabButton from '@/components/TabButton';
|
||||
import TabIcon, { PILL_SIZE } from '@/components/TabIcon';
|
||||
import About from '@/modules/about/About';
|
||||
import EventList from '@/modules/events/EventList';
|
||||
import Login from '@/modules/login/Login';
|
||||
@@ -14,9 +17,9 @@ import Settings from '@/modules/settings/Settings';
|
||||
import SettingsHeaderButton from '@/modules/settings/SettingsHeaderButton';
|
||||
import theme from '@/theme';
|
||||
import type { EventStackParamList, HomeTabParamList, UserStackParamList } from '@/types/navigation';
|
||||
import IconEvent from '@assets/icons/event.svg';
|
||||
import IconInfo from '@assets/icons/information-circle.svg';
|
||||
import IconPerson from '@assets/icons/person.svg';
|
||||
import IconCalendar from '@assets/icons/calendar.svg';
|
||||
import IconIdCard from '@assets/icons/id-card.svg';
|
||||
import IconInfo from '@assets/icons/info.svg';
|
||||
|
||||
const defaultNavigationStyle = {
|
||||
headerTintColor: theme.colors.white,
|
||||
@@ -83,41 +86,61 @@ const UserStackScreen = () => {
|
||||
type TabBarIconType = BottomTabNavigationOptions['tabBarIcon'];
|
||||
|
||||
const HomeTabs = createBottomTabNavigator<HomeTabParamList>();
|
||||
const EventIcon: TabBarIconType = ({ color, size }) => <IconEvent width={size} height={size} fill={color} />;
|
||||
const UserIcon: TabBarIconType = ({ color, size }) => <IconPerson width={size + 8} height={size + 8} fill={color} />;
|
||||
const AboutIcon: TabBarIconType = ({ color, size }) => <IconInfo width={size} height={size} fill={color} />;
|
||||
const Navigation = () => (
|
||||
<NavigationContainer theme={navigationTheme}>
|
||||
<HomeTabs.Navigator initialRouteName="EventStack">
|
||||
<HomeTabs.Screen
|
||||
name="EventStack"
|
||||
component={EventStackScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarLabel: 'Program',
|
||||
tabBarIcon: EventIcon,
|
||||
}}
|
||||
/>
|
||||
<HomeTabs.Screen
|
||||
name="UserStack"
|
||||
component={UserStackScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarLabel: 'Medlemskap',
|
||||
tabBarIcon: UserIcon,
|
||||
}}
|
||||
/>
|
||||
<HomeTabs.Screen
|
||||
name="About"
|
||||
component={About}
|
||||
options={{
|
||||
title: 'Om oss',
|
||||
tabBarLabel: 'Om oss',
|
||||
tabBarIcon: AboutIcon,
|
||||
...defaultNavigationStyle,
|
||||
}}
|
||||
/>
|
||||
</HomeTabs.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
const EventIcon: TabBarIconType = (props) => <TabIcon icon={IconCalendar} {...props} />;
|
||||
const UserIcon: TabBarIconType = (props) => <TabIcon icon={IconIdCard} {...props} />;
|
||||
const AboutIcon: TabBarIconType = (props) => <TabIcon icon={IconInfo} {...props} />;
|
||||
|
||||
const tabBarOptions: BottomTabNavigationOptions = {
|
||||
tabBarActiveTintColor: theme.tabBar.activeTint,
|
||||
tabBarInactiveTintColor: theme.tabBar.inactiveTint,
|
||||
tabBarLabelStyle: { fontSize: 11, fontWeight: '500' },
|
||||
tabBarIconStyle: PILL_SIZE,
|
||||
tabBarButton: TabButton,
|
||||
};
|
||||
|
||||
const Navigation = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
// Setting a height replaces the navigator's own inset handling, so the inset is added back here
|
||||
const tabBarStyle = {
|
||||
height: theme.tabBar.height + insets.bottom,
|
||||
backgroundColor: theme.tabBar.background,
|
||||
borderTopWidth: 0,
|
||||
elevation: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationContainer theme={navigationTheme}>
|
||||
<HomeTabs.Navigator initialRouteName="EventStack" screenOptions={{ ...tabBarOptions, tabBarStyle }}>
|
||||
<HomeTabs.Screen
|
||||
name="EventStack"
|
||||
component={EventStackScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarLabel: 'Program',
|
||||
tabBarIcon: EventIcon,
|
||||
}}
|
||||
/>
|
||||
<HomeTabs.Screen
|
||||
name="UserStack"
|
||||
component={UserStackScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarLabel: 'Medlemskap',
|
||||
tabBarIcon: UserIcon,
|
||||
}}
|
||||
/>
|
||||
<HomeTabs.Screen
|
||||
name="About"
|
||||
component={About}
|
||||
options={{
|
||||
title: 'Om oss',
|
||||
tabBarLabel: 'Om oss',
|
||||
tabBarIcon: AboutIcon,
|
||||
...defaultNavigationStyle,
|
||||
}}
|
||||
/>
|
||||
</HomeTabs.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
export default Navigation;
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
|
||||
import React from 'react';
|
||||
import { Pressable } from 'react-native';
|
||||
|
||||
// The stock button's borderless Android ripple flashes a circle well past the tab; the pill is feedback enough
|
||||
const TabButton = ({
|
||||
android_ripple,
|
||||
pressColor,
|
||||
pressOpacity,
|
||||
hoverEffect,
|
||||
ref,
|
||||
...props
|
||||
}: BottomTabBarButtonProps) => <Pressable {...props} />;
|
||||
|
||||
export default TabButton;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import theme from '@/theme';
|
||||
|
||||
type SvgComponent = React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||
|
||||
export type TabIconProps = {
|
||||
icon: SvgComponent;
|
||||
focused: boolean;
|
||||
color: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export const PILL_SIZE = { width: 60, height: 32 };
|
||||
|
||||
const TabIcon = ({ icon: Icon, focused, color, size }: TabIconProps) => (
|
||||
<View style={[styles.pill, focused && styles.pillActive]}>
|
||||
<Icon width={size} height={size} color={focused ? theme.tabBar.pill.text : color} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pill: {
|
||||
...PILL_SIZE,
|
||||
borderRadius: 999,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
pillActive: {
|
||||
backgroundColor: theme.tabBar.pill.background,
|
||||
},
|
||||
});
|
||||
|
||||
export default TabIcon;
|
||||
+10
-1
@@ -38,6 +38,15 @@ const buttons = {
|
||||
secondary: { background: colors.goldenBeige, text: colors.deepBrick },
|
||||
};
|
||||
|
||||
// Bottom tab bar: the screen's own surface with no edge, the active icon in a pill of the secondary button colours
|
||||
const tabBar = {
|
||||
height: 56,
|
||||
background: colors.betongGray,
|
||||
activeTint: colors.chateauBlue,
|
||||
inactiveTint: colors.chateauBlue05,
|
||||
pill: buttons.secondary,
|
||||
};
|
||||
|
||||
// neuf.no's type scale at phone widths: body 1.5 line height, headings 600 at 1.2
|
||||
const typography = {
|
||||
h1: { fontSize: 32, fontWeight: '600', lineHeight: 38 },
|
||||
@@ -49,4 +58,4 @@ const typography = {
|
||||
suphead: { fontSize: 14, fontWeight: '700', lineHeight: 21, textTransform: 'uppercase', letterSpacing: 0.7 },
|
||||
} as const;
|
||||
|
||||
export default { card, colors, background, text, typography, buttons };
|
||||
export default { card, colors, background, text, typography, buttons, tabBar };
|
||||
Reference in new issue
Block a user