Add mobile floating banners for specialization status and course selection progress
On mobile, the single-column layout makes it easy to lose context when scrolling between the specializations and course selection panels. This adds two floating banners that appear via IntersectionObserver: - Top banner: summarizes specialization statuses (achieved/achievable/missing/unreachable) - Bottom banner: shows course selection progress (N/12 selected) Both slide in/out with CSS transitions and scroll to their respective sections on tap. Only rendered on mobile viewports (max-width: 639px).
This commit is contained in:
38
app/src/components/MobileCourseBanner.tsx
Normal file
38
app/src/components/MobileCourseBanner.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
interface MobileCourseBannerProps {
|
||||
selectedCount: number;
|
||||
totalSets: number;
|
||||
visible: boolean;
|
||||
onTap: () => void;
|
||||
}
|
||||
|
||||
export function MobileCourseBanner({ selectedCount, totalSets, visible, onTap }: MobileCourseBannerProps) {
|
||||
const bannerStyle: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
zIndex: 1000,
|
||||
background: '#fff',
|
||||
borderTop: '1px solid #ddd',
|
||||
padding: '8px 12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
transform: visible ? 'translateY(0)' : 'translateY(100%)',
|
||||
transition: 'transform 200ms ease-out',
|
||||
cursor: 'pointer',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={bannerStyle} onClick={onTap}>
|
||||
<span style={{ fontSize: '13px', fontWeight: 600, color: '#333' }}>
|
||||
{selectedCount} / {totalSets}
|
||||
</span>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>
|
||||
courses selected
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
app/src/components/MobileStatusBanner.tsx
Normal file
61
app/src/components/MobileStatusBanner.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { STATUS_STYLES } from './SpecializationRanking';
|
||||
import type { SpecStatus } from '../data/types';
|
||||
|
||||
const STATUS_ORDER: SpecStatus[] = ['achieved', 'achievable', 'missing_required', 'unreachable'];
|
||||
|
||||
interface MobileStatusBannerProps {
|
||||
statuses: Record<string, SpecStatus>;
|
||||
visible: boolean;
|
||||
onTap: () => void;
|
||||
}
|
||||
|
||||
export function MobileStatusBanner({ statuses, visible, onTap }: MobileStatusBannerProps) {
|
||||
const counts: Record<SpecStatus, number> = { achieved: 0, achievable: 0, missing_required: 0, unreachable: 0 };
|
||||
for (const status of Object.values(statuses)) {
|
||||
counts[status]++;
|
||||
}
|
||||
|
||||
const bannerStyle: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
zIndex: 1000,
|
||||
background: '#fff',
|
||||
borderBottom: '1px solid #ddd',
|
||||
padding: '8px 12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
flexWrap: 'wrap',
|
||||
transform: visible ? 'translateY(0)' : 'translateY(-100%)',
|
||||
transition: 'transform 200ms ease-out',
|
||||
cursor: 'pointer',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={bannerStyle} onClick={onTap}>
|
||||
{STATUS_ORDER.map((key) => {
|
||||
const style = STATUS_STYLES[key];
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '10px',
|
||||
background: style.color + '20',
|
||||
color: style.color,
|
||||
fontWeight: 600,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{counts[key]} {style.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import { SPECIALIZATIONS } from '../data/specializations';
|
||||
import { courseById } from '../data/lookups';
|
||||
import type { SpecStatus, AllocationResult } from '../data/types';
|
||||
|
||||
const STATUS_STYLES: Record<SpecStatus, { bg: string; color: string; label: string }> = {
|
||||
export const STATUS_STYLES: Record<SpecStatus, { bg: string; color: string; label: string }> = {
|
||||
achieved: { bg: '#dcfce7', color: '#16a34a', label: 'Achieved' },
|
||||
achievable: { bg: '#dbeafe', color: '#2563eb', label: 'Achievable' },
|
||||
missing_required: { bg: '#fef3c7', color: '#d97706', label: 'Missing Req.' },
|
||||
|
||||
Reference in New Issue
Block a user