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 (
- <>
-
-
-
-
-
-
-
-
-
-
- Guillaume Martinez
- {i18n.t('home.title')}
-
-
-
-
-
+
+
-
-
- 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 => (
))}
-
+
)}
{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 (
-
- )
+ // const setLocale = useLocalStore(state => state.setLocale)
+ // const locale = useLocalStore(state => state.locale)
+ return null
+ // return (
+ //
+ // )
})
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)',
}),
},
}