2026 Version + better Admin Panel
Some checks failed
Deploy to Firebase Hosting on merge / build_and_deploy (push) Has been cancelled

This commit is contained in:
Laptop-Luis
2026-03-27 06:32:11 +01:00
parent 9c57f94ccb
commit cb700679a0
9 changed files with 472 additions and 138 deletions

View File

@@ -1,13 +1,13 @@
robots.txt,1747753185483,bfe106a3fb878dc83461c86818bf74fc1bdc7f28538ba613cd3e775516ce8b49
manifest.json,1747753184548,ee04fb47e525c67d8424ffe9b4d8a8a24e434504478afca4e0ca602146836d4c
logo512.png,1747753185347,212b102aa09e51b3b3e06647e81f7801a61333e171f6582e8124379aabccb41d
logo192.png,1747753185281,79e2b749561016bc8af300ea19f48347ceed3cb1a54f48ae456172eca45e08f0
index.html,1747847326960,849e0ace18b79b41ecd4cf058617c8fe30d0260e9f780e7d6c5d9b0f4a393a08
favicon.ico,1747753183459,27edce7be5922cf0bef7d4136f69b5bfbdd5bf8c13c7b026f71187d41a00aa7d
asset-manifest.json,1747847326960,71f2b660bf82dcac89eca1a39967b8c1183022b8276ecf79af76141ae5267d1b
static/media/image.68b1c4e66e36b61c1dd0.png,1747847326963,4c693c0814e4164a776deb6b47cbd2d1a3efd933c8a20b56d4859a09448cabe6
static/js/main.fa819294.js.map,1747847326966,828b1179d7eed41ab38293e4b1b99783e76289b31fa7e7189a6aef7f641e0c9b
static/js/main.fa819294.js.LICENSE.txt,1747847326961,a7ea19f57bfd4e3c0af924b53ff76690cd6095ac68e363fd14818efd5827d75f
static/js/main.fa819294.js,1747847326965,4275a7875fa16aff74b58f53c91d869b9c645341db38a509231d4ba471f68fbf
static/css/main.27c24027.css.map,1747847326965,033259ccb0f0ac7257a4783cdb717910d00da508d2342f0524eac6cce55fc742
static/css/main.27c24027.css,1747847326965,e4823df6aa4a5d438454fbcff833647c6b1f5418513454474837484107f75b87
robots.txt,1773230060143,2544ca049f223a42bff01f72ad930a5edba75bbb7199d0f8430a02ff5aca16ec
manifest.json,1773230060143,0958a5e0c831126100c8c2d06a6bbaa665a3900f21aaff4130238a6f5a113aa1
logo512.png,1773230060142,7779210d56c1f3741e2e487799fe3092def4fa6ac450a60532b807c3a8971205
logo192.png,1773230060142,76c449ccb9cd117c2f2338f091b18f7050f3210e249b2228f5c81b23f34377cd
index.html,1773230082934,bd194f2036618038faf2160756bebb04b2f1e83066a17bf2acc47ef40111a307
favicon.ico,1773230060141,c599b7a91ab3627e3538125d9f40adc2d4bf949046984262670545dc7738af06
asset-manifest.json,1773230082934,7f4cf7a8d279a367d5ef87ec1b7e011ef95f754fe965d6cc8ab7c83f516422b2
static/media/image.68b1c4e66e36b61c1dd0.png,1773230082958,4c15c35a390623a3f0336b6b0e8f485a2021e5056859a01cf4d3ac2f0811dc1a
static/js/main.a4c2285e.js.map,1773230082958,6305b722062c4030817b783079dabc604bdeb4773e043d395b56426fc22eb2fc
static/js/main.a4c2285e.js.LICENSE.txt,1773230082958,0f20024d1acb99c151d60b35e6b4d4f4bc0306cc5146c2dc78faeeae7003b732
static/js/main.a4c2285e.js,1773230082958,8e177703342bda82fe4a97629b6cd3c97a79ac0a0b5d7c173f780ab682e897f3
static/css/main.6ccb1dfd.css.map,1773230082958,fb800f7cc089f3be092246a049b4646aa51b068dc5cd5e70916bc952f4d75d1e
static/css/main.6ccb1dfd.css,1773230082958,a1b343c74ba990507a2752e290e43a0857f2c68bbbdc1507a23e05e41a998def

