diff --git a/.classpath b/.classpath index fe5de2c..8ce7b51 100644 --- a/.classpath +++ b/.classpath @@ -1,26 +1,25 @@ - - + - + - + - + @@ -28,553 +27,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/.gitignore b/.gitignore index 2d6aca8..b18b1d3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build # Secrets – niemals einchecken .env +src/main/resources/application-dev.properties diff --git a/.project b/.project index 3a71ae4..d9c8418 100644 --- a/.project +++ b/.project @@ -6,12 +6,12 @@ - org.eclipse.buildship.core.gradleprojectbuilder + org.eclipse.jdt.core.javabuilder - org.eclipse.jdt.core.javabuilder + org.eclipse.buildship.core.gradleprojectbuilder diff --git a/bin/main/Ideen.txt b/bin/main/Ideen.txt index 12c52cd..64b32e5 100644 --- a/bin/main/Ideen.txt +++ b/bin/main/Ideen.txt @@ -1,39 +1,11 @@ -Slomo und Speedup Card +Umsetzung des Spiels: +Der Lockee hat eine Stunde Zeit das Spiel zu starten, dies geschieht per Knopfdruck +Wenn er dies nicht schafft -> bei Keyholder, benachrichtige Keyholder und lass sie/ihn entscheiden, ansonsten freeze wie bei freeze card +Übernimm die Logik des Spiels aus dem BDSM Game. +Falls eine Zeitstrafe eine temporäre Öffnung vor oder nach der Aufgabe benötigt, öffne das Lock für 5 Minuten. Überzogene Zeit wird addiert und am Ende des Locks gefreezed +Selbiges gilt, falls der finisher eine temporäre Öffnung danach erfoldert +Benötigt der Finisher eine Öffnung davor, verwende die Logik der Cum Card, und addiere diese Zeit auf die möglicherweise schon vorhandenen Freeze Zeit am Ende des Locks -Sammeln von Erfahrung - -TODO: Im Time Lock, wenn im Spinning Wheel tasks drin sind, dürfen keine sonst keine Tasks gefordert sein und umgekehrt - -Ich kann Spieler einladen zu spielen, dann kriegt die Person eine E-Mail und muss bestätigen, dass es diese PErson ist, sie wird dann ins spiel übernommen - -- Falls fall mit Chastity auftritt wird die Spielpartnerin als Keyholder eingetragen, diese Person darf entscheiden, was für ein Lock das wird. - - - Hier ein paar Ideen für neue Kartentypen: - - Bestrafungskarten - - Straf-Karte – Lockee muss eine vorher definierte Strafe erfüllen (ähnlich Task, aber negativer konnotiert) - - Extra-Rot – Fügt sofort 2-3 rote Karten hinzu, kein Ziehen möglich - - Belohnungskarten - - Bonus-Grün – LatestOpeningTime wird auf jetzt gesetzt (sofortige Öffnungsmöglichkeit), aber nur kurz gültig (z.B. 30 Minuten Fenster) - - Karten entfernen – Lockee darf eine bestimmte Anzahl roter Karten aus dem Deck entfernen - - Ereigniskarten - - Würfel-Karte – Zufällige Aktion: 1-2 = Freeze, 3-4 = Nichts, 5-6 = Grüne Karte - - Umkehr-Karte – Die nächste Karte hat den umgekehrten Effekt (Rot → Grün, Freeze → Beschleunigung) - - Überraschungs-Karte – Community, Keyholder oder Zufalls-Task, je nachdem was gerade konfiguriert ist - - Zeitkarten - - Verlängerungs-Karte – Verschiebt die latestOpeningtime nach hinten (nur bei Keyholder-Locks sinnvoll) - - Countdown-Karte – Setzt einen Timer; wenn die Lockee innerhalb der Zeit eine Aufgabe erledigt, wird eine grüne Karte freigeschaltet - - Hygiene-Skip – Nächste Hygiene-Öffnung wird übersprungen/gezählt ohne tatsächliche Öffnung - - Soziale Karten - - Verifizierungs-Karte – Erzwingt sofort eine Verifikations-Session - - Keyholder-Wahl – Keyholder entscheidet frei was passiert (Freitext-Eingabe möglich) - - Community-Entscheid – Community stimmt nicht über eine Aufgabe ab, sondern darüber was als nächstes passiert (z.B. Freeze vs. Aufgabe) - - Die interessantesten wären wohl Würfel und Countdown, da sie mehr Spannung erzeugen ohne den Ablauf zu sehr zu unterbrechen. diff --git a/bin/main/application-dev.properties b/bin/main/application-dev.properties index 94b18e0..75437c9 100644 --- a/bin/main/application-dev.properties +++ b/bin/main/application-dev.properties @@ -6,4 +6,4 @@ app.cookie.secure=false # Klartext-Credentials für lokale DB (kein Umgebungsvariablen-Zwang) spring.mail.username=local@dev.invalid spring.mail.password=unused -jwt.keystore.password=XUR!Rv&f$j3UsqD& +jwt.keystore.password=${JWT_KEYSTORE_PASSWORD} diff --git a/bin/main/application.properties b/bin/main/application.properties index 9861494..2702739 100644 --- a/bin/main/application.properties +++ b/bin/main/application.properties @@ -6,6 +6,8 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA / Hibernate spring.jpa.hibernate.ddl-auto=update +spring.flyway.baseline-on-migrate=true +spring.flyway.locations=classpath:db/migration spring.jpa.show-sql=false spring.jpa.open-in-view=false spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect diff --git a/bin/main/db/migration/V1__baseline.sql b/bin/main/db/migration/V1__baseline.sql new file mode 100644 index 0000000..7c444a0 --- /dev/null +++ b/bin/main/db/migration/V1__baseline.sql @@ -0,0 +1,8 @@ +-- Baseline marker für Flyway. +-- Diese Migration wird auf bestehenden Datenbanken nicht ausgeführt +-- (spring.flyway.baseline-on-migrate=true markiert sie als bereits angewendet). +-- Für neue Datenbanken: Schema wird von Hibernate (ddl-auto=update) angelegt, +-- da kein vollständiges CREATE-Skript vorhanden ist. +-- Sobald das Schema stabil ist, diesen Inhalt durch ein vollständiges +-- mysqldump --no-data xxx_sphere > V1__baseline.sql ersetzen +-- und ddl-auto auf validate umstellen. diff --git a/bin/main/db/migration/V2__available_in.sql b/bin/main/db/migration/V2__available_in.sql new file mode 100644 index 0000000..62e7453 --- /dev/null +++ b/bin/main/db/migration/V2__available_in.sql @@ -0,0 +1,34 @@ +-- Migration: vanillaAvailable (boolean) → availableIn (VARCHAR enum) +-- +-- BDSM_AND_VANILLA = ehemals vanilla_available = TRUE +-- BDSM_ONLY = ehemals vanilla_available = FALSE (Default) +-- CHASTITY_ONLY = neuer Wert +-- +-- Die Prozedur prüft zuerst, ob vanilla_available noch existiert, bevor +-- sie etwas tut – dadurch ist die Migration auf leeren Datenbanken ein No-op. + +DROP PROCEDURE IF EXISTS proc_migrate_available_in; + +CREATE PROCEDURE proc_migrate_available_in() +BEGIN + DECLARE col_exists INT DEFAULT 0; + SELECT COUNT(*) INTO col_exists + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'aufgaben_gruppe' + AND COLUMN_NAME = 'vanilla_available'; + + IF col_exists > 0 THEN + ALTER TABLE aufgaben_gruppe + ADD COLUMN available_in VARCHAR(50) NOT NULL DEFAULT 'BDSM_ONLY'; + UPDATE aufgaben_gruppe + SET available_in = 'BDSM_AND_VANILLA' + WHERE vanilla_available = 1; + ALTER TABLE aufgaben_gruppe + DROP COLUMN vanilla_available; + END IF; +END; + +CALL proc_migrate_available_in(); + +DROP PROCEDURE IF EXISTS proc_migrate_available_in; diff --git a/bin/main/de/oaa/xxx/admin/AdminController.class b/bin/main/de/oaa/xxx/admin/AdminController.class index 8be8ade..4ec3074 100644 Binary files a/bin/main/de/oaa/xxx/admin/AdminController.class and b/bin/main/de/oaa/xxx/admin/AdminController.class differ diff --git a/bin/main/de/oaa/xxx/config/JwtFilter.class b/bin/main/de/oaa/xxx/config/JwtFilter.class index 5960bf6..4c108bf 100644 Binary files a/bin/main/de/oaa/xxx/config/JwtFilter.class and b/bin/main/de/oaa/xxx/config/JwtFilter.class differ diff --git a/bin/main/de/oaa/xxx/config/RateLimitFilter$Window.class b/bin/main/de/oaa/xxx/config/RateLimitFilter$Window.class index 8927b01..fc83964 100644 Binary files a/bin/main/de/oaa/xxx/config/RateLimitFilter$Window.class and b/bin/main/de/oaa/xxx/config/RateLimitFilter$Window.class differ diff --git a/bin/main/de/oaa/xxx/config/RateLimitFilter.class b/bin/main/de/oaa/xxx/config/RateLimitFilter.class index cb2cbc6..ae23216 100644 Binary files a/bin/main/de/oaa/xxx/config/RateLimitFilter.class and b/bin/main/de/oaa/xxx/config/RateLimitFilter.class differ diff --git a/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class b/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class index c0a0d33..16c6a4b 100644 Binary files a/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class and b/bin/main/de/oaa/xxx/feed/FeedController$FeedPage.class differ diff --git a/bin/main/de/oaa/xxx/feed/FeedController$UpdateOptionRequest.class b/bin/main/de/oaa/xxx/feed/FeedController$UpdateOptionRequest.class index c609d7d..c73b587 100644 Binary files a/bin/main/de/oaa/xxx/feed/FeedController$UpdateOptionRequest.class and b/bin/main/de/oaa/xxx/feed/FeedController$UpdateOptionRequest.class differ diff --git a/bin/main/de/oaa/xxx/feed/FeedController$UpdatePostRequest.class b/bin/main/de/oaa/xxx/feed/FeedController$UpdatePostRequest.class index bdd49c7..f4b3caa 100644 Binary files a/bin/main/de/oaa/xxx/feed/FeedController$UpdatePostRequest.class and b/bin/main/de/oaa/xxx/feed/FeedController$UpdatePostRequest.class differ diff --git a/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class b/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class index 9adea41..ff0303f 100644 Binary files a/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class and b/bin/main/de/oaa/xxx/feed/FeedController$VoteRequest.class differ diff --git a/bin/main/de/oaa/xxx/feed/FeedController.class b/bin/main/de/oaa/xxx/feed/FeedController.class index f780e29..02d12cf 100644 Binary files a/bin/main/de/oaa/xxx/feed/FeedController.class and b/bin/main/de/oaa/xxx/feed/FeedController.class differ diff --git a/bin/main/de/oaa/xxx/feed/repository/FeedPostLikeRepository.class b/bin/main/de/oaa/xxx/feed/repository/FeedPostLikeRepository.class index 1c15590..9379a8c 100644 Binary files a/bin/main/de/oaa/xxx/feed/repository/FeedPostLikeRepository.class and b/bin/main/de/oaa/xxx/feed/repository/FeedPostLikeRepository.class differ diff --git a/bin/main/de/oaa/xxx/feed/repository/FeedPostOptionRepository.class b/bin/main/de/oaa/xxx/feed/repository/FeedPostOptionRepository.class index 5c24e62..42abdbc 100644 Binary files a/bin/main/de/oaa/xxx/feed/repository/FeedPostOptionRepository.class and b/bin/main/de/oaa/xxx/feed/repository/FeedPostOptionRepository.class differ diff --git a/bin/main/de/oaa/xxx/feed/repository/FeedPostVoteRepository.class b/bin/main/de/oaa/xxx/feed/repository/FeedPostVoteRepository.class index a776385..f4d97eb 100644 Binary files a/bin/main/de/oaa/xxx/feed/repository/FeedPostVoteRepository.class and b/bin/main/de/oaa/xxx/feed/repository/FeedPostVoteRepository.class differ diff --git a/bin/main/de/oaa/xxx/feedback/FeedbackController.class b/bin/main/de/oaa/xxx/feedback/FeedbackController.class index 2845318..3deeb6a 100644 Binary files a/bin/main/de/oaa/xxx/feedback/FeedbackController.class and b/bin/main/de/oaa/xxx/feedback/FeedbackController.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/BdsmGameDurchfuehren.class b/bin/main/de/oaa/xxx/games/bdsm/BdsmGameDurchfuehren.class index f18c40b..b043416 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/BdsmGameDurchfuehren.class and b/bin/main/de/oaa/xxx/games/bdsm/BdsmGameDurchfuehren.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabenGruppeController.class b/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabenGruppeController.class index 9b18d3b..8aa7476 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabenGruppeController.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabenGruppeController.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenRequest.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenRequest.class index 42d3aff..6d9509b 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenRequest.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenResponse.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenResponse.class index faefe3f..902f08b 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenResponse.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$AbschliessenResponse.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskRequest.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskRequest.class index 36144b3..02a5d3e 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskRequest.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskResponse.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskResponse.class index e560bb2..72aeccd 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskResponse.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ActiveTaskResponse.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$SperreFreigabe.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$SperreFreigabe.class index 6a1e045..360f020 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$SperreFreigabe.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$SperreFreigabe.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ZuChastityRequest.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ZuChastityRequest.class index 5dd16d4..ceb0552 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ZuChastityRequest.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController$ZuChastityRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class index 5234f44..cba4c4a 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$10.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$10.class new file mode 100644 index 0000000..6b6d71d Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$10.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$11.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$11.class new file mode 100644 index 0000000..796f023 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$11.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$12.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$12.class new file mode 100644 index 0000000..188bb6f Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum$12.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum.class index bbc46c2..8d5cedf 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardEnum.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class index ae9220c..6f4b32e 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class index 935af10..e678ff8 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class index 36dee6b..c6feb72 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$SpeedConfirmRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$SpeedConfirmRequest.class new file mode 100644 index 0000000..fc6a0dd Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$SpeedConfirmRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class index 36fec47..b0fbcc2 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class index c05654d..ee5d5b4 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class index 2b5e949..98ac737 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockSimulation.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockSimulation.class new file mode 100644 index 0000000..0a0c952 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockSimulation.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController$TemplateRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController$TemplateRequest.class index 6f021a2..d8356ee 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController$TemplateRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController$TemplateRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController.class index 10416fa..abfaa87 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateEntity.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateEntity.class index 3688e27..0437bf6 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateEntity.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardlockTemplateEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/GameCard.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/GameCard.class new file mode 100644 index 0000000..18c7bb1 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/GameCard.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/SlowmoCard.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/SlowmoCard.class new file mode 100644 index 0000000..ac05736 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/SlowmoCard.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/SpeedupCard.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/SpeedupCard.class new file mode 100644 index 0000000..02eb838 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/cardlock/SpeedupCard.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.class b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.class index d292deb..f7f3752 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.class and b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.class b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.class index b91f935..dd00661 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.class and b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockTemplateEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameEntity.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameEntity.class new file mode 100644 index 0000000..7ffadf8 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameLockEntity.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameLockEntity.class new file mode 100644 index 0000000..d88eab1 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameLockEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameService.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameService.class new file mode 100644 index 0000000..71ebaae Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameService.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetController$GameSetRequest.class b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetController$GameSetRequest.class new file mode 100644 index 0000000..6b1a566 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetController$GameSetRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetController.class b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetController.class new file mode 100644 index 0000000..c8a3132 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetEntity.class b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetEntity.class new file mode 100644 index 0000000..76d3e05 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetRepository.class b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetRepository.class new file mode 100644 index 0000000..cc4f314 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/ChastityGameSetRepository.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabe.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabe.class new file mode 100644 index 0000000..b7c350d Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabe.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabeListConverter$1.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabeListConverter$1.class new file mode 100644 index 0000000..e28f7ef Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabeListConverter$1.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabeListConverter.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabeListConverter.class new file mode 100644 index 0000000..ce9ba8a Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetAufgabeListConverter.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisher.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisher.class new file mode 100644 index 0000000..65846f6 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisher.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisherListConverter$1.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisherListConverter$1.class new file mode 100644 index 0000000..d967971 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisherListConverter$1.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisherListConverter.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisherListConverter.class new file mode 100644 index 0000000..e7ac63d Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetFinisherListConverter.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafe.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafe.class new file mode 100644 index 0000000..badc644 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafe.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafeListConverter$1.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafeListConverter$1.class new file mode 100644 index 0000000..104243a Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafeListConverter$1.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafeListConverter.class b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafeListConverter.class new file mode 100644 index 0000000..6309b4a Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/gameset/GameSetZeitstrafeListConverter.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetController$TaskSetRequest.class b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetController$TaskSetRequest.class new file mode 100644 index 0000000..95b2909 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetController$TaskSetRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetController.class b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetController.class new file mode 100644 index 0000000..3fb47d4 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetEntity.class b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetEntity.class new file mode 100644 index 0000000..d341ded Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetRepository.class b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetRepository.class new file mode 100644 index 0000000..85da82f Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/tasks/ChastityTaskSetRepository.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController$TemplateRequest.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController$TemplateRequest.class index fb7b800..3acb7a8 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController$TemplateRequest.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController$TemplateRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController.class index 113196e..9bfef60 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockTemplateController.class differ diff --git a/bin/main/de/oaa/xxx/games/common/AbstractGameDurchfuehren.class b/bin/main/de/oaa/xxx/games/common/AbstractGameDurchfuehren.class new file mode 100644 index 0000000..147597a Binary files /dev/null and b/bin/main/de/oaa/xxx/games/common/AbstractGameDurchfuehren.class differ diff --git a/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppe.class b/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppe.class index c776833..6dc1760 100644 Binary files a/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppe.class and b/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppe.class differ diff --git a/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppeDisplay.class b/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppeDisplay.class index 2141bbc..41831f2 100644 Binary files a/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppeDisplay.class and b/bin/main/de/oaa/xxx/games/common/aufgaben/AufgabenGruppeDisplay.class differ diff --git a/bin/main/de/oaa/xxx/games/common/aufgaben/AvailableIn.class b/bin/main/de/oaa/xxx/games/common/aufgaben/AvailableIn.class new file mode 100644 index 0000000..7a8f211 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/common/aufgaben/AvailableIn.class differ diff --git a/bin/main/de/oaa/xxx/games/common/entity/AufgabenGruppeEntity.class b/bin/main/de/oaa/xxx/games/common/entity/AufgabenGruppeEntity.class index f1d78b3..bd4426d 100644 Binary files a/bin/main/de/oaa/xxx/games/common/entity/AufgabenGruppeEntity.class and b/bin/main/de/oaa/xxx/games/common/entity/AufgabenGruppeEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/common/repository/AufgabenGruppeRepository.class b/bin/main/de/oaa/xxx/games/common/repository/AufgabenGruppeRepository.class index 12ad477..87961e7 100644 Binary files a/bin/main/de/oaa/xxx/games/common/repository/AufgabenGruppeRepository.class and b/bin/main/de/oaa/xxx/games/common/repository/AufgabenGruppeRepository.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/VanillaGameDurchfuehren.class b/bin/main/de/oaa/xxx/games/vanilla/VanillaGameDurchfuehren.class index 33deb6f..9ed8c99 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/VanillaGameDurchfuehren.class and b/bin/main/de/oaa/xxx/games/vanilla/VanillaGameDurchfuehren.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAboController.class b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAboController.class index 8a3a2f4..db8dd47 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAboController.class and b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAboController.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAufgabenGruppeController.class b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAufgabenGruppeController.class index 8c670c8..15a7252 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAufgabenGruppeController.class and b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaAufgabenGruppeController.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenRequest.class b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenRequest.class index 9821102..8bbd4aa 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenRequest.class and b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenResponse.class b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenResponse.class index 105f898..8c52405 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenResponse.class and b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$AbschliessenResponse.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskRequest.class b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskRequest.class index 8dc6a68..d3bf5f4 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskRequest.class and b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskResponse.class b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskResponse.class index 9335982..f550410 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskResponse.class and b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController$ActiveTaskResponse.class differ diff --git a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController.class b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController.class index e16b7e7..c5ee54b 100644 Binary files a/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController.class and b/bin/main/de/oaa/xxx/games/vanilla/controller/VanillaGameController.class differ diff --git a/bin/main/de/oaa/xxx/gruppe/GruppeController.class b/bin/main/de/oaa/xxx/gruppe/GruppeController.class index b57064f..26b5ff0 100644 Binary files a/bin/main/de/oaa/xxx/gruppe/GruppeController.class and b/bin/main/de/oaa/xxx/gruppe/GruppeController.class differ diff --git a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class index e260232..824ef2e 100644 Binary files a/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class and b/bin/main/de/oaa/xxx/gruppe/GruppenbeitragController.class differ diff --git a/bin/main/de/oaa/xxx/location/LocationController$BatchRequest.class b/bin/main/de/oaa/xxx/location/LocationController$BatchRequest.class index 0c6c3c8..ebe3370 100644 Binary files a/bin/main/de/oaa/xxx/location/LocationController$BatchRequest.class and b/bin/main/de/oaa/xxx/location/LocationController$BatchRequest.class differ diff --git a/bin/main/de/oaa/xxx/location/LocationController$InboxConversationDto.class b/bin/main/de/oaa/xxx/location/LocationController$InboxConversationDto.class index f7fcdeb..ff71ecc 100644 Binary files a/bin/main/de/oaa/xxx/location/LocationController$InboxConversationDto.class and b/bin/main/de/oaa/xxx/location/LocationController$InboxConversationDto.class differ diff --git a/bin/main/de/oaa/xxx/location/LocationController$InboxSummaryDto.class b/bin/main/de/oaa/xxx/location/LocationController$InboxSummaryDto.class index ebb4f2f..3b45058 100644 Binary files a/bin/main/de/oaa/xxx/location/LocationController$InboxSummaryDto.class and b/bin/main/de/oaa/xxx/location/LocationController$InboxSummaryDto.class differ diff --git a/bin/main/de/oaa/xxx/location/LocationController$ReplyRequest.class b/bin/main/de/oaa/xxx/location/LocationController$ReplyRequest.class index 251909f..e70d798 100644 Binary files a/bin/main/de/oaa/xxx/location/LocationController$ReplyRequest.class and b/bin/main/de/oaa/xxx/location/LocationController$ReplyRequest.class differ diff --git a/bin/main/de/oaa/xxx/location/LocationController.class b/bin/main/de/oaa/xxx/location/LocationController.class index 7334dca..a1cabf9 100644 Binary files a/bin/main/de/oaa/xxx/location/LocationController.class and b/bin/main/de/oaa/xxx/location/LocationController.class differ diff --git a/bin/main/de/oaa/xxx/mail/MailService.class b/bin/main/de/oaa/xxx/mail/MailService.class index e99e266..a633150 100644 Binary files a/bin/main/de/oaa/xxx/mail/MailService.class and b/bin/main/de/oaa/xxx/mail/MailService.class differ diff --git a/bin/main/de/oaa/xxx/social/NotificationController.class b/bin/main/de/oaa/xxx/social/NotificationController.class index 98b59a6..81fa3ed 100644 Binary files a/bin/main/de/oaa/xxx/social/NotificationController.class and b/bin/main/de/oaa/xxx/social/NotificationController.class differ diff --git a/bin/main/de/oaa/xxx/social/ScheduledNotificationService.class b/bin/main/de/oaa/xxx/social/ScheduledNotificationService.class new file mode 100644 index 0000000..c2f5b80 Binary files /dev/null and b/bin/main/de/oaa/xxx/social/ScheduledNotificationService.class differ diff --git a/bin/main/de/oaa/xxx/social/SystemMessageService.class b/bin/main/de/oaa/xxx/social/SystemMessageService.class index 6f47d2a..3a22d5f 100644 Binary files a/bin/main/de/oaa/xxx/social/SystemMessageService.class and b/bin/main/de/oaa/xxx/social/SystemMessageService.class differ diff --git a/bin/main/de/oaa/xxx/social/entity/MessageEntity.class b/bin/main/de/oaa/xxx/social/entity/MessageEntity.class index ce404c5..9ed831c 100644 Binary files a/bin/main/de/oaa/xxx/social/entity/MessageEntity.class and b/bin/main/de/oaa/xxx/social/entity/MessageEntity.class differ diff --git a/bin/main/de/oaa/xxx/social/repository/KommentarRepository.class b/bin/main/de/oaa/xxx/social/repository/KommentarRepository.class index b8e8c54..a6a97d1 100644 Binary files a/bin/main/de/oaa/xxx/social/repository/KommentarRepository.class and b/bin/main/de/oaa/xxx/social/repository/KommentarRepository.class differ diff --git a/bin/main/de/oaa/xxx/social/repository/MessageRepository.class b/bin/main/de/oaa/xxx/social/repository/MessageRepository.class index 9334187..d4efe12 100644 Binary files a/bin/main/de/oaa/xxx/social/repository/MessageRepository.class and b/bin/main/de/oaa/xxx/social/repository/MessageRepository.class differ diff --git a/bin/main/de/oaa/xxx/user/LoginController.class b/bin/main/de/oaa/xxx/user/LoginController.class index a360f35..d58a710 100644 Binary files a/bin/main/de/oaa/xxx/user/LoginController.class and b/bin/main/de/oaa/xxx/user/LoginController.class differ diff --git a/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class b/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class index b4e9d51..9355202 100644 Binary files a/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class and b/bin/main/de/oaa/xxx/user/UserController$LocationFilterRequest.class differ diff --git a/bin/main/de/oaa/xxx/user/UserController$NewMemberDto.class b/bin/main/de/oaa/xxx/user/UserController$NewMemberDto.class index 8f40b45..c54e7c7 100644 Binary files a/bin/main/de/oaa/xxx/user/UserController$NewMemberDto.class and b/bin/main/de/oaa/xxx/user/UserController$NewMemberDto.class differ diff --git a/bin/main/de/oaa/xxx/user/UserController.class b/bin/main/de/oaa/xxx/user/UserController.class index e7a5ce3..44827ae 100644 Binary files a/bin/main/de/oaa/xxx/user/UserController.class and b/bin/main/de/oaa/xxx/user/UserController.class differ diff --git a/bin/main/de/oaa/xxx/util/BaseController.class b/bin/main/de/oaa/xxx/util/BaseController.class new file mode 100644 index 0000000..8b3505f Binary files /dev/null and b/bin/main/de/oaa/xxx/util/BaseController.class differ diff --git a/bin/main/static/admin/admin.html b/bin/main/static/admin/admin.html index cadf95c..0900533 100644 --- a/bin/main/static/admin/admin.html +++ b/bin/main/static/admin/admin.html @@ -122,6 +122,7 @@ .gruppe-badge { font-size:0.65rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(255,255,255,0.07); color:var(--color-muted); } .gruppe-badge-public { background:rgba(46,204,113,0.15); color:var(--color-success); } .gruppe-badge-vanilla { background:#e8f5e9; color:#2e7d32; border:1px solid #a5d6a7; } + .gruppe-badge-chastity { background:rgba(155,89,182,0.15); color:#9b59b6; border:1px solid rgba(155,89,182,0.4); } .gruppe-toggle { font-size:0.75rem; color:var(--color-muted); flex-shrink:0; transition:transform 0.2s; } .gruppe-card.open .gruppe-toggle { transform:rotate(90deg); } .gruppe-body { border-top:1px solid var(--color-secondary); padding:1rem 1rem 0.75rem; } @@ -278,11 +279,12 @@ Aktuelles Bild – neues wählen zum Ersetzen - - +
+ + +
+ +
+
Simulation
+

