Änderungen am Msessage System, Datenschutz-Einstellungen hinzugefügt, BDSM und CardLock Game weiterverfeinert

This commit is contained in:
2026-03-18 22:35:21 +01:00
parent d22b87a79b
commit 655cdad796
106 changed files with 7156 additions and 3686 deletions

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.aufgaben;
import de.oaa.xxx.session.GeschlechtEnum;
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import lombok.Getter;
import lombok.Setter;

View File

@@ -67,9 +67,9 @@ public class AufgabeEntity {
public Aufgabe toAufgabe() {
Aufgabe aufgabe = new Aufgabe();
aufgabe.setAufgabeId(aufgabeId);
aufgabe.setBenoetigtAktiv(benoetigtAktiv);
aufgabe.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList());
aufgabe.setBenoetigtPassiv(benoetigtPassiv);
aufgabe.setBenoetigtAktiv(benoetigtAktiv != null ? new ArrayList<>(benoetigtAktiv) : new ArrayList<>());
aufgabe.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
aufgabe.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>());
aufgabe.setGruppeId(aufgabenGruppe.getGruppenId());
aufgabe.setKurzText(kurzText);
aufgabe.setLevel(level);

View File

@@ -18,7 +18,7 @@ import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "aufgabenGruppe")
@Table(name = "aufgaben_gruppe")
public class AufgabenGruppeEntity {
@Id

View File

@@ -2,7 +2,7 @@ package de.oaa.xxx.aufgaben.entity;
import de.oaa.xxx.aufgaben.Finisher;
import de.oaa.xxx.aufgaben.Werkzeug;
import de.oaa.xxx.session.GeschlechtEnum;
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import jakarta.persistence.CascadeType;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
@@ -67,9 +67,9 @@ public class FinisherEntity {
finisher.setKurzText(kurzText);
finisher.setText(text);
finisher.setGeschlecht(geschlecht);
finisher.setBenoetigtAktiv(benoetigtAktiv);
finisher.setBenoetigtPassiv(benoetigtPassiv);
finisher.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList());
finisher.setBenoetigtAktiv(benoetigtAktiv != null ? new ArrayList<>(benoetigtAktiv) : new ArrayList<>());
finisher.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>());
finisher.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
finisher.setGruppeId(aufgabenGruppe.getGruppenId());
return finisher;
}

View File

