Files
bscscore/src/components/new-game/Player1Step.tsx
Frank Schwenk 99be99d120 Make game creation wizard fit viewport without scrolling
Replace scrollable form content with responsive sizing that
automatically scales elements to fit available viewport height.

CSS improvements:
- Disable scrolling: overflow-y:auto → overflow:hidden in form-content
- Implement fluid typography with clamp() for titles, labels, buttons
- Add responsive spacing using clamp() for margins and padding
- Scale progress dots from 10px-16px based on viewport height
- Reduce button dimensions (60px min-width, 36px min-height)
- Enable element shrinking with flex-shrink:1 and min-height:0

Component cleanup:
- Remove auto-focus useEffect from Player1/2/3Step components
- Prevents unwanted layout shifts on wizard mount

Benefits:
- All elements visible without scrolling
- Responsive design scales smoothly across viewport sizes
- Cleaner UX with no scrollbars in form wizard
- Better space utilization on small screens
2025-11-07 14:30:39 +01:00

257 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { h } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import styles from '../NewGame.module.css';
import { UI_CONSTANTS, ERROR_MESSAGES, ARIA_LABELS, FORM_CONFIG, ERROR_STYLES } from '../../utils/constants';
import { PlayerSelectModal } from './PlayerSelectModal';
interface PlayerStepProps {
playerNameHistory: string[];
onNext: (name: string) => void;
onCancel: () => void;
initialValue?: string;
}
export const Player1Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }: PlayerStepProps) => {
const [player1, setPlayer1] = useState(initialValue);
const [error, setError] = useState<string | null>(null);
const [filteredNames, setFilteredNames] = useState(playerNameHistory);
const [isModalOpen, setIsModalOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!player1) {
setFilteredNames(playerNameHistory);
} else {
setFilteredNames(
playerNameHistory.filter(name =>
name.toLowerCase().includes(player1.toLowerCase())
)
);
}
}, [player1, playerNameHistory]);
const handleSubmit = (e: Event) => {
e.preventDefault();
const trimmedName = player1.trim();
if (!trimmedName) {
setError(ERROR_MESSAGES.PLAYER1_REQUIRED);
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.setAttribute('aria-invalid', 'true');
}
return;
}
if (trimmedName.length > FORM_CONFIG.MAX_PLAYER_NAME_LENGTH) {
setError(`Spielername darf maximal ${FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen lang sein`);
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.setAttribute('aria-invalid', 'true');
}
return;
}
setError(null);
if (inputRef.current) {
inputRef.current.setAttribute('aria-invalid', 'false');
}
onNext(trimmedName);
};
const handleQuickPick = (name: string) => {
setError(null);
onNext(name);
};
const handleModalSelect = (name: string) => {
setIsModalOpen(false);
handleQuickPick(name);
};
const handleClear = () => {
setPlayer1('');
setError(null);
if (inputRef.current) inputRef.current.focus();
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 1 Eingabe" autoComplete="off">
<div className={styles['form-header']}>
<div className={styles['screen-title']}>Name Spieler 1</div>
<div className={styles['progress-indicator']} style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM_MEDIUM }}>
<span className={styles['progress-dot'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div>
</div>
<div className={styles['form-content']}>
<div className={styles['player-input'] + ' ' + styles['player1-input']} style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM_LARGE, position: 'relative' }}>
<label htmlFor="player1-input" style={{ fontSize: UI_CONSTANTS.LABEL_FONT_SIZE, fontWeight: 600 }}>Spieler 1</label>
<div style={{ position: 'relative', width: '100%' }}>
<input
id="player1-input"
className={styles['name-input']}
placeholder="Name Spieler 1"
value={player1}
onInput={(e: Event) => {
const target = e.target as HTMLInputElement;
const value = target.value;
setPlayer1(value);
if (value.length > FORM_CONFIG.MAX_PLAYER_NAME_LENGTH) {
setError(`Spielername darf maximal ${FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen lang sein`);
target.setAttribute('aria-invalid', 'true');
} else if (value.trim() && error) {
setError(null);
target.setAttribute('aria-invalid', 'false');
}
}}
autoComplete="off"
aria-label="Name Spieler 1"
aria-describedby="player1-help"
style={{
fontSize: UI_CONSTANTS.INPUT_FONT_SIZE,
minHeight: UI_CONSTANTS.INPUT_MIN_HEIGHT,
marginTop: 12,
marginBottom: 12,
width: '100%',
paddingRight: UI_CONSTANTS.INPUT_PADDING_RIGHT
}}
ref={inputRef}
/>
<div id="player1-help" className="sr-only">
Geben Sie den Namen für Spieler 1 ein. Maximal {FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen erlaubt.
</div>
{player1.length > FORM_CONFIG.CHARACTER_COUNT_WARNING_THRESHOLD && (
<div style={{
fontSize: '0.875rem',
color: player1.length > FORM_CONFIG.MAX_PLAYER_NAME_LENGTH ? '#f44336' : '#ff9800',
marginTop: '4px',
textAlign: 'right'
}}>
{player1.length}/{FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen
</div>
)}
{player1 && (
<button
type="button"
className={styles['clear-input-btn']}
aria-label="Feld leeren"
onClick={handleClear}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: 24,
color: '#aaa',
padding: 0,
zIndex: 2
}}
tabIndex={0}
>
×
</button>
)}
</div>
{filteredNames.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, marginTop: 8 }}>
{filteredNames.slice(0, UI_CONSTANTS.MAX_QUICK_PICKS).map((name, idx) => (
<button
type="button"
key={name + idx}
className={styles['quick-pick-btn']}
style={{
fontSize: UI_CONSTANTS.QUICK_PICK_FONT_SIZE,
padding: UI_CONSTANTS.QUICK_PICK_PADDING,
borderRadius: 8,
background: '#333',
color: '#fff',
border: 'none',
cursor: 'pointer'
}}
onClick={() => handleQuickPick(name)}
aria-label={ARIA_LABELS.QUICK_PICK(name)}
>
{name}
</button>
))}
{playerNameHistory.length > UI_CONSTANTS.MAX_QUICK_PICKS && (
<button
type="button"
className={styles['quick-pick-btn']}
style={{
fontSize: UI_CONSTANTS.QUICK_PICK_FONT_SIZE,
padding: UI_CONSTANTS.QUICK_PICK_PADDING,
borderRadius: 8,
background: '#333',
color: '#fff',
border: 'none',
cursor: 'pointer'
}}
onClick={() => setIsModalOpen(true)}
aria-label={ARIA_LABELS.SHOW_MORE_PLAYERS}
>
...
</button>
)}
</div>
)}
</div>
{error && (
<div
className={styles['validation-error']}
style={{
marginBottom: 16,
...ERROR_STYLES.CONTAINER
}}
role="alert"
aria-live="polite"
>
<span style={ERROR_STYLES.ICON}></span>
{error}
</div>
)}
</div>
<div className={styles['form-footer']}>
<div className={styles['arrow-nav']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<button
type="button"
className={styles['arrow-btn']}
aria-label="Zurück"
onClick={onCancel}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer' }}
>
&#8592;
</button>
<button
type="submit"
className={styles['arrow-btn']}
aria-label="Weiter"
disabled={!player1.trim()}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer', opacity: !player1.trim() ? 0.5 : 1 }}
>
&#8594;
</button>
</div>
</div>
{isModalOpen && (
<PlayerSelectModal
players={playerNameHistory}
onSelect={handleModalSelect}
onClose={() => setIsModalOpen(false)}
/>
)}
</form>
);
};