Part 2B: Building UI Components (Lanjutan)
Module 3 (continued) | Building Complete Garden UI - Part 2
ð Overviewâ
Ini adalah lanjutan dari Part 2A. Di sini kita akan menyelesaikan semua UI components:
Yang akan dibuat:
- â PlantDetailsModal - Modal lengkap untuk plant actions dengan visualisasi
- â PlantSeedModal - Modal untuk plant new seed
- â StatsSidebar - Sidebar dengan statistics & game info
- â Main Page (app/page.tsx) - Menggabungkan semua components
Section C: Plant Modals (60 menit)â
Step 5: Create PlantDetailsModal Componentâ
Modal lengkap dengan plant visualization, growth progress, water level, dan action buttons.
Create: components/plant-details-modal.tsx
"use client"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Card } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
import { Button } from "@/components/ui/button"
import { Droplets, Sparkles, TrendingUp, Coins, Skull, RefreshCw } from "lucide-react"
import { Plant, GrowthStage, STAGE_NAMES } from "@/types/contracts"
import { formatPlantAge, formatLastWatered, canHarvest, getPlantProgress, getClientWaterLevel, isCritical, isStageOutOfSync, getExpectedStage } from "@/lib/contract"
import { usePlants } from "@/hooks/usePlants"
import { HARVEST_REWARD } from "@/types/contracts"
const STAGE_EMOJIS = {
[GrowthStage.SEED]: "ðą",
[GrowthStage.SPROUT]: "ðŋ",
[GrowthStage.GROWING]: "ðŠī",
[GrowthStage.BLOOMING]: "ðļ",
}
const STAGE_BACKGROUNDS = {
[GrowthStage.SEED]: "from-amber-50 to-yellow-50 dark:from-amber-950 dark:to-yellow-950",
[GrowthStage.SPROUT]: "from-green-50 to-lime-50 dark:from-green-950 dark:to-lime-950",
[GrowthStage.GROWING]: "from-emerald-50 to-teal-50 dark:from-emerald-950 dark:to-teal-950",
[GrowthStage.BLOOMING]: "from-pink-50 to-rose-50 dark:from-pink-950 dark:to-rose-950",
}
interface PlantDetailsModalProps {
plant: Plant | null
isOpen: boolean
onClose: () => void
}
export default function PlantDetailsModal({ plant, isOpen, onClose }: PlantDetailsModalProps) {
const { harvestPlant, waterPlant, updatePlantStage, loading } = usePlants()
if (!plant) return null
const stageKey = STAGE_NAMES[plant.stage]
const progress = getPlantProgress(plant)
const canHarvestPlant = canHarvest(plant)
const currentWaterLevel = getClientWaterLevel(plant)
const critical = isCritical(plant)
const stageOutOfSync = isStageOutOfSync(plant)
const expectedStage = getExpectedStage(plant)
const handleHarvest = async () => {
await harvestPlant(plant.id)
onClose()
}
const handleWater = async () => {
await waterPlant(plant.id)
onClose()
}
const handleUpdateStage = async () => {
await updatePlantStage(plant.id)
// Don't close modal - let user see the updated stage
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<span className="text-3xl">{plant.isDead ? 'ð' : STAGE_EMOJIS[plant.stage]}</span>
Plant #{plant.id.toString()}
{plant.isDead && <span className="text-sm text-gray-500">(Dead)</span>}
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* Plant visualization */}
<div className={`h-40 rounded-lg flex items-center justify-center relative overflow-hidden ${
plant.isDead
? 'bg-gradient-to-b from-gray-200 to-gray-300 dark:from-gray-800 dark:to-gray-900'
: `bg-gradient-to-b ${STAGE_BACKGROUNDS[plant.stage]}`
}`}>
{plant.isDead ? (
<div className="text-8xl grayscale opacity-50">ð</div>
) : (
<>
<div className="text-8xl animate-float">{STAGE_EMOJIS[plant.stage]}</div>
{/* Stage-specific decorations */}
{plant.stage === GrowthStage.SEED && (
<>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-40 h-3 bg-amber-600/20 dark:bg-amber-400/20 rounded-full" />
<div className="absolute top-4 right-6 text-3xl opacity-30">âïļ</div>
</>
)}
{plant.stage === GrowthStage.SPROUT && (
<>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-32 h-3 bg-green-600/30 dark:bg-green-400/30 rounded-full" />
<div className="absolute top-4 left-6 text-2xl opacity-40 animate-pulse">ð§</div>
<div className="absolute top-4 right-6 text-2xl opacity-40">âïļ</div>
</>
)}
{plant.stage === GrowthStage.GROWING && (
<>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-36 h-4 bg-emerald-600/40 dark:bg-emerald-400/40 rounded-full" />
<div className="absolute top-6 left-8 text-2xl opacity-30 animate-bounce">ð</div>
<div className="absolute bottom-8 right-8 text-2xl opacity-30 animate-bounce delay-100">ð</div>
</>
)}
{plant.stage === GrowthStage.BLOOMING && (
<>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 w-40 h-5 bg-rose-600/40 dark:bg-rose-400/40 rounded-full" />
<div className="absolute top-3 right-3 animate-bounce">
<Sparkles className="w-6 h-6 text-yellow-500" />
</div>
<div className="absolute top-6 left-6 text-3xl animate-pulse">âĻ</div>
<div className="absolute bottom-8 left-8 text-2xl opacity-50 animate-bounce">ðš</div>
<div className="absolute bottom-8 right-8 text-2xl opacity-50 animate-bounce delay-100">ðĶ</div>
</>
)}
</>
)}
{!plant.isDead && critical && (
<div className="absolute top-3 left-3 animate-pulse">
<Skull className="w-6 h-6 text-red-500" />
</div>
)}
</div>
{/* Plant info */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Stage</span>
<span className="text-sm font-semibold text-foreground capitalize">{stageKey}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Planted</span>
<span className="text-sm font-semibold text-foreground">{formatPlantAge(plant.plantedDate)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Last Watered</span>
<span className="text-sm font-semibold text-foreground">{formatLastWatered(plant.lastWatered)}</span>
</div>
</div>
{/* Growth Progress */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-sm font-medium text-foreground">
<TrendingUp className="w-4 h-4 text-primary" />
Growth Progress
</span>
<span className="text-sm font-semibold text-foreground">{Math.floor(progress)}%</span>
</div>
<Progress value={progress} className="h-3" />
<p className="text-xs text-muted-foreground">
{plant.stage === GrowthStage.SEED && "ðą Germinating... Water regularly to sprout!"}
{plant.stage === GrowthStage.SPROUT && "ðŋ Growing strong! Keep watering to reach next stage."}
{plant.stage === GrowthStage.GROWING && "ðŠī Almost there! One more stage until blooming."}
{plant.stage === GrowthStage.BLOOMING && "ðļ Fully grown! Ready to harvest for rewards!"}
</p>
</div>
{/* Water level */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-sm font-medium text-foreground">
<Droplets className={`w-4 h-4 ${plant.isDead ? 'text-gray-400' : 'text-blue-500'}`} />
Water Level
</span>
<span className="text-sm font-semibold text-foreground">{currentWaterLevel}%</span>
</div>
<Progress value={currentWaterLevel} className="h-3" />
{!plant.isDead && currentWaterLevel < 50 && currentWaterLevel > 20 && (
<p className="text-xs text-orange-600 dark:text-orange-400">â ïļ Water level low! Water your plant soon.</p>
)}
{!plant.isDead && currentWaterLevel <= 20 && currentWaterLevel > 0 && (
<p className="text-xs text-red-600 dark:text-red-400 animate-pulse">ðĻ Critical! Plant will die soon!</p>
)}
{plant.isDead && (
<p className="text-xs text-gray-600 dark:text-gray-400">ð Plant died from dehydration</p>
)}
</div>
{/* Stage sync warning */}
{!plant.isDead && stageOutOfSync && (
<Card className="p-4 bg-gradient-to-br from-orange-500/10 to-yellow-500/10 border-orange-500/30">
<div className="space-y-3">
<div className="text-center space-y-1">
<p className="font-semibold text-foreground flex items-center justify-center gap-2">
<RefreshCw className="w-4 h-4 text-orange-500" />
Stage Out of Sync
</p>
<p className="text-sm text-muted-foreground">
On-chain: {STAGE_NAMES[plant.stage]} â Expected: {STAGE_NAMES[expectedStage]}
</p>
<p className="text-xs text-muted-foreground">
Update the stage to sync blockchain state with actual growth time
</p>
</div>
<Button
onClick={handleUpdateStage}
disabled={loading}
className="w-full gap-2 bg-orange-600 hover:bg-orange-700 text-white"
size="sm"
>
{loading ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Updating...
</>
) : (
<>
<RefreshCw className="w-4 h-4" />
Update Stage
</>
)}
</Button>
</div>
</Card>
)}
{/* Harvest info */}
{canHarvestPlant && (
<Card className="p-4 bg-gradient-to-br from-yellow-500/10 to-green-500/10 border-yellow-500/30">
<div className="text-center space-y-2">
<p className="font-semibold text-foreground flex items-center justify-center gap-2">
<Sparkles className="w-4 h-4 text-yellow-500" />
Ready to Harvest!
</p>
<p className="text-sm text-muted-foreground">
Harvest this plant to receive
</p>
<p className="flex items-center justify-center gap-2 font-bold text-lg text-primary">
<Coins className="w-5 h-5" />
{HARVEST_REWARD} ETH
</p>
</div>
</Card>
)}
{/* Dead plant warning */}
{plant.isDead && (
<Card className="p-4 bg-gradient-to-br from-gray-500/10 to-gray-500/10 border-gray-500/30">
<div className="text-center space-y-2">
<p className="font-semibold text-foreground flex items-center justify-center gap-2">
<Skull className="w-4 h-4 text-gray-500" />
Plant Died
</p>
<p className="text-sm text-muted-foreground">
This plant died from lack of water. Plant a new seed to try again!
</p>
</div>
</Card>
)}
{/* Action buttons */}
<div className="flex gap-2 pt-4">
<Button
variant="outline"
onClick={onClose}
className="flex-1 bg-transparent"
disabled={loading}
>
Close
</Button>
{!plant.isDead && (
<>
{canHarvestPlant ? (
<Button
onClick={handleHarvest}
disabled={loading}
className="flex-1 gap-2 bg-gradient-to-r from-yellow-500 to-green-600 hover:from-yellow-600 hover:to-green-700 text-white"
>
{loading ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Harvesting...
</>
) : (
<>
<Sparkles className="w-4 h-4" />
Harvest Plant
</>
)}
</Button>
) : (
<Button
onClick={handleWater}
disabled={loading}
className="flex-1 gap-2 bg-blue-600 hover:bg-blue-700 text-white"
>
{loading ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Watering...
</>
) : (
<>
<Droplets className="w-4 h-4" />
Water Plant
</>
)}
</Button>
)}
</>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}
ðĄ Features:
- Plant Visualization - Animated background dengan stage-specific decorations
- Real-time Progress - Growth & water level dengan client-side calculation
- Stage Sync Warning - Deteksi kalau blockchain state out of sync
- Dynamic Actions - Water atau harvest button tergantung plant state
- Critical Warnings - Alert kalau water level critical
- Beautiful Animations - Smooth transitions & floating effects
Step 6: Create PlantSeedModal Componentâ
Modal untuk plant new seed dengan cost preview & game info.
Create: components/plant-seed-modal.tsx
"use client"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Sparkles, Coins } from "lucide-react"
import { usePlants } from "@/hooks/usePlants"
import { PLANT_PRICE } from "@/types/contracts"
interface PlantSeedModalProps {
isOpen: boolean
onClose: () => void
}
export default function PlantSeedModal({ isOpen, onClose }: PlantSeedModalProps) {
const { plantSeed, loading } = usePlants()
const handlePlant = async () => {
await plantSeed()
onClose()
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-accent" />
Plant a New Seed
</DialogTitle>
<DialogDescription>Plant a seed and watch it grow!</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Seed card */}
<Card className="p-6 border-2 border-primary/30 bg-gradient-to-br from-primary/5 to-accent/5">
<div className="text-center">
<div className="text-6xl mb-4">ðą</div>
<h3 className="font-bold text-xl text-foreground mb-2">Garden Seed</h3>
<p className="text-sm text-muted-foreground mb-4">
A magical seed that grows through 4 stages over time
</p>
{/* Growth stages preview */}
<div className="flex justify-center gap-2 mb-4">
<span className="text-2xl" title="Seed">ðą</span>
<span className="text-xl text-muted-foreground">â</span>
<span className="text-2xl" title="Sprout">ðŋ</span>
<span className="text-xl text-muted-foreground">â</span>
<span className="text-2xl" title="Growing">ðŠī</span>
<span className="text-xl text-muted-foreground">â</span>
<span className="text-2xl" title="Blooming">ðļ</span>
</div>
{/* Price */}
<div className="bg-card border border-border rounded-lg p-3">
<p className="text-xs text-muted-foreground mb-1">Cost</p>
<p className="flex items-center justify-center gap-2 font-bold text-lg text-accent">
<Coins className="w-5 h-5" />
{PLANT_PRICE} ETH
</p>
<p className="text-xs text-muted-foreground mt-1">
Harvest reward: 0.003 ETH (3x profit!)
</p>
</div>
</div>
</Card>
{/* Info card */}
<Card className="p-3 bg-muted/30 border-primary/20">
<p className="text-xs text-muted-foreground">
ð§ <strong>Watering is FREE</strong> - no cost, just gas!
<br />
âąïļ <strong>Growth time</strong>: 3 minutes from seed to blooming
<br />
ð° <strong>Profit</strong>: Earn 3x your investment when you harvest
<br />
â ïļ <strong>Keep watering</strong>: Plant dies if water reaches 0%
</p>
</Card>
{/* Action buttons */}
<div className="flex gap-2 pt-2">
<Button
variant="outline"
onClick={onClose}
className="flex-1 bg-transparent"
disabled={loading}
>
Cancel
</Button>
<Button
onClick={handlePlant}
disabled={loading}
className="flex-1 gap-2 bg-primary hover:bg-primary/90"
>
{loading ? (
<>
<div className="w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin" />
Planting...
</>
) : (
<>
<Sparkles className="w-4 h-4" />
Plant Seed
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}
ðĄ Features:
- Growth Preview - Visual representation 4 growth stages
- Cost Display - Clear pricing & profit information
- Game Mechanics - Quick tips untuk new players
- Loading States - Disabled buttons during transactions
Section D: Statistics Sidebar (30 menit)â
Step 7: Create StatsSidebar Componentâ
Sidebar dengan garden statistics, wallet info, dan game mechanics information.
Create: components/stats-sidebar.tsx
"use client"
import { Card } from "@/components/ui/card"
import { Leaf, Sparkles, Coins, Skull, Droplets, Clock } from "lucide-react"
import { usePlants } from "@/hooks/usePlants"
import { useContract } from "@/hooks/useContract"
import { GrowthStage } from "@/types/contracts"
import {
PLANT_PRICE,
HARVEST_REWARD,
STAGE_DURATION,
WATER_DEPLETION_TIME,
WATER_DEPLETION_RATE
} from "@/types/contracts"
interface StatsSidebarProps {
selectedPlantId: bigint | null
}
export default function StatsSidebar({ selectedPlantId }: StatsSidebarProps) {
const { plants } = usePlants()
const { isConnected, address } = useContract()
const bloomingPlants = plants.filter((p) => p.stage === GrowthStage.BLOOMING && !p.isDead).length
const growingPlants = plants.filter((p) => p.stage !== GrowthStage.BLOOMING && !p.isDead).length
const deadPlants = plants.filter((p) => p.isDead).length
const alivePlants = plants.filter((p) => !p.isDead).length
return (
<div className="space-y-4 sticky top-24">
{/* Garden Stats */}
<Card className="p-4 bg-gradient-to-br from-card to-card/50 border border-border animate-slide-in-up hover:shadow-lg transition-all duration-300 ease-out">
<h3 className="font-semibold text-foreground mb-4 flex items-center gap-2">
<Leaf className="w-5 h-5 text-primary" />
Garden Stats
</h3>
{isConnected ? (
<div className="space-y-3">
<div className="flex items-center justify-between p-2 rounded bg-muted/50 hover:bg-muted transition-all duration-300 ease-out">
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<Leaf className="w-4 h-4 text-primary" />
Total Plants
</span>
<span className="font-semibold text-foreground">{plants.length}</span>
</div>
<div className="flex items-center justify-between p-2 rounded bg-muted/50 hover:bg-muted transition-all duration-300 ease-out">
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<Leaf className="w-4 h-4 text-green-500" />
Alive
</span>
<span className="font-semibold text-foreground">{alivePlants}</span>
</div>
<div className="flex items-center justify-between p-2 rounded bg-muted/50 hover:bg-muted transition-all duration-300 ease-out">
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<Sparkles className="w-4 h-4 text-yellow-500" />
Blooming
</span>
<span className="font-semibold text-foreground">{bloomingPlants}</span>
</div>
<div className="flex items-center justify-between p-2 rounded bg-muted/50 hover:bg-muted transition-all duration-300 ease-out">
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<Leaf className="w-4 h-4 text-emerald-500" />
Growing
</span>
<span className="font-semibold text-foreground">{growingPlants}</span>
</div>
<div className="flex items-center justify-between p-2 rounded bg-muted/50 hover:bg-muted transition-all duration-300 ease-out">
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<Skull className="w-4 h-4 text-gray-500" />
Dead
</span>
<span className="font-semibold text-foreground">{deadPlants}</span>
</div>
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-4">Connect wallet to view stats</p>
)}
</Card>
{/* Wallet Info */}
{isConnected && (
<Card
className="p-4 border border-border animate-slide-in-up transition-all duration-300 ease-out"
style={{ animationDelay: "0.1s" }}
>
<h3 className="font-semibold text-foreground mb-3 flex items-center gap-2">
<Coins className="w-5 h-5 text-accent" />
Wallet
</h3>
<div className="space-y-2">
<div className="p-2 rounded bg-muted/30">
<p className="text-xs text-muted-foreground mb-1">Address</p>
<p className="text-xs font-mono text-foreground truncate">{address}</p>
</div>
</div>
</Card>
)}
{/* Quick Info */}
<Card
className="p-4 border border-border animate-slide-in-up transition-all duration-300 ease-out"
style={{ animationDelay: "0.2s" }}
>
<h3 className="font-semibold text-foreground mb-3 flex items-center gap-2">
<Sparkles className="w-5 h-5 text-primary" />
Game Info
</h3>
<div className="space-y-3 text-sm">
<div className="p-3 rounded bg-primary/10 border border-primary/20">
<p className="text-xs text-muted-foreground mb-1">Plant Cost</p>
<p className="font-semibold text-foreground">{PLANT_PRICE} ETH</p>
</div>
<div className="p-3 rounded bg-green-500/10 border border-green-500/20">
<p className="text-xs text-muted-foreground mb-1">Harvest Reward</p>
<p className="font-semibold text-foreground">{HARVEST_REWARD} ETH</p>
</div>
<div className="p-3 rounded bg-blue-500/10 border border-blue-500/20">
<p className="text-xs text-muted-foreground mb-1">Watering Cost</p>
<p className="font-semibold text-primary">FREE (gas only)</p>
</div>
</div>
</Card>
{/* How to Play */}
<Card
className="p-4 border border-border animate-slide-in-up transition-all duration-300 ease-out"
style={{ animationDelay: "0.3s" }}
>
<h3 className="font-semibold text-foreground mb-3">How to Play</h3>
<div className="space-y-2 text-xs text-muted-foreground">
<p>1. Plant a seed (costs {PLANT_PRICE} ETH)</p>
<p>2. Water it regularly (FREE!)</p>
<p>3. Wait 3 minutes for full growth</p>
<p>4. Harvest for {HARVEST_REWARD} ETH reward</p>
<p className="text-primary font-semibold pt-2">ð° 3x profit on every harvest!</p>
<p className="text-red-500 font-semibold pt-2">â ïļ Keep water above 0% or plant dies!</p>
</div>
</Card>
{/* Game Mechanics */}
<Card
className="p-4 border border-border animate-slide-in-up transition-all duration-300 ease-out"
style={{ animationDelay: "0.4s" }}
>
<h3 className="font-semibold text-foreground mb-3 flex items-center gap-2">
<Clock className="w-5 h-5 text-primary" />
Growth & Water
</h3>
<div className="space-y-3 text-sm">
<div className="p-3 rounded bg-blue-500/10 border border-blue-500/20">
<p className="text-xs text-muted-foreground mb-1">Stage Duration</p>
<p className="font-semibold text-foreground">{STAGE_DURATION} seconds each</p>
</div>
<div className="p-3 rounded bg-red-500/10 border border-red-500/20">
<p className="text-xs text-muted-foreground mb-1">Water Depletion</p>
<p className="font-semibold text-foreground">{WATER_DEPLETION_RATE}% every {WATER_DEPLETION_TIME}s</p>
</div>
</div>
</Card>
</div>
)
}
ðĄ Features:
- Garden Statistics - Real-time count untuk plants by status
- Wallet Information - Display connected wallet address
- Game Economics - Plant cost, harvest reward, watering cost
- How to Play - Step-by-step game instructions
- Game Mechanics - Growth & water depletion timing info
- Staggered Animations - Cards animate dengan delay untuk smooth effect
Section E: Main Page (20 menit)â
Step 8: Create Main Page (app/page.tsx)â
Main application page yang menggabungkan semua components.
Update: app/page.tsx
"use client"
import { useState } from "react"
import GardenHeader from "@/components/garden-header"
import GardenGrid from "@/components/garden-grid"
import StatsSidebar from "@/components/stats-sidebar"
import PlantDetailsModal from "@/components/plant-details-modal"
import PlantSeedModal from "@/components/plant-seed-modal"
import { usePlants } from "@/hooks/usePlants"
import { usePlantStageScheduler } from "@/hooks/usePlantStageScheduler"
export default function Home() {
const [selectedPlantId, setSelectedPlantId] = useState<bigint | null>(null)
const [showPlantSeedModal, setShowPlantSeedModal] = useState(false)
const { plants } = usePlants()
// Start background scheduler for automatic stage updates
const { isRunning } = usePlantStageScheduler()
const selectedPlant = plants.find((p) => p.id === selectedPlantId) || null
return (
<div className="min-h-screen bg-background">
<GardenHeader schedulerRunning={isRunning} />
<div className="flex gap-6 p-6 max-w-7xl mx-auto">
<main className="flex-1">
<GardenGrid onSelectPlant={setSelectedPlantId} onPlantSeed={() => setShowPlantSeedModal(true)} />
</main>
<aside className="w-80">
<StatsSidebar selectedPlantId={selectedPlantId} />
</aside>
</div>
{/* Modals */}
<PlantDetailsModal
plant={selectedPlant}
isOpen={!!selectedPlantId}
onClose={() => setSelectedPlantId(null)}
/>
<PlantSeedModal isOpen={showPlantSeedModal} onClose={() => setShowPlantSeedModal(false)} />
</div>
)
}
ðĄ Penjelasan:
- State Management - Local state untuk selected plant & modal visibility
- usePlantStageScheduler - Auto-scheduler running di background
- Layout - Flex layout dengan main content (flex-1) & sidebar (fixed width)
- Modal Control - PlantDetailsModal & PlantSeedModal dengan controlled state
- Plant Selection - Find selected plant dari plants array
ð Testing Complete Applicationâ
Sekarang semua components sudah selesai! Mari test aplikasi:
1. Start Development Serverâ
yarn dev
2. Test Flow Lengkapâ
A. Wallet Connection:
- Buka http://localhost:3000
- Click "Login" button di header
- Connect wallet (MetaMask atau provider lainnya)
- Pastikan network: Lisk Sepolia (Chain ID 4202)
B. Plant First Seed:
- Click "Plant Seed" button
- Modal akan muncul dengan seed preview
- Click "Plant Seed" untuk confirm
- Approve transaction di wallet (bayar 0.001 ETH)
- Wait transaction confirm
- Plant card baru akan muncul di grid!
C. Watch Plant Grow:
- Plant dimulai sebagai ðą Seed
- Background scheduler akan auto-update stage setiap 60 detik
- Stage progression: Seed â Sprout â Growing â Blooming
- Total growth time: 3 menit
D. Water Your Plant:
- Click plant card untuk open details modal
- Lihat water level berkurang setiap 30 detik (-20%)
- Click "Water Plant" button (GRATIS via Panna SDK!)
- Water level akan kembali ke 100%
- Modal akan close otomatis setelah success
E. Harvest Rewards:
- Wait hingga plant mencapai ðļ Blooming stage
- Click plant card
- Modal akan show "Ready to Harvest!"
- Click "Harvest Plant" button
- Receive 0.003 ETH reward (3x profit!)
F. Monitor Statistics:
- Sidebar shows real-time stats:
- Total plants
- Alive/dead count
- Blooming plants
- Game mechanics info
ð Component Architecture Reviewâ
Component Hierarchy:
app/page.tsx (Main)
âââ GardenHeader
â âââ LoginButton (dari Panna SDK)
âââ GardenGrid
â âââ PlantCard (map dari plants array)
âââ StatsSidebar
â âââ Garden Stats
â âââ Wallet Info
â âââ Game Info
â âââ How to Play
â âââ Game Mechanics
âââ PlantDetailsModal (conditional)
â âââ Plant Visualization
â âââ Growth Progress
â âââ Water Level
â âââ Stage Sync Warning
â âââ Action Buttons (Water/Harvest)
âââ PlantSeedModal (conditional)
âââ Seed Preview
âââ Growth Stages
âââ Cost Display
âââ Game Tips
State Management:
// Global State (via hooks)
- usePlants() â plants, loading, plantSeed(), waterPlant(), harvestPlant()
- useContract() â client, account, isConnected, address
- usePlantStageScheduler() â isRunning
// Local State (page.tsx)
- selectedPlantId â untuk PlantDetailsModal
- showPlantSeedModal â untuk PlantSeedModal control
Data Flow:
1. User connects wallet â Panna SDK (automatic gasless setup!)
2. usePlants hook fetches data â blockchain via Thirdweb
3. Auto-refresh setiap 5 detik â real-time updates
4. Background scheduler runs setiap 60 detik â auto-update stages
5. User actions (water/harvest) â gasless transactions via Panna
6. UI updates automatically â React state + hooks
â Checklist Completionâ
All UI Components Complete:
- â GardenHeader dengan Panna SDK LoginButton
- â usePlantStageScheduler untuk auto-sync
- â PlantCard dengan real-time water calculation
- â GardenGrid dengan loading, empty, dan data states
- â PlantDetailsModal dengan visualization & actions
- â PlantSeedModal untuk planting
- â StatsSidebar dengan complete game info
- â Main page.tsx yang menggabungkan semua
All Features Working:
- â Wallet connection (gasless!)
- â Plant seeds (payable transaction)
- â Water plants (free gasless transaction)
- â Harvest rewards (receive ETH)
- â Auto-refresh data setiap 5 detik
- â Auto-update stages setiap 60 detik
- â Real-time water depletion calculation
- â Stage sync detection & manual update
- â Dark/light mode support
- â Responsive layout
- â Loading states & error handling
ðŊ Next Stepsâ
Sekarang aplikasi sudah complete! Lanjut ke Part 3 untuk deployment:
Continue: Part 3 - Deployment & Production â
Di Part 3 kita akan:
- Build production-ready application
- Setup environment variables
- Deploy ke Vercel
- Testing production deployment
- Setup custom domain (optional)
Congratulations! ð
Anda sudah berhasil build COMPLETE LiskGarden DApp dengan:
- Modern React 19 & Next.js 15
- Gasless transactions via Panna SDK
- Real-time blockchain interactions
- Beautiful UI dengan animations
- Auto-refresh & auto-sync features
Keep building! ð
â Back: Part 2A | Next: Part 3 - Deployment â
#BuildOnLisk | #EthereumJakarta | #Web3Indonesia