@@ -0,0 +1,66 @@
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dialog-panel {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0 0 1rem 0;
|
||||
font-family: 'Sora', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-cancel, .btn-confirm {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius-m);
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: var(--teal);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: #25d9ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px var(--teal-soft);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import './ConfirmDialog.css';
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ isOpen, title, message, onConfirm, onCancel }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog-panel panel">
|
||||
<h3 className="dialog-title">{title}</h3>
|
||||
<p className="dialog-message">{message}</p>
|
||||
<div className="dialog-actions">
|
||||
<button className="btn-cancel" onClick={onCancel}>Abbrechen</button>
|
||||
<button className="btn-confirm" onClick={onConfirm}>Bestätigen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
padding: 1.5rem 0 0.5rem 0;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-content:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--teal);
|
||||
}
|
||||
|
||||
.footer-credits {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-credits p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer-credits strong {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import './Footer.css';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="footer-content">
|
||||
<a href="/impressum" className="footer-link">Impressum</a>
|
||||
<div className="footer-credits">
|
||||
<p>Developed by <strong>Tim Müller</strong></p>
|
||||
<p>Original Design by <strong>Alexey Sukhov</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
background: rgba(8, 14, 20, 0.6);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.header-brand:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: 'Sora', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.header-admin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.header-username {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--teal);
|
||||
background: var(--teal-soft);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.app-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { LogOut } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './Header.css';
|
||||
|
||||
interface HeaderProps {
|
||||
showAdminControls?: boolean;
|
||||
username?: string | null;
|
||||
onLogout?: () => void;
|
||||
showResultsLink?: boolean;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ showAdminControls, username, onLogout, showResultsLink }) => {
|
||||
return (
|
||||
<header className="app-header">
|
||||
<Link to="/" className="header-brand">
|
||||
<img src="/logo.png" alt="Goethe+ Logo" className="header-logo" />
|
||||
<span className="header-title">Outfit-Voting Abi26</span>
|
||||
</Link>
|
||||
|
||||
<div className="header-admin">
|
||||
{showResultsLink && (
|
||||
<Link to="/result" className="header-username" style={{ textDecoration: 'none' }}>
|
||||
Ergebnisse
|
||||
</Link>
|
||||
)}
|
||||
{showAdminControls && (
|
||||
<>
|
||||
{username && (
|
||||
<a href="/account" className="header-username" style={{ textDecoration: 'none' }}>
|
||||
{username}
|
||||
</a>
|
||||
)}
|
||||
{onLogout && (
|
||||
<button className="btn-logout" onClick={onLogout}>
|
||||
<LogOut size={18} /> Logout
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
.lock-screen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.lock-icon-container {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
background: var(--teal-soft);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--teal);
|
||||
box-shadow: 0 0 40px var(--teal-soft);
|
||||
}
|
||||
|
||||
.lock-title {
|
||||
font-family: 'Sora', sans-serif;
|
||||
font-size: 2rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.lock-message {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
max-width: 400px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Lock } from 'lucide-react';
|
||||
import './LockScreen.css';
|
||||
|
||||
interface LockScreenProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const LockScreen: React.FC<LockScreenProps> = ({ message = "Voting oder Zählung noch nicht abgeschlossen" }) => {
|
||||
return (
|
||||
<div className="lock-screen panel">
|
||||
<div className="lock-icon-container">
|
||||
<Lock size={48} className="lock-icon" />
|
||||
</div>
|
||||
<h2 className="lock-title">Derzeit Gesperrt</h2>
|
||||
<p className="lock-message">{message}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
.search-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
.search-select.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.select-header {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: 0.8rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-select:not(.disabled) .select-header:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.select-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem 1rem;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.options-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.options-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: var(--radius-m);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.option-item.selected {
|
||||
background: var(--teal-soft);
|
||||
color: var(--teal);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-options {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { ChevronDown, Search } from 'lucide-react';
|
||||
import './SearchableSelect.css';
|
||||
|
||||
interface Option { value: string; label: string; }
|
||||
|
||||
interface SearchableSelectProps {
|
||||
options: Option[];
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const SearchableSelect: React.FC<SearchableSelectProps> = ({ options, value, onChange, disabled }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, []);
|
||||
|
||||
const filtered = options.filter(o => o.label.toLowerCase().includes(search.toLowerCase()));
|
||||
const selected = options.find(o => o.value === value);
|
||||
|
||||
return (
|
||||
<div className={`search-select ${disabled ? 'disabled' : ''}`} ref={ref}>
|
||||
<div className="select-header" onClick={() => !disabled && setIsOpen(!isOpen)}>
|
||||
<span>{selected ? selected.label : 'Bitte wählen...'}</span>
|
||||
<ChevronDown size={18} className={`chevron ${isOpen ? 'open' : ''}`} />
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="select-dropdown panel">
|
||||
<div className="search-box">
|
||||
<Search size={16} className="search-icon" />
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="options-list">
|
||||
{filtered.map(o => (
|
||||
<div
|
||||
key={o.value}
|
||||
className={`option-item ${o.value === value ? 'selected' : ''}`}
|
||||
onClick={() => { onChange(o.value); setIsOpen(false); setSearch(''); }}
|
||||
>
|
||||
{o.label}
|
||||
</div>
|
||||
))}
|
||||
{filtered.length === 0 && <div className="no-options">Keine Ergebnisse</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user