15 files changed
+178
-91
No files matched your search
@@ -55,6 +55,13 @@
|
||||
{}
|
||||
],
|
||||
"expo-status-bar",
|
||||
[
|
||||
"expo-navigation-bar",
|
||||
{
|
||||
"enforceContrast": false,
|
||||
"style": "dark"
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- The calendar icon from neuf-www (web/src/components/general/Icon.tsx) -->
|
||||
<svg viewBox="0 -960 960 960" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M204-90q-30.94 0-52.97-22.03Q129-134.06 129-165v-552q0-30.94 22.03-52.97Q173.06-792 204-792h45v-80h75v80h312v-80h75v80h45q30.94 0 52.97 22.03Q831-747.94 831-717v552q0 30.94-22.03 52.97Q786.94-90 756-90H204Zm0-75h552v-393H204v393Zm0-468h552v-84H204v84Zm0 0v-84 84Zm276.05 232q-16.55 0-27.8-11.2Q441-423.39 441-439.95q0-16.55 11.2-27.8Q463.39-479 479.95-479q16.55 0 27.8 11.2Q519-456.61 519-440.05q0 16.55-11.2 27.8Q496.61-401 480.05-401Zm-160 0q-16.55 0-27.8-11.2Q281-423.39 281-439.95q0-16.55 11.2-27.8Q303.39-479 319.95-479q16.55 0 27.8 11.2Q359-456.61 359-440.05q0 16.55-11.2 27.8Q336.61-401 320.05-401Zm320 0q-16.55 0-27.8-11.2Q601-423.39 601-439.95q0-16.55 11.2-27.8Q623.39-479 639.95-479q16.55 0 27.8 11.2Q679-456.61 679-440.05q0 16.55-11.2 27.8Q656.61-401 640.05-401Zm-160 160q-16.55 0-27.8-11.2Q441-263.39 441-279.95q0-16.55 11.2-27.8Q463.39-319 479.95-319q16.55 0 27.8 11.2Q519-296.61 519-280.05q0 16.55-11.2 27.8Q496.61-241 480.05-241Zm-160 0q-16.55 0-27.8-11.2Q281-263.39 281-279.95q0-16.55 11.2-27.8Q303.39-319 319.95-319q16.55 0 27.8 11.2Q359-296.61 359-280.05q0 16.55-11.2 27.8Q336.61-241 320.05-241Zm320 0q-16.55 0-27.8-11.2Q601-263.39 601-279.95q0-16.55 11.2-27.8Q623.39-319 639.95-319q16.55 0 27.8 11.2Q679-296.61 679-280.05q0 16.55-11.2 27.8Q656.61-241 640.05-241Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M676 862c-16 0-28-13-28-29V691c0-16 12-28 28-28h142c16 0 29 12 29 28v142c0 16-13 29-29 29H676zm142-171H676v142h142V691zM960 96c35 0 64 29 64 64v800c0 35-29 64-64 64H64c-35 0-64-29-64-64V160c0-35 29-64 64-64h256V32c0-18 14-32 32-32s32 14 32 32v64h256V32c0-18 14-32 32-32s32 14 32 32v64h256zM64 960h896V160H704v32c0 18-14 32-32 32s-32-14-32-32v-32H384v32c0 18-14 32-32 32s-32-14-32-32v-32H64v800z"/></svg>
|
||||
|
Before Width: | Height: | Size: 640 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Material Symbols Outlined "id_card" at weight 600, the set and weight neuf-www uses (Apache 2.0) -->
|
||||
<svg viewBox="0 -960 960 960" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M585.09-432.48h169.82v-66.78H585.09v66.78Zm0-126.78h169.82v-66.78H585.09v66.78Zm-380 234.74h320v-10.52q0-41.44-42.5-68.22t-117.5-26.78q-75 0-117.5 26.78t-42.5 68.22v10.52Zm159.92-165.52q30.08 0 51.58-21.14t21.5-51.78q0-30.08-21.42-51.58t-51.5-21.5q-30.08 0-51.58 21.42t-21.5 51.5q0 30.64 21.42 51.86t51.5 21.22ZM145.09-145.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-509.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h669.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v509.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H145.09Zm0-79.22h669.82v-509.82H145.09v509.82Zm0 0v-509.82 509.82Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 855 B |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Material Symbols Outlined "info" at weight 600, the set and weight neuf-www uses (Apache 2.0) -->
|
||||
<svg viewBox="0 -960 960 960" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M446.78-273.78h72.44V-520h-72.44v246.22Zm33.25-311.74q17.64 0 29.55-11.64 11.9-11.64 11.9-28.84 0-18.69-11.92-30.58-11.92-11.9-29.54-11.9-18.26 0-29.88 11.9-11.62 11.89-11.62 30.3 0 17.55 11.94 29.16 11.93 11.6 29.57 11.6Zm.31 519.65q-86.2 0-161.5-32.39-75.3-32.4-131.74-88.84-56.44-56.44-88.84-131.73-32.39-75.3-32.39-161.59t32.39-161.67q32.4-75.37 88.75-131.34t131.69-88.62q75.34-32.65 161.67-32.65 86.34 0 161.78 32.61 75.45 32.6 131.37 88.5 55.93 55.89 88.55 131.45 32.63 75.56 32.63 161.87 0 86.29-32.65 161.58t-88.62 131.48q-55.97 56.18-131.42 88.76-75.46 32.58-161.67 32.58Zm.16-79.22q139.24 0 236.83-97.73 97.58-97.73 97.58-237.68 0-139.24-97.4-236.83-97.4-97.58-237.79-97.58-139.02 0-236.83 97.4-97.8 97.4-97.8 237.79 0 139.02 97.73 236.83 97.73 97.8 237.68 97.8ZM480-480Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>information-circle</title>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="invisible_box" data-name="invisible box">
|
||||
<rect width="48" height="48" fill="none"/>
|
||||
</g>
|
||||
<g id="icons_Q2" data-name="icons Q2">
|
||||
<path d="M24,2A22,22,0,1,0,46,24,21.9,21.9,0,0,0,24,2Zm0,40A18,18,0,1,1,42,24,18.1,18.1,0,0,1,24,42Z"/>
|
||||
<path d="M24,20a2,2,0,0,0-2,2V34a2,2,0,0,0,4,0V22A2,2,0,0,0,24,20Z"/>
|
||||
<circle cx="24" cy="14" r="2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 687 B |
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g data-name="Layer 2">
|
||||
|
||||
<g data-name="person">
|
||||
|
||||
<rect width="24" height="24" opacity="0"/>
|
||||
|
||||
<path d="M12 11a4 4 0 1 0-4-4 4 4 0 0 0 4 4zm0-6a2 2 0 1 1-2 2 2 2 0 0 1 2-2z"/>
|
||||
|
||||
<path d="M12 13a7 7 0 0 0-7 7 1 1 0 0 0 2 0 5 5 0 0 1 10 0 1 1 0 0 0 2 0 7 7 0 0 0-7-7z"/>
|
||||
|
||||
</g>
|
||||
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 511 B |
@@ -29,6 +29,9 @@ jest.mock('@react-native-async-storage/async-storage', () =>
|
||||
|
||||
jest.mock('@stripe/stripe-react-native', () => require('@stripe/stripe-react-native/jest/mock.js'));
|
||||
|
||||
// Navigation sizes the tab bar from the bottom inset; the mock answers with zeros without a provider
|
||||
jest.mock('react-native-safe-area-context', () => require('react-native-safe-area-context/jest/mock').default);
|
||||
|
||||
// The preset's RefreshControl mock drops its props, so onRefresh and testID never reach the host tree
|
||||
jest.mock('react-native/Libraries/Components/RefreshControl/RefreshControl', () => {
|
||||
const React = require('react');
|
||||
|
||||
Generated
+16
-1
@@ -27,6 +27,7 @@
|
||||
"expo-constants": "~57.0.17",
|
||||
"expo-dev-client": "~57.0.18",
|
||||
"expo-font": "~57.0.3",
|
||||
"expo-navigation-bar": "~57.0.2",
|
||||
"expo-notifications": "~57.0.17",
|
||||
"expo-splash-screen": "~57.0.8",
|
||||
"expo-status-bar": "~57.0.1",
|
||||
@@ -54,7 +55,7 @@
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~57.0.5",
|
||||
"prettier": "^3.9.6",
|
||||
"test-renderer": "1.2",
|
||||
"test-renderer": "~1.2.0",
|
||||
"typescript": "~6.0.3"
|
||||
}
|
||||
},
|
||||
@@ -8488,6 +8489,20 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-navigation-bar": {
|
||||
"version": "57.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-navigation-bar/-/expo-navigation-bar-57.0.2.tgz",
|
||||
"integrity": "sha512-Ufe49dsTZjarA7gMMwwwnNJWpXo+oTyl5Ud0g3lsGVrj2HXjyw9cb9098N8+CoWpjfIa5PghuRYYXrcCrGQIHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-notifications": {
|
||||
"version": "57.0.17",
|
||||
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-57.0.17.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"expo-constants": "~57.0.17",
|
||||
"expo-dev-client": "~57.0.18",
|
||||
"expo-font": "~57.0.3",
|
||||
"expo-navigation-bar": "~57.0.2",
|
||||
"expo-notifications": "~57.0.17",
|
||||
"expo-splash-screen": "~57.0.8",
|
||||
"expo-status-bar": "~57.0.1",
|
||||
|
||||
+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