Add desktop shell
parent
802222fe71
commit
967f9fc474
|
@ -24,8 +24,6 @@ Uses:
|
|||
|
||||
## TODOs
|
||||
|
||||
- Web
|
||||
- Desktop vs mobile styling
|
||||
- API
|
||||
- Create mock api
|
||||
- Tests
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react'
|
||||
import {Pressable, View, StyleSheet} from 'react-native'
|
||||
import {Link} from '@react-navigation/native'
|
||||
import {useRoute} from '@react-navigation/native'
|
||||
|
||||
export const NavItem: React.FC<{label: string; screen: string}> = ({
|
||||
label,
|
||||
screen,
|
||||
}) => {
|
||||
const route = useRoute()
|
||||
return (
|
||||
<View>
|
||||
<Pressable
|
||||
style={state => [
|
||||
// @ts-ignore it does exist! (react-native-web) -prf
|
||||
state.hovered && styles.navItemHovered,
|
||||
]}>
|
||||
<Link
|
||||
style={[
|
||||
styles.navItemLink,
|
||||
route.name === screen && styles.navItemLinkSelected,
|
||||
]}
|
||||
to={{screen, params: {}}}>
|
||||
{label}
|
||||
</Link>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const DesktopLeftColumn: React.FC = () => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<NavItem screen="Home" label="Home" />
|
||||
<NavItem screen="Search" label="Search" />
|
||||
<NavItem screen="Notifications" label="Notifications" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
left: 'calc(50vw - 500px)',
|
||||
width: '200px',
|
||||
height: '100%',
|
||||
},
|
||||
navItemHovered: {
|
||||
backgroundColor: 'gray',
|
||||
},
|
||||
navItemLink: {
|
||||
padding: '1rem',
|
||||
},
|
||||
navItemLinkSelected: {
|
||||
color: 'blue',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
import {Text, View, StyleSheet} from 'react-native'
|
||||
|
||||
export const DesktopRightColumn: React.FC = () => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Right Column</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
right: 'calc(50vw - 500px)',
|
||||
width: '200px',
|
||||
height: '100%',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {View, StyleSheet} from 'react-native'
|
||||
import {DesktopLeftColumn} from './left-column'
|
||||
import {DesktopRightColumn} from './right-column'
|
||||
import {useStores} from '../../state'
|
||||
|
||||
export const DesktopWebShell: React.FC = observer(({children}) => {
|
||||
const store = useStores()
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
{store.session.isAuthed ? (
|
||||
<>
|
||||
<DesktopLeftColumn />
|
||||
<View style={styles.innerContainer}>{children}</View>
|
||||
<DesktopRightColumn />
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.innerContainer}>{children}</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outerContainer: {
|
||||
height: '100%',
|
||||
},
|
||||
innerContainer: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
width: '600px',
|
||||
height: '100%',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,11 @@
|
|||
import {Platform} from 'react-native'
|
||||
|
||||
export const isIOS = Platform.OS === 'ios'
|
||||
export const isAndroid = Platform.OS === 'android'
|
||||
export const isNative = isIOS || isAndroid
|
||||
export const isWeb = !isNative
|
||||
export const isMobileWeb =
|
||||
isWeb &&
|
||||
// @ts-ignore we know window exists -prf
|
||||
global.window.matchMedia('only screen and (max-width: 1000px)')?.matches
|
||||
export const isDesktopWeb = isWeb && !isMobileWeb
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react'
|
||||
import {SafeAreaView} from 'react-native'
|
||||
import {isDesktopWeb} from './detection'
|
||||
import {DesktopWebShell} from './desktop-web/shell'
|
||||
|
||||
export const Shell: React.FC = ({children}) => {
|
||||
return isDesktopWeb ? (
|
||||
<DesktopWebShell>{children}</DesktopWebShell>
|
||||
) : (
|
||||
<SafeAreaView>{children}</SafeAreaView>
|
||||
)
|
||||
}
|
|
@ -9,8 +9,9 @@ import {
|
|||
import {createNativeStackNavigator} from '@react-navigation/native-stack'
|
||||
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import type {RootStackParamList} from './types'
|
||||
import type {RootTabsParamList} from './types'
|
||||
import {useStores} from '../state'
|
||||
import * as platform from '../platform/detection'
|
||||
import {Home} from '../screens/Home'
|
||||
import {Search} from '../screens/Search'
|
||||
import {Notifications} from '../screens/Notifications'
|
||||
|
@ -20,74 +21,77 @@ import {Login} from '../screens/Login'
|
|||
import {Signup} from '../screens/Signup'
|
||||
import {NotFound} from '../screens/NotFound'
|
||||
|
||||
const linking: LinkingOptions<RootStackParamList> = {
|
||||
const linking: LinkingOptions<RootTabsParamList> = {
|
||||
prefixes: [
|
||||
'http://localhost:3000', // local dev
|
||||
],
|
||||
config: {
|
||||
screens: {
|
||||
Primary: {
|
||||
screens: {
|
||||
Home: '',
|
||||
Search: 'search',
|
||||
Notifications: 'notifications',
|
||||
Menu: 'menu',
|
||||
},
|
||||
},
|
||||
Home: '',
|
||||
Profile: 'profile/:name',
|
||||
Search: 'search',
|
||||
Notifications: 'notifications',
|
||||
Menu: 'menu',
|
||||
Login: 'login',
|
||||
Signup: 'signup',
|
||||
Profile: 'profile/:name',
|
||||
NotFound: '*',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const RootStack = createNativeStackNavigator()
|
||||
export const PrimaryTab = createBottomTabNavigator()
|
||||
export const RootTabs = createBottomTabNavigator()
|
||||
export const PrimaryStack = createNativeStackNavigator()
|
||||
|
||||
const tabBarScreenOptions = ({
|
||||
route,
|
||||
}: {
|
||||
route: RouteProp<ParamListBase, string>
|
||||
}) => ({
|
||||
headerShown: false,
|
||||
tabBarIcon: (_state: {focused: boolean; color: string; size: number}) => {
|
||||
// TODO: icons
|
||||
return <Text>{route.name.at(0)}</Text>
|
||||
},
|
||||
})
|
||||
|
||||
function Primary() {
|
||||
return (
|
||||
<PrimaryTab.Navigator
|
||||
screenOptions={tabBarScreenOptions}
|
||||
initialRouteName="Home">
|
||||
<PrimaryTab.Screen name="Home" component={Home} />
|
||||
<PrimaryTab.Screen name="Search" component={Search} />
|
||||
<PrimaryTab.Screen name="Notifications" component={Notifications} />
|
||||
<PrimaryTab.Screen name="Menu" component={Menu} />
|
||||
</PrimaryTab.Navigator>
|
||||
)
|
||||
}
|
||||
const HIDE_TAB = {tabBarButton: () => null}
|
||||
|
||||
export const Root = observer(() => {
|
||||
const store = useStores()
|
||||
|
||||
// hide the tabbar on desktop web
|
||||
const tabBar = platform.isDesktopWeb ? () => null : undefined
|
||||
|
||||
return (
|
||||
<NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
|
||||
<RootStack.Navigator
|
||||
initialRouteName={store.session.isAuthed ? 'Primary' : 'Login'}>
|
||||
<RootTabs.Navigator
|
||||
initialRouteName={store.session.isAuthed ? 'Home' : 'Login'}
|
||||
screenOptions={tabBarScreenOptions}
|
||||
tabBar={tabBar}>
|
||||
{store.session.isAuthed ? (
|
||||
<>
|
||||
<RootStack.Screen name="Primary" component={Primary} />
|
||||
<RootStack.Screen name="Profile" component={Profile} />
|
||||
<RootStack.Screen name="NotFound" component={NotFound} />
|
||||
<RootTabs.Screen name="Home" component={Home} />
|
||||
<RootTabs.Screen name="Search" component={Search} />
|
||||
<RootTabs.Screen name="Notifications" component={Notifications} />
|
||||
<RootTabs.Screen name="Menu" component={Menu} />
|
||||
<RootTabs.Screen
|
||||
name="Profile"
|
||||
component={Profile}
|
||||
options={HIDE_TAB}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RootStack.Screen name="Login" component={Login} />
|
||||
<RootStack.Screen name="Signup" component={Signup} />
|
||||
<RootTabs.Screen name="Login" component={Login} />
|
||||
<RootTabs.Screen name="Signup" component={Signup} />
|
||||
</>
|
||||
)}
|
||||
</RootStack.Navigator>
|
||||
<RootTabs.Screen
|
||||
name="NotFound"
|
||||
component={NotFound}
|
||||
options={HIDE_TAB}
|
||||
/>
|
||||
</RootTabs.Navigator>
|
||||
</NavigationContainer>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
import type {NavigatorScreenParams} from '@react-navigation/native'
|
||||
import type {CompositeScreenProps} from '@react-navigation/native'
|
||||
import type {StackScreenProps} from '@react-navigation/stack'
|
||||
import type {BottomTabScreenProps} from '@react-navigation/bottom-tabs'
|
||||
|
||||
export type RootStackParamList = {
|
||||
Primary: undefined
|
||||
export type RootTabsParamList = {
|
||||
Home: undefined
|
||||
Search: undefined
|
||||
Notifications: undefined
|
||||
Menu: undefined
|
||||
Profile: {name: string}
|
||||
Login: undefined
|
||||
Signup: undefined
|
||||
NotFound: undefined
|
||||
}
|
||||
export type RootStackScreenProps<T extends keyof RootStackParamList> =
|
||||
StackScreenProps<RootStackParamList, T>
|
||||
export type RootTabsScreenProps<T extends keyof RootTabsParamList> =
|
||||
StackScreenProps<RootTabsParamList, T>
|
||||
|
||||
export type PrimaryTabParamList = {
|
||||
Home: NavigatorScreenParams<RootStackParamList>
|
||||
Search: undefined
|
||||
Notifications: undefined
|
||||
Menu: undefined
|
||||
/*
|
||||
NOTE
|
||||
this is leftover from a nested nav implementation
|
||||
keeping it around for future reference
|
||||
-prf
|
||||
|
||||
import type {NavigatorScreenParams} from '@react-navigation/native'
|
||||
import type {CompositeScreenProps} from '@react-navigation/native'
|
||||
import type {BottomTabScreenProps} from '@react-navigation/bottom-tabs'
|
||||
|
||||
Container: NavigatorScreenParams<PrimaryStacksParamList>
|
||||
export type PrimaryStacksParamList = {
|
||||
Home: undefined
|
||||
Profile: {name: string}
|
||||
}
|
||||
export type PrimaryTabScreenProps<T extends keyof PrimaryTabParamList> =
|
||||
export type PrimaryStacksScreenProps<T extends keyof PrimaryStacksParamList> =
|
||||
CompositeScreenProps<
|
||||
BottomTabScreenProps<PrimaryTabParamList, T>,
|
||||
RootStackScreenProps<keyof RootStackParamList>
|
||||
BottomTabScreenProps<PrimaryStacksParamList, T>,
|
||||
RootTabsScreenProps<keyof RootTabsParamList>
|
||||
>
|
||||
*/
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import React from 'react'
|
||||
import {Text, Button, View, SafeAreaView} from 'react-native'
|
||||
import type {PrimaryTabScreenProps} from '../routes/types'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import {Shell} from '../platform/shell'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
import {useStores} from '../state'
|
||||
|
||||
export function Home({navigation}: PrimaryTabScreenProps<'Home'>) {
|
||||
export function Home({navigation}: RootTabsScreenProps<'Home'>) {
|
||||
const store = useStores()
|
||||
return (
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text>Hello world</Text>
|
||||
<Shell>
|
||||
<View style={{alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Home</Text>
|
||||
<Button
|
||||
title="Go to Jane's profile"
|
||||
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
|
||||
/>
|
||||
<Button title="Logout" onPress={() => store.session.setAuthed(false)} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import React from 'react'
|
||||
import {Text, Button, View, SafeAreaView} from 'react-native'
|
||||
import type {RootStackScreenProps} from '../routes/types'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import {Shell} from '../platform/shell'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
import {useStores} from '../state'
|
||||
|
||||
export function Login({navigation}: RootStackScreenProps<'Login'>) {
|
||||
export function Login({navigation}: RootTabsScreenProps<'Login'>) {
|
||||
const store = useStores()
|
||||
return (
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text>Welcome! Time to sign in</Text>
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
||||
<Button title="Login" onPress={() => store.session.setAuthed(true)} />
|
||||
<Button title="Sign Up" onPress={() => navigation.navigate('Signup')} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
import React from 'react'
|
||||
import {SafeAreaView, ScrollView, Text, Button, View} from 'react-native'
|
||||
import type {PrimaryTabScreenProps} from '../routes/types'
|
||||
import {Shell} from '../platform/shell'
|
||||
import {ScrollView, Text, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
|
||||
export const Menu = ({navigation}: PrimaryTabScreenProps<'Menu'>) => {
|
||||
export const Menu = (_props: RootTabsScreenProps<'Menu'>) => {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Shell>
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<View>
|
||||
<Text>Hello world</Text>
|
||||
<Button
|
||||
title="Go to Jane's profile"
|
||||
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
|
||||
/>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Menu</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,27 +1,15 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
Text,
|
||||
Button,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import type {RootStackScreenProps} from '../routes/types'
|
||||
|
||||
export const NotFound = ({navigation}: RootStackScreenProps<'NotFound'>) => {
|
||||
const isDarkMode = useColorScheme() === 'dark'
|
||||
import {Shell} from '../platform/shell'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
|
||||
export const NotFound = ({navigation}: RootTabsScreenProps<'NotFound'>) => {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<View>
|
||||
<Text>Page not found</Text>
|
||||
<Button title="Home" onPress={() => navigation.navigate('Primary')} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text>
|
||||
<Button title="Home" onPress={() => navigation.navigate('Home')} />
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
import React from 'react'
|
||||
import {SafeAreaView, ScrollView, Text, Button, View} from 'react-native'
|
||||
import type {PrimaryTabScreenProps} from '../routes/types'
|
||||
import {Shell} from '../platform/shell'
|
||||
import {Text, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
|
||||
export const Notifications = ({
|
||||
navigation,
|
||||
}: PrimaryTabScreenProps<'Notifications'>) => {
|
||||
export const Notifications = (_props: RootTabsScreenProps<'Notifications'>) => {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<View>
|
||||
<Text>Hello world</Text>
|
||||
<Button
|
||||
title="Go to Jane's profile"
|
||||
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Notifications</Text>
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import React from 'react'
|
||||
import {Text} from 'react-native'
|
||||
import type {RootStackScreenProps} from '../routes/types'
|
||||
import {Shell} from '../platform/shell'
|
||||
import {View, Text} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
|
||||
export const Profile = ({route}: RootStackScreenProps<'Profile'>) => {
|
||||
return <Text>This is {route.params.name}'s profile</Text>
|
||||
export const Profile = ({route}: RootTabsScreenProps<'Profile'>) => {
|
||||
return (
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>
|
||||
{route.params?.name}'s profile
|
||||
</Text>
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
import React from 'react'
|
||||
import {SafeAreaView, ScrollView, Text, Button, View} from 'react-native'
|
||||
import type {PrimaryTabScreenProps} from '../routes/types'
|
||||
import {Shell} from '../platform/shell'
|
||||
import {Text, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
|
||||
export const Search = ({navigation}: PrimaryTabScreenProps<'Search'>) => {
|
||||
export const Search = (_props: RootTabsScreenProps<'Search'>) => {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<View>
|
||||
<Text>Hello world</Text>
|
||||
<Button
|
||||
title="Go to Jane's profile"
|
||||
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Search</Text>
|
||||
</View>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import React from 'react'
|
||||
import {Text, Button, View, SafeAreaView} from 'react-native'
|
||||
import type {RootStackScreenProps} from '../routes/types'
|
||||
import {Shell} from '../platform/shell'
|
||||
import {Text, Button, View} from 'react-native'
|
||||
import type {RootTabsScreenProps} from '../routes/types'
|
||||
import {useStores} from '../state'
|
||||
|
||||
export function Signup({navigation}: RootStackScreenProps<'Signup'>) {
|
||||
export function Signup({navigation}: RootTabsScreenProps<'Signup'>) {
|
||||
const store = useStores()
|
||||
return (
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text>Let's create your account</Text>
|
||||
<Shell>
|
||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||
<Button
|
||||
title="Create new account"
|
||||
onPress={() => store.session.setAuthed(true)}
|
||||
|
@ -18,6 +19,6 @@ export function Signup({navigation}: RootStackScreenProps<'Signup'>) {
|
|||
onPress={() => navigation.navigate('Login')}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue