BDSM Game umgesetzt, Community Features ergänzt
@@ -14,6 +14,7 @@ public class AufgabenGruppe {
|
||||
private List<Aufgabe> aufgaben;
|
||||
private List<Strafe> strafen;
|
||||
private List<Sperre> sperren;
|
||||
private List<Finisher> finisher;
|
||||
private String bild;
|
||||
private long subscriberCount;
|
||||
private boolean subscribed;
|
||||
@@ -45,6 +46,9 @@ public class AufgabenGruppe {
|
||||
public List<Sperre> getSperren() { return sperren; }
|
||||
public void setSperren(List<Sperre> sperren) { this.sperren = sperren; }
|
||||
|
||||
public List<Finisher> getFinisher() { return finisher; }
|
||||
public void setFinisher(List<Finisher> finisher) { this.finisher = finisher; }
|
||||
|
||||
public String getBild() { return bild; }
|
||||
public void setBild(String bild) { this.bild = bild; }
|
||||
|
||||
|
||||
47
xxxthegame/src/main/java/de/oaa/xxx/aufgaben/Finisher.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package de.oaa.xxx.aufgaben;
|
||||
|
||||
import de.oaa.xxx.session.GeschlechtEnum;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Finisher {
|
||||
|
||||
private UUID finisherId;
|
||||
private String kurzText;
|
||||
private String text;
|
||||
private GeschlechtEnum geschlecht;
|
||||
private List<Werkzeug> benoetigtAktiv;
|
||||
private List<Werkzeug> benoetigtPassiv;
|
||||
private List<Toy> benoetigteToys;
|
||||
private UUID gruppeId;
|
||||
|
||||
public UUID getFinisherId() { return finisherId; }
|
||||
public void setFinisherId(UUID finisherId) { this.finisherId = finisherId; }
|
||||
|
||||
public String getKurzText() { return kurzText; }
|
||||
public void setKurzText(String kurzText) { this.kurzText = kurzText; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public GeschlechtEnum getGeschlecht() { return geschlecht; }
|
||||
public void setGeschlecht(GeschlechtEnum geschlecht) { this.geschlecht = geschlecht; }
|
||||
|
||||
public List<Werkzeug> getBenoetigtAktiv() { return benoetigtAktiv; }
|
||||
public void setBenoetigtAktiv(List<Werkzeug> benoetigtAktiv) { this.benoetigtAktiv = benoetigtAktiv; }
|
||||
|
||||
public List<Werkzeug> getBenoetigtPassiv() { return benoetigtPassiv; }
|
||||
public void setBenoetigtPassiv(List<Werkzeug> benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; }
|
||||
|
||||
public List<Toy> getBenoetigteToys() { return benoetigteToys; }
|
||||
public void setBenoetigteToys(List<Toy> benoetigteToys) { this.benoetigteToys = benoetigteToys; }
|
||||
|
||||
public UUID getGruppeId() { return gruppeId; }
|
||||
public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Finisher[id=" + finisherId + ", kurzText=" + kurzText + ", geschlecht=" + geschlecht + ", gruppeId=" + gruppeId + "]";
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import de.oaa.xxx.aufgaben.AufgabenGruppeList;
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppePage;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.FinisherEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.SperreEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.StrafeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.FinisherRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.SperreRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
|
||||
@@ -57,6 +59,7 @@ public class AufgabenGruppeController {
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final StrafeRepository strafeRepository;
|
||||
private final SperreRepository sperreRepository;
|
||||
private final FinisherRepository finisherRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GruppenAboRepository aboRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
@@ -65,6 +68,7 @@ public class AufgabenGruppeController {
|
||||
AufgabeRepository aufgabeRepository,
|
||||
StrafeRepository strafeRepository,
|
||||
SperreRepository sperreRepository,
|
||||
FinisherRepository finisherRepository,
|
||||
UserRepository userRepository,
|
||||
GruppenAboRepository aboRepository,
|
||||
ToyRepository toyRepository) {
|
||||
@@ -72,6 +76,7 @@ public class AufgabenGruppeController {
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.finisherRepository = finisherRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.aboRepository = aboRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
@@ -198,6 +203,7 @@ public class AufgabenGruppeController {
|
||||
source.getAufgaben().forEach(a -> { if (a.getBenoetigteToys() != null) allSourceToys.addAll(a.getBenoetigteToys()); });
|
||||
source.getStrafen().forEach(s -> { if (s.getBenoetigteToys() != null) allSourceToys.addAll(s.getBenoetigteToys()); });
|
||||
source.getSperren().forEach(sp -> { if (sp.getBenoetigteToys() != null) allSourceToys.addAll(sp.getBenoetigteToys()); });
|
||||
source.getFinisher().forEach(f -> { if (f.getBenoetigteToys() != null) allSourceToys.addAll(f.getBenoetigteToys()); });
|
||||
|
||||
Map<UUID, ToyEntity> toyMapping = new HashMap<>();
|
||||
for (ToyEntity sourceToy : allSourceToys) {
|
||||
@@ -274,6 +280,19 @@ public class AufgabenGruppeController {
|
||||
sperreRepository.save(spc);
|
||||
}
|
||||
|
||||
for (FinisherEntity f : source.getFinisher()) {
|
||||
FinisherEntity fc = new FinisherEntity();
|
||||
fc.setFinisherId(UUID.randomUUID());
|
||||
fc.setAufgabenGruppe(copy);
|
||||
fc.setKurzText(f.getKurzText());
|
||||
fc.setText(f.getText());
|
||||
fc.setGeschlecht(f.getGeschlecht());
|
||||
fc.setBenoetigtAktiv(f.getBenoetigtAktiv() != null ? new ArrayList<>(f.getBenoetigtAktiv()) : null);
|
||||
fc.setBenoetigtPassiv(f.getBenoetigtPassiv() != null ? new ArrayList<>(f.getBenoetigtPassiv()) : null);
|
||||
fc.setBenoetigteToys(mapToys(f.getBenoetigteToys(), toyMapping));
|
||||
finisherRepository.save(fc);
|
||||
}
|
||||
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@@ -298,6 +317,7 @@ public class AufgabenGruppeController {
|
||||
aufgabeRepository.deleteAll(entity.getAufgaben());
|
||||
strafeRepository.deleteAll(entity.getStrafen());
|
||||
sperreRepository.deleteAll(entity.getSperren());
|
||||
finisherRepository.deleteAll(entity.getFinisher());
|
||||
gruppeRepository.delete(entity);
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package de.oaa.xxx.aufgaben.controller;
|
||||
|
||||
import de.oaa.xxx.aufgaben.Finisher;
|
||||
import de.oaa.xxx.aufgaben.Toy;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.FinisherEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.FinisherRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.RestController;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/finisher")
|
||||
@Transactional
|
||||
public class FinisherController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FinisherController.class);
|
||||
|
||||
private final FinisherRepository finisherRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
|
||||
public FinisherController(FinisherRepository finisherRepository,
|
||||
AufgabenGruppeRepository gruppeRepository,
|
||||
ToyRepository toyRepository) {
|
||||
this.finisherRepository = finisherRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{finisherId}")
|
||||
public ResponseEntity<Finisher> get(@PathVariable UUID finisherId) {
|
||||
return finisherRepository.findById(finisherId)
|
||||
.map(entity -> ResponseEntity.ok(entity.toFinisher()))
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> create(@RequestBody Finisher finisher) {
|
||||
if (finisher.getKurzText() == null || finisher.getText() == null
|
||||
|| finisher.getGeschlecht() == null || finisher.getGruppeId() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(finisher.getGruppeId()).orElse(null);
|
||||
if (gruppeEntity == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (gruppeEntity.getFinisher().size() >= 100) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
List<ToyEntity> toys = resolveToys(finisher.getBenoetigteToys());
|
||||
FinisherEntity entity = FinisherEntity.create(finisher, gruppeEntity, toys);
|
||||
finisherRepository.save(entity);
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getFinisherId()).toUri()
|
||||
).build();
|
||||
}
|
||||
|
||||
@PutMapping("/{finisherId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID finisherId, @RequestBody Finisher finisher) {
|
||||
if (finisher.getKurzText() == null || finisher.getText() == null || finisher.getGeschlecht() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
FinisherEntity entity = finisherRepository.findById(finisherId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
entity.setKurzText(finisher.getKurzText());
|
||||
entity.setText(finisher.getText());
|
||||
entity.setGeschlecht(finisher.getGeschlecht());
|
||||
entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv());
|
||||
entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv());
|
||||
entity.setBenoetigteToys(resolveToys(finisher.getBenoetigteToys()));
|
||||
finisherRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<Void> delete(@RequestBody Finisher finisher) {
|
||||
try {
|
||||
finisherRepository.findById(finisher.getFinisherId()).ifPresent(finisherRepository::delete);
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
private List<ToyEntity> resolveToys(List<Toy> toys) {
|
||||
if (toys == null || toys.isEmpty()) return new ArrayList<>();
|
||||
List<UUID> ids = toys.stream()
|
||||
.filter(t -> t.getToyId() != null)
|
||||
.map(Toy::getToyId)
|
||||
.toList();
|
||||
if (ids.isEmpty()) return new ArrayList<>();
|
||||
return toyRepository.findAllById(ids);
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,12 @@ public class AufgabeEntity {
|
||||
public List<ToyEntity> getBenoetigteToys() { return benoetigteToys; }
|
||||
public void setBenoetigteToys(List<ToyEntity> benoetigteToys) { this.benoetigteToys = benoetigteToys; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AufgabeEntity[id=" + aufgabeId + ", kurzText=" + kurzText + ", level=" + level
|
||||
+ ", sekunden=" + sekundenVon + "-" + sekundenBis + "]";
|
||||
}
|
||||
|
||||
public Aufgabe toAufgabe() {
|
||||
Aufgabe aufgabe = new Aufgabe();
|
||||
aufgabe.setAufgabeId(aufgabeId);
|
||||
|
||||
@@ -39,6 +39,8 @@ public class AufgabenGruppeEntity {
|
||||
private List<StrafeEntity> strafen;
|
||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||
private List<SperreEntity> sperren;
|
||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||
private List<FinisherEntity> finisher;
|
||||
|
||||
public UUID getGruppenId() { return gruppenId; }
|
||||
public void setGruppenId(UUID gruppenId) { this.gruppenId = gruppenId; }
|
||||
@@ -70,6 +72,15 @@ public class AufgabenGruppeEntity {
|
||||
public List<SperreEntity> getSperren() { return sperren; }
|
||||
public void setSperren(List<SperreEntity> sperren) { this.sperren = sperren; }
|
||||
|
||||
public List<FinisherEntity> getFinisher() { return finisher; }
|
||||
public void setFinisher(List<FinisherEntity> finisher) { this.finisher = finisher; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AufgabenGruppeEntity[gruppenId=" + gruppenId + ", name=" + name + ", userId=" + userId
|
||||
+ ", privat=" + privateGruppe + ", von=" + von + "]";
|
||||
}
|
||||
|
||||
public AufgabenGruppe toAufgabenGruppe() {
|
||||
AufgabenGruppe gruppe = new AufgabenGruppe();
|
||||
gruppe.setGruppenId(gruppenId);
|
||||
@@ -82,6 +93,7 @@ public class AufgabenGruppeEntity {
|
||||
gruppe.setAufgaben(aufgaben.stream().map(AufgabeEntity::toAufgabe).toList());
|
||||
gruppe.setStrafen(strafen.stream().map(StrafeEntity::toStrafe).toList());
|
||||
gruppe.setSperren(sperren.stream().map(SperreEntity::toSperre).toList());
|
||||
gruppe.setFinisher(finisher.stream().map(FinisherEntity::toFinisher).toList());
|
||||
return gruppe;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,11 @@ public class FavoritEntity {
|
||||
public UUID getAufgabenGruppeId() { return aufgabenGruppeId; }
|
||||
public void setAufgabenGruppeId(UUID aufgabenGruppeId) { this.aufgabenGruppeId = aufgabenGruppeId; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FavoritEntity[favoritId=" + favoritId + ", userId=" + userId + ", gruppeId=" + aufgabenGruppeId + "]";
|
||||
}
|
||||
|
||||
public Favorit toFavorit() {
|
||||
Favorit favorit = new Favorit();
|
||||
favorit.setAufgabenGruppeId(aufgabenGruppeId);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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 jakarta.persistence.CascadeType;
|
||||
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.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "finisher")
|
||||
public class FinisherEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID finisherId;
|
||||
@Column
|
||||
private String kurzText;
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String text;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column
|
||||
private GeschlechtEnum geschlecht;
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "gruppeId")
|
||||
private AufgabenGruppeEntity aufgabenGruppe;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(targetClass = Werkzeug.class)
|
||||
@CollectionTable(name = "finisher_benoetigtAktiv", joinColumns = @JoinColumn(name = "finisherId"))
|
||||
@Column(name = "werkzeug")
|
||||
private List<Werkzeug> benoetigtAktiv;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(targetClass = Werkzeug.class)
|
||||
@CollectionTable(name = "finisher_benoetigtPassiv", joinColumns = @JoinColumn(name = "finisherId"))
|
||||
@Column(name = "werkzeug")
|
||||
private List<Werkzeug> benoetigtPassiv;
|
||||
@ManyToMany(cascade = CascadeType.DETACH)
|
||||
@JoinTable(name = "finisherToy", joinColumns = {@JoinColumn(name = "finisherId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")})
|
||||
private List<ToyEntity> benoetigteToys;
|
||||
|
||||
public UUID getFinisherId() { return finisherId; }
|
||||
public void setFinisherId(UUID finisherId) { this.finisherId = finisherId; }
|
||||
|
||||
public String getKurzText() { return kurzText; }
|
||||
public void setKurzText(String kurzText) { this.kurzText = kurzText; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public GeschlechtEnum getGeschlecht() { return geschlecht; }
|
||||
public void setGeschlecht(GeschlechtEnum geschlecht) { this.geschlecht = geschlecht; }
|
||||
|
||||
public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; }
|
||||
public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; }
|
||||
|
||||
public List<Werkzeug> getBenoetigtAktiv() { return benoetigtAktiv; }
|
||||
public void setBenoetigtAktiv(List<Werkzeug> benoetigtAktiv) { this.benoetigtAktiv = benoetigtAktiv; }
|
||||
|
||||
public List<Werkzeug> getBenoetigtPassiv() { return benoetigtPassiv; }
|
||||
public void setBenoetigtPassiv(List<Werkzeug> benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; }
|
||||
|
||||
public List<ToyEntity> getBenoetigteToys() { return benoetigteToys; }
|
||||
public void setBenoetigteToys(List<ToyEntity> benoetigteToys) { this.benoetigteToys = benoetigteToys; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FinisherEntity[id=" + finisherId + ", kurzText=" + kurzText + ", geschlecht=" + geschlecht + "]";
|
||||
}
|
||||
|
||||
public Finisher toFinisher() {
|
||||
Finisher finisher = new Finisher();
|
||||
finisher.setFinisherId(finisherId);
|
||||
finisher.setKurzText(kurzText);
|
||||
finisher.setText(text);
|
||||
finisher.setGeschlecht(geschlecht);
|
||||
finisher.setBenoetigtAktiv(benoetigtAktiv);
|
||||
finisher.setBenoetigtPassiv(benoetigtPassiv);
|
||||
finisher.setBenoetigteToys(benoetigteToys.stream().map(ToyEntity::toToy).toList());
|
||||
finisher.setGruppeId(aufgabenGruppe.getGruppenId());
|
||||
return finisher;
|
||||
}
|
||||
|
||||
public static FinisherEntity create(Finisher finisher, AufgabenGruppeEntity aufgabenGruppeEntity, List<ToyEntity> toys) {
|
||||
FinisherEntity entity = new FinisherEntity();
|
||||
entity.setFinisherId(UUID.randomUUID());
|
||||
entity.setAufgabenGruppe(aufgabenGruppeEntity);
|
||||
entity.setKurzText(finisher.getKurzText());
|
||||
entity.setText(finisher.getText());
|
||||
entity.setGeschlecht(finisher.getGeschlecht());
|
||||
entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv());
|
||||
entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv());
|
||||
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,10 @@ public class GruppenAboEntity {
|
||||
|
||||
public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; }
|
||||
public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GruppenAboEntity[aboId=" + aboId + ", userId=" + userId
|
||||
+ ", gruppe=" + (aufgabenGruppe != null ? aufgabenGruppe.getName() : null) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,12 @@ public class SperreEntity {
|
||||
public List<ToyEntity> getBenoetigteToys() { return benoetigteToys; }
|
||||
public void setBenoetigteToys(List<ToyEntity> benoetigteToys) { this.benoetigteToys = benoetigteToys; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SperreEntity[id=" + sperreId + ", kurzText=" + kurzText
|
||||
+ ", minuten=" + minutenVon + "-" + minutenBis + ", fuer=" + sperreFuer + "]";
|
||||
}
|
||||
|
||||
public Sperre toSperre() {
|
||||
Sperre sperre = new Sperre();
|
||||
sperre.setSperreId(sperreId);
|
||||
|
||||
@@ -84,6 +84,12 @@ public class StrafeEntity {
|
||||
public List<ToyEntity> getBenoetigteToys() { return benoetigteToys; }
|
||||
public void setBenoetigteToys(List<ToyEntity> benoetigteToys) { this.benoetigteToys = benoetigteToys; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StrafeEntity[id=" + strafeId + ", kurzText=" + kurzText + ", level=" + level
|
||||
+ ", sekunden=" + sekundenVon + "-" + sekundenBis + "]";
|
||||
}
|
||||
|
||||
public Strafe toStrafe() {
|
||||
Strafe strafe = new Strafe();
|
||||
strafe.setStrafeId(strafeId);
|
||||
|
||||
@@ -42,6 +42,11 @@ public class ToyEntity {
|
||||
public byte[] getBild() { return bild; }
|
||||
public void setBild(byte[] bild) { this.bild = bild; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ToyEntity[toyId=" + toyId + ", name=" + name + ", userId=" + userId + "]";
|
||||
}
|
||||
|
||||
public Toy toToy() {
|
||||
Toy toy = new Toy();
|
||||
toy.setToyId(toyId);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package de.oaa.xxx.aufgaben.repository;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AufgabeRepository extends JpaRepository<AufgabeEntity, UUID> {
|
||||
|
||||
List<AufgabeEntity> findByAufgabenGruppeIn(List<AufgabenGruppeEntity> gruppen);
|
||||
}
|
||||
|
||||
@@ -11,4 +11,6 @@ public interface FavoritRepository extends JpaRepository<FavoritEntity, UUID> {
|
||||
List<FavoritEntity> findByUserId(UUID userId);
|
||||
|
||||
List<FavoritEntity> findByUserIdAndAufgabenGruppeId(UUID userId, UUID aufgabenGruppeId);
|
||||
|
||||
void deleteByAufgabenGruppeId(UUID aufgabenGruppeId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.aufgaben.repository;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.FinisherEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FinisherRepository extends JpaRepository<FinisherEntity, UUID> {
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package de.oaa.xxx.aufgaben.repository;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.SperreEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface SperreRepository extends JpaRepository<SperreEntity, UUID> {
|
||||
|
||||
List<SperreEntity> findByAufgabenGruppeIn(List<AufgabenGruppeEntity> gruppen);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package de.oaa.xxx.aufgaben.repository;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.StrafeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface StrafeRepository extends JpaRepository<StrafeEntity, UUID> {
|
||||
|
||||
List<StrafeEntity> findByAufgabenGruppeIn(List<AufgabenGruppeEntity> gruppen);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -16,6 +17,8 @@ public interface ToyRepository extends JpaRepository<ToyEntity, UUID> {
|
||||
|
||||
Page<ToyEntity> findByUserId(UUID userId, Pageable pageable);
|
||||
|
||||
List<ToyEntity> findByUserId(UUID userId);
|
||||
|
||||
boolean existsByNameIgnoreCaseAndUserIdIsNull(String name);
|
||||
|
||||
boolean existsByNameIgnoreCaseAndUserId(String name, UUID userId);
|
||||
|
||||
@@ -36,18 +36,40 @@ public class SecurityConfig {
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/toys.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/aufgaben.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/entdecken.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/profile.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/infovanilla.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/infobdsm.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/infochastity.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionvanilla.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsm.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionchastity.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtasks.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtoys.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmingame.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/personen-suchen.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/freunde.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/nachrichten.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/benutzer.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.html")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/css/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/js/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/images/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/favicon.ico")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.png")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.jpg")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.svg")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.webp")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/login")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/login/publickey")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/login/logout")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/user")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/registration")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/registration")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/activation")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/activation/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/password-reset/request")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/password-reset/confirm")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/email-change/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/filler")).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package de.oaa.xxx.emailchange;
|
||||
|
||||
import de.oaa.xxx.mail.Email;
|
||||
import de.oaa.xxx.mail.MailService;
|
||||
import de.oaa.xxx.mail.MailTemplateService;
|
||||
import de.oaa.xxx.registration.RegistrationRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/email-change")
|
||||
public class EmailChangeController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(EmailChangeController.class);
|
||||
|
||||
@Value("${app.base-url:http://localhost:8080}")
|
||||
private String baseUrl;
|
||||
|
||||
private final EmailChangeRepository emailChangeRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final MailService mailService;
|
||||
private final MailTemplateService mailTemplateService;
|
||||
|
||||
public EmailChangeController(EmailChangeRepository emailChangeRepository,
|
||||
UserRepository userRepository,
|
||||
RegistrationRepository registrationRepository,
|
||||
MailService mailService,
|
||||
MailTemplateService mailTemplateService) {
|
||||
this.emailChangeRepository = emailChangeRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.mailService = mailService;
|
||||
this.mailTemplateService = mailTemplateService;
|
||||
}
|
||||
|
||||
record EmailChangeRequest(String newEmail) {}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> requestChange(@RequestBody EmailChangeRequest request, Principal principal) {
|
||||
String currentEmail = principal.getName();
|
||||
String newEmail = request.newEmail();
|
||||
|
||||
if (userRepository.findByEmail(newEmail).isPresent()
|
||||
|| registrationRepository.findByEmail(newEmail).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
// Remove any pending request for this user
|
||||
emailChangeRepository.findByUserEmail(currentEmail)
|
||||
.ifPresent(emailChangeRepository::delete);
|
||||
|
||||
var user = userRepository.findByEmail(currentEmail);
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
|
||||
EmailChangeEntity entity = EmailChangeEntity.create(currentEmail, newEmail);
|
||||
emailChangeRepository.save(entity);
|
||||
|
||||
Email email = new Email();
|
||||
email.setTitel("Bitte bestätige deine neue E-Mail-Adresse");
|
||||
email.setEmailAdresse(newEmail);
|
||||
String confirmLink = baseUrl + "/email-change/" + entity.getTokenId().toString();
|
||||
email.setText(mailTemplateService.buildEmailChangeMail(user.get().getName(), confirmLink, newEmail));
|
||||
|
||||
if (!mailService.send(email)) {
|
||||
emailChangeRepository.delete(entity);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.status(202).build();
|
||||
}
|
||||
|
||||
@GetMapping("/{token}")
|
||||
public void confirm(@PathVariable String token, HttpServletResponse response) throws IOException {
|
||||
UUID tokenId;
|
||||
try {
|
||||
tokenId = UUID.fromString(token);
|
||||
} catch (IllegalArgumentException e) {
|
||||
response.sendRedirect("/login.html");
|
||||
return;
|
||||
}
|
||||
|
||||
var entity = emailChangeRepository.findById(tokenId);
|
||||
if (entity.isEmpty()) {
|
||||
response.sendRedirect("/login.html");
|
||||
return;
|
||||
}
|
||||
|
||||
var user = userRepository.findByEmail(entity.get().getUserEmail());
|
||||
if (user.isPresent()) {
|
||||
user.get().setEmail(entity.get().getNewEmail());
|
||||
userRepository.save(user.get());
|
||||
LOGGER.info("E-Mail geändert von {} zu {}", entity.get().getUserEmail(), entity.get().getNewEmail());
|
||||
}
|
||||
|
||||
emailChangeRepository.delete(entity.get());
|
||||
|
||||
// Clear JWT cookie so user must log in with new email
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", "")
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
response.sendRedirect("/login.html?emailChanged=1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.oaa.xxx.emailchange;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "email_change")
|
||||
public class EmailChangeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID tokenId;
|
||||
|
||||
@Column
|
||||
private String userEmail;
|
||||
|
||||
@Column
|
||||
private String newEmail;
|
||||
|
||||
@Column
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getTokenId() { return tokenId; }
|
||||
public void setTokenId(UUID tokenId) { this.tokenId = tokenId; }
|
||||
|
||||
public String getUserEmail() { return userEmail; }
|
||||
public void setUserEmail(String userEmail) { this.userEmail = userEmail; }
|
||||
|
||||
public String getNewEmail() { return newEmail; }
|
||||
public void setNewEmail(String newEmail) { this.newEmail = newEmail; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EmailChangeEntity[tokenId=" + tokenId + ", userEmail=" + userEmail + ", newEmail=" + newEmail + ", createdAt=" + createdAt + "]";
|
||||
}
|
||||
|
||||
public static EmailChangeEntity create(String userEmail, String newEmail) {
|
||||
EmailChangeEntity entity = new EmailChangeEntity();
|
||||
entity.setTokenId(UUID.randomUUID());
|
||||
entity.setUserEmail(userEmail);
|
||||
entity.setNewEmail(newEmail);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.oaa.xxx.emailchange;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface EmailChangeRepository extends JpaRepository<EmailChangeEntity, UUID> {
|
||||
|
||||
Optional<EmailChangeEntity> findByUserEmail(String userEmail);
|
||||
}
|
||||
@@ -32,6 +32,90 @@ public class MailTemplateService {
|
||||
@Value("${app.theme.color-success:#2ecc71}")
|
||||
private String colorSuccess;
|
||||
|
||||
public String buildEmailChangeMail(String name, String confirmLink, String newEmail) {
|
||||
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;">Moin %s,</p>
|
||||
<p style="color:%s; margin:0 0 0.5rem 0;">Du hast eine Änderung deiner E-Mail-Adresse angefordert.</p>
|
||||
<p style="color:%s; margin:0 0 2rem 0;">Klick auf den Button, um deine neue Adresse <strong style="color:%s;">%s</strong> zu bestätigen:</p>
|
||||
|
||||
<div style="text-align:center; margin:0 0 2rem 0;">
|
||||
<a href="%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;">
|
||||
E-Mail-Adresse bestätigen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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;">
|
||||
Falls du diese Änderung nicht angefordert hast, kannst du diese E-Mail einfach ignorieren.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""".formatted(
|
||||
colorBg, colorText,
|
||||
colorCard, colorSecondary,
|
||||
colorPrimary,
|
||||
colorText, name,
|
||||
colorText,
|
||||
colorText, colorPrimary, newEmail,
|
||||
confirmLink, colorPrimary,
|
||||
colorSecondary,
|
||||
colorMuted
|
||||
);
|
||||
}
|
||||
|
||||
public String buildPasswordResetMail(String name, String resetLink) {
|
||||
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;">Moin %s,</p>
|
||||
<p style="color:%s; margin:0 0 0.5rem 0;">Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
|
||||
<p style="color:%s; margin:0 0 2rem 0;">Klick auf den Button, um ein neues Passwort zu vergeben:</p>
|
||||
|
||||
<div style="text-align:center; margin:0 0 2rem 0;">
|
||||
<a href="%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;">
|
||||
Passwort zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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;">
|
||||
Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail einfach ignorieren.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""".formatted(
|
||||
colorBg, colorText,
|
||||
colorCard, colorSecondary,
|
||||
colorPrimary,
|
||||
colorText, name,
|
||||
colorText,
|
||||
colorText,
|
||||
resetLink, colorPrimary,
|
||||
colorSecondary,
|
||||
colorMuted
|
||||
);
|
||||
}
|
||||
|
||||
public String buildActivationMail(String name, String activationLink, String activatePageUrl, String uuid) {
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.oaa.xxx.passwordreset;
|
||||
|
||||
public record PasswordResetConfirm(String token, String passwordHash) {}
|
||||
@@ -0,0 +1,77 @@
|
||||
package de.oaa.xxx.passwordreset;
|
||||
|
||||
import de.oaa.xxx.mail.Email;
|
||||
import de.oaa.xxx.mail.MailService;
|
||||
import de.oaa.xxx.mail.MailTemplateService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/password-reset")
|
||||
public class PasswordResetController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PasswordResetController.class);
|
||||
|
||||
@Value("${app.base-url:http://localhost:8080}")
|
||||
private String baseUrl;
|
||||
|
||||
private final PasswordResetRepository passwordResetRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final MailService mailService;
|
||||
private final MailTemplateService mailTemplateService;
|
||||
|
||||
public PasswordResetController(PasswordResetRepository passwordResetRepository,
|
||||
UserRepository userRepository,
|
||||
MailService mailService,
|
||||
MailTemplateService mailTemplateService) {
|
||||
this.passwordResetRepository = passwordResetRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.mailService = mailService;
|
||||
this.mailTemplateService = mailTemplateService;
|
||||
}
|
||||
|
||||
@PostMapping("/request")
|
||||
public ResponseEntity<Void> request(@RequestBody PasswordResetRequest request) {
|
||||
userRepository.findByEmail(request.email()).ifPresent(user -> {
|
||||
passwordResetRepository.findByEmail(request.email())
|
||||
.ifPresent(passwordResetRepository::delete);
|
||||
PasswordResetEntity entity = PasswordResetEntity.create(request.email());
|
||||
passwordResetRepository.save(entity);
|
||||
String resetLink = baseUrl + "/reset-password.html?token=" + entity.getTokenId();
|
||||
Email email = new Email();
|
||||
email.setTitel("Passwort zurücksetzen");
|
||||
email.setEmailAdresse(request.email());
|
||||
email.setText(mailTemplateService.buildPasswordResetMail(user.getName(), resetLink));
|
||||
mailService.send(email);
|
||||
LOGGER.info("Passwort-Reset angefordert für: {}", request.email());
|
||||
});
|
||||
return ResponseEntity.status(202).build();
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
public ResponseEntity<Void> confirm(@RequestBody PasswordResetConfirm confirm) {
|
||||
UUID tokenId;
|
||||
try {
|
||||
tokenId = UUID.fromString(confirm.token());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
var entity = passwordResetRepository.findById(tokenId);
|
||||
if (entity.isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
userRepository.findByEmail(entity.get().getEmail()).ifPresent(user -> {
|
||||
user.setPassword(confirm.passwordHash());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("Passwort zurückgesetzt für: {}", entity.get().getEmail());
|
||||
});
|
||||
passwordResetRepository.delete(entity.get());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.oaa.xxx.passwordreset;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "password_reset")
|
||||
public class PasswordResetEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID tokenId;
|
||||
|
||||
@Column(unique = true)
|
||||
private String email;
|
||||
|
||||
@Column
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getTokenId() { return tokenId; }
|
||||
public void setTokenId(UUID tokenId) { this.tokenId = tokenId; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PasswordResetEntity[tokenId=" + tokenId + ", email=" + email + ", createdAt=" + createdAt + "]";
|
||||
}
|
||||
|
||||
public static PasswordResetEntity create(String email) {
|
||||
PasswordResetEntity entity = new PasswordResetEntity();
|
||||
entity.setTokenId(UUID.randomUUID());
|
||||
entity.setEmail(email);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.oaa.xxx.passwordreset;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PasswordResetRepository extends JpaRepository<PasswordResetEntity, UUID> {
|
||||
Optional<PasswordResetEntity> findByEmail(String email);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.oaa.xxx.passwordreset;
|
||||
|
||||
public record PasswordResetRequest(String email) {}
|
||||
@@ -44,6 +44,11 @@ public class RegistrationController {
|
||||
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (registrationRepository.findByName(registration.getName()).isPresent()
|
||||
|| userRepository.findByName(registration.getName()).isPresent()) {
|
||||
LOGGER.warn("User mit Name {} bereits vorhanden", registration.getName());
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
RegistrationEntity entity = RegistrationEntity.create(registration);
|
||||
registrationRepository.save(entity);
|
||||
|
||||
|
||||
@@ -8,4 +8,5 @@ import java.util.UUID;
|
||||
public interface RegistrationRepository extends JpaRepository<RegistrationEntity, UUID> {
|
||||
|
||||
Optional<RegistrationEntity> findByEmail(String email);
|
||||
Optional<RegistrationEntity> findByName(String name);
|
||||
}
|
||||
|
||||
@@ -34,4 +34,10 @@ public class AktiveSperre {
|
||||
|
||||
public String getReleaseText() { return releaseText; }
|
||||
public void setReleaseText(String releaseText) { this.releaseText = releaseText; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AktiveSperre[id=" + aktiveSperreId + ", mitspieler=" + (mitspieler != null ? mitspieler.getName() : null)
|
||||
+ ", " + minuten + "min, von=" + startzeit + ", bis=" + endzeit + ", fuer=" + fuer + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public class AufgabeAnzeige {
|
||||
private String aufgabeText;
|
||||
private Integer timer;
|
||||
private Callback callback;
|
||||
private Integer level;
|
||||
|
||||
public String getNameAktiverMitspieler() { return nameAktiverMitspieler; }
|
||||
public void setNameAktiverMitspieler(String nameAktiverMitspieler) { this.nameAktiverMitspieler = nameAktiverMitspieler; }
|
||||
@@ -18,4 +19,13 @@ public class AufgabeAnzeige {
|
||||
|
||||
public Callback getCallback() { return callback; }
|
||||
public void setCallback(Callback callback) { this.callback = callback; }
|
||||
|
||||
public Integer getLevel() { return level; }
|
||||
public void setLevel(Integer level) { this.level = level; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AufgabeAnzeige[mitspieler=" + nameAktiverMitspieler + ", level=" + level + ", timer=" + timer
|
||||
+ ", callback=" + (callback != null ? callback.getClass().getSimpleName() : null) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,9 @@ public abstract class Callback {
|
||||
|
||||
public UUID getSessionId() { return sessionId; }
|
||||
public void setSessionId(UUID sessionId) { this.sessionId = sessionId; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[sessionId=" + sessionId + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ public class Mitspieler {
|
||||
return verfuegbareWerkzeuge.contains(werkzeug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Mitspieler[id=" + id + ", name=" + name + ", geschlecht=" + geschlecht
|
||||
+ ", rollen=" + rollen + ", werkzeuge=" + verfuegbareWerkzeuge + "]";
|
||||
}
|
||||
|
||||
public boolean isPassenderSpielpartner(Mitspieler other) {
|
||||
if (!spieltMit.contains(other.getGeschlecht())) {
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.oaa.xxx.session;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Session {
|
||||
@@ -10,6 +11,10 @@ public class Session {
|
||||
private Integer wahrscheinlichkeitStrafe;
|
||||
private Integer aufgabenProLevel;
|
||||
private Double zeitfaktorZeitstrafen;
|
||||
private Integer level;
|
||||
private Integer aufgabenAufAktuellemLevel;
|
||||
private LocalDateTime startZeit;
|
||||
private LocalDateTime letzteAktivitaet;
|
||||
|
||||
public UUID getSessionId() { return sessionId; }
|
||||
public void setSessionId(UUID sessionId) { this.sessionId = sessionId; }
|
||||
@@ -28,4 +33,24 @@ public class Session {
|
||||
|
||||
public Double getZeitfaktorZeitstrafen() { return zeitfaktorZeitstrafen; }
|
||||
public void setZeitfaktorZeitstrafen(Double zeitfaktorZeitstrafen) { this.zeitfaktorZeitstrafen = zeitfaktorZeitstrafen; }
|
||||
|
||||
public Integer getLevel() { return level; }
|
||||
public void setLevel(Integer level) { this.level = level; }
|
||||
|
||||
public Integer getAufgabenAufAktuellemLevel() { return aufgabenAufAktuellemLevel; }
|
||||
public void setAufgabenAufAktuellemLevel(Integer aufgabenAufAktuellemLevel) { this.aufgabenAufAktuellemLevel = aufgabenAufAktuellemLevel; }
|
||||
|
||||
public LocalDateTime getStartZeit() { return startZeit; }
|
||||
public void setStartZeit(LocalDateTime startZeit) { this.startZeit = startZeit; }
|
||||
|
||||
public LocalDateTime getLetzteAktivitaet() { return letzteAktivitaet; }
|
||||
public void setLetzteAktivitaet(LocalDateTime letzteAktivitaet) { this.letzteAktivitaet = letzteAktivitaet; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Session[sessionId=" + sessionId + ", userId=" + userId
|
||||
+ ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel
|
||||
+ ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%"
|
||||
+ ", zeitfaktor=" + zeitfaktorZeitstrafen + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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;
|
||||
@@ -9,11 +15,6 @@ import de.oaa.xxx.session.entity.SessionEntity;
|
||||
import de.oaa.xxx.session.sperre.SperreCallback;
|
||||
import de.oaa.xxx.session.sperre.SperrenVerlaengernCallback;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SessionDurchfuehren {
|
||||
|
||||
private final AufgabenList aufgabenList;
|
||||
@@ -23,12 +24,12 @@ public class SessionDurchfuehren {
|
||||
private final Integer wahrscheinlichkeitSperre;
|
||||
private final Integer wahrscheinlichkeitStrafe;
|
||||
|
||||
private final Integer aufgabenProLevel;
|
||||
private Integer level;
|
||||
private Integer aufgabenAufAktuellemLevel;
|
||||
private int aufgabenProLevel;
|
||||
private int level;
|
||||
private int aufgabenAufAktuellemLevel;
|
||||
|
||||
public SessionDurchfuehren(SessionEntity entity) throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
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)));
|
||||
@@ -36,13 +37,16 @@ public class SessionDurchfuehren {
|
||||
wahrscheinlichkeitSperre = entity.getWahrscheinlichkeitSperre();
|
||||
wahrscheinlichkeitStrafe = entity.getWahrscheinlichkeitStrafe();
|
||||
|
||||
aufgabenProLevel = entity.getAufgabenProLevel() != null ? entity.getAufgabenProLevel() : 5;
|
||||
level = entity.getLevel() != null ? entity.getLevel() : 1;
|
||||
aufgabenAufAktuellemLevel = entity.getAufgabenAufAktuellemLevel() != null ? entity.getAufgabenAufAktuellemLevel() : 0;
|
||||
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) {
|
||||
@@ -67,9 +71,34 @@ public class SessionDurchfuehren {
|
||||
}
|
||||
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);
|
||||
aufgabenList.getFinisher().stream()
|
||||
.filter(finisher -> geschlecht == finisher.getGeschlecht())
|
||||
.findAny()
|
||||
.ifPresent(aufgabe -> {
|
||||
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 >= aufgabenProLevel && level < 5) {
|
||||
if (++aufgabenAufAktuellemLevel >= 1 + aufgabenProLevel) {
|
||||
aufgabenAufAktuellemLevel = 0;
|
||||
level++;
|
||||
}
|
||||
@@ -210,4 +239,12 @@ public class SessionDurchfuehren {
|
||||
.toList();
|
||||
return list.isEmpty() ? null : list.get(new Random().nextInt(list.size()));
|
||||
}
|
||||
|
||||
public int getAufgabenAufAktuellemLevel() {
|
||||
return aufgabenAufAktuellemLevel;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ public class Aufgabe {
|
||||
public List<Werkzeug> getBenoetigtPassiv() { return benoetigtPassiv; }
|
||||
public void setBenoetigtPassiv(List<Werkzeug> benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Aufgabe[id=" + aufgabeId + ", kurzText=" + kurzText + ", level=" + level
|
||||
+ ", sekunden=" + sekundenVon + "-" + sekundenBis + ", gruppeId=" + gruppeId + "]";
|
||||
}
|
||||
|
||||
public boolean isAufgabePassend(int level, Mitspieler aktiv, Mitspieler passiv) {
|
||||
if (level != this.level && level - 1 != this.level) {
|
||||
return false;
|
||||
|
||||
@@ -7,6 +7,7 @@ public class AufgabenList {
|
||||
private List<Aufgabe> aufgaben;
|
||||
private List<Sperre> sperren;
|
||||
private List<Strafe> strafen;
|
||||
private List<Finisher> finisher;
|
||||
|
||||
public List<Aufgabe> getAufgaben() { return aufgaben; }
|
||||
public void setAufgaben(List<Aufgabe> aufgaben) { this.aufgaben = aufgaben; }
|
||||
@@ -17,11 +18,15 @@ public class AufgabenList {
|
||||
public List<Strafe> getStrafen() { return strafen; }
|
||||
public void setStrafen(List<Strafe> strafen) { this.strafen = strafen; }
|
||||
|
||||
public List<Finisher> getFinisher() { return finisher; }
|
||||
public void setFinisher(List<Finisher> finisher) { this.finisher = finisher; }
|
||||
|
||||
public int size() {
|
||||
int size = 0;
|
||||
if (aufgaben != null) size += aufgaben.size();
|
||||
if (sperren != null) size += sperren.size();
|
||||
if (strafen != null) size += strafen.size();
|
||||
if (getFinisher() != null) size += getFinisher().size();
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.oaa.xxx.session.aufgaben;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.session.GeschlechtEnum;
|
||||
import de.oaa.xxx.session.Werkzeug;
|
||||
|
||||
public class Finisher {
|
||||
|
||||
private UUID finisherId;
|
||||
private String kurzText;
|
||||
private String text;
|
||||
private GeschlechtEnum geschlecht;
|
||||
private List<Werkzeug> benoetigtAktiv;
|
||||
private List<Werkzeug> benoetigtPassiv;
|
||||
|
||||
public UUID getFinisherId() { return finisherId; }
|
||||
public void setFinisherId(UUID finisherId) { this.finisherId = finisherId; }
|
||||
|
||||
public String getKurzText() { return kurzText; }
|
||||
public void setKurzText(String kurzText) { this.kurzText = kurzText; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public GeschlechtEnum getGeschlecht() { return geschlecht; }
|
||||
public void setGeschlecht(GeschlechtEnum geschlecht) { this.geschlecht = geschlecht; }
|
||||
|
||||
public List<Werkzeug> getBenoetigtAktiv() { return benoetigtAktiv; }
|
||||
public void setBenoetigtAktiv(List<Werkzeug> benoetigtAktiv) { this.benoetigtAktiv = benoetigtAktiv; }
|
||||
|
||||
public List<Werkzeug> getBenoetigtPassiv() { return benoetigtPassiv; }
|
||||
public void setBenoetigtPassiv(List<Werkzeug> benoetigtPassiv) { this.benoetigtPassiv = benoetigtPassiv; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Finisher[id=" + finisherId + ", kurzText=" + kurzText + ", geschlecht=" + geschlecht + "]";
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,12 @@ public class Sperre {
|
||||
public Integer getMinutenBis() { return minutenBis; }
|
||||
public void setMinutenBis(Integer minutenBis) { this.minutenBis = minutenBis; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Sperre[id=" + sperreId + ", kurzText=" + kurzText
|
||||
+ ", minuten=" + minutenVon + "-" + minutenBis + ", fuer=" + sperreFuer + ", gruppeId=" + gruppeId + "]";
|
||||
}
|
||||
|
||||
public boolean isAufgabePassend(Mitspieler passiv) {
|
||||
for (Werkzeug werkzeug : sperreFuer) {
|
||||
if (!passiv.isVerfuegbar(werkzeug)) {
|
||||
|
||||
@@ -8,8 +8,10 @@ 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.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -26,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@@ -37,12 +40,17 @@ public class SessionController {
|
||||
|
||||
private final SessionRepository sessionRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
private final AktiveSperreRepository aktiveSperreRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SessionController(SessionRepository sessionRepository, MitspielerRepository mitspielerRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
|
||||
ObjectMapper objectMapper) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -62,7 +70,9 @@ public class SessionController {
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> create(@RequestBody Session session) {
|
||||
UUID userId = (UUID) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
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();
|
||||
entity.setSessionId(UUID.randomUUID());
|
||||
entity.setUserId(userId);
|
||||
@@ -76,6 +86,10 @@ public class SessionController {
|
||||
entity.setZeitfaktorZeitstrafen(session.getZeitfaktorZeitstrafen() != null ? session.getZeitfaktorZeitstrafen() : 1.0);
|
||||
entity.setLevel(1);
|
||||
sessionRepository.save(entity);
|
||||
LOGGER.debug("Session gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]",
|
||||
entity.getSessionId(), entity.getUserId(), entity.getAufgabenProLevel(),
|
||||
entity.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(),
|
||||
entity.getZeitfaktorZeitstrafen());
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getSessionId()).toUri()
|
||||
).build();
|
||||
@@ -85,6 +99,8 @@ public class SessionController {
|
||||
public ResponseEntity<Void> deleteSession(@RequestBody Session session) {
|
||||
return sessionRepository.findById(session.getSessionId())
|
||||
.map(entity -> {
|
||||
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(entity.getMitspieler());
|
||||
sessionRepository.delete(entity);
|
||||
return ResponseEntity.accepted().<Void>build();
|
||||
})
|
||||
@@ -119,7 +135,22 @@ public class SessionController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
session.setLetzteAktivitaet(LocalDateTime.now());
|
||||
AufgabeAnzeige next = new SessionDurchfuehren(session).getNext();
|
||||
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
|
||||
AufgabeAnzeige next = durchfuehren.getNext();
|
||||
session.setLevel(durchfuehren.getLevel());
|
||||
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
|
||||
if (next == null) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
next.setLevel(durchfuehren.getLevel());
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Neue Aufgabe [sessionId={}, level={}, aufgaben={}/{}, aktiveSperren={}]",
|
||||
sessionId, session.getLevel(), session.getAufgabenAufAktuellemLevel(),
|
||||
session.getAufgabenProLevel(), session.getAktiveSperren().size());
|
||||
session.getAktiveSperren().forEach(s ->
|
||||
LOGGER.debug(" Sperre [mitspieler={}, {}min, ende={}]",
|
||||
s.getMitspieler().getName(), s.getMinuten(), s.getEndzeit()));
|
||||
}
|
||||
return ResponseEntity.ok(next);
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
@@ -150,14 +181,48 @@ public class SessionController {
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@GetMapping("/{sessionId}/finisher")
|
||||
public ResponseEntity<List<AufgabeAnzeige>> getFinisher(@PathVariable UUID sessionId) {
|
||||
try {
|
||||
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) return ResponseEntity.badRequest().build();
|
||||
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
|
||||
return ResponseEntity.ok(durchfuehren.getFinisher());
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{sessionId}/backToLevel5")
|
||||
public ResponseEntity<Void> backToLevel5(@PathVariable UUID sessionId) {
|
||||
try {
|
||||
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) return ResponseEntity.badRequest().build();
|
||||
SessionDurchfuehren durchfuehren = new SessionDurchfuehren(session);
|
||||
durchfuehren.backToLvl5();
|
||||
session.setLevel(durchfuehren.getLevel());
|
||||
session.setAufgabenAufAktuellemLevel(durchfuehren.getAufgabenAufAktuellemLevel());
|
||||
sessionRepository.save(session);
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
private Session toSession(SessionEntity entity) {
|
||||
Session session = new Session();
|
||||
session.setSessionId(entity.getSessionId());
|
||||
session.setUserId(entity.getUserId());
|
||||
session.setAufgabenProLevel(entity.getAufgabenAufAktuellemLevel());
|
||||
session.setAufgabenProLevel(entity.getAufgabenProLevel());
|
||||
session.setWahrscheinlichkeitSperre(entity.getWahrscheinlichkeitSperre());
|
||||
session.setWahrscheinlichkeitStrafe(entity.getWahrscheinlichkeitStrafe());
|
||||
session.setZeitfaktorZeitstrafen(entity.getZeitfaktorZeitstrafen());
|
||||
session.setLevel(entity.getLevel());
|
||||
session.setAufgabenAufAktuellemLevel(entity.getAufgabenAufAktuellemLevel());
|
||||
session.setStartZeit(entity.getStartZeit());
|
||||
session.setLetzteAktivitaet(entity.getLetzteAktivitaet());
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package de.oaa.xxx.session.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;
|
||||
@@ -21,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController("sessionSperreController")
|
||||
@RequestMapping("/session/sperre")
|
||||
@@ -70,6 +74,24 @@ public class SperreController {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/aktive")
|
||||
public ResponseEntity<List<AktiveSperre>> getAktiveSperren(@RequestParam UUID sessionId) {
|
||||
try {
|
||||
SessionEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) return ResponseEntity.noContent().build();
|
||||
List<Mitspieler> mitspielerList = session.getMitspieler().stream()
|
||||
.map(m -> m.toMitspieler())
|
||||
.collect(Collectors.toList());
|
||||
List<AktiveSperre> sperren = session.getAktiveSperren().stream()
|
||||
.map(e -> e.toSperre(mitspielerList))
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.ok(sperren);
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/verlaengern")
|
||||
public ResponseEntity<Void> aktiveVerlaengern(@RequestBody SperrenVerlaengernCallback callback) {
|
||||
if (callback == null || callback.getSpielerId() == null || callback.getFaktor() == null) {
|
||||
|
||||
@@ -84,6 +84,12 @@ public class AktiveSperreEntity {
|
||||
return sperre;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AktiveSperreEntity[id=" + aktiveSperreId + ", mitspieler=" + (mitspieler != null ? mitspieler.getName() : null)
|
||||
+ ", " + minuten + "min, von=" + startzeit + ", bis=" + endzeit + ", fuer=" + fuer + "]";
|
||||
}
|
||||
|
||||
private Mitspieler getMitspielerFromList(List<Mitspieler> mitspielerList, UUID id) {
|
||||
Optional<Mitspieler> first = mitspielerList.stream().filter(m -> m.getId().equals(id)).findFirst();
|
||||
return first.orElse(null);
|
||||
|
||||
@@ -78,6 +78,12 @@ public class MitspielerEntity {
|
||||
public List<AktiveSperreEntity> getAktiveSperren() { return aktiveSperren; }
|
||||
public void setAktiveSperren(List<AktiveSperreEntity> aktiveSperren) { this.aktiveSperren = aktiveSperren; }
|
||||
|
||||
@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);
|
||||
|
||||
@@ -82,4 +82,12 @@ public class SessionEntity {
|
||||
|
||||
public Double getZeitfaktorZeitstrafen() { return zeitfaktorZeitstrafen; }
|
||||
public void setZeitfaktorZeitstrafen(Double zeitfaktorZeitstrafen) { this.zeitfaktorZeitstrafen = zeitfaktorZeitstrafen; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SessionEntity[sessionId=" + sessionId + ", userId=" + userId
|
||||
+ ", level=" + level + ", aufgaben=" + aufgabenAufAktuellemLevel + "/" + aufgabenProLevel
|
||||
+ ", pStrafe=" + wahrscheinlichkeitStrafe + "%, pSperre=" + wahrscheinlichkeitSperre + "%"
|
||||
+ ", zeitfaktor=" + zeitfaktorZeitstrafen + ", start=" + startZeit + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,9 @@ public class SperreCallback extends Callback {
|
||||
|
||||
public String getReleaseText() { return releaseText; }
|
||||
public void setReleaseText(String releaseText) { this.releaseText = releaseText; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SperreCallback[sessionId=" + getSessionId() + ", sperreId=" + sperreId + ", spielerId=" + spielerId + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,9 @@ public class SperrenVerlaengernCallback extends Callback {
|
||||
|
||||
public Integer getFaktor() { return faktor; }
|
||||
public void setFaktor(Integer faktor) { this.faktor = faktor; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SperrenVerlaengernCallback[sessionId=" + getSessionId() + ", spielerId=" + spielerId + ", faktor=" + faktor + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package de.oaa.xxx.social;
|
||||
|
||||
import de.oaa.xxx.social.dto.ProfileImageDto;
|
||||
import de.oaa.xxx.social.entity.ProfileImageEntity;
|
||||
import de.oaa.xxx.social.entity.ProfileImageLikeEntity;
|
||||
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/social/profile-images")
|
||||
public class ProfileImageController {
|
||||
|
||||
private static final int MAX_IMAGES_PER_USER = 20;
|
||||
|
||||
private final ProfileImageRepository profileImageRepository;
|
||||
private final ProfileImageLikeRepository profileImageLikeRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public ProfileImageController(ProfileImageRepository profileImageRepository,
|
||||
ProfileImageLikeRepository profileImageLikeRepository,
|
||||
UserRepository userRepository) {
|
||||
this.profileImageRepository = profileImageRepository;
|
||||
this.profileImageLikeRepository = profileImageLikeRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record UploadRequest(String imageData) {}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ProfileImageDto> uploadImage(@RequestBody UploadRequest request, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (request.imageData() == null || request.imageData().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (profileImageRepository.countByUserId(myId) >= MAX_IMAGES_PER_USER) {
|
||||
return ResponseEntity.status(422).build();
|
||||
}
|
||||
|
||||
ProfileImageEntity entity = new ProfileImageEntity();
|
||||
entity.setImageId(UUID.randomUUID());
|
||||
entity.setUserId(myId);
|
||||
entity.setImageData(request.imageData());
|
||||
entity.setUploadedAt(LocalDateTime.now());
|
||||
profileImageRepository.save(entity);
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(entity, myId));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<ProfileImageDto>> getImages(@RequestParam UUID userId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<ProfileImageEntity> images = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId);
|
||||
List<ProfileImageDto> dtos = images.stream().map(img -> toDto(img, myId)).toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{imageId}")
|
||||
public ResponseEntity<Void> deleteImage(@PathVariable UUID imageId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var imgOpt = profileImageRepository.findById(imageId);
|
||||
if (imgOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!imgOpt.get().getUserId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
profileImageLikeRepository.deleteByImageId(imageId);
|
||||
profileImageRepository.delete(imgOpt.get());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{imageId}/like")
|
||||
public ResponseEntity<Void> toggleLike(@PathVariable UUID imageId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (profileImageRepository.findById(imageId).isEmpty()) return ResponseEntity.notFound().build();
|
||||
|
||||
var existing = profileImageLikeRepository.findByImageIdAndUserId(imageId, myId);
|
||||
if (existing.isPresent()) {
|
||||
profileImageLikeRepository.delete(existing.get());
|
||||
} else {
|
||||
ProfileImageLikeEntity like = new ProfileImageLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
like.setImageId(imageId);
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
profileImageLikeRepository.save(like);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private ProfileImageDto toDto(ProfileImageEntity entity, UUID myId) {
|
||||
long likeCount = profileImageLikeRepository.countByImageId(entity.getImageId());
|
||||
boolean likedByMe = profileImageLikeRepository.findByImageIdAndUserId(entity.getImageId(), myId).isPresent();
|
||||
return new ProfileImageDto(entity.getImageId(), entity.getUserId(), entity.getImageData(),
|
||||
entity.getUploadedAt(), likeCount, likedByMe);
|
||||
}
|
||||
}
|
||||
273
xxxthegame/src/main/java/de/oaa/xxx/social/SocialController.java
Normal file
@@ -0,0 +1,273 @@
|
||||
package de.oaa.xxx.social;
|
||||
|
||||
import de.oaa.xxx.social.dto.ConversationSummary;
|
||||
import de.oaa.xxx.social.dto.FriendshipDto;
|
||||
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.MessageEntity;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.social.repository.MessageRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/social")
|
||||
public class SocialController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final FriendshipRepository friendshipRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
|
||||
public SocialController(UserRepository userRepository,
|
||||
FriendshipRepository friendshipRepository,
|
||||
MessageRepository messageRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.friendshipRepository = friendshipRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
}
|
||||
|
||||
record FriendRequestBody(UUID receiverId) {}
|
||||
record FriendshipActionBody(UUID friendshipId) {}
|
||||
record SendMessageBody(UUID receiverId, String text) {}
|
||||
|
||||
// ── User Profile ──
|
||||
|
||||
@GetMapping("/users/{userId}")
|
||||
public ResponseEntity<UserProfile> getUserProfile(@PathVariable UUID userId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
return userRepository.findById(userId)
|
||||
.map(u -> ResponseEntity.ok(toUserProfileWithStatus(u, myId)))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── User Search ──
|
||||
|
||||
@GetMapping("/users/search")
|
||||
public ResponseEntity<List<UserProfile>> searchUsers(@RequestParam String q, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<UserEntity> results = userRepository.findByNameContainingIgnoreCase(q);
|
||||
List<UserProfile> profiles = results.stream()
|
||||
.filter(u -> !u.getUserId().equals(myId))
|
||||
.limit(20)
|
||||
.map(u -> toUserProfileWithStatus(u, myId))
|
||||
.toList();
|
||||
return ResponseEntity.ok(profiles);
|
||||
}
|
||||
|
||||
// ── Friendship ──
|
||||
|
||||
@PostMapping("/friends/request")
|
||||
public ResponseEntity<Void> sendFriendRequest(@RequestBody FriendRequestBody body, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (friendshipRepository.findExisting(myId, body.receiverId()).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
FriendshipEntity f = new FriendshipEntity();
|
||||
f.setFriendshipId(UUID.randomUUID());
|
||||
f.setSenderId(myId);
|
||||
f.setReceiverId(body.receiverId());
|
||||
f.setStatus(Status.PENDING);
|
||||
f.setCreatedAt(LocalDateTime.now());
|
||||
friendshipRepository.save(f);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@PostMapping("/friends/accept")
|
||||
public ResponseEntity<Void> acceptFriendRequest(@RequestBody FriendshipActionBody body, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var fOpt = friendshipRepository.findById(body.friendshipId());
|
||||
if (fOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
FriendshipEntity f = fOpt.get();
|
||||
if (!f.getReceiverId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
f.setStatus(Status.ACCEPTED);
|
||||
friendshipRepository.save(f);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/friends/reject")
|
||||
public ResponseEntity<Void> rejectOrRemoveFriend(@RequestBody FriendshipActionBody body, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var fOpt = friendshipRepository.findById(body.friendshipId());
|
||||
if (fOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
FriendshipEntity f = fOpt.get();
|
||||
if (!f.getSenderId().equals(myId) && !f.getReceiverId().equals(myId)) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
friendshipRepository.delete(f);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/friends")
|
||||
public ResponseEntity<List<FriendshipDto>> getFriends(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<FriendshipDto> dtos = friendshipRepository.findFriends(myId, Status.ACCEPTED).stream()
|
||||
.map(f -> {
|
||||
UUID friendId = f.getSenderId().equals(myId) ? f.getReceiverId() : f.getSenderId();
|
||||
return userRepository.findById(friendId)
|
||||
.map(u -> new FriendshipDto(
|
||||
f.getFriendshipId(),
|
||||
new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), "FRIEND"),
|
||||
f.getStatus().name(),
|
||||
f.getCreatedAt()))
|
||||
.orElse(null);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@GetMapping("/friends/pending")
|
||||
public ResponseEntity<List<FriendshipDto>> getPendingRequests(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<FriendshipDto> dtos = friendshipRepository.findByReceiverIdAndStatus(myId, Status.PENDING).stream()
|
||||
.map(f -> userRepository.findById(f.getSenderId())
|
||||
.map(u -> new FriendshipDto(
|
||||
f.getFriendshipId(),
|
||||
new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), "PENDING_RECEIVED"),
|
||||
f.getStatus().name(),
|
||||
f.getCreatedAt()))
|
||||
.orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@GetMapping("/friends/pending/count")
|
||||
public ResponseEntity<Long> getPendingCount(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
return ResponseEntity.ok(friendshipRepository.countByReceiverIdAndStatus(myId, Status.PENDING));
|
||||
}
|
||||
|
||||
// ── Messages ──
|
||||
|
||||
@PostMapping("/messages")
|
||||
public ResponseEntity<Void> sendMessage(@RequestBody SendMessageBody body, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (body.text() == null || body.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
MessageEntity msg = new MessageEntity();
|
||||
msg.setMessageId(UUID.randomUUID());
|
||||
msg.setSenderId(myId);
|
||||
msg.setReceiverId(body.receiverId());
|
||||
msg.setText(body.text().trim());
|
||||
msg.setSentAt(LocalDateTime.now());
|
||||
messageRepository.save(msg);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@GetMapping("/messages")
|
||||
public ResponseEntity<List<ConversationSummary>> getConversations(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<MessageEntity> allMessages = messageRepository.findAllByUser(myId);
|
||||
|
||||
// Group by partner, keep most recent message per partner
|
||||
Map<UUID, MessageEntity> latestByPartner = new LinkedHashMap<>();
|
||||
for (MessageEntity m : allMessages) {
|
||||
UUID partnerId = m.getSenderId().equals(myId) ? m.getReceiverId() : m.getSenderId();
|
||||
latestByPartner.putIfAbsent(partnerId, m);
|
||||
}
|
||||
|
||||
List<ConversationSummary> summaries = new ArrayList<>();
|
||||
for (Map.Entry<UUID, MessageEntity> entry : latestByPartner.entrySet()) {
|
||||
UUID partnerId = entry.getKey();
|
||||
MessageEntity lastMsg = entry.getValue();
|
||||
var partnerOpt = userRepository.findById(partnerId);
|
||||
if (partnerOpt.isEmpty()) continue;
|
||||
|
||||
UserProfile partnerProfile = toUserProfileWithStatus(partnerOpt.get(), myId);
|
||||
MessageDto lastMsgDto = toMessageDto(lastMsg);
|
||||
long unreadCount = allMessages.stream()
|
||||
.filter(m -> m.getSenderId().equals(partnerId)
|
||||
&& m.getReceiverId().equals(myId)
|
||||
&& m.getReadAt() == null)
|
||||
.count();
|
||||
summaries.add(new ConversationSummary(partnerProfile, lastMsgDto, unreadCount));
|
||||
}
|
||||
return ResponseEntity.ok(summaries);
|
||||
}
|
||||
|
||||
@GetMapping("/messages/unread/count")
|
||||
public ResponseEntity<Long> getUnreadCount(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
return ResponseEntity.ok(messageRepository.countUnread(myId));
|
||||
}
|
||||
|
||||
@GetMapping("/messages/{partnerId}")
|
||||
public ResponseEntity<List<MessageDto>> getConversation(@PathVariable UUID partnerId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<MessageEntity> messages = messageRepository.findConversation(myId, partnerId, PageRequest.of(0, 50));
|
||||
messageRepository.markAsRead(myId, partnerId, LocalDateTime.now());
|
||||
|
||||
return ResponseEntity.ok(messages.stream().map(this::toMessageDto).toList());
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private UserProfile toUserProfileWithStatus(UserEntity user, UUID 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";
|
||||
}
|
||||
}
|
||||
return new UserProfile(user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(), status);
|
||||
}
|
||||
|
||||
private MessageDto toMessageDto(MessageEntity m) {
|
||||
String senderName = userRepository.findById(m.getSenderId())
|
||||
.map(UserEntity::getName)
|
||||
.orElse("Unbekannt");
|
||||
return new MessageDto(
|
||||
m.getMessageId(), m.getSenderId(), senderName,
|
||||
m.getReceiverId(), m.getText(), m.getSentAt(), m.getReadAt() != null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
public record ConversationSummary(UserProfile partner, MessageDto lastMessage, long unreadCount) {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record FriendshipDto(UUID friendshipId, UserProfile user, String status, LocalDateTime createdAt) {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record MessageDto(UUID messageId, UUID senderId, String senderName, UUID receiverId, String text, LocalDateTime sentAt, boolean read) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ProfileImageDto(UUID imageId, UUID userId, String imageData,
|
||||
LocalDateTime uploadedAt, long likeCount, boolean likedByMe) {}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "friendship")
|
||||
public class FriendshipEntity {
|
||||
|
||||
public enum Status { PENDING, ACCEPTED }
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID friendshipId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID senderId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID receiverId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10)
|
||||
private Status status;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getFriendshipId() { return friendshipId; }
|
||||
public void setFriendshipId(UUID friendshipId) { this.friendshipId = friendshipId; }
|
||||
|
||||
public UUID getSenderId() { return senderId; }
|
||||
public void setSenderId(UUID senderId) { this.senderId = senderId; }
|
||||
|
||||
public UUID getReceiverId() { return receiverId; }
|
||||
public void setReceiverId(UUID receiverId) { this.receiverId = receiverId; }
|
||||
|
||||
public Status getStatus() { return status; }
|
||||
public void setStatus(Status status) { this.status = status; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "message")
|
||||
public class MessageEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID messageId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID senderId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID receiverId;
|
||||
|
||||
@Column(columnDefinition = "MEDIUMTEXT", nullable = false)
|
||||
private String text;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime sentAt;
|
||||
|
||||
@Column
|
||||
private LocalDateTime readAt;
|
||||
|
||||
public UUID getMessageId() { return messageId; }
|
||||
public void setMessageId(UUID messageId) { this.messageId = messageId; }
|
||||
|
||||
public UUID getSenderId() { return senderId; }
|
||||
public void setSenderId(UUID senderId) { this.senderId = senderId; }
|
||||
|
||||
public UUID getReceiverId() { return receiverId; }
|
||||
public void setReceiverId(UUID receiverId) { this.receiverId = receiverId; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public LocalDateTime getSentAt() { return sentAt; }
|
||||
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
|
||||
|
||||
public LocalDateTime getReadAt() { return readAt; }
|
||||
public void setReadAt(LocalDateTime readAt) { this.readAt = readAt; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "profile_image")
|
||||
public class ProfileImageEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID imageId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(columnDefinition = "MEDIUMTEXT", nullable = false)
|
||||
private String imageData;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime uploadedAt;
|
||||
|
||||
public UUID getImageId() { return imageId; }
|
||||
public void setImageId(UUID imageId) { this.imageId = imageId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public String getImageData() { return imageData; }
|
||||
public void setImageData(String imageData) { this.imageData = imageData; }
|
||||
|
||||
public LocalDateTime getUploadedAt() { return uploadedAt; }
|
||||
public void setUploadedAt(LocalDateTime uploadedAt) { this.uploadedAt = uploadedAt; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "profile_image_like")
|
||||
public class ProfileImageLikeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID likeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID imageId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime likedAt;
|
||||
|
||||
public UUID getLikeId() { return likeId; }
|
||||
public void setLikeId(UUID likeId) { this.likeId = likeId; }
|
||||
|
||||
public UUID getImageId() { return imageId; }
|
||||
public void setImageId(UUID imageId) { this.imageId = imageId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public LocalDateTime getLikedAt() { return likedAt; }
|
||||
public void setLikedAt(LocalDateTime likedAt) { this.likedAt = likedAt; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.FriendshipEntity;
|
||||
import de.oaa.xxx.social.entity.FriendshipEntity.Status;
|
||||
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.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FriendshipRepository extends JpaRepository<FriendshipEntity, UUID> {
|
||||
|
||||
List<FriendshipEntity> findByReceiverIdAndStatus(UUID receiverId, Status status);
|
||||
|
||||
long countByReceiverIdAndStatus(UUID receiverId, Status status);
|
||||
|
||||
@Query("SELECT f FROM FriendshipEntity f WHERE (f.senderId = :userId OR f.receiverId = :userId) AND f.status = :status")
|
||||
List<FriendshipEntity> findFriends(@Param("userId") UUID userId, @Param("status") Status status);
|
||||
|
||||
@Query("SELECT f FROM FriendshipEntity f WHERE (f.senderId = :userA AND f.receiverId = :userB) OR (f.senderId = :userB AND f.receiverId = :userA)")
|
||||
Optional<FriendshipEntity> findExisting(@Param("userA") UUID userA, @Param("userB") UUID userB);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.MessageEntity;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface MessageRepository extends JpaRepository<MessageEntity, UUID> {
|
||||
|
||||
@Query("SELECT m FROM MessageEntity m WHERE (m.senderId = :userA AND m.receiverId = :userB) OR (m.senderId = :userB AND m.receiverId = :userA) ORDER BY m.sentAt DESC")
|
||||
List<MessageEntity> findConversation(@Param("userA") UUID userA, @Param("userB") UUID userB, Pageable pageable);
|
||||
|
||||
@Query("SELECT m FROM MessageEntity m WHERE m.senderId = :userId OR m.receiverId = :userId ORDER BY m.sentAt DESC")
|
||||
List<MessageEntity> findAllByUser(@Param("userId") UUID userId);
|
||||
|
||||
@Query("SELECT COUNT(m) FROM MessageEntity m WHERE m.receiverId = :userId AND m.readAt IS NULL")
|
||||
long countUnread(@Param("userId") UUID userId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE MessageEntity m SET m.readAt = :now WHERE m.senderId = :partnerId AND m.receiverId = :userId AND m.readAt IS NULL")
|
||||
void markAsRead(@Param("userId") UUID userId, @Param("partnerId") UUID partnerId, @Param("now") LocalDateTime now);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.ProfileImageLikeEntity;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ProfileImageLikeRepository extends JpaRepository<ProfileImageLikeEntity, UUID> {
|
||||
|
||||
Optional<ProfileImageLikeEntity> findByImageIdAndUserId(UUID imageId, UUID userId);
|
||||
|
||||
long countByImageId(UUID imageId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("DELETE FROM ProfileImageLikeEntity l WHERE l.imageId = :imageId")
|
||||
void deleteByImageId(@Param("imageId") UUID imageId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("DELETE FROM ProfileImageLikeEntity l WHERE l.userId = :userId")
|
||||
void deleteByUserId(@Param("userId") UUID userId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.ProfileImageEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ProfileImageRepository extends JpaRepository<ProfileImageEntity, UUID> {
|
||||
|
||||
List<ProfileImageEntity> findByUserIdOrderByUploadedAtDesc(UUID userId);
|
||||
|
||||
long countByUserId(UUID userId);
|
||||
}
|
||||
@@ -62,6 +62,18 @@ public class LoginController {
|
||||
.orElse(ResponseEntity.status(401).build());
|
||||
}
|
||||
|
||||
@GetMapping("/logout")
|
||||
public void logout(HttpServletResponse response) throws java.io.IOException {
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", "")
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public ResponseEntity<User> get(@PathVariable UUID userId) {
|
||||
return userRepository.findById(userId)
|
||||
|
||||
@@ -8,6 +8,7 @@ public class User {
|
||||
private String name;
|
||||
private String email;
|
||||
private String password;
|
||||
private String profilePicture;
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
@@ -20,4 +21,12 @@ public class User {
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getProfilePicture() { return profilePicture; }
|
||||
public void setProfilePicture(String profilePicture) { this.profilePicture = profilePicture; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User[userId=" + userId + ", name=" + name + ", email=" + email + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.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.RestController;
|
||||
|
||||
import java.util.UUID;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.FavoritRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
|
||||
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.passwordreset.PasswordResetRepository;
|
||||
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 jakarta.transaction.Transactional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@@ -17,9 +42,150 @@ public class UserController {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final RegistrationRepository registrationRepository;
|
||||
private final AufgabenGruppeRepository aufgabenGruppeRepository;
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final StrafeRepository strafeRepository;
|
||||
private final SperreRepository sperreRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
private final FavoritRepository favoritRepository;
|
||||
private final GruppenAboRepository gruppenAboRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
private final AktiveSperreRepository aktiveSperreRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
private final EmailChangeRepository emailChangeRepository;
|
||||
private final PasswordResetRepository passwordResetRepository;
|
||||
private final ProfileImageRepository profileImageRepository;
|
||||
private final ProfileImageLikeRepository profileImageLikeRepository;
|
||||
|
||||
public UserController(UserRepository userRepository) {
|
||||
public UserController(UserRepository userRepository,
|
||||
RegistrationRepository registrationRepository,
|
||||
AufgabenGruppeRepository aufgabenGruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
StrafeRepository strafeRepository,
|
||||
SperreRepository sperreRepository,
|
||||
ToyRepository toyRepository,
|
||||
FavoritRepository favoritRepository,
|
||||
GruppenAboRepository gruppenAboRepository,
|
||||
SessionRepository sessionRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository,
|
||||
MitspielerRepository mitspielerRepository,
|
||||
EmailChangeRepository emailChangeRepository,
|
||||
PasswordResetRepository passwordResetRepository,
|
||||
ProfileImageRepository profileImageRepository,
|
||||
ProfileImageLikeRepository profileImageLikeRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
this.favoritRepository = favoritRepository;
|
||||
this.gruppenAboRepository = gruppenAboRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.emailChangeRepository = emailChangeRepository;
|
||||
this.passwordResetRepository = passwordResetRepository;
|
||||
this.profileImageRepository = profileImageRepository;
|
||||
this.profileImageLikeRepository = profileImageLikeRepository;
|
||||
}
|
||||
|
||||
record ProfilePictureRequest(String picture, String pictureHq) {}
|
||||
record NameChangeRequest(String name) {}
|
||||
|
||||
@PutMapping("/me/picture")
|
||||
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName());
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
user.get().setProfilePicture(request.picture());
|
||||
user.get().setProfilePictureHq(request.pictureHq());
|
||||
userRepository.save(user.get());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/name")
|
||||
public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) {
|
||||
String newName = request.name();
|
||||
if (userRepository.findByName(newName).isPresent()
|
||||
|| registrationRepository.findByName(newName).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
var user = userRepository.findByEmail(principal.getName());
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
user.get().setName(newName);
|
||||
userRepository.save(user.get());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/me")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> deleteAccount(Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
UUID userId = user.getUserId();
|
||||
String email = user.getEmail();
|
||||
|
||||
LOGGER.info("Lösche Konto für User {}", email);
|
||||
|
||||
// 1. Delete user's AufgabenGruppen and all their content
|
||||
var gruppen = aufgabenGruppeRepository.findByUserId(userId);
|
||||
if (!gruppen.isEmpty()) {
|
||||
aufgabeRepository.deleteAll(aufgabeRepository.findByAufgabenGruppeIn(gruppen));
|
||||
strafeRepository.deleteAll(strafeRepository.findByAufgabenGruppeIn(gruppen));
|
||||
sperreRepository.deleteAll(sperreRepository.findByAufgabenGruppeIn(gruppen));
|
||||
for (var gruppe : gruppen) {
|
||||
gruppenAboRepository.deleteByAufgabenGruppe(gruppe);
|
||||
favoritRepository.deleteByAufgabenGruppeId(gruppe.getGruppenId());
|
||||
}
|
||||
aufgabenGruppeRepository.deleteAll(gruppen);
|
||||
}
|
||||
|
||||
// 2. Delete user's Toys (join table refs already cleared above)
|
||||
toyRepository.deleteAll(toyRepository.findByUserId(userId));
|
||||
|
||||
// 3. Delete user's own Favoriten and Gruppenabos (to other groups)
|
||||
favoritRepository.deleteAll(favoritRepository.findByUserId(userId));
|
||||
gruppenAboRepository.deleteAll(gruppenAboRepository.findByUserId(userId));
|
||||
|
||||
// 4. Delete Session with Mitspieler and AktiveSperre
|
||||
var sessionOpt = sessionRepository.findByUserId(userId);
|
||||
if (sessionOpt.isPresent()) {
|
||||
var session = sessionOpt.get();
|
||||
List<AktiveSperreEntity> sperren = session.getAktiveSperren();
|
||||
List<MitspielerEntity> mitspieler = session.getMitspieler();
|
||||
aktiveSperreRepository.deleteAll(sperren);
|
||||
mitspielerRepository.deleteAll(mitspieler);
|
||||
sessionRepository.delete(session);
|
||||
}
|
||||
|
||||
// 5. Delete pending tokens
|
||||
emailChangeRepository.findByUserEmail(email).ifPresent(emailChangeRepository::delete);
|
||||
passwordResetRepository.findByEmail(email).ifPresent(passwordResetRepository::delete);
|
||||
|
||||
// 5b. Delete profile images and likes
|
||||
var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId);
|
||||
for (var img : profileImages) {
|
||||
profileImageLikeRepository.deleteByImageId(img.getImageId());
|
||||
}
|
||||
profileImageRepository.deleteAll(profileImages);
|
||||
profileImageLikeRepository.deleteByUserId(userId);
|
||||
|
||||
// 6. Delete user
|
||||
userRepository.delete(user);
|
||||
|
||||
// Clear JWT cookie
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", "")
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, cookie.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
||||
@@ -21,6 +21,12 @@ public class UserEntity {
|
||||
@Column
|
||||
private String password;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String profilePicture;
|
||||
|
||||
@Column(columnDefinition = "MEDIUMTEXT")
|
||||
private String profilePictureHq;
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
@@ -33,11 +39,23 @@ public class UserEntity {
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getProfilePicture() { return profilePicture; }
|
||||
public void setProfilePicture(String profilePicture) { this.profilePicture = profilePicture; }
|
||||
|
||||
public String getProfilePictureHq() { return profilePictureHq; }
|
||||
public void setProfilePictureHq(String profilePictureHq) { this.profilePictureHq = profilePictureHq; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserEntity[userId=" + userId + ", name=" + name + ", email=" + email + "]";
|
||||
}
|
||||
|
||||
public User toUser() {
|
||||
User user = new User();
|
||||
user.setEmail(email);
|
||||
user.setName(name);
|
||||
user.setUserId(userId);
|
||||
user.setProfilePicture(profilePicture);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package de.oaa.xxx.user;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
|
||||
|
||||
Optional<UserEntity> findByEmailAndPassword(String email, String password);
|
||||
|
||||
Optional<UserEntity> findByEmail(String email);
|
||||
Optional<UserEntity> findByName(String name);
|
||||
List<UserEntity> findByNameContainingIgnoreCase(String name);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ app.theme.color-text=#eeeeee
|
||||
app.theme.color-muted=#888888
|
||||
app.theme.color-success=#2ecc71
|
||||
|
||||
# Logging
|
||||
logging.level.de.oaa.xxx=DEBUG
|
||||
|
||||
# Server
|
||||
server.port=8080
|
||||
server.servlet.context-path=/
|
||||
|
||||
@@ -7,70 +7,6 @@
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
body { display: block; min-height: 100vh; }
|
||||
.layout { display: flex; min-height: 100vh; }
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.sidebar {
|
||||
width: 240px; background: var(--color-card);
|
||||
border-right: 1px solid var(--color-secondary);
|
||||
display: flex; flex-direction: column;
|
||||
position: fixed; top: 0; left: 0;
|
||||
height: 100vh; overflow-y: auto;
|
||||
z-index: 100; transition: transform 0.25s ease;
|
||||
}
|
||||
.sidebar-brand {
|
||||
color: var(--color-primary); font-size: 1.1rem; font-weight: 700;
|
||||
padding: 1.25rem 1.25rem 1rem;
|
||||
border-bottom: 1px solid var(--color-secondary); flex-shrink: 0;
|
||||
}
|
||||
.sidebar ul { list-style: none; padding: 0.5rem 0; }
|
||||
.sidebar ul li a {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.7rem 1.25rem; color: var(--color-text);
|
||||
text-decoration: none; font-size: 0.95rem;
|
||||
border-left: 3px solid transparent;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.sidebar ul li a:hover, .sidebar ul li a.active {
|
||||
background: var(--color-secondary); color: var(--color-primary);
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
|
||||
|
||||
/* ── Main ── */
|
||||
.main { margin-left: 240px; flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
|
||||
|
||||
/* ── Topbar ── */
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
padding: 0.9rem 1.5rem; background: var(--color-card);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.topbar h1 { font-size: 1.2rem; font-weight: 600; }
|
||||
.burger {
|
||||
display: none; background: none; border: none; cursor: pointer;
|
||||
color: var(--color-text); padding: 0.25rem 0.4rem;
|
||||
border-radius: 4px; transition: background 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.burger:hover { background: var(--color-secondary); }
|
||||
.burger-icon { display: flex; flex-direction: column; gap: 5px; width: 22px; }
|
||||
.burger-icon span {
|
||||
display: block; height: 2px; background: var(--color-text);
|
||||
border-radius: 2px; transition: transform 0.25s, opacity 0.25s;
|
||||
}
|
||||
.burger.open .burger-icon span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.burger.open .burger-icon span:nth-child(2) { opacity: 0; }
|
||||
.burger.open .burger-icon span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
|
||||
.content { padding: 2rem 1.5rem; flex: 1; }
|
||||
|
||||
.overlay {
|
||||
display: none; position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.55); z-index: 90;
|
||||
}
|
||||
|
||||
/* ── Section ── */
|
||||
.section + .section { margin-top: 2.5rem; }
|
||||
.section-header {
|
||||
@@ -272,6 +208,25 @@
|
||||
.modal-actions .btn-save:disabled { opacity: 0.5; cursor: default; }
|
||||
.modal-error { color: var(--color-primary); font-size: 0.82rem; margin-top: 0.75rem; display: none; }
|
||||
|
||||
/* ── Placeholder-Hint ── */
|
||||
.label-with-hint { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.btn-hint {
|
||||
background: none; border: 1px solid rgba(136,136,136,0.4); border-radius: 50%;
|
||||
color: var(--color-muted); font-size: 0.7rem; font-style: italic; font-weight: 700;
|
||||
width: 16px; height: 16px; line-height: 1; padding: 0;
|
||||
cursor: pointer; flex-shrink: 0; transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-hint:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.placeholder-hint {
|
||||
background: rgba(255,255,255,0.04); border: 1px solid rgba(136,136,136,0.25);
|
||||
border-radius: 6px; padding: 0.5rem 0.7rem; margin-bottom: 0.3rem;
|
||||
font-size: 0.78rem; color: var(--color-muted); line-height: 1.6;
|
||||
}
|
||||
.placeholder-hint code {
|
||||
background: rgba(233,69,96,0.12); color: var(--color-primary);
|
||||
border-radius: 3px; padding: 0.05rem 0.3rem; font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Item-Add-Modal extra ── */
|
||||
.modal-two-col { display: flex; gap: 0.75rem; }
|
||||
.modal-two-col > * { flex: 1; }
|
||||
@@ -326,8 +281,7 @@
|
||||
}
|
||||
.btn-toy-add:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
|
||||
/* ── Toy-Suchmodal ── */
|
||||
#toySearchModal .modal-box { max-width: 420px; }
|
||||
/* ── Toy-Suche (inline) ── */
|
||||
.toy-search-input {
|
||||
width: 100%; box-sizing: border-box; padding: 0.45rem 0.7rem;
|
||||
background: var(--color-secondary); border: 1px solid rgba(136,136,136,0.3);
|
||||
@@ -368,19 +322,9 @@
|
||||
flex-shrink: 0; margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { transform: translateX(-100%); }
|
||||
.sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,0.5); }
|
||||
.main { margin-left: 0; }
|
||||
.burger { display: flex; }
|
||||
.overlay.visible { display: block; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="overlay" id="overlay"></div>
|
||||
<body class="app">
|
||||
|
||||
<!-- Gruppe-Modal -->
|
||||
<div class="modal-backdrop" id="gruppeModal">
|
||||
@@ -439,9 +383,38 @@
|
||||
<label for="iKurzText">Kurzbezeichnung *</label>
|
||||
<input type="text" id="iKurzText" maxlength="200" placeholder="Kurzer Name">
|
||||
|
||||
<label for="iText">Beschreibung *</label>
|
||||
<label class="label-with-hint">
|
||||
<span>Beschreibung *</span>
|
||||
<button type="button" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
|
||||
</label>
|
||||
<div id="iPlaceholderHint" style="display:none;">
|
||||
<div class="placeholder-hint">
|
||||
In Texten können Platzhalter verwendet werden:<br>
|
||||
<code>{AKTIV}</code> – Name des aktiven Parts<br>
|
||||
<code>{PASSIV}</code> – Name des passiven Parts
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="iText" rows="4" maxlength="4000" placeholder="Ausführliche Beschreibung…"></textarea>
|
||||
|
||||
<!-- Finisher: Geschlecht -->
|
||||
<div id="iGeschlechtRow">
|
||||
<label>Geschlecht der Person die kommt *</label>
|
||||
<div style="display:flex; gap:1.5rem; margin-top:0.5rem;" id="iGeschlecht">
|
||||
<label style="display:flex; align-items:center; gap:0.4rem; font-size:0.85rem; cursor:pointer;">
|
||||
<input type="radio" name="iGeschlechtRadio" value="WEIBLICH" style="accent-color:var(--color-primary);">
|
||||
Weiblich
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:0.4rem; font-size:0.85rem; cursor:pointer;">
|
||||
<input type="radio" name="iGeschlechtRadio" value="DIVERS" style="accent-color:var(--color-primary);">
|
||||
Divers
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:0.4rem; font-size:0.85rem; cursor:pointer;">
|
||||
<input type="radio" name="iGeschlechtRadio" value="MAENNLICH" style="accent-color:var(--color-primary);">
|
||||
Männlich
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aufgabe / Strafe: Level + Sekunden -->
|
||||
<div id="iLevelRow">
|
||||
<label for="iLevel">Level *</label>
|
||||
@@ -481,6 +454,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finisher: Werkzeuge (eigene Labels, kein Umschnalldildo bei aktiv) -->
|
||||
<div id="iWerkzeugFinisherAktivRow">
|
||||
<label>Benötigt (Person die kommt)</label>
|
||||
<div class="werkzeug-checks" id="iWerkzeugFinisherAktiv">
|
||||
<label class="werkzeug-check"><span>Mund</span><input type="checkbox" value="MUND"></label>
|
||||
<label class="werkzeug-check"><span>Vagina</span><input type="checkbox" value="VAGINA"></label>
|
||||
<label class="werkzeug-check"><span>Penis</span><input type="checkbox" value="PENIS"></label>
|
||||
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="iWerkzeugFinisherPassivRow">
|
||||
<label>Benötigt (Person die zum Kommen bringt)</label>
|
||||
<div class="werkzeug-checks" id="iWerkzeugFinisherPassiv">
|
||||
<label class="werkzeug-check"><span>Mund</span><input type="checkbox" value="MUND"></label>
|
||||
<label class="werkzeug-check"><span>Vagina</span><input type="checkbox" value="VAGINA"></label>
|
||||
<label class="werkzeug-check"><span>Penis</span><input type="checkbox" value="PENIS"></label>
|
||||
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
|
||||
<label class="werkzeug-check"><span>Umschnall-Dildo</span><input type="checkbox" value="UMSCHNALLDILDO"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: Minuten + SperreFuer + releaseText -->
|
||||
<div id="iMinutenRow">
|
||||
<label>Dauer (Minuten) *</label>
|
||||
@@ -513,6 +507,11 @@
|
||||
<label>Benötigte Toys (optional)</label>
|
||||
<div class="selected-toys-row" id="iSelectedToys"></div>
|
||||
<button class="btn-toy-add" type="button" id="iToyAddBtn">+ Toy hinzufügen</button>
|
||||
<div id="iToySearchArea" style="display:none; margin-top:0.5rem;">
|
||||
<input class="toy-search-input" type="text" id="toySearchInput" placeholder="Name filtern…" autocomplete="off">
|
||||
<div class="toy-search-results" id="toySearchResults"></div>
|
||||
<div id="toySearchEmpty" style="font-size:0.82rem; color:var(--color-muted); display:none; margin-top:0.4rem;">Keine Toys gefunden.</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-error" id="itemModalError"></div>
|
||||
<div class="modal-actions">
|
||||
@@ -522,46 +521,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Toy-Suchmodal ── -->
|
||||
<div class="modal-overlay" id="toySearchModal">
|
||||
<div class="modal-box" style="max-width:420px;">
|
||||
<h2 class="modal-title">Toy auswählen</h2>
|
||||
<input class="toy-search-input" type="text" id="toySearchInput" placeholder="Name filtern…" autocomplete="off">
|
||||
<div class="toy-search-results" id="toySearchResults"></div>
|
||||
<div id="toySearchEmpty" style="font-size:0.82rem; color:var(--color-muted); display:none; margin-top:0.5rem;">Keine Toys gefunden.</div>
|
||||
<div class="modal-actions" style="margin-top:1rem;">
|
||||
<button class="btn-save" id="toySearchDoneBtn">Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-brand">XXX The Game</div>
|
||||
<ul>
|
||||
<li><a href="/userhome.html"><span class="icon">⊞</span> Dashboard</a></li>
|
||||
<li><a href="#"><span class="icon">▶</span> Meine Session</a></li>
|
||||
<li><a href="/aufgaben.html" class="active"><span class="icon">✓</span> Aufgaben</a></li>
|
||||
<li><a href="/entdecken.html"><span class="icon">⊙</span> Entdecken</a></li>
|
||||
<li><a href="#"><span class="icon">⚡</span> Strafen</a></li>
|
||||
<li><a href="/toys.html"><span class="icon">◈</span> Toys</a></li>
|
||||
<li><a href="#"><span class="icon">♥</span> Favoriten</a></li>
|
||||
<li><a href="#"><span class="icon">☰</span> Rangliste</a></li>
|
||||
<li><a href="#"><span class="icon">✉</span> Nachrichten</a></li>
|
||||
<li><a href="#"><span class="icon">⚙</span> Einstellungen</a></li>
|
||||
<li><a href="#" id="logoutLink"><span class="icon">⏏</span> Abmelden</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<header class="topbar">
|
||||
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
||||
<span class="burger-icon"><span></span><span></span><span></span></span>
|
||||
</button>
|
||||
<h1>Aufgaben</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<!-- Meine Aufgabengruppen -->
|
||||
<div class="section">
|
||||
@@ -621,7 +583,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -708,13 +669,15 @@
|
||||
}
|
||||
list.innerHTML = gruppen.map(g => {
|
||||
_gruppeData[g.gruppenId] = g;
|
||||
const aufgabenCount = (g.aufgaben || []).length;
|
||||
const strafeCount = (g.strafen || []).length;
|
||||
const sperreCount = (g.sperren || []).length;
|
||||
const aufgabenCount = (g.aufgaben || []).length;
|
||||
const strafeCount = (g.strafen || []).length;
|
||||
const sperreCount = (g.sperren || []).length;
|
||||
const finisherCount = (g.finisher || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : '',
|
||||
finisherCount ? `${finisherCount} Finisher` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
const badges = [];
|
||||
@@ -738,9 +701,10 @@
|
||||
</div>
|
||||
<div class="gruppe-body" id="body-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type)}
|
||||
${renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type)}
|
||||
${renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
|
||||
${renderSubSection('Finisher', sortByName(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -864,6 +828,33 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
|
||||
|
||||
function renderFinisher(f, type, gruppenId) {
|
||||
_itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId };
|
||||
const badges = [];
|
||||
if (f.geschlecht) badges.push(`<span class="badge badge-neutral">${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (f.text) detailRows.push(`<div class="item-detail-text">${esc(f.text)}</div>`);
|
||||
if (f.benoetigtAktiv && f.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(f.benoetigtAktiv)}</div>`);
|
||||
if (f.benoetigtPassiv && f.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(f.benoetigtPassiv)}</div>`);
|
||||
if (f.benoetigteToys && f.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(f.benoetigteToys)}</div>`);
|
||||
const actionBtns = type === 'user' ? `
|
||||
<div class="item-action-btns">
|
||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(f.finisherId)}',event)">✎ Bearbeiten</button>
|
||||
<button class="btn-item-delete" onclick="deleteItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||
</div>` : '';
|
||||
|
||||
return `<div class="item" id="item-${esc(f.finisherId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(f.finisherId)}')">
|
||||
<span class="item-text">${esc(f.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${(detailRows.length || actionBtns) ? `<div class="item-detail">${detailRows.join('')}${actionBtns}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Item toggle (detail panel) ──
|
||||
let openItemId = null;
|
||||
|
||||
@@ -883,8 +874,8 @@
|
||||
}
|
||||
|
||||
// ── Item löschen ──
|
||||
const ITEM_DELETE_URL = { aufgabe: '/aufgabe', strafe: '/strafe', zeitstrafe: '/sperre' };
|
||||
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId' };
|
||||
const ITEM_DELETE_URL = { aufgabe: '/aufgabe', strafe: '/strafe', zeitstrafe: '/sperre', finisher: '/finisher' };
|
||||
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
|
||||
|
||||
function deleteItem(kind, itemId, gruppenId, event) {
|
||||
event.stopPropagation();
|
||||
@@ -1225,29 +1216,34 @@
|
||||
if (chip) chip.classList.remove('selected');
|
||||
}
|
||||
|
||||
// Toy-Suchmodal
|
||||
const toySearchModal = document.getElementById('toySearchModal');
|
||||
|
||||
document.getElementById('iToyAddBtn').addEventListener('click', openToySearch);
|
||||
document.getElementById('toySearchDoneBtn').addEventListener('click', closeToySearch);
|
||||
toySearchModal.addEventListener('click', e => { if (e.target === toySearchModal) closeToySearch(); });
|
||||
// Toy-Suche (inline)
|
||||
document.getElementById('iToyAddBtn').addEventListener('click', toggleToySearch);
|
||||
document.getElementById('toySearchInput').addEventListener('input', renderToySearchResults);
|
||||
|
||||
function openToySearch() {
|
||||
document.getElementById('toySearchInput').value = '';
|
||||
document.getElementById('toySearchResults').innerHTML = '';
|
||||
document.getElementById('toySearchEmpty').style.display = 'none';
|
||||
toySearchModal.classList.add('open');
|
||||
_loadAvailableToys()
|
||||
.then(() => renderToySearchResults())
|
||||
.catch(() => {
|
||||
document.getElementById('toySearchResults').innerHTML =
|
||||
'<span style="font-size:0.82rem;color:var(--color-muted)">Fehler beim Laden.</span>';
|
||||
});
|
||||
document.getElementById('toySearchInput').focus();
|
||||
function toggleToySearch() {
|
||||
const area = document.getElementById('iToySearchArea');
|
||||
if (area.style.display === 'none') {
|
||||
area.style.display = 'block';
|
||||
document.getElementById('toySearchInput').value = '';
|
||||
document.getElementById('toySearchResults').innerHTML = '';
|
||||
document.getElementById('toySearchEmpty').style.display = 'none';
|
||||
document.getElementById('iToyAddBtn').textContent = '▲ Suche schließen';
|
||||
_loadAvailableToys()
|
||||
.then(() => renderToySearchResults())
|
||||
.catch(() => {
|
||||
document.getElementById('toySearchResults').innerHTML =
|
||||
'<span style="font-size:0.82rem;color:var(--color-muted)">Fehler beim Laden.</span>';
|
||||
});
|
||||
document.getElementById('toySearchInput').focus();
|
||||
} else {
|
||||
closeToySearch();
|
||||
}
|
||||
}
|
||||
|
||||
function closeToySearch() { toySearchModal.classList.remove('open'); }
|
||||
function closeToySearch() {
|
||||
document.getElementById('iToySearchArea').style.display = 'none';
|
||||
document.getElementById('iToyAddBtn').textContent = '+ Toy hinzufügen';
|
||||
}
|
||||
|
||||
function renderToySearchResults() {
|
||||
const query = document.getElementById('toySearchInput').value.trim().toLowerCase();
|
||||
@@ -1297,17 +1293,29 @@
|
||||
let currentItemKind = null; // 'aufgabe' | 'strafe' | 'zeitstrafe'
|
||||
let currentItemEditId = null; // null = neu, sonst ID des zu bearbeitenden Items
|
||||
|
||||
const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen' };
|
||||
const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten' };
|
||||
const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen', finisher: 'Finisher hinzufügen' };
|
||||
const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten', finisher: 'Finisher bearbeiten' };
|
||||
|
||||
function _setupItemModal(kind) {
|
||||
const isZeit = kind === 'zeitstrafe';
|
||||
document.getElementById('iLevelRow').style.display = isZeit ? 'none' : 'block';
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = isZeit ? 'none' : 'block';
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = isZeit ? 'none' : 'block';
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
const isZeit = kind === 'zeitstrafe';
|
||||
const isFinisher = kind === 'finisher';
|
||||
document.querySelector('#iPlaceholderHint .placeholder-hint').innerHTML =
|
||||
isFinisher
|
||||
? 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
'<code>{AKTIV}</code> – Name der Person die kommt<br>' +
|
||||
'<code>{PASSIV}</code> – Name der Person die zum Kommen bringt'
|
||||
: 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
'<code>{AKTIV}</code> – Name des aktiven Parts<br>' +
|
||||
'<code>{PASSIV}</code> – Name des passiven Parts';
|
||||
document.getElementById('iGeschlechtRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherAktivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function _resetItemFields() {
|
||||
@@ -1319,9 +1327,12 @@
|
||||
document.getElementById('iMinVon').value = '';
|
||||
document.getElementById('iMinBis').value = '';
|
||||
document.getElementById('iReleaseText').value = '';
|
||||
document.querySelectorAll('#iWerkzeugAktiv input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iWerkzeugPassiv input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iWerkzeugAktiv input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iWerkzeugPassiv input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iWerkzeugFinisherAktiv input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false);
|
||||
_selectedToys = [];
|
||||
renderSelectedToys();
|
||||
document.getElementById('itemModalError').style.display = 'none';
|
||||
@@ -1359,6 +1370,13 @@
|
||||
document.getElementById('iSekBis').value = d.sekundenBis != null ? d.sekundenBis : '';
|
||||
(d.benoetigtAktiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugAktiv input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||
(d.benoetigtPassiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugPassiv input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||
} else if (d._kind === 'finisher') {
|
||||
(d.benoetigtAktiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugFinisherAktiv input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||
(d.benoetigtPassiv || []).forEach(w => { const cb = document.querySelector(`#iWerkzeugFinisherPassiv input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||
if (d.geschlecht) {
|
||||
const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`);
|
||||
if (rb) rb.checked = true;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : '';
|
||||
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
|
||||
@@ -1372,7 +1390,11 @@
|
||||
document.getElementById('iKurzText').focus();
|
||||
}
|
||||
|
||||
function closeItemModal() { itemModal.classList.remove('open'); }
|
||||
function closeItemModal() { itemModal.classList.remove('open'); closeToySearch(); document.getElementById('iPlaceholderHint').style.display = 'none'; }
|
||||
function togglePlaceholderHint() {
|
||||
const el = document.getElementById('iPlaceholderHint');
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
document.getElementById('itemCancelBtn').addEventListener('click', closeItemModal);
|
||||
itemModal.addEventListener('click', e => { if (e.target === itemModal) closeItemModal(); });
|
||||
@@ -1408,6 +1430,19 @@
|
||||
url = isEdit ? `${base}/${currentItemEditId}` : base;
|
||||
method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
} else if (kind === 'finisher') {
|
||||
const geschlecht = document.querySelector('#iGeschlecht input:checked')?.value;
|
||||
if (!geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
payload = {
|
||||
kurzText, text, geschlecht,
|
||||
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
||||
benoetigtAktiv: checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? `/finisher/${currentItemEditId}` : '/finisher';
|
||||
method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
} else {
|
||||
const minVon = document.getElementById('iMinVon').value.trim();
|
||||
if (!minVon) { showItemError('Bitte eine Mindestdauer in Minuten angeben.'); return; }
|
||||
@@ -1422,7 +1457,7 @@
|
||||
minutenBis: minBis ? parseInt(minBis, 10) : null,
|
||||
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
||||
sperreFuer,
|
||||
benoetigteToys: Array.from(_selectedToyIds).map(id => ({ toyId: id }))
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre';
|
||||
method = isEdit ? 'PUT' : 'POST';
|
||||
@@ -1558,27 +1593,12 @@
|
||||
// ── ESC schließt alle Modals ──
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (toySearchModal.classList.contains('open')) { closeToySearch(); return; }
|
||||
if (publishModal.classList.contains('open')) { closePublishModal(); return; }
|
||||
if (gruppeModal.classList.contains('open')) { closeGruppeModal(); return; }
|
||||
if (itemModal.classList.contains('open')) { closeItemModal(); return; }
|
||||
});
|
||||
|
||||
// ── Burger menu ──
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const burgerBtn = document.getElementById('burgerBtn');
|
||||
const overlay = document.getElementById('overlay');
|
||||
function openMenu() {
|
||||
sidebar.classList.add('open'); overlay.classList.add('visible');
|
||||
burgerBtn.classList.add('open'); burgerBtn.setAttribute('aria-label', 'Menü schließen');
|
||||
}
|
||||
function closeMenu() {
|
||||
sidebar.classList.remove('open'); overlay.classList.remove('visible');
|
||||
burgerBtn.classList.remove('open'); burgerBtn.setAttribute('aria-label', 'Menü öffnen');
|
||||
}
|
||||
burgerBtn.addEventListener('click', () => sidebar.classList.contains('open') ? closeMenu() : openMenu());
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
sidebar.querySelectorAll('a').forEach(l => l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); }));
|
||||
</script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
443
xxxthegame/src/main/resources/static/benutzer.html
Normal file
@@ -0,0 +1,443 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Profil – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.profile-view {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-pic-large {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
border: 3px solid var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: var(--color-muted);
|
||||
margin: 0 auto 1.25rem;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-pic-large img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.75rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.profile-actions button,
|
||||
.profile-actions a.btn {
|
||||
margin-top: 0;
|
||||
width: auto;
|
||||
padding: 0.65rem 1.5rem;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
padding: 0.65rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-outline:hover {
|
||||
background: #3d0f1a;
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.profile-pic-large img { cursor: zoom-in; }
|
||||
|
||||
.lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.85);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.lightbox.open { display: flex; }
|
||||
.lightbox img {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.7);
|
||||
}
|
||||
.lightbox-close {
|
||||
position: fixed !important;
|
||||
top: 1rem !important;
|
||||
right: 1rem !important;
|
||||
background: rgba(0,0,0,0.55) !important;
|
||||
border: 1px solid rgba(255,255,255,0.2) !important;
|
||||
color: #fff !important;
|
||||
font-size: 1.1rem !important;
|
||||
width: 2.2rem !important;
|
||||
height: 2.2rem !important;
|
||||
border-radius: 50% !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
line-height: 1 !important;
|
||||
z-index: 501;
|
||||
}
|
||||
.lightbox-close:hover { background: rgba(0,0,0,0.85) !important; }
|
||||
|
||||
/* ── Profile Gallery ── */
|
||||
.profile-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.4rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.gallery-thumb {
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
.gallery-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.gallery-thumb:hover img { transform: scale(1.04); }
|
||||
|
||||
/* ── Lightbox gallery extensions ── */
|
||||
.lightbox-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0,0,0,0.45);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
width: 2.8rem;
|
||||
height: 4rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: background 0.15s;
|
||||
z-index: 502;
|
||||
}
|
||||
|
||||
.lightbox-nav:hover { background: rgba(0,0,0,0.8); }
|
||||
#lbPrev { left: 1rem; }
|
||||
#lbNext { right: 1rem; }
|
||||
|
||||
.lightbox-footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
z-index: 502;
|
||||
}
|
||||
|
||||
#lbCounter {
|
||||
color: rgba(255,255,255,0.75);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lightbox-like {
|
||||
background: rgba(0,0,0,0.45);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 0.3rem 0.9rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.lightbox-like:hover { background: rgba(180,30,30,0.7); }
|
||||
|
||||
.lightbox.gallery-mode .lightbox-nav,
|
||||
.lightbox.gallery-mode .lightbox-footer { display: flex; }
|
||||
|
||||
#lbPrev:disabled, #lbNext:disabled { opacity: 0.25; cursor: default; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<p id="loadingHint" style="color:var(--color-muted);">Wird geladen…</p>
|
||||
|
||||
<div class="profile-view" id="profileView" style="display:none;">
|
||||
<div class="profile-pic-large" id="profilePic">◉</div>
|
||||
<div class="profile-name" id="profileName"></div>
|
||||
<div class="profile-actions" id="profileActions"></div>
|
||||
<div class="profile-gallery" id="profileGallery"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
|
||||
<img id="lightboxImg" src="" alt="" onclick="event.stopPropagation()">
|
||||
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen">✕</button>
|
||||
<button class="lightbox-nav" id="lbPrev" onclick="event.stopPropagation(); galleryPrev()">‹</button>
|
||||
<button class="lightbox-nav" id="lbNext" onclick="event.stopPropagation(); galleryNext()">›</button>
|
||||
<div class="lightbox-footer" id="lbFooter" onclick="event.stopPropagation()">
|
||||
<span id="lbCounter"></span>
|
||||
<button class="lightbox-like" id="lbLike" onclick="toggleLike()">♡ <span id="lbLikeCount"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const targetUserId = params.get('userId');
|
||||
|
||||
// Kein userId-Parameter → eigenes Profil laden
|
||||
if (!targetUserId) {
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(user => {
|
||||
if (user) window.location.replace('/benutzer.html?userId=' + user.userId);
|
||||
else window.location.href = '/login.html';
|
||||
});
|
||||
} else {
|
||||
Promise.all([
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null),
|
||||
fetch('/social/users/' + targetUserId).then(r => r.ok ? r.json() : null),
|
||||
fetch('/social/profile-images?userId=' + targetUserId).then(r => r.ok ? r.json() : [])
|
||||
]).then(([me, profile, images]) => {
|
||||
document.getElementById('loadingHint').style.display = 'none';
|
||||
|
||||
if (!profile) {
|
||||
document.getElementById('loadingHint').textContent = 'Profil nicht gefunden.';
|
||||
document.getElementById('loadingHint').style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Avatar – HQ bevorzugt, Fallback auf kleine Version
|
||||
const picData = profile.profilePictureHq || profile.profilePicture;
|
||||
if (picData) {
|
||||
document.getElementById('profilePic').innerHTML =
|
||||
`<img src="data:image/png;base64,${picData}" alt="Profilbild" onclick="openLightbox(this.src)">`;
|
||||
}
|
||||
|
||||
// Name + Seitentitel
|
||||
document.getElementById('profileName').textContent = profile.name;
|
||||
document.title = profile.name + ' – XXX The Game';
|
||||
|
||||
// Aktions-Buttons
|
||||
const actions = document.getElementById('profileActions');
|
||||
const isOwnProfile = me && me.userId === profile.userId;
|
||||
|
||||
if (isOwnProfile) {
|
||||
actions.innerHTML = `<a href="/profile.html" class="btn">Profil bearbeiten</a>`;
|
||||
} else {
|
||||
let html = '';
|
||||
if (profile.friendStatus === 'FRIEND') {
|
||||
html += `<a href="/nachrichten.html?userId=${profile.userId}" class="btn" style="background:var(--color-secondary);color:var(--color-text);">✉ Nachricht</a>`;
|
||||
html += `<button class="btn-outline" id="friendActionBtn" onclick="removeFriend()">Freundschaft beenden</button>`;
|
||||
} else if (profile.friendStatus === 'PENDING_SENT') {
|
||||
html += `<button disabled>Anfrage gesendet</button>`;
|
||||
html += `<a href="/nachrichten.html?userId=${profile.userId}" class="btn" style="background:var(--color-secondary);color:var(--color-text);">✉ Nachricht</a>`;
|
||||
} else if (profile.friendStatus === 'PENDING_RECEIVED') {
|
||||
html += `<button id="friendActionBtn" onclick="acceptFriend()">✓ Anfrage annehmen</button>`;
|
||||
html += `<a href="/nachrichten.html?userId=${profile.userId}" class="btn" style="background:var(--color-secondary);color:var(--color-text);">✉ Nachricht</a>`;
|
||||
} else {
|
||||
html += `<button id="friendActionBtn" onclick="addFriend()">+ Freund hinzufügen</button>`;
|
||||
html += `<a href="/nachrichten.html?userId=${profile.userId}" class="btn" style="background:var(--color-secondary);color:var(--color-text);">✉ Nachricht</a>`;
|
||||
}
|
||||
actions.innerHTML = html;
|
||||
}
|
||||
|
||||
// Gallery
|
||||
galleryImages = images;
|
||||
galleryIsOwn = isOwnProfile;
|
||||
renderProfileGallery();
|
||||
|
||||
document.getElementById('profileView').style.display = '';
|
||||
}).catch(() => {
|
||||
document.getElementById('loadingHint').textContent = 'Fehler beim Laden.';
|
||||
document.getElementById('loadingHint').style.display = '';
|
||||
});
|
||||
}
|
||||
|
||||
async function addFriend() {
|
||||
const btn = document.getElementById('friendActionBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const res = await fetch('/social/friends/request', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ receiverId: targetUserId })
|
||||
});
|
||||
btn.textContent = (res.ok || res.status === 201) ? 'Anfrage gesendet' : 'Fehler';
|
||||
} catch {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '+ Freund hinzufügen';
|
||||
}
|
||||
}
|
||||
|
||||
async function acceptFriend() {
|
||||
const btn = document.getElementById('friendActionBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const pending = await fetch('/social/friends/pending').then(r => r.json());
|
||||
const f = pending.find(p => p.user.userId === targetUserId);
|
||||
if (!f) { btn.textContent = 'Fehler'; return; }
|
||||
const res = await fetch('/social/friends/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ friendshipId: f.friendshipId })
|
||||
});
|
||||
if (res.ok) location.reload();
|
||||
else { btn.disabled = false; btn.textContent = '✓ Anfrage annehmen'; }
|
||||
} catch {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✓ Anfrage annehmen';
|
||||
}
|
||||
}
|
||||
|
||||
function openLightbox(src) {
|
||||
document.getElementById('lightboxImg').src = src;
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
}
|
||||
function closeLightbox() {
|
||||
document.getElementById('lightbox').classList.remove('open', 'gallery-mode');
|
||||
}
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeLightbox();
|
||||
if (e.key === 'ArrowLeft') galleryPrev();
|
||||
if (e.key === 'ArrowRight') galleryNext();
|
||||
});
|
||||
|
||||
// ── Gallery ──
|
||||
let galleryImages = [];
|
||||
let galleryIndex = 0;
|
||||
let galleryIsOwn = false;
|
||||
|
||||
function renderProfileGallery() {
|
||||
const grid = document.getElementById('profileGallery');
|
||||
grid.innerHTML = galleryImages.map((img, i) => `
|
||||
<div class="gallery-thumb" onclick="openGallery(${i})">
|
||||
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galerie-Bild ${i+1}">
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openGallery(index) {
|
||||
galleryIndex = index;
|
||||
document.getElementById('lightbox').classList.add('open', 'gallery-mode');
|
||||
showGalleryItem();
|
||||
}
|
||||
|
||||
function showGalleryItem() {
|
||||
const img = galleryImages[galleryIndex];
|
||||
document.getElementById('lightboxImg').src = 'data:image/jpeg;base64,' + img.imageData;
|
||||
document.getElementById('lbCounter').textContent = (galleryIndex + 1) + ' / ' + galleryImages.length;
|
||||
document.getElementById('lbPrev').disabled = galleryIndex === 0;
|
||||
document.getElementById('lbNext').disabled = galleryIndex === galleryImages.length - 1;
|
||||
|
||||
const likeBtn = document.getElementById('lbLike');
|
||||
if (galleryIsOwn) {
|
||||
likeBtn.style.display = 'none';
|
||||
} else {
|
||||
likeBtn.style.display = '';
|
||||
likeBtn.innerHTML = (img.likedByMe ? '♥' : '♡') + ' <span id="lbLikeCount">' + img.likeCount + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function galleryPrev() {
|
||||
if (!document.getElementById('lightbox').classList.contains('gallery-mode')) return;
|
||||
if (galleryIndex > 0) { galleryIndex--; showGalleryItem(); }
|
||||
}
|
||||
|
||||
function galleryNext() {
|
||||
if (!document.getElementById('lightbox').classList.contains('gallery-mode')) return;
|
||||
if (galleryIndex < galleryImages.length - 1) { galleryIndex++; showGalleryItem(); }
|
||||
}
|
||||
|
||||
async function toggleLike() {
|
||||
const img = galleryImages[galleryIndex];
|
||||
// Optimistic update
|
||||
img.likedByMe = !img.likedByMe;
|
||||
img.likeCount += img.likedByMe ? 1 : -1;
|
||||
showGalleryItem();
|
||||
|
||||
try {
|
||||
await fetch('/social/profile-images/' + img.imageId + '/like', { method: 'POST' });
|
||||
} catch (e) {
|
||||
// Revert on error
|
||||
img.likedByMe = !img.likedByMe;
|
||||
img.likeCount += img.likedByMe ? 1 : -1;
|
||||
showGalleryItem();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFriend() {
|
||||
const btn = document.getElementById('friendActionBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const friends = await fetch('/social/friends').then(r => r.json());
|
||||
const f = friends.find(f => f.user.userId === targetUserId);
|
||||
if (!f) { btn.textContent = 'Fehler'; return; }
|
||||
await fetch('/social/friends/reject', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ friendshipId: f.friendshipId })
|
||||
});
|
||||
location.reload();
|
||||
} catch {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Freundschaft beenden';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -132,16 +132,278 @@ button.secondary:hover {
|
||||
|
||||
.message.error {
|
||||
background: #3d0f1a;
|
||||
border: 1px solid var(--color-primary);
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: #3a2c0a;
|
||||
border: 2px solid #f5c518;
|
||||
color: #f5c518;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #0f3d1a;
|
||||
border: 1px solid var(--color-success);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
/* ── App layout ── */
|
||||
body.app {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
min-height: calc(100vh - 3rem);
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content { padding: 2rem 1.5rem; flex: 1; }
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
max-height: calc(100vh - 3rem);
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.sidebar-icon-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1.25rem 1.25rem 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-icon-area a { display: block; line-height: 0; }
|
||||
|
||||
.sidebar-icon-area img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-icon-area img:hover { transform: scale(1.1); }
|
||||
|
||||
.sidebar-mobile-only { display: none; }
|
||||
|
||||
.sidebar ul { list-style: none; padding: 0.5rem 0; }
|
||||
|
||||
.sidebar ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 1.25rem;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
border-left: 3px solid transparent;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.sidebar ul li a:hover,
|
||||
.sidebar ul li a.active {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-primary);
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
|
||||
|
||||
.sidebar-profile-img {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
/* ── Burger (mobile only) ── */
|
||||
.burger {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0.75rem; right: 0.75rem;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text);
|
||||
padding: 0.35rem 0.5rem;
|
||||
z-index: 110;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.burger:hover { background: var(--color-secondary); }
|
||||
|
||||
.burger-icon { display: flex; flex-direction: column; gap: 5px; width: 22px; }
|
||||
|
||||
.burger-icon span {
|
||||
display: block;
|
||||
height: 2px;
|
||||
background: var(--color-text);
|
||||
border-radius: 2px;
|
||||
transition: transform 0.25s, opacity 0.25s;
|
||||
}
|
||||
|
||||
.burger.open .burger-icon span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.burger.open .burger-icon span:nth-child(2) { opacity: 0; }
|
||||
.burger.open .burger-icon span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
|
||||
/* ── Sidebar overlay ── */
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.sidebar-overlay.visible { display: block; }
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
body.app { padding: 0; }
|
||||
|
||||
.app-wrapper {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0; right: 0;
|
||||
width: 240px;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-secondary);
|
||||
box-shadow: none;
|
||||
transform: translateX(100%);
|
||||
align-self: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); }
|
||||
.sidebar-icon-area { display: none; }
|
||||
|
||||
.main {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.burger { display: flex; }
|
||||
.sidebar-mobile-only { display: block; }
|
||||
}
|
||||
|
||||
/* ── Social Sidebar ── */
|
||||
.social-sidebar {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
max-height: calc(100vh - 3rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.social-sidebar ul { list-style: none; padding: 0.5rem 0; }
|
||||
|
||||
.social-sidebar ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.7rem 1.25rem;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
border-left: 3px solid transparent;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.social-sidebar ul li a:hover,
|
||||
.social-sidebar ul li a.active {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-primary);
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.social-sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
|
||||
|
||||
.social-sidebar-title {
|
||||
padding: 1rem 1.25rem 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.social-badge {
|
||||
margin-left: auto;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
border-radius: 9999px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
line-height: 1.4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.social-sidebar {
|
||||
position: static;
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
box-shadow: none;
|
||||
align-self: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Token box ── */
|
||||
.token-box {
|
||||
margin-top: 1.25rem;
|
||||
@@ -161,3 +423,38 @@ button.secondary:hover {
|
||||
color: #666;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* ── Sidebar groups ── */
|
||||
.sidebar-group-toggle {
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-arrow {
|
||||
margin-left: auto;
|
||||
font-size: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-group.open > a .sidebar-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.sidebar-sub {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-group.open > .sidebar-sub {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.sidebar .sidebar-sub li a {
|
||||
padding: 0.55rem 1.25rem 0.55rem 2.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
@@ -7,70 +7,6 @@
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
body { display: block; min-height: 100vh; }
|
||||
.layout { display: flex; min-height: 100vh; }
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.sidebar {
|
||||
width: 240px; background: var(--color-card);
|
||||
border-right: 1px solid var(--color-secondary);
|
||||
display: flex; flex-direction: column;
|
||||
position: fixed; top: 0; left: 0;
|
||||
height: 100vh; overflow-y: auto;
|
||||
z-index: 100; transition: transform 0.25s ease;
|
||||
}
|
||||
.sidebar-brand {
|
||||
color: var(--color-primary); font-size: 1.1rem; font-weight: 700;
|
||||
padding: 1.25rem 1.25rem 1rem;
|
||||
border-bottom: 1px solid var(--color-secondary); flex-shrink: 0;
|
||||
}
|
||||
.sidebar ul { list-style: none; padding: 0.5rem 0; }
|
||||
.sidebar ul li a {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.7rem 1.25rem; color: var(--color-text);
|
||||
text-decoration: none; font-size: 0.95rem;
|
||||
border-left: 3px solid transparent;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.sidebar ul li a:hover, .sidebar ul li a.active {
|
||||
background: var(--color-secondary); color: var(--color-primary);
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
|
||||
|
||||
/* ── Main ── */
|
||||
.main { margin-left: 240px; flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
|
||||
|
||||
/* ── Topbar ── */
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
padding: 0.9rem 1.5rem; background: var(--color-card);
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.topbar h1 { font-size: 1.2rem; font-weight: 600; }
|
||||
.burger {
|
||||
display: none; background: none; border: none; cursor: pointer;
|
||||
color: var(--color-text); padding: 0.25rem 0.4rem;
|
||||
border-radius: 4px; transition: background 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.burger:hover { background: var(--color-secondary); }
|
||||
.burger-icon { display: flex; flex-direction: column; gap: 5px; width: 22px; }
|
||||
.burger-icon span {
|
||||
display: block; height: 2px; background: var(--color-text);
|
||||
border-radius: 2px; transition: transform 0.25s, opacity 0.25s;
|
||||
}
|
||||
.burger.open .burger-icon span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.burger.open .burger-icon span:nth-child(2) { opacity: 0; }
|
||||
.burger.open .burger-icon span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
|
||||
.content { padding: 2rem 1.5rem; flex: 1; }
|
||||
|
||||
.overlay {
|
||||
display: none; position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.55); z-index: 90;
|
||||
}
|
||||
|
||||
/* ── Search ── */
|
||||
.search-bar {
|
||||
display: flex; gap: 0.6rem; margin-bottom: 1.5rem; align-items: center;
|
||||
@@ -208,47 +144,12 @@
|
||||
.item-detail-chip-toy { background: rgba(233,69,96,0.12); color: var(--color-primary); }
|
||||
.sub-empty { font-size: 0.78rem; color: var(--color-muted); padding: 0.2rem 0; }
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { transform: translateX(-100%); }
|
||||
.sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,0.5); }
|
||||
.main { margin-left: 0; }
|
||||
.burger { display: flex; }
|
||||
.overlay.visible { display: block; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="app">
|
||||
|
||||
<div class="overlay" id="overlay"></div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-brand">XXX The Game</div>
|
||||
<ul>
|
||||
<li><a href="/userhome.html"><span class="icon">⊞</span> Dashboard</a></li>
|
||||
<li><a href="#"><span class="icon">▶</span> Meine Session</a></li>
|
||||
<li><a href="/aufgaben.html"><span class="icon">✓</span> Aufgaben</a></li>
|
||||
<li><a href="/entdecken.html" class="active"><span class="icon">⊙</span> Entdecken</a></li>
|
||||
<li><a href="#"><span class="icon">⚡</span> Strafen</a></li>
|
||||
<li><a href="/toys.html"><span class="icon">◈</span> Toys</a></li>
|
||||
<li><a href="#"><span class="icon">♥</span> Favoriten</a></li>
|
||||
<li><a href="#"><span class="icon">☰</span> Rangliste</a></li>
|
||||
<li><a href="#"><span class="icon">✉</span> Nachrichten</a></li>
|
||||
<li><a href="#"><span class="icon">⚙</span> Einstellungen</a></li>
|
||||
<li><a href="#" id="logoutLink"><span class="icon">⏏</span> Abmelden</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<header class="topbar">
|
||||
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
||||
<span class="burger-icon"><span></span><span></span><span></span></span>
|
||||
</button>
|
||||
<h1>Entdecken</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
|
||||
<button class="btn-search" id="searchBtn">Suchen</button>
|
||||
@@ -260,10 +161,10 @@
|
||||
<span class="page-info" id="pageInfo"></span>
|
||||
<button id="nextBtn">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 0, totalPages = 1;
|
||||
@@ -281,12 +182,6 @@
|
||||
.then(user => { if (!user) return; loadGroups(); })
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
document.getElementById('logoutLink').addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
document.cookie = 'jwt=; Max-Age=0; path=/';
|
||||
window.location.href = '/login.html';
|
||||
});
|
||||
|
||||
// ── Load ──
|
||||
function loadGroups() {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
@@ -583,21 +478,6 @@
|
||||
if (currentPage < totalPages - 1) { currentPage++; loadGroups(); }
|
||||
});
|
||||
|
||||
// ── Burger menu ──
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const burgerBtn = document.getElementById('burgerBtn');
|
||||
const overlay = document.getElementById('overlay');
|
||||
function openMenu() {
|
||||
sidebar.classList.add('open'); overlay.classList.add('visible');
|
||||
burgerBtn.classList.add('open'); burgerBtn.setAttribute('aria-label', 'Menü schließen');
|
||||
}
|
||||
function closeMenu() {
|
||||
sidebar.classList.remove('open'); overlay.classList.remove('visible');
|
||||
burgerBtn.classList.remove('open'); burgerBtn.setAttribute('aria-label', 'Menü öffnen');
|
||||
}
|
||||
burgerBtn.addEventListener('click', () => sidebar.classList.contains('open') ? closeMenu() : openMenu());
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
sidebar.querySelectorAll('a').forEach(l => l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); }));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
112
xxxthegame/src/main/resources/static/forgot-password.html
Normal file
@@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>xXx Games – Passwort vergessen</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
.modal {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
max-width: 340px;
|
||||
width: 90%;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
.modal p {
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<img src="icon.png" alt="Logo">
|
||||
<h1>Passwort vergessen</h1>
|
||||
<p class="subtitle">Gib deine E-Mail-Adresse ein. Falls sie bei uns registriert ist, erhältst du einen Link zum Zurücksetzen.</p>
|
||||
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" placeholder="deine@email.de" autocomplete="email" />
|
||||
|
||||
<button class="full-width" id="submitBtn" onclick="submit()">Link anfordern</button>
|
||||
|
||||
<div class="message" id="message"></div>
|
||||
|
||||
<p style="text-align:center; margin-top:1.25rem; font-size:0.85rem;">
|
||||
<a href="/login.html" style="color:var(--color-primary);">Zurück zum Login</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="overlay">
|
||||
<div class="modal">
|
||||
<p>Falls diese E-Mail-Adresse bei uns registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen deines Passworts.</p>
|
||||
<button class="full-width" onclick="goToLogin()">Zum Login</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') submit();
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const btn = document.getElementById('submitBtn');
|
||||
|
||||
if (!email) {
|
||||
showMessage('Bitte E-Mail-Adresse eingeben.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird gesendet…';
|
||||
hideMessage();
|
||||
|
||||
try {
|
||||
await fetch('/password-reset/request', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
document.getElementById('overlay').classList.add('active');
|
||||
}
|
||||
|
||||
function goToLogin() {
|
||||
window.location.href = '/login.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';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
278
xxxthegame/src/main/resources/static/freunde.html
Normal file
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Freunde – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-radius: 0;
|
||||
padding: 0.6rem 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-muted);
|
||||
cursor: pointer;
|
||||
margin-bottom: -1px;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.tab-btn:hover { color: var(--color-text); background: none; }
|
||||
.tab-btn.active { color: var(--color-primary); border-bottom-color: var(--color-primary); }
|
||||
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
.user-list { list-style: none; margin: 0; padding: 0; }
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
.user-item:last-child { border-bottom: none; }
|
||||
|
||||
.user-avatar {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-secondary);
|
||||
}
|
||||
.user-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.user-name { font-weight: 600; flex: 1; }
|
||||
|
||||
.user-actions { display: flex; gap: 0.5rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.user-actions button, .user-actions a.btn {
|
||||
margin-top: 0;
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.user-profile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.user-profile-link:hover .user-name { color: var(--color-primary); }
|
||||
.btn-reject {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-reject:hover { background: #3d0f1a; border-color: var(--color-primary); color: var(--color-primary); }
|
||||
|
||||
.empty-hint { color: var(--color-muted); font-size: 0.9rem; margin-top: 0.5rem; }
|
||||
|
||||
.tab-badge {
|
||||
display: inline-block;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
border-radius: 9999px;
|
||||
padding: 0.05rem 0.35rem;
|
||||
margin-left: 0.35rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1 style="margin-bottom: 1.25rem;">Freunde</h1>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('friends', this)">Freunde</button>
|
||||
<button class="tab-btn" onclick="switchTab('pending', this)" id="pendingTabBtn">
|
||||
Anfragen<span class="tab-badge" id="pendingBadge" style="display:none;"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Friends tab -->
|
||||
<div class="tab-panel active" id="tab-friends">
|
||||
<ul class="user-list" id="friendsList"></ul>
|
||||
<p class="empty-hint" id="friendsEmpty" style="display:none;">Du hast noch keine Freunde. <a href="/personen-suchen.html" style="color:var(--color-primary);">Personen suchen</a></p>
|
||||
</div>
|
||||
|
||||
<!-- Pending tab -->
|
||||
<div class="tab-panel" id="tab-pending">
|
||||
<ul class="user-list" id="pendingList"></ul>
|
||||
<p class="empty-hint" id="pendingEmpty" style="display:none;">Keine offenen Anfragen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function avatar(u) {
|
||||
return u.profilePicture
|
||||
? `<img src="data:image/png;base64,${u.profilePicture}" alt="">`
|
||||
: '◉';
|
||||
}
|
||||
|
||||
async function loadFriends() {
|
||||
try {
|
||||
const res = await fetch('/social/friends');
|
||||
if (!res.ok) return;
|
||||
const friends = await res.json();
|
||||
const list = document.getElementById('friendsList');
|
||||
list.innerHTML = '';
|
||||
if (friends.length === 0) {
|
||||
document.getElementById('friendsEmpty').style.display = '';
|
||||
return;
|
||||
}
|
||||
document.getElementById('friendsEmpty').style.display = 'none';
|
||||
friends.forEach(f => {
|
||||
list.insertAdjacentHTML('beforeend', `
|
||||
<li class="user-item" id="friend-${f.friendshipId}">
|
||||
<a href="/benutzer.html?userId=${f.user.userId}" class="user-profile-link">
|
||||
<div class="user-avatar">${avatar(f.user)}</div>
|
||||
<div class="user-name">${esc(f.user.name)}</div>
|
||||
</a>
|
||||
<div class="user-actions">
|
||||
<a href="/nachrichten.html?userId=${f.user.userId}" class="btn" style="background:var(--color-secondary); color:var(--color-text);">✉ Nachricht</a>
|
||||
<button class="btn-reject" onclick="removeFriend('${f.friendshipId}', this)">Entfernen</button>
|
||||
</div>
|
||||
</li>`);
|
||||
});
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function loadPending() {
|
||||
try {
|
||||
const res = await fetch('/social/friends/pending');
|
||||
if (!res.ok) return;
|
||||
const pending = await res.json();
|
||||
const list = document.getElementById('pendingList');
|
||||
list.innerHTML = '';
|
||||
|
||||
const badge = document.getElementById('pendingBadge');
|
||||
if (pending.length > 0) {
|
||||
badge.textContent = pending.length;
|
||||
badge.style.display = '';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
|
||||
if (pending.length === 0) {
|
||||
document.getElementById('pendingEmpty').style.display = '';
|
||||
return;
|
||||
}
|
||||
document.getElementById('pendingEmpty').style.display = 'none';
|
||||
pending.forEach(f => {
|
||||
list.insertAdjacentHTML('beforeend', `
|
||||
<li class="user-item" id="pending-${f.friendshipId}">
|
||||
<a href="/benutzer.html?userId=${f.user.userId}" class="user-profile-link">
|
||||
<div class="user-avatar">${avatar(f.user)}</div>
|
||||
<div class="user-name">${esc(f.user.name)}</div>
|
||||
</a>
|
||||
<div class="user-actions">
|
||||
<button onclick="accept('${f.friendshipId}', this)">✓ Annehmen</button>
|
||||
<button class="btn-reject" onclick="reject('${f.friendshipId}', this)">✕ Ablehnen</button>
|
||||
</div>
|
||||
</li>`);
|
||||
});
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function accept(friendshipId, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const res = await fetch('/social/friends/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ friendshipId })
|
||||
});
|
||||
if (res.ok) {
|
||||
document.getElementById('pending-' + friendshipId)?.remove();
|
||||
await loadFriends();
|
||||
await loadPending();
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✓ Annehmen';
|
||||
}
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✓ Annehmen';
|
||||
}
|
||||
}
|
||||
|
||||
async function reject(friendshipId, btn) {
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await fetch('/social/friends/reject', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ friendshipId })
|
||||
});
|
||||
document.getElementById('pending-' + friendshipId)?.remove();
|
||||
await loadPending();
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFriend(friendshipId, btn) {
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await fetch('/social/friends/reject', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ friendshipId })
|
||||
});
|
||||
document.getElementById('friend-' + friendshipId)?.remove();
|
||||
const list = document.getElementById('friendsList');
|
||||
if (list.children.length === 0) {
|
||||
document.getElementById('friendsEmpty').style.display = '';
|
||||
}
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
loadFriends();
|
||||
loadPending();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
xxxthegame/src/main/resources/static/icon.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
xxxthegame/src/main/resources/static/img/lvl1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
xxxthegame/src/main/resources/static/img/lvl2.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
xxxthegame/src/main/resources/static/img/lvl3.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
xxxthegame/src/main/resources/static/img/lvl4.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
xxxthegame/src/main/resources/static/img/lvl5.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
@@ -3,14 +3,16 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>XXX The Game</title>
|
||||
<title>xXx Games</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="icon" type="image/png" href="icon.png">
|
||||
</head>
|
||||
<body>
|
||||
<h1>XXX The Game</h1>
|
||||
<p>Das Erwachsenenspiel für Paare und Gruppen</p>
|
||||
<div style="display:flex; gap:1rem;">
|
||||
<img src="logo.png" alt="Logo">
|
||||
<h1>Erwachsenenspiele</h1>
|
||||
<p>Alleine, als Paar oder mehr...</p>
|
||||
<div id="authButtons" style="display:flex; gap:1rem;">
|
||||
<a class="btn" href="/login.html">Anmelden</a>
|
||||
<a class="btn" href="/registration.html" style="background:#0f3460;">Registrieren</a>
|
||||
</div>
|
||||
@@ -22,6 +24,18 @@
|
||||
</button>
|
||||
|
||||
<script>
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(user => {
|
||||
if (!user) return;
|
||||
const btns = document.getElementById('authButtons');
|
||||
btns.insertAdjacentHTML('beforebegin',
|
||||
`<p style="text-align:center;">Willkommen zurück, <strong style="color:var(--color-text);">${user.name}</strong></p>`
|
||||
);
|
||||
btns.innerHTML = '<a class="btn" href="/userhome.html">Zu den Games</a>';
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
async function fillDb() {
|
||||
const btn = event.currentTarget;
|
||||
btn.disabled = true;
|
||||
|
||||
19
xxxthegame/src/main/resources/static/infobdsm.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BDSM Game – Info – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1>BDSM Game</h1>
|
||||
<p>Informationen zum BDSM Game folgen hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
19
xxxthegame/src/main/resources/static/infochastity.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chastity Game – Info – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1>Chastity Game</h1>
|
||||
<p>Informationen zum Chastity Game folgen hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
19
xxxthegame/src/main/resources/static/infovanilla.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vanilla Game – Info – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1>Vanilla Game</h1>
|
||||
<p>Informationen zum Vanilla Game folgen hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
146
xxxthegame/src/main/resources/static/js/sidebar.js
Normal file
@@ -0,0 +1,146 @@
|
||||
(function () {
|
||||
const path = window.location.pathname;
|
||||
|
||||
const groups = [
|
||||
{
|
||||
label: 'Vanilla Game',
|
||||
icon: '♡',
|
||||
items: [
|
||||
{ href: '/infovanilla.html', icon: 'ℹ', label: 'Info' },
|
||||
{ href: '/sessionvanilla.html', icon: '▷', label: 'Neue Session' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'BDSM Game',
|
||||
icon: '◆',
|
||||
items: [
|
||||
{ href: '/infobdsm.html', icon: 'ℹ', label: 'Info' },
|
||||
{ href: '/sessionbdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '/sessionbdsmingame.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' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Chastity Game',
|
||||
icon: '⊗',
|
||||
items: [
|
||||
{ href: '/infochastity.html', icon: 'ℹ', label: 'Info' },
|
||||
{ href: '/sessionchastity.html', icon: '▷', label: 'Neue Session' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
const homeCls = path === '/userhome.html' ? ' class="active"' : '';
|
||||
const homeItem = `
|
||||
<li class="sidebar-mobile-only">
|
||||
<a href="/userhome.html"${homeCls}><span class="icon">⊞</span> Home</a>
|
||||
</li>`;
|
||||
|
||||
const nav = groups.map(({ label, icon, items }) => {
|
||||
const isOpen = items.some(item => item.href === path);
|
||||
const openCls = isOpen ? ' open' : '';
|
||||
const subItems = items.map(({ href, icon: iIcon, label: iLabel, id: iId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const idAt = iId ? ` id="${iId}"` : '';
|
||||
return `<li${idAt}><a href="${href}"${cls}><span class="icon">${iIcon}</span> ${iLabel}</a></li>`;
|
||||
}).join('');
|
||||
return `
|
||||
<li class="sidebar-group${openCls}">
|
||||
<a class="sidebar-group-toggle"><span class="icon">${icon}</span> ${label}<span class="sidebar-arrow">▸</span></a>
|
||||
<ul class="sidebar-sub">
|
||||
${subItems}
|
||||
</ul>
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
document.body.insertAdjacentHTML('afterbegin', `
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
||||
<span class="burger-icon"><span></span><span></span><span></span></span>
|
||||
</button>
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-icon-area">
|
||||
<a href="/userhome.html"><img src="/icon.png" alt="Home"></a>
|
||||
</div>
|
||||
<ul>
|
||||
${homeItem}
|
||||
${nav}
|
||||
<li><hr style="border:none; border-top:1px solid var(--color-secondary); margin:0.4rem 1rem;"></li>
|
||||
<li>
|
||||
<a href="/login/logout"><span class="icon">⏏</span> Abmelden</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
`);
|
||||
|
||||
// Sidebar und .main in einen zentrierten App-Wrapper verschieben
|
||||
const appWrapper = document.createElement('div');
|
||||
appWrapper.className = 'app-wrapper';
|
||||
const sidebarEl = document.getElementById('sidebar');
|
||||
const mainEl = document.querySelector('.main');
|
||||
document.body.insertBefore(appWrapper, sidebarEl);
|
||||
appWrapper.appendChild(sidebarEl);
|
||||
if (mainEl) appWrapper.appendChild(mainEl);
|
||||
|
||||
// Group toggle
|
||||
document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
toggle.closest('.sidebar-group').classList.toggle('open');
|
||||
});
|
||||
});
|
||||
|
||||
// "Im Spiel" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
|
||||
const navNeu = document.getElementById('navBdsmNeu');
|
||||
const navImSpiel = document.getElementById('navBdsmImSpiel');
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
|
||||
// Session-Status prüfen
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(async user => {
|
||||
if (!user) return;
|
||||
|
||||
// Session-Status prüfen und Menü anpassen
|
||||
try {
|
||||
const sessionRes = await fetch(`/session?userId=${user.userId}`);
|
||||
const hasSession = sessionRes.status === 200;
|
||||
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
|
||||
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
|
||||
} catch (_) { /* Menü bleibt im Standardzustand */ }
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const burgerBtn = document.getElementById('burgerBtn');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
function openMenu() {
|
||||
sidebar.classList.add('open');
|
||||
overlay.classList.add('visible');
|
||||
burgerBtn.classList.add('open');
|
||||
burgerBtn.setAttribute('aria-label', 'Menü schließen');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('visible');
|
||||
burgerBtn.classList.remove('open');
|
||||
burgerBtn.setAttribute('aria-label', 'Menü öffnen');
|
||||
}
|
||||
|
||||
burgerBtn.addEventListener('click', () =>
|
||||
sidebar.classList.contains('open') ? closeMenu() : openMenu()
|
||||
);
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
sidebar.querySelectorAll('a:not([href="/login/logout"]):not(.sidebar-group-toggle)').forEach(l =>
|
||||
l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); })
|
||||
);
|
||||
|
||||
// Social sidebar auf allen App-Seiten nachladen
|
||||
const s = document.createElement('script');
|
||||
s.src = '/js/social-sidebar.js';
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
111
xxxthegame/src/main/resources/static/js/social-sidebar.js
Normal file
@@ -0,0 +1,111 @@
|
||||
(function () {
|
||||
// Verhindert doppelte Ausführung (z.B. wenn sidebar.js nachladen UND direktes <script>-Tag vorhanden)
|
||||
if (document.querySelector('.social-sidebar')) return;
|
||||
|
||||
const path = window.location.pathname;
|
||||
|
||||
const links = [
|
||||
{ href: '/personen-suchen.html', icon: '⊕', label: 'Personen suchen', badgeId: null, mobileBadgeId: null },
|
||||
{ href: '/freunde.html', icon: '♡', label: 'Freunde', badgeId: 'socialFriendsBadge', mobileBadgeId: 'socialMobileFriendsBadge' },
|
||||
{ href: '/nachrichten.html', icon: '✉', label: 'Nachrichten', badgeId: 'socialMsgBadge', mobileBadgeId: 'socialMobileMsgBadge' },
|
||||
];
|
||||
|
||||
const profileActive = (path === '/benutzer.html' || path === '/profile.html') ? ' class="active"' : '';
|
||||
|
||||
// ── Rechte Desktop-Sidebar (kein Titel) ──
|
||||
const desktopItems = links.map(({ href, icon, label, badgeId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const badge = badgeId ? `<span class="social-badge" id="${badgeId}" style="display:none;"></span>` : '';
|
||||
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
|
||||
}).join('');
|
||||
|
||||
const aside = document.createElement('aside');
|
||||
aside.className = 'social-sidebar';
|
||||
aside.innerHTML = `
|
||||
<ul>
|
||||
<li id="socialProfileItem">
|
||||
<a href="/benutzer.html"${profileActive}>
|
||||
<span class="icon" id="socialProfileIcon">◉</span>
|
||||
<span id="socialProfileName">Profil</span>
|
||||
</a>
|
||||
</li>
|
||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
||||
${desktopItems}
|
||||
</ul>`;
|
||||
|
||||
const appWrapper = document.querySelector('.app-wrapper');
|
||||
if (appWrapper) appWrapper.appendChild(aside);
|
||||
|
||||
// ── Mobile: Links + Profil ins Burger-Menü einhängen ──
|
||||
const sidebarUl = document.querySelector('.sidebar ul');
|
||||
if (sidebarUl) {
|
||||
const mobileLinks = links.map(({ href, icon, label, mobileBadgeId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const badge = mobileBadgeId
|
||||
? `<span class="social-badge" id="${mobileBadgeId}" style="display:none;"></span>`
|
||||
: '';
|
||||
return `<li class="sidebar-mobile-only"><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
|
||||
}).join('');
|
||||
|
||||
const mobileProfileActive = profileActive;
|
||||
const mobileProfile = `
|
||||
<li class="sidebar-mobile-only" id="socialMobileProfileItem">
|
||||
<a href="/benutzer.html"${mobileProfileActive}>
|
||||
<span class="icon" id="socialMobileProfileIcon">◉</span>
|
||||
<span id="socialMobileProfileName">Profil</span>
|
||||
</a>
|
||||
</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 logoutLi = sidebarUl.querySelector('a[href="/login/logout"]')?.closest('li');
|
||||
if (logoutLi) {
|
||||
logoutLi.insertAdjacentHTML('beforebegin', sep + mobileLinks + mobileProfile);
|
||||
} else {
|
||||
sidebarUl.insertAdjacentHTML('beforeend', sep + mobileLinks + mobileProfile);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Profil-Daten nachladen (Name + Avatar) ──
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(user => {
|
||||
if (!user) return;
|
||||
|
||||
function updateProfileEntry(nameId, iconId, itemId) {
|
||||
const nameEl = document.getElementById(nameId);
|
||||
if (nameEl) nameEl.textContent = user.name;
|
||||
|
||||
const iconEl = document.getElementById(iconId);
|
||||
if (iconEl && user.profilePicture) {
|
||||
iconEl.innerHTML = `<img src="data:image/png;base64,${user.profilePicture}" class="sidebar-profile-img" alt="">`;
|
||||
}
|
||||
const anchor = document.querySelector('#' + itemId + ' a');
|
||||
if (anchor) anchor.href = '/benutzer.html?userId=' + user.userId;
|
||||
}
|
||||
|
||||
updateProfileEntry('socialProfileName', 'socialProfileIcon', 'socialProfileItem');
|
||||
updateProfileEntry('socialMobileProfileName', 'socialMobileProfileIcon', 'socialMobileProfileItem');
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// ── Badge-Zähler ──
|
||||
function setBadge(ids, count) {
|
||||
ids.forEach(id => {
|
||||
if (!id) return;
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = count;
|
||||
el.style.display = count > 0 ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
fetch('/social/friends/pending/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialFriendsBadge', 'socialMobileFriendsBadge'], n))
|
||||
.catch(() => {});
|
||||
|
||||
fetch('/social/messages/unread/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialMsgBadge', 'socialMobileMsgBadge'], n))
|
||||
.catch(() => {});
|
||||
})();
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>XXX The Game – Login</title>
|
||||
<title>xXx Games – Login</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>XXX The Game</h1>
|
||||
<p class="subtitle">Bitte melde dich an</p>
|
||||
<img src="icon.png" alt="Logo">
|
||||
<h1>Bitte melde dich an</h1>
|
||||
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" placeholder="deine@email.de" autocomplete="username" />
|
||||
@@ -21,6 +21,13 @@
|
||||
<button class="full-width" id="loginBtn" onclick="login()">Anmelden</button>
|
||||
|
||||
<div class="message" id="message"></div>
|
||||
|
||||
<p style="text-align:center; margin-top:1.25rem; font-size:0.85rem;">
|
||||
<a href="/forgot-password.html" style="color:var(--color-primary);">Passwort vergessen?</a>
|
||||
</p>
|
||||
<p style="text-align:center; margin-top:0.5rem; font-size:0.85rem;">
|
||||
Noch kein Konto? <a href="/registration.html" style="color:var(--color-primary);">Registrieren</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -31,6 +38,10 @@
|
||||
document.getElementById('email').value = email;
|
||||
document.getElementById('password').focus();
|
||||
showMessage('E-Mail-Adresse bestätigt! Du kannst dich jetzt anmelden.', 'success');
|
||||
} else if (params.get('emailChanged')) {
|
||||
showMessage('E-Mail-Adresse erfolgreich geändert. Bitte melde dich mit deiner neuen Adresse an.', 'success');
|
||||
} else if (params.get('accountDeleted')) {
|
||||
showMessage('Dein Konto wurde gelöscht.', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
BIN
xxxthegame/src/main/resources/static/logo.png
Normal file
|
After Width: | Height: | Size: 459 KiB |
593
xxxthegame/src/main/resources/static/nachrichten.html
Normal file
@@ -0,0 +1,593 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nachrichten – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
/* Override .main and .content for full-height chat layout */
|
||||
.main { overflow: hidden; }
|
||||
|
||||
.msg-layout {
|
||||
display: flex;
|
||||
height: calc(100vh - 3rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Left: conversation list */
|
||||
.conv-list-pane {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--color-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.conv-list-header {
|
||||
padding: 1.25rem 1rem 0.75rem;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.conv-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.conv-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.7rem 1rem;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.conv-item:hover { background: var(--color-secondary); }
|
||||
.conv-item.active { background: var(--color-secondary); border-left-color: var(--color-primary); }
|
||||
|
||||
.conv-avatar {
|
||||
width: 38px; height: 38px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.conv-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.conv-info { flex: 1; min-width: 0; }
|
||||
.conv-name { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.conv-preview { font-size: 0.78rem; color: var(--color-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.conv-unread {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
border-radius: 9999px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Right: thread */
|
||||
.thread-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.thread-header {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.thread-back {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-muted);
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.thread-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.thread-placeholder {
|
||||
color: var(--color-muted);
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.bubble-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bubble-wrap.me { align-items: flex-end; }
|
||||
.bubble-wrap.them { align-items: flex-start; }
|
||||
.bubble {
|
||||
max-width: 70%;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 14px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
.bubble-wrap.me .bubble {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.bubble-wrap.them .bubble {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.bubble-time {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.thread-input-wrap {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thread-input-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
}
|
||||
.thread-input-area input {
|
||||
flex: 1;
|
||||
}
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.3rem 0.4rem;
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.65;
|
||||
border-radius: 6px;
|
||||
transition: opacity 0.15s, background 0.15s;
|
||||
line-height: 1;
|
||||
}
|
||||
.btn-icon:hover { opacity: 1; background: var(--color-secondary); }
|
||||
.btn-send {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
padding: 0.55rem 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.emoji-picker {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0; right: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding: 0.5rem;
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 -4px 16px rgba(0,0,0,0.4);
|
||||
z-index: 50;
|
||||
}
|
||||
.emoji-picker.open { display: flex; }
|
||||
.emoji-picker button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
padding: 0.2rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.emoji-picker button:hover { background: var(--color-secondary); }
|
||||
|
||||
.bubble-img {
|
||||
max-width: 220px;
|
||||
max-height: 220px;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.85);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.lightbox.open { display: flex; }
|
||||
.lightbox img {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
border-radius: 6px;
|
||||
object-fit: contain;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.7);
|
||||
}
|
||||
.lightbox-close {
|
||||
position: fixed !important;
|
||||
top: 1rem !important;
|
||||
right: 1rem !important;
|
||||
background: rgba(0,0,0,0.55) !important;
|
||||
border: 1px solid rgba(255,255,255,0.2) !important;
|
||||
color: #fff !important;
|
||||
font-size: 1.1rem !important;
|
||||
width: 2.2rem !important;
|
||||
height: 2.2rem !important;
|
||||
border-radius: 50% !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
line-height: 1 !important;
|
||||
transition: background 0.15s !important;
|
||||
z-index: 501;
|
||||
}
|
||||
.lightbox-close:hover { background: rgba(0,0,0,0.85) !important; }
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.msg-layout { height: auto; flex-direction: column; }
|
||||
.conv-list-pane { width: 100%; border-right: none; border-bottom: 1px solid var(--color-secondary); max-height: 240px; }
|
||||
.conv-list-pane.hidden { display: none; }
|
||||
.thread-pane.hidden { display: none; }
|
||||
.thread-back { display: block; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="msg-layout">
|
||||
<!-- Left pane: conversation list -->
|
||||
<div class="conv-list-pane" id="convListPane">
|
||||
<div class="conv-list-header">Nachrichten</div>
|
||||
<ul class="conv-list" id="convList">
|
||||
<li style="padding:1rem; color:var(--color-muted); font-size:0.9rem;">Wird geladen…</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Right pane: thread -->
|
||||
<div class="thread-pane" id="threadPane">
|
||||
<div class="thread-header" id="threadHeader">
|
||||
<button class="thread-back" id="backBtn" onclick="showList()" aria-label="Zurück">‹</button>
|
||||
<div class="conv-avatar" id="threadPartnerAvatar" style="display:none;"></div>
|
||||
<span id="threadPartnerName">Konversation auswählen</span>
|
||||
</div>
|
||||
<div class="thread-messages" id="threadMessages">
|
||||
<div class="thread-placeholder" id="threadPlaceholder">Wähle eine Konversation aus oder schreibe jemanden direkt an.</div>
|
||||
</div>
|
||||
<div class="thread-input-wrap" id="threadInputWrap" style="display:none;">
|
||||
<div class="emoji-picker" id="emojiPicker"></div>
|
||||
<div class="thread-input-area">
|
||||
<button class="btn-icon" id="emojiBtn" onclick="toggleEmoji()" title="Emoji">😊</button>
|
||||
<input type="text" id="msgInput" placeholder="Nachricht eingeben…" autocomplete="off">
|
||||
<input type="file" id="imgFile" accept="image/*" style="display:none;">
|
||||
<button class="btn-icon" onclick="document.getElementById('imgFile').click()" title="Bild senden">📷</button>
|
||||
<button class="btn-send" onclick="sendMsg()">↑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
|
||||
<img id="lightboxImg" src="" alt="" onclick="event.stopPropagation()">
|
||||
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let myId = null;
|
||||
let activePartnerId = null;
|
||||
let pollTimer = null;
|
||||
|
||||
// Load current user
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(user => {
|
||||
if (!user) return;
|
||||
myId = user.userId;
|
||||
loadConversations();
|
||||
// Open thread from URL param
|
||||
const urlPartnerId = new URLSearchParams(window.location.search).get('userId');
|
||||
if (urlPartnerId) openThread(urlPartnerId);
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
async function loadConversations() {
|
||||
try {
|
||||
const res = await fetch('/social/messages');
|
||||
if (!res.ok) return;
|
||||
const convs = await res.json();
|
||||
renderConvList(convs);
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderConvList(convs) {
|
||||
const list = document.getElementById('convList');
|
||||
list.innerHTML = '';
|
||||
if (convs.length === 0) {
|
||||
list.innerHTML = '<li style="padding:1rem; color:var(--color-muted); font-size:0.9rem;">Noch keine Nachrichten. <a href="/personen-suchen.html" style="color:var(--color-primary);">Personen suchen</a></li>';
|
||||
return;
|
||||
}
|
||||
convs.forEach(c => {
|
||||
const av = c.partner.profilePicture
|
||||
? `<img src="data:image/png;base64,${c.partner.profilePicture}" alt="" style="cursor:zoom-in;" onclick="event.stopPropagation();openLightbox(this.src)">`
|
||||
: '◉';
|
||||
const unreadHtml = c.unreadCount > 0
|
||||
? `<span class="conv-unread">${c.unreadCount}</span>`
|
||||
: '';
|
||||
const preview = c.lastMessage
|
||||
? (c.lastMessage.text.startsWith('data:image/') ? '📷 Bild' : esc(c.lastMessage.text.substring(0, 40)))
|
||||
: '';
|
||||
const li = document.createElement('li');
|
||||
li.className = 'conv-item' + (c.partner.userId === activePartnerId ? ' active' : '');
|
||||
li.dataset.partnerId = c.partner.userId;
|
||||
li.innerHTML = `
|
||||
<div class="conv-avatar">${av}</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-name">${esc(c.partner.name)}</div>
|
||||
<div class="conv-preview">${preview}</div>
|
||||
</div>
|
||||
${unreadHtml}`;
|
||||
li.addEventListener('click', () => openThread(c.partner.userId, c.partner.name, c.partner.profilePicture));
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function openThread(partnerId, partnerName, partnerPic) {
|
||||
activePartnerId = partnerId;
|
||||
|
||||
// Update active state in list
|
||||
document.querySelectorAll('.conv-item').forEach(li => {
|
||||
li.classList.toggle('active', li.dataset.partnerId === partnerId);
|
||||
});
|
||||
|
||||
// Update header – if name not provided, fetch from conversation list or user lookup
|
||||
if (!partnerName) {
|
||||
const convItem = document.querySelector(`.conv-item[data-partner-id="${partnerId}"]`);
|
||||
partnerName = convItem ? convItem.querySelector('.conv-name').textContent : '…';
|
||||
}
|
||||
document.getElementById('threadPartnerName').innerHTML =
|
||||
`<a href="/benutzer.html?userId=${partnerId}" style="color:inherit;text-decoration:none;">${esc(partnerName)}</a>`;
|
||||
|
||||
const avatarEl = document.getElementById('threadPartnerAvatar');
|
||||
if (partnerPic) {
|
||||
avatarEl.innerHTML = `<img src="data:image/png;base64,${partnerPic}" alt="" style="cursor:zoom-in;" onclick="openLightbox(this.src)">`;
|
||||
avatarEl.style.display = '';
|
||||
} else {
|
||||
avatarEl.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('threadInputWrap').style.display = '';
|
||||
document.getElementById('msgInput').focus();
|
||||
|
||||
// Mobile: hide list, show thread
|
||||
if (window.innerWidth <= 768) showThread();
|
||||
|
||||
await loadThread();
|
||||
startPolling();
|
||||
}
|
||||
|
||||
async function loadThread() {
|
||||
if (!activePartnerId) return;
|
||||
try {
|
||||
const res = await fetch('/social/messages/' + activePartnerId);
|
||||
if (!res.ok) return;
|
||||
const msgs = await res.json();
|
||||
renderThread(msgs);
|
||||
loadConversations(); // refresh unread counts in list
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderThread(msgs) {
|
||||
const container = document.getElementById('threadMessages');
|
||||
container.innerHTML = '';
|
||||
if (msgs.length === 0) {
|
||||
container.appendChild(Object.assign(document.createElement('div'), {
|
||||
className: 'thread-placeholder',
|
||||
textContent: 'Noch keine Nachrichten. Schreib als Erster!'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// msgs are newest-first; reverse to show oldest first
|
||||
[...msgs].reverse().forEach(m => {
|
||||
const isMe = m.senderId === myId;
|
||||
const time = new Date(m.sentAt).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit' });
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'bubble-wrap ' + (isMe ? 'me' : 'them');
|
||||
const isImg = m.text.startsWith('data:image/');
|
||||
const content = isImg
|
||||
? `<img src="${m.text}" class="bubble-img" onclick="openLightbox(this.src)" alt="Bild">`
|
||||
: esc(m.text);
|
||||
wrap.innerHTML = `
|
||||
<div class="bubble">${content}</div>
|
||||
<div class="bubble-time">${time}</div>`;
|
||||
container.appendChild(wrap);
|
||||
});
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
async function sendMsg() {
|
||||
if (!activePartnerId) return;
|
||||
const input = document.getElementById('msgInput');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
input.value = '';
|
||||
try {
|
||||
await fetch('/social/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ receiverId: activePartnerId, text })
|
||||
});
|
||||
await loadThread();
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
document.getElementById('msgInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); }
|
||||
});
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = setInterval(loadThread, 10000);
|
||||
}
|
||||
|
||||
// Mobile: show/hide panes
|
||||
function showThread() {
|
||||
document.getElementById('convListPane').classList.add('hidden');
|
||||
document.getElementById('threadPane').classList.remove('hidden');
|
||||
}
|
||||
function showList() {
|
||||
document.getElementById('convListPane').classList.remove('hidden');
|
||||
document.getElementById('threadPane').classList.add('hidden');
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||
activePartnerId = null;
|
||||
}
|
||||
|
||||
// ── Lightbox ──
|
||||
function openLightbox(src) {
|
||||
document.getElementById('lightboxImg').src = src;
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
}
|
||||
function closeLightbox() {
|
||||
document.getElementById('lightbox').classList.remove('open');
|
||||
}
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeLightbox();
|
||||
});
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ── Emoji-Picker ──
|
||||
const EMOJIS = [
|
||||
'😀','😂','🤣','😅','😊','😍','🥰','😘','😎','🤩',
|
||||
'😉','🙂','😐','😏','😒','😢','😭','😤','😡','🤬',
|
||||
'🤔','🥺','😳','🤭','😈','💀','🙈','🙉','🙊','😴',
|
||||
'👍','👎','👏','🙏','💪','🫶','🤗','🫠','✌️','🤞',
|
||||
'❤️','🧡','💛','💚','💙','💜','🖤','💕','💖','💘',
|
||||
'🔥','✨','⚡','🌟','💯','🎉','🎊','🎀','🌹','🌸',
|
||||
'🍕','🍔','🍟','🌮','🍜','🍣','🍩','🍪','☕','🍷',
|
||||
'🐱','🐶','🦊','🐼','🐨','🐸','🦄','🐝','🦋','🐬',
|
||||
];
|
||||
|
||||
(function initEmojis() {
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
EMOJIS.forEach(emoji => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = emoji;
|
||||
btn.title = emoji;
|
||||
btn.onclick = () => {
|
||||
const input = document.getElementById('msgInput');
|
||||
const pos = input.selectionStart ?? input.value.length;
|
||||
const val = input.value;
|
||||
input.value = val.slice(0, pos) + emoji + val.slice(pos);
|
||||
input.selectionStart = input.selectionEnd = pos + [...emoji].length;
|
||||
input.focus();
|
||||
};
|
||||
picker.appendChild(btn);
|
||||
});
|
||||
})();
|
||||
|
||||
function toggleEmoji() {
|
||||
document.getElementById('emojiPicker').classList.toggle('open');
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('#emojiPicker') && !e.target.closest('#emojiBtn')) {
|
||||
document.getElementById('emojiPicker').classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Bild-Upload ──
|
||||
(function attachImgHandler() {
|
||||
document.getElementById('imgFile').addEventListener('change', async function () {
|
||||
const file = this.files[0];
|
||||
if (!file || !activePartnerId) return;
|
||||
// Frisches Input-Element einsetzen, damit jedes weitere Bild (auch dasselbe) wählbar bleibt
|
||||
const fresh = this.cloneNode(false);
|
||||
this.replaceWith(fresh);
|
||||
attachImgHandler();
|
||||
try {
|
||||
const dataUrl = await resizeImage(file);
|
||||
await fetch('/social/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ receiverId: activePartnerId, text: dataUrl })
|
||||
});
|
||||
await loadThread();
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
})();
|
||||
|
||||
function resizeImage(file) {
|
||||
const MAX = 800;
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(file);
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > MAX || h > MAX) {
|
||||
if (w >= h) { h = Math.max(1, Math.round(MAX * h / w)); w = MAX; }
|
||||
else { w = Math.max(1, Math.round(MAX * w / h)); h = MAX; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.72));
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
204
xxxthegame/src/main/resources/static/personen-suchen.html
Normal file
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Personen suchen – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.search-bar input { flex: 1; }
|
||||
.search-bar button { width: auto; margin-top: 0; padding: 0.65rem 1.25rem; }
|
||||
|
||||
.user-list { list-style: none; margin: 0; padding: 0; }
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
.user-item:last-child { border-bottom: none; }
|
||||
|
||||
.user-avatar {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-secondary);
|
||||
}
|
||||
.user-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.user-name { font-weight: 600; flex: 1; }
|
||||
|
||||
.user-actions { display: flex; gap: 0.5rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.user-actions button, .user-actions a.btn {
|
||||
margin-top: 0;
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.user-profile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.user-profile-link:hover .user-name { color: var(--color-primary); }
|
||||
|
||||
.hint { color: var(--color-muted); font-size: 0.9rem; margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1 style="margin-bottom: 1.25rem;">Personen suchen</h1>
|
||||
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Name eingeben (mind. 2 Zeichen)…" autocomplete="off">
|
||||
<button onclick="doSearch()">Suchen</button>
|
||||
</div>
|
||||
|
||||
<ul class="user-list" id="resultList"></ul>
|
||||
<p class="hint" id="hint">Gib mindestens 2 Zeichen ein, um zu suchen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let debounceTimer;
|
||||
|
||||
document.getElementById('searchInput').addEventListener('input', function () {
|
||||
clearTimeout(debounceTimer);
|
||||
const q = this.value.trim();
|
||||
if (q.length < 2) {
|
||||
document.getElementById('resultList').innerHTML = '';
|
||||
document.getElementById('hint').textContent = 'Gib mindestens 2 Zeichen ein, um zu suchen.';
|
||||
document.getElementById('hint').style.display = '';
|
||||
return;
|
||||
}
|
||||
document.getElementById('hint').style.display = 'none';
|
||||
debounceTimer = setTimeout(doSearch, 400);
|
||||
});
|
||||
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { clearTimeout(debounceTimer); doSearch(); }
|
||||
});
|
||||
|
||||
async function doSearch() {
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
if (q.length < 2) return;
|
||||
try {
|
||||
const res = await fetch('/social/users/search?q=' + encodeURIComponent(q));
|
||||
if (!res.ok) return;
|
||||
renderResults(await res.json());
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderResults(users) {
|
||||
const list = document.getElementById('resultList');
|
||||
const hint = document.getElementById('hint');
|
||||
list.innerHTML = '';
|
||||
if (users.length === 0) {
|
||||
hint.textContent = 'Keine Ergebnisse gefunden.';
|
||||
hint.style.display = '';
|
||||
return;
|
||||
}
|
||||
hint.style.display = 'none';
|
||||
users.forEach(u => {
|
||||
const avatar = u.profilePicture
|
||||
? `<img src="data:image/png;base64,${u.profilePicture}" alt="">`
|
||||
: '◉';
|
||||
list.insertAdjacentHTML('beforeend', `
|
||||
<li class="user-item" data-user-id="${u.userId}">
|
||||
<a href="/benutzer.html?userId=${u.userId}" class="user-profile-link">
|
||||
<div class="user-avatar">${avatar}</div>
|
||||
<div class="user-name">${esc(u.name)}</div>
|
||||
</a>
|
||||
<div class="user-actions">${buildActions(u)}</div>
|
||||
</li>`);
|
||||
});
|
||||
}
|
||||
|
||||
function buildActions(u) {
|
||||
if (u.friendStatus === 'FRIEND') {
|
||||
return `<a href="/nachrichten.html?userId=${u.userId}" class="btn" style="background:var(--color-secondary); color:var(--color-text);">✉ Nachricht</a>`;
|
||||
}
|
||||
if (u.friendStatus === 'PENDING_SENT') {
|
||||
return `<button disabled>Anfrage gesendet</button>`;
|
||||
}
|
||||
if (u.friendStatus === 'PENDING_RECEIVED') {
|
||||
return `<button onclick="acceptByUserId('${u.userId}', this)">✓ Annehmen</button>`;
|
||||
}
|
||||
return `<button onclick="sendRequest('${u.userId}', this)">+ Freund hinzufügen</button>`;
|
||||
}
|
||||
|
||||
async function sendRequest(receiverId, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird gesendet…';
|
||||
try {
|
||||
const res = await fetch('/social/friends/request', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ receiverId })
|
||||
});
|
||||
btn.textContent = (res.ok || res.status === 201 || res.status === 409) ? 'Anfrage gesendet' : 'Fehler';
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '+ Freund hinzufügen';
|
||||
}
|
||||
}
|
||||
|
||||
async function acceptByUserId(senderId, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const pendingRes = await fetch('/social/friends/pending');
|
||||
const pending = await pendingRes.json();
|
||||
const f = pending.find(p => p.user.userId === senderId);
|
||||
if (!f) { btn.textContent = 'Fehler'; return; }
|
||||
|
||||
const res = await fetch('/social/friends/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ friendshipId: f.friendshipId })
|
||||
});
|
||||
if (res.ok) {
|
||||
btn.textContent = '✓ Freund';
|
||||
const item = btn.closest('.user-item');
|
||||
if (item) {
|
||||
item.querySelector('.user-actions').innerHTML =
|
||||
`<a href="/nachrichten.html?userId=${senderId}" class="btn" style="background:var(--color-secondary); color:var(--color-text);">✉ Nachricht</a>`;
|
||||
}
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✓ Annehmen';
|
||||
}
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✓ Annehmen';
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
646
xxxthegame/src/main/resources/static/profile.html
Normal file
@@ -0,0 +1,646 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Profil – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.profile-picture-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid var(--color-secondary);
|
||||
background: var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: var(--color-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-picture img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-field {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.profile-field label {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.profile-field-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-field-row p {
|
||||
flex: 1;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
background: var(--color-secondary);
|
||||
font-size: 1rem;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-field-row button {
|
||||
flex-shrink: 0;
|
||||
width: 175px;
|
||||
padding: 0.65rem 0.75rem;
|
||||
margin-top: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
/* ── Gallery ── */
|
||||
.gallery-section-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 1.75rem 0 0.75rem;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.gallery-upload-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#ownGallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.own-thumb {
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
background: var(--color-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.own-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.own-thumb-delete {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: rgba(0,0,0,0.6);
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.own-thumb:hover .own-thumb-delete { display: flex; }
|
||||
.own-thumb-delete:hover { background: rgba(180,30,30,0.85); }
|
||||
|
||||
.btn-delete-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
width: 175px;
|
||||
padding: 0.65rem 0.75rem;
|
||||
margin-top: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.85rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #3d0f1a;
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── 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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="main">
|
||||
<div class="content" style="max-width: 600px;">
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="gallery-section-label">Meine Bilder</div>
|
||||
<div class="gallery-upload-row">
|
||||
<input type="file" id="galleryFile" accept="image/*" multiple style="display:none;" onchange="handleGalleryUpload(this.files)">
|
||||
<button onclick="document.getElementById('galleryFile').click()">+ Bilder hochladen</button>
|
||||
<span id="galleryUploadStatus" style="font-size:0.85rem;color:var(--color-muted);"></span>
|
||||
</div>
|
||||
<div id="ownGallery"></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>
|
||||
|
||||
<!-- 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/sidebar.js"></script>
|
||||
<script>
|
||||
let currentPicture = null;
|
||||
let currentPictureHq = null;
|
||||
|
||||
fetch('/login/me')
|
||||
.then(r => {
|
||||
if (r.status === 401) { window.location.href = '/login.html'; return null; }
|
||||
return r.json();
|
||||
})
|
||||
.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);
|
||||
}
|
||||
myUserId = user.userId;
|
||||
loadOwnGallery();
|
||||
})
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// Check if redirected back after email change
|
||||
if (new URLSearchParams(window.location.search).get('emailChanged')) {
|
||||
// Already handled by login.html redirect — nothing to do here
|
||||
}
|
||||
|
||||
document.getElementById('picFile').addEventListener('change', async e => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
[currentPicture, currentPictureHq] = await Promise.all([toBase64(file, 96), toBase64(file, 1024)]);
|
||||
renderPicture(currentPicture);
|
||||
});
|
||||
|
||||
function renderPicture(base64) {
|
||||
const el = document.getElementById('profilePicDisplay');
|
||||
el.innerHTML = `<img src="data:image/png;base64,${base64}" alt="Profilbild">`;
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
const btn = document.getElementById('saveBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird gespeichert…';
|
||||
hideMessage();
|
||||
|
||||
try {
|
||||
const response = await fetch('/user/me/picture', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ picture: currentPicture, pictureHq: currentPictureHq })
|
||||
});
|
||||
if (response.ok) {
|
||||
window.location.href = '/userhome.html';
|
||||
} else {
|
||||
showMessage(`Fehler: HTTP ${response.status}`, 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Profil speichern';
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage('Server nicht erreichbar.', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Profil speichern';
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
|
||||
async function loadOwnGallery() {
|
||||
if (!myUserId) return;
|
||||
const res = await fetch('/social/profile-images?userId=' + myUserId);
|
||||
if (!res.ok) return;
|
||||
const images = await res.json();
|
||||
renderOwnGallery(images);
|
||||
}
|
||||
|
||||
function renderOwnGallery(images) {
|
||||
const grid = document.getElementById('ownGallery');
|
||||
if (images.length === 0) {
|
||||
grid.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;grid-column:1/-1;">Noch keine Bilder hochgeladen.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = images.map(img => `
|
||||
<div class="own-thumb">
|
||||
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galerie-Bild">
|
||||
<button class="own-thumb-delete" onclick="deleteGalleryImage('${img.imageId}', event)" title="Bild löschen">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function deleteGalleryImage(imageId, event) {
|
||||
event.stopPropagation();
|
||||
const res = await fetch('/social/profile-images/' + imageId, { method: 'DELETE' });
|
||||
if (res.ok || res.status === 204) loadOwnGallery();
|
||||
}
|
||||
|
||||
async function handleGalleryUpload(files) {
|
||||
if (!files || files.length === 0) return;
|
||||
const status = document.getElementById('galleryUploadStatus');
|
||||
status.textContent = '0 / ' + files.length + ' hochgeladen…';
|
||||
let done = 0;
|
||||
for (const file of Array.from(files)) {
|
||||
try {
|
||||
const base64 = await toJpeg(file, 1024);
|
||||
const res = await fetch('/social/profile-images', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ imageData: base64 })
|
||||
});
|
||||
if (res.status === 422) {
|
||||
status.textContent = 'Limit von 20 Bildern erreicht.';
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload-Fehler:', e);
|
||||
}
|
||||
done++;
|
||||
status.textContent = done + ' / ' + files.length + ' hochgeladen…';
|
||||
}
|
||||
status.textContent = done + ' Bild' + (done !== 1 ? 'er' : '') + ' hochgeladen.';
|
||||
document.getElementById('galleryFile').value = '';
|
||||
loadOwnGallery();
|
||||
}
|
||||
|
||||
function toJpeg(file, max) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(file);
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > max || h > max) {
|
||||
if (w >= h) { h = Math.max(1, Math.round(max * h / w)); w = max; }
|
||||
else { w = Math.max(1, Math.round(max * w / h)); h = max; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.85).split(',')[1]);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function toBase64(file, max) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(file);
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
let w = img.naturalWidth, h = img.naturalHeight;
|
||||
if (w > max || h > max) {
|
||||
if (w >= h) { h = Math.max(1, Math.round(max * h / w)); w = max; }
|
||||
else { w = Math.max(1, Math.round(max * w / h)); h = max; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||
resolve(canvas.toDataURL('image/png').split(',')[1]);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
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 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>
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>XXX The Game – Registrierung</title>
|
||||
<title>xXx Games – Neues Konto erstellen</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>XXX The Game</h1>
|
||||
<p class="subtitle">Neues Konto erstellen</p>
|
||||
<img src="icon.png" alt="Logo">
|
||||
<h1>Neues Konto erstellen</h1>
|
||||
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" placeholder="Dein Name" autocomplete="name" />
|
||||
@@ -84,6 +84,10 @@
|
||||
showMessage('Diese E-Mail-Adresse ist bereits registriert.', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Registrieren';
|
||||
} else if (response.status === 409) {
|
||||
showMessage('Dieser Name ist bereits vergeben.', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Registrieren';
|
||||
} else {
|
||||
showMessage(`Fehler: HTTP ${response.status}`, 'error');
|
||||
btn.disabled = false;
|
||||
|
||||
150
xxxthegame/src/main/resources/static/reset-password.html
Normal file
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>xXx Games – Neues Passwort</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
.modal {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
max-width: 340px;
|
||||
width: 90%;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
.modal p {
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<img src="icon.png" alt="Logo">
|
||||
<h1>Neues Passwort</h1>
|
||||
<p class="subtitle">Gib dein neues Passwort ein.</p>
|
||||
|
||||
<label for="password">Neues Passwort</label>
|
||||
<input type="password" id="password" placeholder="••••••••" autocomplete="new-password" />
|
||||
|
||||
<label for="passwordConfirm">Passwort wiederholen</label>
|
||||
<input type="password" id="passwordConfirm" placeholder="••••••••" autocomplete="new-password" />
|
||||
|
||||
<button class="full-width" id="submitBtn" onclick="submit()">Passwort speichern</button>
|
||||
|
||||
<div class="message" id="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="overlay">
|
||||
<div class="modal">
|
||||
<p>Dein Passwort wurde erfolgreich geändert. Du kannst dich jetzt anmelden.</p>
|
||||
<button class="full-width" onclick="goToLogin()">Zum Login</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') submit();
|
||||
});
|
||||
|
||||
function getToken() {
|
||||
return new URLSearchParams(window.location.search).get('token');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!getToken()) {
|
||||
showMessage('Ungültiger oder fehlender Reset-Link.', 'error');
|
||||
document.getElementById('submitBtn').disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
async function sha256(text) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const password = document.getElementById('password').value;
|
||||
const passwordConfirm = document.getElementById('passwordConfirm').value;
|
||||
const btn = document.getElementById('submitBtn');
|
||||
const token = getToken();
|
||||
|
||||
if (!password || !passwordConfirm) {
|
||||
showMessage('Bitte beide Felder ausfüllen.', 'error');
|
||||
return;
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
showMessage('Die Passwörter stimmen nicht überein.', 'error');
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
showMessage('Ungültiger Reset-Link.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird gespeichert…';
|
||||
hideMessage();
|
||||
|
||||
try {
|
||||
const passwordHash = await sha256(password);
|
||||
const response = await fetch('/password-reset/confirm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, passwordHash })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('overlay').classList.add('active');
|
||||
} else {
|
||||
showMessage('Der Reset-Link ist ungültig oder abgelaufen.', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Passwort speichern';
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage('Server nicht erreichbar.', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Passwort speichern';
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function goToLogin() {
|
||||
window.location.href = '/login.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';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
297
xxxthegame/src/main/resources/static/sessionbdsm.html
Normal file
@@ -0,0 +1,297 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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; }
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.setting-row { margin-bottom: 1.25rem; }
|
||||
.setting-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.setting-header label {
|
||||
font-size: 0.85rem;
|
||||
color: #aaa;
|
||||
margin: 0;
|
||||
}
|
||||
.setting-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
min-width: 3.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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: 2rem;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.modal-text {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.modal-actions { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.modal-actions button { width: 100%; padding: 0.75rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="modal-overlay" id="modal" style="display:none;">
|
||||
<div class="modal-card">
|
||||
<div class="modal-title" id="modalTitle"></div>
|
||||
<div class="modal-text" id="modalText"></div>
|
||||
<div class="modal-actions" id="modalActions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content session-setup">
|
||||
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Schritt 1 von 4 – Session-Einstellungen</p>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Session-Einstellungen</h2>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldStrafe">Wahrscheinlichkeit Strafe</label>
|
||||
<span class="setting-value"><span id="valStrafe">15</span> %</span>
|
||||
</div>
|
||||
<input type="range" id="sldStrafe" min="0" max="100" value="15"
|
||||
oninput="document.getElementById('valStrafe').textContent=this.value; updateWarnung()">
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldZeitstrafe">Wahrscheinlichkeit Zeitstrafe</label>
|
||||
<span class="setting-value"><span id="valZeitstrafe">15</span> %</span>
|
||||
</div>
|
||||
<input type="range" id="sldZeitstrafe" min="0" max="100" value="15"
|
||||
oninput="document.getElementById('valZeitstrafe').textContent=this.value; updateWarnung()">
|
||||
</div>
|
||||
|
||||
<div class="message" id="wahrschWarnung" style="display:none; margin-top:0.75rem;"></div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldAufgaben">Aufgaben pro Level</label>
|
||||
<span class="setting-value" id="valAufgaben">5</span>
|
||||
</div>
|
||||
<input type="range" id="sldAufgaben" min="4" max="20" value="5"
|
||||
oninput="document.getElementById('valAufgaben').textContent=this.value">
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-header">
|
||||
<label for="sldZeit">Zeitfaktor Zeitstrafen</label>
|
||||
<span class="setting-value" id="valZeit">1,0</span>
|
||||
</div>
|
||||
<input type="range" id="sldZeit" min="5" max="20" value="10"
|
||||
oninput="document.getElementById('valZeit').textContent=(this.value/10).toFixed(1).replace('.',',')">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message" id="message"></div>
|
||||
<button class="full-width" onclick="weiter()">Weiter</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
function updateWarnung() {
|
||||
const strafe = parseInt(document.getElementById('sldStrafe').value);
|
||||
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
|
||||
const summe = strafe + zeitstrafe;
|
||||
const el = document.getElementById('wahrschWarnung');
|
||||
if (summe > 98) {
|
||||
el.textContent = `Kombiniert ${summe} % – Werte über 98 % sind nicht möglich.`;
|
||||
el.className = 'message error';
|
||||
el.style.display = 'block';
|
||||
} else if (summe > 60) {
|
||||
el.textContent = `Hinweis: Bei ${summe} % kombinierten Wahrscheinlichkeiten ist die Chance auf Vanilla-Aufgaben sehr gering.`;
|
||||
el.className = 'message warning';
|
||||
el.style.display = 'block';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function weiter() {
|
||||
hideMessage();
|
||||
const strafe = parseInt(document.getElementById('sldStrafe').value);
|
||||
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
|
||||
if (strafe + zeitstrafe > 98) {
|
||||
showMessage('Die kombinierten Wahrscheinlichkeiten dürfen 98 % nicht überschreiten.', 'error');
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem('bdsm-session-settings', JSON.stringify({
|
||||
wahrscheinlichkeitStrafe: strafe,
|
||||
wahrscheinlichkeitSperre: zeitstrafe,
|
||||
aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value),
|
||||
zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10,
|
||||
}));
|
||||
window.location.href = '/sessionbdsmplayers.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';
|
||||
}
|
||||
|
||||
// ── Aktive-Session-Check ──
|
||||
function zeigeModal(title, text, actions) {
|
||||
document.getElementById('modalTitle').textContent = title;
|
||||
const textEl = document.getElementById('modalText');
|
||||
textEl.textContent = text;
|
||||
textEl.style.display = text ? '' : 'none';
|
||||
const actEl = document.getElementById('modalActions');
|
||||
actEl.innerHTML = '';
|
||||
actions.forEach(a => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = a.label;
|
||||
btn.className = a.primary ? 'full-width' : 'full-width secondary';
|
||||
btn.onclick = () => a.onClick();
|
||||
actEl.appendChild(btn);
|
||||
});
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function versteckeModal() {
|
||||
document.getElementById('modal').style.display = 'none';
|
||||
}
|
||||
|
||||
const BDSM_STORAGE_KEYS = [
|
||||
'bdsm-session-id', 'bdsm-session-settings', 'bdsm-session-setup',
|
||||
'bdsm-session-gruppen', 'bdsm-session-toys', 'bdsm-session-game',
|
||||
];
|
||||
|
||||
function sessionFortfahren(sid) {
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
sessionStorage.setItem('bdsm-session-id', sid);
|
||||
window.location.href = '/sessionbdsmingame.html';
|
||||
}
|
||||
|
||||
function sessionBeendenFragen(sid) {
|
||||
zeigeModal(
|
||||
'Session wirklich beenden?',
|
||||
'Die Session und alle aktiven Sperren werden gelöscht.',
|
||||
[
|
||||
{ label: 'Ja, beenden', primary: true, onClick: () => sessionLoeschen(sid) },
|
||||
{ label: 'Nein, fortfahren', onClick: () => sessionFortfahren(sid) },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async function sessionLoeschen(sid) {
|
||||
versteckeModal();
|
||||
try {
|
||||
await fetch('/session', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId: sid }),
|
||||
});
|
||||
} catch (_) { /* ignorieren */ }
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
}
|
||||
|
||||
(async function checkAktiveSession() {
|
||||
try {
|
||||
const meRes = await fetch('/login/me');
|
||||
if (!meRes.ok) return;
|
||||
const user = await meRes.json();
|
||||
|
||||
const sessionRes = await fetch(`/session?userId=${user.userId}`);
|
||||
if (sessionRes.status === 204) return;
|
||||
if (!sessionRes.ok) return;
|
||||
|
||||
const session = await sessionRes.json();
|
||||
zeigeModal(
|
||||
'Aktive Session vorhanden',
|
||||
'Du hast noch eine laufende Session. Möchtest du fortfahren?',
|
||||
[
|
||||
{ label: 'Ja, fortfahren', primary: true, onClick: () => sessionFortfahren(session.sessionId) },
|
||||
{ label: 'Nein', onClick: () => sessionBeendenFragen(session.sessionId) },
|
||||
]
|
||||
);
|
||||
} catch (_) { /* ignorieren */ }
|
||||
})();
|
||||
|
||||
// Gespeicherte Einstellungen wiederherstellen
|
||||
(function restore() {
|
||||
const saved = sessionStorage.getItem('bdsm-session-settings');
|
||||
if (!saved) return;
|
||||
const s = JSON.parse(saved);
|
||||
|
||||
function setSlider(id, displayId, value, transform) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.value = value;
|
||||
document.getElementById(displayId).textContent = transform ? transform(value) : value;
|
||||
}
|
||||
|
||||
setSlider('sldStrafe', 'valStrafe', s.wahrscheinlichkeitStrafe);
|
||||
setSlider('sldZeitstrafe', 'valZeitstrafe', s.wahrscheinlichkeitSperre);
|
||||
setSlider('sldAufgaben', 'valAufgaben', s.aufgabenProLevel);
|
||||
setSlider('sldZeit', 'valZeit', Math.round(s.zeitfaktorZeitstrafen * 10),
|
||||
v => (v / 10).toFixed(1).replace('.', ','));
|
||||
|
||||
updateWarnung();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
588
xxxthegame/src/main/resources/static/sessionbdsmingame.html
Normal file
@@ -0,0 +1,588 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BDSM Game – Im Spiel – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.game-overview { max-width: 700px; }
|
||||
|
||||
.overview-section { margin-bottom: 2rem; }
|
||||
.overview-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.kv-table { width: 100%; border-collapse: collapse; }
|
||||
.kv-table td { padding: 0.35rem 0; font-size: 0.9rem; vertical-align: top; }
|
||||
.kv-table td:first-child { color: var(--color-muted); width: 200px; }
|
||||
|
||||
.player-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.player-name { font-weight: 600; margin-bottom: 0.4rem; }
|
||||
.player-sub { font-size: 0.8rem; color: var(--color-muted); }
|
||||
|
||||
.tag-list { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.25rem; }
|
||||
.tag {
|
||||
background: var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
padding: 0.2rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.count-row { display: flex; gap: 2.5rem; }
|
||||
.count-item { text-align: center; }
|
||||
.count-num { font-size: 2rem; font-weight: 700; color: var(--color-primary); }
|
||||
.count-label { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.1rem; }
|
||||
|
||||
/* ── 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: 2rem 2rem 1.75rem;
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.modal-text {
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-muted);
|
||||
line-height: 1.65;
|
||||
margin-bottom: 1.5rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-actions button { min-width: 110px; }
|
||||
|
||||
/* ── Aufgaben-Karte ── */
|
||||
.task-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.task-card.loading {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.task-player-badge {
|
||||
display: inline-block;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.7rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.task-text {
|
||||
flex: 1;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
cursor: default;
|
||||
}
|
||||
.task-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.task-btns {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
.task-timer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.timer-big {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
min-width: 3.5rem;
|
||||
}
|
||||
.timer-big.expired { color: var(--color-muted); }
|
||||
.btn-sm-cancel {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-weight: normal;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn-sm-cancel:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
background: transparent;
|
||||
}
|
||||
.btn-session-beenden {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.78rem;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.btn-session-beenden:hover {
|
||||
color: var(--color-text);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ── Level-Anzeige ── */
|
||||
.level-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.level-display img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<!-- ── Modal ── -->
|
||||
<div class="modal-overlay" id="modal" style="display:none;">
|
||||
<div class="modal-card">
|
||||
<div class="modal-title" id="modalTitle"></div>
|
||||
<div class="modal-text" id="modalText"></div>
|
||||
<div class="modal-actions" id="modalActions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content game-overview">
|
||||
|
||||
<!-- ── Level ── -->
|
||||
<div class="level-display" id="levelDisplay" style="display:none;">
|
||||
<img id="levelImg" src="" alt="Level">
|
||||
</div>
|
||||
|
||||
<!-- ── Aufgabe ── -->
|
||||
<div style="margin-bottom:2rem;">
|
||||
<div class="task-card loading" id="taskCard">Aufgabe wird geladen…</div>
|
||||
<div class="message" id="taskMessage" style="display:none; margin-top:0.75rem;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const game = JSON.parse(sessionStorage.getItem('bdsm-session-game') || 'null');
|
||||
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');
|
||||
|
||||
// ── Modal ──
|
||||
function zeigeModal(title, text, actions) {
|
||||
document.getElementById('modalTitle').textContent = title;
|
||||
const textEl = document.getElementById('modalText');
|
||||
textEl.textContent = text;
|
||||
textEl.style.display = text ? '' : 'none';
|
||||
const actEl = document.getElementById('modalActions');
|
||||
actEl.innerHTML = '';
|
||||
actions.forEach(a => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = a.label;
|
||||
if (!a.primary) btn.classList.add('secondary');
|
||||
btn.onclick = () => { versteckeModal(); a.onClick(); };
|
||||
actEl.appendChild(btn);
|
||||
});
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function versteckeModal() {
|
||||
document.getElementById('modal').style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Hilfsfunktionen ──
|
||||
function escapeAttr(str) {
|
||||
return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function badgeHtml(name) {
|
||||
return name ? `<div class="task-player-badge">${name} ist dran</div>` : '';
|
||||
}
|
||||
|
||||
const SESSION_BEENDEN_BTN = `<button class="btn-session-beenden" onclick="sessionBeendenFragen()">Session beenden</button>`;
|
||||
|
||||
// ── Aufgaben-Logik ──
|
||||
let currentTask = null;
|
||||
let timerInterval = null;
|
||||
|
||||
function formatTime(sec) {
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = sec % 60;
|
||||
return `${m}:${String(s).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||
}
|
||||
|
||||
function zeigeTaskFehler(text) {
|
||||
const el = document.getElementById('taskMessage');
|
||||
el.textContent = text;
|
||||
el.className = 'message error';
|
||||
el.style.display = '';
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card';
|
||||
card.innerHTML = `
|
||||
<div style="flex:1;"></div>
|
||||
<div class="task-footer">
|
||||
<div class="task-btns">
|
||||
<button onclick="ladeAufgabe()">Erneut versuchen</button>
|
||||
</div>
|
||||
${SESSION_BEENDEN_BTN}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function ladeAufgabe() {
|
||||
clearTimer();
|
||||
document.getElementById('taskMessage').style.display = 'none';
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = 'Aufgabe wird geladen…';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/session/${sessionId}/aufgaben/next`);
|
||||
if (res.status === 204) { zeigeFinaleDialog(); return; }
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
currentTask = await res.json();
|
||||
if (currentTask.level) {
|
||||
document.getElementById('levelImg').src = `/img/lvl${currentTask.level}.png`;
|
||||
document.getElementById('levelDisplay').style.display = '';
|
||||
}
|
||||
const name = currentTask.nameAktiverMitspieler || '';
|
||||
const title = name ? `${name}, du bist an der Reihe` : 'Du bist an der Reihe';
|
||||
zeigeModal(title, '', [{ label: 'OK', primary: true, onClick: zeigeAufgabe }]);
|
||||
} catch (e) {
|
||||
zeigeTaskFehler('Aufgabe konnte nicht geladen werden: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeAufgabe() {
|
||||
const task = currentTask;
|
||||
const cb = task.callback;
|
||||
if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
|
||||
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
|
||||
else if (task.timer != null) zeigeTimerAufgabe(task);
|
||||
else zeigeEinfacheAufgabe(task);
|
||||
}
|
||||
|
||||
function renderCard(task, footerInner) {
|
||||
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-btns" id="taskActions">${footerInner}</div>
|
||||
${SESSION_BEENDEN_BTN}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function zeigeSperreAufgabe(task) {
|
||||
renderCard(task, `
|
||||
<button onclick="sperreAnwenden()">Zeitstrafe anwenden</button>
|
||||
<button class="secondary" onclick="gnadeErweisen()">Gnade erweisen</button>`);
|
||||
}
|
||||
|
||||
function gnadeErweisen() {
|
||||
const name = currentTask?.nameAktiverMitspieler;
|
||||
const title = name ? `${name}, erweist du wirklich Gnade?` : 'Wirklich Gnade erweisen?';
|
||||
zeigeModal(title, 'Die Zeitstrafe wird nicht eingetragen und das Spiel geht weiter.', [
|
||||
{ label: 'Ja, Gnade erweisen', primary: true, onClick: aufgabeAbgeschlossen },
|
||||
{ label: 'Nein', onClick: versteckeModal },
|
||||
]);
|
||||
}
|
||||
|
||||
function zeigeVerlaengernAufgabe(task) {
|
||||
renderCard(task, `
|
||||
<button onclick="sperrenVerlaengern()">Ja, verlängern</button>
|
||||
<button class="secondary" onclick="aufgabeAbgeschlossen()">Nein</button>`);
|
||||
}
|
||||
|
||||
function zeigeTimerAufgabe(task) {
|
||||
renderCard(task, `<button onclick="timerStarten()">Starten</button>`);
|
||||
}
|
||||
|
||||
function timerStarten() {
|
||||
const task = currentTask;
|
||||
const actions = document.getElementById('taskActions');
|
||||
let remaining = task.timer;
|
||||
actions.className = 'task-timer-row';
|
||||
actions.innerHTML = `
|
||||
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
|
||||
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>`;
|
||||
|
||||
timerInterval = setInterval(() => {
|
||||
remaining--;
|
||||
const el = document.getElementById('timerValue');
|
||||
if (remaining <= 0) {
|
||||
clearTimer();
|
||||
if (el) { el.textContent = formatTime(0); el.classList.add('expired'); }
|
||||
aufgabeAbgeschlossen();
|
||||
} else {
|
||||
if (el) el.textContent = formatTime(remaining);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function timerAbbrechen() {
|
||||
clearTimer();
|
||||
aufgabeAbgeschlossen();
|
||||
}
|
||||
|
||||
function zeigeEinfacheAufgabe(task) {
|
||||
renderCard(task, `<button onclick="aufgabeAbgeschlossen()">Erledigt</button>`);
|
||||
}
|
||||
|
||||
async function aufgabeAbgeschlossen() {
|
||||
clearTimer();
|
||||
try {
|
||||
const res = await fetch(`/session/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);
|
||||
if (texte.length > 0) { zeigeAbgelaufeneSperre(texte, 0); return; }
|
||||
}
|
||||
} catch (_) { /* ignorieren */ }
|
||||
ladeAufgabe();
|
||||
}
|
||||
|
||||
function zeigeAbgelaufeneSperre(texte, index) {
|
||||
if (index >= texte.length) { ladeAufgabe(); return; }
|
||||
zeigeModal(
|
||||
'Zeitstrafe abgelaufen',
|
||||
texte[index],
|
||||
[{ label: 'OK', primary: true, onClick: () => zeigeAbgelaufeneSperre(texte, index + 1) }]
|
||||
);
|
||||
}
|
||||
|
||||
async function sperreAnwenden() {
|
||||
const cb = currentTask?.callback;
|
||||
if (!cb) return;
|
||||
try {
|
||||
const res = await fetch('/session/sperre', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...cb, sessionId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
aufgabeAbgeschlossen();
|
||||
} catch (e) {
|
||||
zeigeTaskFehler('Zeitstrafe konnte nicht verhängt werden: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sperrenVerlaengern() {
|
||||
const cb = currentTask?.callback;
|
||||
if (!cb) return;
|
||||
try {
|
||||
const res = await fetch('/session/sperre/verlaengern', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...cb, sessionId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
aufgabeAbgeschlossen();
|
||||
} catch (e) {
|
||||
zeigeTaskFehler('Sperren verlängern fehlgeschlagen: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Finale ──
|
||||
let _finisherListe = [];
|
||||
let _finisherIndex = 0;
|
||||
|
||||
function zeigeFinaleDialog() {
|
||||
clearTimer();
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = 'Level 5 abgeschlossen…';
|
||||
zeigeModal(
|
||||
'Level 5 abgeschlossen!',
|
||||
'Seid ihr bereit für das große Finale?',
|
||||
[
|
||||
{ label: 'Ja, Finale!', primary: true, onClick: starteFinale },
|
||||
{ label: 'Nein, weiter spielen', onClick: zurueckZuLevel5 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async function zurueckZuLevel5() {
|
||||
try {
|
||||
await fetch(`/session/${sessionId}/backToLevel5`, { method: 'POST' });
|
||||
} catch (_) {}
|
||||
ladeAufgabe();
|
||||
}
|
||||
|
||||
async function starteFinale() {
|
||||
try {
|
||||
const res = await fetch(`/session/sperre/aktive?sessionId=${sessionId}`);
|
||||
if (res.ok) {
|
||||
const sperren = await res.json();
|
||||
const texte = (sperren || []).map(s => s.releaseText).filter(t => t);
|
||||
if (texte.length > 0) { zeigeFinaleSperre(texte, 0); return; }
|
||||
}
|
||||
} catch (_) {}
|
||||
ladeFinisher();
|
||||
}
|
||||
|
||||
function zeigeFinaleSperre(texte, index) {
|
||||
if (index >= texte.length) { ladeFinisher(); return; }
|
||||
zeigeModal(
|
||||
'Zeitstrafe aufgelöst',
|
||||
texte[index],
|
||||
[{ label: 'OK', primary: true, onClick: () => zeigeFinaleSperre(texte, index + 1) }]
|
||||
);
|
||||
}
|
||||
|
||||
async function ladeFinisher() {
|
||||
try {
|
||||
const res = await fetch(`/session/${sessionId}/finisher`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const liste = await res.json();
|
||||
naechsterFinisher(liste, 0);
|
||||
} catch (e) {
|
||||
zeigeTaskFehler('Finisher konnten nicht geladen werden: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function naechsterFinisher(liste, index) {
|
||||
_finisherListe = liste;
|
||||
_finisherIndex = index;
|
||||
if (index >= liste.length) {
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = '';
|
||||
zeigeModal(
|
||||
'Das Finale ist abgeschlossen!',
|
||||
'Wir hoffen, ihr hattet viel Spaß! 🎉',
|
||||
[{ label: 'Session beenden', primary: true, onClick: sessionLoeschen }]
|
||||
);
|
||||
return;
|
||||
}
|
||||
const finisher = liste[index];
|
||||
const name = finisher.nameAktiverMitspieler || '';
|
||||
zeigeModal(
|
||||
name ? `${name} ist dran` : 'Finale',
|
||||
'',
|
||||
[{ label: 'OK', primary: true, onClick: zeigeFinisherAufgabe }]
|
||||
);
|
||||
}
|
||||
|
||||
function zeigeFinisherAufgabe() {
|
||||
const finisher = _finisherListe[_finisherIndex];
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card';
|
||||
card.innerHTML = `
|
||||
${badgeHtml(finisher.nameAktiverMitspieler)}
|
||||
<div class="task-text" title="${escapeAttr(finisher.aufgabeText)}">${finisher.aufgabeText}</div>
|
||||
<div class="task-footer">
|
||||
<div class="task-btns">
|
||||
<button onclick="naechsterFinisher(_finisherListe, _finisherIndex + 1)">Erledigt</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Session beenden ──
|
||||
const BDSM_STORAGE_KEYS = [
|
||||
'bdsm-session-id', 'bdsm-session-settings', 'bdsm-session-setup',
|
||||
'bdsm-session-gruppen', 'bdsm-session-toys', 'bdsm-session-game',
|
||||
];
|
||||
|
||||
function sessionBeendenFragen() {
|
||||
zeigeModal(
|
||||
'Wirklich beenden?',
|
||||
'Möchtest du die aktive Session wirklich beenden?',
|
||||
[
|
||||
{ label: 'Ja, beenden', primary: true, onClick: sessionLoeschen },
|
||||
{ label: 'Nein', onClick: versteckeModal },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async function sessionLoeschen() {
|
||||
versteckeModal();
|
||||
try {
|
||||
await fetch('/session', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId }),
|
||||
});
|
||||
} catch (_) { /* ignorieren */ }
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
|
||||
// ── Start ──
|
||||
ladeAufgabe();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
438
xxxthegame/src/main/resources/static/sessionbdsmplayers.html
Normal file
@@ -0,0 +1,438 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
324
xxxthegame/src/main/resources/static/sessionbdsmtasks.html
Normal file
@@ -0,0 +1,324 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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; }
|
||||
|
||||
.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;
|
||||
}
|
||||
.select-all-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.select-all-label input {
|
||||
accent-color: var(--color-primary);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gruppe-list { list-style: none; padding: 0; margin: 0; }
|
||||
.gruppe-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.gruppe-item.is-checked { border-color: var(--color-primary); }
|
||||
.gruppe-item input {
|
||||
accent-color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gruppe-item span { flex: 1; min-width: 0; }
|
||||
.item-img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gruppe-item-name {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.gruppe-item-desc {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
.empty-hint {
|
||||
color: var(--color-muted);
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content session-setup">
|
||||
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Schritt 3 von 4 – Aufgaben</p>
|
||||
|
||||
<div class="setup-section" id="sectionOwn">
|
||||
<h2><label class="select-all-label">
|
||||
<input type="checkbox" class="select-all-cb" data-list="listOwn">
|
||||
Eigene Gruppen
|
||||
</label></h2>
|
||||
<ul class="gruppe-list" id="listOwn"></ul>
|
||||
</div>
|
||||
|
||||
<div class="setup-section" id="sectionSubscribed">
|
||||
<h2><label class="select-all-label">
|
||||
<input type="checkbox" class="select-all-cb" data-list="listSubscribed">
|
||||
Abonnierte Gruppen
|
||||
</label></h2>
|
||||
<ul class="gruppe-list" id="listSubscribed"></ul>
|
||||
</div>
|
||||
|
||||
<div class="setup-section" id="sectionSystem">
|
||||
<h2><label class="select-all-label">
|
||||
<input type="checkbox" class="select-all-cb" data-list="listSystem">
|
||||
System-Gruppen
|
||||
</label></h2>
|
||||
<ul class="gruppe-list" id="listSystem"></ul>
|
||||
</div>
|
||||
|
||||
<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:2;" onclick="weiter()">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
if (!sessionStorage.getItem('bdsm-session-setup')) {
|
||||
window.location.replace('/sessionbdsm.html');
|
||||
}
|
||||
|
||||
const savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
|
||||
|
||||
let warnungsAkzeptiert = false;
|
||||
|
||||
document.addEventListener('change', e => {
|
||||
const cb = e.target;
|
||||
if (cb.type !== 'checkbox') return;
|
||||
|
||||
if (cb.classList.contains('select-all-cb')) {
|
||||
// Alle Gruppen in dieser Sektion (de-)selektieren
|
||||
const list = document.getElementById(cb.dataset.list);
|
||||
list.querySelectorAll('input[type="checkbox"]').forEach(itemCb => {
|
||||
itemCb.checked = cb.checked;
|
||||
itemCb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
} else {
|
||||
// Einzelne Gruppe: is-checked-Klasse anpassen und Alles-Haken aktualisieren
|
||||
cb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
|
||||
updateSelectAll(cb.closest('.gruppe-list'));
|
||||
}
|
||||
|
||||
warnungsAkzeptiert = false;
|
||||
hideMessage();
|
||||
});
|
||||
|
||||
function updateSelectAll(list) {
|
||||
if (!list) return;
|
||||
const itemCbs = [...list.querySelectorAll('input[type="checkbox"]')];
|
||||
if (!itemCbs.length) return;
|
||||
const section = list.closest('.setup-section');
|
||||
const selectAllCb = section?.querySelector('.select-all-cb');
|
||||
if (!selectAllCb) return;
|
||||
const checkedCount = itemCbs.filter(cb => cb.checked).length;
|
||||
selectAllCb.checked = checkedCount === itemCbs.length;
|
||||
selectAllCb.indeterminate = checkedCount > 0 && checkedCount < itemCbs.length;
|
||||
}
|
||||
|
||||
function renderList(containerId, gruppen) {
|
||||
const ul = document.getElementById(containerId);
|
||||
const section = ul.closest('.setup-section');
|
||||
const selectAllWrap = section?.querySelector('.select-all-label');
|
||||
|
||||
if (!gruppen.length) {
|
||||
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
|
||||
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden';
|
||||
return;
|
||||
}
|
||||
|
||||
ul.innerHTML = gruppen.map(g => {
|
||||
const checked = savedGruppen.has(g.gruppenId);
|
||||
return `
|
||||
<li>
|
||||
<label class="gruppe-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
|
||||
<span>
|
||||
<span class="gruppe-item-name">${g.name}</span>
|
||||
${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}
|
||||
</span>
|
||||
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
|
||||
</label>
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
updateSelectAll(ul);
|
||||
}
|
||||
|
||||
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
|
||||
|
||||
function validateContent(content, settings, mitspieler) {
|
||||
const errors = [], warnings = [];
|
||||
|
||||
const aufgabenByLevel = {};
|
||||
content.aufgaben.forEach(a => {
|
||||
const l = a.level ?? 0;
|
||||
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
|
||||
});
|
||||
|
||||
for (const [level, count] of Object.entries(aufgabenByLevel)) {
|
||||
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) – Minimum 5 erforderlich`);
|
||||
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben – empfohlen ≥ 10`);
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitStrafe > 1) {
|
||||
const strafenByLevel = {};
|
||||
content.strafen.forEach(s => {
|
||||
const l = s.level ?? 0;
|
||||
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
|
||||
});
|
||||
for (const level of Object.keys(aufgabenByLevel)) {
|
||||
const count = strafenByLevel[level] || 0;
|
||||
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
|
||||
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) – empfohlen ≥ 2`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitSperre > 1) {
|
||||
const count = content.sperren.length;
|
||||
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
|
||||
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) – empfohlen ≥ 5`);
|
||||
}
|
||||
|
||||
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
|
||||
for (const g of beteiligtGeschlecht) {
|
||||
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
|
||||
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
function showValidation(errors, warnings, mitHinweis) {
|
||||
const el = document.getElementById('message');
|
||||
el.innerHTML = [
|
||||
...errors.map(e => `<div>✕ ${e}</div>`),
|
||||
...warnings.map(w => `<div>⚠ ${w}</div>`),
|
||||
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Weiter klicken um fortzufahren.</div>'] : []),
|
||||
].join('');
|
||||
el.className = `message ${errors.length ? 'error' : 'warning'}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
const el = document.getElementById('message');
|
||||
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
|
||||
el.textContent = icon + text;
|
||||
el.className = `message ${type}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideMessage() {
|
||||
document.getElementById('message').style.display = 'none';
|
||||
}
|
||||
|
||||
async function weiter() {
|
||||
hideMessage();
|
||||
const selected = [...document.querySelectorAll('.gruppe-list input[type="checkbox"]:checked')]
|
||||
.map(cb => cb.value);
|
||||
if (selected.length === 0) {
|
||||
showMessage('Bitte mindestens eine Aufgaben-Gruppe auswählen.', 'error');
|
||||
warnungsAkzeptiert = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector('button[onclick="weiter()"]');
|
||||
btn.disabled = true;
|
||||
const gruppen = await Promise.all(
|
||||
selected.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
|
||||
);
|
||||
btn.disabled = false;
|
||||
|
||||
const content = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
|
||||
gruppen.filter(Boolean).forEach(g => {
|
||||
content.aufgaben.push(...(g.aufgaben || []));
|
||||
content.strafen.push(...(g.strafen || []));
|
||||
content.sperren.push(...(g.sperren || []));
|
||||
content.finisher.push(...(g.finisher || []));
|
||||
});
|
||||
|
||||
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
|
||||
const { errors, warnings } = validateContent(content, settings, setup?.mitspieler || []);
|
||||
|
||||
if (errors.length > 0) {
|
||||
showValidation(errors, warnings, false);
|
||||
warnungsAkzeptiert = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && !warnungsAkzeptiert) {
|
||||
showValidation([], warnings, true);
|
||||
warnungsAkzeptiert = true;
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
|
||||
window.location.href = '/sessionbdsmtoys.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: [] }),
|
||||
]).then(([own, abo, system]) => {
|
||||
renderList('listOwn', own.content || []);
|
||||
renderList('listSubscribed', abo.content || []);
|
||||
renderList('listSystem', system.content || []);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
312
xxxthegame/src/main/resources/static/sessionbdsmtoys.html
Normal file
@@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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; }
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.toy-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.toy-item.is-checked { border-color: var(--color-primary); }
|
||||
.toy-item input {
|
||||
accent-color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toy-item span { flex: 1; min-width: 0; }
|
||||
.item-img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
|
||||
.toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
|
||||
.no-toys { color: var(--color-muted); font-size: 0.875rem; font-style: italic; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content session-setup">
|
||||
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Schritt 4 von 4 – Toys</p>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Benötigte Toys</h2>
|
||||
<p style="font-size:0.85rem; color:var(--color-muted); margin-bottom:1.25rem;">
|
||||
Deaktiviere Toys, die nicht zur Verfügung stehen. Aufgaben, die diese benötigen, werden nicht gespielt.
|
||||
</p>
|
||||
<div id="toyList"></div>
|
||||
</div>
|
||||
|
||||
<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:2;" onclick="spielStarten()">Spiel starten</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null');
|
||||
if (!savedGruppen) window.location.replace('/sessionbdsm.html');
|
||||
|
||||
// Previously saved toy selection (when navigating back from game page)
|
||||
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
|
||||
const savedToyIds = savedToysRaw
|
||||
? new Set(JSON.parse(savedToysRaw).map(t => t.toyId))
|
||||
: null; // null = first visit → default all checked
|
||||
|
||||
// All content collected from selected groups
|
||||
const allContent = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
|
||||
|
||||
let warnungsAkzeptiert = false;
|
||||
|
||||
document.addEventListener('change', e => {
|
||||
const cb = e.target;
|
||||
if (cb.type !== 'checkbox') return;
|
||||
cb.closest('.toy-item')?.classList.toggle('is-checked', cb.checked);
|
||||
warnungsAkzeptiert = false;
|
||||
hideMessage();
|
||||
});
|
||||
|
||||
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
|
||||
|
||||
function validateContent(content, settings, mitspieler) {
|
||||
const errors = [], warnings = [];
|
||||
|
||||
const aufgabenByLevel = {};
|
||||
content.aufgaben.forEach(a => {
|
||||
const l = a.level ?? 0;
|
||||
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
|
||||
});
|
||||
|
||||
for (const [level, count] of Object.entries(aufgabenByLevel)) {
|
||||
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) – Minimum 5 erforderlich`);
|
||||
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben – empfohlen ≥ 10`);
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitStrafe > 1) {
|
||||
const strafenByLevel = {};
|
||||
content.strafen.forEach(s => {
|
||||
const l = s.level ?? 0;
|
||||
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
|
||||
});
|
||||
for (const level of Object.keys(aufgabenByLevel)) {
|
||||
const count = strafenByLevel[level] || 0;
|
||||
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
|
||||
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) – empfohlen ≥ 2`);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.wahrscheinlichkeitSperre > 1) {
|
||||
const count = content.sperren.length;
|
||||
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
|
||||
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) – empfohlen ≥ 5`);
|
||||
}
|
||||
|
||||
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
|
||||
for (const g of beteiligtGeschlecht) {
|
||||
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
|
||||
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
function showValidation(errors, warnings, mitHinweis) {
|
||||
const el = document.getElementById('message');
|
||||
el.innerHTML = [
|
||||
...errors.map(e => `<div>✕ ${e}</div>`),
|
||||
...warnings.map(w => `<div>⚠ ${w}</div>`),
|
||||
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Spiel starten klicken um fortzufahren.</div>'] : []),
|
||||
].join('');
|
||||
el.className = `message ${errors.length ? 'error' : 'warning'}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
const el = document.getElementById('message');
|
||||
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
|
||||
el.textContent = icon + text;
|
||||
el.className = `message ${type}`;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideMessage() {
|
||||
document.getElementById('message').style.display = 'none';
|
||||
}
|
||||
|
||||
function renderToys(toys) {
|
||||
const container = document.getElementById('toyList');
|
||||
if (!toys.length) {
|
||||
container.innerHTML = '<p class="no-toys">Keine Toys erforderlich – alle Aufgaben können gespielt werden.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = toys.map(toy => {
|
||||
const checked = savedToyIds === null || savedToyIds.has(toy.toyId);
|
||||
return `
|
||||
<label class="toy-item${checked ? ' is-checked' : ''}">
|
||||
<input type="checkbox" value="${toy.toyId}"${checked ? ' checked' : ''}>
|
||||
<span>
|
||||
<span class="toy-item-name">${toy.name}</span>
|
||||
${toy.beschreibung ? `<span class="toy-item-desc">${toy.beschreibung}</span>` : ''}
|
||||
</span>
|
||||
${toy.bild ? `<img class="item-img" src="data:image/png;base64,${toy.bild}" alt="">` : ''}
|
||||
</label>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function spielStarten() {
|
||||
const checkedToyIds = new Set(
|
||||
[...document.querySelectorAll('#toyList input[type="checkbox"]:checked')].map(cb => cb.value)
|
||||
);
|
||||
|
||||
// Collect full toy objects for the checked ones (for name display on overview)
|
||||
const toyMap = new Map();
|
||||
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
|
||||
(item.benoetigteToys || []).forEach(t => toyMap.set(t.toyId, t));
|
||||
});
|
||||
const checkedToys = [...checkedToyIds].map(id => toyMap.get(id)).filter(Boolean);
|
||||
sessionStorage.setItem('bdsm-session-toys', JSON.stringify(checkedToys));
|
||||
|
||||
function toyOk(item) {
|
||||
const toys = item.benoetigteToys || [];
|
||||
return toys.length === 0 || toys.every(t => checkedToyIds.has(t.toyId));
|
||||
}
|
||||
|
||||
const gameContent = {
|
||||
aufgaben: allContent.aufgaben.filter(toyOk),
|
||||
strafen: allContent.strafen.filter(toyOk),
|
||||
sperren: allContent.sperren.filter(toyOk),
|
||||
finisher: allContent.finisher.filter(toyOk),
|
||||
};
|
||||
|
||||
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
|
||||
const { errors, warnings } = validateContent(gameContent, settings, setup?.mitspieler || []);
|
||||
|
||||
if (errors.length > 0) {
|
||||
showValidation(errors, warnings, false);
|
||||
warnungsAkzeptiert = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && !warnungsAkzeptiert) {
|
||||
showValidation([], warnings, true);
|
||||
warnungsAkzeptiert = true;
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('bdsm-session-game', JSON.stringify(gameContent));
|
||||
|
||||
const btn = document.querySelector('button[onclick="spielStarten()"]');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
// 1. Session anlegen
|
||||
const sessionRes = await fetch('/session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
wahrscheinlichkeitStrafe: settings.wahrscheinlichkeitStrafe,
|
||||
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
|
||||
aufgabenProLevel: settings.aufgabenProLevel,
|
||||
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
|
||||
}),
|
||||
});
|
||||
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
|
||||
const location = sessionRes.headers.get('Location');
|
||||
const sessionId = location.split('/').pop();
|
||||
|
||||
// 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`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: p.name,
|
||||
geschlecht: p.geschlecht,
|
||||
spieltMit: p.spieltMit,
|
||||
rollen: p.rollen,
|
||||
verfuegbareWerkzeuge: p.werkzeuge,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
|
||||
}
|
||||
|
||||
// 3. Aufgaben setzen
|
||||
const aufgabenRes = await fetch(`/session/${sessionId}/aufgaben`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(gameContent),
|
||||
});
|
||||
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
|
||||
|
||||
sessionStorage.setItem('bdsm-session-id', sessionId);
|
||||
window.location.href = '/sessionbdsmingame.html';
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load all selected groups, collect content and unique toys
|
||||
Promise.all(
|
||||
savedGruppen.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
|
||||
).then(gruppen => {
|
||||
gruppen.filter(Boolean).forEach(g => {
|
||||
allContent.aufgaben.push(...(g.aufgaben || []));
|
||||
allContent.strafen.push(...(g.strafen || []));
|
||||
allContent.sperren.push(...(g.sperren || []));
|
||||
allContent.finisher.push(...(g.finisher || []));
|
||||
});
|
||||
|
||||
const toyMap = new Map();
|
||||
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
|
||||
(item.benoetigteToys || []).forEach(t => {
|
||||
if (!toyMap.has(t.toyId)) toyMap.set(t.toyId, t);
|
||||
});
|
||||
});
|
||||
|
||||
renderToys([...toyMap.values()].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
19
xxxthegame/src/main/resources/static/sessionchastity.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chastity Game – Neue Session – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1>Chastity Game – Neue Session</h1>
|
||||
<p>Session-Setup für das Chastity Game folgt hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
19
xxxthegame/src/main/resources/static/sessionvanilla.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vanilla Game – Neue Session – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1>Vanilla Game – Neue Session</h1>
|
||||
<p>Session-Setup für das Vanilla Game folgt hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||