Replace first-hatch modal with inline card, hide dashboard controls during tour
UX change: the first-hatch experience is now a focused onboarding screen instead of a modal interruption. Layout during first-hatch tour: - Egg visual (top, with tour animations) - Stats (if any visible) - FirstHatchTourCard inline below stats (mission + post CTA) - No floating hero controls (camera, info, companion, incubation) - No bottom action bar (blobbies, missions, actions, shop, inventory) - No inline activity area (music, sing) The page feels like a dedicated guided flow rather than a dashboard with overlays. Normal dashboard controls return after tour completion. Architecture: clean branch in BlobbiDashboard render -- isFirstHatchTourActive gates visibility of controls/bar/activities. The inline card lives at the same level as other content sections. The first egg is treated as already in the hatch onboarding path without requiring the normal 'start incubation' entry point.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* FirstHatchTourCard - Inline card shown below the egg during the first-hatch tour.
|
||||
*
|
||||
* Replaces the modal. Rendered directly in the BlobbiPage layout so the
|
||||
* experience feels focused and guided rather than interrupted.
|
||||
*/
|
||||
|
||||
import { Send, Check } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface FirstHatchTourCardProps {
|
||||
/** The Blobbi's display name */
|
||||
blobbiName: string;
|
||||
/** The exact phrase the user needs to include in their post */
|
||||
requiredPhrase: string;
|
||||
/** Whether the post mission has been completed */
|
||||
postCompleted: boolean;
|
||||
/** Open the post composer */
|
||||
onCreatePost: () => void;
|
||||
/** Advance the tour (called after post is confirmed complete) */
|
||||
onContinue: () => void;
|
||||
}
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function FirstHatchTourCard({
|
||||
blobbiName,
|
||||
requiredPhrase,
|
||||
postCompleted,
|
||||
onCreatePost,
|
||||
onContinue,
|
||||
}: FirstHatchTourCardProps) {
|
||||
const capitalizedName = blobbiName.charAt(0).toUpperCase() + blobbiName.slice(1);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-sm mx-auto px-4 space-y-4">
|
||||
{/* Title + description */}
|
||||
<div className="text-center space-y-1.5">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{capitalizedName} is ready to hatch!
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Share a post to the Nostr network and help {capitalizedName} break free.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mission card */}
|
||||
<div className="rounded-xl border bg-card p-4 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Status indicator */}
|
||||
<div className={
|
||||
postCompleted
|
||||
? 'mt-0.5 size-5 rounded-full bg-emerald-500/15 flex items-center justify-center shrink-0'
|
||||
: 'mt-0.5 size-5 rounded-full border-2 border-muted-foreground/30 shrink-0'
|
||||
}>
|
||||
{postCompleted && <Check className="size-3 text-emerald-500" />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{postCompleted ? 'Post shared!' : 'Share a hatch post'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your post must include:
|
||||
</p>
|
||||
<p className="text-xs font-medium text-primary break-words">
|
||||
{requiredPhrase}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!postCompleted && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={onCreatePost}
|
||||
>
|
||||
<Send className="size-3.5 mr-2" />
|
||||
Create Post
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Continue or hint */}
|
||||
{postCompleted ? (
|
||||
<Button className="w-full" onClick={onContinue}>
|
||||
Continue
|
||||
</Button>
|
||||
) : (
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
You can add extra text before or after the required phrase.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,4 +43,4 @@ export type {
|
||||
} from './hooks/useFirstHatchTourActivation';
|
||||
|
||||
// ── First Hatch Tour - Components ──
|
||||
export { FirstHatchTourModal } from './components/FirstHatchTourModal';
|
||||
export { FirstHatchTourCard } from './components/FirstHatchTourCard';
|
||||
|
||||
+45
-51
@@ -90,7 +90,7 @@ import { getActionEmotion, type ActionType } from '@/blobbi/ui/lib/status-reacti
|
||||
import type { BlobbiEmotion } from '@/blobbi/ui/lib/emotions';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useFirstHatchTour, useFirstHatchTourActivation, FirstHatchTourModal } from '@/blobbi/tour';
|
||||
import { useFirstHatchTour, useFirstHatchTourActivation, FirstHatchTourCard } from '@/blobbi/tour';
|
||||
import { buildHatchPhrase, isValidHatchPost } from '@/blobbi/actions';
|
||||
import type { EggTourVisualState } from '@/blobbi/egg';
|
||||
|
||||
@@ -946,26 +946,30 @@ function BlobbiDashboard({
|
||||
// The required phrase for the first-hatch post
|
||||
const firstHatchPhrase = useMemo(() => buildHatchPhrase(companion.name), [companion.name]);
|
||||
|
||||
// Auto-advance from idle to egg_ready_hint, then to show_hatch_modal
|
||||
// Auto-advance from idle -> egg_ready_hint -> show_hatch_modal (inline card)
|
||||
useEffect(() => {
|
||||
if (!isFirstHatchTourActive) return;
|
||||
if (firstHatchTour.isStep('idle')) {
|
||||
// Advance immediately to egg_ready_hint
|
||||
firstHatchTour.actions.advance();
|
||||
firstHatchTour.actions.advance(); // -> egg_ready_hint
|
||||
}
|
||||
}, [isFirstHatchTourActive, firstHatchTour]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFirstHatchTourActive) return;
|
||||
if (firstHatchTour.isStep('egg_ready_hint')) {
|
||||
// Show the ready hint briefly, then move to the modal
|
||||
// Show the ready hint wiggle briefly, then show the inline card
|
||||
const timer = setTimeout(() => {
|
||||
firstHatchTour.actions.advance();
|
||||
firstHatchTour.actions.advance(); // -> show_hatch_modal (inline card step)
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isFirstHatchTourActive, firstHatchTour]);
|
||||
|
||||
// Whether the inline first-hatch card should be shown
|
||||
const showFirstHatchCard = isFirstHatchTourActive && (
|
||||
firstHatchTour.isStep('show_hatch_modal') || firstHatchTour.isStep('await_create_post')
|
||||
);
|
||||
|
||||
// Detect hatch post completion for the first-hatch tour
|
||||
const { user } = useCurrentUser();
|
||||
const { nostr } = useNostr();
|
||||
@@ -1419,8 +1423,8 @@ function BlobbiDashboard({
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-4 py-4 sm:px-6">
|
||||
{/* Floating Dashboard Controls */}
|
||||
<BlobbiDashboardFloatingControls
|
||||
{/* Floating Dashboard Controls - hidden during first-hatch tour */}
|
||||
{!isFirstHatchTourActive && <BlobbiDashboardFloatingControls
|
||||
stage={companion.stage}
|
||||
onSetAsCompanion={handleSetAsCompanion}
|
||||
isCurrentCompanion={isCurrentCompanion}
|
||||
@@ -1453,7 +1457,7 @@ function BlobbiDashboard({
|
||||
onDevOpenEditor={() => setShowDevEditor(true)}
|
||||
// DEV ONLY: Open emotion tester panel
|
||||
onDevOpenEmotionPanel={() => setShowEmotionPanel(true)}
|
||||
/>
|
||||
/>}
|
||||
|
||||
{/* Blobbi Name */}
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
@@ -1528,8 +1532,23 @@ function BlobbiDashboard({
|
||||
|
||||
|
||||
|
||||
{/* Inline Activity Area - inside padded container for proper spacing above bottom bar */}
|
||||
{inlineActivity.type === 'music' && (
|
||||
{/* First Hatch Tour: inline card below stats */}
|
||||
{showFirstHatchCard && (
|
||||
<div className="mt-6">
|
||||
<FirstHatchTourCard
|
||||
blobbiName={companion.name}
|
||||
requiredPhrase={firstHatchPhrase}
|
||||
postCompleted={!!tourPostFound}
|
||||
onCreatePost={() => setShowPostModal(true)}
|
||||
onContinue={() => {
|
||||
firstHatchTour.actions.goTo('egg_glowing_waiting_click');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inline Activity Area - hidden during first-hatch tour */}
|
||||
{!isFirstHatchTourActive && inlineActivity.type === 'music' && (
|
||||
<div className="mt-6">
|
||||
<InlineMusicPlayer
|
||||
selection={inlineActivity.selection}
|
||||
@@ -1543,7 +1562,7 @@ function BlobbiDashboard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inlineActivity.type === 'sing' && (
|
||||
{!isFirstHatchTourActive && inlineActivity.type === 'sing' && (
|
||||
<div className="mt-6">
|
||||
<InlineSingCard
|
||||
onConfirm={handleConfirmSing}
|
||||
@@ -1556,27 +1575,20 @@ function BlobbiDashboard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Action Bar */}
|
||||
<BlobbiBottomBar
|
||||
onBlobbiesClick={() => setShowSelector(true)}
|
||||
onMissionsClick={() => {
|
||||
if (isFirstHatchTourActive) {
|
||||
// During the first-hatch tour, open the tour modal instead
|
||||
if (!firstHatchTour.isStep('show_hatch_modal') && !firstHatchTour.isStep('await_create_post')) {
|
||||
firstHatchTour.actions.goTo('show_hatch_modal');
|
||||
}
|
||||
} else {
|
||||
setShowMissionsModal(true);
|
||||
}
|
||||
}}
|
||||
onActionsClick={() => setShowActionsModal(true)}
|
||||
onShopClick={() => setShowShopModal(true)}
|
||||
onInventoryClick={() => setShowInventoryModal(true)}
|
||||
needyBlobbiesCount={companions.filter(companionNeedsCare).length}
|
||||
isInTaskProcess={isFirstHatchTourActive || isInTaskProcess}
|
||||
remainingTasksCount={isFirstHatchTourActive ? (tourPostFound ? 0 : 1) : remainingTasksCount}
|
||||
allTasksComplete={isFirstHatchTourActive ? !!tourPostFound : allTasksComplete}
|
||||
/>
|
||||
{/* Bottom Action Bar - hidden during first-hatch tour */}
|
||||
{!isFirstHatchTourActive && (
|
||||
<BlobbiBottomBar
|
||||
onBlobbiesClick={() => setShowSelector(true)}
|
||||
onMissionsClick={() => setShowMissionsModal(true)}
|
||||
onActionsClick={() => setShowActionsModal(true)}
|
||||
onShopClick={() => setShowShopModal(true)}
|
||||
onInventoryClick={() => setShowInventoryModal(true)}
|
||||
needyBlobbiesCount={companions.filter(companionNeedsCare).length}
|
||||
isInTaskProcess={isInTaskProcess}
|
||||
remainingTasksCount={remainingTasksCount}
|
||||
allTasksComplete={allTasksComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Blobbi Selector Modal */}
|
||||
<Dialog open={showSelector} onOpenChange={setShowSelector}>
|
||||
@@ -1724,24 +1736,6 @@ function BlobbiDashboard({
|
||||
onSuccess={refetchCurrentTasks}
|
||||
/>
|
||||
|
||||
{/* First Hatch Tour Modal */}
|
||||
<FirstHatchTourModal
|
||||
open={isFirstHatchTourActive && (firstHatchTour.isStep('show_hatch_modal') || firstHatchTour.isStep('await_create_post'))}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && isFirstHatchTourActive && firstHatchTour.isStep('show_hatch_modal')) {
|
||||
// User dismissed the modal -- move to await_create_post so they can
|
||||
// come back to it (the modal stays open for await_create_post too)
|
||||
firstHatchTour.actions.advance();
|
||||
}
|
||||
}}
|
||||
blobbiName={companion.name}
|
||||
requiredPhrase={firstHatchPhrase}
|
||||
postCompleted={!!tourPostFound}
|
||||
onCreatePost={() => setShowPostModal(true)}
|
||||
onContinue={() => {
|
||||
firstHatchTour.actions.goTo('egg_glowing_waiting_click');
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Blobbi Photo Modal - polaroid-style photo capture */}
|
||||
<BlobbiPhotoModal
|
||||
|
||||
Reference in New Issue
Block a user