Styling fix and scraper bug fix

This commit is contained in:
2026-06-14 18:39:55 +02:00
parent 6ed2a3ae0c
commit 566595984f
21 changed files with 2150 additions and 119 deletions
@@ -1,4 +1,5 @@
import { ReactNode } from 'react'
import './loading-spinner.css'
interface LoadingSpinnerProps {
message?: string
@@ -7,9 +8,9 @@ interface LoadingSpinnerProps {
export function LoadingSpinner({ message = 'Loading...', ariaLabel = 'Loading' }: LoadingSpinnerProps): ReactNode {
return (
<div role="status" aria-label={ariaLabel} aria-busy={true} data-testid="loading-spinner">
<div data-testid="spinner-icon" role="presentation">⊙</div>
<p data-testid="spinner-message">{message}</p>
<div className="loading-spinner" role="status" aria-label={ariaLabel} aria-busy={true} data-testid="loading-spinner">
<div className="loading-spinner__icon" data-testid="spinner-icon" role="presentation">⊙</div>
<p className="loading-spinner__message" data-testid="spinner-message">{message}</p>
</div>
)
}
@@ -0,0 +1,39 @@
/* Loading Spinner */
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--sp-8);
gap: var(--sp-4);
min-height: 300px;
}
.loading-spinner__icon {
font-size: 48px;
color: var(--c-accent);
animation: spin 2s linear infinite;
display: block;
}
.loading-spinner__message {
font-size: var(--fs-base);
color: var(--c-text-muted);
margin: 0;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Reduce Motion */
@media (prefers-reduced-motion: reduce) {
.loading-spinner__icon {
animation: none;
}
}
@@ -13,18 +13,19 @@ interface OverviewMetricsProps {
export function OverviewMetrics({ metrics, title = 'Overview' }: OverviewMetricsProps): ReactNode {
return (
<section role="region" aria-label={title} data-testid="overview-metrics">
<h2 data-testid="metrics-title">{title}</h2>
<div role="list" data-testid="metrics-list">
<section role="region" aria-label={title} data-testid="overview-metrics" className="overview-metrics">
<h2 data-testid="metrics-title" className="overview-metrics__title">{title}</h2>
<div role="list" data-testid="metrics-list" className="overview-metrics__list">
{metrics.map((metric, index) => (
<div
key={index}
role="listitem"
data-testid={metric.testId || `metric-${index}`}
data-label={metric.label}
className="overview-metrics__item"
>
<span data-testid={`metric-label-${index}`}>{metric.label}:</span>
<span data-testid={`metric-value-${index}`}>{metric.value}</span>
<span data-testid={`metric-label-${index}`} className="overview-metrics__label">{metric.label}:</span>
<span data-testid={`metric-value-${index}`} className="overview-metrics__value">{metric.value}</span>
</div>
))}
</div>
@@ -1,4 +1,5 @@
import { ReactNode } from 'react'
import './footer.css'
export function Footer(): ReactNode {
const currentYear = new Date().getFullYear()
@@ -4,6 +4,7 @@ import { useAuth } from '@/context/AuthContext'
import { useWorkspace } from '@/context/WorkspaceContext'
import { Header, HeaderProps } from './Header'
import { Footer } from './Footer'
import './layout.css'
interface LayoutProps {
children: ReactNode
+13
View File
@@ -0,0 +1,13 @@
footer {
margin-top: var(--sp-8);
padding: var(--sp-5);
border-top: 1px solid var(--c-border);
background-color: var(--c-bg);
text-align: center;
color: var(--c-text-muted);
}
footer p {
margin: 0;
font-size: var(--fs-sm);
}
+63 -33
View File
@@ -1,83 +1,113 @@
.header {
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
padding: 16px 20px;
position: sticky;
top: 0;
z-index: 100;
background-color: var(--c-surface);
border-bottom: 1px solid var(--c-border);
padding: var(--sp-4) var(--sp-5);
box-shadow: var(--shadow-sm);
}
.header__top {
max-width: var(--maxw);
margin: 0 auto var(--sp-4);
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header__title {
font-size: 18px;
font-weight: 600;
color: #333;
font-size: var(--fs-lg);
font-weight: 700;
color: var(--c-text);
}
.header__identity {
display: flex;
align-items: center;
gap: 12px;
gap: var(--sp-4);
}
.header__parent-name {
font-size: 14px;
color: #666;
font-size: var(--fs-sm);
color: var(--c-text-muted);
font-weight: 500;
}
.header__child-select {
padding: 6px 10px;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 14px;
background-color: #fff;
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--c-border);
border-radius: var(--radius);
font-size: var(--fs-sm);
background-color: var(--c-surface);
color: var(--c-text);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
}
.header__child-select:focus {
outline: none;
border-color: var(--c-accent);
box-shadow: 0 0 0 3px rgba(47, 111, 79, 0.1);
}
.header__child-name {
font-size: 14px;
color: #0066cc;
font-size: var(--fs-sm);
color: var(--c-accent);
font-weight: 500;
}
.header__nav {
max-width: var(--maxw);
margin: 0 auto;
display: flex;
gap: 8px;
gap: var(--sp-3);
flex-wrap: wrap;
justify-content: center;
}
.header__nav-link {
padding: 8px 14px;
font-size: 14px;
padding: var(--sp-2) var(--sp-4);
font-size: var(--fs-sm);
text-decoration: none;
color: #333;
border-radius: 4px;
transition: background-color 0.2s;
color: var(--c-text);
border-radius: var(--radius);
border-bottom: 2px solid transparent;
transition: background-color 0.2s, border-color 0.2s;
}
.header__nav-link:hover {
background-color: #e8e8e8;
background-color: var(--c-bg);
}
.header__nav-link:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
.header__nav-link--active {
background-color: #0066cc;
color: #fff;
background-color: var(--c-accent);
color: var(--c-surface);
border-bottom-color: var(--c-accent);
}
.header__logout-button {
padding: 8px 14px;
font-size: 14px;
background-color: #f8f8f8;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: var(--sp-2) var(--sp-4);
font-size: var(--fs-sm);
background-color: transparent;
border: 1px solid var(--c-border);
border-radius: var(--radius);
color: var(--c-text);
cursor: pointer;
transition: background-color 0.2s;
transition: background-color 0.2s, border-color 0.2s;
}
.header__logout-button:hover {
background-color: #e8e8e8;
background-color: var(--c-bg);
border-color: var(--c-accent);
}
.header__logout-button:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
+112
View File
@@ -0,0 +1,112 @@
/* Layout container */
[data-testid='layout'] {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--c-bg);
}
[data-testid='layout-main'] {
flex: 1;
max-width: var(--maxw);
width: 100%;
margin: 0 auto;
padding: var(--sp-5);
}
/* Dashboard & Pages */
.dashboard,
.calendar,
.settings-page,
.tips-page,
.auth-page {
width: 100%;
}
.dashboard__section,
.settings-page__section,
.calendar__section,
.tips-page__section {
background-color: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius);
padding: var(--sp-5);
margin-bottom: var(--sp-5);
box-shadow: var(--shadow-sm);
}
.dashboard__section:last-child,
.settings-page__section:last-child,
.calendar__section:last-child,
.tips-page__section:last-child {
margin-bottom: 0;
}
.dashboard__section h2,
.settings-page__section h2,
.calendar__section h2,
.tips-page__section h2 {
margin-top: 0;
}
.dashboard__error {
padding: var(--sp-4);
background-color: var(--c-danger-weak);
color: var(--c-danger);
border-left: 4px solid var(--c-danger);
border-radius: var(--radius);
margin-bottom: var(--sp-5);
}
.dashboard__break-list {
list-style: none;
margin: 0;
padding: 0;
}
.dashboard__break-item {
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--c-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.dashboard__break-item:last-child {
border-bottom: none;
}
.dashboard__break-item strong {
display: block;
margin-bottom: var(--sp-2);
}
.dashboard__break-item small {
color: var(--c-text-muted);
font-size: var(--fs-sm);
}
/* Status & Freshness Info */
.status-info,
.freshness-info {
font-size: var(--fs-sm);
color: var(--c-text-muted);
margin-top: var(--sp-3);
padding-top: var(--sp-3);
border-top: 1px solid var(--c-border);
}
/* Responsive */
@media (max-width: 640px) {
[data-testid='layout-main'] {
padding: var(--sp-4);
}
.dashboard__section,
.settings-page__section,
.calendar__section,
.tips-page__section {
padding: var(--sp-4);
margin-bottom: var(--sp-4);
}
}
+35 -59
View File
@@ -1,5 +1,6 @@
import { ReactNode } from 'react'
import type { ScraperStatus } from '@/hooks/useScraperFetch'
import './qrcode-modal.css'
interface QRCodeModalProps {
isOpen: boolean
@@ -34,102 +35,77 @@ export function QRCodeModal({
}
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: 'white', borderRadius: 8, padding: '2rem', maxWidth: 500, width: '90%', boxShadow: '0 2px 8px rgba(0,0,0,0.2)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
<h2 style={{ margin: 0, fontSize: '1.2rem' }}>Hämta från Försäkringskassan</h2>
<div className="qrcode-modal-overlay">
<div className="qrcode-modal">
<div className="qrcode-modal__header">
<h2 className="qrcode-modal__title">Hämta från Försäkringskassan</h2>
<button
className="qrcode-modal__close-btn"
onClick={handleClose}
disabled={isRunning}
style={{
background: 'none',
border: 'none',
fontSize: '1.5rem',
cursor: isRunning ? 'not-allowed' : 'pointer',
opacity: isRunning ? 0.5 : 1,
}}
aria-label="Close modal"
>
×
</button>
</div>
{status && (
<div>
<div style={{ marginBottom: '1rem' }}>
<strong>Status:</strong>{' '}
<span style={{ color: status === 'done' ? 'green' : status === 'error' ? 'red' : 'inherit' }}>
<div className="qrcode-modal__status-container">
<div className="qrcode-modal__status-row">
<span className="qrcode-modal__status-label">Status:</span>
<span
className={`qrcode-modal__status-value qrcode-modal__status-value--${
status === 'done' ? 'success' : status === 'error' ? 'error' : 'pending'
}`}
>
{status}
</span>
{message && <span style={{ color: '#888', marginLeft: '0.5rem' }}>— {message}</span>}
{message && <span className="qrcode-modal__status-message">— {message}</span>}
</div>
{status === 'awaiting_scan' && qrData && (
<div style={{ marginBottom: '1rem', textAlign: 'center' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: '0.9rem' }}>Öppna BankID-appen och skanna QR-koden:</p>
<div className="qrcode-modal__qr-section">
<p className="qrcode-modal__qr-instruction">Öppna BankID-appen och skanna QR-koden:</p>
<img
src={`data:image/png;base64,${qrData}`}
alt="BankID QR-kod"
style={{ width: 220, height: 220, imageRendering: 'pixelated', border: '1px solid #ccc' }}
className="qrcode-modal__qr-image"
/>
</div>
)}
{(status === 'authenticating' || status === 'fetching') && (
<p style={{ color: '#555', textAlign: 'center' }}>
<p className="qrcode-modal__loading-message">
⏳ {status === 'authenticating' ? 'Autentiserar…' : 'Hämtar data från FK…'}
</p>
)}
{status === 'done' && result && (
<div>
<p style={{ color: 'green', marginBottom: '0.5rem' }}>✅ Klar! Data hämtad från Försäkringskassan.</p>
</div>
<p className="qrcode-modal__success-message">✅ Klar! Data hämtad från Försäkringskassan.</p>
)}
{status === 'cancelled' && (
<p style={{ color: '#888' }}>Sessionen avbröts.</p>
<p className="qrcode-modal__cancelled-message">Sessionen avbröts.</p>
)}
{status === 'done' && (
<button
onClick={onDone}
style={{
width: '100%',
padding: '0.5rem 1rem',
background: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
marginTop: '1rem',
}}
>
Stäng
</button>
)}
<div className="qrcode-modal__button-group">
{status === 'done' && (
<button className="qrcode-modal__button qrcode-modal__button--success" onClick={onDone}>
Stäng
</button>
)}
{isRunning && (
<button
onClick={onCancel}
style={{
width: '100%',
padding: '0.5rem 1rem',
background: '#f44336',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
marginTop: '1rem',
}}
>
Avbryt
</button>
)}
{isRunning && (
<button className="qrcode-modal__button qrcode-modal__button--danger" onClick={onCancel}>
Avbryt
</button>
)}
</div>
</div>
)}
{error && (
<div style={{ background: '#fee', border: '1px solid #f88', padding: '0.75rem', borderRadius: 4, color: '#c00' }}>
<div className="qrcode-modal__error">
<strong>Fel:</strong> {error}
</div>
)}
@@ -0,0 +1,226 @@
/* QRCode Modal Container */
.qrcode-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(16, 24, 40, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.qrcode-modal {
background-color: var(--c-surface);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
max-width: 500px;
width: 90%;
padding: var(--sp-6);
max-height: 90vh;
overflow-y: auto;
}
/* Modal Header */
.qrcode-modal__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--sp-5);
}
.qrcode-modal__title {
font-size: var(--fs-xl);
font-weight: 700;
margin: 0;
}
.qrcode-modal__close-btn {
background: none;
border: none;
font-size: var(--fs-lg);
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
transition: background-color 0.2s;
color: var(--c-text);
}
.qrcode-modal__close-btn:hover:not(:disabled) {
background-color: var(--c-bg);
}
.qrcode-modal__close-btn:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
.qrcode-modal__close-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Status Display */
.qrcode-modal__status-container {
margin-bottom: var(--sp-5);
}
.qrcode-modal__status-row {
margin-bottom: var(--sp-4);
}
.qrcode-modal__status-label {
font-weight: 600;
margin-right: var(--sp-2);
}
.qrcode-modal__status-value {
font-weight: 600;
}
.qrcode-modal__status-value--pending {
color: var(--c-warning);
}
.qrcode-modal__status-value--success {
color: var(--c-success);
}
.qrcode-modal__status-value--error {
color: var(--c-danger);
}
.qrcode-modal__status-message {
color: var(--c-text-muted);
margin-left: var(--sp-2);
font-size: var(--fs-sm);
}
/* QR Code Display */
.qrcode-modal__qr-section {
margin-bottom: var(--sp-5);
text-align: center;
}
.qrcode-modal__qr-instruction {
font-size: var(--fs-sm);
color: var(--c-text-muted);
margin-bottom: var(--sp-4);
}
.qrcode-modal__qr-image {
max-width: 300px;
width: 100%;
height: auto;
border: 1px solid var(--c-border);
border-radius: var(--radius);
background-color: var(--c-surface);
padding: var(--sp-2);
image-rendering: pixelated;
}
/* Status Messages */
.qrcode-modal__loading-message {
color: var(--c-text-muted);
text-align: center;
padding: var(--sp-4);
}
.qrcode-modal__success-message {
color: var(--c-success);
text-align: center;
padding: var(--sp-4);
}
.qrcode-modal__cancelled-message {
color: var(--c-text-muted);
text-align: center;
padding: var(--sp-4);
}
.qrcode-modal__error {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-danger-weak);
border-left: 4px solid var(--c-danger);
border-radius: var(--radius);
color: var(--c-danger);
margin-bottom: var(--sp-4);
font-size: var(--fs-sm);
}
.qrcode-modal__error strong {
display: block;
margin-bottom: var(--sp-2);
}
/* Button Group */
.qrcode-modal__button-group {
display: flex;
gap: var(--sp-3);
margin-top: var(--sp-5);
}
.qrcode-modal__button {
flex: 1;
padding: var(--sp-3) var(--sp-4);
border: none;
border-radius: var(--radius);
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.qrcode-modal__button--success {
background-color: var(--c-success);
color: var(--c-surface);
}
.qrcode-modal__button--success:hover {
background-color: #0d6a37;
}
.qrcode-modal__button--danger {
background-color: var(--c-danger);
color: var(--c-surface);
}
.qrcode-modal__button--danger:hover {
background-color: #8a1a15;
}
.qrcode-modal__button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.qrcode-modal__button:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
}
/* Responsive */
@media (max-width: 640px) {
.qrcode-modal {
width: 95%;
padding: var(--sp-4);
}
.qrcode-modal__qr-image {
max-width: 250px;
}
.qrcode-modal__button-group {
flex-direction: column-reverse;
}
.qrcode-modal__button {
width: 100%;
}
}