Simuliert 100 Durchläufe mit der aktuellen Konfiguration und zeigt die erwartete Sperrdauer. Die Simulation basiert auf dem Idealfall, dass jede Karte sofort gezogen wird, sobald sie verfügbar ist – die tatsächliche Sperrdauer wird in der Praxis höher ausfallen.

+ + + +
+
+ + + + + + + + + + + + @@ -555,8 +839,14 @@ let editId = null; let editType = null; // 'CARDLOCK' | 'TIMELOCK' – beim Bearbeiten fest let isDirty = false; - let taskCtr = 0; let wheelCtr = 0; + + // ── Aufgaben-Sets ── + let _taskSets = []; + let _taskSetEditId = null; + let _taskSetTaskCtr = 0; + let _taskSetCallerType = null; // 'card' | 'timelock' | null + let _taskSetIsDirty = false; let pageNum = 0; let isLastPage = false; let isLoading = false; @@ -605,7 +895,7 @@ const type = currentModalType(); document.getElementById('sectionCardlock').style.display = type === 'CARDLOCK' ? '' : 'none'; document.getElementById('sectionTimelock').style.display = type === 'TIMELOCK' ? '' : 'none'; - updateTaskModeVisibility(); + document.getElementById('simSection').style.display = type === 'CARDLOCK' ? '' : 'none'; } // ── Karten-Grid ── @@ -767,46 +1057,316 @@ document.getElementById('rowPenaltyValue').style.display = needsVal ? '' : 'none'; } - // ── Aufgaben ── - function addTask(data) { - const id = ++taskCtr; - const titleVal = (data?.title||data?.text||'').replace(/"/g,'"'); - const descVal = (data?.description||'').replace(//g,'>'); + // ── Aufgaben-Sets: Seite ── + async function loadTaskSets() { + try { + const res = await fetch('/chastity/task-sets'); + if (!res.ok) return; + _taskSets = await res.json(); + renderTaskSetList(); + populateTaskSetSelects(); + } catch(e) { console.error(e); } + } + + function renderTaskSetList() { + const list = document.getElementById('taskSetList'); + list.innerHTML = ''; + if (!_taskSets.length) { document.getElementById('taskSetEmpty').style.display = ''; return; } + document.getElementById('taskSetEmpty').style.display = 'none'; + _taskSets.forEach(s => appendTaskSetCard(s)); + } + + function appendTaskSetCard(s) { + const list = document.getElementById('taskSetList'); + const card = document.createElement('div'); + card.className = 'template-card'; + card.style.cursor = 'pointer'; + const preview = s.tasks.length + ? s.tasks.slice(0,3).map(t => esc(t.title)).join(', ') + (s.tasks.length > 3 ? ' …' : '') + : 'Keine Aufgaben'; + card.innerHTML = ` +
+
+ 📋 +
+
+
${esc(s.name)}
+
${s.tasks.length} Aufgabe(n): ${preview}
+
+
+ +
+
`; + card.addEventListener('click', () => openTaskSetModal(s.id)); + list.appendChild(card); + } + + // ── Aufgaben-Sets: Modal ── + function openTaskSetModal(id, callerType) { + _taskSetEditId = id || null; + _taskSetCallerType = callerType || null; + _taskSetTaskCtr = 0; + document.getElementById('taskSetTaskList').innerHTML = ''; + document.getElementById('taskSetError').style.display = 'none'; + document.getElementById('taskSetModalTitle').textContent = id ? 'Aufgaben-Set bearbeiten' : 'Aufgaben-Set erstellen'; + if (id) { + const set = _taskSets.find(s => s.id === id); + if (set) { document.getElementById('fTaskSetName').value = set.name; (set.tasks||[]).forEach(t => addTaskSetTask(t)); } + } else { + document.getElementById('fTaskSetName').value = ''; + } + document.getElementById('taskSetDiscardConfirm').style.display = 'none'; + alignModalToContent(); + document.getElementById('taskSetModalBackdrop').classList.add('open'); + _taskSetIsDirty = false; + setTimeout(() => { + document.getElementById('taskSetModalBackdrop').querySelectorAll('input, textarea, select').forEach(el => { + el.addEventListener('input', () => { _taskSetIsDirty = true; }, { passive: true }); + el.addEventListener('change', () => { _taskSetIsDirty = true; }, { passive: true }); + }); + }, 0); + document.getElementById('fTaskSetName').focus(); + } + + function tryCloseTaskSetModal() { + if (_taskSetIsDirty) { + const confirm = document.getElementById('taskSetDiscardConfirm'); + confirm.style.display = 'flex'; + confirm.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } else { + closeTaskSetModal(); + } + } + + function cancelTaskSetDiscard() { + document.getElementById('taskSetDiscardConfirm').style.display = 'none'; + } + + function closeTaskSetModal() { + document.getElementById('taskSetModalBackdrop').classList.remove('open'); + document.getElementById('taskSetDiscardConfirm').style.display = 'none'; + _taskSetIsDirty = false; + _taskSetEditId = null; _taskSetCallerType = null; + } + + function toggleTaskAccItem(id) { + const target = document.getElementById('ts-' + id); + if (!target) return; + const isOpen = target.classList.contains('is-open'); + document.querySelectorAll('#taskSetTaskList .task-acc-item').forEach(el => el.classList.remove('is-open')); + if (!isOpen) target.classList.add('is-open'); + } + + function addTaskSetTask(data) { + const id = ++_taskSetTaskCtr; + const titleVal = (data?.title || '').replace(/"/g, '"'); + const descVal = (data?.description || '').replace(//g, '>'); const minVal = data?.minutes != null ? data.minutes : ''; const div = document.createElement('div'); - div.className = 'task-item'; div.id = 'mt-' + id; + div.className = 'task-acc-item'; div.id = 'ts-' + id; div.innerHTML = ` -
-
Titel *
-
-
Minuten
-
- +
+ + +
- `; - const containerId = currentModalType() === 'CARDLOCK' ? 'modalCardTaskList' : 'modalTaskList'; - document.getElementById(containerId).appendChild(div); - updateTaskModeVisibility(); +
+
+ + +
+
+ + +
+
`; + document.getElementById('taskSetTaskList').appendChild(div); + _taskSetIsDirty = true; } - function removeTask(id) { document.getElementById('mt-'+id)?.remove(); updateTaskModeVisibility(); } - function updateTaskModeVisibility() { - const type = currentModalType(); - if (type === 'CARDLOCK') { - const hasCardTasks = document.querySelectorAll('#modalCardTaskList .task-item').length > 0; - document.getElementById('sectionCardTaskMode').style.display = hasCardTasks ? '' : 'none'; - } - // For TimeLock: sectionTaskMode is always visible when taskTimingFields is open - } - function collectTasks() { - return Array.from(document.querySelectorAll('.task-item')).map(item => { - const id = item.id.replace('mt-',''); - const title = document.getElementById('mt-title-'+id)?.value.trim(); - const desc = document.getElementById('mt-desc-' +id)?.value.trim(); - const mins = parseInt(document.getElementById('mt-min-' +id)?.value); + + function removeTaskSetTask(id) { document.getElementById('ts-'+id)?.remove(); _taskSetIsDirty = true; } + + function collectTaskSetTasks() { + return Array.from(document.querySelectorAll('#taskSetTaskList .task-acc-item')).map(item => { + const id = item.id.replace('ts-',''); + const title = document.getElementById('ts-title-'+id)?.value.trim(); + const desc = document.getElementById('ts-desc-' +id)?.value.trim(); + const mins = parseInt(document.getElementById('ts-min-' +id)?.value); return title ? { title, description: desc||null, minutes: isNaN(mins)?null:mins } : null; }).filter(Boolean); } + async function saveTaskSet() { + const name = document.getElementById('fTaskSetName').value.trim(); + const errEl = document.getElementById('taskSetError'); + if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; } + const tasks = collectTaskSetTasks(); + const url = _taskSetEditId ? `/chastity/task-sets/${_taskSetEditId}` : '/chastity/task-sets'; + const method = _taskSetEditId ? 'PUT' : 'POST'; + try { + const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body:JSON.stringify({name, tasks}) }); + if (!res.ok) { errEl.textContent = 'Fehler beim Speichern.'; errEl.style.display = ''; return; } + const saved = await res.json(); + const caller = _taskSetCallerType; + closeTaskSetModal(); + await loadTaskSets(); + if (caller) { + const sel = document.getElementById(caller === 'card' ? 'fCardTaskSetId' : 'fTimelockTaskSetId'); + if (sel) { sel.value = saved.id; onTaskSetChange(caller); markDirty(); } + } + } catch(e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; } + } + + async function deleteTaskSet(id, name) { + if (!confirm(`Aufgaben-Set „${name}" wirklich löschen?`)) return; + const res = await fetch(`/chastity/task-sets/${id}`, { method:'DELETE' }); + if (res.ok || res.status === 204) await loadTaskSets(); + } + + const GS_TOOLS = [ + { value: 'UMSCHNALLDILDO', label: 'Strap-on' }, + { value: 'MUND', label: 'Oral' }, + { value: 'ANUS', label: 'Anal' }, + ]; + function gsGetChecked(prefix) { + return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value); + } + function gsSetChecked(prefix, values) { + GS_TOOLS.forEach(t => { + const el = document.getElementById(prefix + t.value); + if (el) el.checked = (values || []).includes(t.value); + }); + } + + const GAME_SPIELDAUER = [ + { label: 'Sehr kurz' }, + { label: 'Kurz' }, + { label: 'Mittel' }, + { label: 'Lang' }, + { label: 'Sehr lang' }, + ]; + + function populateGameSetSelect() { + const sel = document.getElementById('fGameSetId'); + if (!sel) return; + const cur = sel.value; + sel.innerHTML = ''; + _gameSets.forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; opt.textContent = s.name; + sel.appendChild(opt); + }); + sel.value = cur; + } + + function onGameSetChange() { + const val = document.getElementById('fGameSetId')?.value; + document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none'; + } + + function updateGameSpieldauer(val) { + document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || ''; + } + + function populateTaskSetSelects() { + for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) { + const sel = document.getElementById(selId); + if (!sel) continue; + const cur = sel.value; + sel.innerHTML = ''; + _taskSets.forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; opt.textContent = `${s.name} (${s.tasks.length} Aufgabe${s.tasks.length !== 1 ? 'n' : ''})`; + sel.appendChild(opt); + }); + sel.value = cur; + } + } + + function onTaskSetChange(type) { + const selId = type === 'card' ? 'fCardTaskSetId' : 'fTimelockTaskSetId'; + const previewId = type === 'card' ? 'cardTaskSetPreview' : 'timelockTaskSetPreview'; + const val = document.getElementById(selId)?.value; + const preview = document.getElementById(previewId); + if (!preview) return; + if (!val) { preview.style.display = 'none'; preview.innerHTML = ''; return; } + const set = _taskSets.find(s => s.id === val); + if (!set || !set.tasks.length) { preview.style.display = 'none'; preview.innerHTML = ''; return; } + preview.style.display = ''; + preview.innerHTML = set.tasks.map(t => ` +
+ ${esc(t.title)} + ${t.minutes ? `${t.minutes} Min.` : ''} + ${t.description ? `
${esc(t.description)}
` : ''} +
`).join(''); + } + + // ── Simulation ── + async function runSimulation() { + const cardCountsMin = {}, cardCountsMax = {}; + CARD_DEFS.forEach(c => { + const mn = parseInt(document.getElementById('min_' + c.id)?.value) || 0; + const mx = parseInt(document.getElementById('max_' + c.id)?.value) || 0; + if (mn > 0) cardCountsMin[c.id] = mn; + if (mx > 0) cardCountsMax[c.id] = mx; + }); + + const btn = document.getElementById('simBtn'); + btn.disabled = true; + document.getElementById('simRunning').style.display = ''; + document.getElementById('simResult').style.display = 'none'; + document.getElementById('simProgressBar').style.width = '0%'; + document.getElementById('simProgressText').textContent = '0 von 100'; + + try { + const res = await fetch('/cardlock/templates/simulate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + cardCountsMin, + cardCountsMax, + pickEveryMinute: tpToMinutes('pe'), + accumulatePicks: document.getElementById('fAccumulate').checked + }) + }); + if (!res.ok) return; + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + let pos; + while ((pos = buffer.indexOf('\n\n')) !== -1) { + const chunk = buffer.slice(0, pos); + buffer = buffer.slice(pos + 2); + let eventName = '', data = ''; + for (const line of chunk.split('\n')) { + if (line.startsWith('event:')) eventName = line.slice(6).trim(); + else if (line.startsWith('data:')) data = line.slice(5).trim(); + } + if (eventName === 'progress') { + const p = JSON.parse(data); + document.getElementById('simProgressBar').style.width = (p.done / p.total * 100) + '%'; + document.getElementById('simProgressText').textContent = `${p.done} von ${p.total}`; + } else if (eventName === 'result') { + const r = JSON.parse(data); + document.getElementById('simMin').textContent = fmtMinutes(r.min); + document.getElementById('simAvg').textContent = fmtMinutes(r.avg); + document.getElementById('simMax').textContent = fmtMinutes(r.max); + document.getElementById('simRunning').style.display = 'none'; + document.getElementById('simResult').style.display = ''; + } + } + } + } finally { + btn.disabled = false; + } + } + // ── Fehler ── function clearErr(rowId) { const r = document.getElementById(rowId); r?.classList.remove('field-error'); r?.querySelector('.field-error-msg')?.remove(); } function setErr(rowId, msg) { @@ -826,8 +1386,8 @@ function alignModalToContent() { const rect = document.querySelector('.content')?.getBoundingClientRect(); if (!rect) return; - const box = document.querySelector('.modal-box'); - box.style.width = Math.min(rect.width, 720) + 'px'; + document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 720) + 'px'; + document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px'; } function openModal(template) { @@ -837,11 +1397,10 @@ document.getElementById('modalTitle').textContent = editId ? 'Vorlage bearbeiten' : 'Vorlage erstellen'; document.getElementById('modalError').style.display = 'none'; document.getElementById('modalSaveBtn').disabled = false; - document.getElementById('modalTaskList').innerHTML = ''; document.getElementById('fSpinToggle').checked = false; toggleWheel(false); document.getElementById('errGreen').style.display = 'none'; - taskCtr = 0; wheelCtr = 0; + wheelCtr = 0; // Typ-Auswahl: nur beim Erstellen sichtbar document.getElementById('sectionTypeSelect').style.display = editId ? 'none' : ''; @@ -903,13 +1462,27 @@ toggleHygiene(hygieneOn); if (hygieneOn) { tpFromMinutes('he', template.hygineOpeningEveryMinites); tpFromMinutes('hd', template.hygineOpeningDurationMinutes||30); } - // Aufgaben - (template?.tasks||[]).forEach(t => addTask(t)); + // Task mode const mode = template?.taskMode || template?.taskCardMode || 'RANDOM'; const radioName = type === 'CARDLOCK' ? 'modalCardTaskMode' : 'modalTaskMode'; const radioEl = document.querySelector(`input[name="${radioName}"][value="${mode}"]`); if (radioEl) radioEl.checked = true; - updateTaskModeVisibility(); + + // Aufgaben-Set + populateTaskSetSelects(); + const taskSetId = template?.taskSetId || ''; + document.getElementById('fCardTaskSetId').value = taskSetId; + document.getElementById('fTimelockTaskSetId').value = taskSetId; + onTaskSetChange('card'); + onTaskSetChange('timelock'); + + // Spiel-Set + populateGameSetSelect(); + document.getElementById('fGameSetId').value = template?.gameSetId || ''; + onGameSetChange(); + const sdIdx = template?.gameSpieldauerIdx ?? 2; + document.getElementById('sldGameSpieldauer').value = sdIdx; + updateGameSpieldauer(sdIdx); alignModalToContent(); document.getElementById('modalBackdrop').classList.add('open'); @@ -953,9 +1526,17 @@ document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); }); document.addEventListener('keydown', e => { - if (e.key === 'Escape' && document.getElementById('modalBackdrop').classList.contains('open')) { - e.preventDefault(); - tryCloseModal(); + if (e.key !== 'Escape') return; + if (document.getElementById('gsItemModal').classList.contains('open')) { + e.preventDefault(); closeGsItemModal(); + } else if (document.getElementById('gsSetModal').classList.contains('open')) { + e.preventDefault(); closeGsSetModal(); + } else if (document.getElementById('gsEditModal').classList.contains('open')) { + e.preventDefault(); closeGsEditModal(); + } else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) { + e.preventDefault(); tryCloseTaskSetModal(); + } else if (document.getElementById('modalBackdrop').classList.contains('open')) { + e.preventDefault(); tryCloseModal(); } }); window.addEventListener('resize', () => { if (document.getElementById('modalBackdrop').classList.contains('open')) alignModalToContent(); }); @@ -972,7 +1553,6 @@ if (!name) { setErr('rowName','Name ist ein Pflichtfeld.'); firstError = document.getElementById('rowName'); } else clearErr('rowName'); - const tasks = collectTasks(); const hygieneOn = document.getElementById('fHygieneToggle').checked; const hygieneEvery = hygieneOn ? tpToMinutes('he') : null; const hygieneDur = hygieneOn ? tpToMinutes('hd') : null; @@ -1007,7 +1587,9 @@ const totalMax = CARD_DEFS.reduce((s,c)=>s+(parseInt(document.getElementById('max_'+c.id).value)||0),0); if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); } const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0; - if (hasTaskCards && tasks.length===0) { showModalError('Aufgaben-Karten konfiguriert, aber keine Aufgaben definiert.'); firstError=firstError||document.getElementById('modalError'); } + if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); } + const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0; + if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); } if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; } @@ -1024,8 +1606,11 @@ showRemainingCards: document.getElementById('fShowRemaining').checked, hygineOpeningEveryMinites: hygieneEvery, hygineOpeningDurationMinutes: hygieneDur, - tasks, requiresVerification: document.getElementById('fRequiresVerification').checked, + taskSetId: document.getElementById('fCardTaskSetId').value || null, + requiresVerification: document.getElementById('fRequiresVerification').checked, taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM', + gameSetId: document.getElementById('fGameSetId').value || null, + gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2, }; } else { // TimeLock @@ -1038,7 +1623,7 @@ if (hasTaskTiming) { taskEvery = tpToMinutes('te'); if (taskEvery < 1) { showModalError('Aufgaben-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); } - if (tasks.length === 0) { showModalError('Aufgaben-Timing aktiviert, aber keine Aufgaben definiert.'); firstError=firstError||document.getElementById('modalError'); } + if (!document.getElementById('fTimelockTaskSetId').value) { showModalError('Aufgaben-Timing aktiviert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); } const mt = parseInt(document.getElementById('fMinTasks').value); minTasksPerDay = isNaN(mt)||mt<1 ? null : mt; } @@ -1100,7 +1685,8 @@ endTimeVisible: document.getElementById('fEndTimeVisible').checked, hygineOpeningEveryMinites: hygieneEvery, hygineOpeningDurationMinutes: hygieneDur, - tasks, taskEveryMinutes: taskEvery, minTasksPerDay, + taskSetId: document.getElementById('fTimelockTaskSetId').value || null, + taskEveryMinutes: taskEvery, minTasksPerDay, spinningWheelEntries: wheelEntries, spinsEveryMinutes: spinsEvery, minSpinsPerDay, requiresVerification: document.getElementById('fRequiresVerification').checked, taskMode: document.querySelector('input[name="modalTaskMode"]:checked')?.value||'RANDOM', @@ -1145,7 +1731,8 @@ const hygText = t.hygineOpeningEveryMinites ? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen` : 'Keine'; - const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${t.taskCount ? ' · ' + t.taskCount + ' Aufgabe(n)' : ''}`; + const setName = t.taskSetId ? (_taskSets.find(s => s.id === t.taskSetId)?.name || 'Set') : null; + const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${setName ? ' · Set: ' + esc(setName) : ''}`; const publishedBadge = t.published ? `🌐 Veröffentlicht` : ''; @@ -1194,10 +1781,11 @@ } } - function resetList() { + async function resetList() { pageNum = 0; isLastPage = false; isLoading = false; document.getElementById('templateList').innerHTML = ''; document.getElementById('listEmpty').style.display = 'none'; + await loadTaskSets(); loadNextPage(); loadSubscribedTemplates(); } @@ -1296,6 +1884,10 @@ if (res.ok || res.status === 204) resetList(); } + document.getElementById('taskSetModalBackdrop').addEventListener('click', e => { + if (e.target === e.currentTarget) tryCloseTaskSetModal(); + }); + // ── IntersectionObserver für Infinite Scroll ── const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) loadNextPage(); @@ -1303,6 +1895,426 @@ observer.observe(document.getElementById('scrollSentinel')); resetList(); + loadGameSets(); + + // ════════════════════════════════════════════════ + // Spiel-Sets + // ════════════════════════════════════════════════ + + let _gameSets = []; + let _gsEditSetId = null; // set being renamed + let _gsSetCaller = null; // 'template' when opened from the template modal + let _gsOpenSetId = null; // set currently open in the content popup + let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher' + let _gsItemSetId = null; + let _gsItemIdx = null; // null = new, number = editing + + async function loadGameSets() { + try { + const res = await fetch('/chastity/game-sets'); + if (!res.ok) return; + _gameSets = await res.json(); + renderGameSetList(); + populateGameSetSelect(); + if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId); + } catch (e) { console.error(e); } + } + + function renderGameSetList() { + const list = document.getElementById('gameSetList'); + list.innerHTML = ''; + document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : ''; + document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5; + + _gameSets.forEach(s => { + const aufgaben = s.aufgaben || []; + const zeitstrafen = s.zeitstrafen || []; + const finisher = s.finisher || []; + const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length); + + const lvlBadges = levelCounts.map((c, i) => { + const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge'; + return `L${i+1}: ${c}`; + }).join(''); + const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge'; + + const card = document.createElement('div'); + card.className = 'gs-card'; + card.id = 'gscard_' + s.id; + card.addEventListener('click', () => openGsEditModal(s.id)); + card.innerHTML = ` +
+
+
${esc(s.name)}
+
+ ${lvlBadges} + Zeitstrafen: ${zeitstrafen.length} + Finisher: ${finisher.length} +
+
+
+ + +
+
`; + list.appendChild(card); + }); + } + + function toggleGsListItem(id) { + document.getElementById(id)?.classList.toggle('open'); + } + + // ── Set content popup ────────────────────────── + + function openGsEditModal(setId) { + _gsOpenSetId = setId; + renderGsEditModalContent(setId); + document.getElementById('gsEditModal').classList.add('open'); + } + + function closeGsEditModal() { + document.getElementById('gsEditModal').classList.remove('open'); + _gsOpenSetId = null; + } + + function renderGsEditModalContent(setId) { + const container = document.getElementById('gsEditModalContent'); + if (!container) return; + const s = _gameSets.find(x => x.id === setId); + if (!s) { closeGsEditModal(); return; } + document.getElementById('gsEditModalTitle').textContent = s.name; + const aufgaben = s.aufgaben || []; + const zeitstrafen = s.zeitstrafen || []; + const finisher = s.finisher || []; + let html = ''; + for (let l = 1; l <= 5; l++) { + const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l); + const warnCls = items.length < 3 ? ' gs-sub-warn' : ''; + const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') || + '
'; + html += `
+
+ Level ${l} (${items.length}/3+) + +
+
${itemsHtml}
`; + } + const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') || + '
'; + html += `
+
+ Zeitstrafen (${zeitstrafen.length}) + +
+
${zeitHtml}
`; + const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : ''; + const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') || + '
'; + html += `
+
+ Finisher (${finisher.length}/1+) + +
+
${finHtml}
`; + container.innerHTML = html; + } + + // ── Row HTML helpers ─────────────────────────── + + function gsAufgabeRowHtml(setId, gi, a) { + const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean); + const badges = [ + a.minutes ? `${a.minutes} Min.` : '', + ...toolLabels.map(l => `${l}`), + ].join(''); + const desc = a.description ? `
${esc(a.description)}
` : ''; + return `
+
+ ${esc(a.title)} +
${badges}
+
+
${desc} +
+ + + +
+
`; + } + + function gsZeitstrafeRowHtml(setId, idx, z) { + const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '–' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.'; + const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean); + const badges = [ + z.level ? `L${z.level}` : '', + `${timeStr}`, + ...sperrtLabels.map(l => `🔒 ${l}`), + z.releaseText ? `📝 Aufhebung` : '', + z.tempUnlockBeforeRequired ? `🔓 Vorher` : '', + z.tempUnlockAfterRequired ? `🔓 Nachher` : '', + ].join(''); + const releaseRow = z.releaseText ? `
Bei Aufhebung:
${esc(z.releaseText)}
` : ''; + const desc = z.description ? `
${esc(z.description)}
` : ''; + return `
+
+ ${esc(z.title)} +
${badges}
+
+
${desc}${releaseRow} +
+ + + +
+
`; + } + + function gsFinisherRowHtml(setId, idx, f) { + const badges = [ + f.tempUnlockBeforeRequired ? `🔓 Vorher` : '', + f.tempUnlockAfterRequired ? `🔓 Nachher` : '', + ].join(''); + const desc = f.description ? `
${esc(f.description)}
` : ''; + return `
+
+ ${esc(f.title)} +
${badges}
+
+
${desc} +
+ + + +
+
`; + } + + // ── Set create / rename modal ────────────────── + + function openGsSetModal(id, caller) { + _gsEditSetId = id || null; + _gsSetCaller = caller || null; + document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set'; + document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : ''; + document.getElementById('gsSetError').style.display = 'none'; + document.getElementById('gsSetModal').classList.add('open'); + setTimeout(() => document.getElementById('gsSetName').focus(), 50); + } + + function closeGsSetModal() { + document.getElementById('gsSetModal').classList.remove('open'); + _gsEditSetId = _gsSetCaller = null; + } + + async function saveGsSet() { + const name = document.getElementById('gsSetName').value.trim(); + const errEl = document.getElementById('gsSetError'); + if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; } + errEl.style.display = 'none'; + const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null; + const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets'; + const method = _gsEditSetId ? 'PUT' : 'POST'; + const body = { name, + aufgaben: set?.aufgaben || [], + zeitstrafen: set?.zeitstrafen || [], + finisher: set?.finisher || [] }; + try { + const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }); + if (res.ok) { + const saved = await res.json().catch(() => null); + const caller = _gsSetCaller; + closeGsSetModal(); + await loadGameSets(); + if (caller === 'template' && saved?.id) { + document.getElementById('fGameSetId').value = saved.id; + onGameSetChange(); + markDirty(); + } + return; + } + const b = await res.json().catch(() => ({})); + errEl.textContent = b.error || 'Fehler.'; errEl.style.display = ''; + } catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; } + } + + async function deleteGameSet(id, name) { + if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return; + const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' }); + if (res.ok || res.status === 204) loadGameSets(); + } + + // ── Item modal ───────────────────────────────── + + function openGsItemModal(type, setId, itemIdx, contextLevel) { + _gsItemType = type; + _gsItemSetId = setId; + _gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null; + + const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' }; + document.getElementById('gsItemModalTitle').textContent = + (_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type]; + + // Reset fields + document.getElementById('gsItemTitle').value = ''; + document.getElementById('gsItemDesc').value = ''; + document.getElementById('gsItemMinutes').value = ''; + document.getElementById('gsItemMinMin').value = ''; + document.getElementById('gsItemMaxMin').value = ''; + document.getElementById('gsItemReleaseText').value = ''; + document.getElementById('gsItemBefore').checked = false; + document.getElementById('gsItemAfter').checked = false; + document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1; + document.getElementById('gsItemZeitstrafeLevel').value = 1; + document.getElementById('gsItemError').style.display = 'none'; + gsSetChecked('gsItemBen_', []); + gsSetChecked('gsItemSperr_', []); + + // Show/hide type-specific rows + document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none'; + document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none'; + document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none'; + document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none'; + document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none'; + + // Pre-fill when editing + if (_gsItemIdx !== null) { + const set = _gameSets.find(s => s.id === setId); + if (set) { + let item; + if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx]; + if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx]; + if (type === 'finisher') item = set.finisher[_gsItemIdx]; + if (item) { + document.getElementById('gsItemTitle').value = item.title || ''; + document.getElementById('gsItemDesc').value = item.description || ''; + if (type === 'aufgabe') { + document.getElementById('gsItemAufgabeLevel').value = item.level || 1; + document.getElementById('gsItemMinutes').value = item.minutes || ''; + gsSetChecked('gsItemBen_', item.benoetigt || []); + } + if (type === 'zeitstrafe') { + document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1; + document.getElementById('gsItemMinMin').value = item.minMinutes ?? ''; + document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? ''; + document.getElementById('gsItemReleaseText').value = item.releaseText || ''; + gsSetChecked('gsItemSperr_', item.sperrt || []); + } + if (type === 'zeitstrafe' || type === 'finisher') { + document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired; + document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired; + } + } + } + } + + document.getElementById('gsItemModal').classList.add('open'); + setTimeout(() => document.getElementById('gsItemTitle').focus(), 50); + } + + function closeGsItemModal() { + document.getElementById('gsItemModal').classList.remove('open'); + _gsItemType = _gsItemSetId = _gsItemIdx = null; + } + + async function saveGsItem() { + const title = document.getElementById('gsItemTitle').value.trim(); + const errEl = document.getElementById('gsItemError'); + if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; } + errEl.style.display = 'none'; + + const set = _gameSets.find(s => s.id === _gsItemSetId); + if (!set) return; + const updated = { + name: set.name, + aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])), + zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])), + finisher: JSON.parse(JSON.stringify(set.finisher || [])), + }; + + const desc = document.getElementById('gsItemDesc').value.trim() || null; + let item; + if (_gsItemType === 'aufgabe') { + const min = parseInt(document.getElementById('gsItemMinutes').value); + const ben = gsGetChecked('gsItemBen_'); + item = { title, description: desc, + level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1, + minutes: isNaN(min) ? null : min, + benoetigt: ben.length ? ben : null }; + if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item; + else updated.aufgaben.push(item); + } else if (_gsItemType === 'zeitstrafe') { + const minMin = parseInt(document.getElementById('gsItemMinMin').value); + const maxMin = parseInt(document.getElementById('gsItemMaxMin').value); + const sperrt = gsGetChecked('gsItemSperr_'); + const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null; + item = { title, description: desc, + level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1, + minMinutes: isNaN(minMin) ? null : minMin, + maxMinutes: isNaN(maxMin) ? null : maxMin, + releaseText, + tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked, + tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked, + sperrt: sperrt.length ? sperrt : null }; + if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item; + else updated.zeitstrafen.push(item); + } else if (_gsItemType === 'finisher') { + item = { title, description: desc, + tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked, + tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked }; + if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item; + else updated.finisher.push(item); + } + + try { + const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, { + method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) + }); + if (res.ok) { closeGsItemModal(); await loadGameSets(); } + else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; } + } catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; } + } + + async function deleteGsItem(type, setId, idx) { + if (!confirm('Eintrag wirklich löschen?')) return; + const set = _gameSets.find(s => s.id === setId); + if (!set) return; + const updated = { + name: set.name, + aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])), + zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])), + finisher: JSON.parse(JSON.stringify(set.finisher || [])), + }; + if (type === 'aufgabe') updated.aufgaben.splice(idx, 1); + if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1); + if (type === 'finisher') updated.finisher.splice(idx, 1); + const res = await fetch(`/chastity/game-sets/${setId}`, { + method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) + }); + if (res.ok) loadGameSets(); + } + + async function duplicateGsItem(type, setId, idx) { + const set = _gameSets.find(s => s.id === setId); + if (!set) return; + const updated = { + name: set.name, + aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])), + zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])), + finisher: JSON.parse(JSON.stringify(set.finisher || [])), + }; + if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx]))); + if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx]))); + if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx]))); + const res = await fetch(`/chastity/game-sets/${setId}`, { + method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) + }); + if (res.ok) loadGameSets(); + } + + document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); }); + document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); }); + document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); }); diff --git a/bin/main/static/games/vanilla/aufgaben.html b/bin/main/static/games/vanilla/aufgaben.html index ec355e0..41e2d67 100644 --- a/bin/main/static/games/vanilla/aufgaben.html +++ b/bin/main/static/games/vanilla/aufgaben.html @@ -3,6 +3,7 @@ + Aufgaben – Vanilla – xXx Sphere @@ -629,8 +630,13 @@ .then(user => { if (!user) return; loadUserGruppen(); loadAboGruppen(); loadSystemGruppen(); }) .catch(() => { window.location.href = '/login.html'; }); + // ── Cross-tab notification ── + let _notifyOnLoad = false; + const gruppenBc = new BroadcastChannel('vanilla-gruppen-updated'); + // ── Load ── function loadUserGruppen() { + if (_notifyOnLoad) { _notifyOnLoad = false; try { gruppenBc.postMessage(1); } catch (_) {} } resetSelection(); document.getElementById('userLoading').style.display = 'block'; fetch(apiUrl(`/gruppe/list/user`) + `?page=${userPage}&size=${PAGE_SIZE}`) @@ -924,7 +930,7 @@ openItemId = null; pendingExpandId = gruppenId; pendingExpandType = 'user'; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else { document.getElementById('userActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').'; } @@ -1131,7 +1137,7 @@ pendingExpandType = 'user'; } userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else if (r.status === 409) { showModalError('Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.'); } else { @@ -1157,7 +1163,7 @@ .then(r => { if (r.ok || r.status === 202) { userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else if (r.status === 403) { document.getElementById('userActionError').textContent = 'Keine Berechtigung.'; btn.disabled = false; @@ -1179,7 +1185,7 @@ .then(r => { if (r.ok || r.status === 201) { userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); document.getElementById('systemActionError').textContent = ''; } else { document.getElementById('systemActionError').textContent = 'Fehler beim Kopieren (HTTP ' + r.status + ').'; @@ -1198,7 +1204,7 @@ .then(r => { if (r.ok || r.status === 201) { userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); document.getElementById('aboActionError').textContent = ''; } else if (r.status === 409) { document.getElementById('aboActionError').textContent = 'Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.'; @@ -1628,7 +1634,7 @@ pendingExpandId = currentItemGruppeId; pendingExpandType = 'user'; userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else if (r.status === 409) { showItemError('Limit erreicht: maximal 100 Einträge pro Gruppe möglich.'); } else { @@ -1724,7 +1730,7 @@ pendingExpandId = selectedGruppeId; pendingExpandType = 'user'; userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else { const errEl = document.getElementById('publishError'); errEl.textContent = 'Fehler beim Veröffentlichen (HTTP ' + r.status + ').'; diff --git a/bin/main/static/games/vanilla/entdecken.html b/bin/main/static/games/vanilla/entdecken.html index 9ca94d1..a65f1d3 100644 --- a/bin/main/static/games/vanilla/entdecken.html +++ b/bin/main/static/games/vanilla/entdecken.html @@ -3,6 +3,7 @@ + Entdecken – xXx Sphere diff --git a/bin/main/static/games/vanilla/neuvanilla.html b/bin/main/static/games/vanilla/neuvanilla.html index 27a9699..fc0fa25 100644 --- a/bin/main/static/games/vanilla/neuvanilla.html +++ b/bin/main/static/games/vanilla/neuvanilla.html @@ -69,13 +69,36 @@ .card-field:last-child { margin-bottom: 0; } .card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; } .check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; } - .check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; } - .check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; } + .check-group--two-col { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); } + .check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; position: relative; } .check-item.is-checked { border-color: var(--color-primary); } .check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; } .check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; } - .check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; } - .check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; } + .check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; display: flex; align-items: center; gap: 0.2rem; flex-wrap: wrap; } + .check-item-desc { display: none; } + .check-item-tooltip { + display: none; position: absolute; bottom: calc(100% + 6px); left: 0; + background: var(--color-card); border: 1px solid var(--color-secondary); + border-radius: 6px; padding: 0.4rem 0.65rem; + font-size: 0.78rem; color: var(--color-muted); line-height: 1.4; + width: max-content; max-width: 210px; + z-index: 50; pointer-events: none; + box-shadow: 0 4px 12px rgba(0,0,0,0.35); + } + .check-item:hover .check-item-tooltip { display: block; } + .check-item-info-btn { + display: none; background: none; border: 1px solid var(--color-muted); + border-radius: 50%; width: 1.1rem; height: 1.1rem; font-size: 0.62rem; + color: var(--color-muted); cursor: pointer; padding: 0; line-height: 1; + flex-shrink: 0; font-style: normal; font-weight: normal; + align-items: center; justify-content: center; + } + .check-item-info-btn.active { border-color: var(--color-primary); color: var(--color-primary); } + .check-item-desc-mobile { display: none; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.25rem; line-height: 1.4; } + @media (max-width: 679px) { + .check-item:hover .check-item-tooltip { display: none; } + .check-item-info-btn { display: inline-flex; } + } .field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; } .add-player-btn { width: 100%; background: transparent; border: 1px dashed var(--color-secondary); color: var(--color-muted); padding: 0.7rem; border-radius: 8px; font-size: 0.88rem; font-weight: normal; cursor: pointer; transition: border-color 0.15s, color 0.15s; margin-top: 0.5rem; } .add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; } @@ -163,7 +186,6 @@
📰 Feed & Profil
Beiträge teilen, Profile entdecken und die Community kennenlernen.
🏆 Community Votes
Verifikationen bewerten und an Community-Abstimmungen teilnehmen.
@@ -200,15 +204,16 @@
🔐 Sicherheit & Datenschutz
Wie deine Daten gespeichert werden und welche Sicherheitsmaßnahmen wir treffen.
🐛 Fehler melden
Hast du einen Fehler gefunden oder einen Verbesserungsvorschlag?
diff --git a/bin/main/static/help/vanilla.html b/bin/main/static/help/vanilla.html new file mode 100644 index 0000000..a1fdfad --- /dev/null +++ b/bin/main/static/help/vanilla.html @@ -0,0 +1,163 @@ + + + + + + + Hilfe Vanilla Game – xXx Sphere + + + + + +
+
+ + ‹ Zurück zur Hilfe-Übersicht + +
+

⚪ Vanilla Game

+

Leichtere, verspielte Sessions ohne strenge Regeln – für den entspannten Einstieg.

+
+ +
+
+ 📖 Was ist das Vanilla Game? + +
+
+

+ Das Vanilla Game ist der entspannte Einstieg in die Spielwelt von xXx Sphere. Es gibt keine festen Rollen und keine strikten Regeln – stattdessen ziehen beide Parteien abwechselnd Karten und erfüllen lockere Aufgaben. +

+

+ Das Spiel eignet sich besonders für Paare, die etwas Neues ausprobieren möchten, ohne sich auf ein intensiveres Regelwerk einzulassen. +

+
+ Tipp: Du kannst jederzeit eigene Aufgaben erstellen und den Schwierigkeitsgrad für jede Session selbst bestimmen. +
+
+
+ +
+
+ 🚀 Session starten + +
+
+

So startest du eine Vanilla-Session:

+
    +
  1. 1Navigiere zu Vanilla → Neue Session.
  2. +
  3. 2Wähle einen Aufgaben-Pool (eigene Aufgaben oder Community-Vorlagen).
  4. +
  5. 3Lege fest, ob ihr abwechselnd zieht oder eine Person die Aufgaben stellt.
  6. +
  7. 4Lade deinen Mitspieler per Nutzername oder Einladungslink ein.
  8. +
  9. 5Starte die Session – der erste Spieler zieht die erste Karte.
  10. +
+
+
+ +
+
+ 🃏 Karten und Aufgaben + +
+
+

+ Im Vanilla Game werden Karten aus einem gemeinsam gewählten Pool gezogen. Jede Karte beschreibt eine Aufgabe, die von einer oder beiden Personen erfüllt wird. Nach Erfüllung zieht die andere Person. +

+ + + + + + + +
AufgabentypBeschreibung
SoloNur die ziehende Person führt die Aufgabe aus.
GemeinsamBeide Personen führen die Aufgabe zusammen aus.
WahlDie ziehende Person entscheidet, wer die Aufgabe übernimmt.
+

+ Eigene Aufgaben kannst du unter Vanilla → Aufgaben verwalten. +

+
+
+ +
+
+ ❓ Kann ich eine Session pausieren? + +
+
+

+ Ja. Eine laufende Session kann von beiden Spielern jederzeit pausiert werden. Sie bleibt für 24 Stunden gespeichert und kann danach fortgesetzt werden. Nach 24 Stunden Inaktivität wird die Session automatisch beendet. +

+
+
+ +
+
+ ❓ Unterschied zwischen Vanilla und BDSM Game? + +
+
+

+ Das Vanilla Game hat keine festen Rollen, kein Protokoll und keine Strafmechanismen. Es eignet sich als Einstieg oder für entspannte Abende. Das BDSM Game hat explizite Rollen (Dom/Sub), ein Aufgaben- und Strafprotokoll sowie striktere Regeln. +

+
+ Du kannst beide Spiele unabhängig voneinander nutzen – deine Aufgaben-Sets lassen sich zwischen den Spielen teilen. +
+
+
+ +
+
+ + + + + + diff --git a/bin/main/static/js/card-defs.js b/bin/main/static/js/card-defs.js index 765fc0f..4726869 100644 --- a/bin/main/static/js/card-defs.js +++ b/bin/main/static/js/card-defs.js @@ -78,6 +78,30 @@ const CARD_DEFS = [ defMin: 0, defMax: 0, }, + { + id: 'SLOWMO_CARD', + img: '/img/card_slowmo.png', + name: 'Slow Motion', + desc: 'Alle gestarteten Aktionen (Hygiene-Öffnung, Freeze, Kartenintervall) dauern bis zum gewählten Zeitpunkt viermal so lange.', + defMin: 0, + defMax: 0, + }, + { + id: 'SPEEDUP_CARD', + img: '/img/card_speedup.png', + name: 'Speed Up', + desc: 'Alle gestarteten Aktionen (Hygiene-Öffnung, Freeze, Kartenintervall) dauern bis zum gewählten Zeitpunkt viermal so kurz.', + defMin: 0, + defMax: 0, + }, + { + id: 'GAME_CARD', + img: '/img/card_game.png', + name: 'Spiel-Karte', + desc: 'Ein Minispiel wird gestartet.', + defMin: 0, + defMax: 0, + }, ]; /** Lookup-Objekt für Konsumenten, die nach ID auf Name/Bild/Beschreibung zugreifen. */ diff --git a/bin/main/static/js/mobile-nav.js b/bin/main/static/js/mobile-nav.js index 6e97bd7..03dd981 100644 --- a/bin/main/static/js/mobile-nav.js +++ b/bin/main/static/js/mobile-nav.js @@ -4,7 +4,8 @@ const path = window.location.pathname; const I = window.IC || function () { return ''; }; - const TOPBAR_H = '4.875rem'; + const TOPBAR_H = '4.875rem'; + const BOT_NAV_H = '3.75rem'; // ── CSS ────────────────────────────────────────────────────────────────── const style = document.createElement('style'); @@ -82,7 +83,7 @@ display: none; position: fixed; top: ${TOPBAR_H}; - left: 0; right: 0; bottom: 0; + left: 0; right: 0; bottom: ${BOT_NAV_H}; background: rgba(0,0,0,0.55); z-index: 998; } @@ -93,7 +94,7 @@ position: fixed; top: ${TOPBAR_H}; right: 0; - bottom: 0; + bottom: ${BOT_NAV_H}; width: min(80%, 360px); background: var(--color-card); border-left: 1px solid var(--color-secondary); @@ -155,10 +156,54 @@ .mnav-link--danger { color: var(--color-primary); } .mnav-link--danger:hover { background: rgba(var(--color-primary-rgb,233,69,96),0.1); color: var(--color-primary); } + /* ── Bottom Navigation Bar ── */ + .mob-bottom-nav { + display: none; + position: fixed; + bottom: 0; left: 0; right: 0; + height: ${BOT_NAV_H}; + background: var(--color-card); + border-top: 1px solid var(--color-secondary); + box-shadow: 0 -2px 12px rgba(0,0,0,0.3); + z-index: 500; + align-items: stretch; + } + .mob-bn-tab { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.18rem; + text-decoration: none; + color: var(--color-muted); + font-size: 1.25rem; + line-height: 1; + cursor: pointer; + background: none; + border: none; + padding: 0.25rem 0; + transition: color 0.12s; + -webkit-tap-highlight-color: transparent; + } + .mob-bn-tab.active { color: var(--color-primary); } + .mob-bn-tab:hover { color: var(--color-text); } + .mob-bn-label { + font-size: 0.6rem; + font-weight: 600; + line-height: 1; + letter-spacing: 0.01em; + } + /* ── Show only on mobile ── */ @media (max-width: 768px) { .mobile-topbar { display: flex; } - body.app { padding-top: ${TOPBAR_H}; } + .mob-bottom-nav { display: flex; } + body.app { + padding-top: ${TOPBAR_H}; + padding-bottom: ${BOT_NAV_H}; + } + #mobMenuToggle { display: none; } } `; document.head.appendChild(style); @@ -322,6 +367,35 @@ document.body.appendChild(backdropEl); document.body.appendChild(panelEl); + // ── Bottom Navigation Bar ───────────────────────────────────────────────── + const onGames = path.startsWith('/games/'); + const bottomNavEl = document.createElement('nav'); + bottomNavEl.className = 'mob-bottom-nav'; + bottomNavEl.id = 'mobBottomNav'; + bottomNavEl.innerHTML = ` + + ${I('HOME') || '🏠'} + Home + + + ${I('GROUPS') || '👥'} + Community + + + ${I('DATING') || '♥'} + Dating + + + + ${I('SETTINGS') || '⚙️'} + Konto + + `; + document.body.appendChild(bottomNavEl); + // ── Accordion (nur eine Sektion gleichzeitig offen) ────────────────────── panelEl.querySelectorAll('.mnav-section-header').forEach(h => { h.addEventListener('click', () => { @@ -333,7 +407,13 @@ }); // ── Open / Close ────────────────────────────────────────────────────────── - function openMenu() { + function openMenu(focusSectionLabel) { + if (focusSectionLabel) { + panelEl.querySelectorAll('.mnav-section').forEach(s => { + const label = s.querySelector('.mnav-section-header span')?.textContent?.trim(); + s.classList.toggle('open', label === focusSectionLabel); + }); + } panelEl.classList.add('open'); backdropEl.classList.add('open'); } @@ -352,6 +432,22 @@ l.addEventListener('click', () => { if (l.getAttribute('href') !== '#') closeMenu(); }); }); + // ── Games-Tab öffnet Panel mit Games-Sektionen ──────────────────────────── + document.getElementById('mobBnGamesTab').addEventListener('click', e => { + e.stopPropagation(); + if (panelEl.classList.contains('open')) { + closeMenu(); + } else { + // Ersten aktiven Game-Bereich öffnen, sonst Vanilla Game als Default + const gameLabels = ['Vanilla Game', 'BDSM Game', 'Chastity Game']; + const activeLabel = gameLabels.find(lbl => { + const prefix = { 'Vanilla Game': '/games/vanilla/', 'BDSM Game': '/games/bdsm/', 'Chastity Game': '/games/chastity/' }[lbl]; + return path.startsWith(prefix); + }) || 'Vanilla Game'; + openMenu(activeLabel); + } + }); + // ── Badges ──────────────────────────────────────────────────────────────── function setBadge(id, n) { const el = document.getElementById(id); @@ -406,13 +502,14 @@ if (el) el.style.display = ''; } - // Dating + // Dating (Panel-Link + Bottom-Nav-Tab) + const datingHref = user.datingAktiv + ? '/dating/dating.html' + : '/konto/einstellungen.html#sec-dating'; const datingLink = document.getElementById('mnavDatingLink'); - if (datingLink) { - datingLink.href = user.datingAktiv - ? '/dating/dating.html' - : '/konto/einstellungen.html#sec-dating'; - } + if (datingLink) datingLink.href = datingHref; + const bnDatingTab = document.getElementById('mobBnDatingTab'); + if (bnDatingTab) bnDatingTab.href = datingHref; const hide = id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }; const show = id => { const el = document.getElementById(id); if (el) el.style.display = ''; }; diff --git a/bin/main/static/js/nav.js b/bin/main/static/js/nav.js index 1a44fb6..fac9c88 100644 --- a/bin/main/static/js/nav.js +++ b/bin/main/static/js/nav.js @@ -16,9 +16,9 @@ margin-right: 0.5rem; line-height: 1; } - .nav-burger:hover { border-color: var(--color-primary); color: var(--color-primary); } + .nav-burger:hover { border-color: var(--color-primary); color: #fff; } .nav-burger-icon { - font-size: 1.05rem; line-height: 1; + font-size: 1.575rem; line-height: 1; position: relative; display: inline-flex; align-items: center; justify-content: center; width: 1.2em; height: 1.2em; @@ -91,16 +91,16 @@ } .nav-col:last-child { border-right: none; } - /* Überschrift: auf Desktop ausgeblendet, auf Mobile als Accordion-Toggle */ .nav-col-header { - display: none; + display: flex; align-items: center; justify-content: space-between; - padding: 0.75rem 1.1rem; - font-size: 0.85rem; font-weight: 600; + padding: 0.75rem 1.1rem 0.5rem; + font-size: 1.275rem; font-weight: 700; color: var(--color-text); - cursor: pointer; + cursor: default; + border-bottom: 1px solid var(--color-secondary); } - .nav-col-arrow { font-size: 0.65rem; transition: transform 0.2s; } + .nav-col-arrow { display: none; font-size: 0.65rem; transition: transform 0.2s; } .nav-col-body { padding: 0.35rem 0; } @@ -158,7 +158,8 @@ .nav-col { border-right: none; border-bottom: 1px solid var(--color-secondary); } .nav-col:last-child { border-bottom: none; } - .nav-col-header { display: flex; } + .nav-col-header { font-size: 0.85rem; font-weight: 600; cursor: pointer; padding: 0.75rem 1.1rem; border-bottom: none; } + .nav-col-arrow { display: block; } .nav-col.col-open .nav-col-arrow { transform: rotate(90deg); } .nav-col-body { display: none; padding: 0; } @@ -256,23 +257,18 @@ ${link('/dating/matches.html', '', 'Matches' )} `; + const bdsmActive = ['/games/bdsm/neubdsm.html', '/games/bdsm/bdsmingame.html', '/games/bdsm/bdsmplayers.html'].some(p => path.startsWith(p)) ? ' active' : ''; + const vanillaActive = ['/games/vanilla/neuvanilla.html', '/games/vanilla/vanillaingame.html', '/games/vanilla/vanillawarten.html'].some(p => path.startsWith(p)) ? ' active' : ''; + const col4Html = ` - ${gameGroup('VANILLA', 'Vanilla Game', [ - { href: '/games/vanilla/neuvanilla.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navVanillaNeu' }, - { href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navVanillaAktiv' }, - { href: '/games/vanilla/vanillaingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navVanillaImSpiel' }, - { href: '/games/vanilla/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' }, - { href: '/games/vanilla/toys.html', icon: 'TOYS', label: 'Toys' }, - { href: '/games/vanilla/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' }, - ])} - ${gameGroup('BDSM', 'BDSM Game', [ - { href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navBdsmNeu' }, - { href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navBdsmAktiv' }, - { href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navBdsmImSpiel' }, - { href: '/games/bdsm/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' }, - { href: '/games/bdsm/toys.html', icon: 'TOYS', label: 'Toys' }, - { href: '/games/bdsm/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' }, - ])} + + ${I('VANILLA') || ''} + Vanilla Game + + + ${I('BDSM') || ''} + BDSM Game + ${gameGroup('CHASTITY', 'Chastity Game', [ { href: '/games/chastity/neulock.html', icon: 'NEW_LOCK', label: 'Neues Lock', id: 'navChastityNeu' }, { href: '#', icon: 'ACTIVE_LOCK', label: 'Aktives Lock', id: 'navChastityAktiv' }, @@ -283,6 +279,11 @@ { href: '/games/chastity/keyholder.html', icon: 'KEY', label: 'Keyholder' }, { href: '/games/chastity/unlock-history.html', icon: 'HISTORY', label: 'Code-Historie' }, ])} + ${gameGroup('CHECK', 'Aufgabenverwaltung', [ + { href: '/games/aufgaben/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' }, + { href: '/games/aufgaben/toys.html', icon: 'TOYS', label: 'Toys' }, + { href: '/games/aufgaben/entdecken.html',icon: 'DISCOVER', label: 'Entdecken' }, + ])} `; // ── Dropdown-HTML ──────────────────────────────────────────────────────── @@ -306,7 +307,7 @@ ])} ${column('colDating', 'Dating', col3Html, ['/dating/'])} ${column('colGames', 'Games', col4Html, [ - '/games/vanilla/', '/games/bdsm/', '/games/chastity/', + '/games/vanilla/', '/games/bdsm/', '/games/chastity/', '/games/aufgaben/', ])} - - +
+ + +
+ +
+
Simulation
+

