Umsetzung Aufgabenverwaltung
This commit is contained in:
@@ -11,11 +11,12 @@ public class AufgabenGruppe {
|
||||
private String von;
|
||||
private UUID userId;
|
||||
private boolean privateGruppe;
|
||||
private List<Toy> toys;
|
||||
private List<Aufgabe> aufgaben;
|
||||
private List<Strafe> strafen;
|
||||
private List<Sperre> sperren;
|
||||
private String bild;
|
||||
private long subscriberCount;
|
||||
private boolean subscribed;
|
||||
|
||||
public UUID getGruppenId() { return gruppenId; }
|
||||
public void setGruppenId(UUID gruppenId) { this.gruppenId = gruppenId; }
|
||||
@@ -35,9 +36,6 @@ public class AufgabenGruppe {
|
||||
public boolean isPrivateGruppe() { return privateGruppe; }
|
||||
public void setPrivateGruppe(boolean privateGruppe) { this.privateGruppe = privateGruppe; }
|
||||
|
||||
public List<Toy> getToys() { return toys; }
|
||||
public void setToys(List<Toy> toys) { this.toys = toys; }
|
||||
|
||||
public List<Aufgabe> getAufgaben() { return aufgaben; }
|
||||
public void setAufgaben(List<Aufgabe> aufgaben) { this.aufgaben = aufgaben; }
|
||||
|
||||
@@ -49,4 +47,10 @@ public class AufgabenGruppe {
|
||||
|
||||
public String getBild() { return bild; }
|
||||
public void setBild(String bild) { this.bild = bild; }
|
||||
|
||||
public long getSubscriberCount() { return subscriberCount; }
|
||||
public void setSubscriberCount(long subscriberCount) { this.subscriberCount = subscriberCount; }
|
||||
|
||||
public boolean isSubscribed() { return subscribed; }
|
||||
public void setSubscribed(boolean subscribed) { this.subscribed = subscribed; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.oaa.xxx.aufgaben;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AufgabenGruppePage {
|
||||
|
||||
private List<AufgabenGruppe> content;
|
||||
private int currentPage;
|
||||
private int totalPages;
|
||||
private long totalElements;
|
||||
|
||||
public List<AufgabenGruppe> getContent() { return content; }
|
||||
public void setContent(List<AufgabenGruppe> content) { this.content = content; }
|
||||
|
||||
public int getCurrentPage() { return currentPage; }
|
||||
public void setCurrentPage(int currentPage) { this.currentPage = currentPage; }
|
||||
|
||||
public int getTotalPages() { return totalPages; }
|
||||
public void setTotalPages(int totalPages) { this.totalPages = totalPages; }
|
||||
|
||||
public long getTotalElements() { return totalElements; }
|
||||
public void setTotalElements(long totalElements) { this.totalElements = totalElements; }
|
||||
}
|
||||
@@ -57,10 +57,10 @@ public class DefaultFiller {
|
||||
|
||||
void chastityFemale() {
|
||||
AufgabenGruppeEntity keuschWiebl = createAufgGruppe("Keuschhaltung weiblich", "Enthält verschiedene Aufgaben für Keuschhaltung von weiblichen Spielpartnern", getClass().getClassLoader().getResourceAsStream("femaleCB.png"));
|
||||
ToyEntity kg = createToy("KG weiblich", "Ein Voll-Keuschheitsgürtel für die Frau", keuschWiebl);
|
||||
ToyEntity kgVaginal = createToy("KG weiblich, Vaginaldildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginaldildos", keuschWiebl);
|
||||
ToyEntity kgAnal = createToy("KG weiblich, Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Analdildos", keuschWiebl);
|
||||
ToyEntity kgDouble = createToy("KG weiblich, Vaginal- u. Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginal- und Analdildos", keuschWiebl);
|
||||
ToyEntity kg = createToy("KG weiblich", "Ein Voll-Keuschheitsgürtel für die Frau");
|
||||
ToyEntity kgVaginal = createToy("KG weiblich, Vaginaldildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginaldildos");
|
||||
ToyEntity kgAnal = createToy("KG weiblich, Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Analdildos");
|
||||
ToyEntity kgDouble = createToy("KG weiblich, Vaginal- u. Analdildo", "Ein Voll-Keuschheitsgürtel für die Frau inkl. eines Vaginal- und Analdildos");
|
||||
|
||||
createSperre("Voll-KG", "{PASSIV} trägt fortan einen Voll-KG, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien", 10, 30, Arrays.asList(kg), Arrays.asList(VAGINA), keuschWiebl);
|
||||
createSperre("Voll-KG + Vaginaldildo", "{PASSIV} trägt fortan einen Voll-KG mit Vaginaldildo, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von ihrem KG zu befreien", 10, 30, Arrays.asList(kgVaginal), Arrays.asList(VAGINA), keuschWiebl);
|
||||
@@ -70,9 +70,9 @@ public class DefaultFiller {
|
||||
|
||||
void chastityMale() {
|
||||
AufgabenGruppeEntity keuschMaennl = createAufgGruppe("Keuschhaltung männlich", "Enthält verschiedene Aufgaben für Keuschhaltung von männlichen Spielpartnern", getClass().getClassLoader().getResourceAsStream("maleCB.png"));
|
||||
ToyEntity kaefig = createToy("Peniskäfig", "Ein gewöhnlicher Peniskäfig", keuschMaennl);
|
||||
ToyEntity kgMaennl = createToy("KG männlich", "Ein Voll-Keuschheitsgürtel für den Mann", keuschMaennl);
|
||||
ToyEntity knMaennlAnal = createToy("KG männlich, Analdildo", "Ein Voll-Keuschheitsgürtel für den Mann inkl. eines Analdildos oder -plugs", keuschMaennl);
|
||||
ToyEntity kaefig = createToy("Peniskäfig", "Ein gewöhnlicher Peniskäfig");
|
||||
ToyEntity kgMaennl = createToy("KG männlich", "Ein Voll-Keuschheitsgürtel für den Mann");
|
||||
ToyEntity knMaennlAnal = createToy("KG männlich, Analdildo", "Ein Voll-Keuschheitsgürtel für den Mann inkl. eines Analdildos oder -plugs");
|
||||
|
||||
createSperre("Peniskäfig", "{PASSIV} trägt fortan einen Peniskäfig, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von seinem Peniskäfig zu befreien", 10, 30, Arrays.asList(kaefig), Arrays.asList(PENIS), keuschMaennl);
|
||||
createSperre("Voll-KG", "{PASSIV} trägt fortan einen Voll-KG, {AKTIV} ist der Keyholder", "{AKTIV}, es ist ab der Zeit {PASSIV} von seinem KG zu befreien", 10, 30, Arrays.asList(kgMaennl), Arrays.asList(PENIS), keuschMaennl);
|
||||
@@ -81,10 +81,10 @@ public class DefaultFiller {
|
||||
|
||||
void plugs() {
|
||||
AufgabenGruppeEntity gruppe = createAufgGruppe("Plugs", "Enthält verschiedene Aufgaben für das Tragen von Buttplugs über einen gewissen Zeitraum.", getClass().getClassLoader().getResourceAsStream("plugs.png"));
|
||||
ToyEntity plugKlein = createToy("Plug klein", "Ein kleiner Buttplug", gruppe);
|
||||
ToyEntity plugMittel = createToy("Plug mittel", "Ein mittelgroßer Buttplug", gruppe);
|
||||
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug", gruppe);
|
||||
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst", gruppe);
|
||||
ToyEntity plugKlein = createToy("Plug klein", "Ein kleiner Buttplug");
|
||||
ToyEntity plugMittel = createToy("Plug mittel", "Ein mittelgroßer Buttplug");
|
||||
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug");
|
||||
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst");
|
||||
|
||||
createSperre("Plug klein", "{AKTIV} führt {PASSIV} einen kleinen Buttplug in anal ein, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien", 10, 30, Arrays.asList(plugKlein), Arrays.asList(ANUS), gruppe);
|
||||
createSperre("Plug mittel", "{AKTIV} führt {PASSIV} einen mittelgroßen Buttplug anal ein, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Plug zu befreien", 10, 30, Arrays.asList(plugMittel), Arrays.asList(ANUS), gruppe);
|
||||
@@ -95,10 +95,10 @@ public class DefaultFiller {
|
||||
|
||||
void knebel() {
|
||||
AufgabenGruppeEntity gruppe = createAufgGruppe("Knebel", "Enthält verschiedene Aufgaben für das Tragen von Knebeln über einen gewissen Zeitraum.", getClass().getClassLoader().getResourceAsStream("knebel.png"));
|
||||
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel", gruppe);
|
||||
ToyEntity penisKnebel = createToy("Penisknebel", "Ein Penisknebel", gruppe);
|
||||
ToyEntity aufblKnebel = createToy("Aufblasbarer Knebel", "Ein aufblasbarer Knebel", gruppe);
|
||||
ToyEntity isolationsmaske = createToy("Isolationsmaske", "Eine Isolationsmaske", gruppe);
|
||||
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel");
|
||||
ToyEntity penisKnebel = createToy("Penisknebel", "Ein Penisknebel");
|
||||
ToyEntity aufblKnebel = createToy("Aufblasbarer Knebel", "Ein aufblasbarer Knebel");
|
||||
ToyEntity isolationsmaske = createToy("Isolationsmaske", "Eine Isolationsmaske");
|
||||
|
||||
createSperre("Ballknebel", "{AKTIV}, lege {PASSIV} einen Ballknebel an, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.", 10, 30, Arrays.asList(ballKnebel), Arrays.asList(MUND), gruppe);
|
||||
createSperre("Penisknebel", "{AKTIV}, lege {PASSIV} einen Dildoknebel an, dieser ist bis auf weiteres zu tragen.", "{AKTIV}, es ist Zeit {PASSIV} von seinem Knebel zu befreien.", 10, 30, Arrays.asList(penisKnebel), Arrays.asList(MUND), gruppe);
|
||||
@@ -109,22 +109,22 @@ public class DefaultFiller {
|
||||
void stafen() {
|
||||
AufgabenGruppeEntity strafen = createAufgGruppe("Strafen", "Enthält verschiedene Bestrafungen", getClass().getClassLoader().getResourceAsStream("peitsche.png"));
|
||||
|
||||
ToyEntity gerte = createToy("Gerte", "Eine gewöhnliche Gerte", strafen);
|
||||
ToyEntity paddel = createToy("Paddel", "Eine gewöhnliches Paddel", strafen);
|
||||
ToyEntity peitsche = createToy("Peitsche", "Eine gewöhnliche Peitsche", strafen);
|
||||
ToyEntity penisKnebel = createToy("Doppel-Penisknebel", "Ein Doppel-Penisknebel", strafen);
|
||||
ToyEntity handfesseln = createToy("Handfesseln", "Fesseln zum Binden der Hände, z.B. Handschellen", strafen);
|
||||
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug", strafen);
|
||||
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst", strafen);
|
||||
ToyEntity plugPump = createToy("Pump-Plug", "Ein aufblasbarer Plug", strafen);
|
||||
ToyEntity nippelklemmen = createToy("Nippelklemmen", "Nippelklemmen", strafen);
|
||||
ToyEntity augenbinde = createToy("Augenbinde", "Eine Augenbinde", strafen);
|
||||
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel", strafen);
|
||||
ToyEntity strapon = createToy("Strapon", "Ein Umschnalldildo", strafen);
|
||||
ToyEntity kgMann = createToy("KG Mann", "Ein Voll-KG oder Peniskäfig für den Mann", strafen);
|
||||
ToyEntity kgFrau = createToy("KG Frau", "Ein Voll-KG die Frau", strafen);
|
||||
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo", strafen);
|
||||
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo", strafen);
|
||||
ToyEntity gerte = createToy("Gerte", "Eine gewöhnliche Gerte");
|
||||
ToyEntity paddel = createToy("Paddel", "Eine gewöhnliches Paddel");
|
||||
ToyEntity peitsche = createToy("Peitsche", "Eine gewöhnliche Peitsche");
|
||||
ToyEntity penisKnebel = createToy("Doppel-Penisknebel", "Ein Doppel-Penisknebel");
|
||||
ToyEntity handfesseln = createToy("Handfesseln", "Fesseln zum Binden der Hände, z.B. Handschellen");
|
||||
ToyEntity plugGross = createToy("Plug groß", "Ein großer Buttplug");
|
||||
ToyEntity plugElektro = createToy("Elektro-Plug", "Ein Elektroplug, der Stromstöße verpasst");
|
||||
ToyEntity plugPump = createToy("Pump-Plug", "Ein aufblasbarer Plug");
|
||||
ToyEntity nippelklemmen = createToy("Nippelklemmen", "Nippelklemmen");
|
||||
ToyEntity augenbinde = createToy("Augenbinde", "Eine Augenbinde");
|
||||
ToyEntity ballKnebel = createToy("Ballknebel", "Ein Ballknebel");
|
||||
ToyEntity strapon = createToy("Strapon", "Ein Umschnalldildo");
|
||||
ToyEntity kgMann = createToy("KG Mann", "Ein Voll-KG oder Peniskäfig für den Mann");
|
||||
ToyEntity kgFrau = createToy("KG Frau", "Ein Voll-KG die Frau");
|
||||
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo");
|
||||
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo");
|
||||
|
||||
createStrafe("5 Schläge mit flachen Hand", "{PASSIV} stellt sich mit dem Gesicht zur Wand, Hände hinterm Kopf, Beine schulterbreit, {AKTIV} verpasst {PASSIV} 5 Schläge mit der flachen Hand auf das Gesäß.",
|
||||
1, null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), strafen);
|
||||
@@ -213,9 +213,9 @@ public class DefaultFiller {
|
||||
void aufgaben() {
|
||||
AufgabenGruppeEntity aufgaben = createAufgGruppe("Aufgaben", "Enthält verschiedene Sex-Aufgaben.", getClass().getClassLoader().getResourceAsStream("sex.png"));
|
||||
|
||||
ToyEntity vibrator = createToy("Vibrator", "Ein herkömmlicher Vibrator.", aufgaben);
|
||||
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo", aufgaben);
|
||||
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo", aufgaben);
|
||||
ToyEntity vibrator = createToy("Vibrator", "Ein herkömmlicher Vibrator.");
|
||||
ToyEntity dildoKlein = createToy("Dildo klein", "Ein kleiner Dildo");
|
||||
ToyEntity dildoGross = createToy("Dildo groß", "Ein großer Dildo");
|
||||
|
||||
createAufgabe("Hintern präsentieren", "{AKTIV}, zeig {PASSIV} deinen Hintern, gib dir selber dabei ein oder zwei Klappse auf den Po",
|
||||
1, null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), aufgaben);
|
||||
@@ -367,12 +367,11 @@ public class DefaultFiller {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private ToyEntity createToy(String name, String beschreibung, AufgabenGruppeEntity gruppe) {
|
||||
private ToyEntity createToy(String name, String beschreibung) {
|
||||
ToyEntity toy = new ToyEntity();
|
||||
toy.setToyId(UUID.randomUUID());
|
||||
toy.setName(name);
|
||||
toy.setBeschreibung(beschreibung);
|
||||
toy.setAufgabenGruppe(gruppe);
|
||||
toyRepository.save(toy);
|
||||
return toy;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package de.oaa.xxx.aufgaben;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Image;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -11,19 +12,47 @@ import java.io.IOException;
|
||||
|
||||
public class ImageScaler {
|
||||
|
||||
public byte[] scale(byte[] origByte) {
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(origByte)) {
|
||||
private static final int MAX_SIZE = 128;
|
||||
|
||||
public byte[] scale(byte[] origBytes) {
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(origBytes)) {
|
||||
BufferedImage orig = ImageIO.read(bais);
|
||||
BufferedImage scaled = (BufferedImage) orig.getScaledInstance(128, 128, Image.SCALE_DEFAULT);
|
||||
if (orig == null) {
|
||||
return origBytes;
|
||||
}
|
||||
|
||||
int origWidth = orig.getWidth();
|
||||
int origHeight = orig.getHeight();
|
||||
|
||||
// Bereits klein genug – unverändern zurückgeben
|
||||
if (origWidth <= MAX_SIZE && origHeight <= MAX_SIZE) {
|
||||
return origBytes;
|
||||
}
|
||||
|
||||
// Seitenverhältnis beibehalten: längste Seite auf MAX_SIZE
|
||||
int newWidth, newHeight;
|
||||
if (origWidth >= origHeight) {
|
||||
newWidth = MAX_SIZE;
|
||||
newHeight = Math.max(1, Math.round((float) MAX_SIZE * origHeight / origWidth));
|
||||
} else {
|
||||
newHeight = MAX_SIZE;
|
||||
newWidth = Math.max(1, Math.round((float) MAX_SIZE * origWidth / origHeight));
|
||||
}
|
||||
|
||||
BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = scaled.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g.drawImage(orig, 0, 0, newWidth, newHeight, null);
|
||||
g.dispose();
|
||||
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(scaled, "png", baos);
|
||||
return baos.toByteArray();
|
||||
} catch (IOException exception) {
|
||||
LoggerFactory.getLogger(getClass()).error("Fehler beim Skalieren des Bildes", exception);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
LoggerFactory.getLogger(getClass()).error("Fehler beim Skalieren des Bildes", exception);
|
||||
} catch (IOException e) {
|
||||
LoggerFactory.getLogger(ImageScaler.class).error("Fehler beim Skalieren des Bildes", e);
|
||||
return origBytes;
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ public class Toy {
|
||||
private UUID toyId;
|
||||
private String name;
|
||||
private String beschreibung;
|
||||
private UUID gruppeId;
|
||||
private UUID userId;
|
||||
private String bild;
|
||||
|
||||
public UUID getToyId() { return toyId; }
|
||||
public void setToyId(UUID toyId) { this.toyId = toyId; }
|
||||
@@ -18,6 +19,9 @@ public class Toy {
|
||||
public String getBeschreibung() { return beschreibung; }
|
||||
public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
|
||||
|
||||
public UUID getGruppeId() { return gruppeId; }
|
||||
public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; }
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public String getBild() { return bild; }
|
||||
public void setBild(String bild) { this.bild = bild; }
|
||||
}
|
||||
|
||||
15
xxxthegame/src/main/java/de/oaa/xxx/aufgaben/ToyList.java
Normal file
15
xxxthegame/src/main/java/de/oaa/xxx/aufgaben/ToyList.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package de.oaa.xxx.aufgaben;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ToyList {
|
||||
|
||||
private List<Toy> systemToys;
|
||||
private List<Toy> userToys;
|
||||
|
||||
public List<Toy> getSystemToys() { return systemToys; }
|
||||
public void setSystemToys(List<Toy> systemToys) { this.systemToys = systemToys; }
|
||||
|
||||
public List<Toy> getUserToys() { return userToys; }
|
||||
public void setUserToys(List<Toy> userToys) { this.userToys = userToys; }
|
||||
}
|
||||
23
xxxthegame/src/main/java/de/oaa/xxx/aufgaben/ToyPage.java
Normal file
23
xxxthegame/src/main/java/de/oaa/xxx/aufgaben/ToyPage.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package de.oaa.xxx.aufgaben;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ToyPage {
|
||||
|
||||
private List<Toy> content;
|
||||
private int currentPage;
|
||||
private int totalPages;
|
||||
private long totalElements;
|
||||
|
||||
public List<Toy> getContent() { return content; }
|
||||
public void setContent(List<Toy> content) { this.content = content; }
|
||||
|
||||
public int getCurrentPage() { return currentPage; }
|
||||
public void setCurrentPage(int currentPage) { this.currentPage = currentPage; }
|
||||
|
||||
public int getTotalPages() { return totalPages; }
|
||||
public void setTotalPages(int totalPages) { this.totalPages = totalPages; }
|
||||
|
||||
public long getTotalElements() { return totalElements; }
|
||||
public void setTotalElements(long totalElements) { this.totalElements = totalElements; }
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package de.oaa.xxx.aufgaben.controller;
|
||||
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppe;
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppePage;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.GruppenAboEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/abo")
|
||||
@Transactional
|
||||
public class AboController {
|
||||
|
||||
private static final int DEFAULT_PAGE_SIZE = 5;
|
||||
private static final int DISCOVER_PAGE_SIZE = 10;
|
||||
|
||||
private final GruppenAboRepository aboRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public AboController(GruppenAboRepository aboRepository,
|
||||
AufgabenGruppeRepository gruppeRepository,
|
||||
UserRepository userRepository) {
|
||||
this.aboRepository = aboRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
// ── Abonnierte Gruppen laden ──
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<AufgabenGruppePage> listSubscribed(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size,
|
||||
Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
List<AufgabenGruppe> dtos = aboRepository.findByUserId(user.getUserId()).stream()
|
||||
.map(GruppenAboEntity::getAufgabenGruppe)
|
||||
.filter(g -> !g.isPrivateGruppe()) // ignoriere inzwischen wieder private Gruppen
|
||||
.map(g -> enrich(g, user.getUserId(), true))
|
||||
.sorted(Comparator.comparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(manualPage(dtos, page, size));
|
||||
}
|
||||
|
||||
// ── Entdecken ──
|
||||
|
||||
@GetMapping("/discover")
|
||||
public ResponseEntity<AufgabenGruppePage> discover(
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "" + DISCOVER_PAGE_SIZE) int size,
|
||||
Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
String namePattern = name != null && !name.isBlank() ? "%" + name.trim() + "%" : null;
|
||||
|
||||
List<AufgabenGruppe> dtos = gruppeRepository
|
||||
.findPublicFromOthers(user.getUserId(), namePattern).stream()
|
||||
.map(g -> enrich(g, user.getUserId(), aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), g)))
|
||||
.sorted(Comparator.comparingLong(AufgabenGruppe::getSubscriberCount).reversed()
|
||||
.thenComparing(AufgabenGruppe::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(manualPage(dtos, page, size));
|
||||
}
|
||||
|
||||
// ── Abonnieren ──
|
||||
|
||||
@PostMapping("/{gruppenId}")
|
||||
public ResponseEntity<Void> subscribe(@PathVariable UUID gruppenId, Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
AufgabenGruppeEntity gruppe = gruppeRepository.findById(gruppenId).orElse(null);
|
||||
if (gruppe == null || gruppe.isPrivateGruppe() || user.getUserId().equals(gruppe.getUserId())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (aboRepository.existsByUserIdAndAufgabenGruppe(user.getUserId(), gruppe)) {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
GruppenAboEntity abo = new GruppenAboEntity();
|
||||
abo.setAboId(UUID.randomUUID());
|
||||
abo.setUserId(user.getUserId());
|
||||
abo.setAufgabenGruppe(gruppe);
|
||||
aboRepository.save(abo);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
// ── Abonnement kündigen ──
|
||||
|
||||
@DeleteMapping("/{gruppenId}")
|
||||
public ResponseEntity<Void> unsubscribe(@PathVariable UUID gruppenId, Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
AufgabenGruppeEntity gruppe = gruppeRepository.findById(gruppenId).orElse(null);
|
||||
if (gruppe == null) return ResponseEntity.noContent().build();
|
||||
|
||||
aboRepository.deleteByUserIdAndAufgabenGruppe(user.getUserId(), gruppe);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ──
|
||||
|
||||
private AufgabenGruppe enrich(AufgabenGruppeEntity entity, UUID userId, boolean subscribed) {
|
||||
AufgabenGruppe g = entity.toAufgabenGruppe();
|
||||
g.setSubscriberCount(aboRepository.countByAufgabenGruppe(entity));
|
||||
g.setSubscribed(subscribed);
|
||||
return g;
|
||||
}
|
||||
|
||||
private AufgabenGruppePage manualPage(List<AufgabenGruppe> all, int page, int size) {
|
||||
int total = all.size();
|
||||
int start = page * size;
|
||||
List<AufgabenGruppe> content = start >= total ? List.of() : all.subList(start, Math.min(start + size, total));
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(content);
|
||||
result.setCurrentPage(page);
|
||||
result.setTotalPages(total == 0 ? 1 : (int) Math.ceil((double) total / size));
|
||||
result.setTotalElements(total);
|
||||
return result;
|
||||
}
|
||||
|
||||
private UserEntity resolveUser(Principal principal) {
|
||||
return userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package de.oaa.xxx.aufgaben.controller;
|
||||
|
||||
import de.oaa.xxx.aufgaben.Aufgabe;
|
||||
import de.oaa.xxx.aufgaben.Toy;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
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.ToyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -13,11 +16,14 @@ 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
|
||||
@@ -29,10 +35,14 @@ public class AufgabeController {
|
||||
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
|
||||
public AufgabeController(AufgabeRepository aufgabeRepository, AufgabenGruppeRepository gruppeRepository) {
|
||||
public AufgabeController(AufgabeRepository aufgabeRepository,
|
||||
AufgabenGruppeRepository gruppeRepository,
|
||||
ToyRepository toyRepository) {
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{aufgabeId}")
|
||||
@@ -48,16 +58,39 @@ public class AufgabeController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(aufgabe.getGruppeId()).orElse(null);
|
||||
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
|
||||
if (gruppeEntity == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabeEntity entity = AufgabeEntity.create(aufgabe, gruppeEntity);
|
||||
if (gruppeEntity.getAufgaben().size() >= 100) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
List<ToyEntity> toys = resolveToys(aufgabe.getBenoetigteToys());
|
||||
AufgabeEntity entity = AufgabeEntity.create(aufgabe, gruppeEntity, toys);
|
||||
aufgabeRepository.save(entity);
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getAufgabeId()).toUri()
|
||||
).build();
|
||||
}
|
||||
|
||||
@PutMapping("/{aufgabeId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID aufgabeId, @RequestBody Aufgabe aufgabe) {
|
||||
if (aufgabe.getKurzText() == null || aufgabe.getText() == null || aufgabe.getLevel() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabeEntity entity = aufgabeRepository.findById(aufgabeId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
entity.setKurzText(aufgabe.getKurzText());
|
||||
entity.setText(aufgabe.getText());
|
||||
entity.setLevel(aufgabe.getLevel());
|
||||
entity.setSekundenVon(aufgabe.getSekundenVon());
|
||||
entity.setSekundenBis(aufgabe.getSekundenBis());
|
||||
entity.setBenoetigtAktiv(aufgabe.getBenoetigtAktiv());
|
||||
entity.setBenoetigtPassiv(aufgabe.getBenoetigtPassiv());
|
||||
entity.setBenoetigteToys(resolveToys(aufgabe.getBenoetigteToys()));
|
||||
aufgabeRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<Void> delete(@RequestBody Aufgabe aufgabe) {
|
||||
try {
|
||||
@@ -68,4 +101,14 @@ public class AufgabeController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,25 @@ package de.oaa.xxx.aufgaben.controller;
|
||||
|
||||
import de.oaa.xxx.aufgaben.AufgabenGruppe;
|
||||
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.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.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.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -14,12 +28,21 @@ import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@@ -28,13 +51,57 @@ import java.util.UUID;
|
||||
public class AufgabenGruppeController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AufgabenGruppeController.class);
|
||||
private static final int DEFAULT_PAGE_SIZE = 5;
|
||||
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final StrafeRepository strafeRepository;
|
||||
private final SperreRepository sperreRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GruppenAboRepository aboRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
|
||||
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository) {
|
||||
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
StrafeRepository strafeRepository,
|
||||
SperreRepository sperreRepository,
|
||||
UserRepository userRepository,
|
||||
GruppenAboRepository aboRepository,
|
||||
ToyRepository toyRepository) {
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.aboRepository = aboRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
}
|
||||
|
||||
// ── Paginierte Listen ──
|
||||
|
||||
@GetMapping("/list/user")
|
||||
public ResponseEntity<AufgabenGruppePage> listUser(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size,
|
||||
Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
Page<AufgabenGruppeEntity> result = gruppeRepository.findByUserId(
|
||||
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
|
||||
return ResponseEntity.ok(toGruppePage(result, true));
|
||||
}
|
||||
|
||||
@GetMapping("/list/system")
|
||||
public ResponseEntity<AufgabenGruppePage> listSystem(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
|
||||
Page<AufgabenGruppeEntity> result = gruppeRepository.findByUserIdIsNull(
|
||||
PageRequest.of(page, size, Sort.by("name")));
|
||||
return ResponseEntity.ok(toGruppePage(result));
|
||||
}
|
||||
|
||||
// ── Bestehende Endpunkte ──
|
||||
|
||||
@GetMapping("/all")
|
||||
public ResponseEntity<AufgabenGruppeList> getAll(@RequestParam(required = false) String search) {
|
||||
UUID userId = (UUID) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
@@ -60,15 +127,185 @@ public class AufgabenGruppeController {
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
// ── Anlegen ──
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> create(@RequestBody AufgabenGruppe gruppe) {
|
||||
public ResponseEntity<Void> create(@RequestBody AufgabenGruppe gruppe, Principal principal) {
|
||||
if (gruppe.getName() == null || gruppe.getName().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
if (gruppeRepository.countByUserId(user.getUserId()) >= 10) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
AufgabenGruppeEntity entity = AufgabenGruppeEntity.create(gruppe);
|
||||
entity.setUserId(user.getUserId());
|
||||
entity.setPrivateGruppe(true);
|
||||
gruppeRepository.save(entity);
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getGruppenId()).toUri()
|
||||
).build();
|
||||
}
|
||||
|
||||
// ── Bearbeiten ──
|
||||
|
||||
@PutMapping("/{gruppeId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID gruppeId,
|
||||
@RequestBody AufgabenGruppe gruppe,
|
||||
Principal principal) {
|
||||
if (gruppe.getName() == null || gruppe.getName().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
|
||||
|
||||
entity.setName(gruppe.getName().trim());
|
||||
entity.setBeschreibung(gruppe.getBeschreibung());
|
||||
entity.setVon(gruppe.getVon());
|
||||
entity.setPrivateGruppe(gruppe.isPrivateGruppe());
|
||||
if (gruppe.getBild() != null) {
|
||||
entity.setBild(Base64.getDecoder().decode(gruppe.getBild()));
|
||||
}
|
||||
gruppeRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── Kopieren (Systemgruppe → eigene) ──
|
||||
|
||||
@PostMapping("/copy/{gruppeId}")
|
||||
public ResponseEntity<Void> copy(@PathVariable UUID gruppeId, Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
if (gruppeRepository.countByUserId(user.getUserId()) >= 10) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
AufgabenGruppeEntity source = gruppeRepository.findById(gruppeId).orElse(null);
|
||||
if (source == null) return ResponseEntity.notFound().build();
|
||||
if (source.isPrivateGruppe()) return ResponseEntity.status(403).build();
|
||||
if (user.getUserId().equals(source.getUserId())) return ResponseEntity.status(403).build();
|
||||
|
||||
// Build toy mapping: source toyId → toy entity the copy will reference
|
||||
Set<ToyEntity> allSourceToys = new HashSet<>();
|
||||
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()); });
|
||||
|
||||
Map<UUID, ToyEntity> toyMapping = new HashMap<>();
|
||||
for (ToyEntity sourceToy : allSourceToys) {
|
||||
if (sourceToy.getUserId() == null) {
|
||||
// System toy – reference directly
|
||||
toyMapping.put(sourceToy.getToyId(), sourceToy);
|
||||
} else {
|
||||
// User toy – find existing toy with same name in user's collection, or create a copy
|
||||
ToyEntity mapped = toyRepository.findByNameIgnoreCaseAndUserId(sourceToy.getName(), user.getUserId())
|
||||
.orElseGet(() -> {
|
||||
ToyEntity tc = new ToyEntity();
|
||||
tc.setToyId(UUID.randomUUID());
|
||||
tc.setName(sourceToy.getName());
|
||||
tc.setBeschreibung(sourceToy.getBeschreibung());
|
||||
tc.setBild(sourceToy.getBild());
|
||||
tc.setUserId(user.getUserId());
|
||||
return toyRepository.save(tc);
|
||||
});
|
||||
toyMapping.put(sourceToy.getToyId(), mapped);
|
||||
}
|
||||
}
|
||||
|
||||
AufgabenGruppeEntity copy = new AufgabenGruppeEntity();
|
||||
copy.setGruppenId(UUID.randomUUID());
|
||||
copy.setName(source.getName());
|
||||
copy.setBeschreibung(source.getBeschreibung());
|
||||
copy.setVon(source.getVon());
|
||||
copy.setBild(source.getBild());
|
||||
copy.setUserId(user.getUserId());
|
||||
copy.setPrivateGruppe(true);
|
||||
gruppeRepository.save(copy);
|
||||
|
||||
for (AufgabeEntity a : source.getAufgaben()) {
|
||||
AufgabeEntity ac = new AufgabeEntity();
|
||||
ac.setAufgabeId(UUID.randomUUID());
|
||||
ac.setAufgabenGruppe(copy);
|
||||
ac.setKurzText(a.getKurzText());
|
||||
ac.setText(a.getText());
|
||||
ac.setLevel(a.getLevel());
|
||||
ac.setSekundenVon(a.getSekundenVon());
|
||||
ac.setSekundenBis(a.getSekundenBis());
|
||||
ac.setBenoetigtAktiv(a.getBenoetigtAktiv() != null ? new ArrayList<>(a.getBenoetigtAktiv()) : null);
|
||||
ac.setBenoetigtPassiv(a.getBenoetigtPassiv() != null ? new ArrayList<>(a.getBenoetigtPassiv()) : null);
|
||||
ac.setBenoetigteToys(mapToys(a.getBenoetigteToys(), toyMapping));
|
||||
aufgabeRepository.save(ac);
|
||||
}
|
||||
|
||||
for (StrafeEntity s : source.getStrafen()) {
|
||||
StrafeEntity sc = new StrafeEntity();
|
||||
sc.setStrafeId(UUID.randomUUID());
|
||||
sc.setAufgabenGruppe(copy);
|
||||
sc.setKurzText(s.getKurzText());
|
||||
sc.setText(s.getText());
|
||||
sc.setLevel(s.getLevel());
|
||||
sc.setSekundenVon(s.getSekundenVon());
|
||||
sc.setSekundenBis(s.getSekundenBis());
|
||||
sc.setBenoetigtAktiv(s.getBenoetigtAktiv() != null ? new ArrayList<>(s.getBenoetigtAktiv()) : null);
|
||||
sc.setBenoetigtPassiv(s.getBenoetigtPassiv() != null ? new ArrayList<>(s.getBenoetigtPassiv()) : null);
|
||||
sc.setBenoetigteToys(mapToys(s.getBenoetigteToys(), toyMapping));
|
||||
strafeRepository.save(sc);
|
||||
}
|
||||
|
||||
for (SperreEntity sp : source.getSperren()) {
|
||||
SperreEntity spc = new SperreEntity();
|
||||
spc.setSperreId(UUID.randomUUID());
|
||||
spc.setAufgabenGruppe(copy);
|
||||
spc.setKurzText(sp.getKurzText());
|
||||
spc.setText(sp.getText());
|
||||
spc.setReleaseText(sp.getReleaseText());
|
||||
spc.setMinutenVon(sp.getMinutenVon());
|
||||
spc.setMinutenBis(sp.getMinutenBis());
|
||||
spc.setSperreFuer(sp.getSperreFuer() != null ? new ArrayList<>(sp.getSperreFuer()) : null);
|
||||
spc.setBenoetigteToys(mapToys(sp.getBenoetigteToys(), toyMapping));
|
||||
sperreRepository.save(spc);
|
||||
}
|
||||
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
private List<ToyEntity> mapToys(List<ToyEntity> source, Map<UUID, ToyEntity> mapping) {
|
||||
if (source == null || source.isEmpty()) return new ArrayList<>();
|
||||
return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList();
|
||||
}
|
||||
|
||||
// ── Löschen ──
|
||||
|
||||
@DeleteMapping("/{gruppeId}")
|
||||
public ResponseEntity<Void> deleteById(@PathVariable UUID gruppeId, Principal principal) {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
AufgabenGruppeEntity entity = gruppeRepository.findById(gruppeId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.noContent().build();
|
||||
if (!user.getUserId().equals(entity.getUserId())) return ResponseEntity.status(403).build();
|
||||
|
||||
try {
|
||||
aboRepository.deleteByAufgabenGruppe(entity);
|
||||
aufgabeRepository.deleteAll(entity.getAufgaben());
|
||||
strafeRepository.deleteAll(entity.getStrafen());
|
||||
sperreRepository.deleteAll(entity.getSperren());
|
||||
gruppeRepository.delete(entity);
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<Void> delete(@RequestBody AufgabenGruppe gruppe) {
|
||||
try {
|
||||
@@ -79,4 +316,27 @@ public class AufgabenGruppeController {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ──
|
||||
|
||||
private UserEntity resolveUser(Principal principal) {
|
||||
return userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
}
|
||||
|
||||
private AufgabenGruppePage toGruppePage(Page<AufgabenGruppeEntity> page) {
|
||||
return toGruppePage(page, false);
|
||||
}
|
||||
|
||||
private AufgabenGruppePage toGruppePage(Page<AufgabenGruppeEntity> page, boolean withSubscriberCount) {
|
||||
AufgabenGruppePage result = new AufgabenGruppePage();
|
||||
result.setContent(page.getContent().stream().map(entity -> {
|
||||
AufgabenGruppe g = entity.toAufgabenGruppe();
|
||||
if (withSubscriberCount) g.setSubscriberCount(aboRepository.countByAufgabenGruppe(entity));
|
||||
return g;
|
||||
}).toList());
|
||||
result.setCurrentPage(page.getNumber());
|
||||
result.setTotalPages(page.getTotalPages());
|
||||
result.setTotalElements(page.getTotalElements());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package de.oaa.xxx.aufgaben.controller;
|
||||
|
||||
import de.oaa.xxx.aufgaben.Sperre;
|
||||
import de.oaa.xxx.aufgaben.Toy;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.SperreEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.SperreRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -13,11 +16,14 @@ 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("aufgabenSperreController")
|
||||
@@ -29,10 +35,14 @@ public class SperreController {
|
||||
|
||||
private final SperreRepository sperreRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
|
||||
public SperreController(SperreRepository sperreRepository, AufgabenGruppeRepository gruppeRepository) {
|
||||
public SperreController(SperreRepository sperreRepository,
|
||||
AufgabenGruppeRepository gruppeRepository,
|
||||
ToyRepository toyRepository) {
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{sperreId}")
|
||||
@@ -49,16 +59,39 @@ public class SperreController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(sperre.getGruppeId()).orElse(null);
|
||||
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
|
||||
if (gruppeEntity == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
SperreEntity entity = SperreEntity.create(sperre, gruppeEntity);
|
||||
if (gruppeEntity.getSperren().size() >= 100) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
List<ToyEntity> toys = resolveToys(sperre.getBenoetigteToys());
|
||||
SperreEntity entity = SperreEntity.create(sperre, gruppeEntity, toys);
|
||||
sperreRepository.save(entity);
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getSperreId()).toUri()
|
||||
).build();
|
||||
}
|
||||
|
||||
@PutMapping("/{sperreId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID sperreId, @RequestBody Sperre sperre) {
|
||||
if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null
|
||||
|| sperre.getSperreFuer() == null || sperre.getSperreFuer().isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
SperreEntity entity = sperreRepository.findById(sperreId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
entity.setKurzText(sperre.getKurzText());
|
||||
entity.setText(sperre.getText());
|
||||
entity.setReleaseText(sperre.getReleaseText());
|
||||
entity.setMinutenVon(sperre.getMinutenVon());
|
||||
entity.setMinutenBis(sperre.getMinutenBis());
|
||||
entity.setSperreFuer(sperre.getSperreFuer());
|
||||
entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys()));
|
||||
sperreRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<Void> delete(@RequestBody Sperre sperre) {
|
||||
try {
|
||||
@@ -69,4 +102,14 @@ public class SperreController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package de.oaa.xxx.aufgaben.controller;
|
||||
|
||||
import de.oaa.xxx.aufgaben.Strafe;
|
||||
import de.oaa.xxx.aufgaben.Toy;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.StrafeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -13,11 +16,14 @@ 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
|
||||
@@ -29,10 +35,14 @@ public class StrafeController {
|
||||
|
||||
private final StrafeRepository strafeRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
|
||||
public StrafeController(StrafeRepository strafeRepository, AufgabenGruppeRepository gruppeRepository) {
|
||||
public StrafeController(StrafeRepository strafeRepository,
|
||||
AufgabenGruppeRepository gruppeRepository,
|
||||
ToyRepository toyRepository) {
|
||||
this.strafeRepository = strafeRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{strafeId}")
|
||||
@@ -48,16 +58,39 @@ public class StrafeController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(strafe.getGruppeId()).orElse(null);
|
||||
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
|
||||
if (gruppeEntity == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
StrafeEntity entity = StrafeEntity.create(strafe, gruppeEntity);
|
||||
if (gruppeEntity.getStrafen().size() >= 100) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
List<ToyEntity> toys = resolveToys(strafe.getBenoetigteToys());
|
||||
StrafeEntity entity = StrafeEntity.create(strafe, gruppeEntity, toys);
|
||||
strafeRepository.save(entity);
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getStrafeId()).toUri()
|
||||
).build();
|
||||
}
|
||||
|
||||
@PutMapping("/{strafeId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID strafeId, @RequestBody Strafe strafe) {
|
||||
if (strafe.getKurzText() == null || strafe.getText() == null || strafe.getLevel() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
StrafeEntity entity = strafeRepository.findById(strafeId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
entity.setKurzText(strafe.getKurzText());
|
||||
entity.setText(strafe.getText());
|
||||
entity.setLevel(strafe.getLevel());
|
||||
entity.setSekundenVon(strafe.getSekundenVon());
|
||||
entity.setSekundenBis(strafe.getSekundenBis());
|
||||
entity.setBenoetigtAktiv(strafe.getBenoetigtAktiv());
|
||||
entity.setBenoetigtPassiv(strafe.getBenoetigtPassiv());
|
||||
entity.setBenoetigteToys(resolveToys(strafe.getBenoetigteToys()));
|
||||
strafeRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<Void> delete(@RequestBody Strafe strafe) {
|
||||
try {
|
||||
@@ -68,4 +101,14 @@ public class StrafeController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
package de.oaa.xxx.aufgaben.controller;
|
||||
|
||||
import de.oaa.xxx.aufgaben.Toy;
|
||||
import de.oaa.xxx.aufgaben.ToyPage;
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@@ -26,13 +41,83 @@ import java.util.UUID;
|
||||
public class ToyController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ToyController.class);
|
||||
private static final int DEFAULT_PAGE_SIZE = 12;
|
||||
|
||||
private final ToyRepository toyRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GruppenAboRepository aboRepository;
|
||||
|
||||
public ToyController(ToyRepository toyRepository, AufgabenGruppeRepository gruppeRepository) {
|
||||
public ToyController(ToyRepository toyRepository,
|
||||
UserRepository userRepository,
|
||||
GruppenAboRepository aboRepository) {
|
||||
this.toyRepository = toyRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.aboRepository = aboRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/list/user")
|
||||
public ResponseEntity<ToyPage> listUser(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size,
|
||||
Principal principal) {
|
||||
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
Page<ToyEntity> result = toyRepository.findByUserId(
|
||||
user.getUserId(), PageRequest.of(page, size, Sort.by("name")));
|
||||
return ResponseEntity.ok(toToyPage(result));
|
||||
}
|
||||
|
||||
@GetMapping("/list/system")
|
||||
public ResponseEntity<ToyPage> listSystem(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "" + DEFAULT_PAGE_SIZE) int size) {
|
||||
Page<ToyEntity> result = toyRepository.findByUserIdIsNull(
|
||||
PageRequest.of(page, size, Sort.by("name")));
|
||||
return ResponseEntity.ok(toToyPage(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all toys available to the current user for assignment to items:
|
||||
* own toys + system toys + toys referenced in subscribed groups' items.
|
||||
*/
|
||||
@GetMapping("/available")
|
||||
public ResponseEntity<List<Toy>> available(Principal principal) {
|
||||
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
List<ToyEntity> own = toyRepository.findByUserId(user.getUserId(), PageRequest.of(0, 500, Sort.by("name"))).getContent();
|
||||
List<ToyEntity> system = toyRepository.findByUserIdIsNull(PageRequest.of(0, 500, Sort.by("name"))).getContent();
|
||||
|
||||
Set<UUID> knownIds = new HashSet<>();
|
||||
own.forEach(t -> knownIds.add(t.getToyId()));
|
||||
system.forEach(t -> knownIds.add(t.getToyId()));
|
||||
|
||||
Set<ToyEntity> fromAbos = new HashSet<>();
|
||||
aboRepository.findByUserId(user.getUserId()).forEach(abo -> {
|
||||
AufgabenGruppeEntity gruppe = abo.getAufgabenGruppe();
|
||||
gruppe.getAufgaben().forEach(a -> {
|
||||
if (a.getBenoetigteToys() != null)
|
||||
a.getBenoetigteToys().stream().filter(t -> !knownIds.contains(t.getToyId())).forEach(fromAbos::add);
|
||||
});
|
||||
gruppe.getStrafen().forEach(s -> {
|
||||
if (s.getBenoetigteToys() != null)
|
||||
s.getBenoetigteToys().stream().filter(t -> !knownIds.contains(t.getToyId())).forEach(fromAbos::add);
|
||||
});
|
||||
gruppe.getSperren().forEach(sp -> {
|
||||
if (sp.getBenoetigteToys() != null)
|
||||
sp.getBenoetigteToys().stream().filter(t -> !knownIds.contains(t.getToyId())).forEach(fromAbos::add);
|
||||
});
|
||||
});
|
||||
|
||||
List<Toy> result = new ArrayList<>();
|
||||
result.addAll(own.stream().map(ToyEntity::toToy).toList());
|
||||
result.addAll(system.stream().map(ToyEntity::toToy).toList());
|
||||
result.addAll(fromAbos.stream()
|
||||
.sorted(Comparator.comparing(ToyEntity::getName, String.CASE_INSENSITIVE_ORDER))
|
||||
.map(ToyEntity::toToy).toList());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{toyId}")
|
||||
@@ -43,31 +128,120 @@ public class ToyController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> create(@RequestBody Toy toy) {
|
||||
if (toy.getName() == null || toy.getGruppeId() == null) {
|
||||
public ResponseEntity<Void> create(@RequestBody Toy toy, Principal principal) {
|
||||
if (toy.getName() == null || toy.getName().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(toy.getGruppeId()).orElse(null);
|
||||
if (gruppeEntity == null || gruppeEntity.getAufgaben().size() > 50) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
ToyEntity entity = ToyEntity.create(toy, gruppeEntity);
|
||||
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNull(toy.getName())
|
||||
|| toyRepository.existsByNameIgnoreCaseAndUserId(toy.getName(), user.getUserId())) {
|
||||
return ResponseEntity.status(409)
|
||||
.header("X-Error", "duplicate-name")
|
||||
.build();
|
||||
}
|
||||
ToyEntity entity = ToyEntity.create(toy);
|
||||
entity.setUserId(user.getUserId());
|
||||
toyRepository.save(entity);
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getToyId()).toUri()
|
||||
).build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@Transactional
|
||||
public ResponseEntity<Void> delete(@RequestBody Toy toy) {
|
||||
// Bug fix: original code had transaction.rollback() here - now correctly uses @Transactional
|
||||
@PostMapping("/copy/{toyId}")
|
||||
public ResponseEntity<Void> copy(@PathVariable UUID toyId, Principal principal) {
|
||||
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
ToyEntity source = toyRepository.findById(toyId).orElse(null);
|
||||
if (source == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (source.getUserId() != null) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
if (toyRepository.existsByNameIgnoreCaseAndUserId(source.getName(), user.getUserId())) {
|
||||
return ResponseEntity.status(409)
|
||||
.header("X-Error", "duplicate-name")
|
||||
.build();
|
||||
}
|
||||
ToyEntity copy = new ToyEntity();
|
||||
copy.setToyId(UUID.randomUUID());
|
||||
copy.setName(source.getName());
|
||||
copy.setBeschreibung(source.getBeschreibung());
|
||||
copy.setUserId(user.getUserId());
|
||||
copy.setBild(source.getBild());
|
||||
toyRepository.save(copy);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@PutMapping("/{toyId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID toyId, @RequestBody Toy toy, Principal principal) {
|
||||
if (toy.getName() == null || toy.getName().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
ToyEntity entity = toyRepository.findById(toyId).orElse(null);
|
||||
if (entity == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (!user.getUserId().equals(entity.getUserId())) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
if (toyRepository.existsByNameIgnoreCaseAndUserIdIsNullAndToyIdNot(toy.getName(), toyId)
|
||||
|| toyRepository.existsByNameIgnoreCaseAndUserIdAndToyIdNot(toy.getName(), user.getUserId(), toyId)) {
|
||||
return ResponseEntity.status(409)
|
||||
.header("X-Error", "duplicate-name")
|
||||
.build();
|
||||
}
|
||||
entity.setName(toy.getName().trim());
|
||||
entity.setBeschreibung(toy.getBeschreibung());
|
||||
if (toy.getBild() != null) {
|
||||
entity.setBild(Base64.getDecoder().decode(toy.getBild()));
|
||||
}
|
||||
toyRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{toyId}")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID toyId, Principal principal) {
|
||||
UserEntity user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
ToyEntity toy = toyRepository.findById(toyId).orElse(null);
|
||||
if (toy == null) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
if (!user.getUserId().equals(toy.getUserId())) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
if (toyRepository.countAufgabeUsage(toyId) > 0
|
||||
|| toyRepository.countStrafeUsage(toyId) > 0
|
||||
|| toyRepository.countSperreUsage(toyId) > 0) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
try {
|
||||
toyRepository.findById(toy.getToyId()).ifPresent(toyRepository::delete);
|
||||
toyRepository.delete(toy);
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
private ToyPage toToyPage(Page<ToyEntity> page) {
|
||||
ToyPage toyPage = new ToyPage();
|
||||
toyPage.setContent(page.getContent().stream().map(ToyEntity::toToy).toList());
|
||||
toyPage.setCurrentPage(page.getNumber());
|
||||
toyPage.setTotalPages(page.getTotalPages());
|
||||
toyPage.setTotalElements(page.getTotalElements());
|
||||
return toyPage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -98,12 +99,12 @@ public class AufgabeEntity {
|
||||
return aufgabe;
|
||||
}
|
||||
|
||||
public static AufgabeEntity create(Aufgabe aufgabe, AufgabenGruppeEntity aufgabenGruppeEntity) {
|
||||
public static AufgabeEntity create(Aufgabe aufgabe, AufgabenGruppeEntity aufgabenGruppeEntity, List<ToyEntity> toys) {
|
||||
AufgabeEntity entity = new AufgabeEntity();
|
||||
entity.setAufgabeId(UUID.randomUUID());
|
||||
entity.setAufgabenGruppe(aufgabenGruppeEntity);
|
||||
entity.setBenoetigtAktiv(aufgabe.getBenoetigtAktiv());
|
||||
entity.setBenoetigteToys(aufgabe.getBenoetigteToys().stream().map(toy -> ToyEntity.create(toy, aufgabenGruppeEntity)).toList());
|
||||
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
|
||||
entity.setBenoetigtPassiv(aufgabe.getBenoetigtPassiv());
|
||||
entity.setKurzText(aufgabe.getKurzText());
|
||||
entity.setLevel(aufgabe.getLevel());
|
||||
|
||||
@@ -33,10 +33,6 @@ public class AufgabenGruppeEntity {
|
||||
private byte[] bild;
|
||||
@Column
|
||||
private String von;
|
||||
@Column
|
||||
private Integer relevanz;
|
||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||
private List<ToyEntity> toys;
|
||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||
private List<AufgabeEntity> aufgaben;
|
||||
@OneToMany(mappedBy = "aufgabenGruppe")
|
||||
@@ -65,12 +61,6 @@ public class AufgabenGruppeEntity {
|
||||
public String getVon() { return von; }
|
||||
public void setVon(String von) { this.von = von; }
|
||||
|
||||
public Integer getRelevanz() { return relevanz; }
|
||||
public void setRelevanz(Integer relevanz) { this.relevanz = relevanz; }
|
||||
|
||||
public List<ToyEntity> getToys() { return toys; }
|
||||
public void setToys(List<ToyEntity> toys) { this.toys = toys; }
|
||||
|
||||
public List<AufgabeEntity> getAufgaben() { return aufgaben; }
|
||||
public void setAufgaben(List<AufgabeEntity> aufgaben) { this.aufgaben = aufgaben; }
|
||||
|
||||
@@ -89,7 +79,6 @@ public class AufgabenGruppeEntity {
|
||||
gruppe.setPrivateGruppe(privateGruppe);
|
||||
gruppe.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
||||
gruppe.setVon(von);
|
||||
gruppe.setToys(toys.stream().map(ToyEntity::toToy).toList());
|
||||
gruppe.setAufgaben(aufgaben.stream().map(AufgabeEntity::toAufgabe).toList());
|
||||
gruppe.setStrafen(strafen.stream().map(StrafeEntity::toStrafe).toList());
|
||||
gruppe.setSperren(sperren.stream().map(SperreEntity::toSperre).toList());
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.aufgaben.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gruppen_abo")
|
||||
public class GruppenAboEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID aboId;
|
||||
|
||||
@Column
|
||||
private UUID userId;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "gruppenId")
|
||||
private AufgabenGruppeEntity aufgabenGruppe;
|
||||
|
||||
public UUID getAboId() { return aboId; }
|
||||
public void setAboId(UUID aboId) { this.aboId = aboId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; }
|
||||
public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; }
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -85,14 +86,15 @@ public class SperreEntity {
|
||||
sperre.setReleaseText(releaseText);
|
||||
sperre.setSperreFuer(sperreFuer);
|
||||
sperre.setText(text);
|
||||
sperre.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
|
||||
return sperre;
|
||||
}
|
||||
|
||||
public static SperreEntity create(Sperre sperre, AufgabenGruppeEntity aufgabenGruppeEntity) {
|
||||
public static SperreEntity create(Sperre sperre, AufgabenGruppeEntity aufgabenGruppeEntity, List<ToyEntity> toys) {
|
||||
SperreEntity entity = new SperreEntity();
|
||||
entity.setSperreId(UUID.randomUUID());
|
||||
entity.setAufgabenGruppe(aufgabenGruppeEntity);
|
||||
entity.setBenoetigteToys(sperre.getBenoetigteToys().stream().map(toy -> ToyEntity.create(toy, aufgabenGruppeEntity)).toList());
|
||||
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
|
||||
entity.setKurzText(sperre.getKurzText());
|
||||
entity.setMinutenBis(sperre.getMinutenBis());
|
||||
entity.setMinutenVon(sperre.getMinutenVon());
|
||||
|
||||
@@ -16,6 +16,7 @@ import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -98,12 +99,12 @@ public class StrafeEntity {
|
||||
return strafe;
|
||||
}
|
||||
|
||||
public static StrafeEntity create(Strafe strafe, AufgabenGruppeEntity aufgabenGruppeEntity) {
|
||||
public static StrafeEntity create(Strafe strafe, AufgabenGruppeEntity aufgabenGruppeEntity, List<ToyEntity> toys) {
|
||||
StrafeEntity entity = new StrafeEntity();
|
||||
entity.setStrafeId(UUID.randomUUID());
|
||||
entity.setAufgabenGruppe(aufgabenGruppeEntity);
|
||||
entity.setBenoetigtAktiv(strafe.getBenoetigtAktiv());
|
||||
entity.setBenoetigteToys(strafe.getBenoetigteToys().stream().map(toy -> ToyEntity.create(toy, aufgabenGruppeEntity)).toList());
|
||||
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
|
||||
entity.setBenoetigtPassiv(strafe.getBenoetigtPassiv());
|
||||
entity.setKurzText(strafe.getKurzText());
|
||||
entity.setLevel(strafe.getLevel());
|
||||
|
||||
@@ -4,10 +4,10 @@ import de.oaa.xxx.aufgaben.Toy;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@@ -21,9 +21,11 @@ public class ToyEntity {
|
||||
private String name;
|
||||
@Column
|
||||
private String beschreibung;
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "gruppeId")
|
||||
private AufgabenGruppeEntity aufgabenGruppe;
|
||||
@Column
|
||||
private UUID userId;
|
||||
@Lob
|
||||
@Column(columnDefinition = "BLOB")
|
||||
private byte[] bild;
|
||||
|
||||
public UUID getToyId() { return toyId; }
|
||||
public void setToyId(UUID toyId) { this.toyId = toyId; }
|
||||
@@ -34,24 +36,29 @@ public class ToyEntity {
|
||||
public String getBeschreibung() { return beschreibung; }
|
||||
public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
|
||||
|
||||
public AufgabenGruppeEntity getAufgabenGruppe() { return aufgabenGruppe; }
|
||||
public void setAufgabenGruppe(AufgabenGruppeEntity aufgabenGruppe) { this.aufgabenGruppe = aufgabenGruppe; }
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public byte[] getBild() { return bild; }
|
||||
public void setBild(byte[] bild) { this.bild = bild; }
|
||||
|
||||
public Toy toToy() {
|
||||
Toy toy = new Toy();
|
||||
toy.setBeschreibung(beschreibung);
|
||||
toy.setName(name);
|
||||
toy.setGruppeId(aufgabenGruppe.getGruppenId());
|
||||
toy.setToyId(toyId);
|
||||
toy.setName(name);
|
||||
toy.setBeschreibung(beschreibung);
|
||||
toy.setUserId(userId);
|
||||
toy.setBild(bild != null ? Base64.getEncoder().encodeToString(bild) : null);
|
||||
return toy;
|
||||
}
|
||||
|
||||
public static ToyEntity create(Toy toy, AufgabenGruppeEntity aufgabenGruppeEntity) {
|
||||
public static ToyEntity create(Toy toy) {
|
||||
ToyEntity entity = new ToyEntity();
|
||||
entity.setAufgabenGruppe(aufgabenGruppeEntity);
|
||||
entity.setBeschreibung(toy.getBeschreibung());
|
||||
entity.setName(toy.getName());
|
||||
entity.setToyId(UUID.randomUUID());
|
||||
entity.setName(toy.getName());
|
||||
entity.setBeschreibung(toy.getBeschreibung());
|
||||
entity.setUserId(toy.getUserId());
|
||||
entity.setBild(toy.getBild() != null ? Base64.getDecoder().decode(toy.getBild()) : null);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package de.oaa.xxx.aufgaben.repository;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
@@ -14,9 +16,18 @@ public interface AufgabenGruppeRepository extends JpaRepository<AufgabenGruppeEn
|
||||
@Query("select age from AufgabenGruppeEntity age where age.userId = :userId")
|
||||
List<AufgabenGruppeEntity> findByUserId(@Param("userId") UUID userId);
|
||||
|
||||
long countByUserId(UUID userId);
|
||||
|
||||
Page<AufgabenGruppeEntity> findByUserIdIsNull(Pageable pageable);
|
||||
|
||||
Page<AufgabenGruppeEntity> findByUserId(UUID userId, Pageable pageable);
|
||||
|
||||
@Query("select age from AufgabenGruppeEntity age where (age.privateGruppe = false or age.userId = :userId) and (:search is null or age.name like :search)")
|
||||
List<AufgabenGruppeEntity> listWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||
|
||||
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and (:search is null or age.name like :search)")
|
||||
List<AufgabenGruppeEntity> listPublicWithSearch(@Param("search") String search, PageRequest pageable);
|
||||
|
||||
@Query("select age from AufgabenGruppeEntity age where age.privateGruppe = false and age.userId is not null and age.userId <> :userId and (:name is null or lower(age.name) like lower(:name))")
|
||||
List<AufgabenGruppeEntity> findPublicFromOthers(@Param("userId") UUID userId, @Param("name") String name);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.oaa.xxx.aufgaben.repository;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.GruppenAboEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GruppenAboRepository extends JpaRepository<GruppenAboEntity, UUID> {
|
||||
|
||||
List<GruppenAboEntity> findByUserId(UUID userId);
|
||||
|
||||
boolean existsByUserIdAndAufgabenGruppe(UUID userId, AufgabenGruppeEntity gruppe);
|
||||
|
||||
void deleteByUserIdAndAufgabenGruppe(UUID userId, AufgabenGruppeEntity gruppe);
|
||||
|
||||
long countByAufgabenGruppe(AufgabenGruppeEntity gruppe);
|
||||
|
||||
void deleteByAufgabenGruppe(AufgabenGruppeEntity gruppe);
|
||||
}
|
||||
@@ -1,9 +1,37 @@
|
||||
package de.oaa.xxx.aufgaben.repository;
|
||||
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ToyRepository extends JpaRepository<ToyEntity, UUID> {
|
||||
|
||||
Page<ToyEntity> findByUserIdIsNull(Pageable pageable);
|
||||
|
||||
Page<ToyEntity> findByUserId(UUID userId, Pageable pageable);
|
||||
|
||||
boolean existsByNameIgnoreCaseAndUserIdIsNull(String name);
|
||||
|
||||
boolean existsByNameIgnoreCaseAndUserId(String name, UUID userId);
|
||||
|
||||
boolean existsByNameIgnoreCaseAndUserIdIsNullAndToyIdNot(String name, UUID toyId);
|
||||
|
||||
boolean existsByNameIgnoreCaseAndUserIdAndToyIdNot(String name, UUID userId, UUID toyId);
|
||||
|
||||
Optional<ToyEntity> findByNameIgnoreCaseAndUserId(String name, UUID userId);
|
||||
|
||||
@Query("SELECT COUNT(a) FROM AufgabeEntity a JOIN a.benoetigteToys t WHERE t.toyId = :toyId")
|
||||
long countAufgabeUsage(@Param("toyId") UUID toyId);
|
||||
|
||||
@Query("SELECT COUNT(s) FROM StrafeEntity s JOIN s.benoetigteToys t WHERE t.toyId = :toyId")
|
||||
long countStrafeUsage(@Param("toyId") UUID toyId);
|
||||
|
||||
@Query("SELECT COUNT(sp) FROM SperreEntity sp JOIN sp.benoetigteToys t WHERE t.toyId = :toyId")
|
||||
long countSperreUsage(@Param("toyId") UUID toyId);
|
||||
}
|
||||
|
||||
48
xxxthegame/src/main/java/de/oaa/xxx/config/JwtFilter.java
Normal file
48
xxxthegame/src/main/java/de/oaa/xxx/config/JwtFilter.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package de.oaa.xxx.config;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
@Component
|
||||
public class JwtFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtService jwtService;
|
||||
|
||||
public JwtFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if ("jwt".equals(cookie.getName())) {
|
||||
try {
|
||||
Claims claims = jwtService.validateAndGetClaims(cookie.getValue());
|
||||
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
|
||||
claims.getSubject(), null, Collections.emptyList()
|
||||
);
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
} catch (Exception e) {
|
||||
// Ungültiger oder abgelaufener Token – ohne Authentifizierung weiter
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
49
xxxthegame/src/main/java/de/oaa/xxx/config/JwtService.java
Normal file
49
xxxthegame/src/main/java/de/oaa/xxx/config/JwtService.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package de.oaa.xxx.config;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
|
||||
private static final long EXPIRATION_MS = 24L * 60 * 60 * 1000; // 24 Stunden
|
||||
|
||||
private final PrivateKey privateKey;
|
||||
private final PublicKey publicKey;
|
||||
|
||||
public JwtService(
|
||||
@Value("${jwt.keystore.path}") Resource keystoreResource,
|
||||
@Value("${jwt.keystore.password}") String password,
|
||||
@Value("${jwt.keystore.alias}") String alias) throws Exception {
|
||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||
keyStore.load(keystoreResource.getInputStream(), password.toCharArray());
|
||||
this.privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
|
||||
this.publicKey = keyStore.getCertificate(alias).getPublicKey();
|
||||
}
|
||||
|
||||
public String generateToken(String email, String name) {
|
||||
return Jwts.builder()
|
||||
.subject(email)
|
||||
.claim("name", name)
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
|
||||
.signWith(privateKey)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Claims validateAndGetClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(publicKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,17 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
public SecurityConfig() {
|
||||
private final JwtFilter jwtFilter;
|
||||
|
||||
public SecurityConfig(JwtFilter jwtFilter) {
|
||||
this.jwtFilter = jwtFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -23,9 +26,16 @@ public class SecurityConfig {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.exceptionHandling(ex -> ex
|
||||
.authenticationEntryPoint((request, response, authException) ->
|
||||
response.sendRedirect("/login.html")))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/userhome.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/toys.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/aufgaben.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/entdecken.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.html")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/css/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/js/**")).permitAll()
|
||||
@@ -40,7 +50,8 @@ public class SecurityConfig {
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/activation/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/filler")).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
)
|
||||
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.config.JwtService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -12,7 +13,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/login")
|
||||
@@ -21,23 +25,43 @@ public class LoginController {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public LoginController(UserRepository userRepository) {
|
||||
public LoginController(UserRepository userRepository, JwtService jwtService) {
|
||||
this.userRepository = userRepository;
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<User> login(@RequestParam String email, @RequestParam String hash,
|
||||
HttpServletRequest req) {
|
||||
HttpServletResponse response) {
|
||||
Optional<UserEntity> user = userRepository.findByEmailAndPassword(email, hash);
|
||||
if (user.isPresent()) {
|
||||
LOGGER.info("User erfolgreich angemeldet: {}", email);
|
||||
String token = jwtService.generateToken(user.get().getEmail(), user.get().getName());
|
||||
ResponseCookie cookie = ResponseCookie.from("jwt", token)
|
||||
.httpOnly(true)
|
||||
.sameSite("Strict")
|
||||
.path("/")
|
||||
.maxAge(Duration.ofHours(24))
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
return ResponseEntity.ok(user.get().toUser());
|
||||
} else {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<User> me(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
return userRepository.findByEmail(principal.getName())
|
||||
.map(entity -> ResponseEntity.ok(entity.toUser()))
|
||||
.orElse(ResponseEntity.status(401).build());
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public ResponseEntity<User> get(@PathVariable UUID userId) {
|
||||
return userRepository.findById(userId)
|
||||
|
||||
1584
xxxthegame/src/main/resources/static/aufgaben.html
Normal file
1584
xxxthegame/src/main/resources/static/aufgaben.html
Normal file
File diff suppressed because it is too large
Load Diff
603
xxxthegame/src/main/resources/static/entdecken.html
Normal file
603
xxxthegame/src/main/resources/static/entdecken.html
Normal file
@@ -0,0 +1,603 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Entdecken – XXX The Game</title>
|
||||
<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;
|
||||
}
|
||||
.search-bar input[type="text"] {
|
||||
flex: 1; padding: 0.55rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
background: var(--color-card); color: var(--color-text);
|
||||
font-size: 0.95rem; outline: none; transition: border-color 0.2s;
|
||||
}
|
||||
.search-bar input[type="text"]:focus { border-color: var(--color-primary); }
|
||||
.search-bar input[type="text"]::placeholder { color: var(--color-muted); }
|
||||
.btn-search {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.btn-search:hover { background: var(--color-primary); color: #fff; }
|
||||
|
||||
/* ── Paging ── */
|
||||
.paging {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; margin-top: 1rem;
|
||||
}
|
||||
.paging button {
|
||||
background: var(--color-secondary); color: var(--color-text);
|
||||
border: none; border-radius: 6px; padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem; cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.paging button:hover:not(:disabled) { background: var(--color-primary); }
|
||||
.paging button:disabled { opacity: 0.35; cursor: default; }
|
||||
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
|
||||
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Gruppe card ── */
|
||||
.gruppe-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.gruppe-card {
|
||||
background: var(--color-card); border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px; overflow: hidden; transition: border-color 0.15s;
|
||||
}
|
||||
.gruppe-card.open { border-color: rgba(233,69,96,0.35); }
|
||||
.gruppe-header {
|
||||
display: flex; align-items: center; gap: 0.9rem;
|
||||
padding: 0.85rem 1rem; cursor: pointer; user-select: none;
|
||||
}
|
||||
.gruppe-img {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
object-fit: cover; flex-shrink: 0;
|
||||
}
|
||||
.gruppe-img-placeholder {
|
||||
width: 48px; height: 48px; border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem; flex-shrink: 0; color: var(--color-muted);
|
||||
}
|
||||
.gruppe-meta { flex: 1; min-width: 0; }
|
||||
.gruppe-name {
|
||||
font-size: 0.95rem; font-weight: 600; color: var(--color-text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.gruppe-info { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.2rem; }
|
||||
.gruppe-badges { display: flex; gap: 0.3rem; margin-top: 0.25rem; flex-wrap: wrap; }
|
||||
.gruppe-badge {
|
||||
font-size: 0.65rem; padding: 0.1rem 0.4rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-muted);
|
||||
}
|
||||
.gruppe-badge-sub { background: rgba(46,204,113,0.15); color: var(--color-success); }
|
||||
.gruppe-toggle { font-size: 0.75rem; color: var(--color-muted); flex-shrink: 0; transition: transform 0.2s; }
|
||||
.gruppe-card.open .gruppe-toggle { transform: rotate(90deg); }
|
||||
|
||||
/* ── Subscribe button ── */
|
||||
.btn-sub {
|
||||
background: none; border: 1px solid var(--color-secondary); border-radius: 6px;
|
||||
color: var(--color-muted); font-size: 0.8rem; padding: 0.3rem 0.75rem;
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
flex-shrink: 0; white-space: nowrap;
|
||||
}
|
||||
.btn-sub:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.btn-sub.subscribed {
|
||||
border-color: rgba(46,204,113,0.5); color: var(--color-success);
|
||||
}
|
||||
.btn-sub.subscribed:hover {
|
||||
border-color: var(--color-primary); color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.08);
|
||||
}
|
||||
.btn-sub:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* ── Gruppe body ── */
|
||||
.gruppe-body { border-top: 1px solid var(--color-secondary); padding: 1rem 1rem 0.75rem; }
|
||||
.gruppe-desc { font-size: 0.82rem; color: var(--color-muted); margin-bottom: 0.85rem; line-height: 1.5; }
|
||||
|
||||
.sub-section + .sub-section { margin-top: 0.85rem; }
|
||||
.sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
|
||||
.sub-section-title {
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
|
||||
text-transform: uppercase; color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── Items ── */
|
||||
.item-list { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.item { border-radius: 6px; background: var(--color-secondary); overflow: hidden; }
|
||||
.item-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.35rem 0.6rem;
|
||||
cursor: pointer; user-select: none; transition: background 0.12s;
|
||||
}
|
||||
.item-row:hover { background: rgba(255,255,255,0.04); }
|
||||
.item.open .item-row { background: rgba(233,69,96,0.08); }
|
||||
.item-text {
|
||||
color: var(--color-text); flex: 1; min-width: 0; font-size: 0.82rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.item-badges { display: flex; gap: 0.35rem; flex-shrink: 0; }
|
||||
.badge {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 20px;
|
||||
background: rgba(233,69,96,0.15); color: var(--color-primary); white-space: nowrap;
|
||||
}
|
||||
.badge-neutral { background: rgba(255,255,255,0.07); color: var(--color-muted); }
|
||||
|
||||
/* ── Item detail ── */
|
||||
.item-detail {
|
||||
display: none; padding: 0.5rem 0.6rem 0.6rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
font-size: 0.8rem; color: var(--color-muted); line-height: 1.55;
|
||||
}
|
||||
.item.open .item-detail { display: block; }
|
||||
.item-detail-text { margin-bottom: 0.4rem; color: var(--color-text); white-space: pre-wrap; }
|
||||
.item-detail-row { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.25rem; }
|
||||
.item-detail-label { font-size: 0.72rem; color: var(--color-muted); }
|
||||
.item-detail-chip {
|
||||
font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 20px;
|
||||
background: rgba(255,255,255,0.07); color: var(--color-text);
|
||||
}
|
||||
.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>
|
||||
|
||||
<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="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" maxlength="200">
|
||||
<button class="btn-search" id="searchBtn">Suchen</button>
|
||||
</div>
|
||||
<div id="loading" class="loading">Wird geladen…</div>
|
||||
<div id="groupList" class="gruppe-list"></div>
|
||||
<div class="paging" id="paging" style="display:none;">
|
||||
<button id="prevBtn">‹ Zurück</button>
|
||||
<span class="page-info" id="pageInfo"></span>
|
||||
<button id="nextBtn">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 0, totalPages = 1;
|
||||
let currentName = '';
|
||||
|
||||
// ── XSS ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── Auth ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.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';
|
||||
document.getElementById('groupList').innerHTML = '';
|
||||
document.getElementById('paging').style.display = 'none';
|
||||
const nameParam = currentName ? `&name=${encodeURIComponent(currentName)}` : '';
|
||||
fetch(`/abo/discover?page=${currentPage}&size=${PAGE_SIZE}${nameParam}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
totalPages = data.totalPages || 1;
|
||||
renderGroups(data.content || []);
|
||||
updatePaging(currentPage, totalPages);
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('loading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
const WERKZEUG_LABEL = {
|
||||
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
|
||||
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
|
||||
};
|
||||
|
||||
function werkzeugChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(w => `<span class="item-detail-chip">${esc(WERKZEUG_LABEL[w] || w)}</span>`).join('');
|
||||
}
|
||||
function toyChips(list) {
|
||||
if (!list || list.length === 0) return '';
|
||||
return list.map(t => `<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`).join('');
|
||||
}
|
||||
function formatSek(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} s`;
|
||||
if (von != null) return `ab ${von} s`;
|
||||
if (bis != null) return `bis ${bis} s`;
|
||||
return '';
|
||||
}
|
||||
function formatMin(von, bis) {
|
||||
if (von != null && bis != null) return `${von}–${bis} min`;
|
||||
if (von != null) return `ab ${von} min`;
|
||||
if (bis != null) return `bis ${bis} min`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Track which group card is open
|
||||
let openGroupId = null;
|
||||
// Track which item detail is open
|
||||
let openItemId = null;
|
||||
|
||||
function renderGroups(groups) {
|
||||
const list = document.getElementById('groupList');
|
||||
if (!groups || groups.length === 0) {
|
||||
list.innerHTML = '<p class="empty">Keine Gruppen gefunden.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = groups.map(g => {
|
||||
const aufgabenCount = (g.aufgaben || []).length;
|
||||
const strafeCount = (g.strafen || []).length;
|
||||
const sperreCount = (g.sperren || []).length;
|
||||
const counts = [
|
||||
aufgabenCount ? `${aufgabenCount} Aufgabe${aufgabenCount !== 1 ? 'n' : ''}` : '',
|
||||
strafeCount ? `${strafeCount} Strafe${strafeCount !== 1 ? 'n' : ''}` : '',
|
||||
sperreCount ? `${sperreCount} Zeitstrafe${sperreCount !== 1 ? 'n' : ''}` : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
const subLabel = g.subscribed
|
||||
? `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`
|
||||
: '';
|
||||
const subCount = g.subscriberCount > 0
|
||||
? `<span class="gruppe-badge">♥ ${g.subscriberCount} Abo${g.subscriberCount !== 1 ? 's' : ''}</span>`
|
||||
: '';
|
||||
|
||||
const subBtnClass = g.subscribed ? 'btn-sub subscribed' : 'btn-sub';
|
||||
const subBtnText = g.subscribed ? '♥ Abonniert' : '♥ Abonnieren';
|
||||
|
||||
return `
|
||||
<div class="gruppe-card" id="dgroup-${esc(g.gruppenId)}">
|
||||
<div class="gruppe-header">
|
||||
<div style="cursor:pointer; display:flex; align-items:center; gap:0.9rem; flex:1; min-width:0;"
|
||||
onclick="toggleGroup('${esc(g.gruppenId)}')">
|
||||
${g.bild
|
||||
? `<img class="gruppe-img" src="data:image/png;base64,${g.bild}" alt="${esc(g.name)}">`
|
||||
: `<div class="gruppe-img-placeholder">⊙</div>`}
|
||||
<div class="gruppe-meta">
|
||||
<div class="gruppe-name">${esc(g.name)}</div>
|
||||
<div class="gruppe-info">${g.von ? esc(g.von) + (counts ? ' · ' : '') : ''}${counts || 'Keine Einträge'}</div>
|
||||
${(subLabel || subCount) ? `<div class="gruppe-badges">${subCount}${subLabel}</div>` : ''}
|
||||
</div>
|
||||
<span class="gruppe-toggle">▶</span>
|
||||
</div>
|
||||
<button class="${subBtnClass}" id="subbtn-${esc(g.gruppenId)}"
|
||||
onclick="toggleSubscribe('${esc(g.gruppenId)}', this)">
|
||||
${subBtnText}
|
||||
</button>
|
||||
</div>
|
||||
<div class="gruppe-body" id="dbody-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), renderAufgabe)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), renderStrafe)}
|
||||
${renderSubSection('Zeitstrafen', sortByName(g.sperren || []), renderZeitstrafe)}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
openItemId = null;
|
||||
}
|
||||
|
||||
function renderSubSection(title, items, renderFn) {
|
||||
return `<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<span class="sub-section-title">${esc(title)} (${items.length})</span>
|
||||
</div>
|
||||
${items.length === 0
|
||||
? '<div class="sub-empty">Keine Einträge</div>'
|
||||
: `<div class="item-list">${items.map(item => renderFn(item)).join('')}</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderAufgabe(a) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (a.text) detailRows.push(`<div class="item-detail-text">${esc(a.text)}</div>`);
|
||||
if (a.benoetigtAktiv && a.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(a.benoetigtAktiv)}</div>`);
|
||||
if (a.benoetigtPassiv && a.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(a.benoetigtPassiv)}</div>`);
|
||||
if (a.benoetigteToys && a.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(a.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(a.aufgabeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(a.aufgabeId)}')">
|
||||
<span class="item-text">${esc(a.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStrafe(s) {
|
||||
const badges = [];
|
||||
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (s.text) detailRows.push(`<div class="item-detail-text">${esc(s.text)}</div>`);
|
||||
if (s.benoetigtAktiv && s.benoetigtAktiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Aktiv:</span>${werkzeugChips(s.benoetigtAktiv)}</div>`);
|
||||
if (s.benoetigtPassiv && s.benoetigtPassiv.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Passiv:</span>${werkzeugChips(s.benoetigtPassiv)}</div>`);
|
||||
if (s.benoetigteToys && s.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(s.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(s.strafeId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(s.strafeId)}')">
|
||||
<span class="item-text">${esc(s.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderZeitstrafe(z) {
|
||||
const badges = [];
|
||||
const zeit = formatMin(z.minutenVon, z.minutenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
||||
if (z.releaseText) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Bei Aufhebung:</span><span style="font-size:0.78rem; color:var(--color-text);">${esc(z.releaseText)}</span></div>`);
|
||||
if (z.sperreFuer && z.sperreFuer.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Sperrt:</span>${werkzeugChips(z.sperreFuer)}</div>`);
|
||||
if (z.benoetigteToys && z.benoetigteToys.length) detailRows.push(`<div class="item-detail-row"><span class="item-detail-label">Toys:</span>${toyChips(z.benoetigteToys)}</div>`);
|
||||
|
||||
return `<div class="item" id="ditem-${esc(z.sperreId)}">
|
||||
<div class="item-row" onclick="toggleItem('${esc(z.sperreId)}')">
|
||||
<span class="item-text">${esc(z.kurzText)}</span>
|
||||
${badges.length ? `<span class="item-badges">${badges.join('')}</span>` : ''}
|
||||
</div>
|
||||
${detailRows.length ? `<div class="item-detail">${detailRows.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Sort ──
|
||||
function sortByLevelThenName(items) {
|
||||
return items.slice().sort((a, b) => {
|
||||
const la = a.level ?? 999, lb = b.level ?? 999;
|
||||
if (la !== lb) return la - lb;
|
||||
return (a.kurzText || '').localeCompare(b.kurzText || '', 'de');
|
||||
});
|
||||
}
|
||||
function sortByName(items) {
|
||||
return items.slice().sort((a, b) => (a.kurzText || '').localeCompare(b.kurzText || '', 'de'));
|
||||
}
|
||||
|
||||
// ── Group toggle ──
|
||||
function toggleGroup(gruppenId) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
const body = document.getElementById('dbody-' + gruppenId);
|
||||
if (!card) return;
|
||||
if (card.classList.contains('open')) {
|
||||
card.classList.remove('open');
|
||||
body.style.display = 'none';
|
||||
if (openGroupId === gruppenId) openGroupId = null;
|
||||
} else {
|
||||
if (openGroupId) {
|
||||
const prev = document.getElementById('dgroup-' + openGroupId);
|
||||
const prevBody = document.getElementById('dbody-' + openGroupId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
if (prevBody) prevBody.style.display = 'none';
|
||||
}
|
||||
card.classList.add('open');
|
||||
body.style.display = 'block';
|
||||
openGroupId = gruppenId;
|
||||
openItemId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Item toggle ──
|
||||
function toggleItem(itemId) {
|
||||
if (openItemId === itemId) {
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.remove('open');
|
||||
openItemId = null;
|
||||
return;
|
||||
}
|
||||
if (openItemId) {
|
||||
const prev = document.getElementById('ditem-' + openItemId);
|
||||
if (prev) prev.classList.remove('open');
|
||||
}
|
||||
const el = document.getElementById('ditem-' + itemId);
|
||||
if (el) el.classList.add('open');
|
||||
openItemId = itemId;
|
||||
}
|
||||
|
||||
// ── Subscribe / Unsubscribe ──
|
||||
function toggleSubscribe(gruppenId, btn) {
|
||||
btn.disabled = true;
|
||||
const isSubscribed = btn.classList.contains('subscribed');
|
||||
const method = isSubscribed ? 'DELETE' : 'POST';
|
||||
fetch(`/abo/${gruppenId}`, { method })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201 || r.status === 202) {
|
||||
if (isSubscribed) {
|
||||
btn.classList.remove('subscribed');
|
||||
btn.textContent = '♥ Abonnieren';
|
||||
updateBadge(gruppenId, false);
|
||||
} else {
|
||||
btn.classList.add('subscribed');
|
||||
btn.textContent = '♥ Abonniert';
|
||||
updateBadge(gruppenId, true);
|
||||
}
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function updateBadge(gruppenId, subscribed) {
|
||||
const card = document.getElementById('dgroup-' + gruppenId);
|
||||
if (!card) return;
|
||||
const badgesEl = card.querySelector('.gruppe-badges');
|
||||
if (!badgesEl) return;
|
||||
const subBadge = badgesEl.querySelector('.gruppe-badge-sub');
|
||||
if (subscribed && !subBadge) {
|
||||
badgesEl.insertAdjacentHTML('beforeend', `<span class="gruppe-badge gruppe-badge-sub">♥ Abonniert</span>`);
|
||||
} else if (!subscribed && subBadge) {
|
||||
subBadge.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search ──
|
||||
document.getElementById('searchBtn').addEventListener('click', () => {
|
||||
currentName = document.getElementById('searchInput').value.trim();
|
||||
currentPage = 0;
|
||||
loadGroups();
|
||||
});
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('searchBtn').click();
|
||||
});
|
||||
|
||||
// ── Paging ──
|
||||
function updatePaging(current, total) {
|
||||
const el = document.getElementById('paging');
|
||||
if (total <= 1) { el.style.display = 'none'; return; }
|
||||
el.style.display = 'flex';
|
||||
document.getElementById('prevBtn').disabled = current === 0;
|
||||
document.getElementById('nextBtn').disabled = current >= total - 1;
|
||||
document.getElementById('pageInfo').textContent = `Seite ${current + 1} von ${total}`;
|
||||
}
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => {
|
||||
if (currentPage > 0) { currentPage--; loadGroups(); }
|
||||
});
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
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>
|
||||
780
xxxthegame/src/main/resources/static/toys.html
Normal file
780
xxxthegame/src/main/resources/static/toys.html
Normal file
@@ -0,0 +1,780 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Toys – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
body { display: block; min-height: 100vh; }
|
||||
|
||||
/* ── Layout ── */
|
||||
.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 ── */
|
||||
.content { padding: 2rem 1.5rem; flex: 1; }
|
||||
|
||||
/* ── Overlay ── */
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
margin: 0;
|
||||
}
|
||||
.btn-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-add:hover { background: #c73652; }
|
||||
|
||||
/* ── Toy grid ── */
|
||||
.toy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Toy card ── */
|
||||
.toy-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 0.8rem 0.9rem;
|
||||
transition: border-color 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
.toy-card { cursor: pointer; }
|
||||
.toy-card:hover { border-color: var(--color-primary); }
|
||||
.toy-card.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.06);
|
||||
}
|
||||
|
||||
.toy-img {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toy-img-placeholder {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 7px;
|
||||
background: var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.toy-info { flex: 1; min-width: 0; }
|
||||
.toy-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.toy-desc {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.2rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Section action buttons ── */
|
||||
.section-actions { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.btn-action {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.btn-action:disabled { opacity: 0.35; cursor: default; }
|
||||
.btn-action:not(:disabled):hover { background: var(--color-primary); color: #fff; }
|
||||
.btn-action-danger:not(:disabled):hover { background: rgba(233,69,96,0.18); color: var(--color-primary); }
|
||||
.action-error {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-primary);
|
||||
min-height: 1.1em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* ── Paging ── */
|
||||
.paging {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.paging button {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.paging button:hover:not(:disabled) { background: var(--color-primary); }
|
||||
.paging button:disabled { opacity: 0.35; cursor: default; }
|
||||
.paging .page-info { font-size: 0.85rem; color: var(--color-muted); }
|
||||
|
||||
/* ── Empty / Loading ── */
|
||||
.empty, .loading { color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
|
||||
|
||||
/* ── Inline-Fehler im Grid ── */
|
||||
.grid-error {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-primary);
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* ── 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.open { display: flex; }
|
||||
.modal {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.6);
|
||||
}
|
||||
.modal h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.modal label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.modal input[type="text"],
|
||||
.modal textarea {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
resize: vertical;
|
||||
}
|
||||
.modal input[type="text"]:focus,
|
||||
.modal textarea:focus { border-color: var(--color-primary); }
|
||||
.modal input[type="file"] {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.modal-actions .btn-cancel {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-cancel:hover { background: #1a4a8a; }
|
||||
.modal-actions .btn-save {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.modal-actions .btn-save:hover { background: #c73652; }
|
||||
.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;
|
||||
}
|
||||
|
||||
/* ── 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; }
|
||||
.toy-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="overlay" id="overlay"></div>
|
||||
|
||||
<!-- Erstell-/Bearbeitungs-Modal -->
|
||||
<div class="modal-backdrop" id="createModal">
|
||||
<div class="modal">
|
||||
<h2 id="modalTitle">Neues Toy</h2>
|
||||
<label for="toyName">Name *</label>
|
||||
<input type="text" id="toyName" placeholder="z.B. Vibrator" maxlength="100">
|
||||
<label for="toyDesc">Beschreibung</label>
|
||||
<textarea id="toyDesc" rows="3" placeholder="Kurze Beschreibung…" maxlength="500"></textarea>
|
||||
<label>Bild (optional)</label>
|
||||
<div id="currentImageWrap" style="display:none; align-items:center; gap:0.5rem; margin-bottom:0.4rem;">
|
||||
<img id="currentImage" style="max-width:64px; max-height:64px; border-radius:6px;" src="" alt="">
|
||||
<span style="font-size:0.78rem; color:var(--color-muted);">Aktuelles Bild – neues Bild wählen zum Ersetzen</span>
|
||||
</div>
|
||||
<input type="file" id="toyBild" accept="image/*">
|
||||
<div class="modal-error" id="modalError"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" id="cancelBtn">Abbrechen</button>
|
||||
<button class="btn-save" id="saveBtn">Speichern</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"><span class="icon">✓</span> Aufgaben</a></li>
|
||||
<li><a href="#"><span class="icon">⚡</span> Strafen</a></li>
|
||||
<li><a href="/toys.html" class="active"><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>Toys</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<!-- Meine Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Meine Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="editBtn" disabled>✎ Bearbeiten</button>
|
||||
<button class="btn-action btn-action-danger" id="deleteBtn" disabled>✕ Löschen</button>
|
||||
<button class="btn-add" id="openCreateBtn">+ Neu</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="actionError"></div>
|
||||
<div id="userLoading" class="loading">Wird geladen…</div>
|
||||
<div class="toy-grid" id="userGrid"></div>
|
||||
<div class="paging" id="userPaging" style="display:none;">
|
||||
<button id="userPrev">‹ Zurück</button>
|
||||
<span class="page-info" id="userPageInfo"></span>
|
||||
<button id="userNext">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System-Toys -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">System-Toys</h2>
|
||||
<div class="section-actions">
|
||||
<button class="btn-action" id="copyBtn" disabled>⊕ In meine Toys kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-error" id="systemActionError"></div>
|
||||
<div id="systemLoading" class="loading">Wird geladen…</div>
|
||||
<div class="toy-grid" id="systemGrid"></div>
|
||||
<div class="paging" id="systemPaging" style="display:none;">
|
||||
<button id="systemPrev">‹ Zurück</button>
|
||||
<span class="page-info" id="systemPageInfo"></span>
|
||||
<button id="systemNext">Weiter ›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PAGE_SIZE = 12;
|
||||
let userPage = 0, userTotalPages = 1;
|
||||
let systemPage = 0, systemTotalPages = 1;
|
||||
|
||||
// ── Auth + initial load ──
|
||||
fetch('/login/me')
|
||||
.then(r => { if (r.status === 401) { window.location.href = '/login.html'; return null; } return r.ok ? r.json() : null; })
|
||||
.then(user => { if (!user) return; loadUserToys(); loadSystemToys(); })
|
||||
.catch(() => { window.location.href = '/login.html'; });
|
||||
|
||||
// ── Load user toys ──
|
||||
function loadUserToys() {
|
||||
resetSelection();
|
||||
document.getElementById('userLoading').style.display = 'block';
|
||||
fetch(`/toy/list/user?page=${userPage}&size=${PAGE_SIZE}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
userTotalPages = data.totalPages || 1;
|
||||
renderGrid('userGrid', data.content, 'selectToy');
|
||||
updatePaging('userPaging', 'userPrev', 'userNext', 'userPageInfo', userPage, userTotalPages);
|
||||
document.getElementById('userLoading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('userLoading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Load system toys ──
|
||||
function loadSystemToys() {
|
||||
resetSystemSelection();
|
||||
document.getElementById('systemLoading').style.display = 'block';
|
||||
fetch(`/toy/list/system?page=${systemPage}&size=${PAGE_SIZE}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
systemTotalPages = data.totalPages || 1;
|
||||
renderGrid('systemGrid', data.content, 'selectSystemToy');
|
||||
updatePaging('systemPaging', 'systemPrev', 'systemNext', 'systemPageInfo', systemPage, systemTotalPages);
|
||||
document.getElementById('systemLoading').style.display = 'none';
|
||||
})
|
||||
.catch(() => { document.getElementById('systemLoading').textContent = 'Fehler beim Laden.'; });
|
||||
}
|
||||
|
||||
// ── Selection ──
|
||||
let selectedUserToyId = null;
|
||||
|
||||
function selectToy(toyId) {
|
||||
const prev = document.querySelector('#userGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedUserToyId === toyId) {
|
||||
selectedUserToyId = null;
|
||||
} else {
|
||||
selectedUserToyId = toyId;
|
||||
document.querySelector(`#userGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
const has = selectedUserToyId != null;
|
||||
document.getElementById('editBtn').disabled = !has;
|
||||
document.getElementById('deleteBtn').disabled = !has;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
selectedUserToyId = null;
|
||||
document.getElementById('editBtn').disabled = true;
|
||||
document.getElementById('deleteBtn').disabled = true;
|
||||
document.getElementById('actionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── System-Toy selection ──
|
||||
let selectedSystemToyId = null;
|
||||
|
||||
function selectSystemToy(toyId) {
|
||||
const prev = document.querySelector('#systemGrid .toy-card.selected');
|
||||
if (prev) prev.classList.remove('selected');
|
||||
if (selectedSystemToyId === toyId) {
|
||||
selectedSystemToyId = null;
|
||||
} else {
|
||||
selectedSystemToyId = toyId;
|
||||
document.querySelector(`#systemGrid .toy-card[data-id="${toyId}"]`).classList.add('selected');
|
||||
}
|
||||
document.getElementById('copyBtn').disabled = selectedSystemToyId == null;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
function resetSystemSelection() {
|
||||
selectedSystemToyId = null;
|
||||
document.getElementById('copyBtn').disabled = true;
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
}
|
||||
|
||||
// ── Copy system toy ──
|
||||
document.getElementById('copyBtn').addEventListener('click', () => {
|
||||
if (!selectedSystemToyId) return;
|
||||
const btn = document.getElementById('copyBtn');
|
||||
btn.disabled = true;
|
||||
fetch(`/toy/copy/${selectedSystemToyId}`, { method: 'POST' })
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
userPage = 0;
|
||||
loadUserToys();
|
||||
document.getElementById('systemActionError').textContent = '';
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Du hast bereits ein Toy mit diesem Namen.';
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
document.getElementById('systemActionError').textContent =
|
||||
'Fehler beim Kopieren (HTTP ' + r.status + ').';
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('systemActionError').textContent = 'Verbindungsfehler.';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Render a grid ──
|
||||
function renderGrid(gridId, toys, selectFn) {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!toys || toys.length === 0) {
|
||||
grid.innerHTML = '<p class="empty">Keine Einträge vorhanden.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = toys.map(toy => `
|
||||
<div class="toy-card" data-id="${esc(toy.toyId)}"
|
||||
${selectFn ? `onclick="${selectFn}('${esc(toy.toyId)}')"` : ''}>
|
||||
${toy.bild
|
||||
? `<img class="toy-img" src="data:image/png;base64,${toy.bild}" alt="${esc(toy.name)}">`
|
||||
: `<div class="toy-img-placeholder">◈</div>`}
|
||||
<div class="toy-info">
|
||||
<div class="toy-name">${esc(toy.name)}</div>
|
||||
${toy.beschreibung ? `<div class="toy-desc">${esc(toy.beschreibung)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ── Update paging controls ──
|
||||
function updatePaging(pagingId, prevId, nextId, infoId, current, total) {
|
||||
const el = document.getElementById(pagingId);
|
||||
if (total <= 1) { el.style.display = 'none'; return; }
|
||||
el.style.display = 'flex';
|
||||
document.getElementById(prevId).disabled = current === 0;
|
||||
document.getElementById(nextId).disabled = current >= total - 1;
|
||||
document.getElementById(infoId).textContent = `Seite ${current + 1} von ${total}`;
|
||||
}
|
||||
|
||||
// ── Paging button handlers ──
|
||||
document.getElementById('userPrev').addEventListener('click', () => { if (userPage > 0) { userPage--; loadUserToys(); } });
|
||||
document.getElementById('userNext').addEventListener('click', () => { if (userPage < userTotalPages - 1) { userPage++; loadUserToys(); } });
|
||||
document.getElementById('systemPrev').addEventListener('click', () => { if (systemPage > 0) { systemPage--; loadSystemToys(); } });
|
||||
document.getElementById('systemNext').addEventListener('click', () => { if (systemPage < systemTotalPages - 1) { systemPage++; loadSystemToys(); } });
|
||||
|
||||
// ── Header action buttons ──
|
||||
document.getElementById('editBtn').addEventListener('click', () => {
|
||||
if (selectedUserToyId) openModal(selectedUserToyId);
|
||||
});
|
||||
|
||||
document.getElementById('deleteBtn').addEventListener('click', () => {
|
||||
if (!selectedUserToyId) return;
|
||||
if (!confirm('Toy wirklich löschen?')) return;
|
||||
const btn = document.getElementById('deleteBtn');
|
||||
btn.disabled = true;
|
||||
const toyId = selectedUserToyId;
|
||||
fetch(`/toy/${toyId}`, { method: 'DELETE' })
|
||||
.then(r => {
|
||||
if (r.status === 409) {
|
||||
showActionError('Wird in Aufgaben verwendet – nicht löschbar.');
|
||||
btn.disabled = false;
|
||||
} else if (r.status === 403) {
|
||||
showActionError('Keine Berechtigung.');
|
||||
btn.disabled = false;
|
||||
} else if (r.ok || r.status === 202) {
|
||||
userPage = 0;
|
||||
loadUserToys();
|
||||
} else {
|
||||
showActionError('Fehler beim Löschen.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => { showActionError('Verbindungsfehler.'); btn.disabled = false; });
|
||||
});
|
||||
|
||||
function showActionError(msg) {
|
||||
const el = document.getElementById('actionError');
|
||||
el.textContent = msg;
|
||||
setTimeout(() => { if (el.textContent === msg) el.textContent = ''; }, 4000);
|
||||
}
|
||||
|
||||
// ── Create / Edit modal ──
|
||||
const modal = document.getElementById('createModal');
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
let currentEditId = null;
|
||||
|
||||
function openModal(editId) {
|
||||
currentEditId = editId || null;
|
||||
document.getElementById('modalError').style.display = 'none';
|
||||
document.getElementById('toyBild').value = '';
|
||||
if (currentEditId) {
|
||||
fetch(`/toy/${currentEditId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(toy => {
|
||||
if (!toy) return;
|
||||
document.getElementById('modalTitle').textContent = 'Toy bearbeiten';
|
||||
document.getElementById('toyName').value = toy.name || '';
|
||||
document.getElementById('toyDesc').value = toy.beschreibung || '';
|
||||
const imgWrap = document.getElementById('currentImageWrap');
|
||||
if (toy.bild) {
|
||||
document.getElementById('currentImage').src = 'data:image/png;base64,' + toy.bild;
|
||||
imgWrap.style.display = 'flex';
|
||||
} else {
|
||||
imgWrap.style.display = 'none';
|
||||
}
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
})
|
||||
.catch(() => alert('Fehler beim Laden des Toys.'));
|
||||
} else {
|
||||
document.getElementById('modalTitle').textContent = 'Neues Toy';
|
||||
document.getElementById('toyName').value = '';
|
||||
document.getElementById('toyDesc').value = '';
|
||||
document.getElementById('currentImageWrap').style.display = 'none';
|
||||
modal.classList.add('open');
|
||||
document.getElementById('toyName').focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
|
||||
|
||||
function closeModal() { modal.classList.remove('open'); }
|
||||
|
||||
function editToy(toyId) { openModal(toyId); }
|
||||
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const name = document.getElementById('toyName').value.trim();
|
||||
if (!name) {
|
||||
showModalError('Bitte einen Namen eingeben.');
|
||||
return;
|
||||
}
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichert…';
|
||||
|
||||
let bildBase64 = null;
|
||||
const fileInput = document.getElementById('toyBild');
|
||||
if (fileInput.files.length > 0) {
|
||||
bildBase64 = await toBase64(fileInput.files[0]);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
beschreibung: document.getElementById('toyDesc').value.trim() || null,
|
||||
bild: bildBase64
|
||||
};
|
||||
|
||||
const isEdit = currentEditId != null;
|
||||
fetch(isEdit ? `/toy/${currentEditId}` : '/toy', {
|
||||
method: isEdit ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(r => {
|
||||
if (r.ok || r.status === 201) {
|
||||
closeModal();
|
||||
userPage = 0;
|
||||
loadUserToys();
|
||||
} else if (r.status === 409 && r.headers.get('X-Error') === 'duplicate-name') {
|
||||
showModalError('Ein Toy mit diesem Namen existiert bereits.');
|
||||
} else {
|
||||
showModalError('Fehler beim Speichern (HTTP ' + r.status + ').');
|
||||
}
|
||||
})
|
||||
.catch(() => showModalError('Verbindungsfehler.'))
|
||||
.finally(() => { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; });
|
||||
});
|
||||
|
||||
function showModalError(msg) {
|
||||
const el = document.getElementById('modalError');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
function toBase64(file) {
|
||||
const MAX = 128;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// ── XSS-Schutz ──
|
||||
function esc(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
@@ -6,19 +6,274 @@
|
||||
<title>XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
body {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Layout ── */
|
||||
.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 ── */
|
||||
.content {
|
||||
padding: 2rem 1.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Overlay ── */
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
/* ── 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>
|
||||
<h1>XXX The Game</h1>
|
||||
<p id="greeting"></p>
|
||||
|
||||
<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="#" class="active"><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"><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>XXX The Game</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<p id="greeting"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const userJson = sessionStorage.getItem('user');
|
||||
if (!userJson) {
|
||||
window.location.href = '/login.html';
|
||||
} else {
|
||||
const user = JSON.parse(userJson);
|
||||
document.getElementById('greeting').textContent = 'Willkommen, ' + user.name + '!';
|
||||
// ── Auth check ──
|
||||
fetch('/login/me')
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login.html';
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(user => {
|
||||
if (user) {
|
||||
document.getElementById('greeting').textContent = 'Willkommen, ' + user.name + '!';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.href = '/login.html';
|
||||
});
|
||||
|
||||
// ── 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);
|
||||
|
||||
// Close on nav link click (mobile)
|
||||
sidebar.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 768) closeMenu();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user