Move tabs

This commit is contained in:
Joey Yakimowich-Payne 2026-01-19 14:04:10 -07:00
commit 3122748bae
No known key found for this signature in database
GPG key ID: DDF6AF5B21B407D4
3 changed files with 118 additions and 68 deletions

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { ArrowLeft, Save, Plus, Play, AlertTriangle } from 'lucide-react'; import { ArrowLeft, Save, Plus, Play, AlertTriangle, List, Settings } from 'lucide-react';
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { Quiz, Question, GameConfig, DEFAULT_GAME_CONFIG } from '../types'; import { Quiz, Question, GameConfig, DEFAULT_GAME_CONFIG } from '../types';
@ -40,6 +40,7 @@ export const QuizEditor: React.FC<QuizEditorProps> = ({
initialQuiz.config || defaultConfig || DEFAULT_GAME_CONFIG initialQuiz.config || defaultConfig || DEFAULT_GAME_CONFIG
); );
const [hasAppliedDefaultConfig, setHasAppliedDefaultConfig] = useState(!!initialQuiz.config); const [hasAppliedDefaultConfig, setHasAppliedDefaultConfig] = useState(!!initialQuiz.config);
const [activeTab, setActiveTab] = useState<'questions' | 'settings'>('questions');
useEffect(() => { useEffect(() => {
if (!hasAppliedDefaultConfig && defaultConfig && defaultConfig !== DEFAULT_GAME_CONFIG) { if (!hasAppliedDefaultConfig && defaultConfig && defaultConfig !== DEFAULT_GAME_CONFIG) {
@ -215,7 +216,38 @@ export const QuizEditor: React.FC<QuizEditorProps> = ({
<div className="absolute right-20 bottom-[-50px] w-24 h-24 bg-white/10 rounded-full"></div> <div className="absolute right-20 bottom-[-50px] w-24 h-24 bg-white/10 rounded-full"></div>
</div> </div>
<div className="flex bg-white border-b border-gray-100">
<button
onClick={() => setActiveTab('questions')}
className={`flex-1 py-3 font-bold text-center border-b-2 transition-all ${
activeTab === 'questions'
? 'border-theme-primary text-theme-primary bg-theme-primary/5'
: 'border-transparent text-gray-400 hover:text-gray-600 hover:bg-gray-50'
}`}
>
<div className="flex items-center justify-center gap-2">
<List size={18} />
<span>Questions</span>
</div>
</button>
<button
onClick={() => setActiveTab('settings')}
className={`flex-1 py-3 font-bold text-center border-b-2 transition-all ${
activeTab === 'settings'
? 'border-theme-primary text-theme-primary bg-theme-primary/5'
: 'border-transparent text-gray-400 hover:text-gray-600 hover:bg-gray-50'
}`}
>
<div className="flex items-center justify-center gap-2">
<Settings size={18} />
<span>Settings</span>
</div>
</button>
</div>
<div className="p-6 space-y-4 flex-1 min-h-0 overflow-y-auto"> <div className="p-6 space-y-4 flex-1 min-h-0 overflow-y-auto">
{activeTab === 'questions' ? (
<>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-lg font-bold text-gray-700">Questions</h2> <h2 className="text-lg font-bold text-gray-700">Questions</h2>
<button <button
@ -259,16 +291,21 @@ export const QuizEditor: React.FC<QuizEditorProps> = ({
<p className="text-sm">Click "Add Question" to get started</p> <p className="text-sm">Click "Add Question" to get started</p>
</div> </div>
)} )}
</div> </>
) : (
<div className="p-6 bg-gray-50 border-t-2 border-gray-100 space-y-4"> <div className="space-y-6">
<h2 className="text-lg font-bold text-gray-700">Game Settings</h2>
<GameConfigPanel <GameConfigPanel
config={config} config={config}
onChange={handleConfigChange} onChange={handleConfigChange}
questionCount={quiz.questions.length} questionCount={quiz.questions.length}
compact compact={false}
/> />
</div>
)}
</div>
<div className="p-6 bg-gray-50 border-t-2 border-gray-100 space-y-4">
<button <button
onClick={handleStartGame} onClick={handleStartGame}
disabled={!canStartGame} disabled={!canStartGame}

View file

@ -280,7 +280,7 @@ describe('QuizEditor - Async Default Config Loading', () => {
defaultConfig: DEFAULT_GAME_CONFIG, defaultConfig: DEFAULT_GAME_CONFIG,
}); });
await user.click(screen.getByText('Game Settings')); await user.click(screen.getByRole('button', { name: /Settings/i }));
await user.click(screen.getByText('Host Participates')); await user.click(screen.getByText('Host Participates'));
rerender( rerender(

View file

@ -48,11 +48,22 @@ describe('QuizEditor - Game Config Integration', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
const findTab = (name: 'Questions' | 'Settings') => {
const tabButtons = screen.getAllByRole('button');
return tabButtons.find(btn => {
const text = btn.textContent || '';
if (name === 'Questions') {
return text.includes('Questions') && !text.includes('Add');
}
return text.includes('Settings');
})!;
};
describe('config initialization', () => { describe('config initialization', () => {
it('uses DEFAULT_GAME_CONFIG when quiz has no config', () => { it('uses DEFAULT_GAME_CONFIG when quiz has no config', () => {
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
expect(screen.getByText('Game Settings')).toBeInTheDocument(); expect(findTab('Settings')).toBeInTheDocument();
}); });
it('uses quiz config when provided', async () => { it('uses quiz config when provided', async () => {
@ -67,7 +78,7 @@ describe('QuizEditor - Game Config Integration', () => {
render(<QuizEditor {...defaultProps} quiz={quizWithConfig} />); render(<QuizEditor {...defaultProps} quiz={quizWithConfig} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
const getCheckbox = (labelText: string) => { const getCheckbox = (labelText: string) => {
const label = screen.getByText(labelText); const label = screen.getByText(labelText);
@ -88,7 +99,7 @@ describe('QuizEditor - Game Config Integration', () => {
render(<QuizEditor {...defaultProps} defaultConfig={customDefault} />); render(<QuizEditor {...defaultProps} defaultConfig={customDefault} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
const getCheckbox = (labelText: string) => { const getCheckbox = (labelText: string) => {
const label = screen.getByText(labelText); const label = screen.getByText(labelText);
@ -116,7 +127,7 @@ describe('QuizEditor - Game Config Integration', () => {
render(<QuizEditor {...defaultProps} quiz={quizWithConfig} defaultConfig={customDefault} />); render(<QuizEditor {...defaultProps} quiz={quizWithConfig} defaultConfig={customDefault} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
const getCheckbox = (labelText: string) => { const getCheckbox = (labelText: string) => {
const label = screen.getByText(labelText); const label = screen.getByText(labelText);
@ -130,31 +141,32 @@ describe('QuizEditor - Game Config Integration', () => {
}); });
describe('config panel interactions', () => { describe('config panel interactions', () => {
it('renders collapsed Game Settings by default (compact mode)', () => { it('shows Questions tab by default', () => {
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
expect(screen.getByText('Game Settings')).toBeInTheDocument(); expect(findTab('Questions')).toBeInTheDocument();
expect(findTab('Settings')).toBeInTheDocument();
expect(screen.queryByText('Shuffle Questions')).not.toBeInTheDocument(); expect(screen.queryByText('Shuffle Questions')).not.toBeInTheDocument();
}); });
it('expands config panel when Game Settings is clicked', async () => { it('shows config panel when Settings tab is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
expect(screen.getByText('Shuffle Questions')).toBeInTheDocument(); expect(screen.getByText('Shuffle Questions')).toBeInTheDocument();
expect(screen.getByText('Host Participates')).toBeInTheDocument(); expect(screen.getByText('Host Participates')).toBeInTheDocument();
}); });
it('collapses config panel when Game Settings is clicked again', async () => { it('switches back to Questions tab when clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
expect(screen.getByText('Shuffle Questions')).toBeInTheDocument(); expect(screen.getByText('Shuffle Questions')).toBeInTheDocument();
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Questions'));
expect(screen.queryByText('Shuffle Questions')).not.toBeInTheDocument(); expect(screen.queryByText('Shuffle Questions')).not.toBeInTheDocument();
}); });
@ -162,7 +174,7 @@ describe('QuizEditor - Game Config Integration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Shuffle Questions'));
expect(mockOnConfigChange).toHaveBeenCalledWith({ expect(mockOnConfigChange).toHaveBeenCalledWith({
@ -175,7 +187,7 @@ describe('QuizEditor - Game Config Integration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Shuffle Questions'));
await user.click(screen.getByText('Shuffle Answers')); await user.click(screen.getByText('Shuffle Answers'));
@ -203,7 +215,7 @@ describe('QuizEditor - Game Config Integration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Shuffle Questions'));
await user.click(screen.getByText(/Start Game/)); await user.click(screen.getByText(/Start Game/));
@ -278,7 +290,7 @@ describe('QuizEditor - Game Config Integration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Shuffle Questions'));
await user.click(screen.getByText('Test Quiz')); await user.click(screen.getByText('Test Quiz'));
@ -303,9 +315,10 @@ describe('QuizEditor - Game Config Integration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
await user.click(screen.getByText('Host Participates')); await user.click(screen.getByText('Host Participates'));
await user.click(findTab('Questions'));
await user.click(screen.getByText('Add Question')); await user.click(screen.getByText('Add Question'));
expect(mockOnConfigChange).toHaveBeenCalledWith( expect(mockOnConfigChange).toHaveBeenCalledWith(
@ -359,7 +372,7 @@ describe('QuizEditor - Game Config Integration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} onConfigChange={undefined} />); render(<QuizEditor {...defaultProps} onConfigChange={undefined} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Shuffle Questions'));
await user.click(screen.getByText(/Start Game/)); await user.click(screen.getByText(/Start Game/));
@ -397,7 +410,7 @@ describe('QuizEditor - Game Config Integration', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<QuizEditor {...defaultProps} />); render(<QuizEditor {...defaultProps} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
await user.click(screen.getByText('Shuffle Questions')); await user.click(screen.getByText('Shuffle Questions'));
expect(mockOnConfigChange).toHaveBeenCalledTimes(1); expect(mockOnConfigChange).toHaveBeenCalledTimes(1);
@ -411,7 +424,7 @@ describe('QuizEditor - Game Config Integration', () => {
render(<QuizEditor {...defaultProps} quiz={quizWithStreak} />); render(<QuizEditor {...defaultProps} quiz={quizWithStreak} />);
await user.click(screen.getByText('Game Settings')); await user.click(findTab('Settings'));
const thresholdInput = screen.getByDisplayValue('3'); const thresholdInput = screen.getByDisplayValue('3');
await user.clear(thresholdInput); await user.clear(thresholdInput);