Fix onboarding gate + tab icons + trim New-card screen
- Onboarding gate: the `hydrated` flag never flipped (onRehydrateStorage hit the store const during synchronous MMKV hydration), leaving a blank null gate. Dropped the flag; gate directly on `onboarded` (MMKV hydrates synchronously). - Tab bar: removed the item padding that clipped the icons; clean taller bar with safe-area bottom padding. - New-card screen: header title "Make your card" (centered), removed the body heading and all the detail fields + back-of-card links (captured in onboarding) and the Publish button (returns later on the flipped card). Keeps Take/Choose photo + AI scene/video + Save. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8865dc9683
commit
e77e812609
@@ -19,12 +19,10 @@ function tabIcon(name: IconName) {
|
|||||||
|
|
||||||
export default function TabsLayout() {
|
export default function TabsLayout() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const hydrated = useProfileStore((s) => s.hydrated);
|
|
||||||
const onboarded = useProfileStore((s) => s.onboarded);
|
const onboarded = useProfileStore((s) => s.onboarded);
|
||||||
|
|
||||||
// Wait for MMKV to rehydrate before deciding, then send first-run users to
|
// First-run users go to onboarding (MMKV hydrates synchronously, so this is
|
||||||
// onboarding (avoids a flash of the tabs).
|
// correct on the first render — no flash).
|
||||||
if (!hydrated) return null;
|
|
||||||
if (!onboarded) return <Redirect href="/onboarding" />;
|
if (!onboarded) return <Redirect href="/onboarding" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,11 +36,10 @@ export default function TabsLayout() {
|
|||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
borderTopColor: "#1c1c22",
|
borderTopColor: "#1c1c22",
|
||||||
height: 64 + insets.bottom,
|
height: 64 + insets.bottom,
|
||||||
paddingTop: 10,
|
paddingTop: 8,
|
||||||
paddingBottom: Math.max(insets.bottom, 14),
|
paddingBottom: insets.bottom + 10,
|
||||||
},
|
},
|
||||||
tabBarLabelStyle: { fontSize: 11, fontWeight: "600", marginBottom: 2 },
|
tabBarLabelStyle: { fontSize: 11, fontWeight: "600", marginBottom: 2 },
|
||||||
tabBarItemStyle: { paddingVertical: 4 },
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs.Screen name="index" options={{ title: "Cards", tabBarIcon: tabIcon("bag-personal") }} />
|
<Tabs.Screen name="index" options={{ title: "Cards", tabBarIcon: tabIcon("bag-personal") }} />
|
||||||
|
|||||||
@@ -269,8 +269,9 @@ export default function CardEditorScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
|
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
|
||||||
<Stack.Screen options={{ title: cardId ? "Edit card" : "New card" }} />
|
<Stack.Screen
|
||||||
<Text style={styles.h1}>{cardId ? "Edit your card" : "Make your card"}</Text>
|
options={{ title: cardId ? "Edit card" : "Make your card", headerTitleAlign: "center" }}
|
||||||
|
/>
|
||||||
|
|
||||||
{videoUri ? (
|
{videoUri ? (
|
||||||
<Video
|
<Video
|
||||||
@@ -309,14 +310,6 @@ export default function CardEditorScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.sectionLabel}>Details · prefilled from your profile</Text>
|
|
||||||
<Field label="Name" value={name} onChangeText={setName} />
|
|
||||||
<Field label="Title" value={title} onChangeText={setTitle} />
|
|
||||||
<Field label="QR link" value={url} onChangeText={setUrl} autoCapitalize="none" />
|
|
||||||
|
|
||||||
<Text style={styles.sectionLabel}>Back of card · scannable links</Text>
|
|
||||||
<LinksEditor links={links} onChange={setLinks} />
|
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.cta, (!media || saving) && styles.ctaDisabled]}
|
style={[styles.cta, (!media || saving) && styles.ctaDisabled]}
|
||||||
disabled={!media || saving}
|
disabled={!media || saving}
|
||||||
@@ -325,15 +318,6 @@ export default function CardEditorScreen() {
|
|||||||
{saving ? <ActivityIndicator color="#fff" /> : <Text style={styles.ctaText}>Save card</Text>}
|
{saving ? <ActivityIndicator color="#fff" /> : <Text style={styles.ctaText}>Save card</Text>}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Pressable
|
|
||||||
style={[styles.publishBtn, saving && styles.ctaDisabled]}
|
|
||||||
disabled={saving}
|
|
||||||
onPress={goPublish}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons name="web" size={20} color="#f5f5f7" />
|
|
||||||
<Text style={styles.photoText}>Publish to web · AI welcome</Text>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
{cardId && (
|
{cardId && (
|
||||||
<Pressable onPress={onDelete}>
|
<Pressable onPress={onDelete}>
|
||||||
<Text style={styles.deleteText}>Delete card</Text>
|
<Text style={styles.deleteText}>Delete card</Text>
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ const storage = new MMKV({ id: "cardclaws-profile" });
|
|||||||
interface ProfileState {
|
interface ProfileState {
|
||||||
onboarded: boolean;
|
onboarded: boolean;
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
hydrated: boolean;
|
|
||||||
setProfile: (patch: Partial<Profile>) => void;
|
setProfile: (patch: Partial<Profile>) => void;
|
||||||
completeOnboarding: (p: Profile) => void;
|
completeOnboarding: (p: Profile) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MMKV via createJSONStorage hydrates synchronously, so `onboarded` is correct
|
||||||
|
// on the first render — no async hydration flag needed (matches localCardsStore).
|
||||||
export const useProfileStore = create<ProfileState>()(
|
export const useProfileStore = create<ProfileState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
onboarded: false,
|
onboarded: false,
|
||||||
profile: EMPTY_PROFILE,
|
profile: EMPTY_PROFILE,
|
||||||
hydrated: false,
|
|
||||||
setProfile: (patch) => set((s) => ({ profile: { ...s.profile, ...patch } })),
|
setProfile: (patch) => set((s) => ({ profile: { ...s.profile, ...patch } })),
|
||||||
completeOnboarding: (p) => set({ profile: p, onboarded: true }),
|
completeOnboarding: (p) => set({ profile: p, onboarded: true }),
|
||||||
reset: () => set({ profile: EMPTY_PROFILE, onboarded: false }),
|
reset: () => set({ profile: EMPTY_PROFILE, onboarded: false }),
|
||||||
@@ -36,9 +36,6 @@ export const useProfileStore = create<ProfileState>()(
|
|||||||
removeItem: (k) => storage.delete(k),
|
removeItem: (k) => storage.delete(k),
|
||||||
})),
|
})),
|
||||||
partialize: (s) => ({ onboarded: s.onboarded, profile: s.profile }),
|
partialize: (s) => ({ onboarded: s.onboarded, profile: s.profile }),
|
||||||
onRehydrateStorage: () => () => {
|
|
||||||
useProfileStore.setState({ hydrated: true });
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user