@@ -67,7 +67,7 @@ public class SperreEntity {
sperre.setMinutenBis(minutenBis);
sperre.setMinutenVon(minutenVon);
sperre.setReleaseText(releaseText);
sperre.setSperreFuer(sperreFuer);
sperre.setSperreFuer(sperreFuer != null ? new ArrayList<>(sperreFuer) : new ArrayList<>());
sperre.setText(text);
sperre.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
return sperre;

View File

@@ -67,9 +67,9 @@ public class StrafeEntity {
public Strafe toStrafe() {
Strafe strafe = new Strafe();
strafe.setStrafeId(strafeId);
strafe.setBenoetigtAktiv(benoetigtAktiv);
strafe.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList());
strafe.setBenoetigtPassiv(benoetigtPassiv);
strafe.setBenoetigtAktiv(benoetigtAktiv != null ? new ArrayList<>(benoetigtAktiv) : new ArrayList<>());
strafe.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
strafe.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>());
strafe.setGruppeId(aufgabenGruppe.getGruppenId());
strafe.setKurzText(kurzText);
strafe.setLevel(level);

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
import lombok.Getter;
import lombok.Setter;

View File

@@ -1,21 +1,25 @@
package de.oaa.xxx.session;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AufgabeAnzeige {
private String nameAktiverMitspieler;
private String aufgabeText;
private Integer timer;
private Callback callback;
private Integer level;
@Override
public String toString() {
return "AufgabeAnzeige[mitspieler=" + nameAktiverMitspieler + ", level=" + level + ", timer=" + timer
+ ", callback=" + (callback != null ? callback.getClass().getSimpleName() : null) + "]";
}
}
package de.oaa.xxx.games.bdsm;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
public class AufgabeAnzeige {
private String nameAktiverMitspieler;
private String aufgabeText;
private Integer timer;
private Callback callback;
private Integer level;
private UUID mitspielerId;
private boolean eigenesGeraet;
@Override
public String toString() {
return "AufgabeAnzeige[mitspieler=" + nameAktiverMitspieler + ", level=" + level + ", timer=" + timer
+ ", callback=" + (callback != null ? callback.getClass().getSimpleName() : null) + "]";
}
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
public enum AufgabeArt {
AUFGABE,

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
import lombok.Getter;
import lombok.Setter;
@@ -8,10 +8,11 @@ import java.util.UUID;
@Getter
@Setter
public class Session {
public class BdsmGame {
private UUID sessionId;
private UUID userId;
private UUID setupId;
private Integer wahrscheinlichkeitSperre;
private Integer wahrscheinlichkeitStrafe;
private Integer aufgabenProLevel;

View File

@@ -1,251 +1,265 @@
package de.oaa.xxx.session;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.session.aufgaben.Aufgabe;
import de.oaa.xxx.session.aufgaben.AufgabenList;
import de.oaa.xxx.session.aufgaben.Sperre;
import de.oaa.xxx.session.aufgaben.Strafe;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.sperre.SperreCallback;
import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback;
public class SessionDurchfuehren {
private final AufgabenList aufgabenList;
private final List<Mitspieler> mitspieler = new ArrayList<>();
private final List<AktiveSperre> aktiveSperren = new ArrayList<>();
private final Integer wahrscheinlichkeitSperre;
private final Integer wahrscheinlichkeitStrafe;
private int aufgabenProLevel;
private int level;
private int aufgabenAufAktuellemLevel;
public SessionDurchfuehren(SessionEntity entity) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class);
entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler()));
entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler)));
wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre();
wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe();
this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5;
this.level = entity.getLevel() != null ? entity.getLevel() : 1;
this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0;
}
public AufgabeAnzeige getNext() {
checkLevel();
if (level == 6) {
return null;
}
AufgabeAnzeige anzeige = null;
int nextInt = new Random().nextInt(1, 100);
if (nextInt == 1) {
anzeige = findUltimativeStrafe();
} else if (nextInt == 2) {
anzeige = findSperreVerlaengern();
} else if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
anzeige = findeAufgabe();
} else if (nextInt > wahrscheinlichkeitSperre + 2) {
anzeige = findeStrafe();
} else {
anzeige = findeSperre();
}
if (anzeige == null) {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
String text = "Ups, da ist etwas schief gelaufen. Keine potenzielle Aufgabe gefunden. Entweder seid ihr inzwischen so gut weggesperrt, dass wirklich keine Aufgaben mehr zur Verfügung stehen, oder uns ist ein Fehler unterlaufen. {AKTIV} und {PASSIV} überbrücken die Zeit mit ein wenig Petting.";
anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
anzeige.setTimer(120);
}
return anzeige;
}
public void backToLvl5() {
this.level = 5;
this.aufgabenAufAktuellemLevel = 0;
}
public List<AufgabeAnzeige> getFinisher() {
var list = new ArrayList<AufgabeAnzeige>();
List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> {
mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> {
var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming);
var finishers = aufgabenList.getFinisher().stream()
.filter(finisher -> geschlecht == finisher.getGeschlecht())
.toList();
if (!finishers.isEmpty()) {
var aufgabe = finishers.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(cumming.getName());
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(),
cumming.getName(), partner != null ? partner.getName() : ""));
list.add(anzeige);
}
});
});
return list;
}
private void checkLevel() {
if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
aufgabenAufAktuellemLevel = 0;
level++;
}
}
private AufgabeAnzeige findUltimativeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
String text = "{AKTIV}, verschnüre {PASSIV} fachmännisch inkl. KG, Plugs, Knebel, Augenbinde und was dir sonst einfällt. Nutze die Ruhe für was auch immer du möchtest.";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setTimer(new Random().nextInt(1800, 7200));
return anzeige;
}
}
return findeStrafe();
}
private AufgabeAnzeige findSperreVerlaengern() {
if (!aktiveSperren.isEmpty()) {
AktiveSperre sperre = aktiveSperren.get(new Random().nextInt(aktiveSperren.size()));
Mitspieler passiv = sperre.getMitspieler();
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV, passiv);
if (aktiv != null) {
String text = "{AKTIV}, du entscheidest. Sollen alle bestehenden Zeitstrafen von {PASSIV} verlängert werden...?";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setNameAktiverMitspieler(aktiv.getName());
SperrenVerlaengernCallback callback = new SperrenVerlaengernCallback();
callback.setFaktor(new Random().nextInt(2, 4));
callback.setSpielerId(passiv.getId());
anzeige.setCallback(callback);
return anzeige;
}
}
return findeSperre();
}
private AufgabeAnzeige findeAufgabe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
if (passiv != null) {
List<Aufgabe> list = aufgabenList.getAufgaben().stream()
.filter(aufgabe -> aufgabe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Aufgabe aufgabe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), aktiv.getName(), passiv.getName()));
if (aufgabe.getSekundenVon() != null) {
if (aufgabe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis()));
} else {
anzeige.setTimer(aufgabe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeStrafe();
}
private AufgabeAnzeige findeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Strafe> list = aufgabenList.getStrafen().stream()
.filter(strafe -> strafe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Strafe strafe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(strafe.getText(), aktiv.getName(), passiv.getName()));
if (strafe.getSekundenVon() != null) {
if (strafe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(strafe.getSekundenVon(), strafe.getSekundenBis()));
} else {
anzeige.setTimer(strafe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeSperre();
}
private AufgabeAnzeige findeSperre() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Sperre> list = aufgabenList.getSperren().stream()
.filter(sperre -> sperre.isAufgabePassend(passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Sperre sperre = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
anzeige.setAufgabeText(getAnzeigeText(sperre.getText(), aktiv.getName(), passiv.getName()));
SperreCallback callback = new SperreCallback();
callback.setSperreId(sperre.getSperreId());
callback.setSpielerId(passiv.getId());
callback.setReleaseText(getAnzeigeText(sperre.getReleaseText(), aktiv.getName(), passiv.getName()));
anzeige.setCallback(callback);
return anzeige;
}
}
}
return null;
}
private String getAnzeigeText(String textMitPlatzhaltern, String nameAktiv, String namePassiv) {
return textMitPlatzhaltern.replace("{AKTIV}", nameAktiv).replace("{PASSIV}", namePassiv);
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle) {
List<Mitspieler> list = mitspieler.stream()
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle, Mitspieler gegenspieler) {
if (gegenspieler == null) return findeMitspielerMitRolle(rolle);
List<Mitspieler> list = mitspieler.stream()
.filter(m -> m != gegenspieler)
.filter(m -> m.isPassenderSpielpartner(gegenspieler))
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
public int getAufgabenAufAktuellemLevel() {
return aufgabenAufAktuellemLevel;
}
public int getLevel() {
return level;
}
}
package de.oaa.xxx.games.bdsm;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.games.bdsm.aufgaben.Aufgabe;
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.games.bdsm.aufgaben.Sperre;
import de.oaa.xxx.games.bdsm.aufgaben.Strafe;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
public class BdsmGameDurchfuehren {
private final AufgabenList aufgabenList;
private final List<Mitspieler> mitspieler = new ArrayList<>();
private final List<AktiveSperre> aktiveSperren = new ArrayList<>();
private final Integer wahrscheinlichkeitSperre;
private final Integer wahrscheinlichkeitStrafe;
private int aufgabenProLevel;
private int level;
private int aufgabenAufAktuellemLevel;
public BdsmGameDurchfuehren(BdsmGameEntity entity) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
aufgabenList = objectMapper.readValue(entity.getAufgaben(), AufgabenList.class);
entity.getMitspieler().forEach(mitspielerEntity -> mitspieler.add(mitspielerEntity.toMitspieler()));
entity.getAktiveSperren().forEach(sperreEntity -> aktiveSperren.add(sperreEntity.toSperre(mitspieler)));
wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre();
wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe();
this.aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5;
this.level = entity.getLevel() != null ? entity.getLevel() : 1;
this.aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0;
}
public AufgabeAnzeige getNext() {
checkLevel();
if (level == 6) {
return null;
}
AufgabeAnzeige anzeige = null;
int nextInt = new Random().nextInt(1, 100);
if (nextInt == 1) {
anzeige = findUltimativeStrafe();
} else if (nextInt == 2) {
anzeige = findSperreVerlaengern();
} else if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
anzeige = findeAufgabe();
} else if (nextInt > wahrscheinlichkeitSperre + 2) {
anzeige = findeStrafe();
} else {
anzeige = findeSperre();
}
if (anzeige == null) {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
String text = "Ups, da ist etwas schief gelaufen. Keine potenzielle Aufgabe gefunden. Entweder seid ihr inzwischen so gut weggesperrt, dass wirklich keine Aufgaben mehr zur Verfügung stehen, oder uns ist ein Fehler unterlaufen. {AKTIV} und {PASSIV} überbrücken die Zeit mit ein wenig Petting.";
anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
anzeige.setTimer(120);
}
return anzeige;
}
public void backToLvl5() {
this.level = 5;
this.aufgabenAufAktuellemLevel = 0;
}
public List<AufgabeAnzeige> getFinisher() {
var list = new ArrayList<AufgabeAnzeige>();
List.of(GeschlechtEnum.WEIBLICH, GeschlechtEnum.DIVERS, GeschlechtEnum.MAENNLICH).forEach(geschlecht -> {
mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> {
var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming);
var finishers = aufgabenList.getFinisher().stream()
.filter(finisher -> geschlecht == finisher.getGeschlecht())
.toList();
if (!finishers.isEmpty()) {
var aufgabe = finishers.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(cumming.getName());
setMitspielerInfo(anzeige, cumming);
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(),
cumming.getName(), partner != null ? partner.getName() : ""));
list.add(anzeige);
}
});
});
return list;
}
private void checkLevel() {
if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
aufgabenAufAktuellemLevel = 0;
level++;
}
}
private void setMitspielerInfo(AufgabeAnzeige anzeige, Mitspieler aktiv) {
if (aktiv != null) {
anzeige.setMitspielerId(aktiv.getId());
anzeige.setEigenesGeraet(aktiv.isEigenesGeraet());
}
}
private AufgabeAnzeige findUltimativeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
String text = "{AKTIV}, verschnüre {PASSIV} fachmännisch inkl. KG, Plugs, Knebel, Augenbinde und was dir sonst einfällt. Nutze die Ruhe für was auch immer du möchtest.";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setTimer(new Random().nextInt(1800, 7200));
return anzeige;
}
}
return findeStrafe();
}
private AufgabeAnzeige findSperreVerlaengern() {
if (!aktiveSperren.isEmpty()) {
AktiveSperre sperre = aktiveSperren.get(new Random().nextInt(aktiveSperren.size()));
Mitspieler passiv = sperre.getMitspieler();
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV, passiv);
if (aktiv != null) {
String text = "{AKTIV}, du entscheidest. Sollen alle bestehenden Zeitstrafen von {PASSIV} verlängert werden...?";
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setAufgabeText(getAnzeigeText(text, aktiv.getName(), passiv.getName()));
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
SperrenVerlaengernCallback callback = new SperrenVerlaengernCallback();
callback.setFaktor(new Random().nextInt(2, 4));
callback.setSpielerId(passiv.getId());
anzeige.setCallback(callback);
return anzeige;
}
}
return findeSperre();
}
private AufgabeAnzeige findeAufgabe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
if (passiv != null) {
List<Aufgabe> list = aufgabenList.getAufgaben().stream()
.filter(aufgabe -> aufgabe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Aufgabe aufgabe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(), aktiv.getName(), passiv.getName()));
if (aufgabe.getSekundenVon() != null) {
if (aufgabe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis()));
} else {
anzeige.setTimer(aufgabe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeStrafe();
}
private AufgabeAnzeige findeStrafe() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Strafe> list = aufgabenList.getStrafen().stream()
.filter(strafe -> strafe.isAufgabePassend(level, aktiv, passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Strafe strafe = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(strafe.getText(), aktiv.getName(), passiv.getName()));
if (strafe.getSekundenVon() != null) {
if (strafe.getSekundenBis() != null) {
anzeige.setTimer(new Random().nextInt(strafe.getSekundenVon(), strafe.getSekundenBis()));
} else {
anzeige.setTimer(strafe.getSekundenVon());
}
}
return anzeige;
}
}
}
return findeSperre();
}
private AufgabeAnzeige findeSperre() {
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_AKTIV);
if (aktiv != null) {
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.BESTRAFUNG_PASSIV, aktiv);
if (passiv != null) {
List<Sperre> list = aufgabenList.getSperren().stream()
.filter(sperre -> sperre.isAufgabePassend(passiv))
.collect(Collectors.toList());
if (!list.isEmpty()) {
Sperre sperre = list.get(new Random().nextInt(list.size()));
AufgabeAnzeige anzeige = new AufgabeAnzeige();
anzeige.setNameAktiverMitspieler(aktiv.getName());
setMitspielerInfo(anzeige, aktiv);
anzeige.setAufgabeText(getAnzeigeText(sperre.getText(), aktiv.getName(), passiv.getName()));
SperreCallback callback = new SperreCallback();
callback.setSperreId(sperre.getSperreId());
callback.setSpielerId(passiv.getId());
callback.setReleaseText(getAnzeigeText(sperre.getReleaseText(), aktiv.getName(), passiv.getName()));
anzeige.setCallback(callback);
return anzeige;
}
}
}
return null;
}
private String getAnzeigeText(String textMitPlatzhaltern, String nameAktiv, String namePassiv) {
return textMitPlatzhaltern.replace("{AKTIV}", nameAktiv).replace("{PASSIV}", namePassiv);
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle) {
List<Mitspieler> list = mitspieler.stream()
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
private Mitspieler findeMitspielerMitRolle(RolleEnum rolle, Mitspieler gegenspieler) {
if (gegenspieler == null) return findeMitspielerMitRolle(rolle);
List<Mitspieler> list = mitspieler.stream()
.filter(m -> m != gegenspieler)
.filter(m -> m.isPassenderSpielpartner(gegenspieler))
.filter(m -> m.getRollen().contains(rolle))
.toList();
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
}
public int getAufgabenAufAktuellemLevel() {
return aufgabenAufAktuellemLevel;
}
public int getLevel() {
return level;
}
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
public enum GeschlechtEnum {
WEIBLICH,

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
import lombok.Getter;
import lombok.Setter;
@@ -11,6 +11,8 @@ import java.util.UUID;
public class Mitspieler {
private UUID id;
private UUID userId;
private boolean eigenesGeraet;
private String name;
private GeschlechtEnum geschlecht;
private List<GeschlechtEnum> spieltMit;

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
public enum RolleEnum {
BESTRAFUNG_AKTIV,

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session;
package de.oaa.xxx.games.bdsm;
public enum Werkzeug {

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben;
package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.Werkzeug;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
import lombok.Setter;

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session.aufgaben;
package de.oaa.xxx.games.bdsm.aufgaben;
import lombok.Getter;
import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben;
package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.GeschlechtEnum;
import de.oaa.xxx.session.Werkzeug;
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben;
package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.Werkzeug;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package de.oaa.xxx.session.aufgaben;
package de.oaa.xxx.games.bdsm.aufgaben;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.Werkzeug;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import lombok.Getter;
import lombok.Setter;

View File

@@ -0,0 +1,195 @@
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.repository.FriendshipRepository;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/bdsm/einladung")
@Transactional
public class BdsmEinladungController {
private final BdsmEinladungRepository einladungRepository;
private final UserRepository userRepository;
private final FriendshipRepository friendshipRepository;
private final SystemMessageService systemMessageService;
public BdsmEinladungController(BdsmEinladungRepository einladungRepository,
UserRepository userRepository,
FriendshipRepository friendshipRepository,
SystemMessageService systemMessageService) {
this.einladungRepository = einladungRepository;
this.userRepository = userRepository;
this.friendshipRepository = friendshipRepository;
this.systemMessageService = systemMessageService;
}
record EinladungRequest(UUID setupId, int slotIndex, UUID inviteeId) {}
record AntwortRequest(boolean accepted, String mode) {} // mode: OWN_DEVICE | HOST_DEVICE
private UUID currentUserId(Principal principal) {
return userRepository.findByEmail(principal.getName())
.map(u -> u.getUserId()).orElse(null);
}
@PostMapping
public ResponseEntity<Map<String, Object>> sendEinladung(@RequestBody EinladungRequest req, Principal principal) {
UUID inviterId = currentUserId(principal);
if (inviterId == null) return ResponseEntity.status(401).build();
// Freundschaft prüfen
var friendship = friendshipRepository.findExisting(inviterId, req.inviteeId());
if (friendship.isEmpty() || friendship.get().getStatus() != de.oaa.xxx.social.entity.FriendshipEntity.Status.ACCEPTED) {
return ResponseEntity.status(403).build();
}
// Alte Einladung für diesen Slot canceln
einladungRepository.findBySetupId(req.setupId()).stream()
.filter(e -> e.getSlotIndex() == req.slotIndex() && e.getStatus() == Status.PENDING)
.forEach(e -> e.setStatus(Status.CANCELLED));
BdsmEinladungEntity entity = new BdsmEinladungEntity();
entity.setEinladungId(UUID.randomUUID());
entity.setSetupId(req.setupId());
entity.setInviterId(inviterId);
entity.setInviteeId(req.inviteeId());
entity.setSlotIndex(req.slotIndex());
entity.setStatus(Status.PENDING);
entity.setCreatedAt(LocalDateTime.now());
einladungRepository.save(entity);
String inviterName = userRepository.findById(inviterId).map(u -> u.getName()).orElse("Jemand");
systemMessageService.send(
inviterId, req.inviteeId(),
inviterName + " hat dich zum BDSM Game eingeladen.",
"/einladungen.html",
MessageCause.INVITATION
);
Map<String, Object> result = new LinkedHashMap<>();
result.put("einladungId", entity.getEinladungId());
return ResponseEntity.ok(result);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> cancelEinladung(@PathVariable UUID id, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build();
e.setStatus(Status.CANCELLED);
return ResponseEntity.accepted().build();
}
@GetMapping
public ResponseEntity<List<Map<String, Object>>> getBySetupId(@RequestParam UUID setupId, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findBySetupId(setupId).stream()
.map(this::toMap).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/pending")
public ResponseEntity<List<Map<String, Object>>> getPending(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findByInviteeIdAndStatus(userId, Status.PENDING)
.stream().map(e -> {
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviterId()).ifPresent(u -> {
m.put("inviterName", u.getName());
m.put("inviterAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return m;
}).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/sent")
public ResponseEntity<List<Map<String, Object>>> getSent(Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
List<Map<String, Object>> list = einladungRepository.findByInviterIdAndStatus(userId, Status.PENDING)
.stream().map(e -> {
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviteeId()).ifPresent(u -> {
m.put("inviteeName", u.getName());
m.put("inviteeAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return m;
}).toList();
return ResponseEntity.ok(list);
}
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getById(@PathVariable UUID id, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviteeId().equals(userId) && !e.getInviterId().equals(userId)) {
return ResponseEntity.status(403).build();
}
Map<String, Object> m = toMap(e);
userRepository.findById(e.getInviterId()).ifPresent(u -> {
m.put("inviterName", u.getName());
m.put("inviterAvatar", u.getProfilePicture() != null ? u.getProfilePicture() : "");
});
return ResponseEntity.ok(m);
}
@PutMapping("/{id}/antwort")
public ResponseEntity<Void> antwort(@PathVariable UUID id, @RequestBody AntwortRequest req, Principal principal) {
UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviteeId().equals(userId)) return ResponseEntity.status(403).build();
if (e.getStatus() != Status.PENDING) return ResponseEntity.badRequest().build();
if (!req.accepted()) {
e.setStatus(Status.DECLINED);
} else if ("OWN_DEVICE".equals(req.mode())) {
e.setStatus(Status.ACCEPTED_OWN);
} else {
e.setStatus(Status.ACCEPTED_HOST);
}
return ResponseEntity.accepted().build();
}
private Map<String, Object> toMap(BdsmEinladungEntity e) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("einladungId", e.getEinladungId());
m.put("setupId", e.getSetupId());
m.put("slotIndex", e.getSlotIndex());
m.put("inviteeId", e.getInviteeId());
m.put("inviterId", e.getInviterId());
m.put("status", e.getStatus().name());
m.put("sessionId", e.getSessionId());
return m;
}
}

View File

@@ -1,16 +1,22 @@
package de.oaa.xxx.session.controller;
package de.oaa.xxx.games.bdsm.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.session.AufgabeAnzeige;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.Session;
import de.oaa.xxx.session.SessionDurchfuehren;
import de.oaa.xxx.session.aufgaben.AufgabenList;
import de.oaa.xxx.session.entity.MitspielerEntity;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.BdsmGame;
import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren;
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.history.GameHistoryEntity;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.history.GameRole;
import de.oaa.xxx.games.history.GameType;
import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,59 +27,69 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/session")
@RequestMapping("/bdsm")
@Transactional
public class SessionController {
public class BdsmGameController {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionController.class);
private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class);
private final SessionRepository sessionRepository;
private final BdsmGameRepository sessionRepository;
private final MitspielerRepository mitspielerRepository;
private final AktiveSperreRepository aktiveSperreRepository;
private final UserRepository userRepository;
private final GameHistoryRepository gameHistoryRepository;
private final BdsmEinladungRepository einladungRepository;
private final ObjectMapper objectMapper;
public SessionController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository,
public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
GameHistoryRepository gameHistoryRepository, BdsmEinladungRepository einladungRepository,
ObjectMapper objectMapper) {
this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository;
this.aktiveSperreRepository = aktiveSperreRepository;
this.userRepository = userRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.einladungRepository = einladungRepository;
this.objectMapper = objectMapper;
}
@GetMapping("/{sessionId}")
public ResponseEntity<Session> getBySessionId(@PathVariable UUID sessionId) {
public ResponseEntity<BdsmGame> getBySessionId(@PathVariable UUID sessionId) {
return sessionRepository.findById(sessionId)
.map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build());
}
@GetMapping
public ResponseEntity<Session> getByUserId(@RequestParam UUID userId) {
public ResponseEntity<BdsmGame> getByUserId(@RequestParam UUID userId) {
return sessionRepository.findByUserId(userId)
.map(entity -> ResponseEntity.ok(toSession(entity)))
.orElse(ResponseEntity.noContent().build());
}
@PostMapping
public ResponseEntity<Void> create(@RequestBody Session session) {
public ResponseEntity<Void> create(@RequestBody BdsmGame session) {
String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
UUID userId = userRepository.findByEmail(email).map(u -> u.getUserId()).orElse(null);
if (userId == null) return ResponseEntity.status(401).build();
SessionEntity entity = new SessionEntity();
BdsmGameEntity entity = new BdsmGameEntity();
entity.setSessionId(UUID.randomUUID());
entity.setUserId(userId);
entity.setAufgabenAufAktuellemLevel(0);
@@ -85,8 +101,16 @@ public class SessionController {
entity.setWahrscheinlichkeitStrafe(session.getWahrscheinlichkeitStrafe() != null ? session.getWahrscheinlichkeitStrafe() : 10);
entity.setZeitfaktorZeitstrafen(session.getZeitfaktorZeitstrafen() != null ? session.getZeitfaktorZeitstrafen() : 1.0);
entity.setLevel(1);
entity.setSetupId(session.getSetupId());
sessionRepository.save(entity);
LOGGER.debug("Session gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]",
// Akzeptierte Einladungen mit der neuen Session verknüpfen
if (session.getSetupId() != null) {
einladungRepository.findBySetupId(session.getSetupId()).stream()
.filter(e -> e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_OWN
|| e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_HOST)
.forEach(e -> e.setSessionId(entity.getSessionId()));
}
LOGGER.debug("BdsmGame gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]",
entity.getSessionId(), entity.getUserId(), entity.getAufgabenProLevel(),
entity.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(),
entity.getZeitfaktorZeitstrafen());
@@ -96,9 +120,20 @@ public class SessionController {
}
@DeleteMapping
public ResponseEntity<Void> deleteSession(@RequestBody Session session) {
public ResponseEntity<Void> deleteSession(@RequestBody BdsmGame session) {
return sessionRepository.findById(session.getSessionId())
.map(entity -> {
LocalDateTime endTime = LocalDateTime.now();
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
GameHistoryEntity entry = new GameHistoryEntity();
entry.setGameType(GameType.BDSM);
entry.setStartTime(entity.getStartZeit());
entry.setEndTime(endTime);
entry.setDurationMinutes(durationMinutes);
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
gameHistoryRepository.save(entry);
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
mitspielerRepository.deleteAll(entity.getMitspieler());
sessionRepository.delete(entity);
@@ -114,7 +149,7 @@ public class SessionController {
return ResponseEntity.badRequest().build();
}
String aufgaben = objectMapper.writeValueAsString(list);
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
@@ -130,12 +165,12 @@ public class SessionController {
@GetMapping("/{sessionId}/aufgaben/next")
public ResponseEntity<AufgabeAnzeige> getNextAufgabe(@PathVariable UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
session.setLetzteAktivitaet(LocalDateTime.now());
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
AufgabeAnzeige next = durchfuehren.getNext();
session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
@@ -165,7 +200,7 @@ public class SessionController {
|| mitspieler.getVerfuegbareWerkzeuge() == null || mitspieler.getVerfuegbareWerkzeuge().isEmpty()) {
return ResponseEntity.badRequest().build();
}
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) {
return ResponseEntity.badRequest().build();
}
@@ -176,6 +211,8 @@ public class SessionController {
entity.setRollen(mitspieler.getRollen());
entity.setSpieltMit(mitspieler.getSpieltMit());
entity.setWerkzeuge(mitspieler.getVerfuegbareWerkzeuge());
entity.setUserId(mitspieler.getUserId());
entity.setEigenesGeraet(mitspieler.isEigenesGeraet());
entity.setSession(session);
mitspielerRepository.save(entity);
return ResponseEntity.accepted().build();
@@ -184,9 +221,9 @@ public class SessionController {
@GetMapping("/{sessionId}/finisher")
public ResponseEntity<List<AufgabeAnzeige>> getFinisher(@PathVariable UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build();
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
return ResponseEntity.ok(durchfuehren.getFinisher());
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);
@@ -197,9 +234,9 @@ public class SessionController {
@PostMapping("/{sessionId}/backToLevel5")
public ResponseEntity<Void> backToLevel5(@PathVariable UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.badRequest().build();
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
BdsmGameDurchfuehren durchfuehren = new BdsmGameDurchfuehren(session);
durchfuehren.backToLvl5();
session.setLevel(durchfuehren.getLevel());
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
@@ -211,8 +248,62 @@ public class SessionController {
}
}
private Session toSession(SessionEntity entity) {
Session session = new Session();
@GetMapping("/{sessionId}/mitspieler/me")
public ResponseEntity<Map<String, Object>> getMeinMitspieler(@PathVariable UUID sessionId, Principal principal) {
UUID userId = userRepository.findByEmail(principal.getName()).map(u -> u.getUserId()).orElse(null);
if (userId == null) return ResponseEntity.status(401).build();
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
return session.getMitspieler().stream()
.filter(m -> userId.equals(m.getUserId()))
.findFirst()
.map(m -> {
Map<String, Object> result = new LinkedHashMap<>();
result.put("mitspielerId", m.getMitspielerId());
result.put("name", m.getName());
result.put("eigenesGeraet", m.isEigenesGeraet());
return ResponseEntity.ok(result);
})
.orElse(ResponseEntity.noContent().build());
}
record ActiveTaskRequest(String taskJson, LocalDateTime timerStartedAt) {}
record ActiveTaskResponse(String taskJson, Long elapsedSeconds) {}
@PutMapping("/{sessionId}/active-task")
public ResponseEntity<Void> setActiveTask(@PathVariable UUID sessionId, @RequestBody ActiveTaskRequest req) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
session.setActiveTaskJson(req.taskJson());
session.setTaskStartedAt(req.timerStartedAt());
sessionRepository.save(session);
return ResponseEntity.accepted().build();
}
@DeleteMapping("/{sessionId}/active-task")
public ResponseEntity<Void> clearActiveTask(@PathVariable UUID sessionId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
session.setActiveTaskJson(null);
session.setTaskStartedAt(null);
sessionRepository.save(session);
return ResponseEntity.accepted().build();
}
@GetMapping("/{sessionId}/active-task")
public ResponseEntity<ActiveTaskResponse> getActiveTask(@PathVariable UUID sessionId) {
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.notFound().build();
if (session.getActiveTaskJson() == null) return ResponseEntity.noContent().build();
Long elapsed = null;
if (session.getTaskStartedAt() != null) {
elapsed = Duration.between(session.getTaskStartedAt(), LocalDateTime.now()).getSeconds();
}
return ResponseEntity.ok(new ActiveTaskResponse(session.getActiveTaskJson(), elapsed));
}
private BdsmGame toSession(BdsmGameEntity entity) {
BdsmGame session = new BdsmGame();
session.setSessionId(entity.getSessionId());
session.setUserId(entity.getUserId());
session.setAufgabenProLevel(entity.getAufgabenProLevel());

View File

@@ -1,15 +1,15 @@
package de.oaa.xxx.session.controller;
package de.oaa.xxx.games.bdsm.controller;
import de.oaa.xxx.session.AktiveSperre;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.session.sperre.SperreCallback;
import de.oaa.xxx.session.sperre.SperreVerarbeiten;
import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback;
import de.oaa.xxx.games.bdsm.AktiveSperre;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten;
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
@@ -26,18 +26,18 @@ import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@RestController("sessionSperreController")
@RequestMapping("/session/sperre")
@RestController("bdsmSperreController")
@RequestMapping("/bdsm/sperre")
@Transactional
public class SperreController {
private static final Logger LOGGER = LoggerFactory.getLogger(SperreController.class);
private final SessionRepository sessionRepository;
private final BdsmGameRepository sessionRepository;
private final MitspielerRepository mitspielerRepository;
private final AktiveSperreRepository aktiveSperreRepository;
public SperreController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository,
public SperreController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository) {
this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository;
@@ -78,7 +78,7 @@ public class SperreController {
@GetMapping("/aktive")
public ResponseEntity<List<AktiveSperre>> getAktiveSperren(@RequestParam UUID sessionId) {
try {
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
if (session == null) return ResponseEntity.noContent().build();
List<Mitspieler> mitspielerList = session.getMitspieler().stream()
.map(m -> m.toMitspieler())

View File

@@ -1,8 +1,8 @@
package de.oaa.xxx.session.entity;
package de.oaa.xxx.games.bdsm.entity;
import de.oaa.xxx.session.AktiveSperre;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.Werkzeug;
import de.oaa.xxx.games.bdsm.AktiveSperre;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.Werkzeug;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
@@ -50,7 +50,7 @@ public class AktiveSperreEntity {
private String releaseText;
@ManyToOne
@JoinColumn(name = "sessionId", nullable = false)
private SessionEntity session;
private BdsmGameEntity session;
public AktiveSperre toSperre(List<Mitspieler> mitspielerList) {
AktiveSperre sperre = new AktiveSperre();

View File

@@ -0,0 +1,30 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_defaults")
public class BdsmDefaultsEntity {
@Id
@Column(name = "user_id")
private UUID userId;
@Column(length = 100)
private String spieltMit;
@Column(length = 200)
private String rollen;
@Column(length = 200)
private String werkzeuge;
}

View File

@@ -0,0 +1,50 @@
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "bdsm_einladung")
public class BdsmEinladungEntity {
public enum Status {
PENDING, ACCEPTED_OWN, ACCEPTED_HOST, DECLINED, CANCELLED
}
@Id
@Column
private UUID einladungId;
@Column(nullable = false)
private UUID setupId;
@Column
private UUID sessionId;
@Column(nullable = false)
private UUID inviterId;
@Column(nullable = false)
private UUID inviteeId;
@Column(nullable = false)
private int slotIndex;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private Status status;
@Column(nullable = false)
private LocalDateTime createdAt;
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.session.entity;
package de.oaa.xxx.games.bdsm.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -18,7 +18,7 @@ import java.util.UUID;
@Setter
@Entity
@Table(name = "session")
public class SessionEntity {
public class BdsmGameEntity {
@Id
@Column
@@ -47,10 +47,16 @@ public class SessionEntity {
private String aufgaben;
@Column
private Double zeitfaktorZeitstrafen;
@Column(columnDefinition = "TEXT")
private String activeTaskJson;
@Column
private LocalDateTime taskStartedAt;
@Column
private UUID setupId;
@Override
public String toString() {
return "SessionEntity[sessionId=" + sessionId + ", userId=" + userId
return "BdsmGameEntity[sessionId=" + sessionId + ", userId=" + userId
+ ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel
+ ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%"
+ ", zeitfaktor=" + zeitfaktorZeitstrafen + ", start=" + startZeit + "]";

View File

@@ -1,77 +1,83 @@
package de.oaa.xxx.session.entity;
import de.oaa.xxx.session.GeschlechtEnum;
import de.oaa.xxx.session.Mitspieler;
import de.oaa.xxx.session.RolleEnum;
import de.oaa.xxx.session.Werkzeug;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "mitspieler")
public class MitspielerEntity {
@Id
@Column
private UUID mitspielerId;
@Column
private String name;
@Enumerated(EnumType.STRING)
@Column
private GeschlechtEnum geschlecht;
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = Werkzeug.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_werkzeuge", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "werkzeug")
private List<Werkzeug> werkzeuge = new ArrayList<>();
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = GeschlechtEnum.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_spieltMit", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "geschlecht")
private List<GeschlechtEnum> spieltMit = new ArrayList<>();
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = RolleEnum.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_rollen", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "rolle")
private List<RolleEnum> rollen = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "sessionId", nullable = false)
private SessionEntity session;
@OneToMany(mappedBy = "mitspieler", fetch = FetchType.EAGER)
private List<AktiveSperreEntity> aktiveSperren = new ArrayList<>();
@Override
public String toString() {
return "MitspielerEntity[mitspielerId=" + mitspielerId + ", name=" + name
+ ", geschlecht=" + geschlecht + ", rollen=" + rollen + ", werkzeuge=" + werkzeuge + "]";
}
public Mitspieler toMitspieler() {
Mitspieler mitspieler = new Mitspieler();
mitspieler.setGeschlecht(geschlecht);
mitspieler.setId(mitspielerId);
mitspieler.setName(name);
mitspieler.setRollen(rollen);
mitspieler.setSpieltMit(spieltMit);
mitspieler.setVerfuegbareWerkzeuge(new ArrayList<>(werkzeuge));
return mitspieler;
}
}
package de.oaa.xxx.games.bdsm.entity;
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import de.oaa.xxx.games.bdsm.Mitspieler;
import de.oaa.xxx.games.bdsm.RolleEnum;
import de.oaa.xxx.games.bdsm.Werkzeug;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "mitspieler")
public class MitspielerEntity {
@Id
@Column
private UUID mitspielerId;
@Column
private UUID userId;
@Column
private boolean eigenesGeraet;
@Column
private String name;
@Enumerated(EnumType.STRING)
@Column
private GeschlechtEnum geschlecht;
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = Werkzeug.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_werkzeuge", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "werkzeug")
private List<Werkzeug> werkzeuge = new ArrayList<>();
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = GeschlechtEnum.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_spieltMit", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "geschlecht")
private List<GeschlechtEnum> spieltMit = new ArrayList<>();
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = RolleEnum.class, fetch = FetchType.EAGER)
@CollectionTable(name = "mitspieler_rollen", joinColumns = @JoinColumn(name = "mitspielerId"))
@Column(name = "rolle")
private List<RolleEnum> rollen = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "sessionId", nullable = false)
private BdsmGameEntity session;
@OneToMany(mappedBy = "mitspieler", fetch = FetchType.EAGER)
private List<AktiveSperreEntity> aktiveSperren = new ArrayList<>();
@Override
public String toString() {
return "MitspielerEntity[mitspielerId=" + mitspielerId + ", name=" + name
+ ", geschlecht=" + geschlecht + ", rollen=" + rollen + ", werkzeuge=" + werkzeuge + "]";
}
public Mitspieler toMitspieler() {
Mitspieler mitspieler = new Mitspieler();
mitspieler.setGeschlecht(geschlecht);
mitspieler.setId(mitspielerId);
mitspieler.setUserId(userId);
mitspieler.setEigenesGeraet(eigenesGeraet);
mitspieler.setName(name);
mitspieler.setRollen(rollen);
mitspieler.setSpieltMit(spieltMit);
mitspieler.setVerfuegbareWerkzeuge(new ArrayList<>(werkzeuge));
return mitspieler;
}
}

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.repository;
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.session.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BdsmDefaultsRepository extends JpaRepository<BdsmDefaultsEntity, UUID> {
Optional<BdsmDefaultsEntity> findByUserId(UUID userId);
}

View File

@@ -0,0 +1,17 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface BdsmEinladungRepository extends JpaRepository<BdsmEinladungEntity, UUID> {
List<BdsmEinladungEntity> findBySetupId(UUID setupId);
List<BdsmEinladungEntity> findByInviteeIdAndStatus(UUID inviteeId, Status status);
List<BdsmEinladungEntity> findByInviterIdAndStatus(UUID inviterId, Status status);
}

View File

@@ -0,0 +1,12 @@
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface BdsmGameRepository extends JpaRepository<BdsmGameEntity, UUID> {
Optional<BdsmGameEntity> findByUserId(UUID userId);
}

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.repository;
package de.oaa.xxx.games.bdsm.repository;
import de.oaa.xxx.session.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.sperre;
package de.oaa.xxx.games.bdsm.sperre;
import de.oaa.xxx.session.Callback;
import de.oaa.xxx.games.bdsm.Callback;
import java.util.UUID;

View File

@@ -1,14 +1,14 @@
package de.oaa.xxx.session.sperre;
package de.oaa.xxx.games.bdsm.sperre;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.session.aufgaben.AufgabenList;
import de.oaa.xxx.session.aufgaben.Sperre;
import de.oaa.xxx.session.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.MitspielerEntity;
import de.oaa.xxx.session.entity.SessionEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.games.bdsm.aufgaben.Sperre;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import java.time.LocalDateTime;
import java.util.Optional;
@@ -19,9 +19,9 @@ public class SperreVerarbeiten {
private final ObjectMapper objectMapper = new ObjectMapper();
public void sperreAnwenden(SperreCallback callback, SessionRepository sessionRepository,
public void sperreAnwenden(SperreCallback callback, BdsmGameRepository sessionRepository,
MitspielerRepository mitspielerRepository, AktiveSperreRepository sperreRepository) throws Exception {
SessionEntity session = sessionRepository.findById(callback.getSessionId()).orElse(null);
BdsmGameEntity session = sessionRepository.findById(callback.getSessionId()).orElse(null);
MitspielerEntity mitspieler = mitspielerRepository.findById(callback.getSpielerId()).orElse(null);
if (session != null) {
AufgabenList aufgaben = objectMapper.readValue(session.getAufgaben(), AufgabenList.class);
@@ -56,7 +56,7 @@ public class SperreVerarbeiten {
sperreRepository.save(verlaengern);
}
private void fill(SperreCallback callback, SessionEntity session, MitspielerEntity mitspieler,
private void fill(SperreCallback callback, BdsmGameEntity session, MitspielerEntity mitspieler,
Sperre sperre, AktiveSperreEntity aktiv) {
aktiv.setAktiveSperreId(UUID.randomUUID());
LocalDateTime now = LocalDateTime.now();
@@ -70,7 +70,7 @@ public class SperreVerarbeiten {
aktiv.setReleaseText(callback.getReleaseText());
}
private Integer berechneDauer(SessionEntity session, Sperre sperre) {
private Integer berechneDauer(BdsmGameEntity session, Sperre sperre) {
Integer minuten = 30;
if (sperre.getMinutenVon() != null) {
if (sperre.getMinutenBis() != null) {

View File

@@ -1,6 +1,6 @@
package de.oaa.xxx.session.sperre;
package de.oaa.xxx.games.bdsm.sperre;
import de.oaa.xxx.session.Callback;
import de.oaa.xxx.games.bdsm.Callback;
import java.util.UUID;

View File

@@ -22,9 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
@RestController
@@ -34,8 +32,7 @@ public class LockeeInvitationController {
private final LockeeInvitationRepository lockeeInvitationRepository;
private final CardlockRepository cardlockRepository;
private final UserRepository userRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
@@ -45,27 +42,15 @@ public class LockeeInvitationController {
public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository,
CardlockRepository cardlockRepository,
UserRepository userRepository,
MessageRepository messageRepository,
SseService sseService) {
SystemMessageService systemMessageService) {
this.lockeeInvitationRepository = lockeeInvitationRepository;
this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) {
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", java.util.Map.of("unreadCount", unread, "text", text));
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
}
private String generateUnlockCode(int lines) {
@@ -154,7 +139,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getLockeeUserId(),
me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
null);
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
}
return ResponseEntity.noContent().build();
@@ -248,7 +233,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
"/keyholder.html");
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(),
@@ -278,7 +263,7 @@ public class LockeeInvitationController {
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",
null);
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
}
return ResponseEntity.noContent().build();

View File

@@ -41,7 +41,7 @@ import de.oaa.xxx.games.chastity.KeyholderInvitationEntity;
import de.oaa.xxx.games.chastity.KeyholderInvitationRepository;
import de.oaa.xxx.games.chastity.LockeeInvitationEntity;
import de.oaa.xxx.games.chastity.LockeeInvitationRepository;
import de.oaa.xxx.games.chastity.history.LockHistoryRepository;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
@@ -49,9 +49,7 @@ import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity;
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
@RestController
@@ -65,15 +63,14 @@ public class CardLockController {
private final VerificationRepository verificationRepository;
private final VerificationVoteRepository verificationVoteRepository;
private final HygieneViolationRepository hygieneViolationRepository;
private final MessageRepository messageRepository;
private final LockeeInvitationRepository lockeeInvitationRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final UnlockCodeHistoryRepository unlockCodeHistoryRepository;
private final UnlockCodeHistoryService unlockCodeHistoryService;
private final LockHistoryRepository lockHistoryRepository;
private final SseService sseService;
private final GameHistoryRepository gameHistoryRepository;
private final SystemMessageService systemMessageService;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
@@ -85,15 +82,14 @@ public class CardLockController {
VerificationRepository verificationRepository,
VerificationVoteRepository verificationVoteRepository,
HygieneViolationRepository hygieneViolationRepository,
MessageRepository messageRepository,
LockeeInvitationRepository lockeeInvitationRepository,
AssignedTaskRepository assignedTaskRepository,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository,
UnlockCodeHistoryRepository unlockCodeHistoryRepository,
UnlockCodeHistoryService unlockCodeHistoryService,
LockHistoryRepository lockHistoryRepository,
SseService sseService) {
GameHistoryRepository gameHistoryRepository,
SystemMessageService systemMessageService) {
this.cardlockRepository = cardlockRepository;
this.cardLockRepository = cardLockRepository;
this.userRepository = userRepository;
@@ -101,15 +97,14 @@ public class CardLockController {
this.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository;
this.hygieneViolationRepository = hygieneViolationRepository;
this.messageRepository = messageRepository;
this.lockeeInvitationRepository = lockeeInvitationRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.unlockCodeHistoryRepository = unlockCodeHistoryRepository;
this.unlockCodeHistoryService = unlockCodeHistoryService;
this.lockHistoryRepository = lockHistoryRepository;
this.sseService = sseService;
this.gameHistoryRepository = gameHistoryRepository;
this.systemMessageService = systemMessageService;
}
record CreateCardLockRequest(
@@ -194,7 +189,7 @@ public class CardLockController {
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
sendMessage(myId, lockee.getUserId(),
me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
"/einladungen.html");
"/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(),
@@ -257,7 +252,7 @@ public class CardLockController {
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
sendMessage(me.getUserId(), kh.getUserId(),
me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
"/einladungen.html");
"/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
keyholderPending = true;
}
@@ -282,7 +277,7 @@ public class CardLockController {
var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository);
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
CardDTO dto = service.getNextCard();
if (dto == null) return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar"));
@@ -300,7 +295,7 @@ public class CardLockController {
userRepository.findById(l.getKeyholder()).ifPresent(kh ->
sendMessage(l.getLockee(), kh.getUserId(),
"Deine Lockee hat eine Aufgaben-Karte gezogen wähle eine Aufgabe aus.",
"/keyholder.html"));
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE));
taskPending = "KEYHOLDER";
} else if ("COMMUNITY".equals(l.getTaskCardMode())) {
@@ -320,9 +315,14 @@ public class CardLockController {
result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : "");
if (taskPending != null) result.put("taskPending", taskPending);
// Grüne Karte → Entsperrcode-Historie speichern
// Grüne Karte → Entsperrcode-Historie speichern + Keyholder benachrichtigen
if (dto.unlockCode() != null && !dto.unlockCode().isBlank()) {
unlockCodeHistoryService.save(myId, l.getLockId(), l.getName(), dto.unlockCode(), "GREEN_CARD");
if (l.getKeyholder() != null) {
sendMessage(myId, l.getKeyholder(),
meOpt.get().getName() + " hat die grüne Karte gezogen! Der Entsperrcode wurde angezeigt.",
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
}
return ResponseEntity.ok(result);
@@ -417,7 +417,7 @@ public class CardLockController {
var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository);
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
service.clearTask();
return ResponseEntity.noContent().build();
}
@@ -434,8 +434,16 @@ public class CardLockController {
var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository);
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
service.putBackGreen();
// Grüne Karte zurückgelegt → Keyholder benachrichtigen
if (l.getKeyholder() != null) {
sendMessage(myId, l.getKeyholder(),
meOpt.get().getName() + " hat die grüne Karte zurückgelegt und bleibt im Lock.",
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
return ResponseEntity.noContent().build();
}
@@ -557,7 +565,7 @@ public class CardLockController {
lockDirty = true;
sendMessage(l.getKeyholder(), l.getLockee(),
"Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.",
"/activelock.html?lockId=" + l.getLockId());
"/activelock.html?lockId=" + l.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
if (lockDirty) cardlockRepository.save(l);
@@ -695,7 +703,7 @@ public class CardLockController {
var lockee = meOpt.get();
sendMessage(myId, lock.getKeyholder(),
"📸 " + lockee.getName() + " hat eine Verifikation eingereicht.",
"/keyholder.html");
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
return ResponseEntity.noContent().build();
@@ -793,14 +801,9 @@ public class CardLockController {
if (lockOpt.isPresent()) {
var lock = lockOpt.get();
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
msg.setReceiverId(lock.getLockee());
msg.setText(me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.");
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
sendMessage(myId, lock.getLockee(),
me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.",
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
}
return ResponseEntity.noContent().build();
@@ -855,14 +858,9 @@ public class CardLockController {
String lockName = lockOpt.get().getName() != null && !lockOpt.get().getName().isBlank()
? lockOpt.get().getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
msg.setReceiverId(inv.getKeyholderUserId());
msg.setText(me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.");
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
null, de.oaa.xxx.social.entity.MessageCause.INVITATION);
return ResponseEntity.noContent().build();
}
@@ -1069,7 +1067,7 @@ public class CardLockController {
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
// Entsperrung protokollieren (History + XP) gültig nur wenn Keyholder vorhanden und kein Auto-Notfall
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, lockHistoryRepository, userRepository);
CardLockService service = new CardLockService(l, verificationRepository, verificationVoteRepository, cardLockRepository, gameHistoryRepository, userRepository);
service.unlock(l.getUnlockCode());
var verifications = verificationRepository.findByLockId(lockId);
@@ -1124,14 +1122,8 @@ public class CardLockController {
? me.getName() + " hat " + toAdd.size() + " Karte(n) zu deinem Lock hinzugefügt: " + detail + "."
: me.getName() + " hat Karten zu deinem Lock hinzugefügt.";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
msg.setReceiverId(l.getLockee());
msg.setText(msgText);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId,
de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1189,14 +1181,8 @@ public class CardLockController {
? me.getName() + " hat " + removed.size() + " Karte(n) aus deinem Lock entfernt: " + detail + "."
: me.getName() + " hat Karten aus deinem Lock entfernt.";
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId);
msg.setReceiverId(l.getLockee());
msg.setText(msgText);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg);
sendMessage(myId, l.getLockee(), msgText, "/activelock.html?lockId=" + lockId,
de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1221,19 +1207,8 @@ public class CardLockController {
}
}
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) {
if (senderId == null || receiverId == null) return;
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", java.util.Map.of("unreadCount", unread, "text", text));
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl, de.oaa.xxx.social.entity.MessageCause cause) {
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
}
@GetMapping("/cardlock/unlock-history")
@@ -1305,7 +1280,7 @@ public class CardLockController {
sendMessage(me.getUserId(), l.getLockee(),
me.getName() + " hat dir eine Aufgabe gestellt. Du hast " +
req.acceptDeadlineMinutes() + " Minuten, um sie anzunehmen.",
"/activelock.html?lockId=" + lockId);
"/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1365,7 +1340,7 @@ public class CardLockController {
assignedTaskRepository.save(task);
cardlockRepository.save(l);
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe angenommen.", "/keyholder.html");
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe angenommen.", "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1397,7 +1372,7 @@ public class CardLockController {
assignedTaskRepository.save(task);
cardlockRepository.save(l);
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", "/keyholder.html");
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1465,7 +1440,7 @@ public class CardLockController {
until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) +
" " + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) +
" Uhr eingefroren.",
"/activelock.html?lockId=" + lockId);
"/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1490,7 +1465,7 @@ public class CardLockController {
cardlockRepository.save(l);
sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.",
"/activelock.html?lockId=" + lockId);
"/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1512,7 +1487,7 @@ public class CardLockController {
sendMessage(myId, l.getLockee(),
"Dein Keyholder hat das Lock freigeschaltet. Du erhältst beim nächsten Laden deinen Entsperrcode.",
"/activelock.html?lockId=" + lockId);
"/activelock.html?lockId=" + lockId, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
return ResponseEntity.noContent().build();
}
@@ -1542,7 +1517,7 @@ public class CardLockController {
// Keyholderin benachrichtigen
sendMessage(myId, l.getKeyholder(),
"⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde, sonst öffnet sich das Lock automatisch.",
"/keyholder.html");
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.EMERGENCY);
}
cardlockRepository.save(l);
return ResponseEntity.noContent().build();

View File

@@ -11,10 +11,9 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.games.chastity.LockType;
import de.oaa.xxx.games.chastity.ProcessLock;
import de.oaa.xxx.games.chastity.history.LockHistoryEntity;
import de.oaa.xxx.games.chastity.history.LockHistoryRepository;
import de.oaa.xxx.games.history.GameHistoryEntity;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
@@ -29,15 +28,15 @@ public class CardLockService extends ProcessLock {
private VerificationRepository verificationRepository;
private VerificationVoteRepository verificationVoteRepository;
private CardLockRepository cardLockRepository;
private LockHistoryRepository lockHistoryRepository;
private GameHistoryRepository gameHistoryRepository;
private UserRepository userRepository;
public CardLockService(CardLockEntity lock, VerificationRepository verificationRepository, VerificationVoteRepository verificationVoteRepository, CardLockRepository cardLockRepository, LockHistoryRepository lockHistoryRepository, UserRepository userRepository) {
public CardLockService(CardLockEntity lock, VerificationRepository verificationRepository, VerificationVoteRepository verificationVoteRepository, CardLockRepository cardLockRepository, GameHistoryRepository gameHistoryRepository, UserRepository userRepository) {
this.lock = lock;
this.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository;
this.cardLockRepository = cardLockRepository;
this.lockHistoryRepository = lockHistoryRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository;
}
@@ -101,28 +100,30 @@ public class CardLockService extends ProcessLock {
public void unlock(String unlockCode) {
this.lock.setUnlockTime(LocalDateTime.now());
// Self-Lock oder automatische Entsperrung ohne Keyholder-Zustimmung → ungültig
boolean valid = lock.getKeyholder() != null && !lock.isEmergencyAutoUnlocked();
if (!this.lock.isTestLock()) {
if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
.filter(verification -> isValid(verification))
.map(verification -> verification.getVerificationTime().toLocalDate())
.collect(Collectors.toSet());
LocalDate current = this.lock.getStartTime().toLocalDate();
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1);
while (!current.isAfter(last)) {
if (!verifications.contains(current)) {
valid = false;
break;
}
current = current.plusDays(1);
boolean valid = true;
if (lock.isEmergencyAutoUnlocked()) {
valid = false;
LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)");
}
if (lock.isTestLock()) {
valid = false;
} else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
.filter(verification -> isValid(verification))
.map(verification -> verification.getVerificationTime().toLocalDate())
.collect(Collectors.toSet());
LocalDate current = this.lock.getStartTime().toLocalDate();
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1);
while (!current.isAfter(last)) {
if (!verifications.contains(current)) {
valid = false;
LOGGER.debug("Lock invalid - no daily verification on %s", current.toString());
break;
}
current = current.plusDays(1);
}
}
lock.setUnlockTime(LocalDateTime.now());
@@ -132,31 +133,18 @@ public class CardLockService extends ProcessLock {
if (valid) {
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
// Eintrag für den Lockee
LockHistoryEntity lockeeEntry = new LockHistoryEntity();
lockeeEntry.setUserId(lock.getLockee());
lockeeEntry.setLockedBy(lock.getKeyholder());
lockeeEntry.setLockName(lock.getName());
lockeeEntry.setStartTime(lock.getStartTime());
lockeeEntry.setEndTime(lock.getUnlockTime());
lockeeEntry.setType(LockType.CARD);
lockeeEntry.setDurationMinutes(durationMinutes);
lockeeEntry.setRole("LOCKEE");
lockHistoryRepository.save(lockeeEntry);
// Eintrag für die Keyholderin
// Gemeinsamer History-Eintrag mit Teilnehmerliste
GameHistoryEntity entry = new GameHistoryEntity();
entry.setGameType(de.oaa.xxx.games.history.GameType.CARDLOCK);
entry.setGameName(lock.getName());
entry.setStartTime(lock.getStartTime());
entry.setEndTime(lock.getUnlockTime());
entry.setDurationMinutes(durationMinutes);
entry.addParticipant(lock.getLockee(), de.oaa.xxx.games.history.GameRole.LOCKEE);
if (lock.getKeyholder() != null) {
LockHistoryEntity khEntry = new LockHistoryEntity();
khEntry.setUserId(lock.getKeyholder());
khEntry.setLockedBy(lock.getLockee());
khEntry.setLockName(lock.getName());
khEntry.setStartTime(lock.getStartTime());
khEntry.setEndTime(lock.getUnlockTime());
khEntry.setType(LockType.CARD);
khEntry.setDurationMinutes(durationMinutes);
khEntry.setRole("KEYHOLDER");
lockHistoryRepository.save(khEntry);
entry.addParticipant(lock.getKeyholder(), de.oaa.xxx.games.history.GameRole.KEYHOLDER);
}
gameHistoryRepository.save(entry);
int minutes = (int) durationMinutes;
userRepository.findById(lock.getLockee()).ifPresent(u -> {

View File

@@ -3,10 +3,8 @@ package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
@@ -25,8 +23,7 @@ public class TaskCardController {
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
public TaskCardController(CardlockRepository cardlockRepository,
UserRepository userRepository,
@@ -34,16 +31,14 @@ public class TaskCardController {
CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository,
SseService sseService) {
SystemMessageService systemMessageService) {
this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
// ── Keyholder: ausstehende Aufgaben-Karten-Entscheidungen ─────────────────
@@ -240,16 +235,6 @@ public class TaskCardController {
}
private void sendMessage(UUID fromId, UUID toId, String text, String targetUrl) {
if (toId == null) return;
MessageEntity msg = new MessageEntity();
msg.setMessageId(java.util.UUID.randomUUID());
msg.setSenderId(fromId);
msg.setReceiverId(toId);
msg.setText(text);
msg.setSystemMessage(true);
msg.setTargetUrl(targetUrl);
msg.setSentAt(java.time.LocalDateTime.now());
messageRepository.save(msg);
sseService.push(toId, "notification", Map.of("text", text));
systemMessageService.send(fromId, toId, text, targetUrl, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
}

View File

@@ -4,7 +4,6 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
@@ -17,9 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.SystemMessageService;
@Component
public class TaskVoteScheduler {
@@ -30,21 +27,18 @@ public class TaskVoteScheduler {
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final CardlockRepository cardlockRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
public TaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
CardlockRepository cardlockRepository,
AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository,
SseService sseService) {
SystemMessageService systemMessageService) {
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.cardlockRepository = cardlockRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
@Scheduled(fixedDelay = 60_000)
@@ -117,16 +111,6 @@ public class TaskVoteScheduler {
}
private void sendMessage(UUID toId, String text, String targetUrl) {
if (toId == null) return;
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(toId); // System-Nachricht, kein echter Sender
msg.setReceiverId(toId);
msg.setText(text);
msg.setSystemMessage(true);
msg.setTargetUrl(targetUrl);
msg.setSentAt(LocalDateTime.now());
messageRepository.save(msg);
sseService.push(toId, "notification", Map.of("text", text));
systemMessageService.send(toId, toId, text, targetUrl, de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
}
}

View File

@@ -1,57 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/lockhistory")
public class LockHistoryController {
private final UserRepository userRepository;
private final LockHistoryRepository lockHistoryRepository;
public LockHistoryController(UserRepository userRepository, LockHistoryRepository lockHistoryRepository) {
this.userRepository = userRepository;
this.lockHistoryRepository = lockHistoryRepository;
}
@GetMapping
public ResponseEntity<List<Map<String, Object>>> get(@RequestParam UUID userId, Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var result = lockHistoryRepository.findByUserIdOrderByEndTimeDesc(userId).stream()
.map(e -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("role", e.getRole());
item.put("lockName", e.getLockName() != null ? e.getLockName() : "");
item.put("startTime", e.getStartTime().toString());
item.put("unlockTime", e.getEndTime().toString());
item.put("durationMinutes", e.getDurationMinutes());
if (e.getLockedBy() != null) {
userRepository.findById(e.getLockedBy()).ifPresent(u -> {
if ("LOCKEE".equals(e.getRole())) {
item.put("keyholderName", u.getName());
} else {
item.put("lockeeName", u.getName());
}
if (u.getProfilePicture() != null) {
item.put("partnerPic", u.getProfilePicture());
}
});
}
return item;
}).toList();
return ResponseEntity.ok(result);
}
}

View File

@@ -1,11 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import java.time.LocalDateTime;
import java.util.UUID;
import de.oaa.xxx.games.chastity.LockType;
public record LockHistoryDTO (UUID historyId, UUID userId, LocalDateTime startTime, LocalDateTime endTime, LockType type, UUID lockedBy, String lockName, long durationMinutes, String role) {
}

View File

@@ -1,53 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import java.time.LocalDateTime;
import java.util.UUID;
import de.oaa.xxx.games.chastity.LockType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "lock_history")
public class LockHistoryEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID historyId;
@Column(nullable = false)
private UUID userId;
@Column(nullable = false)
private LocalDateTime startTime;
@Column(nullable = false)
private LocalDateTime endTime;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LockType type;
@Column
private UUID lockedBy;
@Column
private String lockName;
@Column(nullable = false, columnDefinition = "BIGINT DEFAULT 0")
private long durationMinutes;
// LOCKEE oder KEYHOLDER
@Column(nullable = false, length = 20)
private String role;
public LockHistoryDTO toLockHistory() {
return new LockHistoryDTO(historyId, userId, startTime, endTime, type, lockedBy, lockName, durationMinutes, role);
}
}

View File

@@ -1,12 +0,0 @@
package de.oaa.xxx.games.chastity.history;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LockHistoryRepository extends JpaRepository<LockHistoryEntity, UUID> {
List<LockHistoryEntity> findByUserIdOrderByEndTimeDesc(UUID userId);
}

View File

@@ -0,0 +1,82 @@
package de.oaa.xxx.games.history;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/gamehistory")
public class GameHistoryController {
private final UserRepository userRepository;
private final GameHistoryRepository gameHistoryRepository;
public GameHistoryController(UserRepository userRepository, GameHistoryRepository gameHistoryRepository) {
this.userRepository = userRepository;
this.gameHistoryRepository = gameHistoryRepository;
}
@GetMapping
@Transactional(readOnly = true)
public ResponseEntity<List<Map<String, Object>>> get(@RequestParam UUID userId, Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var result = gameHistoryRepository.findByParticipantUserId(userId).stream()
.map(e -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("historyId", e.getHistoryId());
item.put("gameType", e.getGameType());
item.put("gameName", e.getGameName() != null ? e.getGameName() : "");
item.put("lockName", e.getGameName() != null ? e.getGameName() : "");
item.put("startTime", e.getStartTime().toString());
item.put("unlockTime", e.getEndTime().toString());
item.put("durationMinutes", e.getDurationMinutes());
List<Map<String, Object>> participants = e.getParticipants().stream()
.map(p -> {
Map<String, Object> pm = new LinkedHashMap<>();
pm.put("userId", p.getUserId());
pm.put("role", p.getRole());
userRepository.findById(p.getUserId()).ifPresent(u -> {
pm.put("name", u.getName());
pm.put("picture", u.getProfilePicture());
});
return pm;
})
.toList();
item.put("participants", participants);
// Abwärtskompatible Felder für benutzer.html (wird später angepasst)
e.getParticipants().stream()
.filter(p -> p.getUserId().equals(userId))
.findFirst()
.ifPresent(own -> item.put("role", own.getRole().name()));
e.getParticipants().stream()
.filter(p -> !p.getUserId().equals(userId))
.findFirst()
.ifPresent(partner -> userRepository.findById(partner.getUserId()).ifPresent(u -> {
if (GameRole.LOCKEE == partner.getRole()) {
item.put("lockeeName", u.getName());
} else if (GameRole.KEYHOLDER == partner.getRole()) {
item.put("keyholderName", u.getName());
}
item.put("partnerPic", u.getProfilePicture());
}));
return item;
}).toList();
return ResponseEntity.ok(result);
}
}

View File

@@ -0,0 +1,17 @@
package de.oaa.xxx.games.history;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public record GameHistoryDTO(
UUID historyId,
GameType gameType,
LocalDateTime startTime,
LocalDateTime endTime,
String gameName,
long durationMinutes,
List<ParticipantDTO> participants
) {
public record ParticipantDTO(UUID userId, GameRole role) {}
}

View File

@@ -0,0 +1,49 @@
package de.oaa.xxx.games.history;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "game_history")
public class GameHistoryEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID historyId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private GameType gameType;
@Column(nullable = false)
private LocalDateTime startTime;
@Column(nullable = false)
private LocalDateTime endTime;
@Column
private String gameName;
@Column(nullable = false, columnDefinition = "BIGINT DEFAULT 0")
private long durationMinutes;
@OneToMany(mappedBy = "history", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<GameHistoryParticipantEntity> participants = new ArrayList<>();
public void addParticipant(UUID userId, GameRole role) {
GameHistoryParticipantEntity p = new GameHistoryParticipantEntity();
p.setUserId(userId);
p.setRole(role);
p.setHistory(this);
participants.add(p);
}
}

View File

@@ -0,0 +1,30 @@
package de.oaa.xxx.games.history;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "game_history_participant")
public class GameHistoryParticipantEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID participantId;
@Column(nullable = false)
private UUID userId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private GameRole role;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "history_id", nullable = false)
private GameHistoryEntity history;
}

View File

@@ -0,0 +1,8 @@
package de.oaa.xxx.games.history;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
public interface GameHistoryParticipantRepository extends JpaRepository<GameHistoryParticipantEntity, UUID> {
}

View File

@@ -0,0 +1,17 @@
package de.oaa.xxx.games.history;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.UUID;
public interface GameHistoryRepository extends JpaRepository<GameHistoryEntity, UUID> {
@Query("SELECT DISTINCT h FROM GameHistoryEntity h JOIN h.participants p WHERE p.userId = :userId ORDER BY h.endTime DESC")
List<GameHistoryEntity> findByParticipantUserId(@Param("userId") UUID userId);
@Query("SELECT DISTINCT h FROM GameHistoryEntity h JOIN h.participants p WHERE p.userId = :userId AND h.gameType = :gameType ORDER BY h.endTime DESC")
List<GameHistoryEntity> findByParticipantUserIdAndGameType(@Param("userId") UUID userId, @Param("gameType") GameType gameType);
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.games.history;
public enum GameRole {
LOCKEE,
KEYHOLDER,
PLAYER
}

View File

@@ -0,0 +1,8 @@
package de.oaa.xxx.games.history;
public enum GameType {
CARDLOCK,
TIMELOCK,
BDSM,
VANILLA
}

View File

@@ -165,6 +165,56 @@ public class MailTemplateService {
);
}
public String buildNotificationMail(String name, String text, String targetUrl, String baseUrl) {
String actionButton = targetUrl != null
? """
<div style="text-align:center; margin:0 0 2rem 0;">
<a href="%s%s"
style="display:inline-block; padding:0.75rem 2.5rem; background:%s; color:#ffffff;
border-radius:6px; text-decoration:none; font-weight:600; font-size:1rem;">
Zur Anwendung
</a>
</div>
""".formatted(baseUrl, targetUrl, colorPrimary)
: "<div style=\"margin:0 0 2rem 0;\"></div>";
String settingsUrl = baseUrl + "/einstellungen.html#sec-benachrichtigungen";
return """
<!DOCTYPE html>
<html lang="de">
<body style="margin:0; padding:2rem; background:%s; font-family:'Segoe UI',Arial,sans-serif; color:%s;">
<div style="max-width:460px; margin:0 auto; background:%s; border:1px solid %s; border-radius:12px; padding:2.5rem; box-shadow:0 8px 32px rgba(0,0,0,0.5);">
<h1 style="color:%s; text-align:center; margin:0 0 1.5rem 0; font-size:1.6rem;">XXX The Game</h1>
<p style="color:%s; margin:0 0 0.75rem 0;">Hallo %s,</p>
<p style="color:%s; margin:0 0 2rem 0;">%s</p>
%s
<hr style="border:none; border-top:1px solid %s; margin:0 0 1.5rem 0;">
<p style="color:%s; font-size:0.85em; margin:0;">
Du erhältst diese E-Mail, weil du E-Mail-Benachrichtigungen für diese Kategorie aktiviert hast.
Du kannst deine Einstellungen jederzeit unter
<a href="%s" style="color:%s;">Einstellungen → Benachrichtigungen</a> anpassen.
</p>
</div>
</body>
</html>
""".formatted(
colorBg, colorText,
colorCard, colorSecondary,
colorPrimary,
colorText, name,
colorText, text,
actionButton,
colorSecondary,
colorMuted, settingsUrl, colorPrimary
);
}
public String buildActivationMail(String name, String activationLink, String activatePageUrl, String uuid) {
return """
<!DOCTYPE html>

View File

@@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.user.Registration;
import de.oaa.xxx.user.UserController;
@@ -29,12 +28,7 @@ public class ActivationController {
public ResponseEntity<Void> activate(@PathVariable String uuid) {
RegistrationEntity registration = registrationRepository.findById(UUID.fromString(uuid)).orElse(null);
if (registration != null && !Boolean.TRUE.equals(registration.getActivated())) {
Registration reg = new Registration();
reg.setEmail(registration.getEmail());
reg.setName(registration.getName());
reg.setPasswordHash(registration.getPassword());
ResponseEntity<Void> response = userController.userAnlegen(reg);
ResponseEntity<Void> response = userController.userAnlegen(registration.toRegistration());
if (response.getStatusCode().is2xxSuccessful()) {
registration.setActivated(Boolean.TRUE);
registrationRepository.save(registration);

View File

@@ -3,6 +3,7 @@ package de.oaa.xxx.registration;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.util.UUID;
@Getter
@@ -13,6 +14,7 @@ public class Registration {
private String name;
private String email;
private String passwordHash;
private LocalDate geburtsdatum;
@Override
public String toString() {

View File

@@ -14,6 +14,9 @@ import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.mail.MailTemplateService;
import de.oaa.xxx.user.UserRepository;
import java.time.LocalDate;
import java.time.Period;
@RestController
@RequestMapping("/registration")
public class RegistrationController {
@@ -39,6 +42,11 @@ public class RegistrationController {
@PostMapping
public ResponseEntity<String> create(@RequestBody Registration registration) {
LOGGER.info("POST {}: {}", getClass().getName(), registration);
if (registration.getGeburtsdatum() == null
|| Period.between(registration.getGeburtsdatum(), LocalDate.now()).getYears() < 18) {
LOGGER.warn("Registrierung abgelehnt Mindestalter nicht erreicht");
return ResponseEntity.status(422).build();
}
if (registrationRepository.findByEmail(registration.getEmail()).isPresent()
|| userRepository.findByEmail(registration.getEmail()).isPresent()) {
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());

View File

@@ -7,6 +7,7 @@ import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.util.UUID;
@Getter
@@ -26,6 +27,8 @@ public class RegistrationEntity {
private String password;
@Column
private Boolean activated;
@Column
private LocalDate geburtsdatum;
@Override
public String toString() {
@@ -38,6 +41,7 @@ public class RegistrationEntity {
registration.setEmail(email);
registration.setName(name);
registration.setPasswordHash(password);
registration.setGeburtsdatum(geburtsdatum);
return registration;
}
@@ -48,6 +52,7 @@ public class RegistrationEntity {
entity.setActivated(Boolean.FALSE);
entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash());
entity.setGeburtsdatum(registration.getGeburtsdatum());
return entity;
}
}

View File

@@ -1,12 +0,0 @@
package de.oaa.xxx.session.repository;
import de.oaa.xxx.session.entity.SessionEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface SessionRepository extends JpaRepository<SessionEntity, UUID> {
Optional<SessionEntity> findByUserId(UUID userId);
}

View File

@@ -6,6 +6,7 @@ import de.oaa.xxx.social.dto.MessageDto;
import de.oaa.xxx.social.dto.UserProfile;
import de.oaa.xxx.social.entity.FriendshipEntity;
import de.oaa.xxx.social.entity.FriendshipEntity.Status;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.FriendshipRepository;
import de.oaa.xxx.social.repository.MessageRepository;
@@ -31,15 +32,18 @@ public class SocialController {
private final FriendshipRepository friendshipRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
private final SystemMessageService systemMessageService;
public SocialController(UserRepository userRepository,
FriendshipRepository friendshipRepository,
MessageRepository messageRepository,
SseService sseService) {
SseService sseService,
SystemMessageService systemMessageService) {
this.userRepository = userRepository;
this.friendshipRepository = friendshipRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
this.systemMessageService = systemMessageService;
}
record FriendRequestBody(UUID receiverId) {}
@@ -94,6 +98,13 @@ public class SocialController {
f.setCreatedAt(LocalDateTime.now());
friendshipRepository.save(f);
LOGGER.info("User {} hat Freundschaftsanfrage an User {} gesendet", myId, body.receiverId());
String senderName = meOpt.get().getName();
systemMessageService.send(myId, body.receiverId(),
senderName + " hat dir eine Freundschaftsanfrage gesendet.",
"/benutzer.html?userId=" + myId,
MessageCause.FRIENDREQUEST);
return ResponseEntity.status(201).build();
}
@@ -299,22 +310,52 @@ public class SocialController {
// ── Helpers ──
private UserProfile toUserProfileWithStatus(UserEntity user, UUID myId) {
boolean isOwn = user.getUserId().equals(myId);
String status = "NONE";
var existing = friendshipRepository.findExisting(myId, user.getUserId());
if (existing.isPresent()) {
FriendshipEntity f = existing.get();
if (f.getStatus() == Status.ACCEPTED) {
status = "FRIEND";
} else if (f.getSenderId().equals(myId)) {
status = "PENDING_SENT";
} else {
status = "PENDING_RECEIVED";
if (!isOwn) {
var existing = friendshipRepository.findExisting(myId, user.getUserId());
if (existing.isPresent()) {
FriendshipEntity f = existing.get();
if (f.getStatus() == Status.ACCEPTED) {
status = "FRIEND";
} else if (f.getSenderId().equals(myId)) {
status = "PENDING_SENT";
} else {
status = "PENDING_RECEIVED";
}
}
}
return new UserProfile(user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(),
status, user.getAlter(), user.getGroesse(), user.getGewicht(),
user.getGeschlecht(), user.getNeigung(), user.getBeziehungsstatus(), user.getBeschreibung(),
user.getLockeeXp(), user.getKeyholderXp());
boolean isFriend = isOwn || "FRIEND".equals(status);
// Grunddaten nur zurückgeben wenn berechtigt
de.oaa.xxx.user.Sichtbarkeit svGd = user.getSichtbarkeitGrunddaten();
boolean showGrunddaten = isOwn || svGd == de.oaa.xxx.user.Sichtbarkeit.ALLE
|| (svGd == de.oaa.xxx.user.Sichtbarkeit.NUR_FREUNDE && isFriend);
// XP nur zurückgeben wenn berechtigt
de.oaa.xxx.user.Sichtbarkeit svXp = user.getSichtbarkeitXp();
boolean showXp = isOwn || svXp == de.oaa.xxx.user.Sichtbarkeit.ALLE
|| (svXp == de.oaa.xxx.user.Sichtbarkeit.NUR_FREUNDE && isFriend);
return new UserProfile(
user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(),
status,
showGrunddaten ? user.getAlter() : null,
showGrunddaten ? user.getGroesse() : null,
showGrunddaten ? user.getGewicht() : null,
showGrunddaten ? user.getGeschlecht() : null,
showGrunddaten ? user.getNeigung() : null,
showGrunddaten ? user.getBeziehungsstatus() : null,
showGrunddaten ? user.getBeschreibung() : null,
showXp ? user.getLockeeXp() : 0,
showXp ? user.getKeyholderXp() : 0,
user.getSichtbarkeitGrunddaten(),
user.getSichtbarkeitGalerie(),
user.getSichtbarkeitFreunde(),
user.getSichtbarkeitFeed(),
user.getSichtbarkeitPinnwand(),
user.getSichtbarkeitXp(),
user.getSichtbarkeitLockhistorie());
}
private MessageDto toMessageDto(MessageEntity m) {

View File

@@ -0,0 +1,103 @@
package de.oaa.xxx.social;
import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.mail.MailTemplateService;
import org.springframework.beans.factory.annotation.Value;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
@Service
public class SystemMessageService {
private static final Logger LOGGER = LoggerFactory.getLogger(SystemMessageService.class);
private final MessageRepository messageRepository;
private final NotificationPreferenceRepository preferenceRepository;
private final UserRepository userRepository;
private final SseService sseService;
private final MailService mailService;
private final MailTemplateService mailTemplateService;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
public SystemMessageService(MessageRepository messageRepository,
NotificationPreferenceRepository preferenceRepository,
UserRepository userRepository,
SseService sseService,
MailService mailService,
MailTemplateService mailTemplateService) {
this.messageRepository = messageRepository;
this.preferenceRepository = preferenceRepository;
this.userRepository = userRepository;
this.sseService = sseService;
this.mailService = mailService;
this.mailTemplateService = mailTemplateService;
}
/**
* Sendet eine Systemnachricht unter Berücksichtigung der Benachrichtigungseinstellungen des Empfängers.
*/
public void send(UUID senderId, UUID receiverId, String text, String targetUrl, MessageCause cause) {
if (senderId == null || receiverId == null) return;
NotificationPreferenceEntity pref = preferenceRepository
.findByUserIdAndCause(receiverId, cause)
.orElseGet(() -> NotificationPreferenceEntity.defaultFor(receiverId, cause));
// FRIENDREQUEST ist immer in-app, unabhängig von der Einstellung
boolean sendInApp = cause == MessageCause.FRIENDREQUEST || pref.isInApp();
if (sendInApp) {
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
msg.setMessageCause(cause);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", Map.of("unreadCount", unread, "text", text));
}
if (pref.isEmail()) {
userRepository.findById(receiverId).ifPresent(user -> {
try {
Email email = new Email();
email.setEmailAdresse(user.getEmail());
email.setTitel(causeTitel(cause));
email.setText(mailTemplateService.buildNotificationMail(user.getName(), text, targetUrl, baseUrl));
mailService.send(email);
} catch (Exception e) {
LOGGER.error("E-Mail-Benachrichtigung fehlgeschlagen für userId={}: {}", receiverId, e.getMessage());
}
});
}
}
private String causeTitel(MessageCause cause) {
return switch (cause) {
case INVITATION -> "XXX The Game Neue Einladung";
case GAME_STATE -> "XXX The Game Spielstatus-Änderung";
case EMERGENCY -> "XXX The Game ⚠️ Notfall";
case FRIENDREQUEST -> "XXX The Game Neue Freundschaftsanfrage";
};
}
}

View File

@@ -3,6 +3,7 @@ package de.oaa.xxx.social.dto;
import de.oaa.xxx.user.Beziehungsstatus;
import de.oaa.xxx.user.Geschlecht;
import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.Sichtbarkeit;
import java.util.UUID;
@@ -20,10 +21,20 @@ public record UserProfile(
Beziehungsstatus beziehungsstatus,
String beschreibung,
int lockeeXp,
int keyholderXp
int keyholderXp,
// Datenschutz-Einstellungen
Sichtbarkeit sichtbarkeitGrunddaten,
Sichtbarkeit sichtbarkeitGalerie,
Sichtbarkeit sichtbarkeitFreunde,
Sichtbarkeit sichtbarkeitFeed,
Sichtbarkeit sichtbarkeitPinnwand,
Sichtbarkeit sichtbarkeitXp,
Sichtbarkeit sichtbarkeitLockhistorie
) {
/** Compact constructor for contexts where profile details are not needed (friend list etc.) */
public UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {
this(userId, name, profilePicture, profilePictureHq, friendStatus, null, null, null, null, null, null, null, 0, 0);
this(userId, name, profilePicture, profilePictureHq, friendStatus,
null, null, null, null, null, null, null, 0, 0,
null, null, null, null, null, null, null);
}
}

View File

@@ -0,0 +1,8 @@
package de.oaa.xxx.social.entity;
public enum MessageCause {
INVITATION,
GAME_STATE,
EMERGENCY,
FRIENDREQUEST
}

View File

@@ -35,6 +35,10 @@ public class MessageEntity {
@Column(nullable = false)
private boolean systemMessage = false;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private MessageCause messageCause;
@Column(length = 500)
private String targetUrl;
}

View File

@@ -0,0 +1,40 @@
package de.oaa.xxx.social.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "notification_preference", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "cause"}))
public class NotificationPreferenceEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "user_id", nullable = false)
private UUID userId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private MessageCause cause;
@Column(nullable = false)
private boolean inApp = true;
@Column(nullable = false)
private boolean email = false;
/** Erzeugt eine nicht persistierte Standardpräferenz für unbekannte/neue Causes. */
public static NotificationPreferenceEntity defaultFor(UUID userId, MessageCause cause) {
NotificationPreferenceEntity p = new NotificationPreferenceEntity();
p.setUserId(userId);
p.setCause(cause);
// inApp=true und email=false sind bereits die Java-Felddefaults
return p;
}
}

View File

@@ -0,0 +1,16 @@
package de.oaa.xxx.social.repository;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface NotificationPreferenceRepository extends JpaRepository<NotificationPreferenceEntity, UUID> {
List<NotificationPreferenceEntity> findByUserId(UUID userId);
Optional<NotificationPreferenceEntity> findByUserIdAndCause(UUID userId, MessageCause cause);
}

View File

@@ -1,21 +0,0 @@
package de.oaa.xxx.user;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
public class Registration {
private UUID id;
private String name;
private String email;
private String passwordHash;
@Override
public String toString() {
return "Registration [id=" + id + ", name=" + name + ", email=" + email + "]";
}
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.user;
public enum Sichtbarkeit {
ALLE,
NUR_FREUNDE,
NUR_ICH
}

View File

@@ -3,6 +3,8 @@ package de.oaa.xxx.user;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.time.Period;
import java.util.UUID;
@Getter
@@ -14,7 +16,7 @@ public class User {
private String email;
private String password;
private String profilePicture;
private Integer alter;
private LocalDate geburtsdatum;
private Integer groesse;
private Integer gewicht;
private Geschlecht geschlecht;
@@ -22,6 +24,10 @@ public class User {
private Beziehungsstatus beziehungsstatus;
private String beschreibung;
public Integer getAlter() {
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
}
@Override
public String toString() {
return "User[userId=" + userId + ", name=" + name + ", email=" + email + "]";

View File

@@ -1,8 +1,13 @@
package de.oaa.xxx.user;
import java.security.Principal;
import java.time.LocalDate;
import java.time.Period;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -10,6 +15,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -17,6 +23,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.FavoritRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
@@ -24,19 +32,23 @@ import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.emailchange.EmailChangeRepository;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.passwordreset.PasswordResetRepository;
import de.oaa.xxx.registration.Registration;
import de.oaa.xxx.registration.RegistrationRepository;
import de.oaa.xxx.session.entity.AktiveSperreEntity;
import de.oaa.xxx.session.entity.MitspielerEntity;
import de.oaa.xxx.session.repository.AktiveSperreRepository;
import de.oaa.xxx.session.repository.MitspielerRepository;
import de.oaa.xxx.session.repository.SessionRepository;
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageRepository;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import de.oaa.xxx.social.repository.KommentarLikeRepository;
import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
import de.oaa.xxx.social.repository.PinnwandEintragRepository;
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.social.repository.KommentarLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageRepository;
import jakarta.transaction.Transactional;
@RestController
@@ -54,7 +66,7 @@ public class UserController {
private final ToyRepository toyRepository;
private final FavoritRepository favoritRepository;
private final GruppenAboRepository gruppenAboRepository;
private final SessionRepository sessionRepository;
private final BdsmGameRepository sessionRepository;
private final AktiveSperreRepository aktiveSperreRepository;
private final MitspielerRepository mitspielerRepository;
private final EmailChangeRepository emailChangeRepository;
@@ -65,6 +77,8 @@ public class UserController {
private final PinnwandLikeRepository pinnwandLikeRepository;
private final KommentarRepository kommentarRepository;
private final KommentarLikeRepository kommentarLikeRepository;
private final NotificationPreferenceRepository notificationPreferenceRepository;
private final BdsmDefaultsRepository bdsmDefaultsRepository;
public UserController(UserRepository userRepository,
RegistrationRepository registrationRepository,
@@ -75,7 +89,7 @@ public class UserController {
ToyRepository toyRepository,
FavoritRepository favoritRepository,
GruppenAboRepository gruppenAboRepository,
SessionRepository sessionRepository,
BdsmGameRepository sessionRepository,
AktiveSperreRepository aktiveSperreRepository,
MitspielerRepository mitspielerRepository,
EmailChangeRepository emailChangeRepository,
@@ -85,7 +99,9 @@ public class UserController {
PinnwandEintragRepository pinnwandEintragRepository,
PinnwandLikeRepository pinnwandLikeRepository,
KommentarRepository kommentarRepository,
KommentarLikeRepository kommentarLikeRepository) {
KommentarLikeRepository kommentarLikeRepository,
NotificationPreferenceRepository notificationPreferenceRepository,
BdsmDefaultsRepository bdsmDefaultsRepository) {
this.userRepository = userRepository;
this.registrationRepository = registrationRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
@@ -106,12 +122,23 @@ public class UserController {
this.pinnwandLikeRepository = pinnwandLikeRepository;
this.kommentarRepository = kommentarRepository;
this.kommentarLikeRepository = kommentarLikeRepository;
this.notificationPreferenceRepository = notificationPreferenceRepository;
this.bdsmDefaultsRepository = bdsmDefaultsRepository;
}
record ProfilePictureRequest(String picture, String pictureHq) {}
record NameChangeRequest(String name) {}
record ProfileRequest(Integer alter, Integer groesse, Integer gewicht,
record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {}
record ProfileRequest(Integer groesse, Integer gewicht,
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
record PrivacyRequest(
Sichtbarkeit sichtbarkeitGrunddaten,
Sichtbarkeit sichtbarkeitGalerie,
Sichtbarkeit sichtbarkeitFreunde,
Sichtbarkeit sichtbarkeitFeed,
Sichtbarkeit sichtbarkeitPinnwand,
Sichtbarkeit sichtbarkeitXp,
Sichtbarkeit sichtbarkeitLockhistorie) {}
@PutMapping("/me/picture")
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
@@ -132,7 +159,6 @@ public class UserController {
if (request.beschreibung() != null && request.beschreibung().length() > 600) {
return ResponseEntity.badRequest().build();
}
user.setAlter(request.alter());
user.setGroesse(request.groesse());
user.setGewicht(request.gewicht());
user.setGeschlecht(request.geschlecht());
@@ -144,6 +170,126 @@ public class UserController {
return ResponseEntity.ok().build();
}
@PutMapping("/me/privacy")
public ResponseEntity<Void> updatePrivacy(@RequestBody PrivacyRequest request, Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
var user = userOpt.get();
if (request.sichtbarkeitGrunddaten() != null) user.setSichtbarkeitGrunddaten(request.sichtbarkeitGrunddaten());
if (request.sichtbarkeitGalerie() != null) user.setSichtbarkeitGalerie(request.sichtbarkeitGalerie());
if (request.sichtbarkeitFreunde() != null) user.setSichtbarkeitFreunde(request.sichtbarkeitFreunde());
if (request.sichtbarkeitFeed() != null) user.setSichtbarkeitFeed(request.sichtbarkeitFeed());
if (request.sichtbarkeitPinnwand() != null) user.setSichtbarkeitPinnwand(request.sichtbarkeitPinnwand());
if (request.sichtbarkeitXp() != null) user.setSichtbarkeitXp(request.sichtbarkeitXp());
if (request.sichtbarkeitLockhistorie()!= null) user.setSichtbarkeitLockhistorie(request.sichtbarkeitLockhistorie());
userRepository.save(user);
LOGGER.info("User {} hat Datenschutz-Einstellungen aktualisiert", user.getUserId());
return ResponseEntity.ok().build();
}
record NotificationPreferenceRequest(boolean inApp, boolean email) {}
@GetMapping("/me/notifications")
public ResponseEntity<Map<String, Object>> getNotifications(Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
Map<String, NotificationPreferenceEntity> byKey = notificationPreferenceRepository.findByUserId(userId)
.stream().collect(Collectors.toMap(p -> p.getCause().name(), p -> p));
Map<String, Object> result = new LinkedHashMap<>();
for (MessageCause cause : MessageCause.values()) {
NotificationPreferenceEntity pref = byKey.getOrDefault(
cause.name(), NotificationPreferenceEntity.defaultFor(userId, cause));
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("inApp", pref.isInApp());
entry.put("email", pref.isEmail());
result.put(cause.name(), entry);
}
return ResponseEntity.ok(result);
}
@PutMapping("/me/notifications")
public ResponseEntity<Void> updateNotifications(@RequestBody Map<String, NotificationPreferenceRequest> request, Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
for (var entry : request.entrySet()) {
MessageCause cause;
try {
cause = MessageCause.valueOf(entry.getKey());
} catch (IllegalArgumentException e) {
continue;
}
NotificationPreferenceEntity pref = notificationPreferenceRepository
.findByUserIdAndCause(userId, cause)
.orElseGet(() -> {
NotificationPreferenceEntity n = new NotificationPreferenceEntity();
n.setUserId(userId);
n.setCause(cause);
return n;
});
pref.setInApp(entry.getValue().inApp());
pref.setEmail(entry.getValue().email());
notificationPreferenceRepository.save(pref);
}
return ResponseEntity.ok().build();
}
record BdsmDefaultsRequest(List<String> spieltMit, List<String> rollen, List<String> werkzeuge) {}
@GetMapping("/me/bdsm-defaults")
public ResponseEntity<Map<String, Object>> getBdsmDefaults(Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
.orElse(new BdsmDefaultsEntity());
Map<String, Object> result = new java.util.LinkedHashMap<>();
result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
result.put("rollen", splitOrEmpty(d.getRollen()));
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
return ResponseEntity.ok(result);
}
@PutMapping("/me/bdsm-defaults")
public ResponseEntity<Void> updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID userId = userOpt.get().getUserId();
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
.orElseGet(() -> { BdsmDefaultsEntity n = new BdsmDefaultsEntity(); n.setUserId(userId); return n; });
d.setSpieltMit(request.spieltMit() == null ? "" : String.join(",", request.spieltMit()));
d.setRollen(request.rollen() == null ? "" : String.join(",", request.rollen()));
d.setWerkzeuge(request.werkzeuge() == null ? "" : String.join(",", request.werkzeuge()));
bdsmDefaultsRepository.save(d);
return ResponseEntity.ok().build();
}
private static List<String> splitOrEmpty(String s) {
if (s == null || s.isBlank()) return List.of();
return List.of(s.split(","));
}
@PutMapping("/me/geburtsdatum")
public ResponseEntity<Void> updateGeburtsdatum(@RequestBody GeburtsdatumChangeRequest request, Principal principal) {
if (request.geburtsdatum() == null
|| Period.between(request.geburtsdatum(), LocalDate.now()).getYears() < 18) {
return ResponseEntity.status(422).build();
}
var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
var user = userOpt.get();
user.setGeburtsdatum(request.geburtsdatum());
userRepository.save(user);
LOGGER.info("User {} hat Geburtsdatum aktualisiert", user.getUserId());
return ResponseEntity.ok().build();
}
@PutMapping("/me/name")
public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) {
String newName = request.name();
@@ -264,7 +410,14 @@ public class UserController {
entity.setEmail(registration.getEmail());
entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash());
entity.setGeburtsdatum(registration.getGeburtsdatum());
userRepository.save(entity);
for (MessageCause cause : MessageCause.values()) {
notificationPreferenceRepository.save(
NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause));
}
return ResponseEntity.status(201).build();
} catch (Exception exception) {
LOGGER.error(exception.getMessage(), exception);

View File

@@ -4,6 +4,8 @@ import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.time.Period;
import java.util.UUID;
@Getter
@@ -28,8 +30,8 @@ public class UserEntity {
@Column(columnDefinition = "MEDIUMTEXT")
private String profilePictureHq;
@Column(name = "benutzer_alter")
private Integer alter;
@Column
private LocalDate geburtsdatum;
@Column
private Integer groesse;
@@ -58,6 +60,39 @@ public class UserEntity {
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
private int keyholderXp;
// ── Datenschutz / Sichtbarkeit ──
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitGrunddaten = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitGalerie = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitFreunde = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitFeed = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitPinnwand = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitXp = Sichtbarkeit.ALLE;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
private Sichtbarkeit sichtbarkeitLockhistorie = Sichtbarkeit.ALLE;
public Integer getAlter() {
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
}
@Override
public String toString() {
return "UserEntity[userId=" + userId + ", name=" + name + ", email=" + email + "]";
@@ -69,7 +104,7 @@ public class UserEntity {
user.setName(name);
user.setUserId(userId);
user.setProfilePicture(profilePicture);
user.setAlter(alter);
user.setGeburtsdatum(geburtsdatum);
user.setGroesse(groesse);
user.setGewicht(gewicht);
user.setGeschlecht(geschlecht);

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aufgaben XXX The Game</title>
@@ -608,44 +608,47 @@
resetSelection();
document.getElementById('userLoading').style.display = 'block';
fetch(`/gruppe/list/user?page=${userPage}&size=${PAGE_SIZE}`)
.then(r => r.json())
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => {
console.log('[aufgaben] user gruppen:', data);
userTotalPages = data.totalPages || 1;
renderGruppen('userList', data.content, 'user');
try { renderGruppen('userList', data.content, 'user'); } catch(e) { console.error('[aufgaben] renderGruppen user Fehler:', e); throw e; }
updatePaging('userPaging', 'userPrev', 'userNext', 'userPageInfo', userPage, userTotalPages);
document.getElementById('userLoading').style.display = 'none';
reapplyPendingExpand();
})
.catch(() => { document.getElementById('userLoading').textContent = 'Fehler beim Laden.'; });
.catch(err => { console.error('[aufgaben] Fehler user gruppen:', err); document.getElementById('userLoading').textContent = 'Fehler beim Laden: ' + err.message; });
}
function loadSystemGruppen() {
resetSelection();
document.getElementById('systemLoading').style.display = 'block';
fetch(`/gruppe/list/system?page=${systemPage}&size=${PAGE_SIZE}`)
.then(r => r.json())
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => {
console.log('[aufgaben] system gruppen:', data);
systemTotalPages = data.totalPages || 1;
renderGruppen('systemList', data.content, 'system');
try { renderGruppen('systemList', data.content, 'system'); } catch(e) { console.error('[aufgaben] renderGruppen system Fehler:', e); throw e; }
updatePaging('systemPaging', 'systemPrev', 'systemNext', 'systemPageInfo', systemPage, systemTotalPages);
document.getElementById('systemLoading').style.display = 'none';
reapplyPendingExpand();
})
.catch(() => { document.getElementById('systemLoading').textContent = 'Fehler beim Laden.'; });
.catch(err => { console.error('[aufgaben] Fehler system gruppen:', err); document.getElementById('systemLoading').textContent = 'Fehler beim Laden: ' + err.message; });
}
function loadAboGruppen() {
document.getElementById('aboLoading').style.display = 'block';
fetch(`/abo/list?page=${aboPage}&size=${PAGE_SIZE}`)
.then(r => r.json())
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(data => {
console.log('[aufgaben] abo gruppen:', data);
aboTotalPages = data.totalPages || 1;
renderGruppen('aboList', data.content, 'abo');
updatePaging('aboPaging', 'aboPrev', 'aboNext', 'aboPageInfo', aboPage, aboTotalPages);
document.getElementById('aboLoading').style.display = 'none';
reapplyPendingExpand();
})
.catch(() => { document.getElementById('aboLoading').textContent = 'Fehler beim Laden.'; });
.catch(err => { console.error('[aufgaben] Fehler abo gruppen:', err); document.getElementById('aboLoading').textContent = 'Fehler beim Laden: ' + err.message; });
}
function reapplyPendingExpand() {

View File

@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Einladung XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.invite-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 2rem;
text-align: center;
max-width: 420px;
margin: 0 auto;
}
.invite-icon { font-size: 2.5rem; margin-bottom: 1rem; }
.invite-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.5rem; }
.invite-sub { font-size: 0.9rem; color: var(--color-muted); margin-bottom: 2rem; line-height: 1.6; }
.invite-actions { display: flex; flex-direction: column; gap: 0.75rem; }
.invite-actions button { width: 100%; padding: 0.85rem; }
.decline-btn {
background: transparent;
border: none;
color: var(--color-muted);
font-size: 0.82rem;
cursor: pointer;
text-decoration: underline;
padding: 0.25rem;
margin-top: 0.5rem;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div id="loading" style="text-align:center;color:var(--color-muted);padding:3rem 0;">Einladung wird geladen…</div>
<div class="invite-card" id="card" style="display:none;">
<div class="invite-icon">⛓️</div>
<div class="invite-title" id="title"></div>
<div class="invite-sub" id="sub"></div>
<div class="message" id="message" style="display:none;margin-bottom:1rem;"></div>
<div class="invite-actions" id="actions"></div>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
const params = new URLSearchParams(location.search);
const einladungId = params.get('id');
if (!einladungId) window.location.replace('/userhome.html');
let einladung = null;
async function laden() {
try {
const res = await fetch(`/bdsm/einladung/${einladungId}`);
if (!res.ok) { zeigeFehler('Einladung nicht gefunden.'); return; }
einladung = await res.json();
document.getElementById('loading').style.display = 'none';
document.getElementById('card').style.display = '';
if (einladung.status === 'ACCEPTED_OWN' || einladung.status === 'ACCEPTED_HOST') {
zeigeBestaetigt();
return;
}
if (einladung.status === 'DECLINED' || einladung.status === 'CANCELLED') {
zeigeFehler('Diese Einladung ist nicht mehr gültig.');
return;
}
document.getElementById('title').textContent = `${einladung.inviterName || 'Jemand'} lädt dich ein`;
document.getElementById('sub').textContent = 'Du wurdest zu einem BDSM Game eingeladen. Wie möchtest du mitspielen?';
const actions = document.getElementById('actions');
actions.innerHTML = `
<button onclick="antworten(true, 'OWN_DEVICE')">Am eigenen Gerät mitspielen</button>
<button class="secondary" onclick="antworten(true, 'HOST_DEVICE')">Am Gerät von ${einladung.inviterName || 'der einladenden Person'}</button>
<button class="decline-btn" onclick="antworten(false, null)">Einladung ablehnen</button>`;
} catch (e) {
zeigeFehler('Fehler beim Laden der Einladung.');
}
}
async function antworten(accepted, mode) {
document.getElementById('actions').innerHTML = '<div style="color:var(--color-muted);font-size:0.9rem;">Wird gespeichert…</div>';
try {
const res = await fetch(`/bdsm/einladung/${einladungId}/antwort`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accepted, mode }),
});
if (!res.ok) throw new Error();
if (!accepted) {
document.getElementById('title').textContent = 'Einladung abgelehnt';
document.getElementById('sub').textContent = 'Du hast die Einladung abgelehnt.';
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
} else if (mode === 'OWN_DEVICE') {
window.location.replace(`/bdsmwarten.html?id=${einladungId}`);
} else {
zeigeBestaetigt();
}
} catch (_) {
document.getElementById('actions').innerHTML = '';
zeigeFehler('Fehler beim Speichern der Antwort.');
}
}
function zeigeBestaetigt() {
document.getElementById('title').textContent = 'Einladung angenommen';
document.getElementById('sub').textContent = 'Du spielst am Gerät der einladenden Person mit. Das Spiel wird dort von ihr gestartet.';
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
}
function zeigeFehler(text) {
document.getElementById('loading').style.display = 'none';
document.getElementById('card').style.display = '';
document.getElementById('title').textContent = 'Hinweis';
document.getElementById('sub').textContent = text;
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
}
laden();
</script>
</body>
</html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Neue Session XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { max-width: 540px; }
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
@@ -180,7 +180,7 @@
aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value),
zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10,
}));
window.location.href = '/sessionbdsmplayers.html';
window.location.href = '/bdsmplayers.html';
}
function showMessage(text, type) {
@@ -224,7 +224,7 @@
function sessionFortfahren(sid) {
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
sessionStorage.setItem('bdsm-session-id', sid);
window.location.href = '/sessionbdsmingame.html';
window.location.href = '/bdsmingame.html';
}
function sessionBeendenFragen(sid) {
@@ -241,7 +241,7 @@
async function sessionLoeschen(sid) {
versteckeModal();
try {
await fetch('/session', {
await fetch('/bdsm', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sid }),
@@ -256,7 +256,7 @@
if (!meRes.ok) return;
const user = await meRes.json();
const sessionRes = await fetch(`/session?userId=${user.userId}`);
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
if (sessionRes.status === 204) return;
if (!sessionRes.ok) return;

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Im Spiel XXX The Game</title>
@@ -236,7 +236,12 @@
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null');
const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]');
const sessionId = sessionStorage.getItem('bdsm-session-id');
if (!sessionId) window.location.replace('/sessionbdsm.html');
if (!sessionId) window.location.replace('/bdsm.html');
// Multi-Device: bin ich Gast?
const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true';
const myMitspielerId = sessionStorage.getItem('bdsm-guest-mitspieler-id') || null;
let guestPollInterval = null;
// ── Modal ──
function zeigeModal(title, text, actions) {
@@ -283,6 +288,7 @@
function clearTimer() {
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
stopHostPoll();
}
function zeigeTaskFehler(text) {
@@ -310,7 +316,7 @@
card.innerHTML = 'Aufgabe wird geladen…';
try {
const res = await fetch(`/session/${sessionId}/aufgaben/next`);
const res = await fetch(`/bdsm/${sessionId}/aufgaben/next`);
if (res.status === 204) { zeigeFinaleDialog(); return; }
if (!res.ok) throw new Error(`HTTP ${res.status}`);
currentTask = await res.json();
@@ -326,9 +332,189 @@
}
}
// ── Aktive Aufgabe persistieren ──
async function saveAktiveAufgabe(task, timerStartedAt) {
try {
await fetch(`/bdsm/${sessionId}/active-task`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ taskJson: JSON.stringify(task), timerStartedAt }),
});
} catch (_) { /* ignorieren */ }
}
async function clearAktiveAufgabe() {
try {
await fetch(`/bdsm/${sessionId}/active-task`, { method: 'DELETE' });
} catch (_) { /* ignorieren */ }
}
async function checkAktiveAufgabe() {
try {
const res = await fetch(`/bdsm/${sessionId}/active-task`);
if (res.status === 204 || !res.ok) { ladeAufgabe(); return; }
const data = await res.json();
currentTask = JSON.parse(data.taskJson);
if (currentTask.level) {
document.getElementById('levelImg').src = `/img/lvl${currentTask.level}.png`;
document.getElementById('levelDisplay').style.display = '';
}
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined) {
const remaining = currentTask.timer - data.elapsedSeconds;
if (remaining <= 0) {
await clearAktiveAufgabe();
aufgabeAbgeschlossen();
} else {
restoreTimer(Math.floor(remaining));
}
} else {
zeigeAufgabe();
}
} catch (_) { ladeAufgabe(); }
}
function restoreTimer(remaining) {
const task = currentTask;
const card = document.getElementById('taskCard');
card.className = 'task-card';
card.innerHTML = `
${badgeHtml(task.nameAktiverMitspieler)}
<div class="task-text" title="${escapeAttr(task.aufgabeText)}">${task.aufgabeText}</div>
<div class="task-footer">
<div class="task-timer-row" id="taskActions">
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>
</div>
${SESSION_BEENDEN_BTN}
</div>`;
let rem = remaining;
timerInterval = setInterval(() => {
rem--;
const el = document.getElementById('timerValue');
if (rem <= 0) {
clearTimer();
if (el) { el.textContent = formatTime(0); el.classList.add('expired'); }
aufgabeAbgeschlossen();
} else {
if (el) el.textContent = formatTime(rem);
}
}, 1000);
}
// ── Gast-Polling: wartet auf aktive Aufgabe für mein Gerät ──
function startGastPoll() {
if (guestPollInterval) return;
const card = document.getElementById('taskCard');
card.className = 'task-card loading';
card.innerHTML = 'Warte auf Aufgabe…';
guestPollInterval = setInterval(pollGastAufgabe, 2500);
}
async function pollGastAufgabe() {
try {
const res = await fetch(`/bdsm/${sessionId}/active-task`);
if (res.status === 204) {
// Keine aktive Aufgabe warten
const card = document.getElementById('taskCard');
if (!card.classList.contains('loading')) {
card.className = 'task-card loading';
card.innerHTML = 'Warte auf Aufgabe…';
}
return;
}
if (!res.ok) return;
const data = await res.json();
const task = JSON.parse(data.taskJson);
if (task.mitspielerId && task.mitspielerId === myMitspielerId) {
// Meine Aufgabe!
clearInterval(guestPollInterval); guestPollInterval = null;
currentTask = task;
if (task.level) {
document.getElementById('levelImg').src = `/img/lvl${task.level}.png`;
document.getElementById('levelDisplay').style.display = '';
}
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined && task.timer != null) {
const remaining = task.timer - data.elapsedSeconds;
if (remaining <= 0) { await clearAktiveAufgabe(); gastAufgabeAbgeschlossen(); }
else restoreTimer(Math.floor(remaining));
} else {
zeigeGastAufgabe(task);
}
} else {
// Jemand anderes ist dran
const card = document.getElementById('taskCard');
const name = task.nameAktiverMitspieler || 'Jemand';
card.className = 'task-card loading';
card.innerHTML = `<div style="font-size:1rem;color:var(--color-muted);">${name} ist dran…</div>`;
}
} catch (_) {}
}
function zeigeGastAufgabe(task) {
// Gast sieht seine Aufgabe mit eigenem "Erledigt"-Button
const cb = task.callback;
if (task.timer != null && !data?.elapsedSeconds) zeigeTimerAufgabe(task);
else if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
else zeigeEinfacheAufgabe(task);
}
async function gastAufgabeAbgeschlossen() {
clearTimer();
await clearAktiveAufgabe();
const card = document.getElementById('taskCard');
card.className = 'task-card loading';
card.innerHTML = 'Aufgabe erledigt warte auf nächste Aufgabe…';
startGastPoll();
}
// ── Host: wenn Aufgabe einem eigenem-Gerät-Spieler gehört ──
let hostPollInterval = null;
function startHostPoll() {
if (hostPollInterval) return;
hostPollInterval = setInterval(pollHostAktiv, 2500);
}
function stopHostPoll() {
if (hostPollInterval) { clearInterval(hostPollInterval); hostPollInterval = null; }
}
async function pollHostAktiv() {
try {
const res = await fetch(`/bdsm/${sessionId}/active-task`);
if (res.status === 204) {
// Gast hat Aufgabe erledigt → Host lädt nächste
stopHostPoll();
ladeAufgabe();
}
} catch (_) {}
}
function zeigeAufgabe() {
const task = currentTask;
const cb = task.callback;
saveAktiveAufgabe(task, null);
// Wenn Aufgabe für eigenes-Gerät-Spieler: Host zeigt nur Hinweis
if (!isGuest && task.eigenesGeraet && task.mitspielerId) {
const name = task.nameAktiverMitspieler || 'Jemand';
const card = document.getElementById('taskCard');
card.className = 'task-card';
card.innerHTML = `
<div style="flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:0.5rem;">
<div style="font-size:1.8rem;">📱</div>
<div style="font-size:1rem;color:var(--color-muted);text-align:center;">${name} spielt gerade auf dem eigenen Gerät.</div>
</div>
<div class="task-footer">
<div class="task-btns"></div>
${SESSION_BEENDEN_BTN}
</div>`;
startHostPoll();
return;
}
if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
else if (task.timer != null) zeigeTimerAufgabe(task);
@@ -380,6 +566,7 @@
actions.innerHTML = `
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>`;
saveAktiveAufgabe(task, new Date().toISOString());
timerInterval = setInterval(() => {
remaining--;
@@ -405,8 +592,10 @@
async function aufgabeAbgeschlossen() {
clearTimer();
await clearAktiveAufgabe();
if (isGuest) { gastAufgabeAbgeschlossen(); return; }
try {
const res = await fetch(`/session/sperre/abgelaufene?sessionId=${sessionId}`);
const res = await fetch(`/bdsm/sperre/abgelaufene?sessionId=${sessionId}`);
if (res.ok) {
const text = await res.text();
const texte = text.split(';').map(t => t.trim()).filter(t => t.length > 0);
@@ -429,7 +618,7 @@
const cb = currentTask?.callback;
if (!cb) return;
try {
const res = await fetch('/session/sperre', {
const res = await fetch('/bdsm/sperre', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...cb, sessionId }),
@@ -445,7 +634,7 @@
const cb = currentTask?.callback;
if (!cb) return;
try {
const res = await fetch('/session/sperre/verlaengern', {
const res = await fetch('/bdsm/sperre/verlaengern', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...cb, sessionId }),
@@ -478,14 +667,14 @@
async function zurueckZuLevel5() {
try {
await fetch(`/session/${sessionId}/backToLevel5`, { method: 'POST' });
await fetch(`/bdsm/${sessionId}/backToLevel5`, { method: 'POST' });
} catch (_) {}
ladeAufgabe();
}
async function starteFinale() {
try {
const res = await fetch(`/session/sperre/aktive?sessionId=${sessionId}`);
const res = await fetch(`/bdsm/sperre/aktive?sessionId=${sessionId}`);
if (res.ok) {
const sperren = await res.json();
const texte = (sperren || []).map(s => s.releaseText).filter(t => t);
@@ -506,7 +695,7 @@
async function ladeFinisher() {
try {
const res = await fetch(`/session/${sessionId}/finisher`);
const res = await fetch(`/bdsm/${sessionId}/finisher`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const liste = await res.json();
naechsterFinisher(liste, 0);
@@ -572,7 +761,7 @@
async function sessionLoeschen() {
versteckeModal();
try {
await fetch('/session', {
await fetch('/bdsm', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId }),
@@ -583,7 +772,11 @@
}
// ── Start ──
ladeAufgabe();
if (isGuest) {
startGastPoll();
} else {
checkAktiveAufgabe();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,743 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Mitspieler XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Player cards ── */
.player-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1rem;
}
.player-card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.25rem;
}
.player-title {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
}
.player-badge {
background: var(--color-primary);
color: #fff;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-pending {
background: var(--color-secondary);
color: var(--color-muted);
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-accepted {
background: #1a5c2a;
color: #6fcf97;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-remove {
margin-left: auto;
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.25rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.player-remove:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: transparent;
}
.btn-invite {
background: transparent;
border: 1px solid var(--color-primary);
color: var(--color-primary);
padding: 0.45rem 1rem;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-invite:hover { background: var(--color-primary); color: #fff; }
.btn-cancel-invite {
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.2rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
}
.btn-cancel-invite:hover { border-color: var(--color-primary); color: var(--color-primary); background: transparent; }
.pending-info {
text-align: center;
color: var(--color-muted);
font-size: 0.9rem;
padding: 1.5rem 0;
}
.pending-name {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
margin-bottom: 0.25rem;
}
.pending-mode {
font-size: 0.8rem;
color: var(--color-muted);
}
.card-field { margin-bottom: 1rem; }
.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-item.is-checked { border-color: var(--color-primary); }
.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; }
.add-player-btn {
width: 100%;
background: transparent;
border: 1px dashed var(--color-secondary);
color: var(--color-muted);
padding: 0.75rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: normal;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
margin-bottom: 2rem;
}
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
/* ── Freunde-Modal ── */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.75);
z-index: 1000;
display: flex; align-items: center; justify-content: center;
padding: 1.5rem;
}
.modal-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 1.75rem;
max-width: 420px; width: 100%;
}
.modal-title { font-size: 1rem; font-weight: 700; margin-bottom: 1rem; }
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
.friend-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: var(--color-secondary); flex-shrink: 0; }
.friend-combobox { position: relative; }
.friend-dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 8px; max-height: 220px; overflow-y: auto; z-index: 10; margin-top: 0.25rem;
}
.friend-dropdown-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.6rem 0.75rem; cursor: pointer; transition: background 0.1s;
font-size: 0.9rem; font-weight: 600;
}
.friend-dropdown-item:hover { background: var(--color-secondary); }
.selected-friend-box {
display: none; margin-top: 0.75rem; padding: 0.6rem 0.75rem;
background: var(--color-secondary); border-radius: 8px;
font-size: 0.9rem; font-weight: 600;
border: 1px solid var(--color-primary); color: var(--color-text);
}
.modal-cancel { margin-top: 0.6rem; width: 100%; }
</style>
</head>
<body class="app">
<div class="modal-overlay" id="friendModal" style="display:none;">
<div class="modal-card">
<div class="modal-title">Freund einladen</div>
<div class="friend-combobox">
<input type="text" id="friendSearch" placeholder="Name eingeben…" autocomplete="off" oninput="filterFreunde(this.value)">
<div class="friend-dropdown" id="friendDropdown"></div>
</div>
<div class="selected-friend-box" id="selectedFriendBox"></div>
<button id="btnEinladen" style="margin-top:1rem; width:100%;" disabled onclick="confirmedEinladen()">Einladen</button>
<button class="secondary modal-cancel" onclick="schliesseFriendModal()">Abbrechen</button>
</div>
</div>
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 2 von 4 Mitspieler</p>
<div class="setup-section">
<h2>Mitspieler</h2>
<div id="playersContainer"></div>
<button class="add-player-btn" onclick="addPlayer()">+ Spieler hinzufügen</button>
</div>
<div class="message" id="message"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsm.html'">← Zurück</button>
<button style="flex:2;" id="weiterBtn" onclick="weiter()">Weiter</button>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
if (!sessionStorage.getItem('bdsm-session-settings')) {
window.location.replace('/bdsm.html');
}
// SetupId erzeugen (persistent über sessionStorage)
if (!sessionStorage.getItem('bdsm-setup-id')) {
sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
}
const setupId = sessionStorage.getItem('bdsm-setup-id');
const GESCHLECHTER = [
{ value: 'MAENNLICH', label: 'Männlich' },
{ value: 'WEIBLICH', label: 'Weiblich' },
{ value: 'DIVERS', label: 'Divers' },
];
const ROLLEN = [
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe Aktiv' },
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe Passiv' },
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung Aktiv' },
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung Passiv' },
];
const WERKZEUGE_DEFAULTS = {
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
};
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
];
const ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv', AUFGABE_PASSIV: 'Aufgabe Passiv',
BESTRAFUNG_AKTIV: 'Bestrafung Aktiv', BESTRAFUNG_PASSIV: 'Bestrafung Passiv',
};
let playerSeq = 0;
let playerIds = [];
// { [playerId]: { einladungId, status, inviteeId, inviteeName, mode } | null }
let playerInvitations = {};
let pollIntervalId = null;
let myUserId = null;
let freundeListe = [];
function buildCheckItems(name, items, type, disabled = false) {
return items.map(({ value, label, desc }) => `
<label class="check-item${disabled ? ' is-disabled' : ''}">
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
<span>
<span class="check-item-label">${label}</span>
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
</span>
</label>`).join('');
}
function createCardHtml(id, prefillName, isSelf) {
const badge = isSelf ? '<span class="player-badge">Du</span>' : '';
const num = playerIds.indexOf(id) + 1;
const nameField = isSelf
? `<input type="text" id="p${id}-name" value="${prefillName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`
: `<input type="text" id="p${id}-name" value="${prefillName}" placeholder="Name" autocomplete="off">`;
const inviteBtn = isSelf ? '' : `<button class="btn-invite" onclick="oeffneFreundeModal(${id})">👥 Einladen</button>`;
return `
<div class="player-card" id="player-${id}">
<div class="player-card-header">
<span class="player-title">Spieler ${num}</span>
${badge}
${inviteBtn}
<button class="player-remove" onclick="removePlayer(${id})">✕ Entfernen</button>
</div>
<div id="p${id}-body">
${buildPlayerBody(id, nameField, isSelf)}
</div>
</div>`;
}
function buildPlayerBody(id, nameField, genderDisabled = false) {
return `
<div class="card-field">
<label>Name</label>
${nameField}
<div class="field-error" id="p${id}-name-err">Bitte Namen eingeben.</div>
</div>
<div class="card-field">
<label>Geschlecht${genderDisabled ? ' <span style="font-size:0.75rem;color:var(--color-muted);">(unveränderlich)</span>' : ''}</label>
<div class="check-group">${buildCheckItems('p' + id + '-geschlecht', GESCHLECHTER, 'radio', genderDisabled)}</div>
<div class="field-error" id="p${id}-geschlecht-err">Bitte Geschlecht auswählen.</div>
</div>
<div class="card-field">
<label>Spielt mit</label>
<div class="check-group">${buildCheckItems('p' + id + '-spieltmit', GESCHLECHTER, 'checkbox')}</div>
<div class="field-error" id="p${id}-spieltmit-err">Bitte mindestens eine Option wählen.</div>
<div class="field-error" id="p${id}-partner-err">Kein Mitspieler mit passendem Geschlecht vorhanden.</div>
</div>
<div class="card-field">
<label>Rollen</label>
<div class="check-group">${buildCheckItems('p' + id + '-rollen', ROLLEN, 'checkbox')}</div>
<div class="field-error" id="p${id}-rollen-err">Bitte mindestens eine Rolle wählen.</div>
</div>
<div class="card-field">
<label>Verfügbar</label>
<div class="check-group check-group--two-col">${buildCheckItems('p' + id + '-werkzeuge', WERKZEUGE, 'checkbox')}</div>
<div class="field-error" id="p${id}-werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
</div>`;
}
function addPlayer(prefillName = '', isSelf = false) {
playerSeq++;
const id = playerSeq;
playerIds.push(id);
playerInvitations[id] = null;
document.getElementById('playersContainer')
.insertAdjacentHTML('beforeend', createCardHtml(id, prefillName, isSelf));
refreshRemoveButtons();
return id;
}
function removePlayer(id) {
const inv = playerInvitations[id];
if (inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST')) {
cancelEinladung(id);
}
document.getElementById('player-' + id)?.remove();
playerIds = playerIds.filter(x => x !== id);
delete playerInvitations[id];
refreshPlayerTitles();
refreshRemoveButtons();
}
function refreshPlayerTitles() {
playerIds.forEach((id, idx) => {
const el = document.querySelector(`#player-${id} .player-title`);
if (el) el.textContent = 'Spieler ' + (idx + 1);
});
}
function refreshRemoveButtons() {
const canRemove = playerIds.length > 2;
playerIds.forEach(id => {
const btn = document.querySelector(`#player-${id} .player-remove`);
if (btn) btn.style.display = canRemove ? '' : 'none';
});
}
document.addEventListener('change', e => {
const input = e.target;
if (input.type !== 'checkbox' && input.type !== 'radio') return;
if (input.type === 'radio') {
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r => {
r.closest('.check-item')?.classList.toggle('is-checked', r.checked);
});
if (input.checked && input.name.endsWith('-geschlecht')) {
const prefix = input.name.slice(0, -'-geschlecht'.length);
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
document.querySelectorAll(`input[name="${prefix}-werkzeuge"]`).forEach(cb => {
cb.checked = defaults.includes(cb.value);
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
});
}
} else {
input.closest('.check-item')?.classList.toggle('is-checked', input.checked);
}
});
function getChecked(name) {
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
}
function setFieldError(id, show) {
const el = document.getElementById(id);
if (el) el.style.display = show ? 'block' : 'none';
}
// ── Freunde-Modal ──
let currentInvitePlayerId = null;
let selectedFriend = null; // { userId, name }
async function oeffneFreundeModal(playerId) {
currentInvitePlayerId = playerId;
selectedFriend = null;
document.getElementById('friendSearch').value = '';
document.getElementById('friendDropdown').style.display = 'none';
document.getElementById('friendDropdown').innerHTML = '';
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('selectedFriendBox').textContent = '';
document.getElementById('btnEinladen').disabled = true;
document.getElementById('friendModal').style.display = 'flex';
if (freundeListe.length === 0) {
try {
const res = await fetch('/social/friends');
freundeListe = res.ok ? await res.json() : [];
} catch (_) { freundeListe = []; }
}
}
function filterFreunde(query) {
selectedFriend = null;
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('btnEinladen').disabled = true;
const dropdown = document.getElementById('friendDropdown');
dropdown.innerHTML = '';
const q = query.trim().toLowerCase();
if (!q) { dropdown.style.display = 'none'; return; }
const matches = freundeListe.filter(f => (f.user.name || '').toLowerCase().includes(q));
if (!matches.length) {
dropdown.innerHTML = '<div style="padding:0.6rem 0.75rem;color:var(--color-muted);font-size:0.9rem;">Keine Treffer.</div>';
dropdown.style.display = 'block';
return;
}
matches.forEach(f => {
const item = document.createElement('div');
item.className = 'friend-dropdown-item';
item.addEventListener('click', () => selectFriend(f.user.userId, f.user.name || 'Unbekannt'));
if (f.user.profilePicture) {
const img = document.createElement('img');
img.className = 'friend-avatar';
img.src = 'data:image/png;base64,' + f.user.profilePicture;
img.alt = '';
item.appendChild(img);
} else {
const av = document.createElement('div');
av.className = 'friend-avatar';
item.appendChild(av);
}
const span = document.createElement('span');
span.textContent = f.user.name || 'Unbekannt';
item.appendChild(span);
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
function selectFriend(userId, name) {
selectedFriend = { userId, name };
document.getElementById('friendSearch').value = name;
document.getElementById('friendDropdown').style.display = 'none';
const box = document.getElementById('selectedFriendBox');
box.textContent = '✓ ' + name;
box.style.display = 'block';
document.getElementById('btnEinladen').disabled = false;
}
async function confirmedEinladen() {
if (!selectedFriend) return;
await einladen(selectedFriend.userId, selectedFriend.name);
}
function schliesseFriendModal() {
document.getElementById('friendModal').style.display = 'none';
currentInvitePlayerId = null;
selectedFriend = null;
}
async function einladen(inviteeId, inviteeName) {
const id = currentInvitePlayerId;
schliesseFriendModal();
if (!id) return;
try {
const res = await fetch('/bdsm/einladung', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setupId, slotIndex: id, inviteeId }),
});
if (!res.ok) throw new Error();
const data = await res.json();
playerInvitations[id] = { einladungId: data.einladungId, status: 'PENDING', inviteeId, inviteeName };
renderPending(id);
startPoll();
} catch (_) {
showMessage('Einladung konnte nicht gesendet werden.', 'error');
}
}
function renderPending(id) {
const inv = playerInvitations[id];
if (!inv) return;
const body = document.getElementById(`p${id}-body`);
if (!body) return;
const headerInvBtn = document.querySelector(`#player-${id} .btn-invite`);
if (headerInvBtn) headerInvBtn.style.display = 'none';
if (inv.status === 'PENDING') {
// Badge
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
header.insertAdjacentHTML('afterbegin', `<span class="player-badge-pending">Ausstehend</span>`);
body.innerHTML = `
<div class="pending-info">
<div class="pending-name">${inv.inviteeName}</div>
<div>Einladung wurde gesendet warte auf Antwort…</div>
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
</div>`;
} else if (inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST') {
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
const modeLabel = inv.status === 'ACCEPTED_OWN' ? 'Eigenes Gerät' : 'Host-Gerät';
header.insertAdjacentHTML('afterbegin', `<span class="player-badge-accepted">✓ ${modeLabel}</span>`);
// Name readonly, Geschlecht gesperrt (kommt vom Profil der eingeladenen Person)
const nameField = `<input type="text" id="p${id}-name" value="${inv.inviteeName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`;
body.innerHTML = buildPlayerBody(id, nameField, true);
// Defaults laden falls verfügbar
if (inv.defaults) {
restorePlayer(id, {
geschlecht: inv.defaults.geschlecht,
spieltMit: inv.defaults.spieltMit || [],
rollen: inv.defaults.rollen || [],
werkzeuge: inv.defaults.werkzeuge || [],
});
}
} else if (inv.status === 'DECLINED' || inv.status === 'CANCELLED') {
// Slot wieder freigeben
const header = document.querySelector(`#player-${id} .player-card-header`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
if (headerInvBtn) headerInvBtn.style.display = '';
playerInvitations[id] = null;
body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
}
}
async function cancelEinladung(id) {
const inv = playerInvitations[id];
if (!inv) return;
await fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
playerInvitations[id] = null;
// UI zurücksetzen
const header = document.querySelector(`#player-${id} .player-card-header`);
if (header) {
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
const invBtn = header.querySelector('.btn-invite');
if (invBtn) invBtn.style.display = '';
}
const body = document.getElementById(`p${id}-body`);
if (body) body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
}
// ── Polling ──
function startPoll() {
if (pollIntervalId) return;
pollIntervalId = setInterval(pollEinladungen, 3000);
}
function stopPoll() {
if (pollIntervalId) { clearInterval(pollIntervalId); pollIntervalId = null; }
}
async function pollEinladungen() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (!hasPending) { stopPoll(); return; }
try {
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
if (!res.ok) return;
const liste = await res.json();
for (const e of liste) {
const id = playerIds.find(pid => {
const inv = playerInvitations[pid];
return inv && inv.einladungId === e.einladungId;
});
if (!id) continue;
const inv = playerInvitations[id];
if (!inv || inv.status === e.status) continue;
inv.status = e.status;
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
// Defaults der eingeladenen Person laden
try {
const dRes = await fetch(`/user/${inv.inviteeId}/bdsm-defaults`);
if (dRes.ok) inv.defaults = await dRes.json();
} catch (_) {}
}
renderPending(id);
}
} catch (_) {}
updateWeiterBtn();
}
function updateWeiterBtn() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
document.getElementById('weiterBtn').disabled = hasPending;
}
// ── Validation & Weiter ──
function weiter() {
hideMessage();
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (hasPending) {
showMessage('Bitte warte, bis alle Einladungen beantwortet wurden.', 'error');
return;
}
let valid = true;
playerIds.forEach(id => setFieldError(`p${id}-partner-err`, false));
const mitspieler = playerIds.map(id => {
const inv = playerInvitations[id];
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
const geschlecht = getChecked(`p${id}-geschlecht`);
const spieltMit = getChecked(`p${id}-spieltmit`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-name-err`, !name);
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || geschlecht.length === 0 || spieltMit.length === 0 || rollen.length === 0 || werkzeuge.length === 0) {
valid = false;
}
return {
name,
geschlecht: geschlecht[0] || null,
spieltMit, rollen, werkzeuge,
userId: inv ? inv.inviteeId : null,
eigenesGeraet: inv ? inv.status === 'ACCEPTED_OWN' : false,
};
});
if (!valid) { showMessage('Bitte alle Felder für jeden Spieler ausfüllen.', 'error'); return; }
const allRoles = new Set(mitspieler.flatMap(p => p.rollen));
const missingRoles = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
if (missingRoles.length > 0) {
showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' +
missingRoles.map(r => ROLE_LABELS[r]).join(', '), 'error');
return;
}
let partnerFehler = false;
mitspieler.forEach((player, i) => {
const andereGeschlechter = mitspieler.filter((_, j) => j !== i).map(p => p.geschlecht);
const hatPartner = player.spieltMit.some(g => andereGeschlechter.includes(g));
if (!hatPartner) { setFieldError(`p${playerIds[i]}-partner-err`, true); partnerFehler = true; }
});
if (partnerFehler) { showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error'); return; }
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
sessionStorage.setItem('bdsm-session-setup', JSON.stringify({ settings, mitspieler }));
window.location.href = '/bdsmtasks.html';
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text; el.className = `message ${type}`; el.style.display = 'block';
}
function hideMessage() { document.getElementById('message').style.display = 'none'; }
function restorePlayer(id, data) {
if (data.geschlecht) {
const radio = document.querySelector(`input[name="p${id}-geschlecht"][value="${data.geschlecht}"]`);
if (radio) { radio.checked = true; radio.closest('.check-item')?.classList.add('is-checked'); }
}
(data.spieltMit || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-spieltmit"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.rollen || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-rollen"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.werkzeuge || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-werkzeuge"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
}
// ── Init ──
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
if (savedSetup) {
const { mitspieler } = JSON.parse(savedSetup);
mitspieler.forEach((p, i) => {
const id = addPlayer(p.name, i === 0);
restorePlayer(id, p);
});
} else {
Promise.all([
fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null),
fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}))
]).then(([user, defaults]) => {
myUserId = user?.userId || null;
addPlayer(user ? user.name : '', true);
addPlayer();
const selfId = playerIds[0];
restorePlayer(selfId, {
geschlecht: user?.geschlecht || null,
spieltMit: defaults.spieltMit || [],
rollen: defaults.rollen || [],
werkzeuge: defaults.werkzeuge || [],
});
});
}
</script>
</body>
</html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Aufgaben-Gruppen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { max-width: 700px; }
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
@@ -119,7 +119,7 @@
<div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/sessionbdsmplayers.html'">← Zurück</button>
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmplayers.html'">← Zurück</button>
<button style="flex:2;" onclick="weiter()">Weiter</button>
</div>
</div>
@@ -130,7 +130,7 @@
<script src="/js/sidebar.js"></script>
<script>
if (!sessionStorage.getItem('bdsm-session-setup')) {
window.location.replace('/sessionbdsm.html');
window.location.replace('/bdsm.html');
}
const savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
@@ -308,18 +308,19 @@
}
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
window.location.href = '/sessionbdsmtoys.html';
window.location.href = '/bdsmtoys.html';
}
Promise.all([
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] user HTTP', r.status), { content: [] })),
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] abo HTTP', r.status), { content: [] })),
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] system HTTP', r.status), { content: [] })),
]).then(([own, abo, system]) => {
console.log('[bdsmtasks] own:', own, 'abo:', abo, 'system:', system);
renderList('listOwn', own.content || []);
renderList('listSubscribed', abo.content || []);
renderList('listSystem', system.content || []);
});
}).catch(err => console.error('[bdsmtasks] Fehler beim Laden:', err));
</script>
</body>
</html>

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Toys XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { max-width: 700px; }
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
@@ -74,7 +74,7 @@
<div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/sessionbdsmtasks.html'">← Zurück</button>
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmtasks.html'">← Zurück</button>
<button style="flex:2;" onclick="spielStarten()">Spiel starten</button>
</div>
</div>
@@ -85,7 +85,7 @@
<script src="/js/sidebar.js"></script>
<script>
const savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null');
if (!savedGruppen) window.location.replace('/sessionbdsm.html');
if (!savedGruppen) window.location.replace('/bdsm.html');
// Previously saved toy selection (when navigating back from game page)
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
@@ -241,7 +241,7 @@
try {
// 1. Session anlegen
const sessionRes = await fetch('/session', {
const sessionRes = await fetch('/bdsm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -249,6 +249,7 @@
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
aufgabenProLevel: settings.aufgabenProLevel,
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
setupId: sessionStorage.getItem('bdsm-setup-id'),
}),
});
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
@@ -258,7 +259,7 @@
// 2. Mitspieler hinzufügen
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
for (const p of setup.mitspieler) {
const res = await fetch(`/session/${sessionId}/mitspieler`, {
const res = await fetch(`/bdsm/${sessionId}/mitspieler`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -267,13 +268,15 @@
spieltMit: p.spieltMit,
rollen: p.rollen,
verfuegbareWerkzeuge: p.werkzeuge,
userId: p.userId || null,
eigenesGeraet: p.eigenesGeraet || false,
}),
});
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
}
// 3. Aufgaben setzen
const aufgabenRes = await fetch(`/session/${sessionId}/aufgaben`, {
const aufgabenRes = await fetch(`/bdsm/${sessionId}/aufgaben`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(gameContent),
@@ -281,7 +284,7 @@
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
sessionStorage.setItem('bdsm-session-id', sessionId);
window.location.href = '/sessionbdsmingame.html';
window.location.href = '/bdsmingame.html';
} catch (e) {
showMessage(e.message, 'error');
btn.disabled = false;

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Warten XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.wait-card {
text-align: center;
padding: 3rem 1rem;
}
.wait-icon { font-size: 3rem; margin-bottom: 1.5rem; animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
.wait-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.75rem; }
.wait-sub { font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; margin-bottom: 2rem; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content wait-card">
<div class="wait-icon"></div>
<div class="wait-title">Warte auf Spielstart…</div>
<div class="wait-sub" id="sub">Der Host startet das Spiel in Kürze. Diese Seite aktualisiert sich automatisch.</div>
<div class="message" id="message" style="display:none;"></div>
<button class="secondary" onclick="abbrechen()">Abbrechen</button>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
const params = new URLSearchParams(location.search);
const einladungId = params.get('id');
if (!einladungId) window.location.replace('/userhome.html');
let pollInterval = null;
async function pruefen() {
try {
const res = await fetch(`/bdsm/einladung/${einladungId}`);
if (!res.ok) return;
const data = await res.json();
if (data.status === 'CANCELLED') {
stopPoll();
zeigeFehler('Die Einladung wurde abgebrochen.');
return;
}
if (data.sessionId) {
stopPoll();
// Meinen Mitspieler laden
try {
const mRes = await fetch(`/bdsm/${data.sessionId}/mitspieler/me`);
if (mRes.ok) {
const mData = await mRes.json();
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
sessionStorage.setItem('bdsm-guest-name', mData.name);
}
} catch (_) {}
sessionStorage.setItem('bdsm-session-id', data.sessionId);
sessionStorage.setItem('bdsm-is-guest', 'true');
window.location.replace('/bdsmingame.html');
}
} catch (_) {}
}
function stopPoll() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}
function zeigeFehler(text) {
document.getElementById('sub').style.display = 'none';
const el = document.getElementById('message');
el.textContent = text;
el.className = 'message error';
el.style.display = '';
}
async function abbrechen() {
stopPoll();
await fetch(`/bdsm/einladung/${einladungId}`, { method: 'DELETE' }).catch(() => {});
window.location.href = '/userhome.html';
}
// Sofort prüfen, dann alle 3 Sekunden
pruefen();
pollInterval = setInterval(pruefen, 3000);
</script>
</body>
</html>

View File

@@ -406,11 +406,11 @@
</div>
</div>
<!-- Tabs: Feed | Pinnwand | Lock-Historie -->
<!-- Tabs: Feed | Pinnwand | Spielhistorie -->
<div class="profil-tabs" style="margin-top:1.25rem;">
<button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button>
<button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button>
<button class="profil-tab-btn" id="tabBtnLockHistory" onclick="switchProfilTab('lockhistory', this); loadLockHistory()">Lock-Historie</button>
<button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this); loadGameHistory()">Spielhistorie</button>
</div>
<!-- Feed Tab (vorausgewählt) -->
@@ -429,10 +429,10 @@
<div id="pinnwandList"></div>
</div>
<!-- Lock-Historie Tab -->
<div class="profil-tab-panel" id="tab-lockhistory">
<div id="lockHistoryList" style="margin-top:0.75rem;"></div>
<p id="lockHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p>
<!-- Spielhistorie Tab -->
<div class="profil-tab-panel" id="tab-gamehistory">
<div id="gameHistoryList" style="margin-top:0.75rem;"></div>
<p id="gameHistoryEmpty" style="color:var(--color-muted);font-size:0.9rem;display:none;">Keine abgeschlossenen Locks vorhanden.</p>
</div>
</div>
@@ -467,6 +467,7 @@
// ── State ──
const params = new URLSearchParams(window.location.search);
let targetUserId = params.get('userId');
const previewMode = params.get('preview'); // 'FREUND' | 'UNBEKANNT' | null
let myUserId = null;
let isOwnProfile = false;
let profileData = null;
@@ -524,25 +525,52 @@
}
myUserId = me ? me.userId : null;
isOwnProfile = me && me.userId === profile.userId;
isOwnProfile = !previewMode && me && me.userId === profile.userId;
profileData = profile;
allImages = images;
// ── Preview-Modus: friendStatus simulieren ──
if (previewMode) {
profile.friendStatus = (previewMode === 'FREUND') ? 'FRIEND' : 'NONE';
showPreviewBanner(previewMode);
}
const isFriend = profile.friendStatus === 'FRIEND';
document.title = profile.name + ' XXX The Game';
renderHeader(profile);
renderGallery();
loadFriends();
if (profile.beschreibung) {
// ── Galerie ──
if (canSee(profile.sichtbarkeitGalerie, isFriend, isOwnProfile)) {
renderGallery();
}
// ── Freunde ──
if (canSee(profile.sichtbarkeitFreunde, isFriend, isOwnProfile)) {
loadFriends();
}
if (profile.beschreibung && canSee(profile.sichtbarkeitGrunddaten, isFriend, isOwnProfile)) {
document.getElementById('beschreibungLabel').style.display = '';
const el = document.getElementById('profilBeschreibung');
el.style.display = '';
el.textContent = profile.beschreibung;
}
await loadPinnwand();
// ── Tabs: Feed, Pinnwand, Spielhistorie ──
applyTabPrivacy(profile, isFriend);
if (canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile)) {
await loadPinnwand();
}
document.getElementById('profileView').style.display = '';
// Feed-Tab ist vorausgewählt → sofort laden
loadProfilPosts();
profilPostsObserver.observe(document.getElementById('profilPostsSentinel'));
// Feed-Tab ist vorausgewählt → sofort laden (nur wenn sichtbar)
if (canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile)) {
loadProfilPosts();
profilPostsObserver.observe(document.getElementById('profilPostsSentinel'));
}
} catch {
document.getElementById('loadingHint').textContent = 'Fehler beim Laden.';
document.getElementById('loadingHint').style.display = '';
@@ -568,14 +596,16 @@
d.innerHTML = `<span class="label">${label}</span><span class="value">${esc(value)}</span>`;
tags.appendChild(d);
};
if (profile.alter) addTag('Alter', profile.alter + ' J.');
if (profile.groesse) addTag('Größe', profile.groesse + ' cm');
if (profile.gewicht) addTag('Gewicht', profile.gewicht + ' kg');
if (profile.geschlecht) addTag('Geschlecht', GESCHLECHT_LABEL[profile.geschlecht] || profile.geschlecht);
if (profile.neigung) addTag('Neigung', NEIGUNG_LABEL[profile.neigung] || profile.neigung);
if (profile.beziehungsstatus) addTag('Beziehung', BEZIEHUNG_LABEL[profile.beziehungsstatus] || profile.beziehungsstatus);
if (profile.lockeeXp > 0) addTag('🔒 Lockee XP', profile.lockeeXp + ' XP');
if (profile.keyholderXp > 0) addTag('🔑 Keyholder XP', profile.keyholderXp + ' XP');
const grunddatenVisible = canSee(profile.sichtbarkeitGrunddaten, profile.friendStatus === 'FRIEND', isOwnProfile);
if (grunddatenVisible && profile.alter) addTag('Alter', profile.alter + ' J.');
if (grunddatenVisible && profile.groesse) addTag('Größe', profile.groesse + ' cm');
if (grunddatenVisible && profile.gewicht) addTag('Gewicht', profile.gewicht + ' kg');
if (grunddatenVisible && profile.geschlecht) addTag('Geschlecht', GESCHLECHT_LABEL[profile.geschlecht] || profile.geschlecht);
if (grunddatenVisible && profile.neigung) addTag('Neigung', NEIGUNG_LABEL[profile.neigung] || profile.neigung);
if (grunddatenVisible && profile.beziehungsstatus) addTag('Beziehung', BEZIEHUNG_LABEL[profile.beziehungsstatus] || profile.beziehungsstatus);
const xpVisible = canSee(profile.sichtbarkeitXp, profile.friendStatus === 'FRIEND', isOwnProfile);
if (xpVisible && profile.lockeeXp > 0) addTag('🔒 Lockee XP', profile.lockeeXp + ' XP');
if (xpVisible && profile.keyholderXp > 0) addTag('🔑 Keyholder XP', profile.keyholderXp + ' XP');
// Action buttons
const actions = document.getElementById('profileActions');
@@ -596,6 +626,50 @@
}
}
// ── Privacy helpers ──
function canSee(sichtbarkeit, isFriend, isOwn) {
if (isOwn || !sichtbarkeit) return true;
if (sichtbarkeit === 'ALLE') return true;
if (sichtbarkeit === 'NUR_FREUNDE') return isFriend;
// NUR_ICH
return false;
}
function applyTabPrivacy(profile, isFriend) {
const showFeed = canSee(profile.sichtbarkeitFeed, isFriend, isOwnProfile);
const showPinnwand = canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile);
const showHistory = canSee(profile.sichtbarkeitLockhistorie, isFriend, isOwnProfile);
const btnFeed = document.getElementById('tabBtnPosts');
const btnPinnwand = document.getElementById('tabBtnPinnwand');
const btnHistory = document.getElementById('tabBtnGameHistory');
if (!showFeed) { btnFeed.style.display = 'none'; document.getElementById('tab-posts').classList.remove('active'); }
if (!showPinnwand) { btnPinnwand.style.display = 'none'; }
if (!showHistory) { btnHistory.style.display = 'none'; }
// Ersten sichtbaren Tab aktivieren
if (!showFeed) {
btnFeed.classList.remove('active');
document.getElementById('tab-posts').classList.remove('active');
if (showPinnwand) {
btnPinnwand.classList.add('active');
document.getElementById('tab-pinnwand').classList.add('active');
} else if (showHistory) {
btnHistory.classList.add('active');
document.getElementById('tab-gamehistory').classList.add('active');
}
}
}
function showPreviewBanner(mode) {
const banner = document.createElement('div');
banner.style.cssText = 'background:var(--color-secondary);border:1px solid var(--color-primary);border-radius:8px;padding:0.65rem 1rem;margin-bottom:1rem;font-size:0.88rem;display:flex;align-items:center;justify-content:space-between;gap:0.75rem;';
const label = mode === 'FREUND' ? '👥 Vorschau aus Freundessicht' : '👤 Vorschau aus Sicht einer fremden Person';
banner.innerHTML = `<span>${label}</span><a href="/einstellungen.html" style="font-size:0.82rem;color:var(--color-primary);">← Einstellungen</a>`;
document.getElementById('profileView').prepend(banner);
}
// ── Tab switching ──
function switchProfilTab(name, btn) {
document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active'));
@@ -777,47 +851,60 @@
</div>`;
}
// ── Lock-Historie ──
let lockHistoryLoaded = false;
async function loadLockHistory() {
if (lockHistoryLoaded) return;
lockHistoryLoaded = true;
const list = document.getElementById('lockHistoryList');
const empty = document.getElementById('lockHistoryEmpty');
// ── Spielhistorie ──
const GAME_TYPE_ICON = {
CARDLOCK: '<span style="position:relative;display:inline-block;line-height:1;"><img src="/img/card.png" style="width:2.7rem;height:2.7rem;object-fit:contain;display:block;"><span style="position:absolute;bottom:-2px;right:-4px;font-size:1.3rem;line-height:1;">🔒</span></span>',
TIMELOCK: '<span style="position:relative;display:inline-block;line-height:1;"><span style="font-size:2.7rem;">⏰</span><span style="position:absolute;bottom:-2px;right:-4px;font-size:1.3rem;line-height:1;">🔒</span></span>',
BDSM: '⛓️',
VANILLA: '❤️'
};
const ROLE_BADGE = { KEYHOLDER: '🔑', LOCKEE: '🔒', PLAYER: '' };
let gameHistoryLoaded = false;
async function loadGameHistory() {
if (gameHistoryLoaded) return;
gameHistoryLoaded = true;
const list = document.getElementById('gameHistoryList');
const empty = document.getElementById('gameHistoryEmpty');
list.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;">Lädt…</p>';
try {
const res = await fetch('/lockhistory?userId=' + targetUserId);
const res = await fetch('/gamehistory?userId=' + targetUserId);
if (!res.ok) { list.innerHTML = ''; return; }
const entries = await res.json();
list.innerHTML = '';
if (entries.length === 0) { empty.style.display = ''; return; }
list.innerHTML = entries.map(e => {
const icon = e.role === 'KEYHOLDER' ? '🔑' : '🔒';
const partner = e.role === 'KEYHOLDER'
? (e.lockeeName ? `<span style="color:var(--color-muted);font-size:0.78rem;">Lockee: ${esc(e.lockeeName)}</span>` : '')
: (e.keyholderName ? `<span style="color:var(--color-muted);font-size:0.78rem;">Keyholder: ${esc(e.keyholderName)}</span>` : '<span style="color:var(--color-muted);font-size:0.78rem;">Self-Lock</span>');
const days = Math.floor(e.durationMinutes / 1440);
const hours = Math.floor((e.durationMinutes % 1440) / 60);
const mins = e.durationMinutes % 60;
const dur = days > 0
const gameIconRaw = GAME_TYPE_ICON[e.gameType] || '🎮';
const gameIcon = (e.gameType === 'CARDLOCK' || e.gameType === 'TIMELOCK')
? gameIconRaw
: `<span style="font-size:2.7rem;line-height:1;">${gameIconRaw}</span>`;
const days = Math.floor(e.durationMinutes / 1440);
const hours = Math.floor((e.durationMinutes % 1440) / 60);
const mins = e.durationMinutes % 60;
const dur = days > 0
? `${days}d ${hours}h ${mins}min`
: hours > 0 ? `${hours}h ${mins}min` : `${mins}min`;
const avatar = e.partnerPic
? `<img src="data:image/jpeg;base64,${e.partnerPic}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">`
: `<div style="width:40px;height:40px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1.1rem;">👤</div>`;
return `<div style="display:flex;align-items:center;gap:0.75rem;padding:0.65rem 0;border-bottom:1px solid var(--color-secondary);">
<div style="position:relative;width:40px;height:40px;flex-shrink:0;">
${avatar}
<span style="position:absolute;top:-3px;left:-3px;font-size:1.4rem;line-height:1;">${icon}</span>
</div>
const participants = (e.participants || []).map(p => {
const badge = ROLE_BADGE[p.role] || '';
const img = p.picture
? `<img src="data:image/png;base64,${p.picture}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:block;">`
: `<div style="width:40px;height:40px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;">👤</div>`;
return `<a href="/benutzer.html?userId=${esc(p.userId)}" style="position:relative;flex-shrink:0;text-decoration:none;" title="${esc(p.name || '')} (${p.role})">
${img}
${badge ? `<span style="position:absolute;top:-4px;right:-4px;font-size:0.9rem;line-height:1;">${badge}</span>` : ''}
</a>`;
}).join('');
return `<div style="display:flex;align-items:flex-start;gap:0.85rem;padding:0.75rem 0;border-bottom:1px solid var(--color-secondary);">
<div style="flex-shrink:0;width:3rem;text-align:center;">${gameIcon}</div>
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.92rem;">${esc(e.lockName) || 'Unbenanntes Lock'}</div>
<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-top:0.15rem;">
${partner}
<span style="color:var(--color-muted);font-size:0.78rem;">⏱ ${dur}</span>
<span style="color:var(--color-muted);font-size:0.78rem;">${fmtDate(e.unlockTime)}</span>
</div>
<div style="font-weight:600;font-size:0.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(e.gameName) || 'Unbenannt'}</div>
<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.15rem;">⏱ ${dur} &nbsp;·&nbsp; ${new Date(e.endTime).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'})}</div>
</div>
<div style="display:flex;gap:0.35rem;align-items:center;flex-shrink:0;">${participants}</div>
</div>`;
}).join('');
} catch(e) { list.innerHTML = ''; }
@@ -954,7 +1041,7 @@
: '◉';
const bildRaw = bilderCarousel(p.bilder);
const bildHtml = bildRaw
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}<div class="post-bild-hover-icon">💬</div></div>`
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}</div>`
: '';
const privacyLabel = p.isPublic ? '' : '<span style="font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem;">🔒 privat</span>';

View File

@@ -167,6 +167,33 @@
}
.blind-hint-icon { font-size: 1.4rem; flex-shrink: 0; }
/* Bestätigungs-Modal */
.confirm-modal-bg {
display: none; position: fixed; inset: 0; z-index: 600;
align-items: center; justify-content: center;
}
.confirm-modal-bg.open { display: flex; }
.confirm-modal-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
.confirm-modal-box {
position: relative; background: var(--color-card);
border: 1px solid var(--color-secondary); border-radius: 12px;
padding: 1.75rem 1.5rem 1.5rem; max-width: 380px; width: 92%; z-index: 1;
display: flex; flex-direction: column; gap: 1rem;
}
.confirm-modal-title {
font-weight: 700; font-size: 1rem; padding-right: 1.5rem;
}
.confirm-modal-text {
font-size: 0.9rem; color: var(--color-muted); line-height: 1.5;
}
.confirm-modal-actions {
display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 0.25rem;
}
.confirm-modal-actions button { width: auto; padding: 0.6rem 1.3rem; font-size: 0.9rem; }
.confirm-modal-cancel { background: var(--color-secondary) !important; color: var(--color-text) !important; }
.confirm-modal-ok { background: #c0392b !important; }
.confirm-modal-ok:hover { background: #a93226 !important; }
/* Entsperrcode-Modal */
.unlock-modal-bg {
display: none; position: fixed; inset: 0; z-index: 500;
@@ -214,6 +241,20 @@
</div>
</div>
<!-- Bestätigungs-Modal -->
<div class="confirm-modal-bg" id="confirmModal">
<div class="confirm-modal-overlay" onclick="confirmCancel()"></div>
<div class="confirm-modal-box">
<button onclick="confirmCancel()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;" title="Schließen"></button>
<div class="confirm-modal-title" id="confirmTitle"></div>
<div class="confirm-modal-text" id="confirmText"></div>
<div class="confirm-modal-actions">
<button class="confirm-modal-cancel" onclick="confirmCancel()">Abbrechen</button>
<button class="confirm-modal-ok" id="confirmOkBtn">Bestätigen</button>
</div>
</div>
</div>
<!-- Lockee-Einladungs-Dialog -->
<div class="lockee-dialog-bg" id="lockeeInviteDialog">
<div class="lockee-dialog-overlay" onclick="closeLockeeInviteDialog()"></div>
@@ -475,9 +516,29 @@
renderSentPage();
}
// ── Bestätigungs-Modal ──
let _confirmResolve = null;
function showConfirm(title, text) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmText').textContent = text;
document.getElementById('confirmModal').classList.add('open');
return new Promise(resolve => {
_confirmResolve = resolve;
document.getElementById('confirmOkBtn').onclick = () => { confirmClose(true); };
});
}
function confirmCancel() { confirmClose(false); }
function confirmClose(result) {
document.getElementById('confirmModal').classList.remove('open');
if (_confirmResolve) { _confirmResolve(result); _confirmResolve = null; }
}
// ── Aktionen: Empfangen ──
async function declineLockeeInvitation(token, btn) {
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
if (!await showConfirm('Einladung ablehnen', 'Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
btn.disabled = true;
try {
const res = await fetch('/lockee/invitation/' + encodeURIComponent(token), { method: 'DELETE' });
@@ -487,7 +548,7 @@
}
async function declineKhInvitation(token, btn) {
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
if (!await showConfirm('Einladung ablehnen', 'Bist du sicher, dass du diese Einladung ablehnen möchtest?')) return;
btn.disabled = true;
try {
const res = await fetch('/keyholder/invitations/mine/' + encodeURIComponent(token), { method: 'DELETE' });
@@ -498,10 +559,11 @@
// ── Aktionen: Gesendet ──
async function cancelSentInvitation(token, type, btn) {
const msg = type === 'lockee'
? 'Einladung zurückziehen? Das Lock wird gelöscht und der Lockee wird benachrichtigt.'
: 'Keyholder-Einladung zurückziehen? Der Keyholder wird benachrichtigt.';
if (!confirm(msg)) return;
const title = 'Einladung zurückziehen';
const text = type === 'lockee'
? 'Das Lock wird gelöscht und der Lockee wird benachrichtigt.'
: 'Der Keyholder wird benachrichtigt.';
if (!await showConfirm(title, text)) return;
btn.disabled = true;
const url = type === 'lockee'
? '/lockee/invitations/sent/' + encodeURIComponent(token)
@@ -634,7 +696,7 @@
async function declineLockeeInviteDialog() {
if (!activeDialogToken) return;
if (!confirm('Bist du sicher, dass du diese Einladung ablehnen möchtest? Das Lock wird gelöscht.')) return;
if (!await showConfirm('Einladung ablehnen', 'Bist du sicher, dass du diese Einladung ablehnen möchtest? Das Lock wird gelöscht.')) return;
const declineBtn = document.querySelector('.btn-decline');
declineBtn.disabled = true;
try {

View File

@@ -0,0 +1,968 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstellungen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.settings-page h1 {
font-size: 1.6rem;
margin-bottom: 1.5rem;
}
/* ── Accordion ── */
.settings-section {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
margin-bottom: 0.75rem;
overflow: hidden;
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 1rem 1.25rem;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.settings-section-header:hover {
background: rgba(255,255,255,0.03);
}
.settings-section-title {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1rem;
font-weight: 600;
}
.settings-section-arrow {
font-size: 0.75rem;
color: var(--color-muted);
transition: transform 0.2s;
}
.settings-section.open .settings-section-arrow {
transform: rotate(90deg);
}
.settings-section-body {
display: none;
padding: 0 1.25rem 1.25rem;
border-top: 1px solid var(--color-secondary);
}
.settings-section.open .settings-section-body {
display: block;
}
/* ── Einstellungs-Zeilen ── */
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 0;
}
.settings-row-info {
flex: 1;
}
.settings-row-label {
font-size: 0.95rem;
font-weight: 500;
margin-bottom: 0.15rem;
}
.settings-row-desc {
font-size: 0.8rem;
color: var(--color-muted);
}
.settings-row select {
width: auto;
min-width: 150px;
padding: 0.45rem 0.75rem;
margin: 0;
font-size: 0.9rem;
}
/* ── Benachrichtigungen Grid ── */
.notif-grid {
display: grid;
grid-template-columns: 1fr 5rem 5rem;
align-items: center;
}
.notif-grid-row {
display: contents;
}
.notif-grid-row > * {
padding: 0.75rem 0;
}
.notif-grid-row.notif-header > * {
padding-bottom: 0.4rem;
}
.notif-col-check {
display: flex;
justify-content: center;
align-items: center;
}
.notif-col-check input[type="checkbox"] {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--color-primary, #c0392b);
cursor: pointer;
}
/* ── Speichern-Feedback (Toast) ── */
.save-toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%) translateY(2rem);
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-left: 3px solid var(--color-success, #4caf50);
border-radius: 8px;
padding: 0.6rem 1.25rem;
font-size: 0.88rem;
color: var(--color-success, #4caf50);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 500;
white-space: nowrap;
}
.save-toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ── Spiel-Einstellungen Check-Items ── */
.spiel-check-group {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.35rem;
}
.spiel-check-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: var(--color-secondary);
border: 1px solid transparent;
border-radius: 6px;
padding: 0.35rem 0.65rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
font-size: 0.88rem;
}
.spiel-check-item.is-checked { border-color: var(--color-primary); }
.spiel-check-item input {
accent-color: var(--color-primary);
width: auto;
cursor: pointer;
}
.spiel-subsection {
margin-bottom: 1.25rem;
}
.spiel-subsection-title {
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--color-secondary);
}
.spiel-field { margin-bottom: 1rem; }
.spiel-field > .settings-row-label { margin-bottom: 0.2rem; }
/* ── Modal ── */
.modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 200;
align-items: center;
justify-content: center;
}
.modal-backdrop.visible { display: flex; }
.modal {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 360px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
}
.modal h2 {
color: var(--color-primary);
font-size: 1.2rem;
margin-bottom: 1.25rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.25rem;
}
.modal-actions button { flex: 1; margin-top: 0; }
/* ── Separator ── */
.settings-separator {
border: none;
border-top: 1px solid var(--color-secondary);
margin: 1.25rem 0 0;
}
/* ── Vorschau-Buttons ── */
.preview-row {
padding-top: 1.25rem;
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
align-items: center;
}
.preview-row span {
font-size: 0.85rem;
color: var(--color-muted);
flex-basis: 100%;
margin-bottom: 0.25rem;
}
.preview-row button {
width: auto;
padding: 0.5rem 1.1rem;
margin: 0;
font-size: 0.9rem;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h1>⚙️ Einstellungen</h1>
<!-- Grunddaten -->
<div class="settings-section" id="sec-grunddaten">
<div class="settings-section-header" onclick="toggleSection('sec-grunddaten')">
<span class="settings-section-title">👤 Grunddaten</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Nickname</div>
<div class="settings-row-desc" id="gd-name"></div>
</div>
<button onclick="openNameDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">E-Mail</div>
<div class="settings-row-desc" id="gd-email"></div>
</div>
<button onclick="openEmailDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Geburtsdatum</div>
<div class="settings-row-desc" id="gd-geburtsdatum"></div>
</div>
<button onclick="openGeburtsdatumDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;">Ändern</button>
</div>
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label" style="color:var(--color-primary);">Konto löschen</div>
<div class="settings-row-desc">Alle Daten werden unwiderruflich gelöscht</div>
</div>
<button onclick="openDeleteDialog()" style="width:auto;padding:0.45rem 1rem;margin:0;font-size:0.9rem;background:transparent;border:1px solid var(--color-secondary);color:var(--color-muted);">Löschen</button>
</div>
</div>
</div>
<!-- Spiel Einstellungen -->
<div class="settings-section" id="sec-spiel">
<div class="settings-section-header" onclick="toggleSection('sec-spiel')">
<span class="settings-section-title">🕹️ Spiel Einstellungen</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<!-- BDSM Game -->
<div class="spiel-subsection">
<div class="spiel-subsection-title">BDSM Game</div>
<div class="spiel-field">
<div class="settings-row-label">Spiele mit Geschlecht</div>
<div class="spiel-check-group" id="bdsm-spieltmit">
<label class="spiel-check-item"><input type="checkbox" value="WEIBLICH"> Weiblich</label>
<label class="spiel-check-item"><input type="checkbox" value="DIVERS"> Divers</label>
<label class="spiel-check-item"><input type="checkbox" value="MAENNLICH"> Männlich</label>
</div>
</div>
<div class="spiel-field">
<div class="settings-row-label">Meine Rollen</div>
<div class="spiel-check-group" id="bdsm-rollen">
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_AKTIV"> Aufgaben aktiv</label>
<label class="spiel-check-item"><input type="checkbox" value="AUFGABE_PASSIV"> Aufgaben passiv</label>
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_AKTIV"> Bestrafungen aktiv</label>
<label class="spiel-check-item"><input type="checkbox" value="BESTRAFUNG_PASSIV"> Bestrafungen passiv</label>
</div>
</div>
<div class="spiel-field">
<div class="settings-row-label">Was ich einsetze</div>
<div class="spiel-check-group" id="bdsm-werkzeuge">
<label class="spiel-check-item"><input type="checkbox" value="MUND"> Mund</label>
<label class="spiel-check-item"><input type="checkbox" value="VAGINA"> Vagina</label>
<label class="spiel-check-item"><input type="checkbox" value="PENIS"> Penis</label>
<label class="spiel-check-item"><input type="checkbox" value="ANUS"> Anus</label>
<label class="spiel-check-item"><input type="checkbox" value="UMSCHNALLDILDO"> Umschnall-Dildo</label>
</div>
</div>
</div>
</div>
</div>
<!-- Benachrichtigungen -->
<div class="settings-section" id="sec-benachrichtigungen">
<div class="settings-section-header" onclick="toggleSection('sec-benachrichtigungen')">
<span class="settings-section-title">🔔 Benachrichtigungen</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<div class="notif-grid">
<!-- Header -->
<div class="notif-grid-row notif-header">
<div></div>
<div class="notif-col-check settings-row-label">In-App</div>
<div class="notif-col-check settings-row-label">E-Mail</div>
</div>
<!-- Einladungen -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Einladungen</div>
<div class="settings-row-desc">Einladungen zu Locks und Spielen, Annahmen und Ablehnungen</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-INVITATION-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-INVITATION-email" onchange="saveNotifications()">
</div>
</div>
<!-- Spielstatus -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Spielstatus</div>
<div class="settings-row-desc">Karten, Aufgaben, Verifikationen, Einfrierungen und andere Spielereignisse</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-GAME_STATE-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-GAME_STATE-email" onchange="saveNotifications()">
</div>
</div>
<!-- Notfall -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Notfall</div>
<div class="settings-row-desc">Notfall-Entsperrungen und dringende Meldungen</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-EMERGENCY-inApp" onchange="saveNotifications()">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-EMERGENCY-email" onchange="saveNotifications()">
</div>
</div>
<!-- Freundschaftsanfragen -->
<div class="notif-grid-row">
<div>
<div class="settings-row-label">Freundschaftsanfragen</div>
<div class="settings-row-desc">Neue Freundschaftsanfragen von anderen Nutzern</div>
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-FRIENDREQUEST-inApp" checked disabled
title="In-App-Benachrichtigungen für Freundschaftsanfragen sind immer aktiv">
</div>
<div class="notif-col-check">
<input type="checkbox" id="notif-FRIENDREQUEST-email" onchange="saveNotifications()">
</div>
</div>
</div>
</div>
</div>
<!-- Datenschutz -->
<div class="settings-section" id="sec-datenschutz">
<div class="settings-section-header" onclick="toggleSection('sec-datenschutz')">
<span class="settings-section-title">🛡️ Datenschutz</span>
<span class="settings-section-arrow"></span>
</div>
<div class="settings-section-body">
<!-- Grunddaten -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Grunddaten</div>
<div class="settings-row-desc">Alter, Größe, Gewicht, Geschlecht, Neigung, Beziehungsstatus, Beschreibung</div>
</div>
<select id="sv-grunddaten" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Galerie -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Galerie</div>
<div class="settings-row-desc">Fotos auf dem Profil</div>
</div>
<select id="sv-galerie" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Freunde -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Freundesliste</div>
<div class="settings-row-desc">Wer kann sehen, wer deine Freunde sind</div>
</div>
<select id="sv-freunde" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Feed -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Feed / Posts</div>
<div class="settings-row-desc">Posts auf dem Profil-Tab</div>
</div>
<select id="sv-feed" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- Pinnwand -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Pinnwand</div>
<div class="settings-row-desc">Einträge auf der Pinnwand</div>
</div>
<select id="sv-pinnwand" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
</select>
</div>
<!-- XP -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">XP-Punkte</div>
<div class="settings-row-desc">Lockee-XP und Keyholder-XP</div>
</div>
<select id="sv-xp" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
<option value="NUR_ICH">Nur ich</option>
</select>
</div>
<!-- Lock-Historie -->
<div class="settings-row">
<div class="settings-row-info">
<div class="settings-row-label">Lock-Historie</div>
<div class="settings-row-desc">Abgeschlossene Locks und Keyholder-Aktivitäten</div>
</div>
<select id="sv-lockhistorie" onchange="doSave()">
<option value="ALLE">Alle</option>
<option value="NUR_FREUNDE">Nur Freunde</option>
<option value="NUR_ICH">Nur ich</option>
</select>
</div>
<hr class="settings-separator">
<!-- Vorschau -->
<div class="preview-row">
<span>Profil-Vorschau wie sieht mein Profil für andere aus?</span>
<button onclick="openPreview('FREUND')">👥 Vorschau als Freund</button>
<button onclick="openPreview('UNBEKANNT')" style="background:var(--color-secondary);color:var(--color-text);">👤 Vorschau als Fremder</button>
</div>
</div>
</div>
</div>
</div>
<!-- Nickname Modal -->
<div class="modal-backdrop" id="nameModal">
<div class="modal">
<h2>Nickname ändern</h2>
<label for="newName">Neuer Nickname</label>
<input type="text" id="newName" placeholder="Neuer Name" autocomplete="off">
<div class="message" id="nameMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeNameDialog()">Abbrechen</button>
<button id="nameConfirmBtn" onclick="saveName()">Speichern</button>
</div>
</div>
</div>
<!-- Geburtsdatum Modal -->
<div class="modal-backdrop" id="geburtsdatumModal">
<div class="modal">
<h2>Geburtsdatum ändern</h2>
<label for="newGeburtsdatum">Neues Geburtsdatum</label>
<input type="date" id="newGeburtsdatum" autocomplete="bday">
<div class="message" id="geburtsdatumMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeGeburtsdatumDialog()">Abbrechen</button>
<button id="geburtsdatumConfirmBtn" onclick="saveGeburtsdatum()">Speichern</button>
</div>
</div>
</div>
<!-- E-Mail Modal -->
<div class="modal-backdrop" id="emailModal">
<div class="modal">
<h2>E-Mail-Adresse ändern</h2>
<label for="newEmail">Neue E-Mail-Adresse</label>
<input type="email" id="newEmail" placeholder="neue@email.de" autocomplete="off">
<div class="message" id="emailMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeEmailDialog()">Abbrechen</button>
<button id="emailConfirmBtn" onclick="requestEmailChange()">Bestätigungsmail senden</button>
</div>
</div>
</div>
<!-- Konto löschen Modal -->
<div class="modal-backdrop" id="deleteModal">
<div class="modal">
<h2>Konto löschen</h2>
<p style="color:var(--color-muted);font-size:0.9rem;margin-bottom:0.75rem;">
Dein Konto sowie alle gespeicherten Aufgaben und Toys werden unwiderruflich gelöscht.
</p>
<p style="color:var(--color-primary);font-size:0.85rem;">Dieser Vorgang kann nicht rückgängig gemacht werden.</p>
<div class="message" id="deleteMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeDeleteDialog()">Abbrechen</button>
<button id="deleteConfirmBtn" onclick="deleteAccount()" style="background:#c0392b;">Konto löschen</button>
</div>
</div>
</div>
<div class="save-toast" id="saveToast">✓ Gespeichert</div>
<script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
let myUserId = null;
let toastTimer = null;
function toggleSection(id) {
const target = document.getElementById(id);
const isOpen = target.classList.contains('open');
document.querySelectorAll('.settings-section').forEach(s => s.classList.remove('open'));
if (!isOpen) target.classList.add('open');
}
// ── Grunddaten laden ──
async function loadGrunddaten() {
const res = await fetch('/login/me');
if (res.status === 401) { window.location.href = '/login.html'; return; }
if (!res.ok) return;
const user = await res.json();
myUserId = user.userId;
document.getElementById('gd-name').textContent = user.name || '—';
document.getElementById('gd-email').textContent = user.email || '—';
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(user.geburtsdatum);
}
function formatGeburtsdatum(iso) {
if (!iso) return '—';
const [y, m, d] = iso.split('-');
const today = new Date();
const birth = new Date(iso);
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
return `${d}.${m}.${y} (${age} Jahre)`;
}
// ── Geburtsdatum Dialog ──
function openGeburtsdatumDialog() {
document.getElementById('newGeburtsdatum').value = '';
hideModalMessage('geburtsdatumMessage');
document.getElementById('geburtsdatumModal').classList.add('visible');
document.getElementById('newGeburtsdatum').focus();
}
function closeGeburtsdatumDialog() {
document.getElementById('geburtsdatumModal').classList.remove('visible');
}
async function saveGeburtsdatum() {
const val = document.getElementById('newGeburtsdatum').value;
if (!val) { showModalMessage('geburtsdatumMessage', 'Bitte ein Datum eingeben.', 'error'); return; }
const today = new Date();
const birth = new Date(val);
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
if (age < 18) {
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
return;
}
const btn = document.getElementById('geburtsdatumConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
hideModalMessage('geburtsdatumMessage');
try {
const res = await fetch('/user/me/geburtsdatum', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ geburtsdatum: val })
});
if (res.ok) {
document.getElementById('gd-geburtsdatum').textContent = formatGeburtsdatum(val);
closeGeburtsdatumDialog();
showToast();
} else if (res.status === 422) {
showModalMessage('geburtsdatumMessage', 'Du musst mindestens 18 Jahre alt sein.', 'error');
} else {
showModalMessage('geburtsdatumMessage', `Fehler: HTTP ${res.status}`, 'error');
}
} catch (err) {
showModalMessage('geburtsdatumMessage', 'Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false; btn.textContent = 'Speichern';
}
}
// ── Nickname Dialog ──
function openNameDialog() {
document.getElementById('newName').value = '';
hideModalMessage('nameMessage');
document.getElementById('nameModal').classList.add('visible');
document.getElementById('newName').focus();
}
function closeNameDialog() {
document.getElementById('nameModal').classList.remove('visible');
}
async function saveName() {
const newName = document.getElementById('newName').value.trim();
if (!newName) { showModalMessage('nameMessage', 'Bitte einen Namen eingeben.', 'error'); return; }
const btn = document.getElementById('nameConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gespeichert…';
hideModalMessage('nameMessage');
try {
const res = await fetch('/user/me/name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (res.ok) {
document.getElementById('gd-name').textContent = newName;
closeNameDialog();
showToast();
} else if (res.status === 409) {
showModalMessage('nameMessage', 'Dieser Nickname ist bereits vergeben.', 'error');
} else {
showModalMessage('nameMessage', `Fehler: HTTP ${res.status}`, 'error');
}
} catch (err) {
showModalMessage('nameMessage', 'Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false; btn.textContent = 'Speichern';
}
}
// ── E-Mail Dialog ──
function openEmailDialog() {
document.getElementById('newEmail').value = '';
hideModalMessage('emailMessage');
document.getElementById('emailModal').classList.add('visible');
document.getElementById('newEmail').focus();
}
function closeEmailDialog() {
document.getElementById('emailModal').classList.remove('visible');
}
async function requestEmailChange() {
const newEmail = document.getElementById('newEmail').value.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
showModalMessage('emailMessage', 'Bitte eine gültige E-Mail-Adresse eingeben.', 'error');
return;
}
const btn = document.getElementById('emailConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gesendet…';
hideModalMessage('emailMessage');
try {
const res = await fetch('/email-change', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newEmail })
});
if (res.status === 202) {
showModalMessage('emailMessage',
'Bestätigungsmail wurde an die neue Adresse gesendet. Bitte bestätige die Änderung über den Link in der E-Mail.',
'success');
btn.disabled = true; btn.textContent = 'Gesendet';
} else if (res.status === 409) {
showModalMessage('emailMessage', 'Diese E-Mail-Adresse ist bereits vergeben.', 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
} else {
showModalMessage('emailMessage', `Fehler: HTTP ${res.status}`, 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
}
} catch (err) {
showModalMessage('emailMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false; btn.textContent = 'Bestätigungsmail senden';
console.error(err);
}
}
// ── Konto löschen Dialog ──
function openDeleteDialog() {
hideModalMessage('deleteMessage');
document.getElementById('deleteConfirmBtn').disabled = false;
document.getElementById('deleteConfirmBtn').textContent = 'Konto löschen';
document.getElementById('deleteModal').classList.add('visible');
}
function closeDeleteDialog() {
document.getElementById('deleteModal').classList.remove('visible');
}
async function deleteAccount() {
const btn = document.getElementById('deleteConfirmBtn');
btn.disabled = true; btn.textContent = 'Wird gelöscht…';
hideModalMessage('deleteMessage');
try {
const res = await fetch('/user/me', { method: 'DELETE' });
if (res.ok) {
window.location.href = '/login.html?accountDeleted=1';
} else {
showModalMessage('deleteMessage', `Fehler: HTTP ${res.status}`, 'error');
btn.disabled = false; btn.textContent = 'Konto löschen';
}
} catch (err) {
showModalMessage('deleteMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false; btn.textContent = 'Konto löschen';
console.error(err);
}
}
function showModalMessage(id, text, type) {
const el = document.getElementById(id);
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideModalMessage(id) {
document.getElementById(id).style.display = 'none';
}
// Modal-Backdrop-Klick schließt Modals
['nameModal','geburtsdatumModal','emailModal','deleteModal'].forEach(id => {
document.getElementById(id).addEventListener('click', e => {
if (e.target === document.getElementById(id)) document.getElementById(id).classList.remove('visible');
});
});
document.getElementById('newName').addEventListener('keydown', e => { if (e.key === 'Enter') saveName(); });
document.getElementById('newEmail').addEventListener('keydown', e => { if (e.key === 'Enter') requestEmailChange(); });
// ── Datenschutz laden ──
async function loadPrivacy() {
const meRes = await fetch('/login/me');
if (!meRes.ok) return;
const me = await meRes.json();
myUserId = me.userId;
const profRes = await fetch('/social/users/' + myUserId);
if (!profRes.ok) return;
const profile = await profRes.json();
setValue('sv-grunddaten', profile.sichtbarkeitGrunddaten || 'ALLE');
setValue('sv-galerie', profile.sichtbarkeitGalerie || 'ALLE');
setValue('sv-freunde', profile.sichtbarkeitFreunde || 'ALLE');
setValue('sv-feed', profile.sichtbarkeitFeed || 'ALLE');
setValue('sv-pinnwand', profile.sichtbarkeitPinnwand || 'ALLE');
setValue('sv-xp', profile.sichtbarkeitXp || 'ALLE');
setValue('sv-lockhistorie', profile.sichtbarkeitLockhistorie || 'ALLE');
}
function setValue(id, value) {
const el = document.getElementById(id);
if (el) el.value = value;
}
async function doSave() {
const body = {
sichtbarkeitGrunddaten: document.getElementById('sv-grunddaten').value,
sichtbarkeitGalerie: document.getElementById('sv-galerie').value,
sichtbarkeitFreunde: document.getElementById('sv-freunde').value,
sichtbarkeitFeed: document.getElementById('sv-feed').value,
sichtbarkeitPinnwand: document.getElementById('sv-pinnwand').value,
sichtbarkeitXp: document.getElementById('sv-xp').value,
sichtbarkeitLockhistorie: document.getElementById('sv-lockhistorie').value,
};
const res = await fetch('/user/me/privacy', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) showToast();
}
function showToast() {
const t = document.getElementById('saveToast');
t.classList.add('visible');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('visible'), 2200);
}
function openPreview(mode) {
if (!myUserId) return;
window.open('/benutzer.html?userId=' + myUserId + '&preview=' + mode, '_blank');
}
// ── Benachrichtigungen laden ──
async function loadNotifications() {
const res = await fetch('/user/me/notifications');
if (!res.ok) return;
const data = await res.json();
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
const pref = data[cause] || { inApp: true, email: false };
const inApp = document.getElementById('notif-' + cause + '-inApp');
const email = document.getElementById('notif-' + cause + '-email');
if (inApp) inApp.checked = pref.inApp;
if (email) email.checked = pref.email;
}
}
async function saveNotifications() {
const body = {};
for (const cause of ['INVITATION', 'GAME_STATE', 'EMERGENCY']) {
const inApp = document.getElementById('notif-' + cause + '-inApp');
const email = document.getElementById('notif-' + cause + '-email');
body[cause] = {
inApp: inApp ? inApp.checked : true,
email: email ? email.checked : false
};
}
const res = await fetch('/user/me/notifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) showToast();
}
// Hash-Navigation: Sektion anhand URL-Fragment öffnen
function openSectionFromHash() {
const hash = window.location.hash.replace('#', '');
if (hash) {
const el = document.getElementById(hash);
if (el) el.classList.add('open');
}
}
// ── Spiel Einstellungen BDSM ──
function applyCheckItems(groupId, values) {
document.querySelectorAll(`#${groupId} .spiel-check-item input`).forEach(cb => {
const checked = values.includes(cb.value);
cb.checked = checked;
cb.closest('.spiel-check-item').classList.toggle('is-checked', checked);
});
}
document.querySelectorAll('.spiel-check-item input').forEach(cb => {
cb.addEventListener('change', () => {
cb.closest('.spiel-check-item').classList.toggle('is-checked', cb.checked);
saveBdsmDefaults();
});
});
async function loadBdsmDefaults() {
const res = await fetch('/user/me/bdsm-defaults');
if (!res.ok) return;
const d = await res.json();
applyCheckItems('bdsm-spieltmit', d.spieltMit || []);
applyCheckItems('bdsm-rollen', d.rollen || []);
applyCheckItems('bdsm-werkzeuge', d.werkzeuge || []);
}
async function saveBdsmDefaults() {
const get = groupId => [...document.querySelectorAll(`#${groupId} input:checked`)].map(cb => cb.value);
await fetch('/user/me/bdsm-defaults', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
spieltMit: get('bdsm-spieltmit'),
rollen: get('bdsm-rollen'),
werkzeuge: get('bdsm-werkzeuge'),
})
});
showToast();
}
loadGrunddaten();
loadPrivacy();
loadNotifications();
loadBdsmDefaults();
openSectionFromHash();
</script>
</body>
</html>

View File

@@ -13,8 +13,8 @@
label: 'BDSM Game',
icon: '◆',
items: [
{ href: '/sessionbdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
{ href: '/sessionbdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
{ href: '/bdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
{ href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
{ href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' },
{ href: '/toys.html', icon: '◈', label: 'Toys' },
{ href: '/entdecken.html', icon: '⊙', label: 'Entdecken' },
@@ -105,7 +105,7 @@
// BDSM Session-Status
try {
const sessionRes = await fetch(`/session?userId=${user.userId}`);
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
const hasSession = sessionRes.status === 200;
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';

View File

@@ -39,6 +39,7 @@
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${desktopItems}
<li style="margin-top:auto;"><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
<li><a href="/einstellungen.html"${path === '/einstellungen.html' ? ' class="active"' : ''}><span class="icon">⚙️</span> Einstellungen</a></li>
<li><a href="/login/logout"><span class="icon">⏏</span> Abmelden</a></li>
</ul>`;
@@ -66,11 +67,12 @@
</li>`;
const sep = '<li class="sidebar-mobile-only"><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>';
const mobileSettings = `<li class="sidebar-mobile-only"><a href="/einstellungen.html"${path === '/einstellungen.html' ? ' class="active"' : ''}><span class="icon">⚙️</span> Einstellungen</a></li>`;
const logoutLi = sidebarUl.querySelector('a[href="/login/logout"]')?.closest('li');
if (logoutLi) {
logoutLi.insertAdjacentHTML('beforebegin', sep + mobileLinks + mobileProfile);
logoutLi.insertAdjacentHTML('beforebegin', sep + mobileLinks + mobileProfile + mobileSettings);
} else {
sidebarUl.insertAdjacentHTML('beforeend', sep + mobileLinks + mobileProfile);
sidebarUl.insertAdjacentHTML('beforeend', sep + mobileLinks + mobileProfile + mobileSettings);
}
}

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profil XXX The Game</title>
@@ -269,35 +269,19 @@
<body class="app">
<div class="main">
<div class="content" style="max-width: 600px;">
<div class="content">
<div class="profile-picture-wrap">
<div class="profile-picture" id="profilePicDisplay"></div>
<input type="file" id="picFile" accept="image/*">
</div>
<div class="profile-field">
<label>Nickname</label>
<div class="profile-field-row">
<p id="userName"></p>
<button onclick="openNameDialog()">Nickname ändern</button>
</div>
</div>
<div class="profile-field">
<label>E-Mail</label>
<div class="profile-field-row">
<p id="userEmail"></p>
<button onclick="openEmailDialog()">E-Mail ändern</button>
</div>
</div>
<!-- Freiwillige Profilangaben -->
<div class="gallery-section-label" style="margin-top:1.25rem;">Freiwillige Angaben</div>
<div class="profile-extras-grid">
<div>
<label>Alter</label>
<input type="number" id="profileAlter" min="18" max="99" placeholder="—">
<input type="text" id="profileAlter" readonly style="background:transparent;cursor:default;color:var(--color-muted);" placeholder="—">
</div>
<div>
<label>Größe (cm)</label>
@@ -346,10 +330,6 @@
</div>
</div>
<div class="btn-delete-row">
<button class="btn-delete" onclick="openDeleteDialog()">Konto löschen</button>
</div>
<div class="message" id="message"></div>
<button class="full-width" id="saveBtn" onclick="saveProfile()">Profil speichern</button>
@@ -364,50 +344,6 @@
</div>
</div>
<!-- Nickname modal -->
<div class="modal-backdrop" id="nameModal">
<div class="modal">
<h2>Nickname ändern</h2>
<label for="newName">Neuer Nickname</label>
<input type="text" id="newName" placeholder="Neuer Name" autocomplete="off">
<div class="message" id="nameMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeNameDialog()">Abbrechen</button>
<button id="nameConfirmBtn" onclick="saveName()">Speichern</button>
</div>
</div>
</div>
<!-- Konto löschen modal -->
<div class="modal-backdrop" id="deleteModal">
<div class="modal">
<h2>Konto löschen</h2>
<p style="color:var(--color-muted); font-size:0.9rem; margin-bottom:0.75rem;">
Dein Konto sowie alle gespeicherten Aufgaben und Toys werden unwiderruflich gelöscht.
</p>
<p style="color:var(--color-primary); font-size:0.85rem;">Dieser Vorgang kann nicht rückgängig gemacht werden.</p>
<div class="message" id="deleteMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeDeleteDialog()">Abbrechen</button>
<button id="deleteConfirmBtn" onclick="deleteAccount()" style="background:#c0392b;">Konto löschen</button>
</div>
</div>
</div>
<!-- E-Mail modal -->
<div class="modal-backdrop" id="emailModal">
<div class="modal">
<h2>E-Mail-Adresse ändern</h2>
<label for="newEmail">Neue E-Mail-Adresse</label>
<input type="email" id="newEmail" placeholder="neue@email.de" autocomplete="off">
<div class="message" id="emailMessage"></div>
<div class="modal-actions">
<button class="secondary" onclick="closeEmailDialog()">Abbrechen</button>
<button id="emailConfirmBtn" onclick="requestEmailChange()">Bestätigungsmail senden</button>
</div>
</div>
</div>
<script src="/js/shared.js"></script>
<script src="/js/image-viewer.js"></script>
<script src="/js/sidebar.js"></script>
@@ -424,14 +360,18 @@
})
.then(user => {
if (!user) return;
document.getElementById('userName').textContent = user.name;
document.getElementById('userEmail').textContent = user.email;
if (user.profilePicture) {
currentPicture = user.profilePicture;
renderPicture(currentPicture);
}
// Fill optional profile fields
if (user.alter) document.getElementById('profileAlter').value = user.alter;
if (user.geburtsdatum) {
const birth = new Date(user.geburtsdatum);
const today = new Date();
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
document.getElementById('profileAlter').value = age + ' Jahre';
}
if (user.groesse) document.getElementById('profileGroesse').value = user.groesse;
if (user.gewicht) document.getElementById('profileGewicht').value = user.gewicht;
if (user.geschlecht) document.getElementById('profileGeschlecht').value = user.geschlecht;
@@ -506,7 +446,6 @@
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
alter: toNullOrInt('profileAlter'),
groesse: toNullOrInt('profileGroesse'),
gewicht: toNullOrInt('profileGewicht'),
geschlecht: toNullOrStr('profileGeschlecht'),
@@ -530,105 +469,6 @@
}
}
// ── Nickname dialog ──
function openNameDialog() {
document.getElementById('newName').value = '';
hideModalMessage('nameMessage');
document.getElementById('nameModal').classList.add('visible');
document.getElementById('newName').focus();
}
function closeNameDialog() {
document.getElementById('nameModal').classList.remove('visible');
}
async function saveName() {
const newName = document.getElementById('newName').value.trim();
if (!newName) {
showModalMessage('nameMessage', 'Bitte einen Namen eingeben.', 'error');
return;
}
const btn = document.getElementById('nameConfirmBtn');
btn.disabled = true;
btn.textContent = 'Wird gespeichert…';
hideModalMessage('nameMessage');
try {
const response = await fetch('/user/me/name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName })
});
if (response.ok) {
document.getElementById('userName').textContent = newName;
closeNameDialog();
showMessage('Nickname geändert.', 'success');
} else if (response.status === 409) {
showModalMessage('nameMessage', 'Dieser Nickname ist bereits vergeben.', 'error');
} else {
showModalMessage('nameMessage', `Fehler: HTTP ${response.status}`, 'error');
}
} catch (err) {
showModalMessage('nameMessage', 'Server nicht erreichbar.', 'error');
console.error(err);
} finally {
btn.disabled = false;
btn.textContent = 'Speichern';
}
}
// ── E-Mail dialog ──
function openEmailDialog() {
document.getElementById('newEmail').value = '';
hideModalMessage('emailMessage');
document.getElementById('emailModal').classList.add('visible');
document.getElementById('newEmail').focus();
}
function closeEmailDialog() {
document.getElementById('emailModal').classList.remove('visible');
}
async function requestEmailChange() {
const newEmail = document.getElementById('newEmail').value.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
showModalMessage('emailMessage', 'Bitte eine gültige E-Mail-Adresse eingeben.', 'error');
return;
}
const btn = document.getElementById('emailConfirmBtn');
btn.disabled = true;
btn.textContent = 'Wird gesendet…';
hideModalMessage('emailMessage');
try {
const response = await fetch('/email-change', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newEmail })
});
if (response.status === 202) {
showModalMessage('emailMessage',
'Bestätigungsmail wurde an die neue Adresse gesendet. Bitte bestätige die Änderung über den Link in der E-Mail.',
'success');
btn.disabled = true;
btn.textContent = 'Gesendet';
} else if (response.status === 409) {
showModalMessage('emailMessage', 'Diese E-Mail-Adresse ist bereits vergeben.', 'error');
btn.disabled = false;
btn.textContent = 'Bestätigungsmail senden';
} else {
showModalMessage('emailMessage', `Fehler: HTTP ${response.status}`, 'error');
btn.disabled = false;
btn.textContent = 'Bestätigungsmail senden';
}
} catch (err) {
showModalMessage('emailMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false;
btn.textContent = 'Bestätigungsmail senden';
console.error(err);
}
}
// ── Gallery ──
let myUserId = null;
@@ -755,71 +595,6 @@
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
function showModalMessage(id, text, type) {
const el = document.getElementById(id);
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideModalMessage(id) {
document.getElementById(id).style.display = 'none';
}
// ── Konto löschen ──
function openDeleteDialog() {
hideModalMessage('deleteMessage');
document.getElementById('deleteConfirmBtn').disabled = false;
document.getElementById('deleteConfirmBtn').textContent = 'Konto löschen';
document.getElementById('deleteModal').classList.add('visible');
}
function closeDeleteDialog() {
document.getElementById('deleteModal').classList.remove('visible');
}
async function deleteAccount() {
const btn = document.getElementById('deleteConfirmBtn');
btn.disabled = true;
btn.textContent = 'Wird gelöscht…';
hideModalMessage('deleteMessage');
try {
const response = await fetch('/user/me', { method: 'DELETE' });
if (response.ok) {
window.location.href = '/login.html?accountDeleted=1';
} else {
showModalMessage('deleteMessage', `Fehler: HTTP ${response.status}`, 'error');
btn.disabled = false;
btn.textContent = 'Konto löschen';
}
} catch (err) {
showModalMessage('deleteMessage', 'Server nicht erreichbar.', 'error');
btn.disabled = false;
btn.textContent = 'Konto löschen';
console.error(err);
}
}
// Close modals on backdrop click
document.getElementById('deleteModal').addEventListener('click', e => {
if (e.target === document.getElementById('deleteModal')) closeDeleteDialog();
});
document.getElementById('nameModal').addEventListener('click', e => {
if (e.target === document.getElementById('nameModal')) closeNameDialog();
});
document.getElementById('emailModal').addEventListener('click', e => {
if (e.target === document.getElementById('emailModal')) closeEmailDialog();
});
// Enter key in modal inputs
document.getElementById('newName').addEventListener('keydown', e => {
if (e.key === 'Enter') saveName();
});
document.getElementById('newEmail').addEventListener('keydown', e => {
if (e.key === 'Enter') requestEmailChange();
});
</script>
</body>
</html>

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xXx Games Neues Konto erstellen</title>
@@ -19,6 +19,9 @@
<label for="email">E-Mail</label>
<input type="email" id="email" placeholder="deine@email.de" autocomplete="email" />
<label for="geburtsdatum">Geburtsdatum</label>
<input type="date" id="geburtsdatum" autocomplete="bday" />
<label for="password">Passwort</label>
<input type="password" id="password" placeholder="••••••••" autocomplete="new-password" />
@@ -50,11 +53,12 @@
async function register() {
const name = document.getElementById('name').value.trim();
const email = document.getElementById('email').value.trim();
const geburtsdatum = document.getElementById('geburtsdatum').value;
const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('passwordConfirm').value;
const btn = document.getElementById('registerBtn');
if (!name || !email || !password || !passwordConfirm) {
if (!name || !email || !geburtsdatum || !password || !passwordConfirm) {
showMessage('Bitte alle Felder ausfüllen.', 'error');
return;
}
@@ -62,6 +66,14 @@
showMessage('Bitte eine gültige E-Mail-Adresse eingeben.', 'error');
return;
}
const today = new Date();
const birth = new Date(geburtsdatum);
const age = today.getFullYear() - birth.getFullYear()
- (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate()) ? 1 : 0);
if (age < 18) {
showMessage('Du musst mindestens 18 Jahre alt sein, um dich zu registrieren.', 'error');
return;
}
if (password !== passwordConfirm) {
showMessage('Die Passwörter stimmen nicht überein.', 'error');
return;
@@ -76,11 +88,15 @@
const response = await fetch('/registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, passwordHash })
body: JSON.stringify({ name, email, passwordHash, geburtsdatum })
});
if (response.status === 202) {
window.location.href = `/activate.html?email=${encodeURIComponent(email)}`;
} else if (response.status === 422) {
showMessage('Du musst mindestens 18 Jahre alt sein, um dich zu registrieren.', 'error');
btn.disabled = false;
btn.textContent = 'Registrieren';
} else if (response.status === 400) {
showMessage('Diese E-Mail-Adresse ist bereits registriert.', 'error');
btn.disabled = false;

View File

@@ -1,439 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Mitspieler XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { max-width: 700px; }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Player cards ── */
.player-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1rem;
}
.player-card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.25rem;
}
.player-title {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
}
.player-badge {
background: var(--color-primary);
color: #fff;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-remove {
margin-left: auto;
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.25rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.player-remove:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: transparent;
}
.card-field { margin-bottom: 1rem; }
.card-field > label {
font-size: 0.8rem;
color: #aaa;
margin: 0 0 0.5rem 0;
display: block;
}
/* ── Check/radio items ── */
.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-item.is-checked { border-color: var(--color-primary); }
.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;
}
.add-player-btn {
width: 100%;
background: transparent;
border: 1px dashed var(--color-secondary);
color: var(--color-muted);
padding: 0.75rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: normal;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
margin-bottom: 2rem;
}
.add-player-btn:hover {
border-color: var(--color-primary);
color: var(--color-text);
background: transparent;
}
.field-error {
font-size: 0.78rem;
color: var(--color-primary);
margin-top: 0.3rem;
display: none;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 2 von 4 Mitspieler</p>
<div class="setup-section">
<h2>Mitspieler</h2>
<div id="playersContainer"></div>
<button class="add-player-btn" onclick="addPlayer()">+ Spieler hinzufügen</button>
</div>
<div class="message" id="message"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/sessionbdsm.html'">← Zurück</button>
<button style="flex:2;" onclick="weiter()">Weiter</button>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
// Redirect back if settings are missing
if (!sessionStorage.getItem('bdsm-session-settings')) {
window.location.replace('/sessionbdsm.html');
}
const GESCHLECHTER = [
{ value: 'MAENNLICH', label: 'Männlich' },
{ value: 'WEIBLICH', label: 'Weiblich' },
{ value: 'DIVERS', label: 'Divers' },
];
const ROLLEN = [
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe Aktiv' },
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe Passiv' },
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung Aktiv' },
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung Passiv' },
];
const WERKZEUGE_DEFAULTS = {
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
};
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
];
let playerSeq = 0;
let playerIds = [];
function buildCheckItems(name, items, type) {
return items.map(({ value, label, desc }) => `
<label class="check-item">
<input type="${type}" name="${name}" value="${value}">
<span>
<span class="check-item-label">${label}</span>
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
</span>
</label>`).join('');
}
function createCardHtml(id, prefillName, isSelf) {
const badge = isSelf ? '<span class="player-badge">Du</span>' : '';
const num = playerIds.indexOf(id) + 1;
return `
<div class="player-card" id="player-${id}">
<div class="player-card-header">
<span class="player-title">Spieler ${num}</span>
${badge}
<button class="player-remove" onclick="removePlayer(${id})">✕ Entfernen</button>
</div>
<div class="card-field">
<label>Name</label>
<input type="text" id="p${id}-name" value="${prefillName}" placeholder="Name" autocomplete="off">
<div class="field-error" id="p${id}-name-err">Bitte Namen eingeben.</div>
</div>
<div class="card-field">
<label>Geschlecht</label>
<div class="check-group">${buildCheckItems('p' + id + '-geschlecht', GESCHLECHTER, 'radio')}</div>
<div class="field-error" id="p${id}-geschlecht-err">Bitte Geschlecht auswählen.</div>
</div>
<div class="card-field">
<label>Spielt mit</label>
<div class="check-group">${buildCheckItems('p' + id + '-spieltmit', GESCHLECHTER, 'checkbox')}</div>
<div class="field-error" id="p${id}-spieltmit-err">Bitte mindestens eine Option wählen.</div>
<div class="field-error" id="p${id}-partner-err">Kein Mitspieler mit passendem Geschlecht vorhanden.</div>
</div>
<div class="card-field">
<label>Rollen</label>
<div class="check-group">${buildCheckItems('p' + id + '-rollen', ROLLEN, 'checkbox')}</div>
<div class="field-error" id="p${id}-rollen-err">Bitte mindestens eine Rolle wählen.</div>
</div>
<div class="card-field">
<label>Verfügbar</label>
<div class="check-group check-group--two-col">${buildCheckItems('p' + id + '-werkzeuge', WERKZEUGE, 'checkbox')}</div>
<div class="field-error" id="p${id}-werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
</div>
</div>`;
}
function addPlayer(prefillName = '', isSelf = false) {
playerSeq++;
const id = playerSeq;
playerIds.push(id);
document.getElementById('playersContainer')
.insertAdjacentHTML('beforeend', createCardHtml(id, prefillName, isSelf));
refreshRemoveButtons();
}
function removePlayer(id) {
document.getElementById('player-' + id)?.remove();
playerIds = playerIds.filter(x => x !== id);
refreshPlayerTitles();
refreshRemoveButtons();
}
function refreshPlayerTitles() {
playerIds.forEach((id, idx) => {
const el = document.querySelector(`#player-${id} .player-title`);
if (el) el.textContent = 'Spieler ' + (idx + 1);
});
}
function refreshRemoveButtons() {
const canRemove = playerIds.length > 2;
playerIds.forEach(id => {
const btn = document.querySelector(`#player-${id} .player-remove`);
if (btn) btn.style.display = canRemove ? '' : 'none';
});
}
document.addEventListener('change', e => {
const input = e.target;
if (input.type !== 'checkbox' && input.type !== 'radio') return;
if (input.type === 'radio') {
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r => {
r.closest('.check-item')?.classList.toggle('is-checked', r.checked);
});
// Werkzeuge vorauswählen wenn Geschlecht gewählt wird
if (input.checked && input.name.endsWith('-geschlecht')) {
const prefix = input.name.slice(0, -'-geschlecht'.length);
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
document.querySelectorAll(`input[name="${prefix}-werkzeuge"]`).forEach(cb => {
cb.checked = defaults.includes(cb.value);
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
});
}
} else {
input.closest('.check-item')?.classList.toggle('is-checked', input.checked);
}
});
function getChecked(name) {
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
}
function setFieldError(id, show) {
const el = document.getElementById(id);
if (el) el.style.display = show ? 'block' : 'none';
}
const ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv',
AUFGABE_PASSIV: 'Aufgabe Passiv',
BESTRAFUNG_AKTIV: 'Bestrafung Aktiv',
BESTRAFUNG_PASSIV: 'Bestrafung Passiv',
};
function weiter() {
hideMessage();
let valid = true;
// Reset cross-player errors
playerIds.forEach(id => setFieldError(`p${id}-partner-err`, false));
const mitspieler = playerIds.map(id => {
const name = document.getElementById(`p${id}-name`).value.trim();
const geschlecht = getChecked(`p${id}-geschlecht`);
const spieltMit = getChecked(`p${id}-spieltmit`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-name-err`, !name);
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || geschlecht.length === 0 || spieltMit.length === 0 || rollen.length === 0 || werkzeuge.length === 0) {
valid = false;
}
return { name, geschlecht: geschlecht[0] || null, spieltMit, rollen, werkzeuge };
});
if (!valid) {
showMessage('Bitte alle Felder für jeden Spieler ausfüllen.', 'error');
return;
}
// Jede Rolle muss mindestens einmal vergeben sein
const allRoles = new Set(mitspieler.flatMap(p => p.rollen));
const missingRoles = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
if (missingRoles.length > 0) {
showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' +
missingRoles.map(r => ROLE_LABELS[r]).join(', '), 'error');
return;
}
// Jeder Spieler braucht mindestens einen Mitspieler mit passendem Geschlecht
let partnerFehler = false;
mitspieler.forEach((player, i) => {
const andereGeschlechter = mitspieler
.filter((_, j) => j !== i)
.map(p => p.geschlecht);
const hatPartner = player.spieltMit.some(g => andereGeschlechter.includes(g));
if (!hatPartner) {
setFieldError(`p${playerIds[i]}-partner-err`, true);
partnerFehler = true;
}
});
if (partnerFehler) {
showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error');
return;
}
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
sessionStorage.setItem('bdsm-session-setup', JSON.stringify({ settings, mitspieler }));
window.location.href = '/sessionbdsmtasks.html';
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
function restorePlayer(id, data) {
if (data.geschlecht) {
const radio = document.querySelector(`input[name="p${id}-geschlecht"][value="${data.geschlecht}"]`);
if (radio) { radio.checked = true; radio.closest('.check-item')?.classList.add('is-checked'); }
}
(data.spieltMit || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-spieltmit"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.rollen || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-rollen"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.werkzeuge || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-werkzeuge"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
}
// Init: gespeicherte Spieler wiederherstellen oder neu anlegen
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
if (savedSetup) {
const { mitspieler } = JSON.parse(savedSetup);
mitspieler.forEach((p, i) => {
addPlayer(p.name, i === 0);
restorePlayer(playerIds[playerIds.length - 1], p);
});
} else {
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
.then(user => {
addPlayer(user ? user.name : '', true);
addPlayer();
})
.catch(() => {
addPlayer('', true);
addPlayer();
});
}
</script>
</body>
</html>

View File

@@ -54,7 +54,7 @@
Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen.
Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinem Partner.
</p>
<a href="/sessionbdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
<a href="/bdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
</div>
<div class="game-card">