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