View File

@@ -1,5 +1,5 @@
{
"projects": {
"default": "skatabend0"
"default": "skatabend-e7c88"
}
}

View File

@@ -1,5 +1,6 @@
{
"hosting": {
"site": "skatturnier",
"public": "build",
"ignore": [
"firebase.json",
@@ -11,7 +12,6 @@
"source": "**",
"destination": "/index.html"
}
],
"site": "skatabend"
]
}
}

6
package-lock.json generated
View File

@@ -6226,9 +6226,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"version": "1.0.30001777",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
"integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
"funding": [
{
"type": "opencollective",

View File

@@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
content="Skatabend 2026"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Skatabend 2025</title>
<title>Skatabend 2026</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

233
src/Admin.css Normal file
View File

@@ -0,0 +1,233 @@
.admin-container {
min-height: 100vh;
padding: 30px 20px;
background: rgba(0, 0, 0, 0.4);
color: white;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
border-bottom: 2px solid #61dafb;
padding-bottom: 20px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.admin-header h2 {
margin: 0;
font-size: 28px;
color: #ffffff;
}
.logout-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.logout-btn:hover {
background-color: #c82333;
}
.logout-btn:active {
background-color: #a71d2a;
transform: scale(0.98);
}
.admin-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 40px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.stat-box {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
text-align: center;
border: 1px solid rgba(97, 218, 251, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-box:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(97, 218, 251, 0.2);
border-color: #61dafb;
}
.stat-box h3 {
margin: 0 0 15px 0;
font-size: 16px;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-number {
margin: 0;
font-size: 48px;
font-weight: bold;
color: #61dafb;
}
.admin-table-container {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 25px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
max-width: 1200px;
margin: 0 auto;
border: 1px solid rgba(97, 218, 251, 0.3);
}
.admin-table-container h3 {
margin-top: 0;
margin-bottom: 20px;
color: #ffffff;
font-size: 1.3em;
}
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.admin-table thead {
background-color: rgba(97, 218, 251, 0.1);
border-bottom: 2px solid #61dafb;
}
.admin-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #61dafb;
}
.admin-table th:last-child {
text-align: center;
}
.admin-table td {
padding: 12px;
border-bottom: 1px solid rgba(97, 218, 251, 0.1);
color: #f0f0f0;
}
.admin-table tbody tr:hover {
background-color: rgba(97, 218, 251, 0.05);
}
.count-cell {
font-weight: 600;
color: #61dafb;
text-align: center;
}
.names-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.name-item {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 10px;
background-color: rgba(97, 218, 251, 0.1);
border-radius: 4px;
border: 1px solid rgba(97, 218, 251, 0.2);
}
.name-item span {
color: #f0f0f0;
}
.delete-person-btn {
background: none;
border: none;
color: #dc3545;
cursor: pointer;
font-size: 16px;
padding: 0;
transition: transform 0.2s ease;
display: flex;
align-items: center;
}
.delete-person-btn:hover {
transform: scale(1.2);
}
.actions-cell {
text-align: center;
}
.delete-registration-btn {
background-color: #dc3545;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.delete-registration-btn:hover {
background-color: #c82333;
}
.delete-registration-btn:active {
background-color: #a71d2a;
transform: scale(0.95);
}
@media (max-width: 768px) {
.admin-header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.admin-stats {
grid-template-columns: 1fr;
}
.admin-table {
font-size: 12px;
}
.admin-table th,
.admin-table td {
padding: 8px;
}
.stat-number {
font-size: 36px;
}
.admin-header h2 {
font-size: 24px;
}
}

121
src/Admin.jsx Normal file
View File

@@ -0,0 +1,121 @@
import { collection, onSnapshot, deleteDoc, doc, updateDoc } from 'firebase/firestore';
import React, { useEffect, useState } from 'react'
import { auth, db } from './firebase';
import { signOut } from 'firebase/auth';
import './Admin.css';
export default function Admin() {
const [data, setData] = useState([]);
useEffect(() => {
let unsub = onSnapshot(collection(db, "forms"), (snapshot) => {
let data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setData(data)
})
return () => {
unsub();
};
}, []);
const getTotalPeople = () => {
return data.reduce((sum, entry) => sum + (entry.names ? entry.names.length : 0), 0);
}
const deleteRegistration = async (id) => {
if (window.confirm("Möchtest du diese Anmeldung wirklich löschen?")) {
try {
await deleteDoc(doc(db, "forms", id));
alert("Anmeldung gelöscht.");
} catch (error) {
alert("Fehler beim Löschen: " + error.message);
console.error(error);
}
}
}
const deletePerson = async (id, index) => {
if (window.confirm("Möchtest du diese Person wirklich löschen?")) {
try {
const entry = data.find(item => item.id === id);
if (entry && entry.names) {
const updatedNames = entry.names.filter((_, i) => i !== index);
await updateDoc(doc(db, "forms", id), {
names: updatedNames,
number: updatedNames.length
});
alert("Person gelöscht.");
}
} catch (error) {
alert("Fehler beim Löschen: " + error.message);
console.error(error);
}
}
}
return (
<div className="admin-container">
<div className="admin-header">
<h2>Verwaltung Dashboard</h2>
<button type='button' onClick={() => signOut(auth)} className="logout-btn">Logout</button>
</div>
<div className="admin-stats">
<div className="stat-box">
<h3>Gesamtzahl Anmeldungen</h3>
<p className="stat-number">{data.length}</p>
</div>
<div className="stat-box">
<h3>Gesamtzahl Personen</h3>
<p className="stat-number">{getTotalPeople()}</p>
</div>
</div>
<div className="admin-table-container">
<h3>Anmeldungen</h3>
<table className="admin-table">
<thead>
<tr>
<th>E-Mail</th>
<th>Personen</th>
<th>Anzahl</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{data.map((entry) => (
<tr key={entry.id}>
<td>{entry.email}</td>
<td>
<div className="names-list">
{entry.names && entry.names.map((name, index) => (
<div key={index} className="name-item">
<span>{name}</span>
<button
className="delete-person-btn"
onClick={() => deletePerson(entry.id, index)}
title="Person löschen"
>
🗑
</button>
</div>
))}
</div>
</td>
<td className="count-cell">{entry.names ? entry.names.length : 0}</td>
<td className="actions-cell">
<button
className="delete-registration-btn"
onClick={() => deleteRegistration(entry.id)}
title="Gesamte Anmeldung löschen"
>
Löschen
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -1,41 +1,27 @@
import { useEffect, useRef, useState } from 'react';
import './App.css';
import { addDoc, collection, doc, getDoc, onSnapshot } from 'firebase/firestore';
import { db } from './firebase';
import { auth, db } from './firebase';
import Admin from './Admin';
import { onAuthStateChanged, signInWithEmailAndPassword } from 'firebase/auth';
function App() {
const radioGroupEntries = [1, 2, 3, 4]
const formSectionRef = useRef(null);
const [customNumber, setCustomNumber] = useState(false);
const [number, setNumber] = useState(null)
const [names, setNames] = useState([""]);
const [email, setEmail] = useState("");
const [showManagement, setShowManagement] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loginPassword, setLoginPassword] = useState("");
const [data, setData] = useState([]);
const [isLoggedIn, setisLoggedIn] = useState(false);
useEffect(() => {
let unsub = onSnapshot(collection(db, "forms"), (snapshot) => {
let data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
for(let i = 0; i < data.length; i++){
let element = data[i]
if(element.id === "3GQrSzfPaZHZWleu6sq7") data.splice(i, 1)
}
setData(data)
const unsub = onAuthStateChanged(auth, (user) => {
setisLoggedIn(user ? true : false)
})
return () => {
unsub();
unsub()
};
}, []);
const getNumberOfUsers = () => {
let number = 0
for(let user of data){
number += user.number
}
return number;
}
const scrollToForm = () => {
const formSection = formSectionRef.current;
formSection.classList.add('visible', 'bounce'); // Füge Bounce-Klasse hinzu
@@ -52,50 +38,77 @@ function App() {
const submit = async (e) => {
e.preventDefault(); // Verhindert das Standardverhalten des Formulars
if (!emailIsValid(email)){
alert("Bitte eine gültige E-Mailadresse eintrgen");
alert("Bitte eine gültige E-Mailadresse eintragen");
return;
}
if(!number){
alert("Bitte angeben wie viele Personen mitkommen");
if(names.length === 0){
alert("Bitte mindestens eine Person eintragen.")
return;
}
for (let i = 0; i < names.length; i++) {
if(names[i] === ""){
alert("Bitte einen Name für Person " + (i+1) + " eintragen")
return;
}
}
// send data
try {
addDoc(collection(db, "forms"), {
email: email,
number: number
await addDoc(collection(db, "forms"), {
email,
names,
})
alert("Anmeldung gesendet.")
window.location.reload(true)
} catch (error) {
alert("Fehler beim Senden der Anmeldung. Bitte versuche es später erneut.");
console.error(error);
console.error("Firestore Error:", error);
alert("Fehler beim Senden der Anmeldung: " + error.message);
}
alert("Anmeldung gesendet.")
setEmail("")
setCustomNumber(false)
setNumber(null)
}
const displayManagement = () => {
setShowManagement(true);
}
const hideManagement = () => {
setShowManagement(false);
setIsLoggedIn(false);
setLoginPassword("");
}
const handleLogin = async (e) => {
e.preventDefault();
let pw = await getDoc(doc(db, "forms", "3GQrSzfPaZHZWleu6sq7"))
let pw2 = pw.data()
if (loginPassword === pw2.pw) {
setIsLoggedIn(true);
try {
await signInWithEmailAndPassword(auth, "ex@luisk.de", loginPassword)
setLoginPassword("")
} catch (error) {
if (error.code === 'auth/wrong-password' || error.code === 'auth/invalid-credential') {
alert("Falsches Passwort!");
} else if (error.code === 'auth/user-not-found') {
alert("Benutzer nicht gefunden!");
} else {
alert("Fehler beim Login. Bitte versuchen Sie es später erneut.");
console.error(error);
}
setLoginPassword("");
} else {
alert("Falsches Passwort!");
}
}
const createPersonFields = () => {
return names.map((name, i) => (
<div key={i} style={{ display: 'flex', gap: '10px', marginBottom: '10px', alignItems: 'center' }}>
<input
type="text"
placeholder={"Name " + (i+1) + ". Person"}
value={name}
onChange={(e) => {
let prev = [...names]
prev[i] = e.target.value
setNames(prev)
}}
/>
<button
type="button"
onClick={() => {
setNames(names.filter((_, index) => index !== i))
}}
style={{ background: '#ff4444', color: 'white', border: 'none', padding: '5px 10px', cursor: 'pointer', borderRadius: '4px' }}
>
🗑
</button>
</div>
))
}
return (
<div className="App">
{showManagement ? (
@@ -111,35 +124,9 @@ function App() {
autoFocus
/>
<button type="submit">Login</button>
<button type="button" onClick={hideManagement} style={{background:'#888',marginTop:'10px'}}>Abbrechen</button>
<button type="button" onClick={() => setShowManagement(false)} style={{background:'#888',marginTop:'10px'}}>Abbrechen</button>
</form>
) : (
<>
<h2>Verwaltung</h2>
<button onClick={hideManagement}>Abmelden & Zurück</button>
{/* Gesamtanzahl Personen */}
<div className="gesamtanzahl">
Gesamtanzahl Personen: <b>{ getNumberOfUsers() }</b>
</div>
{/* Tabelle mit Dummy-Daten */}
<table className="management-table">
<thead>
<tr>
<th>E-Mail</th>
<th>Anzahl Personen</th>
</tr>
</thead>
<tbody>
{ data.map(user => (
<tr key={user.id}>
<td>{ user.email }</td>
<td>{ user.number }</td>
</tr>
)) }
</tbody>
</table>
</>
)}
) : <Admin />}
</div>
) : (
<>
@@ -156,7 +143,23 @@ function App() {
</div>
<div className="fact-row">
<span className="fact-label">Datum:</span>
<span className="fact-value">25.06.2025</span>
<span className="fact-value">Fr. 27.03.2026</span>
</div>
<div className="fact-row">
<span className="fact-label">Eintrittspreis:</span>
<span className="fact-value">2</span>
</div>
<div className="fact-row">
<span className="fact-label">Anmeldung spätestens bis:</span>
<span className="fact-value">17:30 (online oder vor Ort)</span>
</div>
<br />
<div className="fact-row">
{/* <span className='fact-label'></span> */}
<span className="fact-value">Ausschließlich für Schüler, Lehrer und Familienmitglieder.</span>
</div>
<div className="fact-row">
<span className='fact-value'>Für Verpflegung ist gesorgt.</span>
</div>
</div>
<button onClick={scrollToForm}>Jetzt Anmelden</button>
@@ -166,35 +169,9 @@ function App() {
<label>E-Mail:</label>
<input type="email" name="email" placeholder="Deine E-Mail-Adresse" onChange={(e => setEmail(e.target.value))} value={email}/>
<br />
<label>Anzahl der Personen</label>
<div className="radio-group">
{ radioGroupEntries.map((entry) => <label key={entry} style={ number === entry && !customNumber ? { backgroundColor: "rgba(0, 128, 0, 0.7)" } : null }>
<input
type="radio"
name="number"
onChange={() => { setNumber(entry); setCustomNumber(false) }}
/>
{ entry } Person{ entry > 1 ? "en" : null }
</label> ) }
<label style={ customNumber ? { backgroundColor: "rgba(0, 128, 0, 0.7)" } : null }>
<input
type="radio"
name="number"
onChange={() => setCustomNumber(true)}
/>
Andere Nummer
</label>
</div>
{customNumber ? (
<input
type="number"
name="customNumber"
placeholder="Anzahl der Personen"
min={1}
onChange={(e) => setNumber(parseInt(e.target.value))}
value={number}
/>
) : null}
<label>Namen</label>
<button type='button' onClick={() => setNames([...names, ""]) }>Neue Person hinzufügen</button>
{ createPersonFields() }
<br />
<button type="submit" onClick={submit}>Anmelden</button>
</form>
@@ -203,7 +180,8 @@ function App() {
)}
<footer>
<p>
<a onClick={displayManagement} style={{ marginRight: '20px', cursor: 'pointer' }}>Verwaltung öffnen</a>
<a onClick={() => setShowManagement(true)} style={{ marginRight: '20px', cursor: 'pointer' }}>Verwaltung öffnen</a>
Bildquelle Hintergrund: <a href="https://store-images.s-microsoft.com/image/apps.42739.13510798887965433.cf25521d-d390-432c-8007-cbc0aa3ebe97.53ed75d0-4a6a-4c11-a3ae-fd528ef6444a?h=1280" target="_blank" rel="noopener noreferrer">hier klicken</a>
</p>
</footer>

View File

@@ -1,16 +1,18 @@
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: "AIzaSyAMimOrp73SW_pffgJ_MXeIjrXr7a-IIPE",
authDomain: "skatabend0.firebaseapp.com",
projectId: "skatabend0",
storageBucket: "skatabend0.firebasestorage.app",
messagingSenderId: "362332041327",
appId: "1:362332041327:web:87779c1bcb2a70f8c8dd9a"
}
apiKey: "AIzaSyB79cGYmKx_bpYFJ7H2AORfaksGsqZuFik",
authDomain: "skatabend-e7c88.firebaseapp.com",
projectId: "skatabend-e7c88",
storageBucket: "skatabend-e7c88.firebasestorage.app",
messagingSenderId: "931117483366",
appId: "1:931117483366:web:071a84ce381dd2701734a7"
};
const app = initializeApp(firebaseConfig)
const app = initializeApp(firebaseConfig);
const db = getFirestore(app)
const auth = getAuth()
export { db }
export { db, auth }