Umsetzung Aufgabenverwaltung

This commit is contained in:
2026-03-02 15:33:35 +01:00
parent c922ef6668
commit abf85f66e4
43 changed files with 6617 additions and 2103 deletions

View File

@@ -1,5 +1,5 @@
#Sun Mar 01 19:35:41 CET 2026
#Mon Mar 02 07:06:09 CET 2026
display=\:0
host=Mario-Linux
process-id=147181
process-id=8641
user=mario

View File

@@ -519,10 +519,33 @@ Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-01 19:35:42.071
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!SESSION 2026-03-02 07:06:00.104 -----------------------------------------------
eclipse.buildId=4.38.0.20251204-0849
java.version=21.0.9
java.vendor=Eclipse Adoptium
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
Framework arguments: -product org.eclipse.epp.package.java.product
Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product
!ENTRY org.eclipse.jface 2 0 2026-03-01 19:57:46.028
!ENTRY ch.qos.logback.classic 1 0 2026-03-02 07:06:04.976
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY ch.qos.logback.classic 1 0 2026-03-02 07:06:09.939
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
!ENTRY org.eclipse.ui 2 0 2026-03-02 07:06:10.126
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-02 07:06:10.126
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui 2 0 2026-03-02 07:06:10.280
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-02 07:06:10.280
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.jface 2 0 2026-03-02 07:17:32.622
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-01 19:57:46.028
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-02 07:17:32.622
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
Binding(CTRL+SHIFT+T,
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
@@ -541,7 +564,7 @@ Binding(CTRL+SHIFT+T,
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)
!ENTRY org.eclipse.debug.core 4 125 2026-03-01 22:04:05.326
!ENTRY org.eclipse.debug.core 4 125 2026-03-02 07:17:32.817
!MESSAGE Error logged from Debug Core:
!STACK 0
java.io.IOException: Stream closed
@@ -553,3 +576,15 @@ java.io.IOException: Stream closed
at org.eclipse.debug.internal.core.OutputStreamMonitor.internalRead(OutputStreamMonitor.java:235)
at org.eclipse.debug.internal.core.OutputStreamMonitor.read(OutputStreamMonitor.java:211)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.lsp4e 2 0 2026-03-02 11:04:28.836
!MESSAGE Javadoc unavailable. Failed to obtain it.
!STACK 0
java.lang.InterruptedException
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096)
at org.eclipse.lsp4e.jdt.LSJavaHoverProvider.getHoverInfo2(LSJavaHoverProvider.java:66)
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:165)
at org.eclipse.jdt.internal.ui.text.java.hover.BestMatchHover.getHoverInfo2(BestMatchHover.java:131)
at org.eclipse.jdt.internal.ui.text.java.hover.JavaEditorTextHoverProxy.getHoverInfo2(JavaEditorTextHoverProxy.java:89)
at org.eclipse.jface.text.TextViewerHoverManager$1.run(TextViewerHoverManager.java:155)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,106 +1,105 @@
INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core
2318770678.index
721517855.index
1914043487.index
836499050.index
1295630681.index
997772539.index
2633787677.index
2769879155.index
3514612140.index
1546736044.index
3515611559.index
970087405.index
2181028596.index
2455962971.index
2070370209.index
2389383899.index
3738696963.index
9341915.index
2978566974.index
1990965588.index
1324521365.index
3912907421.index
1455171009.index
1205982295.index
504781245.index
2237645717.index
225562445.index
1453089870.index
1732769785.index
958756673.index
3372764815.index
3326580390.index
1502997292.index
1502879287.index
1633924572.index
4123041097.index
2519831052.index
2890245412.index
1965154635.index
519552992.index
2874180664.index
3108263030.index
4158338144.index
1653061733.index
2668411497.index
3972616808.index
3602551868.index
2982788279.index
1660713777.index
363836152.index
1256436118.index
2838468603.index
3135354350.index
2891161224.index
2047888269.index
2455882736.index
3758865325.index
2488355463.index
2240786275.index
2191830568.index
2403041570.index
1865797976.index
4195864863.index
1074122571.index
2609856074.index
2236377038.index
198314732.index
1781188320.index
380800336.index
1241285641.index
3662169204.index
3552156823.index
167025465.index
4134502745.index
1118739196.index
2332037983.index
2004806901.index
2503368578.index
2586591901.index
815902026.index
3416862923.index
2655170954.index
1872440599.index
690321491.index
289134298.index
2817101718.index
3547251881.index
808711116.index
3882180612.index
3842019335.index
1446719945.index
1872440599.index
2240786275.index
4150628576.index
2609698604.index
2927822381.index
2725629017.index
3718169413.index
2593736024.index
3154281632.index
2655170954.index
4195864863.index
2982788279.index
2609856074.index
2626965509.index
2398089967.index
1436262503.index
89143789.index
26273648.index
2390245932.index
2769879155.index
4134502745.index
2817101718.index
4158338144.index
519552992.index
2181028596.index
2503368578.index
1453089870.index
2593736024.index
721517855.index
815902026.index
3718169413.index
96642630.index
2488355463.index
1446719945.index
2891161224.index
1118739196.index
2047888269.index
3972616808.index
1205982295.index
1914043487.index
808711116.index
3154281632.index
2390245932.index
2191830568.index
1653061733.index
2586591901.index
2609698604.index
3882180612.index
3758865325.index
2070370209.index
2332037983.index
1732769785.index
2838468603.index
2668411497.index
3662169204.index
2927822381.index
2398089967.index
225562445.index
1436262503.index
1295630681.index
3135354350.index
3602551868.index
363836152.index
504781245.index
2633787677.index
1455171009.index
2725629017.index
3552156823.index
4123041097.index
1865797976.index
3372764815.index
2455962971.index
289134298.index
2389383899.index
26273648.index
3515611559.index
1241285641.index
2978566974.index
958756673.index
3108263030.index
2236377038.index
3547251881.index
2519831052.index
2874180664.index
1781188320.index
970087405.index
1074122571.index
380800336.index
167025465.index
1502997292.index
2237645717.index
3842019335.index
1965154635.index
2403041570.index
2455882736.index
3326580390.index
2004806901.index
836499050.index
3416862923.index
1502879287.index
3912907421.index
89143789.index
1546736044.index
2890245412.index
1990965588.index
3738696963.index
1660713777.index
3514612140.index
997772539.index
198314732.index
1324521365.index
1633924572.index
2318770678.index
1256436118.index

View File

@@ -1,3 +1,4 @@
2026-03-01 17:27:17,460 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is not available. Remote download required.
2026-03-01 18:42:13,387 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-01 19:35:44,100 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-02 07:06:13,818 [Worker-6: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.

View File

@@ -1,3 +1,3 @@
#Sun Mar 01 19:35:41 CET 2026
#Mon Mar 02 07:06:09 CET 2026
org.eclipse.core.runtime=2
org.eclipse.platform=4.38.0.v20251201-0920

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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];
}
}

View File

@@ -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; }
}

View 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; }
}

View 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; }
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());

View File

@@ -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());

View File

@@ -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; }
}

View File

@@ -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());

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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);
}
}

View 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 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>

View 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 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>

View File

@@ -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>