diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f259b48 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,17 @@ +name: test + +on: + push: + branches: + - 'main' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + - run: bun install + - run: bun run deploy \ No newline at end of file diff --git a/app.json b/app.json index 4edef28..94d50c0 100644 --- a/app.json +++ b/app.json @@ -33,7 +33,8 @@ "expo-router" ], "experiments": { - "typedRoutes": true + "typedRoutes": true, + "baseUrl": "/portfolio" } } } diff --git a/app/_layout.tsx b/app/_layout.tsx index c4c21ec..2d5572d 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,80 +1,56 @@ -import AsyncStorage from '@react-native-async-storage/async-storage' -import {DarkTheme, DefaultTheme, ThemeProvider} from '@react-navigation/native' +import { + DarkTheme, + DefaultTheme, + ThemeProvider, + useTheme, +} from '@react-navigation/native' import {useAssets} from 'expo-asset' -import {loadAsync, useFonts} from 'expo-font' +import {useFonts} from 'expo-font' import {Slot, SplashScreen} from 'expo-router' -import * as SystemUI from 'expo-system-ui' -import { - INativebaseConfig, - NativeBaseProvider, - StorageManager, - useColorMode, - useColorModeValue, -} from 'native-base' import {ReactNode, useEffect} from 'react' -import Ionicons from '@expo/vector-icons/Ionicons' -import {useColorScheme} from 'nativewind' +import * as SystemUI from 'expo-system-ui' +import Feather from '@expo/vector-icons/Feather' +import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons' import {View} from 'react-native' -import {useAppStore} from '@/store' +import {useColorSchemeStore, useLocalStore} from '@/stores' import {ICONS, IMAGES} from '@/assets' -import {theme} from '@/theme' import {ThemeName, themes} from '@/themes' import '../global.css' -// Native Base config -const colorModeManager: StorageManager = { - get: async () => { - try { - const val = await AsyncStorage.getItem('@color-mode') - return val === 'dark' ? 'dark' : 'light' - } catch (e) { - return 'dark' - } - }, - set: async value => { - try { - if (value) { - await AsyncStorage.setItem('@color-mode', value) - } - } catch (e) { - console.log(e) - } - }, -} -const config: INativebaseConfig = { - strictMode: 'error', - theme, -} - // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync() export default function RootLayout() { - const [fontsLoaded, error] = useFonts({ - ...Ionicons.font, + const [fontsLoaded, fontsError] = useFonts({ + ...Feather.font, + ...MaterialCommunityIcons.font, }) - const [assets] = useAssets([ + const [assets, assetsError] = useAssets([ ...Object.values(IMAGES), ...Object.values(ICONS), ]) + const isLoading = !fontsLoaded || !assets + // Expo Router uses Error Boundaries to catch errors in the navigation tree. useEffect(() => { - if (error) { - throw error + if (assetsError) { + throw assetsError + } + if (fontsError) { + throw fontsError } - }, [error]) + }, [assetsError, fontsError]) useEffect(() => { - loadAsync(Ionicons.font) - if (fontsLoaded && assets) { + if (!isLoading) { SplashScreen.hideAsync() } - }, [assets, fontsLoaded]) + }, [isLoading]) - if (!fontsLoaded || !assets) { + if (isLoading) { return null } @@ -82,7 +58,13 @@ export default function RootLayout() { } function Theme({name, children}: {name: ThemeName; children: ReactNode}) { - const {colorScheme} = useColorScheme() + const {colorScheme} = useColorSchemeStore() + const {colors} = useTheme() + + useEffect(() => { + SystemUI.setBackgroundColorAsync(colors.background) + }, [colorScheme, colors.background]) + return ( {children} @@ -91,24 +73,14 @@ function Theme({name, children}: {name: ThemeName; children: ReactNode}) { } function RootLayoutNav() { - const locale = useAppStore(state => state.locale) - const {colorMode} = useColorMode() - const backgroundColor = useColorModeValue( - DefaultTheme.colors.background, - DarkTheme.colors.background, - ) - - useEffect(() => { - SystemUI.setBackgroundColorAsync(backgroundColor) - }, [backgroundColor]) + const locale = useLocalStore(state => state.locale) + const {colorScheme} = useColorSchemeStore() return ( - - - - - - - + + + + + ) } diff --git a/app/index.tsx b/app/index.tsx index e51200d..4fa6b9c 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,104 +1,58 @@ -import {Box, Divider, Text} from 'native-base' -import {useCallback, useState, ReactElement} from 'react' -import {LayoutChangeEvent, ListRenderItem, StyleSheet} from 'react-native' -import Animated, {useSharedValue} from 'react-native-reanimated' -import {Route} from 'react-native-tab-view' -import {useTheme} from '@react-navigation/native' +import {useState, ReactElement} from 'react' +import {View} from 'react-native' +import {useSharedValue} from 'react-native-reanimated' import { Header, LocaleFab, - ListItem, TabView, TabViewProps, ColorModeFab, Background, + ListHeader, } from '@/components' -import {data, DataItem} from '@/data' -import {useValues} from '@/hooks' +import {data} from '@/data' import {i18n} from '@/i18n' +import {Text} from '@/components/base' export default function Home(): ReactElement { - const theme = useTheme() const scrollY = useSharedValue(0) - const [headerHeight, setHeaderHeight] = useState(0) - const {appWidth} = useValues() - const onHeaderLayout = useCallback(({nativeEvent}: LayoutChangeEvent) => { - setHeaderHeight(nativeEvent.layout.height) - }, []) - - const [routes] = useState([ - {key: 'projects', title: i18n.t('home.projects')}, - {key: 'experiences', title: i18n.t('home.experiences')}, - ]) - - const getDataKey = useCallback((item: DataItem) => item.id.toString(), []) - const renderData = useCallback>( - ({item}) => , - [], - ) - const renderScene = useCallback['renderScene']>( - ({route, listProps}) => { - return ( - - ) + const [routes] = useState(() => [ + { + key: 'projects', + title: i18n.t('home.projects'), + data: data.projects, }, - [getDataKey, renderData], - ) + { + key: 'experiences', + title: i18n.t('home.experiences'), + data: data.experiences, + }, + ]) return ( <> - -
+ +
} scrollY={scrollY} - paddingTop={headerHeight} /> - - - + + + + Cette app est cross-platform (iOS + Android + Web) ❤️ - + ) } - -const styles = StyleSheet.create({ - tabView: { - zIndex: 2, - ...StyleSheet.absoluteFillObject, - }, -}) diff --git a/assets/images/avatar.jpeg b/assets/images/avatar.jpeg new file mode 100644 index 0000000..3a71166 Binary files /dev/null and b/assets/images/avatar.jpeg differ diff --git a/assets/images/avatar.jpg b/assets/images/avatar.jpg deleted file mode 100644 index 03f1d0a..0000000 Binary files a/assets/images/avatar.jpg and /dev/null differ diff --git a/assets/images/index.ts b/assets/images/index.ts index 5af27cb..8095ca9 100644 --- a/assets/images/index.ts +++ b/assets/images/index.ts @@ -1,6 +1,6 @@ export const IMAGES = { cover: require('./cover.webp'), - avatar: require('./avatar.webp'), + avatar: require('./avatar.jpeg'), totem: require('./totem.webp'), t2m: require('./t2m.jpg'), faks: require('./faks.webp'), diff --git a/bun.lockb b/bun.lockb index 8e0c1a8..d6cc41a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/Background.tsx b/components/Background.tsx index 5b5b30d..2befb48 100644 --- a/components/Background.tsx +++ b/components/Background.tsx @@ -1,6 +1,5 @@ import {memo, useEffect} from 'react' -import {StyleSheet} from 'react-native' -import {Box, useColorMode} from 'native-base' +import {ViewProps} from 'react-native' import Animated, { interpolateColor, useAnimatedStyle, @@ -9,17 +8,17 @@ import Animated, { } from 'react-native-reanimated' import {DarkTheme, DefaultTheme} from '@react-navigation/native' -const AnimatedBox = Animated.createAnimatedComponent(Box) +import {useColorSchemeStore} from '@/stores' -type Props = {} +type Props = ViewProps -export const Background = memo(() => { - const {colorMode} = useColorMode() - const colorProgress = useSharedValue(0) +export const Background = memo(({className, style, ...props}) => { + const {colorScheme} = useColorSchemeStore() + const colorProgress = useSharedValue(colorScheme === 'light' ? 0 : 1) useEffect(() => { - colorProgress.value = withTiming(colorMode === 'light' ? 0 : 1) - }, [colorMode, colorProgress]) + colorProgress.value = withTiming(colorScheme === 'light' ? 0 : 1) + }, [colorProgress, colorScheme]) const contentStyle = useAnimatedStyle(() => { return { @@ -31,14 +30,11 @@ export const Background = memo(() => { } }) - return -}) - -const styles = StyleSheet.create({ - gradient: { - ...StyleSheet.absoluteFillObject, - }, - content: { - ...StyleSheet.absoluteFillObject, - }, + return ( + + ) }) diff --git a/components/ColorModeFab.tsx b/components/ColorModeFab.tsx index b0f4501..37d2d8d 100644 --- a/components/ColorModeFab.tsx +++ b/components/ColorModeFab.tsx @@ -1,26 +1,23 @@ -import {Fab, useColorMode} from 'native-base' import {memo} from 'react' +import {useColorSchemeStore} from '@/stores' + +import {Fab} from './base' + export const ColorModeFab = memo(() => { - const {toggleColorMode} = useColorMode() + const {toggleColorScheme, colorScheme} = useColorSchemeStore() return ( - - // } - /> + + + ) }) diff --git a/components/Header.tsx b/components/Header.tsx index aaf9c5d..48c94e8 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,17 +1,8 @@ -import { - Avatar, - Box, - Icon, - Image, - Row, - Text, - useColorMode, - useColorModeValue, -} from 'native-base' -import {memo, useMemo} from 'react' -import {StyleSheet, Platform, LayoutChangeEvent} from 'react-native' +import {memo} from 'react' +import {Image, Platform, View} from 'react-native' import Animated, { Extrapolation, + SharedValue, interpolate, useAnimatedProps, useAnimatedStyle, @@ -22,34 +13,32 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {i18n} from '@/i18n' import {IMAGES} from '@/assets' import {useValues} from '@/hooks' +import {useColorSchemeStore} from '@/stores' + +import {Text} from './base' export type HeaderProps = { - scrollY: Animated.SharedValue - onLayout: (event: LayoutChangeEvent) => void + scrollY: SharedValue } const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) -export const Header = memo(({scrollY, onLayout}) => { - const {colorMode} = useColorMode() +export const Header = memo(({scrollY}) => { + const {colorScheme} = useColorSchemeStore() const insets = useSafeAreaInsets() - const backgroundColor = useColorModeValue('white', 'black') - const { - headerOffset, - coverHeight, - smallCoverHeight, - avatarSize, - smallAvatarSize, - avatarOffset, - } = useValues() + const {headerHeight, smallHeaderHeight, headerOffset, smallAvatarHeight} = + useValues() + + const headerThreshold = headerOffset + smallAvatarHeight / 2 - const coverStyle = useAnimatedStyle(() => { + const headerStyle = useAnimatedStyle(() => { return { + zIndex: scrollY.value < headerOffset ? 0 : 20, height: interpolate( scrollY.value, - [0, headerOffset], - [coverHeight, smallCoverHeight], + [0, headerHeight - smallHeaderHeight], + [headerHeight, smallHeaderHeight], { extrapolateRight: Extrapolation.CLAMP, }, @@ -57,28 +46,26 @@ export const Header = memo(({scrollY, onLayout}) => { } }) - const avatarStyle = useAnimatedStyle(() => { - const size = interpolate( - scrollY.value, - [0, headerOffset], - [avatarSize, smallAvatarSize], - { - extrapolateRight: Extrapolation.CLAMP, - extrapolateLeft: Extrapolation.CLAMP, - }, - ) + const headerContentStyle = useAnimatedStyle(() => { return { - height: size, - width: size, - marginTop: interpolate( + opacity: interpolate( scrollY.value, - [0, headerOffset], - [0, avatarOffset], + [headerThreshold, headerThreshold + 50], + [0, 1], + ), + transform: [ { - extrapolateLeft: Extrapolation.CLAMP, - extrapolateRight: Extrapolation.CLAMP, + translateY: interpolate( + scrollY.value, + [headerThreshold, headerThreshold + 50], + [50, 0], + { + extrapolateLeft: Extrapolation.CLAMP, + extrapolateRight: Extrapolation.CLAMP, + }, + ), }, - ), + ], } }) @@ -89,10 +76,7 @@ export const Header = memo(({scrollY, onLayout}) => { ? 0 : interpolate( scrollY.value, - [ - headerOffset + smallAvatarSize, - headerOffset + smallAvatarSize + 50, - ], + [headerThreshold, headerThreshold + 50], [0, 50], { extrapolateLeft: Extrapolation.CLAMP, @@ -102,175 +86,56 @@ export const Header = memo(({scrollY, onLayout}) => { } }) - const coverContentStyle = useAnimatedStyle(() => { - return { - opacity: interpolate( - scrollY.value, - [headerOffset + smallAvatarSize, headerOffset + smallAvatarSize + 50], - [0, 1], - ), - transform: [ - { - translateY: interpolate( - scrollY.value, - [ - headerOffset + smallAvatarSize, - headerOffset + smallAvatarSize + 50, - ], - [50, 0], - { - extrapolateLeft: Extrapolation.CLAMP, - extrapolateRight: Extrapolation.CLAMP, - }, - ), - }, - ], - } - }) - - const coverOverlayStyle = useAnimatedStyle(() => { + const headerOverlayStyle = useAnimatedStyle(() => { return { opacity: Platform.OS === 'ios' ? 0 : interpolate( scrollY.value, - [ - headerOffset + smallAvatarSize, - headerOffset + smallAvatarSize + 50, - ], + [headerThreshold, headerThreshold + 50], [0, 0.7], {extrapolateRight: Extrapolation.CLAMP}, ), } }) - const headerStyle = useAnimatedStyle(() => { - return { - zIndex: scrollY.value >= headerOffset ? 1 : 10, - transform: [{translateY: -scrollY.value}], - } - }) - - const data = useMemo<{description?: string; icon: string; link?: string}[]>( - () => [ - { - icon: 'map-pin', - description: i18n.t('home.locationDescription'), - }, - { - icon: 'gift', - description: i18n.t('home.birthdayDescription'), - }, - // { - // icon: 'github', - // link: 'github.com/martinezguillaume/portfolio', - // description: 'github.com', - // }, - { - icon: 'calendar', - description: i18n.t('home.developerDescription'), - }, - ], - [], - ) - return ( - <> - - cover - - - - - - - - - Guillaume Martinez - {i18n.t('home.title')} - - - - - + + cover - - - Guillaume Martinez - - {i18n.t('home.title')} - - {i18n.t('home.description')} - - - {data.map(item => ( - - {/* FIXME: add an icon */} - {' '} - {item.description} - - ))} - - - + className="absolute inset-x-0 inset-y-0 bg-background" + style={headerOverlayStyle} + /> + + + {/* eslint-disable-next-line react-native/no-inline-styles */} + + + + + Guillaume Martinez + {i18n.t('home.title')} + + + + + ) }) - -const styles = StyleSheet.create({ - header: { - pointerEvents: 'none', - position: 'absolute', - left: 0, - right: 0, - paddingHorizontal: 16, - }, - blurView: { - ...StyleSheet.absoluteFillObject, - justifyContent: 'center', - alignItems: 'center', - overflow: 'hidden', - }, - coverOverlay: { - ...StyleSheet.absoluteFillObject, - }, - cover: { - pointerEvents: 'none', - position: 'absolute', - left: 0, - right: 0, - zIndex: 5, - }, - avatar: { - borderRadius: 200, - borderWidth: 4, - }, -}) diff --git a/components/ListHeader.tsx b/components/ListHeader.tsx new file mode 100644 index 0000000..306a8c3 --- /dev/null +++ b/components/ListHeader.tsx @@ -0,0 +1,120 @@ +import Animated, { + Extrapolation, + SharedValue, + interpolate, + useAnimatedStyle, +} from 'react-native-reanimated' +import {Linking, View} from 'react-native' +import {useMemo, useState} from 'react' + +import {IMAGES} from '@/assets' +import {useValues} from '@/hooks' +import {i18n} from '@/i18n' + +import {Icon, IconProps, Text} from './base' + +type ListHeaderProps = { + scrollY: SharedValue +} + +export const ListHeader = ({scrollY}: ListHeaderProps) => { + const { + smallHeaderHeight, + headerOffset, + avatarHeight, + avatarOffset, + smallAvatarHeight, + headerHeight, + } = useValues() + const [isDescExpanded, setIsDescExpanded] = useState(false) + + const data = useMemo< + {description?: string; icon: IconProps; link?: string}[] + >( + () => [ + { + icon: {name: 'map-pin'}, + description: i18n.t('home.locationDescription'), + }, + { + icon: {name: 'gift'}, + description: i18n.t('home.birthdayDescription'), + }, + { + icon: {name: 'github'}, + link: 'https://github.com/martinezguillaume/portfolio', + description: 'github.com', + }, + { + icon: {name: 'calendar'}, + description: i18n.t('home.developerDescription'), + }, + ], + [], + ) + + const avatarStyle = useAnimatedStyle(() => { + const size = interpolate( + scrollY.value, + [0, headerOffset], + [avatarHeight, smallAvatarHeight], + { + extrapolateRight: Extrapolation.CLAMP, + extrapolateLeft: Extrapolation.CLAMP, + }, + ) + return { + height: size, + width: size, + } + }) + + return ( + + + + + + Guillaume Martinez + {i18n.t('home.title')} + + + {i18n.t('home.description')} + + setIsDescExpanded(!isDescExpanded)}> + {isDescExpanded ? i18n.t('home.seeLess') : i18n.t('home.seeMore')} + + + + {data.map(item => ( + + { + if (item.link) { + Linking.openURL(item.link) + } + } + : undefined + } + className={`${ + item.link ? 'text-primary-primary' : 'text-secondary' + } text-sm`}> + {' '} + {item.description} + + + ))} + + + ) +} diff --git a/components/ListItem.tsx b/components/ListItem.tsx index b2829a8..530c1d7 100644 --- a/components/ListItem.tsx +++ b/components/ListItem.tsx @@ -1,84 +1,89 @@ -import { - Avatar, - Column, - Icon, - IIconProps, - Image, - Row, - Text, - IImageProps, -} from 'native-base' import {memo} from 'react' import dayjs from 'dayjs' +import {ImageProps, View, Image} from 'react-native' import {DataItem, DataSkill} from '@/data' import {ICONS} from '@/assets' +import {Icon, IconProps, Text} from './base' + export type ListItemProps = { data: DataItem } const skillIcon: Record< DataSkill, - ({type?: 'icon'} & IIconProps) | ({type: 'image'} & IImageProps) + ({as?: 'icon'} & IconProps) | ({as: 'image'} & ImageProps) > = { react: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'react', }, aws: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'aws', + className: 'text-2xl', }, html: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'language-html5', }, css: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'language-css3', }, js: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'language-javascript', }, ts: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'language-typescript', }, graphql: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'graphql', }, 'react-native': { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'react', }, java: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'language-java', - size: 6, + className: 'text-2xl', }, kotlin: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'language-kotlin', - size: 5, + className: '!text-lg', }, swift: { - // as: MaterialCommunityIcons, + as: 'icon', + type: 'material-community-icons', name: 'language-swift', }, 'objective-c': { - type: 'image', + as: 'image', source: ICONS['objective-c'], alt: 'objective-c', - size: 6, + className: 'size-6', }, expo: { - type: 'image', + as: 'image', source: ICONS.expo, alt: 'expo', - px: 1, + className: 'mx-[1]', }, } @@ -97,13 +102,16 @@ export const ListItem = memo( }, }) => { return ( - - + + - - + + {title} - + {' · '} {subtitle} @@ -112,65 +120,59 @@ export const ListItem = memo( {description && {description}} {pictures && ( - + {pictures.map(picture => ( item-picture ))} - + )} {skills && ( - + {skills.map(skill => { const icon = skillIcon[skill] - if (icon.type === 'image') { + if (icon.as === 'image') { return ( ) } else { return ( - + ) } })} - + )} - + {location && ( - - {/* FIXME: add an icon */} - {location} + + {location} )} - - {/* FIXME: add an icon */} - {' '} + + {' '} {dayjs(startDate).format(!endDate ? 'MMMM YYYY' : 'MMM YYYY')} {endDate && ` - ${dayjs(endDate).format('MMM YYYY')}`} - - - + + + ) }, ) diff --git a/components/LocaleFab.tsx b/components/LocaleFab.tsx index 3e12ee5..f1c185c 100644 --- a/components/LocaleFab.tsx +++ b/components/LocaleFab.tsx @@ -1,36 +1,32 @@ -import {Fab, Menu} from 'native-base' import {FC, memo} from 'react' -import {i18n} from '@/i18n' -import {useAppStore} from '@/store' - export const LocaleFab: FC = memo(() => { - const setLocale = useAppStore(state => state.setLocale) - const locale = useAppStore(state => state.locale) - - return ( - ( - - )}> - - 🇫🇷 Français - 🇬🇧 English - - - ) + // const setLocale = useLocalStore(state => state.setLocale) + // const locale = useLocalStore(state => state.locale) + return null + // return ( + // ( + // + // )}> + // + // 🇫🇷 Français + // 🇬🇧 English + // + // + // ) }) diff --git a/components/TabView.tsx b/components/TabView.tsx index e733f24..dbd0dae 100644 --- a/components/TabView.tsx +++ b/components/TabView.tsx @@ -1,218 +1,96 @@ -import {Box, Divider, Text, useToken} from 'native-base' -import { - ReactElement, - ReactNode, - Ref, - useCallback, - useRef, - useState, -} from 'react' -import {Dimensions, Platform, ScrollViewProps, StyleSheet} from 'react-native' import Animated, { - Extrapolate, - interpolate, SharedValue, useAnimatedScrollHandler, - useAnimatedStyle, } from 'react-native-reanimated' -import { - NavigationState, - Route, - SceneRendererProps, - TabView as RNTabView, - TabViewProps as RNTabViewProps, - TabBar as RNTabBar, -} from 'react-native-tab-view' +import {Pressable, View, ListRenderItem} from 'react-native' +import {useCallback, useMemo, useState} from 'react' import {DataItem} from '@/data' import {useValues} from '@/hooks' +import {ListItem} from './ListItem' +import {Text} from './base' import {Background} from './Background' -const initialLayout = {width: Dimensions.get('window').width} -const INDICATOR_WIDTH = 100 - -export type TabViewProps = Omit< - RNTabViewProps, - 'navigationState' | 'onIndexChange' | 'renderScene' -> & { - routes: T[] - renderScene: ( - props: SceneRendererProps & { - route: T - listProps: Partial & { - ref: Ref> - } - }, - ) => ReactNode +export type TabViewProps = { + ListHeaderComponent: React.ReactElement scrollY: SharedValue - paddingTop: number + routes: {key: string; title: string; data: DataItem[]}[] } -export const TabView = ({ +type TabViewDataItem = DataItem | 'tab' + +export const TabView = ({ routes, - renderScene: renderSceneProps, + ListHeaderComponent, scrollY, - paddingTop, - ...props -}: TabViewProps): ReactElement => { - const primary = useToken('colors', 'primary') - const muted = useToken('colors', 'muted') - - const {smallCoverHeight, insets, tabBarHeight, appWidth} = useValues() - const [index, setIndex] = useState(0) - - const listRef = useRef<{key: string; value: Animated.FlatList}[]>( - [], - ) - const listOffset = useRef>({}) +}: TabViewProps) => { + const {smallHeaderHeight} = useValues() const scrollHandler = useAnimatedScrollHandler(event => { - const y = event.contentOffset.y - scrollY.value = y - const curRoute = routes[index].key - listOffset.current[curRoute] = y + scrollY.value = event.contentOffset.y }) - const syncScrollOffset = useCallback(() => { - const curRouteKey = routes[index].key - listRef.current.forEach(item => { - if (item.key !== curRouteKey) { - if (scrollY.value < paddingTop && scrollY.value >= 0) { - if (item.value) { - // @ts-ignore issue: https://github.com/software-mansion/react-native-reanimated/issues/3023 - item.value.scrollToOffset({ - offset: scrollY.value, - animated: false, - }) - listOffset.current[item.key] = scrollY.value - } - } else if (scrollY.value >= paddingTop) { - if ( - listOffset.current[item.key] < paddingTop || - listOffset.current[item.key] == null - ) { - if (item.value) { - // @ts-ignore issue: https://github.com/software-mansion/react-native-reanimated/issues/3023 - item.value.scrollToOffset({ - offset: paddingTop, - animated: false, - }) - listOffset.current[item.key] = paddingTop - } - } - } - } - }) - }, [index, paddingTop, routes, scrollY.value]) + const [index, setIndex] = useState(0) - const tabBarStyle = useAnimatedStyle(() => { - return { - position: 'absolute', - left: 0, - right: 0, - zIndex: 99, - transform: [ - { - translateY: interpolate( - scrollY.value, - [0, paddingTop - smallCoverHeight], - [paddingTop, smallCoverHeight], - { - extrapolateRight: Extrapolate.CLAMP, - }, - ), - }, - ], - } - }) + const currentRoute = routes[index] + + const data = useMemo( + () => ['tab', ...currentRoute.data], + [currentRoute.data], + ) - const renderTabBar = useCallback( - ( - tabBarProps: SceneRendererProps & { - navigationState: NavigationState - }, - ) => ( - + const TabBar = useMemo( + () => ( + - - ( - - - {route.title} - - - )} - /> - - - + {routes.map((route, i) => ( + setIndex(i)}> + + + + {route.title} + + + + + ))} + ), - [appWidth, muted, primary, routes.length, syncScrollOffset, tabBarStyle], + [index, routes], ) - const renderScene = useCallback['renderScene']>( - sceneProps => - renderSceneProps({ - ...sceneProps, - listProps: { - showsVerticalScrollIndicator: false, - contentContainerStyle: { - paddingTop: paddingTop + tabBarHeight, - paddingBottom: insets.bottom, - }, - scrollEventThrottle: 16, - onScroll: scrollHandler, - onMomentumScrollEnd: syncScrollOffset, - onScrollEndDrag: syncScrollOffset, - ref: ref => { - if (ref) { - const found = listRef.current.find( - e => e.key === sceneProps.route.key, - ) - if (!found) { - listRef.current.push({ - key: sceneProps.route.key, - value: ref, - }) - } - } - }, - }, - }), - [ - insets.bottom, - paddingTop, - renderSceneProps, - scrollHandler, - syncScrollOffset, - tabBarHeight, - ], + const getItemId = useCallback( + (item: TabViewDataItem) => (item === 'tab' ? item : item.id.toString()), + [], + ) + const renderItem = useCallback>( + ({item}) => (item === 'tab' ? TabBar : ), + [TabBar], + ) + const renderSeparator = useCallback( + () => , + [], ) return ( - ) } - -const styles = StyleSheet.create({ - tabBar: { - elevation: 0, - backgroundColor: 'transparent', - }, -}) diff --git a/components/base/Fab.tsx b/components/base/Fab.tsx new file mode 100644 index 0000000..efbc5eb --- /dev/null +++ b/components/base/Fab.tsx @@ -0,0 +1,20 @@ +import {Pressable, PressableProps} from 'react-native' + +import {Icon, IconProps} from './Icon' + +type FabProps = PressableProps + +export const Fab = ({className, ...props}: FabProps) => { + return ( + + ) +} + +type FabIconProps = IconProps + +Fab.Icon = ({className, ...props}: FabIconProps) => ( + +) diff --git a/components/base/Icon.tsx b/components/base/Icon.tsx new file mode 100644 index 0000000..e7b122b --- /dev/null +++ b/components/base/Icon.tsx @@ -0,0 +1,21 @@ +import Feather from '@expo/vector-icons/Feather' +import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons' +import {ComponentProps} from 'react' + +export type IconProps = + | ({ + type?: 'feather' + } & ComponentProps) + | ({ + type: 'material-community-icons' + } & ComponentProps) + +export const Icon = ({className, ...props}: IconProps) => { + const newClassName = `text-primary text-base ${className}` + + if (props.type === 'material-community-icons') { + return + } + + return +} diff --git a/components/base/Text.tsx b/components/base/Text.tsx new file mode 100644 index 0000000..e71a338 --- /dev/null +++ b/components/base/Text.tsx @@ -0,0 +1,5 @@ +import {Text as RNText, TextProps as RNTextProps} from 'react-native' + +export const Text = ({className, ...props}: RNTextProps) => ( + +) diff --git a/components/base/index.ts b/components/base/index.ts new file mode 100644 index 0000000..e17eba9 --- /dev/null +++ b/components/base/index.ts @@ -0,0 +1,3 @@ +export * from './Icon' +export * from './Fab' +export * from './Text' diff --git a/components/index.ts b/components/index.ts index 16ccb86..308844d 100644 --- a/components/index.ts +++ b/components/index.ts @@ -4,3 +4,4 @@ export * from './TabView' export * from './LocaleFab' export * from './ColorModeFab' export * from './Background' +export * from './ListHeader' diff --git a/hooks/useValues.ts b/hooks/useValues.ts index db933f8..42a77c4 100644 --- a/hooks/useValues.ts +++ b/hooks/useValues.ts @@ -1,32 +1,24 @@ -import {useBreakpointValue} from 'native-base' import {useMemo} from 'react' -import {Dimensions} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' -export const COVER_HEIGHT = 160 -export const COVER_HEIGHT_SMALL = 80 -export const AVATAR_SIZE = 160 -export const AVATAR_SIZE_SMALL = 100 +export const HEADER_HEIGHT = 140 +export const HEADER_HEIGHT_SMALL = 80 +export const AVATAR_HEIGHT = 120 +export const AVATAR_HEIGHT_SMALL = 80 export const useValues = () => { const insets = useSafeAreaInsets() - const appWidth = useBreakpointValue({ - base: Dimensions.get('window').width, - md: 600, - }) return useMemo( () => ({ - coverHeight: insets.top + COVER_HEIGHT, - smallCoverHeight: insets.top + COVER_HEIGHT_SMALL, - avatarSize: AVATAR_SIZE, - smallAvatarSize: AVATAR_SIZE_SMALL, - headerOffset: COVER_HEIGHT - COVER_HEIGHT_SMALL, - avatarOffset: AVATAR_SIZE - AVATAR_SIZE_SMALL, - tabBarHeight: 48, + headerHeight: insets.top + HEADER_HEIGHT, + smallHeaderHeight: insets.top + HEADER_HEIGHT_SMALL, + avatarHeight: AVATAR_HEIGHT, + smallAvatarHeight: AVATAR_HEIGHT_SMALL, + headerOffset: HEADER_HEIGHT - HEADER_HEIGHT_SMALL, + avatarOffset: AVATAR_HEIGHT - AVATAR_HEIGHT_SMALL, insets, - appWidth: appWidth, }), - [appWidth, insets], + [insets], ) } diff --git a/locales/en.json b/locales/en.json index f56607b..49619ad 100644 --- a/locales/en.json +++ b/locales/en.json @@ -4,9 +4,11 @@ "developerDescription": "Developer since September 2016", "locationDescription": "Paris, France", "birthdayDescription": "Born August 1, 1996", - "title": "Mobile Developer - React-Native Expert", + "title": "📱Mobile Developer - React-Native Expert", "projects": "Projects", "experiences": "Experiences", - "language": "Language" + "language": "Language", + "seeMore": "See more", + "seeLess": "See less" } } diff --git a/locales/fr.json b/locales/fr.json index 5608377..32269ce 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -4,9 +4,11 @@ "developerDescription": "Développeur depuis septembre 2016", "locationDescription": "Paris, France", "birthdayDescription": "Né le 1 août 1996", - "title": "Développeur Mobile - Expert React-Native", + "title": "📱Développeur Mobile - Expert React-Native", "projects": "Projets", "experiences": "Expériences", - "language": "Langue" + "language": "Langue", + "seeMore": "Voir plus", + "seeLess": "Voir moins" } } diff --git a/package.json b/package.json index 8825564..b619fef 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "web": "expo start --web", "test": "tsc && bun run lint", "lint": "eslint . --ext .ts,.tsx --max-warnings 0", - "deploy": "gh-pages -d dist", - "predeploy": "npx expo export:web", + "predeploy": "expo export -p web", + "deploy": "gh-pages -t -d dist", "postinstall": "patch-package" }, "dependencies": { @@ -29,19 +29,15 @@ "expo-status-bar": "~1.11.1", "expo-system-ui": "~2.9.3", "i18n-js": "^4.3.0", - "native-base": "^3.4.28", "nativewind": "^4.0.1", - "normalize-css-color": "^1.0.2", "patch-package": "^8.0.0", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.73.2", - "react-native-pager-view": "6.2.3", "react-native-reanimated": "~3.6.2", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-svg": "14.1.0", - "react-native-tab-view": "^3.1.1", "react-native-web": "~0.19.6", "zustand": "^4.4.1" }, diff --git a/public/.nojekyll b/public/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/store.ts b/store.ts deleted file mode 100644 index 38778da..0000000 --- a/store.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {create} from 'zustand' -import * as dayjs from 'dayjs' -import * as Localization from 'expo-localization' -import {persist, createJSONStorage} from 'zustand/middleware' -import AsyncStorage from '@react-native-async-storage/async-storage' -import 'dayjs/locale/fr' - -import {i18n} from './i18n' - -type AppState = { - locale: 'fr' | 'en' - setLocale: (locale: AppState['locale']) => void -} - -const initialLocale: AppState['locale'] = Localization.locale.startsWith('fr') - ? 'fr' - : 'en' -const changeAppLanguage = (locale: AppState['locale']) => { - dayjs.locale(locale) - i18n.locale = locale -} -changeAppLanguage(initialLocale) - -export const useAppStore = create()( - persist( - set => ({ - locale: initialLocale, - setLocale: locale => { - changeAppLanguage(locale) - set({locale}) - }, - }), - { - name: 'app-storage', - storage: createJSONStorage(() => AsyncStorage), - onRehydrateStorage: () => { - return (state, error) => { - if (error) { - console.log('an error happened during hydration', error) - } else { - if (state?.locale) { - changeAppLanguage(state.locale) - } - } - } - }, - }, - ), -) diff --git a/stores.ts b/stores.ts new file mode 100644 index 0000000..8652632 --- /dev/null +++ b/stores.ts @@ -0,0 +1,74 @@ +import {create} from 'zustand' +import * as dayjs from 'dayjs' +import * as Localization from 'expo-localization' +import {persist, createJSONStorage} from 'zustand/middleware' +import AsyncStorage from '@react-native-async-storage/async-storage' +import 'dayjs/locale/fr' + +import {i18n} from './i18n' + +type LocaleState = { + locale: 'fr' | 'en' + setLocale: (locale: LocaleState['locale']) => void +} + +const initialLocale: LocaleState['locale'] = Localization.locale.startsWith( + 'fr', +) + ? 'fr' + : 'en' +const changeAppLanguage = (locale: LocaleState['locale']) => { + dayjs.locale(locale) + i18n.locale = locale +} +changeAppLanguage(initialLocale) + +export const useLocalStore = create()( + persist( + set => ({ + locale: initialLocale, + setLocale: locale => { + changeAppLanguage(locale) + set({locale}) + }, + }), + { + name: 'locale-storage', + storage: createJSONStorage(() => AsyncStorage), + onRehydrateStorage: () => { + return (state, error) => { + if (error) { + console.warn('an error happened during hydration', error) + } else { + if (state?.locale) { + changeAppLanguage(state.locale) + } + } + } + }, + }, + ), +) + +type ColorSchemeState = { + colorScheme: 'light' | 'dark' + setColorScheme: (colorScheme: ColorSchemeState['colorScheme']) => void + toggleColorScheme: () => void +} + +export const useColorSchemeStore = create()( + persist( + set => ({ + colorScheme: 'dark', + setColorScheme: colorScheme => set({colorScheme}), + toggleColorScheme: () => + set(state => ({ + colorScheme: state.colorScheme === 'light' ? 'dark' : 'light', + })), + }), + { + name: 'color-scheme-storage', + storage: createJSONStorage(() => AsyncStorage), + }, + ), +) diff --git a/tailwind.config.js b/tailwind.config.js index 7d06379..785889c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { + darkMode: 'class', content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'], presets: [require('nativewind/preset')], theme: { @@ -7,12 +8,24 @@ module.exports = { colors: { primary: { DEFAULT: 'var(--color-primary)', + contrast: 'var(--color-primary-contrast)', + }, + background: { + DEFAULT: 'var(--color-background)', + contrast: 'var(--color-background-contrast)', + }, + divider: { + DEFAULT: 'var(--color-divider)', }, }, textColor: { primary: { + primary: 'var(--color-primary)', DEFAULT: 'var(--color-text-primary)', }, + secondary: { + DEFAULT: 'var(--color-text-secondary)', + }, }, }, }, diff --git a/theme.ts b/theme.ts deleted file mode 100644 index cacabae..0000000 --- a/theme.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {extendTheme} from 'native-base' - -export const theme = extendTheme({ - components: { - Text: { - baseStyle: { - lineHeight: null, - }, - }, - Divider: { - baseStyle: { - _dark: { - backgroundColor: 'muted.800', - }, - _light: { - backgroundColor: 'muted.300', - }, - }, - }, - Box: { - baseStyle: { - _dark: { - borderColor: 'muted.800', - }, - _light: { - borderColor: 'muted.300', - }, - }, - }, - }, -}) - -type CustomThemeType = typeof theme - -declare module 'native-base' { - interface ICustomTheme extends CustomThemeType {} -} diff --git a/themes.ts b/themes.ts index 2b596ab..b1e381b 100644 --- a/themes.ts +++ b/themes.ts @@ -1,11 +1,25 @@ import {vars, cssInterop} from 'nativewind' -import Ionicons from '@expo/vector-icons/Ionicons' +import Feather from '@expo/vector-icons/Feather' +import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons' +import {Image} from 'react-native' -cssInterop(Ionicons, { +cssInterop(Feather, { + className: { + target: 'style', + }, +}) + +cssInterop(MaterialCommunityIcons, { + className: { + target: 'style', + }, +}) + +cssInterop(Image, { className: { target: 'style', nativeStyleToProp: { - fontSize: 'size', + color: 'tintColor', }, }, }) @@ -20,11 +34,27 @@ export const themes: Record< twitter: { light: vars({ '--color-primary': '#1d9bf0', + '--color-primary-contrast': '#FFF', + + '--color-background': '#FFF', + '--color-background-contrast': '#000', + + '--color-divider': 'rgba(0, 0, 0, 0.1)', + '--color-text-primary': '#000', + '--color-text-secondary': 'rgba(0, 0, 0, 0.6)', }), dark: vars({ '--color-primary': '#1d9bf0', + '--color-primary-contrast': '#000', + + '--color-background': '#000', + '--color-background-contrast': '#FFF', + + '--color-divider': 'rgba(255, 255, 255, 0.2)', + '--color-text-primary': '#FFF', + '--color-text-secondary': 'rgba(255, 255, 255, 0.6)', }), }, }