Design: neues Login (weiss, Logo, Code-Schritte), Glitch-Hintergrund entfernt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
988934552f
commit
47a26fb9b2
3 files changed
+84
-397
No files matched your search
@@ -1,245 +0,0 @@
|
|||||||
import { useRef, useEffect } from 'react';
|
|
||||||
|
|
||||||
const LetterGlitch = ({
|
|
||||||
glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],
|
|
||||||
className = '',
|
|
||||||
glitchSpeed = 50,
|
|
||||||
centerVignette = false,
|
|
||||||
outerVignette = true,
|
|
||||||
smooth = true,
|
|
||||||
characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'
|
|
||||||
}) => {
|
|
||||||
const canvasRef = useRef(null);
|
|
||||||
const animationRef = useRef(null);
|
|
||||||
const letters = useRef([]);
|
|
||||||
const grid = useRef({ columns: 0, rows: 0 });
|
|
||||||
const context = useRef(null);
|
|
||||||
const lastGlitchTime = useRef(Date.now());
|
|
||||||
|
|
||||||
const lettersAndSymbols = Array.from(characters);
|
|
||||||
|
|
||||||
const fontSize = 16;
|
|
||||||
const charWidth = 10;
|
|
||||||
const charHeight = 20;
|
|
||||||
|
|
||||||
const getRandomChar = () => {
|
|
||||||
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRandomColor = () => {
|
|
||||||
return glitchColors[Math.floor(Math.random() * glitchColors.length)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const hexToRgb = hex => {
|
|
||||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
|
||||||
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
|
|
||||||
return r + r + g + g + b + b;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
||||||
return result
|
|
||||||
? {
|
|
||||||
r: parseInt(result[1], 16),
|
|
||||||
g: parseInt(result[2], 16),
|
|
||||||
b: parseInt(result[3], 16)
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const interpolateColor = (start, end, factor) => {
|
|
||||||
const result = {
|
|
||||||
r: Math.round(start.r + (end.r - start.r) * factor),
|
|
||||||
g: Math.round(start.g + (end.g - start.g) * factor),
|
|
||||||
b: Math.round(start.b + (end.b - start.b) * factor)
|
|
||||||
};
|
|
||||||
return `rgb(${result.r}, ${result.g}, ${result.b})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateGrid = (width, height) => {
|
|
||||||
const columns = Math.ceil(width / charWidth);
|
|
||||||
const rows = Math.ceil(height / charHeight);
|
|
||||||
return { columns, rows };
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeLetters = (columns, rows) => {
|
|
||||||
grid.current = { columns, rows };
|
|
||||||
const totalLetters = columns * rows;
|
|
||||||
letters.current = Array.from({ length: totalLetters }, () => ({
|
|
||||||
char: getRandomChar(),
|
|
||||||
color: getRandomColor(),
|
|
||||||
targetColor: getRandomColor(),
|
|
||||||
colorProgress: 1
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeCanvas = () => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return;
|
|
||||||
const parent = canvas.parentElement;
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const rect = parent.getBoundingClientRect();
|
|
||||||
|
|
||||||
canvas.width = rect.width * dpr;
|
|
||||||
canvas.height = rect.height * dpr;
|
|
||||||
|
|
||||||
canvas.style.width = `${rect.width}px`;
|
|
||||||
canvas.style.height = `${rect.height}px`;
|
|
||||||
|
|
||||||
if (context.current) {
|
|
||||||
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { columns, rows } = calculateGrid(rect.width, rect.height);
|
|
||||||
initializeLetters(columns, rows);
|
|
||||||
|
|
||||||
drawLetters();
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawLetters = () => {
|
|
||||||
if (!context.current || letters.current.length === 0) return;
|
|
||||||
const ctx = context.current;
|
|
||||||
const { width, height } = canvasRef.current.getBoundingClientRect();
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
ctx.font = `${fontSize}px monospace`;
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
|
|
||||||
letters.current.forEach((letter, index) => {
|
|
||||||
const x = (index % grid.current.columns) * charWidth;
|
|
||||||
const y = Math.floor(index / grid.current.columns) * charHeight;
|
|
||||||
ctx.fillStyle = letter.color;
|
|
||||||
ctx.fillText(letter.char, x, y);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateLetters = () => {
|
|
||||||
if (!letters.current || letters.current.length === 0) return;
|
|
||||||
|
|
||||||
const updateCount = Math.max(1, Math.floor(letters.current.length * 0.05));
|
|
||||||
|
|
||||||
for (let i = 0; i < updateCount; i++) {
|
|
||||||
const index = Math.floor(Math.random() * letters.current.length);
|
|
||||||
if (!letters.current[index]) continue;
|
|
||||||
|
|
||||||
letters.current[index].char = getRandomChar();
|
|
||||||
letters.current[index].targetColor = getRandomColor();
|
|
||||||
|
|
||||||
if (!smooth) {
|
|
||||||
letters.current[index].color = letters.current[index].targetColor;
|
|
||||||
letters.current[index].colorProgress = 1;
|
|
||||||
} else {
|
|
||||||
letters.current[index].colorProgress = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSmoothTransitions = () => {
|
|
||||||
let needsRedraw = false;
|
|
||||||
letters.current.forEach(letter => {
|
|
||||||
if (letter.colorProgress < 1) {
|
|
||||||
letter.colorProgress += 0.05;
|
|
||||||
if (letter.colorProgress > 1) letter.colorProgress = 1;
|
|
||||||
|
|
||||||
const startRgb = hexToRgb(letter.color);
|
|
||||||
const endRgb = hexToRgb(letter.targetColor);
|
|
||||||
if (startRgb && endRgb) {
|
|
||||||
letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
|
|
||||||
needsRedraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (needsRedraw) {
|
|
||||||
drawLetters();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastGlitchTime.current >= glitchSpeed) {
|
|
||||||
updateLetters();
|
|
||||||
drawLetters();
|
|
||||||
lastGlitchTime.current = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (smooth) {
|
|
||||||
handleSmoothTransitions();
|
|
||||||
}
|
|
||||||
|
|
||||||
animationRef.current = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
context.current = canvas.getContext('2d');
|
|
||||||
resizeCanvas();
|
|
||||||
animate();
|
|
||||||
|
|
||||||
let resizeTimeout;
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
clearTimeout(resizeTimeout);
|
|
||||||
resizeTimeout = setTimeout(() => {
|
|
||||||
cancelAnimationFrame(animationRef.current);
|
|
||||||
resizeCanvas();
|
|
||||||
animate();
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(animationRef.current);
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [glitchSpeed, smooth]);
|
|
||||||
|
|
||||||
const containerStyle = {
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
backgroundColor: '#000000',
|
|
||||||
overflow: 'hidden'
|
|
||||||
};
|
|
||||||
|
|
||||||
const canvasStyle = {
|
|
||||||
display: 'block',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%'
|
|
||||||
};
|
|
||||||
|
|
||||||
const outerVignetteStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
background: 'radial-gradient(circle, rgba(0,0,0,0) 60%, rgba(0,0,0,1) 100%)'
|
|
||||||
};
|
|
||||||
|
|
||||||
const centerVignetteStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
background: 'radial-gradient(circle, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 60%)'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={containerStyle} className={className}>
|
|
||||||
<canvas ref={canvasRef} style={canvasStyle} />
|
|
||||||
{outerVignette && <div style={outerVignetteStyle}></div>}
|
|
||||||
{centerVignette && <div style={centerVignetteStyle}></div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LetterGlitch;
|
|
||||||
|
|
||||||
+82
-150
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import LetterGlitch from '../components/LetterGlitch'
|
import Logo from '../components/Logo'
|
||||||
import { safeReturnPath } from '../lib/returnPath'
|
import { safeReturnPath } from '../lib/returnPath'
|
||||||
import { NO_STAFF_ACCESS } from '../lib/staffAccess'
|
import { NO_STAFF_ACCESS } from '../lib/staffAccess'
|
||||||
|
|
||||||
@@ -83,167 +83,99 @@ export default function LoginPage() {
|
|||||||
setPassword('')
|
setPassword('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkButton = {
|
|
||||||
background: 'none',
|
|
||||||
border: 0,
|
|
||||||
padding: 0,
|
|
||||||
color: '#e2e8f0',
|
|
||||||
textDecoration: 'underline',
|
|
||||||
cursor: 'pointer',
|
|
||||||
font: 'inherit',
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeStep = mfaPending && (
|
const codeStep = mfaPending && (
|
||||||
<form onSubmit={handleCode}>
|
<>
|
||||||
{error && (
|
<h1>Code eingeben</h1>
|
||||||
<div className="bg-red text-white p-1 mb-2" style={{ borderRadius: '4px' }}>
|
<p className="sub">
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p style={{ marginTop: 0 }}>
|
|
||||||
{!mfaPending.challengeId
|
{!mfaPending.challengeId
|
||||||
? 'Wir schicken dir einen Code an deine E-Mail-Adresse.'
|
? 'Wir schicken dir einen Code an deine E-Mail-Adresse.'
|
||||||
: `Wir haben dir einen Code an ${mfaPending.email || 'deine E-Mail-Adresse'} geschickt.`}
|
: `Wir haben dir einen Code an ${mfaPending.email || 'deine E-Mail-Adresse'} geschickt.`}
|
||||||
</p>
|
</p>
|
||||||
{notice && <p style={{ marginTop: 0 }}>{notice}</p>}
|
<form onSubmit={handleCode}>
|
||||||
{mfaPending.challengeId ? (
|
{error && <div className="msg err" role="alert">{error}</div>}
|
||||||
<>
|
{notice && <div className="msg ok">{notice}</div>}
|
||||||
<div className="form-group">
|
{mfaPending.challengeId ? (
|
||||||
<label className="form-label" htmlFor="mfa-code">Code aus der E-Mail</label>
|
<>
|
||||||
<input
|
<div className="field">
|
||||||
id="mfa-code"
|
<label htmlFor="mfa-code">Code aus der E-Mail</label>
|
||||||
type="text"
|
<input
|
||||||
className="form-control"
|
id="mfa-code"
|
||||||
value={code}
|
type="text"
|
||||||
onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
|
className="input code-input"
|
||||||
inputMode="numeric"
|
value={code}
|
||||||
autoComplete="one-time-code"
|
onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
|
||||||
maxLength={6}
|
inputMode="numeric"
|
||||||
required
|
autoComplete="one-time-code"
|
||||||
autoFocus
|
maxLength={6}
|
||||||
style={{ letterSpacing: '0.3em' }}
|
required
|
||||||
/>
|
autoFocus
|
||||||
</div>
|
/>
|
||||||
<button type="submit" className="btn btn-green" style={{ width: '100%' }} disabled={loading}>
|
</div>
|
||||||
{loading ? 'Prüfe…' : 'Anmelden'}
|
<button type="submit" className="btn btn-primary btn-block" disabled={loading}>
|
||||||
</button>
|
{loading ? 'Prüfe…' : 'Anmelden'}
|
||||||
<p style={{ textAlign: 'center', marginBottom: 0 }}>
|
</button>
|
||||||
<button type="button" style={linkButton} onClick={handleSend} disabled={loading}>Code erneut senden</button>
|
</>
|
||||||
{' · '}
|
) : (
|
||||||
<button type="button" style={linkButton} onClick={handleCancel}>Abbrechen</button>
|
<button type="button" className="btn btn-primary btn-block" onClick={handleSend} disabled={loading}>
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button type="button" className="btn btn-green" style={{ width: '100%' }} onClick={handleSend} disabled={loading}>
|
|
||||||
{loading ? 'Sende…' : 'Code senden'}
|
{loading ? 'Sende…' : 'Code senden'}
|
||||||
</button>
|
</button>
|
||||||
<p style={{ textAlign: 'center', marginBottom: 0 }}>
|
)}
|
||||||
<button type="button" style={linkButton} onClick={handleCancel}>Abbrechen</button>
|
</form>
|
||||||
</p>
|
<div className="login-links">
|
||||||
</>
|
{mfaPending.challengeId && (
|
||||||
)}
|
<button type="button" className="link" onClick={handleSend} disabled={loading}>Code erneut senden</button>
|
||||||
</form>
|
)}
|
||||||
|
<button type="button" className="link" onClick={handleCancel}>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<>
|
||||||
position: 'relative',
|
<div className="login">
|
||||||
minHeight: '100vh',
|
<div className="login-box">
|
||||||
width: '100%',
|
<Logo size={40} />
|
||||||
overflow: 'hidden'
|
{codeStep || (
|
||||||
}}>
|
<>
|
||||||
{/* LetterGlitch Hintergrund */}
|
<h1>Anmelden</h1>
|
||||||
<div style={{
|
<p className="sub">Ticketsystem für das WEBklar-Team</p>
|
||||||
position: 'absolute',
|
<form onSubmit={handleSubmit}>
|
||||||
top: 0,
|
{error && <div className="msg err" role="alert">{error}</div>}
|
||||||
left: 0,
|
<div className="field">
|
||||||
width: '100%',
|
<label htmlFor="login-email">E-Mail</label>
|
||||||
height: '100%',
|
<input
|
||||||
zIndex: 0
|
id="login-email"
|
||||||
}}>
|
type="email"
|
||||||
<LetterGlitch
|
className="input"
|
||||||
glitchSpeed={50}
|
value={email}
|
||||||
centerVignette={true}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
outerVignette={false}
|
autoComplete="username"
|
||||||
smooth={true}
|
required
|
||||||
/>
|
placeholder="deine@email.com"
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Login-Formular */}
|
|
||||||
<div style={{
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 1,
|
|
||||||
minHeight: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '400px',
|
|
||||||
boxShadow: '0 10px 40px rgba(0,0,0,0.5)',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
WebkitBackdropFilter: 'blur(10px)',
|
|
||||||
background: 'transparent',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
marginBottom: '16px'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
padding: '12px 16px',
|
|
||||||
background: 'transparent',
|
|
||||||
color: '#fff',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
borderBottom: '1px solid rgba(255, 255, 255, 0.2)',
|
|
||||||
textAlign: 'center'
|
|
||||||
}}>
|
|
||||||
<h2>Webklar WOMS 2.0</h2>
|
|
||||||
</div>
|
|
||||||
<div style={{ padding: '16px', color: '#e2e8f0' }}>
|
|
||||||
{codeStep || <form onSubmit={handleSubmit}>
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red text-white p-1 mb-2" style={{ borderRadius: '4px' }}>
|
|
||||||
{error}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="field">
|
||||||
|
<label htmlFor="login-password">Passwort</label>
|
||||||
<div className="form-group">
|
<input
|
||||||
<label className="form-label">Email</label>
|
id="login-password"
|
||||||
<input
|
type="password"
|
||||||
type="email"
|
className="input"
|
||||||
className="form-control"
|
value={password}
|
||||||
value={email}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
placeholder="deine@email.com"
|
minLength={8}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" className="btn btn-primary btn-block" disabled={loading}>
|
||||||
<div className="form-group">
|
{loading ? 'Login läuft...' : 'Weiter'}
|
||||||
<label className="form-label">Password</label>
|
</button>
|
||||||
<input
|
</form>
|
||||||
type="password"
|
</>
|
||||||
className="form-control"
|
)}
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
placeholder="••••••••"
|
|
||||||
minLength={8}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-green"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Login läuft...' : 'Login'}
|
|
||||||
</button>
|
|
||||||
</form>}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="login-foot">Webklar Ticket-Plattform · © {new Date().getFullYear()}</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1034,8 +1034,8 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
|
|||||||
.ui-btn-primary:hover { background: var(--accent-hover); }
|
.ui-btn-primary:hover { background: var(--accent-hover); }
|
||||||
.ui-btn-danger { background: none; box-shadow: none; padding: 0 4px; color: var(--danger); }
|
.ui-btn-danger { background: none; box-shadow: none; padding: 0 4px; color: var(--danger); }
|
||||||
.ui-btn-danger:hover { background: none; text-decoration: underline; text-underline-offset: 3px; }
|
.ui-btn-danger:hover { background: none; text-decoration: underline; text-underline-offset: 3px; }
|
||||||
.btn-dark, .btn-teal, .btn-green { background: var(--accent); color: #fff; box-shadow: var(--shadow-btn); }
|
.btn-dark, .btn-teal { background: var(--accent); color: #fff; box-shadow: var(--shadow-btn); }
|
||||||
.btn-dark:hover, .btn-teal:hover, .btn-green:hover { background: var(--accent-hover); }
|
.btn-dark:hover, .btn-teal:hover { background: var(--accent-hover); }
|
||||||
|
|
||||||
/* Formular (.form-group, .form-label, .form-control) wie .field, Beschriftung und .input / .select / .textarea */
|
/* Formular (.form-group, .form-label, .form-control) wie .field, Beschriftung und .input / .select / .textarea */
|
||||||
.form-group { margin-bottom: 16px; }
|
.form-group { margin-bottom: 16px; }
|
||||||
|
|||||||
Reference in new issue
Block a user