feat(frontend): add swipe to unlock page and various text changes
This commit is contained in:
@@ -57,8 +57,12 @@
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.app-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.header-brand {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
.slide-control {
|
||||
position: relative;
|
||||
height: 78px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
background: rgba(8, 11, 16, 0.88);
|
||||
padding: 4px;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.slide-control::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(22, 193, 231, 0.26), transparent 44%);
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slide-control.is-dragging {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.slide-label,
|
||||
.slide-hint {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slide-label {
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.015em;
|
||||
color: rgba(248, 251, 255, 0.86);
|
||||
}
|
||||
|
||||
.slide-hint {
|
||||
margin-top: 46px;
|
||||
font-size: 0.77rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: rgba(190, 210, 225, 0.57);
|
||||
}
|
||||
|
||||
.slide-knob {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #04070b;
|
||||
background: linear-gradient(180deg, #fff 0%, #e8ecf0 100%);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useRef, useState, type PointerEvent } from 'react';
|
||||
import './SlideToContinue.css';
|
||||
|
||||
interface SlideToContinueProps {
|
||||
label: string;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
const KNOB_SIZE = 68;
|
||||
const TRACK_PADDING = 4;
|
||||
const COMPLETE_THRESHOLD = 0.84;
|
||||
|
||||
export function SlideToContinue({ label, onComplete }: SlideToContinueProps) {
|
||||
const trackRef = useRef<HTMLDivElement | null>(null);
|
||||
const [position, setPosition] = useState(0);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const moveKnob = (clientX: number) => {
|
||||
const track = trackRef.current;
|
||||
if (!track) return;
|
||||
|
||||
const bounds = track.getBoundingClientRect();
|
||||
const max = Math.max(bounds.width - KNOB_SIZE - TRACK_PADDING * 2, 1);
|
||||
const next = clientX - bounds.left - KNOB_SIZE / 2 - TRACK_PADDING;
|
||||
const clamped = Math.min(Math.max(next, 0), max);
|
||||
setPosition(clamped);
|
||||
};
|
||||
|
||||
const completeIfNeeded = () => {
|
||||
const track = trackRef.current;
|
||||
if (!track) return;
|
||||
|
||||
const max = Math.max(track.getBoundingClientRect().width - KNOB_SIZE - TRACK_PADDING * 2, 1);
|
||||
if (position / max >= COMPLETE_THRESHOLD) {
|
||||
setPosition(max);
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
setPosition(0);
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
setDragging(true);
|
||||
moveKnob(event.clientX);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!dragging) return;
|
||||
moveKnob(event.clientX);
|
||||
};
|
||||
|
||||
const handlePointerUp = (event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!dragging) return;
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
setDragging(false);
|
||||
completeIfNeeded();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={trackRef}
|
||||
className={`slide-control${dragging ? ' is-dragging' : ''}`}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={label}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onComplete();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="slide-label">{label}</span>
|
||||
<span className="slide-hint">Nach rechts wischen</span>
|
||||
<span className="slide-knob" style={{ transform: `translateX(${position}px)` }}>
|
||||
<ArrowRight size={28} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user