spaceui
blockblock-interactive-fun-factssource: ok · hit
Interactive 3D Fun Facts Game
Interactive 3D stacked deck quiz game block with True/False feedback, live score and dialog summary.
npx shadcn add "https://www.spaceui.one/r/block-interactive-fun-facts.json"free
Visual style
3dplayful
Interaction
modal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@tabler/icons-react
Registry dependencies
https://www.spaceui.one/r/lib-utils.jsonhttps://www.spaceui.one/r/components-spaceui-badge-squircle.jsonhttps://www.spaceui.one/r/components-spaceui-button-squircle.jsonhttps://www.spaceui.one/r/primitives-card.jsonhttps://www.spaceui.one/r/primitives-dialog.jsonhttps://www.spaceui.one/r/primitives-toast.json
Files4 · source cached 10/9/2026, 8:47:41 AM
src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/index.tsx
'use client'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { toastManager } from '@/components/ui/toast'
import { Button } from '@/components/spaceui/button-squircle'
import { Badge } from '@/components/spaceui/badge-squircle'
import { questionsLocale, type Question, type GameItem } from './data'
import { GuessButton, QuestionCard } from './question-card'
import { AllFactsModal, CustomAlert } from './modals'
export function SectionHeader({
title,
description,
link,
badge,
layoutStart,
isPage,
}: {
title?: string
description?: string
link?: string
badge?: string
layoutStart?: boolean
isPage?: boolean
}) {
return (
<div
className={cn(
'max-w-2xl flex flex-col items-start justify-start gap-4 mb-12',
!layoutStart && ' mx-auto items-center justify-center text-center',
)}
>
{badge && <Badge size="md">{badge}</Badge>}
{title && (
<h2 className={cn(layoutStart ? 'h2 md:h3 font-normals' : 'capitalize', isPage && 'font-normal')}>{title}</h2>
)}
{description && (
<p
className={cn('text-xl md:text-2xl text-muted-foreground whitespace-pre-line', !layoutStart && 'font-medium')}
>
{description}
</p>
)}
</div>
)
}
export function SectionLayout({
id,
title,
description,
className,
contentClassName,
link,
badge,
children,
isFlex,
layoutStart,
isPage,
}: {
id?: string
title?: string
description?: string
className?: string
contentClassName?: string
link?: string
badge?: string
children: React.ReactNode
isFlex?: boolean
layoutStart?: boolean
isPage?: boolean
}) {
return (
<section className={cn('relative w-full py-14 md:py-26 px-2 md:px-8', className)} id={id}>
{(title || badge) && (
<SectionHeader
title={title}
description={description}
link={link}
badge={badge}
layoutStart={layoutStart}
isPage={isPage}
/>
)}
<div
className={cn(
'flex flex-col max-w-5xl mx-auto items-center justify-center justify-items-center self-center place-self-center w-full gap-6',
!isFlex && 'md:grid grid-cols-1 md:grid-cols-2 md:max-w-[90%] place-items-center place-self-center',
contentClassName,
)}
>
{children}
</div>
</section>
)
}
// Interactive Fun Facts Component
export function InteractiveFunFacts() {
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
const [guessedFacts, setGuessedFacts] = useState<{ [key: number]: boolean }>({})
const [showAlert, setShowAlert] = useState(false)
const [alertData, setAlertData] = useState<{
isCorrect: boolean
question: Question | null
}>({ isCorrect: false, question: null })
const [score, setScore] = useState(0)
const [gameStarted, setGameStarted] = useState(true)
const [showAllFacts, setShowAllFacts] = useState(false)
// Derive calculations based on guessedFacts
const answeredQuestionsCount = Object.keys(guessedFacts).length
const allQuestionsAnswered = answeredQuestionsCount === questionsLocale.length
// Verify if there is a false in the guessed answers
const hasGuessedFalse = Object.values(guessedFacts).includes(false)
const handleEmojiClick = (isTrue: boolean) => {
const currentQuestion = questionsLocale[currentQuestionIndex]
if (!currentQuestion || guessedFacts[currentQuestion.id] !== undefined) {
return
}
const isCorrect = currentQuestion.isTrue === isTrue
if (isCorrect) {
setScore((prev) => prev + 1)
}
setGuessedFacts((prev) => ({ ...prev, [currentQuestion.id]: isCorrect }))
setAlertData({ isCorrect, question: currentQuestion })
setShowAlert(true)
}
const closeAlert = () => {
setShowAlert(false)
setAlertData({ isCorrect: false, question: null })
// Pass to the next question after closing the alert
if (currentQuestionIndex < questionsLocale.length - 1) {
setTimeout(() => {
setCurrentQuestionIndex((prev) => prev + 1)
}, 300)
}
}
const resetGame = () => {
setCurrentQuestionIndex(0)
setGuessedFacts({})
setGameStarted(true)
setScore(0)
toastManager.add({
type: 'success',
title: 'Game Reset!',
description: 'Ready for another round of facts! 🚀',
})
}
const canAnswer = gameStarted && !showAllFacts && !allQuestionsAnswered
const badge = gameStarted || showAllFacts ? `Score: ${score}/${questionsLocale.length}` : 'Fun Facts'
return (
<SectionLayout
isFlex
badge={badge}
title="Interactive Fun Facts"
description="Can you spot the difference between real milestones and hilarious myths?"
>
{/* Main Game Zone */}
<div className="content-center flex flex-col md:flex-row gap-4 md:gap-6 items-center justify-between w-full">
<div className="flex flex-row gap-6 sm:gap-0 justify-center w-full sm:w-auto order-2 sm:order-1">
<GuessButton isFalse onClick={() => handleEmojiClick(false)} canAnswer={canAnswer} />
<div className="block sm:hidden">
<GuessButton onClick={() => handleEmojiClick(true)} canAnswer={canAnswer} />
</div>
</div>
{/* Cards Zone */}
<div className="grid grid-cols-1 grid-rows-1 place-items-center w-full max-w-xl order-1 sm:order-2 py-8 md:py-14">
{questionsLocale.map((question, index) => (
<QuestionCard
key={question.id}
question={question}
index={index}
currentQuestionIndex={currentQuestionIndex}
totalQuestions={questionsLocale.length}
/>
))}
</div>
<div className="hidden sm:block order-3">
<GuessButton onClick={() => handleEmojiClick(true)} canAnswer={canAnswer} />
</div>
</div>
{showAllFacts && (
<AllFactsModal
isOpen={showAllFacts}
onClose={() => setShowAllFacts(false)}
questions={questionsLocale}
guessedFacts={guessedFacts}
/>
)}
<div className="relative">
{/* Responsive progression indicators */}
<div className="flex gap-2 sm:gap-3 z-10">
{questionsLocale.map((question, index) => {
const isGuessed = guessedFacts[question.id] !== undefined
const isCorrect = isGuessed && guessedFacts[question.id]
return (
<div
key={index}
className={cn(
'size-3 sm:size-4 rounded-full border-2 flex items-center justify-center',
!allQuestionsAnswered && 'hover:scale-110 transition-transform duration-300 ease-in-out',
isGuessed
? isCorrect
? 'bg-green-500 border-green-500'
: 'bg-red-500 border-red-500'
: index === currentQuestionIndex
? 'bg-[#2a2a2a] border-[#2a2a2a]'
: 'bg-muted border-border',
)}
>
{isGuessed && <span className="text-white text-[6px] sm:text-[8px]">{isCorrect ? '✓' : '✗'}</span>}
</div>
)
})}
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 md:gap-4">
{/* Show all facts button */}
<Button
onClick={() => {
setShowAllFacts(true)
}}
size="lg"
className="cursor-pointer"
>
{allQuestionsAnswered
? hasGuessedFalse
? 'See Answers & Context 📜'
: 'View Perfect Summary 🎉'
: showAllFacts
? 'Retry 🚀'
: 'Explore All Facts 📜'}
</Button>
{allQuestionsAnswered && (
<div className="text-center">
<Button onClick={resetGame} variant="secondary" size="lg" className="cursor-pointer">
Play Again 🚀
</Button>
</div>
)}
</div>
{/* Personalized alert */}
<CustomAlert
isVisible={showAlert}
isCorrect={alertData.isCorrect}
question={alertData.question}
onClose={closeAlert}
/>
</SectionLayout>
)
}
export default function Particle() {
return <InteractiveFunFacts />
}
src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/data.ts
export interface GameItem {
id: number
emoji: string
title: string
description: string
subtitle: string
isTrue: boolean
funFact?: string
funnyTruthMessage?: string
funnyLieMessage?: string
}
export type Question = GameItem
export const questionsLocale: Question[] = [
{
id: 1,
emoji: '🐛',
title: 'The first computer bug was an actual insect',
description: 'A physical moth was trapped in a relay of the Harvard Mark II in 1947.',
subtitle: 'Literal debugging.',
isTrue: true,
funFact: 'Grace Hopper taped the moth into the logbook.',
funnyTruthMessage: 'A real moth taped in the logbook!',
funnyLieMessage: 'It was a real moth trapped inside the machine.',
},
{
id: 2,
emoji: '☕',
title: 'JavaScript was created to be Java for the web',
description: 'Netscape rebranded Mocha to JavaScript purely to ride Java’s hype in 1995.',
subtitle: 'Ham is to hamster as Java is to JavaScript.',
isTrue: false,
funFact: 'Brendan Eich built it in 10 days. They share zero code.',
funnyTruthMessage: 'Zero shared code—pure 90s marketing hype!',
funnyLieMessage: 'They share no architecture at all.',
},
{
id: 3,
emoji: '🍓',
title: 'AI models can easily count letters in words',
description: 'LLMs often fail simple letter counts like the "r"s in "strawberry".',
subtitle: 'How many "r"s in strawberry?',
isTrue: false,
funFact: 'Tokenizers process word chunks, not individual letters.',
funnyTruthMessage: 'Tokenizers see chunks, not letters!',
funnyLieMessage: 'Tokenizers process chunks, so letter counting trips them up.',
},
{
id: 4,
emoji: '🎮',
title: 'Nvidia GPUs were originally made only for video games',
description: 'Founded in 1993, Nvidia built chips strictly for 3D PC gaming graphics.',
subtitle: 'From Doom to Deep Learning.',
isTrue: true,
funFact: 'Jensen Huang bet on CUDA in 2006, years before the AI boom.',
funnyTruthMessage: 'Built for gamers long before modern AI.',
funnyLieMessage: '100% true—designed strictly for PC gaming.',
},
{
id: 5,
emoji: '💾',
title: 'A 3.5-inch floppy disk held less than 2 MB',
description: 'The universal physical save icon held a grand total of only 1.44 MB.',
subtitle: 'Less than one smartphone photo.',
isTrue: true,
funFact: 'A modern smartphone photo needs at least 3 floppy disks.',
funnyTruthMessage: '1.44 MB max—not even one modern song!',
funnyLieMessage: '1.44 MB was all you had for entire games.',
},
{
id: 6,
emoji: '🔄',
title: 'Training AI on AI-generated data makes it smarter',
description: 'Feeding models synthetic text from other AIs improves accuracy.',
subtitle: 'The synthetic data paradox.',
isTrue: false,
funFact: 'It causes "Model Collapse", degrading outputs into gibberish.',
funnyTruthMessage: 'It causes Model Collapse—AI needs fresh human data.',
funnyLieMessage: 'Recursive AI data causes Model Collapse.',
},
{
id: 7,
emoji: '🕹️',
title: 'Nintendo was founded in 1889 as a card company',
description: 'Nintendo began in Kyoto over 135 years ago making Hanafuda playing cards.',
subtitle: 'Over 135 years of history.',
isTrue: true,
funFact: 'They made playing cards for 80 years before their first arcade game.',
funnyTruthMessage: 'Started in 1889 with handmade flower cards!',
funnyLieMessage: 'Founded in 1889—long before video games existed.',
},
{
id: 8,
emoji: '🧠',
title: 'The term "Artificial Intelligence" was coined in 1956',
description: 'John McCarthy proposed the term at the historic Dartmouth conference.',
subtitle: 'Decades before the personal computer.',
isTrue: true,
funFact: 'Researchers gathered in 1956 to explore machine thinking.',
funnyTruthMessage: 'Coined at Dartmouth in 1956!',
funnyLieMessage: 'AI research officially started in 1956.',
},
{
id: 9,
emoji: '🌐',
title: 'The first .com domain was registered before the Web existed',
description: 'Symbolics.com was registered in 1985, four years before the World Wide Web.',
subtitle: 'Pre-web internet history.',
isTrue: true,
funFact: 'Tim Berners-Lee invented the web in 1989, years after symbolics.com.',
funnyTruthMessage: 'Registered in 1985—years before the World Wide Web!',
funnyLieMessage: 'Symbolics.com was registered 4 years before the web.',
},
{
id: 10,
emoji: '📄',
title: 'The Transformer paper was rejected twice by NeurIPS',
description: 'Google’s 2017 "Attention Is All You Need" paper was initially turned down.',
subtitle: 'The foundation of ChatGPT.',
isTrue: false,
funFact: 'NeurIPS accepted it on its very first submission in 2017.',
funnyTruthMessage: 'Accepted on the first try with zero rejections!',
funnyLieMessage: 'Accepted on the first try in 2017.',
},
]
src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/question-card.tsx
'use client'
import { cn } from '@/lib/utils'
import { Card, CardPanel } from '@/components/ui/card'
import { Badge } from '@/components/spaceui/badge-squircle'
import type { Question } from './data'
export function GuessButton({
isFalse,
onClick,
canAnswer,
}: {
isFalse?: boolean
onClick: () => void
canAnswer: boolean
}) {
return (
<div
className={cn(
'relative bg-background border-4 border-border content-stretch flex items-center justify-start p-4 md:p-6 rounded-full shrink-0 cursor-pointer transition-all duration-400',
canAnswer
? isFalse
? 'hover:bg-red-100 hover:border-red-500 dark:hover:border-red-400'
: 'hover:bg-green-100 hover:border-green-500 dark:hover:border-green-400'
: 'pointer-events-none cursor-default',
)}
onClick={onClick}
>
<div className="relative shrink-0 size-11 md:size-20">
<span
className={cn(
'size-full flex items-center justify-center text-5xl md:text-7xl object-cover pointer-events-none',
!canAnswer && 'opacity-50',
)}
>
{isFalse ? '🤥' : '😀'}
</span>
{canAnswer && (
<span
className={cn(
'absolute -bottom-2 left-1/2 -translate-x-1/2 text-xs font-bold px-2 py-0.5 rounded-full text-white whitespace-nowrap shadow-xs',
isFalse ? 'bg-red-500' : 'bg-green-500',
)}
>
{isFalse ? 'Lie' : 'Truth'}
</span>
)}
</div>
</div>
)
}
export function QuestionCard({
question,
index,
currentQuestionIndex,
totalQuestions,
}: {
question: Question
index: number
currentQuestionIndex: number
totalQuestions: number
}) {
const diff = index - currentQuestionIndex
let scale = 1
let translateY = 0
let rotate = 0
let opacity = 0
let zIndex = 0
let pointerEvents: 'auto' | 'none' = 'none'
if (diff === 0) {
scale = 1
translateY = 0
rotate = 0
opacity = 1
zIndex = 30
pointerEvents = 'auto'
} else if (diff > 0 && diff <= 3) {
scale = 1 - diff * 0.04
translateY = -diff * 12
rotate = diff === 1 ? -1.5 : diff === 2 ? 1.5 : -0.75
opacity = 1 - diff * 0.22
zIndex = 30 - diff
pointerEvents = 'none'
} else if (diff === -1) {
scale = 0.98
translateY = -36
rotate = -2.5
opacity = 0
zIndex = 35
pointerEvents = 'none'
} else {
scale = diff > 0 ? 0.88 : 0.95
translateY = diff > 0 ? -36 : -48
rotate = 0
opacity = 0
zIndex = 0
pointerEvents = 'none'
}
return (
<Card
className={cn(
'col-start-1 row-start-1 w-full max-w-xl h-full',
'bg-muted rounded-3xl md:rounded-5xl overflow-hidden',
'will-change-transform select-none',
'transition-[transform,opacity] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]',
)}
style={{
zIndex,
pointerEvents,
transformOrigin: 'center center',
transform: `translate3d(0, ${translateY}px, 0) scale(${scale}) rotate(${rotate}deg)`,
opacity,
}}
>
<CardPanel
className={cn(
'flex relative flex-col min-h-60 h-full items-center justify-center p-4 rounded-xl md:rounded-3xl bg-background overflow-hidden',
)}
>
<div className="flex flex-col items-start gap-4 w-full max-w-[90%] py-12 mx-auto">
<Badge
variant="secondary"
squircle={false}
className="aspect-square p-4 rounded-full size-auto! [&>svg]:size-12! [&>img]:size-12!"
>
<span className="text-4xl">{question.emoji}</span>
</Badge>
<h3 className="text-3xl tracking-wide leading-[120%]">{question.title}</h3>
<p className="text-muted-foreground text-xl leading-[140%]">{question.description}</p>
<p className="text-zinc-400 text-lg leading-[120%]">{question.subtitle}</p>
</div>
</CardPanel>
</Card>
)
}
src/registry/blocks/interactive-fun-facts/interactive-fun-facts-1/modals.tsx
'use client'
import { useEffect } from 'react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/spaceui/button-squircle'
import { Badge } from '@/components/spaceui/badge-squircle'
import {
Dialog,
DialogPopup,
DialogTitle,
DialogDescription,
DialogHeader,
DialogFooter,
DialogPanel,
} from '@/components/ui/dialog'
import type { Question } from './data'
import { IconX } from '@tabler/icons-react'
export function CustomAlert({
isVisible,
isCorrect,
question,
onClose,
}: {
isVisible: boolean
isCorrect: boolean
question: Question | null
onClose: () => void
}) {
useEffect(() => {
if (isVisible) {
const timer = setTimeout(() => {
onClose()
}, 3500)
return () => clearTimeout(timer)
}
}, [isVisible, onClose])
return (
<Dialog open={isVisible} onOpenChange={(open) => !open && onClose()}>
<DialogPopup className="max-w-sm px-4 pt-2 flex flex-col" showCloseButton={false}>
<DialogPanel className="p-4 flex flex-col gap-4 items-center text-center">
<Badge
variant="secondary"
square
squircle={false}
className="bg-background p-0! text-[2.625rem]! rounded-2xl size-auto!"
>
{isCorrect ? '🎉' : '😅'}
</Badge>
<div className="flex flex-col gap-1.5 items-center">
<h4
className={cn('text-2xl font-bold tracking-tight', {
'text-green-600 dark:text-green-400': isCorrect,
'text-red-600 dark:text-red-400': !isCorrect,
})}
>
{isCorrect ? 'Nice!' : 'Not quite!'}
</h4>
<DialogDescription className="text-base text-muted-foreground">
{isCorrect ? question?.funnyTruthMessage : question?.funnyLieMessage}
</DialogDescription>
</div>
{question?.funFact && (
<div className="w-full rounded-2xl border-2 border-muted bg-card p-4 text-center">
<Badge variant={isCorrect ? 'success' : 'warning'} size="sm" className="mb-2">
Behind the fact 🥸
</Badge>
<p className="text-sm text-foreground/90 leading-relaxed">{question.funFact}</p>
</div>
)}
</DialogPanel>
<DialogFooter className="mt-2 border-t border-muted" variant="bare">
<Button onClick={onClose} size="lg" className="w-full cursor-pointer">
{isCorrect ? 'Continue 😍' : 'Retry 😩'}
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
)
}
export function AllFactsModal({
isOpen,
onClose,
guessedFacts,
questions,
}: {
isOpen: boolean
onClose: () => void
guessedFacts: { [key: number]: boolean }
questions: Question[]
}) {
const totalGuessed = Object.keys(guessedFacts).length
const totalQuestions = questions.length
const correctGuesses = questions.filter((fact) => guessedFacts[fact.id] !== undefined && guessedFacts[fact.id]).length
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogPopup className="max-w-2xl max-h-[85vh] px-4 pt-2 flex flex-col" showCloseButton={false}>
<DialogHeader className="flex flex-row p-3.5! items-center justify-between border-b border-muted">
<DialogTitle className="text-base font-normal md:font-medium md:text-xl text-muted-foreground">
Game Summary & All Facts
</DialogTitle>
<Button onClick={onClose} size="icon-sm" className="rounded-lg">
<IconX className="size-5" />
</Button>
</DialogHeader>
<DialogPanel className="flex flex-col gap-4 px-2 py-4 flex-1 min-h-0">
{/* Score Section */}
{totalGuessed > 0 && (
<div className="rounded-2xl border-2 border-muted bg-card p-4">
<h5 className="font-bold mb-2">
🎯 Score: {correctGuesses} / {totalQuestions}
</h5>
<p className="text-sm text-muted-foreground">
{correctGuesses === totalQuestions
? 'Perfect score! You know everything.'
: correctGuesses > totalQuestions / 2
? 'Great run! Most facts guessed correctly.'
: 'Good attempt! Play again to improve your score.'}
</p>
</div>
)}
{/* Facts Grid */}
{questions.map((question) => {
const userGuessed = guessedFacts[question.id] !== undefined
const userGuessedCorrect = userGuessed && guessedFacts[question.id]
return (
<div key={question.id} className="rounded-2xl border-2 border-muted bg-card p-4">
<div className="flex items-start gap-3 sm:gap-4">
<Badge variant="secondary" square squircle={false} className="text-2xl! p-3! rounded-xl size-auto!">
{question.emoji}
</Badge>
<div className="flex flex-col flex-1 min-w-0">
<h5 className="font-bold mb-1">{question.title}</h5>
<p className="text-base text-muted-foreground mb-3">{question.description}</p>
{/* Badges */}
<div className="flex gap-2 flex-wrap">
<Badge variant={question.isTrue ? 'success' : 'error'} size="sm">
{question.isTrue ? 'Real Truth' : 'Lie'}
</Badge>
{userGuessed && (
<Badge variant={userGuessedCorrect ? 'info' : 'warning'} size="sm">
{userGuessedCorrect ? 'Guessed Correctly' : 'Incorrect Guess'}
</Badge>
)}
</div>
</div>
</div>
</div>
)
})}
</DialogPanel>
<DialogFooter className="mt-4 border-t border-muted" variant="bare">
<Button onClick={onClose} size="lg" className="w-full cursor-pointer">
Understood 😊
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
)
}