Outer layout, some primitive updates
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 756 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 246 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -104,6 +104,9 @@ export const atoms = {
|
||||
flex: {
|
||||
display: 'flex',
|
||||
},
|
||||
flex_col: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
flex_row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
@@ -188,6 +191,9 @@ export const atoms = {
|
||||
lineHeight: tokens.fontSize._5xl,
|
||||
},
|
||||
leading_tight: {
|
||||
lineHeight: 1.15,
|
||||
},
|
||||
leading_snug: {
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
leading_normal: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import {Dimensions} from 'react-native'
|
||||
import * as themes from '#/alf/themes'
|
||||
|
||||
export * from '#/alf/types'
|
||||
export * as tokens from '#/alf/tokens'
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * from '#/alf/util/platform'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {StyleProp, ViewStyle, TextStyle} from 'react-native'
|
||||
|
||||
type LiteralToCommon<T extends PropertyKey> = T extends number
|
||||
? number
|
||||
: T extends string
|
||||
@@ -14,3 +16,11 @@ export type Mutable<T> = {
|
||||
? LiteralToCommon<T[K]>
|
||||
: Mutable<T[K]>
|
||||
}
|
||||
|
||||
export type TextStyleProp = {
|
||||
style?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
export type ViewStyleProp = {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
@@ -273,15 +273,15 @@ export function Button({
|
||||
|
||||
if (shape === 'default') {
|
||||
if (size === 'large') {
|
||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm)
|
||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm)
|
||||
}
|
||||
} else if (shape === 'round' || shape === 'square') {
|
||||
if (size === 'large') {
|
||||
baseStyles.push(a.p_lg)
|
||||
baseStyles.push({padding: 15})
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push(a.p_md)
|
||||
baseStyles.push({padding: 10})
|
||||
}
|
||||
|
||||
if (shape === 'round') {
|
||||
@@ -360,6 +360,7 @@ export function Button({
|
||||
flatten(style),
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.overflow_hidden,
|
||||
...baseStyles,
|
||||
...(state.hovered || state.pressed ? hoverStyles : []),
|
||||
@@ -506,9 +507,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
|
||||
return (
|
||||
<Text
|
||||
{...rest}
|
||||
style={[a.w_full, a.font_bold, a.text_center, textStyles, style]}>
|
||||
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
@@ -519,11 +518,11 @@ export function ButtonIcon({
|
||||
}: {
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
}) {
|
||||
const {size} = useButtonContext()
|
||||
const {size, disabled} = useButtonContext()
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
|
||||
return (
|
||||
<View style={[a.z_20]}>
|
||||
<View style={[a.z_20, {marginTop: -1, opacity: disabled ? 0.7 : 1}]}>
|
||||
<Comp
|
||||
size={size === 'large' ? 'md' : 'sm'}
|
||||
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import Animated, {
|
||||
Easing,
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withRepeat,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
||||
|
||||
export function Loader(props: Props) {
|
||||
const common = useCommonSVGProps(props)
|
||||
const rotation = useSharedValue(0)
|
||||
|
||||
const animatedStyles = useAnimatedStyle(() => ({
|
||||
transform: [{rotate: rotation.value + 'deg'}],
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
rotation.value = withRepeat(
|
||||
withTiming(360, {duration: 500, easing: Easing.linear}),
|
||||
-1,
|
||||
)
|
||||
}, [rotation])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
a.relative,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
{width: common.size, height: common.size},
|
||||
animatedStyles,
|
||||
]}>
|
||||
<Icon {...props} style={[a.absolute, a.inset_0, props.style]} />
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
+45
-36
@@ -12,45 +12,54 @@ type ComponentMap = {
|
||||
[id: string]: Component
|
||||
}
|
||||
|
||||
export const Context = React.createContext<ContextType>({
|
||||
outlet: null,
|
||||
append: () => {},
|
||||
remove: () => {},
|
||||
})
|
||||
export function createPortalGroup() {
|
||||
const Context = React.createContext<ContextType>({
|
||||
outlet: null,
|
||||
append: () => {},
|
||||
remove: () => {},
|
||||
})
|
||||
|
||||
export function Provider(props: React.PropsWithChildren<{}>) {
|
||||
const map = React.useRef<ComponentMap>({})
|
||||
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
|
||||
function Provider(props: React.PropsWithChildren<{}>) {
|
||||
const map = React.useRef<ComponentMap>({})
|
||||
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
|
||||
|
||||
const append = React.useCallback<ContextType['append']>((id, component) => {
|
||||
if (map.current[id]) return
|
||||
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
|
||||
setOutlet(<>{Object.values(map.current)}</>)
|
||||
}, [])
|
||||
const append = React.useCallback<ContextType['append']>((id, component) => {
|
||||
if (map.current[id]) return
|
||||
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
|
||||
setOutlet(<>{Object.values(map.current)}</>)
|
||||
}, [])
|
||||
|
||||
const remove = React.useCallback<ContextType['remove']>(id => {
|
||||
delete map.current[id]
|
||||
setOutlet(<>{Object.values(map.current)}</>)
|
||||
}, [])
|
||||
const remove = React.useCallback<ContextType['remove']>(id => {
|
||||
delete map.current[id]
|
||||
setOutlet(<>{Object.values(map.current)}</>)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{outlet, append, remove}}>
|
||||
{props.children}
|
||||
</Context.Provider>
|
||||
)
|
||||
return (
|
||||
<Context.Provider value={{outlet, append, remove}}>
|
||||
{props.children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Outlet() {
|
||||
const ctx = React.useContext(Context)
|
||||
return ctx.outlet
|
||||
}
|
||||
|
||||
function Portal({children}: React.PropsWithChildren<{}>) {
|
||||
const {append, remove} = React.useContext(Context)
|
||||
const id = React.useId()
|
||||
React.useEffect(() => {
|
||||
append(id, children as Component)
|
||||
return () => remove(id)
|
||||
}, [id, children, append, remove])
|
||||
return null
|
||||
}
|
||||
|
||||
return {Provider, Outlet, Portal}
|
||||
}
|
||||
|
||||
export function Outlet() {
|
||||
const ctx = React.useContext(Context)
|
||||
return ctx.outlet
|
||||
}
|
||||
|
||||
export function Portal({children}: React.PropsWithChildren<{}>) {
|
||||
const {append, remove} = React.useContext(Context)
|
||||
const id = React.useId()
|
||||
React.useEffect(() => {
|
||||
append(id, children as Component)
|
||||
return () => remove(id)
|
||||
}, [id, children, append, remove])
|
||||
return null
|
||||
}
|
||||
const DefaultPortal = createPortalGroup()
|
||||
export const Provider = DefaultPortal.Provider
|
||||
export const Outlet = DefaultPortal.Outlet
|
||||
export const Portal = DefaultPortal.Portal
|
||||
|
||||
@@ -3,6 +3,19 @@ import {Text as RNText, TextProps} from 'react-native'
|
||||
|
||||
import {useTheme, atoms, web, flatten} from '#/alf'
|
||||
|
||||
/**
|
||||
* Util to calculate lineHeight from a text size atom and a leading atom
|
||||
*
|
||||
* Example:
|
||||
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
|
||||
*/
|
||||
export function leading<
|
||||
Size extends {fontSize: number},
|
||||
Leading extends {lineHeight: number},
|
||||
>(textSize: Size, leading: Leading) {
|
||||
return (textSize.fontSize || atoms.text_md.fontSize) * leading.lineHeight
|
||||
}
|
||||
|
||||
export function Text({style, ...rest}: TextProps) {
|
||||
const t = useTheme()
|
||||
return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
|
||||
@@ -111,9 +124,11 @@ export function P({style, ...rest}: TextProps) {
|
||||
role: 'paragraph',
|
||||
}) || {}
|
||||
const _style = flatten(style)
|
||||
const lineHeight =
|
||||
(_style?.lineHeight || atoms.text_md.lineHeight) *
|
||||
atoms.leading_normal.lineHeight
|
||||
const lineHeight = leading(
|
||||
// @ts-ignore
|
||||
_style?.fontSize ? _style : atoms.text_md,
|
||||
atoms.leading_normal,
|
||||
)
|
||||
return (
|
||||
<RNText
|
||||
{...attr}
|
||||
|
||||
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
|
||||
export const ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z',
|
||||
})
|
||||
|
||||
export const ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const Loader_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z',
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const PlusLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
@@ -1,2 +1,4 @@
|
||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||
export const PWI_ENABLED = true
|
||||
// TODO
|
||||
export const NEW_ONBOARDING_ENABLED = true
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
|
||||
import {IS_DEV} from '#/env'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
|
||||
import {
|
||||
useTheme,
|
||||
atoms as a,
|
||||
useBreakpoints,
|
||||
web,
|
||||
native,
|
||||
flatten,
|
||||
TextStyleProp,
|
||||
} from '#/alf'
|
||||
import {H2, P, leading} from '#/components/Typography'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {ScrollView} from '#/view/com/util/Views'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/context'
|
||||
|
||||
const COL_WIDTH = 500
|
||||
|
||||
export const OnboardingControls = createPortalGroup()
|
||||
|
||||
export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
|
||||
return (
|
||||
<View style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
|
||||
{IS_DEV && (
|
||||
<View style={[a.absolute, a.p_xl, a.z_10, {right: 0, top: insets.top}]}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="negative"
|
||||
size="small"
|
||||
onPress={() => onboardDispatch({type: 'skip'})}
|
||||
label="Clear onboarding state">
|
||||
Clear
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{state.hasPrev && (
|
||||
<View
|
||||
style={[
|
||||
web(a.fixed),
|
||||
native(a.absolute),
|
||||
a.flex_row,
|
||||
a.w_full,
|
||||
a.justify_center,
|
||||
a.z_20,
|
||||
a.px_xl,
|
||||
{
|
||||
top: a.py_5xl.paddingTop + insets.top,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.w_full, a.align_start, {maxWidth: COL_WIDTH}]}>
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="small"
|
||||
shape="round"
|
||||
label="Go back"
|
||||
style={[a.absolute, t.atoms.shadow_sm]}
|
||||
onPress={() => dispatch({type: 'prev'})}>
|
||||
<ButtonIcon icon={ChevronLeft} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
|
||||
contentContainerStyle={{borderWidth: 0}}>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
|
||||
<View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
|
||||
<View style={[a.w_full, a.align_center, a.pt_5xl]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
a.w_full,
|
||||
{paddingTop: 17, maxWidth: '60%'},
|
||||
]}>
|
||||
{Array(state.totalSteps)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.pt_xs,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
backgroundColor:
|
||||
i + 1 <= state.activeStepIndex
|
||||
? t.palette.primary_500
|
||||
: t.palette.contrast_100,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.w_full, a.mb_5xl, {paddingTop: 120}]}>
|
||||
{children}
|
||||
|
||||
{isWeb && gtMobile && (
|
||||
<View style={[a.w_full, a.flex_row, a.justify_end, a.pt_5xl]}>
|
||||
<OnboardingControls.Outlet />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{(!isWeb || !gtMobile) && (
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
gtMobile ? a.px_5xl : a.px_xl,
|
||||
{
|
||||
paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom),
|
||||
},
|
||||
]}>
|
||||
<View style={[a.w_full, {maxWidth: COL_WIDTH}]}>
|
||||
<OnboardingControls.Outlet />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Title({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<TextStyleProp>) {
|
||||
return (
|
||||
<H2
|
||||
style={[
|
||||
a.pb_sm,
|
||||
{
|
||||
lineHeight: leading(a.text_4xl, a.leading_tight),
|
||||
},
|
||||
flatten(style),
|
||||
]}>
|
||||
{children}
|
||||
</H2>
|
||||
)
|
||||
}
|
||||
|
||||
export function Description({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<TextStyleProp>) {
|
||||
const t = useTheme()
|
||||
return <P style={[t.atoms.text_contrast_700, flatten(style)]}>{children}</P>
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/context'
|
||||
import {
|
||||
Title,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
|
||||
export function StepAlgoFeeds() {
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<Title>Choose your algorithmic feeds</Title>
|
||||
<Description>
|
||||
Feeds are created by users and can give you entirely new experiences.
|
||||
</Description>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="gradient"
|
||||
color="gradient_sky"
|
||||
size="large"
|
||||
label="Continue setting up your account"
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>Continue</ButtonText>
|
||||
<ButtonIcon icon={ChevronRight} />
|
||||
</Button>
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/context'
|
||||
import {
|
||||
Title,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
|
||||
export function StepFollowingFeed() {
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<Title>Your default feed is "Following"</Title>
|
||||
<Description>
|
||||
It only show posts from the people your follow, in the order they were
|
||||
posted.
|
||||
</Description>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="gradient"
|
||||
color="gradient_sky"
|
||||
size="large"
|
||||
label="Continue setting up your account"
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>Continue</ButtonText>
|
||||
<ButtonIcon icon={ChevronRight} />
|
||||
</Button>
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {useTheme, atoms as a} from '#/alf'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Loader} from '#/components/Loader'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/context'
|
||||
import {
|
||||
Title,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
|
||||
export function StepInterests() {
|
||||
const t = useTheme()
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
const [saving, setSaving] = React.useState(false)
|
||||
|
||||
const saveInterests = React.useCallback(async () => {
|
||||
setSaving(true)
|
||||
await new Promise(y => setTimeout(y, 1000))
|
||||
setSaving(false)
|
||||
dispatch({type: 'next'})
|
||||
}, [setSaving, dispatch])
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.p_lg,
|
||||
a.rounded_full,
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975,
|
||||
},
|
||||
]}>
|
||||
<At size="xl" fill={t.palette.primary_500} />
|
||||
</View>
|
||||
|
||||
<Title>What are your interests?</Title>
|
||||
<Description>
|
||||
We'll use this to help customize your experience.
|
||||
</Description>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<Button
|
||||
disabled={saving}
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="gradient"
|
||||
color="gradient_sky"
|
||||
size="large"
|
||||
label="Continue setting up your account"
|
||||
onPress={saveInterests}>
|
||||
<ButtonText>Continue</ButtonText>
|
||||
<ButtonIcon icon={saving ? Loader : ChevronRight} />
|
||||
</Button>
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/context'
|
||||
import {
|
||||
Title,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
|
||||
export function StepSuggestedFollows() {
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<Title>Here are some accounts for your to follow:</Title>
|
||||
<Description>Based on your interest in Pets and Books.</Description>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="gradient"
|
||||
color="gradient_sky"
|
||||
size="large"
|
||||
label="Continue setting up your account"
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>Follow All</ButtonText>
|
||||
<ButtonIcon icon={Plus} />
|
||||
</Button>
|
||||
<Button
|
||||
key={state.activeStep + '2'} // remove focus state on nav
|
||||
variant="outline"
|
||||
color="secondary"
|
||||
size="large"
|
||||
label="Continue setting up your account"
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
Skip
|
||||
</Button>
|
||||
</View>
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/context'
|
||||
import {
|
||||
Title,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
|
||||
export function StepTopicalFeeds() {
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<Title>Feeds can be topic-based as well!</Title>
|
||||
<Description>
|
||||
Here are some topic-based feeds based on your interests in Pets and
|
||||
Books.
|
||||
</Description>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="gradient"
|
||||
color="gradient_sky"
|
||||
size="large"
|
||||
label="Continue setting up your account"
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>Continue</ButtonText>
|
||||
<ButtonIcon icon={ChevronRight} />
|
||||
</Button>
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
|
||||
export type OnboardingState = {
|
||||
hasPrev: boolean
|
||||
totalSteps: number
|
||||
activeStep:
|
||||
| 'interests'
|
||||
| 'suggestedFollows'
|
||||
| 'followingFeed'
|
||||
| 'algoFeeds'
|
||||
| 'topicalFeeds'
|
||||
activeStepIndex: number
|
||||
}
|
||||
|
||||
export type OnboardingAction =
|
||||
| {
|
||||
type: 'next'
|
||||
}
|
||||
| {
|
||||
type: 'prev'
|
||||
}
|
||||
|
||||
export const initialState: OnboardingState = {
|
||||
hasPrev: false,
|
||||
totalSteps: 5,
|
||||
activeStep: 'interests',
|
||||
activeStepIndex: 1,
|
||||
}
|
||||
|
||||
export const Context = React.createContext<{
|
||||
state: OnboardingState
|
||||
dispatch: React.Dispatch<OnboardingAction>
|
||||
}>({
|
||||
state: initialState,
|
||||
dispatch: () => {},
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
||||
import {
|
||||
Context,
|
||||
OnboardingState,
|
||||
OnboardingAction,
|
||||
initialState,
|
||||
} from '#/screens/Onboarding/context'
|
||||
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||
import {StepInterests} from '#/screens/Onboarding/StepInterests'
|
||||
import {StepSuggestedFollows} from '#/screens/Onboarding/StepSuggestedFollows'
|
||||
import {StepFollowingFeed} from '#/screens/Onboarding/StepFollowingFeed'
|
||||
import {StepAlgoFeeds} from '#/screens/Onboarding/StepAlgoFeeds'
|
||||
import {StepTopicalFeeds} from '#/screens/Onboarding/StepTopicalFeeds'
|
||||
|
||||
function reducer(s: OnboardingState, a: OnboardingAction): OnboardingState {
|
||||
const next = s
|
||||
|
||||
switch (a.type) {
|
||||
case 'next': {
|
||||
if (s.activeStep === 'interests') {
|
||||
next.activeStep = 'suggestedFollows'
|
||||
next.activeStepIndex = 2
|
||||
} else if (s.activeStep === 'suggestedFollows') {
|
||||
next.activeStep = 'followingFeed'
|
||||
next.activeStepIndex = 3
|
||||
} else if (s.activeStep === 'followingFeed') {
|
||||
next.activeStep = 'algoFeeds'
|
||||
next.activeStepIndex = 4
|
||||
} else if (s.activeStep === 'algoFeeds') {
|
||||
next.activeStep = 'topicalFeeds'
|
||||
next.activeStepIndex = 5
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'prev': {
|
||||
if (s.activeStep === 'suggestedFollows') {
|
||||
next.activeStep = 'interests'
|
||||
next.activeStepIndex = 1
|
||||
} else if (s.activeStep === 'followingFeed') {
|
||||
next.activeStep = 'suggestedFollows'
|
||||
next.activeStepIndex = 2
|
||||
} else if (s.activeStep === 'algoFeeds') {
|
||||
next.activeStep = 'followingFeed'
|
||||
next.activeStepIndex = 3
|
||||
} else if (s.activeStep === 'topicalFeeds') {
|
||||
next.activeStep = 'algoFeeds'
|
||||
next.activeStepIndex = 4
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const state = {
|
||||
...next,
|
||||
hasPrev: next.activeStep !== 'interests',
|
||||
}
|
||||
|
||||
logger.debug(`onboarding`, state)
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export function Onboarding() {
|
||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<OnboardingControls.Provider>
|
||||
<Context.Provider
|
||||
value={React.useMemo(() => ({state, dispatch}), [state, dispatch])}>
|
||||
<Layout>
|
||||
{state.activeStep === 'interests' && <StepInterests />}
|
||||
{state.activeStep === 'suggestedFollows' && (
|
||||
<StepSuggestedFollows />
|
||||
)}
|
||||
{state.activeStep === 'followingFeed' && <StepFollowingFeed />}
|
||||
{state.activeStep === 'algoFeeds' && <StepAlgoFeeds />}
|
||||
{state.activeStep === 'topicalFeeds' && <StepTopicalFeeds />}
|
||||
</Layout>
|
||||
</Context.Provider>
|
||||
</OnboardingControls.Provider>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {PWI_ENABLED} from '#/lib/build-flags'
|
||||
import {PWI_ENABLED, NEW_ONBOARDING_ENABLED} from '#/lib/build-flags'
|
||||
|
||||
// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
|
||||
// MIT License
|
||||
@@ -37,6 +37,7 @@ import {useSession} from '#/state/session'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {LoggedOut} from '../com/auth/LoggedOut'
|
||||
import {Onboarding} from '../com/auth/Onboarding'
|
||||
import {Onboarding as NewOnboarding} from '#/screens/Onboarding'
|
||||
|
||||
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
||||
requireAuth?: boolean
|
||||
@@ -107,7 +108,11 @@ function NativeStackNavigator({
|
||||
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
||||
}
|
||||
if (onboardingState.isActive) {
|
||||
return <Onboarding />
|
||||
if (NEW_ONBOARDING_ENABLED) {
|
||||
return <NewOnboarding />
|
||||
} else {
|
||||
return <Onboarding />
|
||||
}
|
||||
}
|
||||
const newDescriptors: typeof descriptors = {}
|
||||
for (let key in descriptors) {
|
||||
|
||||
Reference in New Issue
Block a user