MBTI Test
Interactive MBTI personality test using individual stage components and StageRenderer for multi-step personality assessment.
Overview
This example demonstrates how to create an interactive MBTI personality test using Stage Flow for managing complex questionnaire flows with progress tracking and bilingual support.
- Individual Stage Components: Each stage is a separate React component
- StageRenderer: Automatic stage rendering based on current stage
- Step-by-Step Questions: Guided progression through MBTI questions
- Bilingual Support: English and Korean language support
- Progress Tracking: Visual progress indicator
- Data Persistence: Maintaining answers across questions
- Back Navigation: Ability to go back and modify previous answers
- Result Calculation: Automatic MBTI type calculation
Key Features to Observe
- Individual Stage Components: Each stage (welcome, questions, results) is a separate component
- StageRenderer Usage: Automatic stage rendering without conditional rendering
- Bilingual Support: Toggle between English and Korean
- Step-by-Step Questions: Guided progression through MBTI questions
- Progress Tracking: Visual progress indicator
- Data Persistence: Maintaining answers across questions
- Back Navigation: Ability to go back and modify previous answers
- Result Calculation: Automatic MBTI type calculation based on answers
Live Example
Live Editor
// import { StageFlowEngine } from '@stage-flow/core'; // import { StageFlowProvider, useStageFlow } from '@stage-flow/react'; function MBTITest() { // Language context const [language, setLanguage] = React.useState('en'); const translations = { en: { welcome: { title: "MBTI Personality Test", subtitle: "Discover your personality type", description: "This test will help you understand your MBTI personality type. Answer each question honestly based on how you typically feel or behave.", startButton: "Start Test", languageToggle: "한국어" }, questions: { backButton: "Back", nextButton: "Next", question: "Question", of: "of" }, results: { title: "Your MBTI Type", subtitle: "Based on your answers", restartButton: "Take Test Again", languageToggle: "한국어" }, mbtiTypes: { "INTJ": { title: "The Architect", description: "Imaginative and strategic thinkers, with a plan for everything." }, "INTP": { title: "The Logician", description: "Innovative inventors with an unquenchable thirst for knowledge." }, "ENTJ": { title: "The Commander", description: "Bold, imaginative and strong-willed leaders." }, "ENTP": { title: "The Debater", description: "Smart and curious thinkers who cannot resist an intellectual challenge." }, "INFJ": { title: "The Advocate", description: "Quiet and mystical, yet very inspiring and tireless idealists." }, "INFP": { title: "The Mediator", description: "Poetic, kind and altruistic people, always eager to help a good cause." }, "ENFJ": { title: "The Protagonist", description: "Charismatic and inspiring leaders, able to mesmerize their listeners." }, "ENFP": { title: "The Campaigner", description: "Enthusiastic, creative and sociable free spirits." }, "ISTJ": { title: "The Logistician", description: "Practical and fact-minded individuals, whose reliability cannot be doubted." }, "ISFJ": { title: "The Defender", description: "Very dedicated and warm protectors, always ready to defend their loved ones." }, "ESTJ": { title: "The Executive", description: "Excellent administrators, unsurpassed at managing things or people." }, "ESFJ": { title: "The Consul", description: "Extraordinarily caring, social and popular people." }, "ISTP": { title: "The Virtuoso", description: "Bold and practical experimenters, masters of all kinds of tools." }, "ISFP": { title: "The Adventurer", description: "Flexible and charming artists, always ready to explore and experience something new." }, "ESTP": { title: "The Entrepreneur", description: "Smart, energetic and very perceptive people." }, "ESFP": { title: "The Entertainer", description: "Spontaneous, energetic and enthusiastic entertainers." } } }, ko: { welcome: { title: "MBTI 성격 유형 검사", subtitle: "당신의 성격 유형을 발견하세요", description: "이 검사는 당신의 MBTI 성격 유형을 이해하는 데 도움이 됩니다. 각 질문에 대해 당신이 일반적으로 느끼거나 행동하는 방식에 따라 솔직하게 답해주세요.", startButton: "검사 시작", languageToggle: "English" }, questions: { backButton: "이전", nextButton: "다음", question: "질문", of: "/" }, results: { title: "당신의 MBTI 유형", subtitle: "답변을 바탕으로", restartButton: "다시 검사하기", languageToggle: "English" }, mbtiTypes: { "INTJ": { title: "건축가", description: "상상력이 풍부하고 전략적인 사고를 하는 사람으로, 모든 것에 대한 계획을 가지고 있습니다." }, "INTP": { title: "논리술사", description: "지식에 대한 끊임없는 갈증을 가진 혁신적인 발명가입니다." }, "ENTJ": { title: "통솔자", description: "대담하고 상상력이 풍부하며 의지가 강한 리더입니다." }, "ENTP": { title: "변론가", description: "지적 도전을 거부할 수 없는 똑똑하고 호기심 많은 사상가입니다." }, "INFJ": { title: "옹호자", description: "조용하고 신비롭지만 매우 영감을 주고 지칠 줄 모르는 이상주의자입니다." }, "INFP": { title: "중재자", description: "시적이고 친절하며 이타적인 사람으로, 좋은 일을 돕고 싶어 합니다." }, "ENFJ": { title: "주인공", description: "카리스마 있고 영감을 주는 리더로, 청중을 매료시킬 수 있습니다." }, "ENFP": { title: "활동가", description: "열정적이고 창의적이며 사교적인 자유로운 영혼입니다." }, "ISTJ": { title: "현실주의자", description: "실용적이고 사실 중심의 개인으로, 그들의 신뢰성은 의심할 수 없습니다." }, "ISFJ": { title: "수호자", description: "매우 헌신적이고 따뜻한 보호자로, 항상 사랑하는 사람들을 보호할 준비가 되어 있습니다." }, "ESTJ": { title: "경영자", description: "뛰어난 관리자로, 사물이나 사람을 관리하는 데 뛰어납니다." }, "ESFJ": { title: "집정관", description: "특별히 돌보고 사교적이며 인기 있는 사람들입니다." }, "ISTP": { title: "만능재주꾼", description: "대담하고 실용적인 실험가로, 모든 종류의 도구의 마스터입니다." }, "ISFP": { title: "모험가", description: "유연하고 매력적인 예술가로, 항상 새로운 것을 탐험하고 경험할 준비가 되어 있습니다." }, "ESTP": { title: "사업가", description: "똑똑하고 활기차며 매우 통찰력 있는 사람들입니다." }, "ESFP": { title: "연예인", description: "자발적이고 활기차며 열정적인 엔터테이너입니다." } } } }; const t = translations[language]; // MBTI Questions const mbtiQuestions = [ { id: 1, question: { en: "When you're in a group, you usually:", ko: "그룹에 있을 때, 당신은 보통:" }, options: [ { value: "E", text: { en: "Talk to many people", ko: "많은 사람들과 대화한다" } }, { value: "I", text: { en: "Talk to a few people", ko: "소수의 사람들과만 대화한다" } } ] }, { id: 2, question: { en: "You're more likely to:", ko: "당신은 더 가능성이 높은 것은:" }, options: [ { value: "E", text: { en: "Go to a party", ko: "파티에 간다" } }, { value: "I", text: { en: "Stay home", ko: "집에 있다" } } ] }, { id: 3, question: { en: "You prefer to:", ko: "당신은 선호하는 것은:" }, options: [ { value: "S", text: { en: "Focus on details", ko: "세부사항에 집중한다" } }, { value: "N", text: { en: "Focus on the big picture", ko: "큰 그림에 집중한다" } } ] }, { id: 4, question: { en: "You're more interested in:", ko: "당신이 더 관심 있는 것은:" }, options: [ { value: "S", text: { en: "What is real", ko: "실제로 존재하는 것" } }, { value: "N", text: { en: "What is possible", ko: "가능한 것" } } ] }, { id: 5, question: { en: "When making decisions, you rely more on:", ko: "결정을 내릴 때, 당신은 더 의존하는 것은:" }, options: [ { value: "T", text: { en: "Logic and facts", ko: "논리와 사실" } }, { value: "F", text: { en: "Feelings and values", ko: "감정과 가치관" } } ] }, { id: 6, question: { en: "You're more likely to:", ko: "당신은 더 가능성이 높은 것은:" }, options: [ { value: "T", text: { en: "Be direct and honest", ko: "직접적이고 솔직하다" } }, { value: "F", text: { en: "Be tactful and diplomatic", ko: "재치 있고 외교적이다" } } ] }, { id: 7, question: { en: "You prefer to:", ko: "당신은 선호하는 것은:" }, options: [ { value: "J", text: { en: "Have a plan", ko: "계획을 세운다" } }, { value: "P", text: { en: "Keep options open", ko: "선택지를 열어둔다" } } ] }, { id: 8, question: { en: "You're more comfortable with:", ko: "당신이 더 편안해하는 것은:" }, options: [ { value: "J", text: { en: "Structure and order", ko: "구조와 질서" } }, { value: "P", text: { en: "Flexibility and spontaneity", ko: "유연성과 즉흥성" } } ] } ]; // Common UI Components function FormContainer({ children, title = "MBTI Test" }) { const { engine } = useStageFlow(); const handleLanguageToggle = React.useCallback(() => { const newLanguage = language === 'en' ? 'ko' : 'en'; setLanguage(newLanguage); engine.setStageData({ ...engine.getCurrentData(), language: newLanguage }); }, [language, engine]); return ( <div style={{ padding: "20px", border: "1px solid #ddd", borderRadius: "8px", maxWidth: "600px", backgroundColor: "white", }} > <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "20px" }}> <h2 style={{ margin: "0", color: "#333" }}>{title}</h2> <button onClick={handleLanguageToggle} style={{ padding: "4px 8px", border: "1px solid #ddd", borderRadius: "4px", backgroundColor: "white", color: "#333", cursor: "pointer", fontSize: "12px" }} > {language === 'en' ? t.welcome.languageToggle : t.welcome.languageToggle} </button> </div> {children} </div> ); } function FormButton({ onClick, children, variant = "primary", type = "button", style = {} }) { const buttonStyles = { primary: { backgroundColor: "#007bff", color: "white", }, secondary: { backgroundColor: "#6c757d", color: "white", }, success: { backgroundColor: "#28a745", color: "white", }, }; const buttonStyle = { padding: "8px 16px", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "14px", color: "#333", ...buttonStyles[variant], ...style, }; return ( <button onClick={onClick} type={type} style={buttonStyle}> {children} </button> ); } function ProgressBar({ currentStep, totalSteps }) { const percentage = (currentStep / totalSteps) * 100; return ( <div style={{ marginBottom: "30px" }}> <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "10px", }} > <span style={{ fontSize: "14px", color: "#666" }}> {t.questions.question} {currentStep} {t.questions.of} {totalSteps} </span> <span style={{ fontSize: "14px", color: "#666" }}>{Math.round(percentage)}%</span> </div> <div style={{ width: "100%", height: "8px", backgroundColor: "#e9ecef", borderRadius: "4px", overflow: "hidden", }} > <div style={{ width: `${percentage}%`, height: "100%", backgroundColor: "#007bff", transition: "width 0.3s ease", }} /> </div> </div> ); } function DebugInfo() { const { currentStage, data } = useStageFlow(); return ( <div style={{ marginBottom: "20px", padding: "15px", backgroundColor: "#f8f9fa", borderRadius: "4px", border: "1px solid #e9ecef", maxWidth: "600px", width: "100%", }} > <div style={{ marginBottom: "10px" }}> <h4 style={{ margin: "0", color: "#495057" }}>Debug Info:</h4> </div> <div> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Stage:</strong> {currentStage} </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Language:</strong> {language} </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Data:</strong> {JSON.stringify(data)} </p> </div> </div> ); } // Welcome stage component function WelcomeStage({ data, send }) { const handleStartTest = React.useCallback(() => { send("startTest", { ...(data || {}), currentQuestion: 1, totalQuestions: mbtiQuestions.length, answers: {}, }); }, [data, send]); return ( <FormContainer title={t.welcome.title}> <div style={{ padding: "20px", backgroundColor: "#f8f9fa", borderRadius: "4px", minHeight: "300px", textAlign: "center", }} > <h3 style={{ margin: "0 0 20px 0", color: "#333" }}>{t.welcome.subtitle}</h3> <p style={{ color: "#666", marginBottom: "30px", lineHeight: "1.6" }}> {t.welcome.description} </p> <FormButton onClick={handleStartTest} style={{ fontSize: "16px", padding: "12px 24px" }}> {t.welcome.startButton} </FormButton> </div> </FormContainer> ); } // Questions stage component function QuestionsStage({ data, send }) { const { engine } = useStageFlow(); const currentQuestion = mbtiQuestions[data?.currentQuestion - 1]; const currentAnswer = data?.answers?.[currentQuestion?.id]; const handleBack = React.useCallback(() => { const prevQuestion = data?.currentQuestion - 1; if (prevQuestion >= 1) { send("back", { ...(data || {}), currentQuestion: prevQuestion, }); } else { send("backToWelcome", { ...(data || {}), currentQuestion: 1, }); } }, [data, send]); const handleAnswer = React.useCallback((answer) => { const newAnswers = { ...(data?.answers || {}), [currentQuestion.id]: answer }; const nextQuestion = data?.currentQuestion + 1; if (nextQuestion <= mbtiQuestions.length) { engine.setStageData({ ...(data || {}), answers: newAnswers, currentQuestion: nextQuestion, }); } else { send("completeTest", { ...(data || {}), answers: newAnswers, currentQuestion: mbtiQuestions.length, }); } }, [data, send, currentQuestion, engine]); if (!currentQuestion) { return <div>Loading...</div>; } return ( <FormContainer title={t.welcome.title}> <ProgressBar currentStep={data?.currentQuestion || 1} totalSteps={mbtiQuestions.length} /> <div style={{ padding: "20px", backgroundColor: "#f8f9fa", borderRadius: "4px", minHeight: "300px", }} > <h3 style={{ margin: "0 0 20px 0", color: "#333" }}> {t.questions.question} {data?.currentQuestion} {t.questions.of} {mbtiQuestions.length} </h3> <p style={{ color: "#333", marginBottom: "30px", fontSize: "16px", lineHeight: "1.5" }}> {currentQuestion.question[language]} </p> <div style={{ display: "flex", flexDirection: "column", gap: "15px", marginBottom: "30px" }}> {currentQuestion.options.map((option, index) => ( <button key={index} onClick={() => handleAnswer(option.value)} style={{ padding: "15px", border: currentAnswer === option.value ? "2px solid #007bff" : "1px solid #ddd", borderRadius: "8px", backgroundColor: currentAnswer === option.value ? "#e3f2fd" : "white", color: "#333", cursor: "pointer", textAlign: "left", fontSize: "14px", transition: "all 0.2s ease", }} > {option.text[language]} </button> ))} </div> <div style={{ display: "flex", gap: "10px" }}> <FormButton onClick={handleBack} variant="secondary"> {t.questions.backButton} </FormButton> </div> </div> </FormContainer> ); } // Results stage component function ResultsStage({ data, send }) { const calculateMBTI = (answers) => { let E = 0, I = 0, S = 0, N = 0, T = 0, F = 0, J = 0, P = 0; Object.values(answers).forEach(answer => { if (answer === 'E') E++; if (answer === 'I') I++; if (answer === 'S') S++; if (answer === 'N') N++; if (answer === 'T') T++; if (answer === 'F') F++; if (answer === 'J') J++; if (answer === 'P') P++; }); const firstLetter = E > I ? 'E' : 'I'; const secondLetter = S > N ? 'S' : 'N'; const thirdLetter = T > F ? 'T' : 'F'; const fourthLetter = J > P ? 'J' : 'P'; return firstLetter + secondLetter + thirdLetter + fourthLetter; }; const mbtiType = calculateMBTI(data?.answers || {}); const mbtiInfo = t.mbtiTypes[mbtiType] || { title: "Unknown", description: "Type not found" }; const handleRestart = React.useCallback(() => { send("restart", { ...(data || {}), currentQuestion: 1, answers: {}, }); }, [data, send]); return ( <FormContainer title={t.results.title}> <div style={{ padding: "20px", backgroundColor: "#f8f9fa", borderRadius: "4px", minHeight: "300px", textAlign: "center", }} > <h3 style={{ margin: "0 0 20px 0", color: "#333" }}>{t.results.subtitle}</h3> <div style={{ marginBottom: "30px" }}> <div style={{ fontSize: "48px", fontWeight: "bold", color: "#007bff", marginBottom: "10px" }}> {mbtiType} </div> <div style={{ fontSize: "20px", color: "#333", marginBottom: "10px" }}> {mbtiInfo.title} </div> <p style={{ color: "#666", lineHeight: "1.6", maxWidth: "400px", margin: "0 auto" }}> {mbtiInfo.description} </p> </div> <div style={{ marginBottom: "20px" }}> <h4 style={{ color: "#333", marginBottom: "10px" }}> {language === 'en' ? "Your Answers:" : "당신의 답변:"} </h4> <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "10px", textAlign: "left" }}> {Object.entries(data?.answers || {}).map(([questionId, answer]) => { const question = mbtiQuestions.find(q => q.id === parseInt(questionId)); return ( <div key={questionId} style={{ padding: "10px", backgroundColor: "white", borderRadius: "4px", border: "1px solid #ddd" }}> <div style={{ fontSize: "12px", color: "#666", marginBottom: "5px" }}> {language === 'en' ? `Q${questionId}` : `질문${questionId}`} </div> <div style={{ fontSize: "14px", color: "#333" }}> {answer} </div> </div> ); })} </div> </div> <FormButton onClick={handleRestart} style={{ fontSize: "16px", padding: "12px 24px" }}> {t.results.restartButton} </FormButton> </div> </FormContainer> ); } // Create engine directly without useRef const engine = new StageFlowEngine({ initial: "welcome", data: { currentQuestion: 1, totalQuestions: mbtiQuestions.length, answers: {}, language: 'en' }, stages: [ { name: "welcome", transitions: [ { target: "questions", event: "startTest" }, ], }, { name: "questions", transitions: [ { target: "questions", event: "nextQuestion" }, { target: "results", event: "completeTest" }, { target: "welcome", event: "backToWelcome" }, { target: "questions", event: "back" }, ], }, { name: "results", transitions: [ { target: "welcome", event: "restart" }, ], }, ], }); engine.start(); return ( <StageFlowProvider engine={engine}> <DebugInfo /> <StageRenderer stageComponents={{ welcome: WelcomeStage, questions: QuestionsStage, results: ResultsStage, }} /> </StageFlowProvider> ); }
Result
Loading...