Simuliert 100 Durchläufe mit der aktuellen Konfiguration und zeigt die erwartete Sperrdauer. Die Simulation basiert auf dem Idealfall, dass jede Karte sofort gezogen wird, sobald sie verfügbar ist – die tatsächliche Sperrdauer wird in der Praxis höher ausfallen.

+ + + +
+
+ + + + + + + + + + + + @@ -555,8 +839,14 @@ let editId = null; let editType = null; // 'CARDLOCK' | 'TIMELOCK' – beim Bearbeiten fest let isDirty = false; - let taskCtr = 0; let wheelCtr = 0; + + // ── Aufgaben-Sets ── + let _taskSets = []; + let _taskSetEditId = null; + let _taskSetTaskCtr = 0; + let _taskSetCallerType = null; // 'card' | 'timelock' | null + let _taskSetIsDirty = false; let pageNum = 0; let isLastPage = false; let isLoading = false; @@ -605,7 +895,7 @@ const type = currentModalType(); document.getElementById('sectionCardlock').style.display = type === 'CARDLOCK' ? '' : 'none'; document.getElementById('sectionTimelock').style.display = type === 'TIMELOCK' ? '' : 'none'; - updateTaskModeVisibility(); + document.getElementById('simSection').style.display = type === 'CARDLOCK' ? '' : 'none'; } // ── Karten-Grid ── @@ -767,46 +1057,316 @@ document.getElementById('rowPenaltyValue').style.display = needsVal ? '' : 'none'; } - // ── Aufgaben ── - function addTask(data) { - const id = ++taskCtr; - const titleVal = (data?.title||data?.text||'').replace(/"/g,'"'); - const descVal = (data?.description||'').replace(//g,'>'); + // ── Aufgaben-Sets: Seite ── + async function loadTaskSets() { + try { + const res = await fetch('/chastity/task-sets'); + if (!res.ok) return; + _taskSets = await res.json(); + renderTaskSetList(); + populateTaskSetSelects(); + } catch(e) { console.error(e); } + } + + function renderTaskSetList() { + const list = document.getElementById('taskSetList'); + list.innerHTML = ''; + if (!_taskSets.length) { document.getElementById('taskSetEmpty').style.display = ''; return; } + document.getElementById('taskSetEmpty').style.display = 'none'; + _taskSets.forEach(s => appendTaskSetCard(s)); + } + + function appendTaskSetCard(s) { + const list = document.getElementById('taskSetList'); + const card = document.createElement('div'); + card.className = 'template-card'; + card.style.cursor = 'pointer'; + const preview = s.tasks.length + ? s.tasks.slice(0,3).map(t => esc(t.title)).join(', ') + (s.tasks.length > 3 ? ' …' : '') + : 'Keine Aufgaben'; + card.innerHTML = ` +
+
+ 📋 +
+
+
${esc(s.name)}
+
${s.tasks.length} Aufgabe(n): ${preview}
+
+
+ +
+
`; + card.addEventListener('click', () => openTaskSetModal(s.id)); + list.appendChild(card); + } + + // ── Aufgaben-Sets: Modal ── + function openTaskSetModal(id, callerType) { + _taskSetEditId = id || null; + _taskSetCallerType = callerType || null; + _taskSetTaskCtr = 0; + document.getElementById('taskSetTaskList').innerHTML = ''; + document.getElementById('taskSetError').style.display = 'none'; + document.getElementById('taskSetModalTitle').textContent = id ? 'Aufgaben-Set bearbeiten' : 'Aufgaben-Set erstellen'; + if (id) { + const set = _taskSets.find(s => s.id === id); + if (set) { document.getElementById('fTaskSetName').value = set.name; (set.tasks||[]).forEach(t => addTaskSetTask(t)); } + } else { + document.getElementById('fTaskSetName').value = ''; + } + document.getElementById('taskSetDiscardConfirm').style.display = 'none'; + alignModalToContent(); + document.getElementById('taskSetModalBackdrop').classList.add('open'); + _taskSetIsDirty = false; + setTimeout(() => { + document.getElementById('taskSetModalBackdrop').querySelectorAll('input, textarea, select').forEach(el => { + el.addEventListener('input', () => { _taskSetIsDirty = true; }, { passive: true }); + el.addEventListener('change', () => { _taskSetIsDirty = true; }, { passive: true }); + }); + }, 0); + document.getElementById('fTaskSetName').focus(); + } + + function tryCloseTaskSetModal() { + if (_taskSetIsDirty) { + const confirm = document.getElementById('taskSetDiscardConfirm'); + confirm.style.display = 'flex'; + confirm.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } else { + closeTaskSetModal(); + } + } + + function cancelTaskSetDiscard() { + document.getElementById('taskSetDiscardConfirm').style.display = 'none'; + } + + function closeTaskSetModal() { + document.getElementById('taskSetModalBackdrop').classList.remove('open'); + document.getElementById('taskSetDiscardConfirm').style.display = 'none'; + _taskSetIsDirty = false; + _taskSetEditId = null; _taskSetCallerType = null; + } + + function toggleTaskAccItem(id) { + const target = document.getElementById('ts-' + id); + if (!target) return; + const isOpen = target.classList.contains('is-open'); + document.querySelectorAll('#taskSetTaskList .task-acc-item').forEach(el => el.classList.remove('is-open')); + if (!isOpen) target.classList.add('is-open'); + } + + function addTaskSetTask(data) { + const id = ++_taskSetTaskCtr; + const titleVal = (data?.title || '').replace(/"/g, '"'); + const descVal = (data?.description || '').replace(//g, '>'); const minVal = data?.minutes != null ? data.minutes : ''; const div = document.createElement('div'); - div.className = 'task-item'; div.id = 'mt-' + id; + div.className = 'task-acc-item'; div.id = 'ts-' + id; div.innerHTML = ` -
-
Titel *
-
-
Minuten
-
- +
+ + +
- `; - const containerId = currentModalType() === 'CARDLOCK' ? 'modalCardTaskList' : 'modalTaskList'; - document.getElementById(containerId).appendChild(div); - updateTaskModeVisibility(); +
+
+ + +
+
+ + +
+
`; + document.getElementById('taskSetTaskList').appendChild(div); + _taskSetIsDirty = true; } - function removeTask(id) { document.getElementById('mt-'+id)?.remove(); updateTaskModeVisibility(); } - function updateTaskModeVisibility() { - const type = currentModalType(); - if (type === 'CARDLOCK') { - const hasCardTasks = document.querySelectorAll('#modalCardTaskList .task-item').length > 0; - document.getElementById('sectionCardTaskMode').style.display = hasCardTasks ? '' : 'none'; - } - // For TimeLock: sectionTaskMode is always visible when taskTimingFields is open - } - function collectTasks() { - return Array.from(document.querySelectorAll('.task-item')).map(item => { - const id = item.id.replace('mt-',''); - const title = document.getElementById('mt-title-'+id)?.value.trim(); - const desc = document.getElementById('mt-desc-' +id)?.value.trim(); - const mins = parseInt(document.getElementById('mt-min-' +id)?.value); + + function removeTaskSetTask(id) { document.getElementById('ts-'+id)?.remove(); _taskSetIsDirty = true; } + + function collectTaskSetTasks() { + return Array.from(document.querySelectorAll('#taskSetTaskList .task-acc-item')).map(item => { + const id = item.id.replace('ts-',''); + const title = document.getElementById('ts-title-'+id)?.value.trim(); + const desc = document.getElementById('ts-desc-' +id)?.value.trim(); + const mins = parseInt(document.getElementById('ts-min-' +id)?.value); return title ? { title, description: desc||null, minutes: isNaN(mins)?null:mins } : null; }).filter(Boolean); } + async function saveTaskSet() { + const name = document.getElementById('fTaskSetName').value.trim(); + const errEl = document.getElementById('taskSetError'); + if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; } + const tasks = collectTaskSetTasks(); + const url = _taskSetEditId ? `/chastity/task-sets/${_taskSetEditId}` : '/chastity/task-sets'; + const method = _taskSetEditId ? 'PUT' : 'POST'; + try { + const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body:JSON.stringify({name, tasks}) }); + if (!res.ok) { errEl.textContent = 'Fehler beim Speichern.'; errEl.style.display = ''; return; } + const saved = await res.json(); + const caller = _taskSetCallerType; + closeTaskSetModal(); + await loadTaskSets(); + if (caller) { + const sel = document.getElementById(caller === 'card' ? 'fCardTaskSetId' : 'fTimelockTaskSetId'); + if (sel) { sel.value = saved.id; onTaskSetChange(caller); markDirty(); } + } + } catch(e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; } + } + + async function deleteTaskSet(id, name) { + if (!confirm(`Aufgaben-Set „${name}" wirklich löschen?`)) return; + const res = await fetch(`/chastity/task-sets/${id}`, { method:'DELETE' }); + if (res.ok || res.status === 204) await loadTaskSets(); + } + + const GS_TOOLS = [ + { value: 'UMSCHNALLDILDO', label: 'Strap-on' }, + { value: 'MUND', label: 'Oral' }, + { value: 'ANUS', label: 'Anal' }, + ]; + function gsGetChecked(prefix) { + return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value); + } + function gsSetChecked(prefix, values) { + GS_TOOLS.forEach(t => { + const el = document.getElementById(prefix + t.value); + if (el) el.checked = (values || []).includes(t.value); + }); + } + + const GAME_SPIELDAUER = [ + { label: 'Sehr kurz' }, + { label: 'Kurz' }, + { label: 'Mittel' }, + { label: 'Lang' }, + { label: 'Sehr lang' }, + ]; + + function populateGameSetSelect() { + const sel = document.getElementById('fGameSetId'); + if (!sel) return; + const cur = sel.value; + sel.innerHTML = ''; + _gameSets.forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; opt.textContent = s.name; + sel.appendChild(opt); + }); + sel.value = cur; + } + + function onGameSetChange() { + const val = document.getElementById('fGameSetId')?.value; + document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none'; + } + + function updateGameSpieldauer(val) { + document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || ''; + } + + function populateTaskSetSelects() { + for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) { + const sel = document.getElementById(selId); + if (!sel) continue; + const cur = sel.value; + sel.innerHTML = ''; + _taskSets.forEach(s => { + const opt = document.createElement('option'); + opt.value = s.id; opt.textContent = `${s.name} (${s.tasks.length} Aufgabe${s.tasks.length !== 1 ? 'n' : ''})`; + sel.appendChild(opt); + }); + sel.value = cur; + } + } + + function onTaskSetChange(type) { + const selId = type === 'card' ? 'fCardTaskSetId' : 'fTimelockTaskSetId'; + const previewId = type === 'card' ? 'cardTaskSetPreview' : 'timelockTaskSetPreview'; + const val = document.getElementById(selId)?.value; + const preview = document.getElementById(previewId); + if (!preview) return; + if (!val) { preview.style.display = 'none'; preview.innerHTML = ''; return; } + const set = _taskSets.find(s => s.id === val); + if (!set || !set.tasks.length) { preview.style.display = 'none'; preview.innerHTML = ''; return; } + preview.style.display = ''; + preview.innerHTML = set.tasks.map(t => ` +
+ ${esc(t.title)} + ${t.minutes ? `${t.minutes} Min.` : ''} + ${t.description ? `
${esc(t.description)}
` : ''} +
`).join(''); + } + + // ── Simulation ── + async function runSimulation() { + const cardCountsMin = {}, cardCountsMax = {}; + CARD_DEFS.forEach(c => { + const mn = parseInt(document.getElementById('min_' + c.id)?.value) || 0; + const mx = parseInt(document.getElementById('max_' + c.id)?.value) || 0; + if (mn > 0) cardCountsMin[c.id] = mn; + if (mx > 0) cardCountsMax[c.id] = mx; + }); + + const btn = document.getElementById('simBtn'); + btn.disabled = true; + document.getElementById('simRunning').style.display = ''; + document.getElementById('simResult').style.display = 'none'; + document.getElementById('simProgressBar').style.width = '0%'; + document.getElementById('simProgressText').textContent = '0 von 100'; + + try { + const res = await fetch('/cardlock/templates/simulate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + cardCountsMin, + cardCountsMax, + pickEveryMinute: tpToMinutes('pe'), + accumulatePicks: document.getElementById('fAccumulate').checked + }) + }); + if (!res.ok) return; + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + let pos; + while ((pos = buffer.indexOf('\n\n')) !== -1) { + const chunk = buffer.slice(0, pos); + buffer = buffer.slice(pos + 2); + let eventName = '', data = ''; + for (const line of chunk.split('\n')) { + if (line.startsWith('event:')) eventName = line.slice(6).trim(); + else if (line.startsWith('data:')) data = line.slice(5).trim(); + } + if (eventName === 'progress') { + const p = JSON.parse(data); + document.getElementById('simProgressBar').style.width = (p.done / p.total * 100) + '%'; + document.getElementById('simProgressText').textContent = `${p.done} von ${p.total}`; + } else if (eventName === 'result') { + const r = JSON.parse(data); + document.getElementById('simMin').textContent = fmtMinutes(r.min); + document.getElementById('simAvg').textContent = fmtMinutes(r.avg); + document.getElementById('simMax').textContent = fmtMinutes(r.max); + document.getElementById('simRunning').style.display = 'none'; + document.getElementById('simResult').style.display = ''; + } + } + } + } finally { + btn.disabled = false; + } + } + // ── Fehler ── function clearErr(rowId) { const r = document.getElementById(rowId); r?.classList.remove('field-error'); r?.querySelector('.field-error-msg')?.remove(); } function setErr(rowId, msg) { @@ -826,8 +1386,8 @@ function alignModalToContent() { const rect = document.querySelector('.content')?.getBoundingClientRect(); if (!rect) return; - const box = document.querySelector('.modal-box'); - box.style.width = Math.min(rect.width, 720) + 'px'; + document.getElementById('modalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 720) + 'px'; + document.getElementById('taskSetModalBackdrop').querySelector('.modal-box').style.width = Math.min(rect.width, 900) + 'px'; } function openModal(template) { @@ -837,11 +1397,10 @@ document.getElementById('modalTitle').textContent = editId ? 'Vorlage bearbeiten' : 'Vorlage erstellen'; document.getElementById('modalError').style.display = 'none'; document.getElementById('modalSaveBtn').disabled = false; - document.getElementById('modalTaskList').innerHTML = ''; document.getElementById('fSpinToggle').checked = false; toggleWheel(false); document.getElementById('errGreen').style.display = 'none'; - taskCtr = 0; wheelCtr = 0; + wheelCtr = 0; // Typ-Auswahl: nur beim Erstellen sichtbar document.getElementById('sectionTypeSelect').style.display = editId ? 'none' : ''; @@ -903,13 +1462,27 @@ toggleHygiene(hygieneOn); if (hygieneOn) { tpFromMinutes('he', template.hygineOpeningEveryMinites); tpFromMinutes('hd', template.hygineOpeningDurationMinutes||30); } - // Aufgaben - (template?.tasks||[]).forEach(t => addTask(t)); + // Task mode const mode = template?.taskMode || template?.taskCardMode || 'RANDOM'; const radioName = type === 'CARDLOCK' ? 'modalCardTaskMode' : 'modalTaskMode'; const radioEl = document.querySelector(`input[name="${radioName}"][value="${mode}"]`); if (radioEl) radioEl.checked = true; - updateTaskModeVisibility(); + + // Aufgaben-Set + populateTaskSetSelects(); + const taskSetId = template?.taskSetId || ''; + document.getElementById('fCardTaskSetId').value = taskSetId; + document.getElementById('fTimelockTaskSetId').value = taskSetId; + onTaskSetChange('card'); + onTaskSetChange('timelock'); + + // Spiel-Set + populateGameSetSelect(); + document.getElementById('fGameSetId').value = template?.gameSetId || ''; + onGameSetChange(); + const sdIdx = template?.gameSpieldauerIdx ?? 2; + document.getElementById('sldGameSpieldauer').value = sdIdx; + updateGameSpieldauer(sdIdx); alignModalToContent(); document.getElementById('modalBackdrop').classList.add('open'); @@ -953,9 +1526,17 @@ document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); }); document.addEventListener('keydown', e => { - if (e.key === 'Escape' && document.getElementById('modalBackdrop').classList.contains('open')) { - e.preventDefault(); - tryCloseModal(); + if (e.key !== 'Escape') return; + if (document.getElementById('gsItemModal').classList.contains('open')) { + e.preventDefault(); closeGsItemModal(); + } else if (document.getElementById('gsSetModal').classList.contains('open')) { + e.preventDefault(); closeGsSetModal(); + } else if (document.getElementById('gsEditModal').classList.contains('open')) { + e.preventDefault(); closeGsEditModal(); + } else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) { + e.preventDefault(); tryCloseTaskSetModal(); + } else if (document.getElementById('modalBackdrop').classList.contains('open')) { + e.preventDefault(); tryCloseModal(); } }); window.addEventListener('resize', () => { if (document.getElementById('modalBackdrop').classList.contains('open')) alignModalToContent(); }); @@ -972,7 +1553,6 @@ if (!name) { setErr('rowName','Name ist ein Pflichtfeld.'); firstError = document.getElementById('rowName'); } else clearErr('rowName'); - const tasks = collectTasks(); const hygieneOn = document.getElementById('fHygieneToggle').checked; const hygieneEvery = hygieneOn ? tpToMinutes('he') : null; const hygieneDur = hygieneOn ? tpToMinutes('hd') : null; @@ -1007,7 +1587,9 @@ const totalMax = CARD_DEFS.reduce((s,c)=>s+(parseInt(document.getElementById('max_'+c.id).value)||0),0); if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); } const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0; - if (hasTaskCards && tasks.length===0) { showModalError('Aufgaben-Karten konfiguriert, aber keine Aufgaben definiert.'); firstError=firstError||document.getElementById('modalError'); } + if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); } + const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0; + if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); } if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; } @@ -1024,8 +1606,11 @@ showRemainingCards: document.getElementById('fShowRemaining').checked, hygineOpeningEveryMinites: hygieneEvery, hygineOpeningDurationMinutes: hygieneDur, - tasks, requiresVerification: document.getElementById('fRequiresVerification').checked, + taskSetId: document.getElementById('fCardTaskSetId').value || null, + requiresVerification: document.getElementById('fRequiresVerification').checked, taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM', + gameSetId: document.getElementById('fGameSetId').value || null, + gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2, }; } else { // TimeLock @@ -1038,7 +1623,7 @@ if (hasTaskTiming) { taskEvery = tpToMinutes('te'); if (taskEvery < 1) { showModalError('Aufgaben-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); } - if (tasks.length === 0) { showModalError('Aufgaben-Timing aktiviert, aber keine Aufgaben definiert.'); firstError=firstError||document.getElementById('modalError'); } + if (!document.getElementById('fTimelockTaskSetId').value) { showModalError('Aufgaben-Timing aktiviert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); } const mt = parseInt(document.getElementById('fMinTasks').value); minTasksPerDay = isNaN(mt)||mt<1 ? null : mt; } @@ -1100,7 +1685,8 @@ endTimeVisible: document.getElementById('fEndTimeVisible').checked, hygineOpeningEveryMinites: hygieneEvery, hygineOpeningDurationMinutes: hygieneDur, - tasks, taskEveryMinutes: taskEvery, minTasksPerDay, + taskSetId: document.getElementById('fTimelockTaskSetId').value || null, + taskEveryMinutes: taskEvery, minTasksPerDay, spinningWheelEntries: wheelEntries, spinsEveryMinutes: spinsEvery, minSpinsPerDay, requiresVerification: document.getElementById('fRequiresVerification').checked, taskMode: document.querySelector('input[name="modalTaskMode"]:checked')?.value||'RANDOM', @@ -1145,7 +1731,8 @@ const hygText = t.hygineOpeningEveryMinites ? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen` : 'Keine'; - const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${t.taskCount ? ' · ' + t.taskCount + ' Aufgabe(n)' : ''}`; + const setName = t.taskSetId ? (_taskSets.find(s => s.id === t.taskSetId)?.name || 'Set') : null; + const metaLine = `Hygiene: ${hygText} · Verif.: ${t.requiresVerification ? 'Ja' : 'Nein'}${setName ? ' · Set: ' + esc(setName) : ''}`; const publishedBadge = t.published ? `🌐 Veröffentlicht` : ''; @@ -1194,10 +1781,11 @@ } } - function resetList() { + async function resetList() { pageNum = 0; isLastPage = false; isLoading = false; document.getElementById('templateList').innerHTML = ''; document.getElementById('listEmpty').style.display = 'none'; + await loadTaskSets(); loadNextPage(); loadSubscribedTemplates(); } @@ -1296,6 +1884,10 @@ if (res.ok || res.status === 204) resetList(); } + document.getElementById('taskSetModalBackdrop').addEventListener('click', e => { + if (e.target === e.currentTarget) tryCloseTaskSetModal(); + }); + // ── IntersectionObserver für Infinite Scroll ── const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) loadNextPage(); @@ -1303,6 +1895,426 @@ observer.observe(document.getElementById('scrollSentinel')); resetList(); + loadGameSets(); + + // ════════════════════════════════════════════════ + // Spiel-Sets + // ════════════════════════════════════════════════ + + let _gameSets = []; + let _gsEditSetId = null; // set being renamed + let _gsSetCaller = null; // 'template' when opened from the template modal + let _gsOpenSetId = null; // set currently open in the content popup + let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher' + let _gsItemSetId = null; + let _gsItemIdx = null; // null = new, number = editing + + async function loadGameSets() { + try { + const res = await fetch('/chastity/game-sets'); + if (!res.ok) return; + _gameSets = await res.json(); + renderGameSetList(); + populateGameSetSelect(); + if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId); + } catch (e) { console.error(e); } + } + + function renderGameSetList() { + const list = document.getElementById('gameSetList'); + list.innerHTML = ''; + document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : ''; + document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5; + + _gameSets.forEach(s => { + const aufgaben = s.aufgaben || []; + const zeitstrafen = s.zeitstrafen || []; + const finisher = s.finisher || []; + const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length); + + const lvlBadges = levelCounts.map((c, i) => { + const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge'; + return `L${i+1}: ${c}`; + }).join(''); + const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge'; + + const card = document.createElement('div'); + card.className = 'gs-card'; + card.id = 'gscard_' + s.id; + card.addEventListener('click', () => openGsEditModal(s.id)); + card.innerHTML = ` +
+
+
${esc(s.name)}
+
+ ${lvlBadges} + Zeitstrafen: ${zeitstrafen.length} + Finisher: ${finisher.length} +
+
+
+ + +
+
`; + list.appendChild(card); + }); + } + + function toggleGsListItem(id) { + document.getElementById(id)?.classList.toggle('open'); + } + + // ── Set content popup ────────────────────────── + + function openGsEditModal(setId) { + _gsOpenSetId = setId; + renderGsEditModalContent(setId); + document.getElementById('gsEditModal').classList.add('open'); + } + + function closeGsEditModal() { + document.getElementById('gsEditModal').classList.remove('open'); + _gsOpenSetId = null; + } + + function renderGsEditModalContent(setId) { + const container = document.getElementById('gsEditModalContent'); + if (!container) return; + const s = _gameSets.find(x => x.id === setId); + if (!s) { closeGsEditModal(); return; } + document.getElementById('gsEditModalTitle').textContent = s.name; + const aufgaben = s.aufgaben || []; + const zeitstrafen = s.zeitstrafen || []; + const finisher = s.finisher || []; + let html = ''; + for (let l = 1; l <= 5; l++) { + const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l); + const warnCls = items.length < 3 ? ' gs-sub-warn' : ''; + const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') || + '
'; + html += `
+
+ Level ${l} (${items.length}/3+) + +
+
${itemsHtml}
`; + } + const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') || + '
'; + html += `
+
+ Zeitstrafen (${zeitstrafen.length}) + +
+
${zeitHtml}
`; + const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : ''; + const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') || + '
'; + html += `
+
+ Finisher (${finisher.length}/1+) + +
+
${finHtml}
`; + container.innerHTML = html; + } + + // ── Row HTML helpers ─────────────────────────── + + function gsAufgabeRowHtml(setId, gi, a) { + const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean); + const badges = [ + a.minutes ? `${a.minutes} Min.` : '', + ...toolLabels.map(l => `${l}`), + ].join(''); + const desc = a.description ? `
${esc(a.description)}
` : ''; + return `
+
+ ${esc(a.title)} +
${badges}
+
+
${desc} +
+ + + +
+
`; + } + + function gsZeitstrafeRowHtml(setId, idx, z) { + const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '–' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.'; + const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean); + const badges = [ + z.level ? `L${z.level}` : '', + `${timeStr}`, + ...sperrtLabels.map(l => `🔒 ${l}`), + z.releaseText ? `📝 Aufhebung` : '', + z.tempUnlockBeforeRequired ? `🔓 Vorher` : '', + z.tempUnlockAfterRequired ? `🔓 Nachher` : '', + ].join(''); + const releaseRow = z.releaseText ? `
Bei Aufhebung:
${esc(z.releaseText)}
` : ''; + const desc = z.description ? `
${esc(z.description)}
` : ''; + return `
+
+ ${esc(z.title)} +
${badges}
+
+
${desc}${releaseRow} +
+ + + +
+
`; + } + + function gsFinisherRowHtml(setId, idx, f) { + const badges = [ + f.tempUnlockBeforeRequired ? `🔓 Vorher` : '', + f.tempUnlockAfterRequired ? `🔓 Nachher` : '', + ].join(''); + const desc = f.description ? `
${esc(f.description)}
` : ''; + return `
+
+ ${esc(f.title)} +
${badges}
+
+
${desc} +
+ + + +
+
`; + } + + // ── Set create / rename modal ────────────────── + + function openGsSetModal(id, caller) { + _gsEditSetId = id || null; + _gsSetCaller = caller || null; + document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set'; + document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : ''; + document.getElementById('gsSetError').style.display = 'none'; + document.getElementById('gsSetModal').classList.add('open'); + setTimeout(() => document.getElementById('gsSetName').focus(), 50); + } + + function closeGsSetModal() { + document.getElementById('gsSetModal').classList.remove('open'); + _gsEditSetId = _gsSetCaller = null; + } + + async function saveGsSet() { + const name = document.getElementById('gsSetName').value.trim(); + const errEl = document.getElementById('gsSetError'); + if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; } + errEl.style.display = 'none'; + const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null; + const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets'; + const method = _gsEditSetId ? 'PUT' : 'POST'; + const body = { name, + aufgaben: set?.aufgaben || [], + zeitstrafen: set?.zeitstrafen || [], + finisher: set?.finisher || [] }; + try { + const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }); + if (res.ok) { + const saved = await res.json().catch(() => null); + const caller = _gsSetCaller; + closeGsSetModal(); + await loadGameSets(); + if (caller === 'template' && saved?.id) { + document.getElementById('fGameSetId').value = saved.id; + onGameSetChange(); + markDirty(); + } + return; + } + const b = await res.json().catch(() => ({})); + errEl.textContent = b.error || 'Fehler.'; errEl.style.display = ''; + } catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; } + } + + async function deleteGameSet(id, name) { + if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return; + const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' }); + if (res.ok || res.status === 204) loadGameSets(); + } + + // ── Item modal ───────────────────────────────── + + function openGsItemModal(type, setId, itemIdx, contextLevel) { + _gsItemType = type; + _gsItemSetId = setId; + _gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null; + + const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' }; + document.getElementById('gsItemModalTitle').textContent = + (_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type]; + + // Reset fields + document.getElementById('gsItemTitle').value = ''; + document.getElementById('gsItemDesc').value = ''; + document.getElementById('gsItemMinutes').value = ''; + document.getElementById('gsItemMinMin').value = ''; + document.getElementById('gsItemMaxMin').value = ''; + document.getElementById('gsItemReleaseText').value = ''; + document.getElementById('gsItemBefore').checked = false; + document.getElementById('gsItemAfter').checked = false; + document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1; + document.getElementById('gsItemZeitstrafeLevel').value = 1; + document.getElementById('gsItemError').style.display = 'none'; + gsSetChecked('gsItemBen_', []); + gsSetChecked('gsItemSperr_', []); + + // Show/hide type-specific rows + document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none'; + document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none'; + document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none'; + document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none'; + document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none'; + + // Pre-fill when editing + if (_gsItemIdx !== null) { + const set = _gameSets.find(s => s.id === setId); + if (set) { + let item; + if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx]; + if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx]; + if (type === 'finisher') item = set.finisher[_gsItemIdx]; + if (item) { + document.getElementById('gsItemTitle').value = item.title || ''; + document.getElementById('gsItemDesc').value = item.description || ''; + if (type === 'aufgabe') { + document.getElementById('gsItemAufgabeLevel').value = item.level || 1; + document.getElementById('gsItemMinutes').value = item.minutes || ''; + gsSetChecked('gsItemBen_', item.benoetigt || []); + } + if (type === 'zeitstrafe') { + document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1; + document.getElementById('gsItemMinMin').value = item.minMinutes ?? ''; + document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? ''; + document.getElementById('gsItemReleaseText').value = item.releaseText || ''; + gsSetChecked('gsItemSperr_', item.sperrt || []); + } + if (type === 'zeitstrafe' || type === 'finisher') { + document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired; + document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired; + } + } + } + } + + document.getElementById('gsItemModal').classList.add('open'); + setTimeout(() => document.getElementById('gsItemTitle').focus(), 50); + } + + function closeGsItemModal() { + document.getElementById('gsItemModal').classList.remove('open'); + _gsItemType = _gsItemSetId = _gsItemIdx = null; + } + + async function saveGsItem() { + const title = document.getElementById('gsItemTitle').value.trim(); + const errEl = document.getElementById('gsItemError'); + if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; } + errEl.style.display = 'none'; + + const set = _gameSets.find(s => s.id === _gsItemSetId); + if (!set) return; + const updated = { + name: set.name, + aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])), + zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])), + finisher: JSON.parse(JSON.stringify(set.finisher || [])), + }; + + const desc = document.getElementById('gsItemDesc').value.trim() || null; + let item; + if (_gsItemType === 'aufgabe') { + const min = parseInt(document.getElementById('gsItemMinutes').value); + const ben = gsGetChecked('gsItemBen_'); + item = { title, description: desc, + level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1, + minutes: isNaN(min) ? null : min, + benoetigt: ben.length ? ben : null }; + if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item; + else updated.aufgaben.push(item); + } else if (_gsItemType === 'zeitstrafe') { + const minMin = parseInt(document.getElementById('gsItemMinMin').value); + const maxMin = parseInt(document.getElementById('gsItemMaxMin').value); + const sperrt = gsGetChecked('gsItemSperr_'); + const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null; + item = { title, description: desc, + level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1, + minMinutes: isNaN(minMin) ? null : minMin, + maxMinutes: isNaN(maxMin) ? null : maxMin, + releaseText, + tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked, + tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked, + sperrt: sperrt.length ? sperrt : null }; + if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item; + else updated.zeitstrafen.push(item); + } else if (_gsItemType === 'finisher') { + item = { title, description: desc, + tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked, + tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked }; + if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item; + else updated.finisher.push(item); + } + + try { + const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, { + method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) + }); + if (res.ok) { closeGsItemModal(); await loadGameSets(); } + else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; } + } catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; } + } + + async function deleteGsItem(type, setId, idx) { + if (!confirm('Eintrag wirklich löschen?')) return; + const set = _gameSets.find(s => s.id === setId); + if (!set) return; + const updated = { + name: set.name, + aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])), + zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])), + finisher: JSON.parse(JSON.stringify(set.finisher || [])), + }; + if (type === 'aufgabe') updated.aufgaben.splice(idx, 1); + if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1); + if (type === 'finisher') updated.finisher.splice(idx, 1); + const res = await fetch(`/chastity/game-sets/${setId}`, { + method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) + }); + if (res.ok) loadGameSets(); + } + + async function duplicateGsItem(type, setId, idx) { + const set = _gameSets.find(s => s.id === setId); + if (!set) return; + const updated = { + name: set.name, + aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])), + zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])), + finisher: JSON.parse(JSON.stringify(set.finisher || [])), + }; + if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx]))); + if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx]))); + if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx]))); + const res = await fetch(`/chastity/game-sets/${setId}`, { + method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated) + }); + if (res.ok) loadGameSets(); + } + + document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); }); + document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); }); + document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); }); diff --git a/src/main/resources/static/games/vanilla/aufgaben.html b/src/main/resources/static/games/vanilla/aufgaben.html index ec355e0..41e2d67 100644 --- a/src/main/resources/static/games/vanilla/aufgaben.html +++ b/src/main/resources/static/games/vanilla/aufgaben.html @@ -3,6 +3,7 @@ + Aufgaben – Vanilla – xXx Sphere @@ -629,8 +630,13 @@ .then(user => { if (!user) return; loadUserGruppen(); loadAboGruppen(); loadSystemGruppen(); }) .catch(() => { window.location.href = '/login.html'; }); + // ── Cross-tab notification ── + let _notifyOnLoad = false; + const gruppenBc = new BroadcastChannel('vanilla-gruppen-updated'); + // ── Load ── function loadUserGruppen() { + if (_notifyOnLoad) { _notifyOnLoad = false; try { gruppenBc.postMessage(1); } catch (_) {} } resetSelection(); document.getElementById('userLoading').style.display = 'block'; fetch(apiUrl(`/gruppe/list/user`) + `?page=${userPage}&size=${PAGE_SIZE}`) @@ -924,7 +930,7 @@ openItemId = null; pendingExpandId = gruppenId; pendingExpandType = 'user'; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else { document.getElementById('userActionError').textContent = 'Fehler beim Löschen (HTTP ' + r.status + ').'; } @@ -1131,7 +1137,7 @@ pendingExpandType = 'user'; } userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else if (r.status === 409) { showModalError('Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.'); } else { @@ -1157,7 +1163,7 @@ .then(r => { if (r.ok || r.status === 202) { userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else if (r.status === 403) { document.getElementById('userActionError').textContent = 'Keine Berechtigung.'; btn.disabled = false; @@ -1179,7 +1185,7 @@ .then(r => { if (r.ok || r.status === 201) { userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); document.getElementById('systemActionError').textContent = ''; } else { document.getElementById('systemActionError').textContent = 'Fehler beim Kopieren (HTTP ' + r.status + ').'; @@ -1198,7 +1204,7 @@ .then(r => { if (r.ok || r.status === 201) { userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); document.getElementById('aboActionError').textContent = ''; } else if (r.status === 409) { document.getElementById('aboActionError').textContent = 'Limit erreicht: maximal 10 eigene Aufgabengruppen möglich.'; @@ -1628,7 +1634,7 @@ pendingExpandId = currentItemGruppeId; pendingExpandType = 'user'; userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else if (r.status === 409) { showItemError('Limit erreicht: maximal 100 Einträge pro Gruppe möglich.'); } else { @@ -1724,7 +1730,7 @@ pendingExpandId = selectedGruppeId; pendingExpandType = 'user'; userPage = 0; - loadUserGruppen(); + _notifyOnLoad = true; loadUserGruppen(); } else { const errEl = document.getElementById('publishError'); errEl.textContent = 'Fehler beim Veröffentlichen (HTTP ' + r.status + ').'; diff --git a/src/main/resources/static/games/vanilla/entdecken.html b/src/main/resources/static/games/vanilla/entdecken.html index 9ca94d1..a65f1d3 100644 --- a/src/main/resources/static/games/vanilla/entdecken.html +++ b/src/main/resources/static/games/vanilla/entdecken.html @@ -3,6 +3,7 @@ + Entdecken – xXx Sphere diff --git a/src/main/resources/static/games/vanilla/neuvanilla.html b/src/main/resources/static/games/vanilla/neuvanilla.html index 27a9699..fc0fa25 100644 --- a/src/main/resources/static/games/vanilla/neuvanilla.html +++ b/src/main/resources/static/games/vanilla/neuvanilla.html @@ -69,13 +69,36 @@ .card-field:last-child { margin-bottom: 0; } .card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; } .check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; } - .check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; } - .check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; } + .check-group--two-col { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); } + .check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; position: relative; } .check-item.is-checked { border-color: var(--color-primary); } .check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; } .check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; } - .check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; } - .check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; } + .check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; display: flex; align-items: center; gap: 0.2rem; flex-wrap: wrap; } + .check-item-desc { display: none; } + .check-item-tooltip { + display: none; position: absolute; bottom: calc(100% + 6px); left: 0; + background: var(--color-card); border: 1px solid var(--color-secondary); + border-radius: 6px; padding: 0.4rem 0.65rem; + font-size: 0.78rem; color: var(--color-muted); line-height: 1.4; + width: max-content; max-width: 210px; + z-index: 50; pointer-events: none; + box-shadow: 0 4px 12px rgba(0,0,0,0.35); + } + .check-item:hover .check-item-tooltip { display: block; } + .check-item-info-btn { + display: none; background: none; border: 1px solid var(--color-muted); + border-radius: 50%; width: 1.1rem; height: 1.1rem; font-size: 0.62rem; + color: var(--color-muted); cursor: pointer; padding: 0; line-height: 1; + flex-shrink: 0; font-style: normal; font-weight: normal; + align-items: center; justify-content: center; + } + .check-item-info-btn.active { border-color: var(--color-primary); color: var(--color-primary); } + .check-item-desc-mobile { display: none; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.25rem; line-height: 1.4; } + @media (max-width: 679px) { + .check-item:hover .check-item-tooltip { display: none; } + .check-item-info-btn { display: inline-flex; } + } .field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; } .add-player-btn { width: 100%; background: transparent; border: 1px dashed var(--color-secondary); color: var(--color-muted); padding: 0.7rem; border-radius: 8px; font-size: 0.88rem; font-weight: normal; cursor: pointer; transition: border-color 0.15s, color 0.15s; margin-top: 0.5rem; } .add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; } @@ -163,7 +186,6 @@
📰 Feed & Profil
Beiträge teilen, Profile entdecken und die Community kennenlernen.
🏆 Community Votes
Verifikationen bewerten und an Community-Abstimmungen teilnehmen.
@@ -200,15 +204,16 @@
🔐 Sicherheit & Datenschutz
Wie deine Daten gespeichert werden und welche Sicherheitsmaßnahmen wir treffen.
🐛 Fehler melden
Hast du einen Fehler gefunden oder einen Verbesserungsvorschlag?
diff --git a/src/main/resources/static/help/vanilla.html b/src/main/resources/static/help/vanilla.html new file mode 100644 index 0000000..a1fdfad --- /dev/null +++ b/src/main/resources/static/help/vanilla.html @@ -0,0 +1,163 @@ + + + + + + + Hilfe Vanilla Game – xXx Sphere + + + + + +
+
+ + ‹ Zurück zur Hilfe-Übersicht + +
+

⚪ Vanilla Game

+

Leichtere, verspielte Sessions ohne strenge Regeln – für den entspannten Einstieg.

+
+ +
+
+ 📖 Was ist das Vanilla Game? + +
+
+

+ Das Vanilla Game ist der entspannte Einstieg in die Spielwelt von xXx Sphere. Es gibt keine festen Rollen und keine strikten Regeln – stattdessen ziehen beide Parteien abwechselnd Karten und erfüllen lockere Aufgaben. +

+

+ Das Spiel eignet sich besonders für Paare, die etwas Neues ausprobieren möchten, ohne sich auf ein intensiveres Regelwerk einzulassen. +

+
+ Tipp: Du kannst jederzeit eigene Aufgaben erstellen und den Schwierigkeitsgrad für jede Session selbst bestimmen. +
+
+
+ +
+
+ 🚀 Session starten + +
+
+

So startest du eine Vanilla-Session:

+
    +
  1. 1Navigiere zu Vanilla → Neue Session.
  2. +
  3. 2Wähle einen Aufgaben-Pool (eigene Aufgaben oder Community-Vorlagen).
  4. +
  5. 3Lege fest, ob ihr abwechselnd zieht oder eine Person die Aufgaben stellt.
  6. +
  7. 4Lade deinen Mitspieler per Nutzername oder Einladungslink ein.
  8. +
  9. 5Starte die Session – der erste Spieler zieht die erste Karte.
  10. +
+
+
+ +
+
+ 🃏 Karten und Aufgaben + +
+
+

+ Im Vanilla Game werden Karten aus einem gemeinsam gewählten Pool gezogen. Jede Karte beschreibt eine Aufgabe, die von einer oder beiden Personen erfüllt wird. Nach Erfüllung zieht die andere Person. +

+ + + + + + + +
AufgabentypBeschreibung
SoloNur die ziehende Person führt die Aufgabe aus.
GemeinsamBeide Personen führen die Aufgabe zusammen aus.
WahlDie ziehende Person entscheidet, wer die Aufgabe übernimmt.
+

+ Eigene Aufgaben kannst du unter Vanilla → Aufgaben verwalten. +

+
+
+ +
+
+ ❓ Kann ich eine Session pausieren? + +
+
+

+ Ja. Eine laufende Session kann von beiden Spielern jederzeit pausiert werden. Sie bleibt für 24 Stunden gespeichert und kann danach fortgesetzt werden. Nach 24 Stunden Inaktivität wird die Session automatisch beendet. +

+
+
+ +
+
+ ❓ Unterschied zwischen Vanilla und BDSM Game? + +
+
+

+ Das Vanilla Game hat keine festen Rollen, kein Protokoll und keine Strafmechanismen. Es eignet sich als Einstieg oder für entspannte Abende. Das BDSM Game hat explizite Rollen (Dom/Sub), ein Aufgaben- und Strafprotokoll sowie striktere Regeln. +

+
+ Du kannst beide Spiele unabhängig voneinander nutzen – deine Aufgaben-Sets lassen sich zwischen den Spielen teilen. +
+
+
+ +
+
+ + + + + + diff --git a/src/main/resources/static/js/card-defs.js b/src/main/resources/static/js/card-defs.js index 765fc0f..4726869 100644 --- a/src/main/resources/static/js/card-defs.js +++ b/src/main/resources/static/js/card-defs.js @@ -78,6 +78,30 @@ const CARD_DEFS = [ defMin: 0, defMax: 0, }, + { + id: 'SLOWMO_CARD', + img: '/img/card_slowmo.png', + name: 'Slow Motion', + desc: 'Alle gestarteten Aktionen (Hygiene-Öffnung, Freeze, Kartenintervall) dauern bis zum gewählten Zeitpunkt viermal so lange.', + defMin: 0, + defMax: 0, + }, + { + id: 'SPEEDUP_CARD', + img: '/img/card_speedup.png', + name: 'Speed Up', + desc: 'Alle gestarteten Aktionen (Hygiene-Öffnung, Freeze, Kartenintervall) dauern bis zum gewählten Zeitpunkt viermal so kurz.', + defMin: 0, + defMax: 0, + }, + { + id: 'GAME_CARD', + img: '/img/card_game.png', + name: 'Spiel-Karte', + desc: 'Ein Minispiel wird gestartet.', + defMin: 0, + defMax: 0, + }, ]; /** Lookup-Objekt für Konsumenten, die nach ID auf Name/Bild/Beschreibung zugreifen. */ diff --git a/src/main/resources/static/js/nav.js b/src/main/resources/static/js/nav.js index 1a44fb6..fac9c88 100644 --- a/src/main/resources/static/js/nav.js +++ b/src/main/resources/static/js/nav.js @@ -16,9 +16,9 @@ margin-right: 0.5rem; line-height: 1; } - .nav-burger:hover { border-color: var(--color-primary); color: var(--color-primary); } + .nav-burger:hover { border-color: var(--color-primary); color: #fff; } .nav-burger-icon { - font-size: 1.05rem; line-height: 1; + font-size: 1.575rem; line-height: 1; position: relative; display: inline-flex; align-items: center; justify-content: center; width: 1.2em; height: 1.2em; @@ -91,16 +91,16 @@ } .nav-col:last-child { border-right: none; } - /* Überschrift: auf Desktop ausgeblendet, auf Mobile als Accordion-Toggle */ .nav-col-header { - display: none; + display: flex; align-items: center; justify-content: space-between; - padding: 0.75rem 1.1rem; - font-size: 0.85rem; font-weight: 600; + padding: 0.75rem 1.1rem 0.5rem; + font-size: 1.275rem; font-weight: 700; color: var(--color-text); - cursor: pointer; + cursor: default; + border-bottom: 1px solid var(--color-secondary); } - .nav-col-arrow { font-size: 0.65rem; transition: transform 0.2s; } + .nav-col-arrow { display: none; font-size: 0.65rem; transition: transform 0.2s; } .nav-col-body { padding: 0.35rem 0; } @@ -158,7 +158,8 @@ .nav-col { border-right: none; border-bottom: 1px solid var(--color-secondary); } .nav-col:last-child { border-bottom: none; } - .nav-col-header { display: flex; } + .nav-col-header { font-size: 0.85rem; font-weight: 600; cursor: pointer; padding: 0.75rem 1.1rem; border-bottom: none; } + .nav-col-arrow { display: block; } .nav-col.col-open .nav-col-arrow { transform: rotate(90deg); } .nav-col-body { display: none; padding: 0; } @@ -256,23 +257,18 @@ ${link('/dating/matches.html', '', 'Matches' )} `; + const bdsmActive = ['/games/bdsm/neubdsm.html', '/games/bdsm/bdsmingame.html', '/games/bdsm/bdsmplayers.html'].some(p => path.startsWith(p)) ? ' active' : ''; + const vanillaActive = ['/games/vanilla/neuvanilla.html', '/games/vanilla/vanillaingame.html', '/games/vanilla/vanillawarten.html'].some(p => path.startsWith(p)) ? ' active' : ''; + const col4Html = ` - ${gameGroup('VANILLA', 'Vanilla Game', [ - { href: '/games/vanilla/neuvanilla.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navVanillaNeu' }, - { href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navVanillaAktiv' }, - { href: '/games/vanilla/vanillaingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navVanillaImSpiel' }, - { href: '/games/vanilla/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' }, - { href: '/games/vanilla/toys.html', icon: 'TOYS', label: 'Toys' }, - { href: '/games/vanilla/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' }, - ])} - ${gameGroup('BDSM', 'BDSM Game', [ - { href: '/games/bdsm/neubdsm.html', icon: 'PLAY_NEW', label: 'Neue Session', id: 'navBdsmNeu' }, - { href: '#', icon: 'WAITING', label: 'Aktive Session', id: 'navBdsmAktiv' }, - { href: '/games/bdsm/bdsmingame.html', icon: 'PLAY_ACTIVE', label: 'Im Spiel', id: 'navBdsmImSpiel' }, - { href: '/games/bdsm/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' }, - { href: '/games/bdsm/toys.html', icon: 'TOYS', label: 'Toys' }, - { href: '/games/bdsm/entdecken.html', icon: 'DISCOVER', label: 'Entdecken' }, - ])} + + ${I('VANILLA') || ''} + Vanilla Game + + + ${I('BDSM') || ''} + BDSM Game + ${gameGroup('CHASTITY', 'Chastity Game', [ { href: '/games/chastity/neulock.html', icon: 'NEW_LOCK', label: 'Neues Lock', id: 'navChastityNeu' }, { href: '#', icon: 'ACTIVE_LOCK', label: 'Aktives Lock', id: 'navChastityAktiv' }, @@ -283,6 +279,11 @@ { href: '/games/chastity/keyholder.html', icon: 'KEY', label: 'Keyholder' }, { href: '/games/chastity/unlock-history.html', icon: 'HISTORY', label: 'Code-Historie' }, ])} + ${gameGroup('CHECK', 'Aufgabenverwaltung', [ + { href: '/games/aufgaben/aufgaben.html', icon: 'CHECK', label: 'Aufgaben' }, + { href: '/games/aufgaben/toys.html', icon: 'TOYS', label: 'Toys' }, + { href: '/games/aufgaben/entdecken.html',icon: 'DISCOVER', label: 'Entdecken' }, + ])} `; // ── Dropdown-HTML ──────────────────────────────────────────────────────── @@ -306,7 +307,7 @@ ])} ${column('colDating', 'Dating', col3Html, ['/dating/'])} ${column('colGames', 'Games', col4Html, [ - '/games/vanilla/', '/games/bdsm/', '/games/chastity/', + '/games/vanilla/', '/games/bdsm/', '/games/chastity/', '/games/aufgaben/', ])}