Feedacksystem hinzugefügt, Bugs in der Timelock behoben
@@ -39,7 +39,8 @@
|
|||||||
"Bash(head -15 grep -n -B5 -A2 \"max-width: 480px\\\\|max-width:480px\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/joinlock.html)",
|
"Bash(head -15 grep -n -B5 -A2 \"max-width: 480px\\\\|max-width:480px\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/joinlock.html)",
|
||||||
"Bash(stat /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/*)",
|
"Bash(stat /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/*)",
|
||||||
"Bash(git -C /home/mario/Workspaces/xxx-thegame diff HEAD xxxthegame/src/main/resources/static/neulock.html)",
|
"Bash(git -C /home/mario/Workspaces/xxx-thegame diff HEAD xxxthegame/src/main/resources/static/neulock.html)",
|
||||||
"Bash(1:*)"
|
"Bash(1:*)",
|
||||||
|
"Bash(python3:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Tue Mar 24 11:25:59 CET 2026
|
#Wed Mar 25 07:26:10 CET 2026
|
||||||
display=\:0
|
display=\:0
|
||||||
host=mario-mint
|
host=mario-mint
|
||||||
process-id=2972
|
process-id=4033
|
||||||
user=mario
|
user=mario
|
||||||
|
|||||||
@@ -683,3 +683,74 @@ java.lang.NullPointerException: Cannot invoke "org.eclipse.e4.ui.model.applicati
|
|||||||
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
|
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
|
||||||
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
|
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
|
||||||
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
|
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
|
||||||
|
|
||||||
|
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-24 22:52:54.244
|
||||||
|
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||||
|
!SESSION 2026-03-25 07:26:05.291 -----------------------------------------------
|
||||||
|
eclipse.buildId=4.39.0.20260305-0817
|
||||||
|
java.version=21.0.6
|
||||||
|
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 -clean -product org.eclipse.epp.package.java.product
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-25 07:26:06.750
|
||||||
|
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
|
||||||
|
|
||||||
|
!ENTRY ch.qos.logback.classic 1 0 2026-03-25 07:26:10.860
|
||||||
|
!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-25 07:26:11.009
|
||||||
|
!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-25 07:26:11.009
|
||||||
|
!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-25 07:26:11.149
|
||||||
|
!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-25 07:26:11.149
|
||||||
|
!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-25 11:50:50.380
|
||||||
|
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||||
|
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-25 11:50:50.380
|
||||||
|
!MESSAGE A conflict occurred for CTRL+R:
|
||||||
|
Binding(CTRL+R,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.debug.ui.commands.RunToLine,Run to Line,
|
||||||
|
Resume and break when execution reaches the current line,
|
||||||
|
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.debug.ui.commands.RunToLine"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.debug.ui.debugging,,,system)
|
||||||
|
Binding(CTRL+R,
|
||||||
|
ParameterizedCommand(Command(org.springframework.ide.eclipse.boot.restart.commands.restart,Trigger Restart,
|
||||||
|
Restart Spring Boot Application,
|
||||||
|
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.springframework.ide.eclipse.boot.restart.commands.restart"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.debug.ui.console,,,system)
|
||||||
|
|
||||||
|
!ENTRY org.eclipse.jface 2 0 2026-03-25 16:00:33.268
|
||||||
|
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
|
||||||
|
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-25 16:00:33.268
|
||||||
|
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
|
||||||
|
Open a type in a Java editor,
|
||||||
|
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
Binding(CTRL+SHIFT+T,
|
||||||
|
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
|
||||||
|
,
|
||||||
|
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
|
||||||
|
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
|
||||||
|
,,true),null),
|
||||||
|
org.eclipse.ui.defaultAcceleratorConfiguration,
|
||||||
|
org.eclipse.ui.contexts.window,,,system)
|
||||||
|
|
||||||
|
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-25 16:28:18.002
|
||||||
|
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
[ {
|
[ {
|
||||||
"version" : "9.6.0-20260323005019+0000",
|
"version" : "9.5.0-20260325015243+0000",
|
||||||
"buildTime" : "20260323005019+0000",
|
"buildTime" : "20260325015243+0000",
|
||||||
"commitId" : "d20e3feb4da17c82d2df8774ea838cdd230628ce",
|
"commitId" : "627839c6a3532eeab60306fd7b577d6f1c866ece",
|
||||||
"current" : false,
|
|
||||||
"snapshot" : true,
|
|
||||||
"nightly" : true,
|
|
||||||
"releaseNightly" : false,
|
|
||||||
"activeRc" : false,
|
|
||||||
"rcFor" : "",
|
|
||||||
"milestoneFor" : "",
|
|
||||||
"broken" : false,
|
|
||||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260323005019+0000-bin.zip",
|
|
||||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260323005019+0000-bin.zip.sha256",
|
|
||||||
"checksum" : "22865868bc4c8aa1f3cffb398397d3d6058246129490e168ecbbaaed24cd9fa1",
|
|
||||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260323005019+0000-wrapper.jar.sha256",
|
|
||||||
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
|
|
||||||
}, {
|
|
||||||
"version" : "9.5.0-20260322013634+0000",
|
|
||||||
"buildTime" : "20260322013634+0000",
|
|
||||||
"commitId" : "01db0eb99f616dd415a084ffcce4cb2c185d5a2a",
|
|
||||||
"current" : false,
|
"current" : false,
|
||||||
"snapshot" : true,
|
"snapshot" : true,
|
||||||
"nightly" : false,
|
"nightly" : false,
|
||||||
@@ -27,10 +10,27 @@
|
|||||||
"rcFor" : "",
|
"rcFor" : "",
|
||||||
"milestoneFor" : "",
|
"milestoneFor" : "",
|
||||||
"broken" : false,
|
"broken" : false,
|
||||||
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-bin.zip",
|
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip",
|
||||||
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-bin.zip.sha256",
|
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip.sha256",
|
||||||
"checksum" : "3e8a6689594399f81087ad962b1c489e0ae57201af0c6c00ea63d9d07e48506e",
|
"checksum" : "f031a868b2d9707fe07c78ad0888d4be5d6bb87e3f8a118ffbf3b0b497c32922",
|
||||||
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-wrapper.jar.sha256",
|
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-wrapper.jar.sha256",
|
||||||
|
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
|
||||||
|
}, {
|
||||||
|
"version" : "9.6.0-20260325005438+0000",
|
||||||
|
"buildTime" : "20260325005438+0000",
|
||||||
|
"commitId" : "4a2f60ed3e5db8c8eadc899d49da7c6abf7140ee",
|
||||||
|
"current" : false,
|
||||||
|
"snapshot" : true,
|
||||||
|
"nightly" : true,
|
||||||
|
"releaseNightly" : false,
|
||||||
|
"activeRc" : false,
|
||||||
|
"rcFor" : "",
|
||||||
|
"milestoneFor" : "",
|
||||||
|
"broken" : false,
|
||||||
|
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip",
|
||||||
|
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip.sha256",
|
||||||
|
"checksum" : "1419ec3a9f2e924772188c709cd681d073c7c821dbd82beb9a6e8f6b78f7723b",
|
||||||
|
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-wrapper.jar.sha256",
|
||||||
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
|
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
|
||||||
}, {
|
}, {
|
||||||
"version" : "9.4.1",
|
"version" : "9.4.1",
|
||||||
|
|||||||
@@ -2,4 +2,14 @@
|
|||||||
<typeInfoHistroy>
|
<typeInfoHistroy>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1772437686926"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1772437686926"/>
|
||||||
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben.controller{FillerController.java[FillerController" modifiers="1" timestamp="1772385528555"/>
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.aufgaben.controller{FillerController.java[FillerController" modifiers="1" timestamp="1772385528555"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockService.java[TimeLockService" modifiers="1" timestamp="1774441060175"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockService.java[TTLockService" modifiers="1" timestamp="1774375173709"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.user{UserRepository.java[UserRepository" modifiers="513" timestamp="1774016609131"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockUserConfigEntity.java[TTLockUserConfigEntity" modifiers="1" timestamp="1774425822887"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.keyholder{KeyholderNotificationEntity.java[KeyholderNotificationEntity" modifiers="1" timestamp="1774386563354"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.ttlock{TTLockCallback.java[TTLockCallback" modifiers="1" timestamp="1774387007874"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockController.java[CardLockController" modifiers="1" timestamp="1774422277518"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.cardlock{CardLockEntity.java[CardLockEntity" modifiers="1" timestamp="1774171624571"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.timelock{TimeLockController.java[TimeLockController" modifiers="1" timestamp="1774388746804"/>
|
||||||
|
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/<de.oaa.xxx.games.chastity.common{BaseLockEntity.java[BaseLockEntity" modifiers="1" timestamp="1774369125472"/>
|
||||||
</typeInfoHistroy>
|
</typeInfoHistroy>
|
||||||
|
|||||||
@@ -1,2 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<qualifiedTypeNameHistroy/>
|
<qualifiedTypeNameHistroy>
|
||||||
|
<fullyQualifiedTypeName name="jakarta.websocket.server.ServerEndpoint"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.stereotype.Service"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.http.HttpHeaders"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.RestController"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.RequestMapping"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTAuthService"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockService"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockTest"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.http.HttpStatusCode"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.CodeCreator"/>
|
||||||
|
<fullyQualifiedTypeName name="java.util.Random"/>
|
||||||
|
<fullyQualifiedTypeName name="java.util.Collections"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockService.TTLockDetailResponse"/>
|
||||||
|
<fullyQualifiedTypeName name="lombok.Data"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.http.ResponseEntity"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.PathVariable"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.GetMapping"/>
|
||||||
|
<fullyQualifiedTypeName name="java.lang.String"/>
|
||||||
|
<fullyQualifiedTypeName name="org.springframework.http.MediaType"/>
|
||||||
|
<fullyQualifiedTypeName name="java.util.Map"/>
|
||||||
|
<fullyQualifiedTypeName name="java.util.List"/>
|
||||||
|
<fullyQualifiedTypeName name="com.fasterxml.jackson.core.type.TypeReference"/>
|
||||||
|
<fullyQualifiedTypeName name="java.lang.Exception"/>
|
||||||
|
<fullyQualifiedTypeName name="java.time.LocalDateTime"/>
|
||||||
|
<fullyQualifiedTypeName name="java.time.Duration"/>
|
||||||
|
<fullyQualifiedTypeName name="java.util.Optional"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository"/>
|
||||||
|
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockCallback"/>
|
||||||
|
</qualifiedTypeNameHistroy>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<section name="Workbench">
|
<section name="Workbench">
|
||||||
|
<item key="filters_last_used" value="filter_imports;"/>
|
||||||
<section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart">
|
<section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart">
|
||||||
<item key="group_libraries" value="true"/>
|
<item key="group_libraries" value="true"/>
|
||||||
<item key="layout" value="1"/>
|
<item key="layout" value="1"/>
|
||||||
<item key="rootMode" value="1"/>
|
<item key="rootMode" value="1"/>
|
||||||
<item key="linkWithEditor" value="false"/>
|
<item key="linkWithEditor" value="true"/>
|
||||||
<item key="memento" value="<?xml version="1.0" encoding="UTF-8"?>
<packageExplorer group_libraries="1" layout="1" linkWithEditor="0" rootMode="1" workingSetName="Aggregate for window 1774277926242">
<customFilters userDefinedPatternsEnabled="false">
<xmlDefinedFilters>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.StaticsFilter" isEnabled="false"/>
<child filterId="org.eclipse.buildship.ui.packageexplorer.filter.gradle.buildfolder" isEnabled="true"/>
<child filterId="org.eclipse.mylyn.java.ui.MembersFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter" isEnabled="true"/>
<child filterId="org.eclipse.m2e.MavenModuleFilter" isEnabled="false"/>
<child filterId="org.eclipse.buildship.ui.packageexplorer.filter.gradle.subProject" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.DeprecatedMembersFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.FieldsFilter" isEnabled="false"/>
</xmlDefinedFilters>
</customFilters>
</packageExplorer>"/>
|
<item key="memento" value="<?xml version="1.0" encoding="UTF-8"?>
<packageExplorer group_libraries="1" layout="1" linkWithEditor="1" rootMode="1" workingSetName="Aggregate for window 1774277926242">
<customFilters userDefinedPatternsEnabled="false">
<xmlDefinedFilters>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.StaticsFilter" isEnabled="false"/>
<child filterId="org.eclipse.buildship.ui.packageexplorer.filter.gradle.buildfolder" isEnabled="true"/>
<child filterId="org.eclipse.mylyn.java.ui.MembersFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter" isEnabled="true"/>
<child filterId="org.eclipse.m2e.MavenModuleFilter" isEnabled="false"/>
<child filterId="org.eclipse.buildship.ui.packageexplorer.filter.gradle.subProject" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.DeprecatedMembersFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.FieldsFilter" isEnabled="false"/>
</xmlDefinedFilters>
</customFilters>
</packageExplorer>"/>
|
||||||
</section>
|
</section>
|
||||||
<section name="JavaElementSearchActions">
|
<section name="JavaElementSearchActions">
|
||||||
</section>
|
</section>
|
||||||
@@ -25,4 +26,54 @@
|
|||||||
</section>
|
</section>
|
||||||
<section name="quick_assist_proposal_size">
|
<section name="quick_assist_proposal_size">
|
||||||
</section>
|
</section>
|
||||||
|
<section name="NewClassCreationWizard.dialogBounds">
|
||||||
|
<item key="DIALOG_X_ORIGIN" value="981"/>
|
||||||
|
<item key="DIALOG_Y_ORIGIN" value="225"/>
|
||||||
|
<item key="DIALOG_WIDTH" value="599"/>
|
||||||
|
<item key="DIALOG_HEIGHT" value="689"/>
|
||||||
|
<item key="DIALOG_FONT_NAME" value="1|Ubuntu|10.0|0|GTK|1|"/>
|
||||||
|
</section>
|
||||||
|
<section name="NewPackageCreationWizard.dialogBounds">
|
||||||
|
<item key="DIALOG_X_ORIGIN" value="1011"/>
|
||||||
|
<item key="DIALOG_Y_ORIGIN" value="408"/>
|
||||||
|
<item key="DIALOG_WIDTH" value="539"/>
|
||||||
|
<item key="DIALOG_HEIGHT" value="500"/>
|
||||||
|
<item key="DIALOG_FONT_NAME" value="1|Ubuntu|10.0|0|GTK|1|"/>
|
||||||
|
</section>
|
||||||
|
<section name="NewPackageWizardPage">
|
||||||
|
<item key="create_package_info_java" value="false"/>
|
||||||
|
</section>
|
||||||
|
<section name="OptionalMessageDialog.hide.">
|
||||||
|
<item key="org.eclipse.jdt.ui.typecomment.deprecated" value="true"/>
|
||||||
|
</section>
|
||||||
|
<section name="NewClassWizardPage">
|
||||||
|
<item key="create_constructor" value="false"/>
|
||||||
|
<item key="create_unimplemented" value="true"/>
|
||||||
|
</section>
|
||||||
|
<section name="org.eclipse.jdt.internal.ui.text.QuickOutline">
|
||||||
|
<item key="GoIntoTopLevelTypeAction.isChecked" value="false"/>
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_WIDTH" value="400"/>
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_HEIGHT" value="340"/>
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_USE_PERSISTED_SIZE" value="true"/>
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_USE_PERSISTED_LOCATION" value="false"/>
|
||||||
|
</section>
|
||||||
|
<section name="RenameInformationPopup">
|
||||||
|
</section>
|
||||||
|
<section name="org.eclipse.ltk.ui.refactoring.settings">
|
||||||
|
<item key="updateSimilarElements" value="false"/>
|
||||||
|
<item key="updateSimilarElementsMatchStrategy" value="1"/>
|
||||||
|
<item key="updateTextualMatches" value="false"/>
|
||||||
|
<item key="updateQualifiedNames" value="false"/>
|
||||||
|
<item key="patterns" value="*"/>
|
||||||
|
</section>
|
||||||
|
<section name="org.eclipse.jdt.internal.ui.typehierarchy.QuickHierarchy">
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_WIDTH" value="400"/>
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_HEIGHT" value="340"/>
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_USE_PERSISTED_SIZE" value="true"/>
|
||||||
|
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_USE_PERSISTED_LOCATION" value="false"/>
|
||||||
|
</section>
|
||||||
|
<section name="RefactoringWizard.preview">
|
||||||
|
<item key="width" value="600"/>
|
||||||
|
<item key="height" value="400"/>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
2026-03-23 21:09:44,347 [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-23 21:09:44,347 [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-24 06:41:47,661 [Worker-2: 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-24 06:41:47,661 [Worker-2: 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-24 11:26:24,107 [Worker-2: 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-24 11:26:24,107 [Worker-2: 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-25 07:26:14,133 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#Tue Mar 24 11:25:59 CET 2026
|
#Wed Mar 25 07:26:10 CET 2026
|
||||||
org.eclipse.core.runtime=2
|
org.eclipse.core.runtime=2
|
||||||
org.eclipse.platform=4.39.0.v20260226-0420
|
org.eclipse.platform=4.39.0.v20260226-0420
|
||||||
|
|||||||
BIN
bilder/banner.png
Normal file
|
After Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -31,6 +31,10 @@ import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
|
|||||||
import de.oaa.xxx.aufgaben.repository.SperreRepository;
|
import de.oaa.xxx.aufgaben.repository.SperreRepository;
|
||||||
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
|
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
|
||||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||||
|
import de.oaa.xxx.feedback.FeedbackEntity;
|
||||||
|
import de.oaa.xxx.feedback.FeedbackRepository;
|
||||||
|
import de.oaa.xxx.feedback.FeedbackStatus;
|
||||||
|
import de.oaa.xxx.support.SupportUserService;
|
||||||
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigEntity;
|
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigEntity;
|
||||||
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository;
|
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository;
|
||||||
import de.oaa.xxx.meldung.MeldungEntity;
|
import de.oaa.xxx.meldung.MeldungEntity;
|
||||||
@@ -50,6 +54,8 @@ public class AdminController {
|
|||||||
private final AdminRepository adminRepository;
|
private final AdminRepository adminRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final MeldungRepository meldungRepository;
|
private final MeldungRepository meldungRepository;
|
||||||
|
private final FeedbackRepository feedbackRepository;
|
||||||
|
private final SupportUserService supportUserService;
|
||||||
private final AufgabenGruppeRepository aufgabenGruppeRepository;
|
private final AufgabenGruppeRepository aufgabenGruppeRepository;
|
||||||
private final AufgabeRepository aufgabeRepository;
|
private final AufgabeRepository aufgabeRepository;
|
||||||
private final StrafeRepository strafeRepository;
|
private final StrafeRepository strafeRepository;
|
||||||
@@ -62,6 +68,8 @@ public class AdminController {
|
|||||||
|
|
||||||
public AdminController(AdminRepository adminRepository, UserRepository userRepository,
|
public AdminController(AdminRepository adminRepository, UserRepository userRepository,
|
||||||
MeldungRepository meldungRepository,
|
MeldungRepository meldungRepository,
|
||||||
|
FeedbackRepository feedbackRepository,
|
||||||
|
SupportUserService supportUserService,
|
||||||
AufgabenGruppeRepository aufgabenGruppeRepository,
|
AufgabenGruppeRepository aufgabenGruppeRepository,
|
||||||
AufgabeRepository aufgabeRepository,
|
AufgabeRepository aufgabeRepository,
|
||||||
StrafeRepository strafeRepository,
|
StrafeRepository strafeRepository,
|
||||||
@@ -74,6 +82,8 @@ public class AdminController {
|
|||||||
this.adminRepository = adminRepository;
|
this.adminRepository = adminRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.meldungRepository = meldungRepository;
|
this.meldungRepository = meldungRepository;
|
||||||
|
this.feedbackRepository = feedbackRepository;
|
||||||
|
this.supportUserService = supportUserService;
|
||||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||||
this.aufgabeRepository = aufgabeRepository;
|
this.aufgabeRepository = aufgabeRepository;
|
||||||
this.strafeRepository = strafeRepository;
|
this.strafeRepository = strafeRepository;
|
||||||
@@ -109,6 +119,12 @@ public class AdminController {
|
|||||||
record SubscriptionStatusDto(UUID userId, String userName, String subscriptionType,
|
record SubscriptionStatusDto(UUID userId, String userName, String subscriptionType,
|
||||||
LocalDate subscribedAt, LocalDate validUntil) {}
|
LocalDate subscribedAt, LocalDate validUntil) {}
|
||||||
|
|
||||||
|
record FeedbackDto(UUID feedbackId, String name, String seite, String grund,
|
||||||
|
String text, LocalDateTime eingegangen, FeedbackStatus status,
|
||||||
|
String inArbeitVonName) {}
|
||||||
|
|
||||||
|
record FeedbackAntwortRequest(String text) {}
|
||||||
|
|
||||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private AdminEntity requireAdmin(Principal principal) {
|
private AdminEntity requireAdmin(Principal principal) {
|
||||||
@@ -413,6 +429,74 @@ public class AdminController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Feedback ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private FeedbackDto toFeedbackDto(FeedbackEntity e) {
|
||||||
|
String inArbeitName = null;
|
||||||
|
if (e.getInArbeitVon() != null) {
|
||||||
|
inArbeitName = userRepository.findById(e.getInArbeitVon())
|
||||||
|
.map(UserEntity::getName).orElse("?");
|
||||||
|
}
|
||||||
|
return new FeedbackDto(e.getFeedbackId(), e.getName(), e.getSeite(), e.getGrund(),
|
||||||
|
e.getText(), e.getEingegangen(), e.getStatus(), inArbeitName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/feedback")
|
||||||
|
public ResponseEntity<java.util.Map<String, List<FeedbackDto>>> getFeedback(Principal principal) {
|
||||||
|
requireAdmin(principal);
|
||||||
|
List<FeedbackDto> ungelesen = feedbackRepository
|
||||||
|
.findByStatusOrderByEingegangenDesc(FeedbackStatus.UNGELESEN)
|
||||||
|
.stream().map(this::toFeedbackDto).toList();
|
||||||
|
List<FeedbackDto> inArbeit = feedbackRepository
|
||||||
|
.findByStatusOrderByEingegangenDesc(FeedbackStatus.IN_ARBEIT)
|
||||||
|
.stream().map(this::toFeedbackDto).toList();
|
||||||
|
List<FeedbackDto> beantwortet = feedbackRepository
|
||||||
|
.findByStatusOrderByEingegangenDesc(FeedbackStatus.BEANTWORTET)
|
||||||
|
.stream().map(this::toFeedbackDto).toList();
|
||||||
|
return ResponseEntity.ok(java.util.Map.of(
|
||||||
|
"ungelesen", ungelesen,
|
||||||
|
"inArbeit", inArbeit,
|
||||||
|
"beantwortet", beantwortet));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/feedback/{id}/annehmen")
|
||||||
|
public ResponseEntity<Void> feedbackAnnehmen(@PathVariable("id") UUID id, Principal principal) {
|
||||||
|
AdminEntity admin = requireAdmin(principal);
|
||||||
|
FeedbackEntity f = feedbackRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||||
|
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||||
|
if (f.getStatus() == FeedbackStatus.IN_ARBEIT) {
|
||||||
|
// Bereits von jemand anderem in Arbeit – Konflikt
|
||||||
|
return ResponseEntity.status(409).build();
|
||||||
|
}
|
||||||
|
f.setStatus(FeedbackStatus.IN_ARBEIT);
|
||||||
|
f.setInArbeitVon(admin.getUserId());
|
||||||
|
feedbackRepository.save(f);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/feedback/{id}/antworten")
|
||||||
|
public ResponseEntity<Void> feedbackAntworten(@PathVariable("id") UUID id,
|
||||||
|
@RequestBody FeedbackAntwortRequest body,
|
||||||
|
Principal principal) {
|
||||||
|
requireAdmin(principal);
|
||||||
|
if (body.text() == null || body.text().isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
FeedbackEntity f = feedbackRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
|
||||||
|
org.springframework.http.HttpStatus.NOT_FOUND));
|
||||||
|
f.setStatus(FeedbackStatus.BEANTWORTET);
|
||||||
|
feedbackRepository.save(f);
|
||||||
|
|
||||||
|
// DM an den Nutzer senden, falls er eingeloggt war
|
||||||
|
if (f.getUserId() != null) {
|
||||||
|
String dm = "Ursprüngliche Nachricht\n" + f.getText() + "\n\nAntwort\n" + body.text();
|
||||||
|
supportUserService.sendDm(f.getUserId(), dm);
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
// ── TTLock-Konfiguration (nur SUPERADMIN) ─────────────────────────────────
|
// ── TTLock-Konfiguration (nur SUPERADMIN) ─────────────────────────────────
|
||||||
|
|
||||||
@GetMapping("/ttlock")
|
@GetMapping("/ttlock")
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/notifications/**").authenticated()
|
.requestMatchers("/notifications/**").authenticated()
|
||||||
.requestMatchers("/events/**").authenticated()
|
.requestMatchers("/events/**").authenticated()
|
||||||
.requestMatchers("/*.html").permitAll()
|
.requestMatchers("/*.html").permitAll()
|
||||||
|
.requestMatchers("/help/*.html").permitAll()
|
||||||
.requestMatchers("/css/**").permitAll()
|
.requestMatchers("/css/**").permitAll()
|
||||||
.requestMatchers("/js/**").permitAll()
|
.requestMatchers("/js/**").permitAll()
|
||||||
.requestMatchers("/images/**").permitAll()
|
.requestMatchers("/images/**").permitAll()
|
||||||
@@ -100,6 +101,7 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(HttpMethod.POST, "/password-reset/confirm").permitAll()
|
.requestMatchers(HttpMethod.POST, "/password-reset/confirm").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/email-change/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/email-change/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/keyholder/invitation/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/keyholder/invitation/**").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.POST, "/api/feedback").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/filler").permitAll()
|
.requestMatchers(HttpMethod.POST, "/filler").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/ttlock/callback").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/ttlock/callback").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/ttlock/callback").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/ttlock/callback").permitAll()
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package de.oaa.xxx.feedback;
|
||||||
|
|
||||||
|
import de.oaa.xxx.mail.Email;
|
||||||
|
import de.oaa.xxx.mail.MailService;
|
||||||
|
import de.oaa.xxx.support.SupportUserService;
|
||||||
|
import de.oaa.xxx.user.UserRepository;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/feedback")
|
||||||
|
public class FeedbackController {
|
||||||
|
|
||||||
|
private final MailService mailService;
|
||||||
|
private final FeedbackRepository feedbackRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final SupportUserService supportUserService;
|
||||||
|
|
||||||
|
public FeedbackController(MailService mailService,
|
||||||
|
FeedbackRepository feedbackRepository,
|
||||||
|
UserRepository userRepository,
|
||||||
|
SupportUserService supportUserService) {
|
||||||
|
this.mailService = mailService;
|
||||||
|
this.feedbackRepository = feedbackRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.supportUserService = supportUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
record FeedbackRequest(String name, String seite, String grund, String text) {}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<Void> send(@RequestBody FeedbackRequest req, Principal principal) {
|
||||||
|
if (req.text() == null || req.text().isBlank() || req.text().length() < 10 || req.text().length() > 1000) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eingeloggten User ermitteln (optional)
|
||||||
|
UUID userId = null;
|
||||||
|
if (principal != null) {
|
||||||
|
userId = userRepository.findByEmail(principal.getName())
|
||||||
|
.map(u -> u.getUserId()).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedbackEntity entity = new FeedbackEntity();
|
||||||
|
entity.setUserId(userId);
|
||||||
|
entity.setName(req.name());
|
||||||
|
entity.setSeite(req.seite());
|
||||||
|
entity.setGrund(req.grund());
|
||||||
|
entity.setText(req.text());
|
||||||
|
entity.setEingegangen(LocalDateTime.now());
|
||||||
|
entity.setStatus(FeedbackStatus.UNGELESEN);
|
||||||
|
feedbackRepository.save(entity);
|
||||||
|
|
||||||
|
// Bestätigungs-DM an eingeloggten Nutzer
|
||||||
|
if (userId != null) {
|
||||||
|
supportUserService.sendDm(userId,
|
||||||
|
"Vielen Dank für dein Feedback! ✉️\n\n" +
|
||||||
|
"Wir haben deine Nachricht erhalten und werden uns so schnell wie möglich darum kümmern.\n\n" +
|
||||||
|
"Bitte antworte nicht auf diese Nachricht – du kannst uns jederzeit über " +
|
||||||
|
"Kontakt & Feedback erneut erreichen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Email email = new Email();
|
||||||
|
email.setEmailAdresse("kontakt@xxx-sphere.de");
|
||||||
|
email.setTitel("[xXx Sphere] " + esc(req.grund()));
|
||||||
|
email.setText(
|
||||||
|
"<b>Von:</b> " + esc(req.name()) + "<br>" +
|
||||||
|
"<b>Seite:</b> " + esc(req.seite()) + "<br>" +
|
||||||
|
"<b>Grund:</b> " + esc(req.grund()) + "<br><br>" +
|
||||||
|
"<b>Nachricht:</b><br>" + esc(req.text()).replace("\n", "<br>")
|
||||||
|
);
|
||||||
|
mailService.send(email);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Mail-Server nicht erreichbar – Eintrag ist bereits gespeichert
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String esc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.oaa.xxx.feedback;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "feedback")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class FeedbackEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID feedbackId;
|
||||||
|
|
||||||
|
/** Eingeloggter Nutzer – null wenn Gast */
|
||||||
|
private UUID userId;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String seite;
|
||||||
|
private String grund;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
private LocalDateTime eingegangen;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private FeedbackStatus status = FeedbackStatus.UNGELESEN;
|
||||||
|
|
||||||
|
/** Admin-UserId der den Eintrag in Arbeit genommen hat */
|
||||||
|
private UUID inArbeitVon;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.oaa.xxx.feedback;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface FeedbackRepository extends JpaRepository<FeedbackEntity, UUID> {
|
||||||
|
|
||||||
|
List<FeedbackEntity> findByStatusOrderByEingegangenDesc(FeedbackStatus status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.oaa.xxx.feedback;
|
||||||
|
|
||||||
|
public enum FeedbackStatus {
|
||||||
|
UNGELESEN,
|
||||||
|
IN_ARBEIT,
|
||||||
|
BEANTWORTET
|
||||||
|
}
|
||||||
@@ -595,6 +595,7 @@ public class CardLockController {
|
|||||||
|
|
||||||
result.put("testLock", l.isTestLock());
|
result.put("testLock", l.isTestLock());
|
||||||
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
|
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
|
||||||
|
result.put("controllType", l.getControllType() != null ? l.getControllType().name() : "UNLOCK_CODE");
|
||||||
if (l.isTestLock()) {
|
if (l.isTestLock()) {
|
||||||
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
|
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ public class TimeLockController {
|
|||||||
|
|
||||||
TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false);
|
TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false);
|
||||||
lock.setStartTime(null);
|
lock.setStartTime(null);
|
||||||
lock.setUnlockTime(null);
|
|
||||||
timeLockRepository.save(lock);
|
timeLockRepository.save(lock);
|
||||||
|
|
||||||
String token = UUID.randomUUID().toString().replace("-", "");
|
String token = UUID.randomUUID().toString().replace("-", "");
|
||||||
@@ -328,6 +327,7 @@ public class TimeLockController {
|
|||||||
result.put("spinEnabled", spinEnabled);
|
result.put("spinEnabled", spinEnabled);
|
||||||
result.put("spinDue", spinDue);
|
result.put("spinDue", spinDue);
|
||||||
result.put("nextSpinIn", nextSpinIn);
|
result.put("nextSpinIn", nextSpinIn);
|
||||||
|
result.put("spinningWheelEntries", l.getSpinningWheelEntries() != null ? l.getSpinningWheelEntries() : List.of());
|
||||||
|
|
||||||
result.put("taskTimingEnabled", taskTimingEnabled);
|
result.put("taskTimingEnabled", taskTimingEnabled);
|
||||||
result.put("nextTaskIn", nextTaskIn);
|
result.put("nextTaskIn", nextTaskIn);
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ import lombok.Setter;
|
|||||||
@DiscriminatorValue("TIMELOCK")
|
@DiscriminatorValue("TIMELOCK")
|
||||||
public class TimeLockEntity extends BaseLockEntity {
|
public class TimeLockEntity extends BaseLockEntity {
|
||||||
|
|
||||||
@Column
|
|
||||||
private LocalDateTime startTime;
|
|
||||||
@Column
|
|
||||||
private LocalDateTime unlockTime;
|
|
||||||
@Column
|
@Column
|
||||||
private boolean endTimeVisible;
|
private boolean endTimeVisible;
|
||||||
@Column
|
@Column
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void check() {
|
public void check() {
|
||||||
|
if (lock.getStartTime() == null) return;
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
if (!lock.getStartTime().toLocalDate().equals(today)) {
|
if (!lock.getStartTime().toLocalDate().equals(today)) {
|
||||||
if (lock.getLastCheck() != null || today.isAfter(lock.getLastCheck())) {
|
if (lock.getLastCheck() != null || today.isAfter(lock.getLastCheck())) {
|
||||||
|
|||||||
@@ -29,4 +29,8 @@ public class TTLockUserConfigEntity {
|
|||||||
/** ID des aktuell gesetzten PINs auf dem Schloss – wird zum Löschen beim nächsten lock() benötigt */
|
/** ID des aktuell gesetzten PINs auf dem Schloss – wird zum Löschen beim nächsten lock() benötigt */
|
||||||
@Column
|
@Column
|
||||||
private Integer currentKeyboardPwdId;
|
private Integer currentKeyboardPwdId;
|
||||||
|
|
||||||
|
/** true, wenn der Anwender mindestens einmal erfolgreich die Verbindung getestet hat */
|
||||||
|
@Column(nullable = false, columnDefinition = "boolean default false")
|
||||||
|
private boolean testSuccessful;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import de.oaa.xxx.social.entity.MessageCause;
|
|||||||
import de.oaa.xxx.social.entity.MessageEntity;
|
import de.oaa.xxx.social.entity.MessageEntity;
|
||||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||||
import de.oaa.xxx.social.repository.MessageRepository;
|
import de.oaa.xxx.social.repository.MessageRepository;
|
||||||
|
import de.oaa.xxx.support.SupportUserService;
|
||||||
import de.oaa.xxx.user.UserEntity;
|
import de.oaa.xxx.user.UserEntity;
|
||||||
import de.oaa.xxx.user.UserRepository;
|
import de.oaa.xxx.user.UserRepository;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -219,6 +220,11 @@ public class SocialController {
|
|||||||
|
|
||||||
if (body.text() == null || body.text().isBlank()) return ResponseEntity.badRequest().build();
|
if (body.text() == null || body.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||||
|
|
||||||
|
// Nachrichten an den Support-Account sind nicht erlaubt
|
||||||
|
if (SupportUserService.SUPPORT_USER_ID.equals(body.receiverId())) {
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
}
|
||||||
|
|
||||||
MessageEntity msg = new MessageEntity();
|
MessageEntity msg = new MessageEntity();
|
||||||
msg.setMessageId(UUID.randomUUID());
|
msg.setMessageId(UUID.randomUUID());
|
||||||
msg.setSenderId(myId);
|
msg.setSenderId(myId);
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ public class SystemMessageService {
|
|||||||
case GAME_STATE -> "XXX The Game – Spielstatus-Änderung";
|
case GAME_STATE -> "XXX The Game – Spielstatus-Änderung";
|
||||||
case EMERGENCY -> "XXX The Game – ⚠️ Notfall";
|
case EMERGENCY -> "XXX The Game – ⚠️ Notfall";
|
||||||
case FRIENDREQUEST -> "XXX The Game – Neue Freundschaftsanfrage";
|
case FRIENDREQUEST -> "XXX The Game – Neue Freundschaftsanfrage";
|
||||||
|
case SUPPORT -> "xXx Sphere – Nachricht vom Support";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ public enum MessageCause {
|
|||||||
INVITATION,
|
INVITATION,
|
||||||
GAME_STATE,
|
GAME_STATE,
|
||||||
EMERGENCY,
|
EMERGENCY,
|
||||||
FRIENDREQUEST
|
FRIENDREQUEST,
|
||||||
|
SUPPORT
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package de.oaa.xxx.support;
|
||||||
|
|
||||||
|
import de.oaa.xxx.social.entity.MessageCause;
|
||||||
|
import de.oaa.xxx.social.entity.MessageEntity;
|
||||||
|
import de.oaa.xxx.social.repository.MessageRepository;
|
||||||
|
import de.oaa.xxx.user.UserEntity;
|
||||||
|
import de.oaa.xxx.user.UserRepository;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SupportUserService {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(SupportUserService.class);
|
||||||
|
|
||||||
|
/** Deterministischer UUID – ändert sich nie. */
|
||||||
|
public static final UUID SUPPORT_USER_ID =
|
||||||
|
UUID.nameUUIDFromBytes("xxxsphere-support".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
private static final String SUPPORT_NAME = "xXx Support";
|
||||||
|
private static final String SUPPORT_EMAIL = "support@system.local";
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final MessageRepository messageRepository;
|
||||||
|
|
||||||
|
public SupportUserService(UserRepository userRepository, MessageRepository messageRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.messageRepository = messageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stellt sicher, dass der Support-Fake-User in der DB existiert. */
|
||||||
|
@PostConstruct
|
||||||
|
public void ensureExists() {
|
||||||
|
try {
|
||||||
|
String icon = loadIconBase64();
|
||||||
|
userRepository.findById(SUPPORT_USER_ID).ifPresentOrElse(u -> {
|
||||||
|
if (u.getProfilePicture() == null && icon != null) {
|
||||||
|
try {
|
||||||
|
u.setProfilePicture(icon);
|
||||||
|
userRepository.save(u);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Support-User-Avatar konnte nicht gespeichert werden: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, () -> {
|
||||||
|
UserEntity u = new UserEntity();
|
||||||
|
u.setUserId(SUPPORT_USER_ID);
|
||||||
|
u.setName(SUPPORT_NAME);
|
||||||
|
u.setEmail(SUPPORT_EMAIL);
|
||||||
|
u.setPassword("__SYSTEM__"); // kein gültiges Login
|
||||||
|
try {
|
||||||
|
u.setProfilePicture(icon);
|
||||||
|
userRepository.save(u);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Support-User konnte nicht mit Avatar gespeichert werden, versuche ohne: {}", e.getMessage());
|
||||||
|
u.setProfilePicture(null);
|
||||||
|
userRepository.save(u);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Support-User konnte nicht initialisiert werden: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadIconBase64() {
|
||||||
|
try {
|
||||||
|
byte[] bytes = new ClassPathResource("static/img/icon.png").getInputStream().readAllBytes();
|
||||||
|
BufferedImage original = ImageIO.read(new ByteArrayInputStream(bytes));
|
||||||
|
BufferedImage thumb = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g = thumb.createGraphics();
|
||||||
|
g.drawImage(original.getScaledInstance(64, 64, Image.SCALE_SMOOTH), 0, 0, null);
|
||||||
|
g.dispose();
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(thumb, "png", baos);
|
||||||
|
return Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Support-Avatar konnte nicht geladen werden: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet eine Direktnachricht vom Support-Account an den Nutzer.
|
||||||
|
* Die Nachricht erscheint als normale DM (systemMessage = false),
|
||||||
|
* damit sie in nachrichten.html sichtbar ist.
|
||||||
|
*/
|
||||||
|
public void sendDm(UUID receiverId, String text) {
|
||||||
|
if (receiverId == null) return;
|
||||||
|
MessageEntity msg = new MessageEntity();
|
||||||
|
msg.setMessageId(UUID.randomUUID());
|
||||||
|
msg.setSenderId(SUPPORT_USER_ID);
|
||||||
|
msg.setReceiverId(receiverId);
|
||||||
|
msg.setText(text);
|
||||||
|
msg.setSentAt(LocalDateTime.now());
|
||||||
|
msg.setSystemMessage(false);
|
||||||
|
msg.setMessageCause(MessageCause.SUPPORT);
|
||||||
|
messageRepository.save(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ public class UserController {
|
|||||||
record ProfilePictureRequest(String picture, String pictureHq) {}
|
record ProfilePictureRequest(String picture, String pictureHq) {}
|
||||||
record NameChangeRequest(String name) {}
|
record NameChangeRequest(String name) {}
|
||||||
record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {}
|
record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {}
|
||||||
record TtlockUserConfigDto(String username, boolean passwordSet, Integer lockId) {}
|
record TtlockUserConfigDto(String username, boolean passwordSet, Integer lockId, boolean testSuccessful) {}
|
||||||
record TtlockUserConfigRequest(String username, String password, Integer lockId) {}
|
record TtlockUserConfigRequest(String username, String password, Integer lockId) {}
|
||||||
record ProfileRequest(Integer groesse, Integer gewicht,
|
record ProfileRequest(Integer groesse, Integer gewicht,
|
||||||
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
|
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
|
||||||
@@ -308,7 +308,8 @@ public class UserController {
|
|||||||
return ResponseEntity.ok(new TtlockUserConfigDto(
|
return ResponseEntity.ok(new TtlockUserConfigDto(
|
||||||
cfg.getUsername(),
|
cfg.getUsername(),
|
||||||
cfg.getPasswordMd5() != null && !cfg.getPasswordMd5().isBlank(),
|
cfg.getPasswordMd5() != null && !cfg.getPasswordMd5().isBlank(),
|
||||||
cfg.getLockId()
|
cfg.getLockId(),
|
||||||
|
cfg.isTestSuccessful()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +320,12 @@ public class UserController {
|
|||||||
UUID userId = userOpt.get().getUserId();
|
UUID userId = userOpt.get().getUserId();
|
||||||
TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userId)
|
TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userId)
|
||||||
.orElseGet(() -> { TTLockUserConfigEntity n = new TTLockUserConfigEntity(); n.setUserId(userId); return n; });
|
.orElseGet(() -> { TTLockUserConfigEntity n = new TTLockUserConfigEntity(); n.setUserId(userId); return n; });
|
||||||
|
boolean credentialsChanged = !java.util.Objects.equals(cfg.getUsername(), body.username())
|
||||||
|
|| !java.util.Objects.equals(cfg.getLockId(), body.lockId())
|
||||||
|
|| (body.password() != null && !body.password().isBlank());
|
||||||
|
if (credentialsChanged) {
|
||||||
|
cfg.setTestSuccessful(false);
|
||||||
|
}
|
||||||
cfg.setUsername(body.username());
|
cfg.setUsername(body.username());
|
||||||
if (body.password() != null && !body.password().isBlank()) {
|
if (body.password() != null && !body.password().isBlank()) {
|
||||||
cfg.setPasswordMd5(DigestUtils.md5DigestAsHex(body.password().getBytes(StandardCharsets.UTF_8)));
|
cfg.setPasswordMd5(DigestUtils.md5DigestAsHex(body.password().getBytes(StandardCharsets.UTF_8)));
|
||||||
@@ -357,6 +364,9 @@ public class UserController {
|
|||||||
return ResponseEntity.status(502).body(Map.of("error", "lock_detail_failed", "message", msg));
|
return ResponseEntity.status(502).body(Map.of("error", "lock_detail_failed", "message", msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userCfg.setTestSuccessful(true);
|
||||||
|
ttLockUserConfigRepository.save(userCfg);
|
||||||
|
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("lockId", detail.getLockId());
|
result.put("lockId", detail.getLockId());
|
||||||
result.put("lockName", detail.getLockName());
|
result.put("lockName", detail.getLockName());
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Abonnements – XXX The Game</title>
|
<title>Abonnements – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>XXX The Game – Aktivierung</title>
|
<title>Aktivierung – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Chastity Game – XXX The Game</title>
|
<title>Chastity Game – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -719,6 +719,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- TTLock Lade-Dialog -->
|
||||||
|
<div class="warn-modal-backdrop" id="ttlLoadingModal">
|
||||||
|
<div class="warn-modal-box" style="text-align:center;padding:2rem 1.5rem;">
|
||||||
|
<div style="font-size:2rem;margin-bottom:0.75rem;">⏳</div>
|
||||||
|
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
|
||||||
|
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Warn-Modal (TestLock beenden) -->
|
<!-- Warn-Modal (TestLock beenden) -->
|
||||||
<div class="warn-modal-backdrop" id="warnModal">
|
<div class="warn-modal-backdrop" id="warnModal">
|
||||||
<div class="warn-modal-box">
|
<div class="warn-modal-box">
|
||||||
@@ -762,7 +771,7 @@
|
|||||||
<script src="/js/shared.js"></script>
|
<script src="/js/shared.js"></script>
|
||||||
<script src="/js/card-defs.js"></script>
|
<script src="/js/card-defs.js"></script>
|
||||||
<script src="/js/card-display.js"></script>
|
<script src="/js/card-display.js"></script>
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/sidebar.js"></script>
|
<script src="/js/sidebar.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -1421,7 +1430,21 @@
|
|||||||
async function endHygieneOpening() {
|
async function endHygieneOpening() {
|
||||||
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
|
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
|
||||||
|
|
||||||
|
const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK';
|
||||||
|
if (isTtlock) {
|
||||||
|
document.getElementById('hygieneModal').classList.remove('open');
|
||||||
|
document.getElementById('ttlLoadingModal').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
|
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
|
||||||
|
|
||||||
|
if (isTtlock) {
|
||||||
|
document.getElementById('ttlLoadingModal').classList.remove('open');
|
||||||
|
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
|
||||||
|
loadLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
|
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
@@ -1497,6 +1520,9 @@
|
|||||||
|
|
||||||
async function lockLoeschen() {
|
async function lockLoeschen() {
|
||||||
closeWarnModal();
|
closeWarnModal();
|
||||||
|
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
|
||||||
|
document.getElementById('ttlLoadingModal').classList.add('open');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fetch('/keyholder/cardlock/' + lockId, { method: 'DELETE' });
|
await fetch('/keyholder/cardlock/' + lockId, { method: 'DELETE' });
|
||||||
} catch (_) { /* ignorieren */ }
|
} catch (_) { /* ignorieren */ }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>TimeLock – XXX The Game</title>
|
<title>TimeLock – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -166,6 +166,23 @@
|
|||||||
.vote-up { color: #2ecc71; font-weight: 600; }
|
.vote-up { color: #2ecc71; font-weight: 600; }
|
||||||
.vote-down { color: #e74c3c; font-weight: 600; }
|
.vote-down { color: #e74c3c; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── Glücksrad-Animation ── */
|
||||||
|
.wheel-anim-backdrop {
|
||||||
|
display: none; position: fixed; inset: 0;
|
||||||
|
background: rgba(0,0,0,0.82); z-index: 600;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.wheel-anim-backdrop.open { display: flex; }
|
||||||
|
.wheel-canvas-wrap {
|
||||||
|
position: relative; display: flex; flex-direction: column; align-items: center;
|
||||||
|
}
|
||||||
|
.wheel-pointer-top {
|
||||||
|
font-size: 2.2rem; line-height: 1; margin-bottom: -12px; z-index: 1;
|
||||||
|
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.8));
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
#wheelCanvas { border-radius: 50%; display: block; }
|
||||||
|
|
||||||
/* ── Spin-Result-Modal ── */
|
/* ── Spin-Result-Modal ── */
|
||||||
.spin-modal-backdrop {
|
.spin-modal-backdrop {
|
||||||
display: none; position: fixed; inset: 0;
|
display: none; position: fixed; inset: 0;
|
||||||
@@ -295,7 +312,7 @@
|
|||||||
<!-- Spin-Panel -->
|
<!-- Spin-Panel -->
|
||||||
<div class="spin-panel" id="spinPanel" style="display:none;">
|
<div class="spin-panel" id="spinPanel" style="display:none;">
|
||||||
<div>
|
<div>
|
||||||
<div class="spin-panel-title">Spinning Wheel</div>
|
<div class="spin-panel-title">Glücksrad</div>
|
||||||
<div class="spin-countdown" id="spinCountdown">–</div>
|
<div class="spin-countdown" id="spinCountdown">–</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-spin" id="spinBtn" onclick="doSpin()">🎡 Drehen</button>
|
<button class="btn-spin" id="spinBtn" onclick="doSpin()">🎡 Drehen</button>
|
||||||
@@ -346,6 +363,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Glücksrad-Animation-Modal -->
|
||||||
|
<div class="wheel-anim-backdrop" id="wheelAnimModal">
|
||||||
|
<div class="wheel-canvas-wrap">
|
||||||
|
<div class="wheel-pointer-top">▼</div>
|
||||||
|
<canvas id="wheelCanvas" width="290" height="290"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Spin-Result-Modal -->
|
<!-- Spin-Result-Modal -->
|
||||||
<div class="spin-modal-backdrop" id="spinModal">
|
<div class="spin-modal-backdrop" id="spinModal">
|
||||||
<div class="spin-modal-box">
|
<div class="spin-modal-box">
|
||||||
@@ -578,9 +603,169 @@
|
|||||||
tickInterval = setInterval(tick, 1000);
|
tickInterval = setInterval(tick, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Glücksrad-Animation ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let wheelEntries = [];
|
||||||
|
let wheelResult = null;
|
||||||
|
let wheelAnimFrame = null;
|
||||||
|
let wheelLastTime = null;
|
||||||
|
let wheelAngle = 0;
|
||||||
|
let wheelAnimState = 'idle';
|
||||||
|
let wheelSpinStartTime = 0;
|
||||||
|
let wheelDecelStartTime = 0;
|
||||||
|
let wheelDecelStartAngle = 0;
|
||||||
|
let wheelTargetAngle = 0;
|
||||||
|
|
||||||
|
const WHEEL_MAX_SPEED = 0.014; // rad/ms
|
||||||
|
const WHEEL_ACCEL_MS = 500;
|
||||||
|
const WHEEL_DECEL_MS = 2500;
|
||||||
|
const WHEEL_MIN_SPIN_MS = 1000;
|
||||||
|
|
||||||
|
const WHEEL_COLORS = [
|
||||||
|
'#c0392b','#2980b9','#27ae60','#d35400',
|
||||||
|
'#8e44ad','#16a085','#e67e22','#c0392b',
|
||||||
|
'#1565c0','#2e7d32'
|
||||||
|
];
|
||||||
|
const WHEEL_LABELS = {
|
||||||
|
ADD_TIME: '+ Zeit',
|
||||||
|
REMOVE_TIME: '− Zeit',
|
||||||
|
FREEZE_TIME: '❄ Einfrieren',
|
||||||
|
FREEZE: '🧊 Freeze',
|
||||||
|
UNFREEZE: '🌊 Auftauen',
|
||||||
|
TASK: '🎯 Aufgabe',
|
||||||
|
TEXT: '💬 Text',
|
||||||
|
};
|
||||||
|
|
||||||
|
function startWheelSpin() {
|
||||||
|
document.getElementById('wheelAnimModal').classList.add('open');
|
||||||
|
wheelAngle = Math.random() * 2 * Math.PI;
|
||||||
|
wheelResult = null;
|
||||||
|
wheelAnimState = 'accelerating';
|
||||||
|
wheelLastTime = null;
|
||||||
|
wheelSpinStartTime = performance.now();
|
||||||
|
if (wheelAnimFrame) cancelAnimationFrame(wheelAnimFrame);
|
||||||
|
wheelAnimFrame = requestAnimationFrame(wheelTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wheelTick(now) {
|
||||||
|
if (!wheelLastTime) wheelLastTime = now;
|
||||||
|
const dt = Math.min(now - wheelLastTime, 50);
|
||||||
|
wheelLastTime = now;
|
||||||
|
|
||||||
|
const canvas = document.getElementById('wheelCanvas');
|
||||||
|
if (!canvas || !wheelEntries.length) return;
|
||||||
|
|
||||||
|
if (wheelAnimState === 'accelerating') {
|
||||||
|
const p = Math.min((now - wheelSpinStartTime) / WHEEL_ACCEL_MS, 1);
|
||||||
|
wheelAngle += WHEEL_MAX_SPEED * p * dt;
|
||||||
|
if (p >= 1) { wheelAnimState = 'spinning'; wheelSpinStartTime = now; }
|
||||||
|
} else if (wheelAnimState === 'spinning') {
|
||||||
|
wheelAngle += WHEEL_MAX_SPEED * dt;
|
||||||
|
if (wheelResult && (now - wheelSpinStartTime) >= WHEEL_MIN_SPIN_MS) {
|
||||||
|
initWheelDecel();
|
||||||
|
}
|
||||||
|
} else if (wheelAnimState === 'decelerating') {
|
||||||
|
const p = Math.min((now - wheelDecelStartTime) / WHEEL_DECEL_MS, 1);
|
||||||
|
const eased = 1 - Math.pow(1 - p, 4);
|
||||||
|
wheelAngle = wheelDecelStartAngle + eased * (wheelTargetAngle - wheelDecelStartAngle);
|
||||||
|
if (p >= 1) {
|
||||||
|
wheelAngle = wheelTargetAngle;
|
||||||
|
wheelAnimState = 'done';
|
||||||
|
drawWheelFrame(canvas, wheelAngle);
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('wheelAnimModal').classList.remove('open');
|
||||||
|
showSpinResult(wheelResult);
|
||||||
|
loadLock();
|
||||||
|
}, 750);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawWheelFrame(canvas, wheelAngle);
|
||||||
|
wheelAnimFrame = requestAnimationFrame(wheelTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWheelDecel() {
|
||||||
|
const n = wheelEntries.length;
|
||||||
|
let idx = wheelEntries.findIndex(e =>
|
||||||
|
e.type === wheelResult.type &&
|
||||||
|
e.intVal === wheelResult.intVal &&
|
||||||
|
e.stringVal === wheelResult.stringVal
|
||||||
|
);
|
||||||
|
if (idx < 0) idx = wheelEntries.findIndex(e => e.type === wheelResult.type);
|
||||||
|
if (idx < 0) idx = 0;
|
||||||
|
|
||||||
|
const segCenter = ((idx + 0.5) / n) * 2 * Math.PI;
|
||||||
|
const rawTarget = -Math.PI / 2 - segCenter;
|
||||||
|
const targetMod = ((rawTarget % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI);
|
||||||
|
const currMod = ((wheelAngle % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI);
|
||||||
|
let extra = targetMod - currMod;
|
||||||
|
if (extra < 0) extra += 2 * Math.PI;
|
||||||
|
|
||||||
|
wheelDecelStartAngle = wheelAngle;
|
||||||
|
wheelTargetAngle = wheelAngle + extra + 2 * 2 * Math.PI;
|
||||||
|
wheelDecelStartTime = performance.now();
|
||||||
|
wheelAnimState = 'decelerating';
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWheelFrame(canvas, angle) {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const w = canvas.width, h = canvas.height;
|
||||||
|
const cx = w/2, cy = h/2;
|
||||||
|
const r = Math.min(w, h)/2 - 6;
|
||||||
|
const n = wheelEntries.length;
|
||||||
|
if (!n) return;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const a0 = angle + (i / n) * 2 * Math.PI;
|
||||||
|
const a1 = angle + ((i+1) / n) * 2 * Math.PI;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cx, cy);
|
||||||
|
ctx.arc(cx, cy, r, a0, a1);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = WHEEL_COLORS[i % WHEEL_COLORS.length];
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const aMid = angle + ((i+0.5) / n) * 2 * Math.PI;
|
||||||
|
const fontSize = Math.max(9, Math.min(13, Math.floor(170 / n)));
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(cx, cy);
|
||||||
|
ctx.rotate(aMid);
|
||||||
|
ctx.textAlign = 'right';
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.font = `bold ${fontSize}px sans-serif`;
|
||||||
|
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||||
|
ctx.shadowBlur = 3;
|
||||||
|
ctx.fillText(WHEEL_LABELS[wheelEntries[i].type] || wheelEntries[i].type, r - 10, 4);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer ring
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, r, 0, 2*Math.PI);
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Center circle
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, 17, 0, 2*Math.PI);
|
||||||
|
ctx.fillStyle = '#111122';
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Spin-Panel ─────────────────────────────────────────────────────────────
|
// ── Spin-Panel ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function renderSpinPanel(lock) {
|
function renderSpinPanel(lock) {
|
||||||
|
wheelEntries = lock.spinningWheelEntries || [];
|
||||||
if (spinTickInterval) { clearInterval(spinTickInterval); spinTickInterval = null; }
|
if (spinTickInterval) { clearInterval(spinTickInterval); spinTickInterval = null; }
|
||||||
|
|
||||||
const panel = document.getElementById('spinPanel');
|
const panel = document.getElementById('spinPanel');
|
||||||
@@ -622,18 +807,24 @@
|
|||||||
async function doSpin() {
|
async function doSpin() {
|
||||||
const btn = document.getElementById('spinBtn');
|
const btn = document.getElementById('spinBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
startWheelSpin();
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/keyholder/timelock/' + lockId + '/spin', { method: 'POST' });
|
const res = await fetch('/keyholder/timelock/' + lockId + '/spin', { method: 'POST' });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
|
document.getElementById('wheelAnimModal').classList.remove('open');
|
||||||
|
if (wheelAnimFrame) { cancelAnimationFrame(wheelAnimFrame); wheelAnimFrame = null; }
|
||||||
|
wheelAnimState = 'idle';
|
||||||
alert(data.error || 'Spin nicht möglich.');
|
alert(data.error || 'Spin nicht möglich.');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await res.json();
|
wheelResult = await res.json();
|
||||||
showSpinResult(result);
|
// Animation läuft weiter – Ergebnis wird nach Abschluss angezeigt
|
||||||
loadLock();
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
document.getElementById('wheelAnimModal').classList.remove('open');
|
||||||
|
if (wheelAnimFrame) { cancelAnimationFrame(wheelAnimFrame); wheelAnimFrame = null; }
|
||||||
|
wheelAnimState = 'idle';
|
||||||
alert('Fehler beim Drehen.');
|
alert('Fehler beim Drehen.');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Administration – XXX The Game</title>
|
<title>Administration – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -435,6 +435,7 @@
|
|||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" data-tab="meldungen">Meldungen</button>
|
<button class="tab-btn active" data-tab="meldungen">Meldungen</button>
|
||||||
|
<button class="tab-btn" data-tab="feedback">Feedback</button>
|
||||||
<button class="tab-btn" data-tab="aufgabengruppen">Aufgabengruppen</button>
|
<button class="tab-btn" data-tab="aufgabengruppen">Aufgabengruppen</button>
|
||||||
<button class="tab-btn" data-tab="toys">Toys</button>
|
<button class="tab-btn" data-tab="toys">Toys</button>
|
||||||
<button class="tab-btn superadmin-only" data-tab="admins">Admins</button>
|
<button class="tab-btn superadmin-only" data-tab="admins">Admins</button>
|
||||||
@@ -468,6 +469,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Feedback ── -->
|
||||||
|
<div class="tab-panel" id="panel-feedback">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">📬 Ungelesen</h2>
|
||||||
|
<div class="section-actions">
|
||||||
|
<button class="btn-action" onclick="loadFeedback()">↺ Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="feedback-ungelesen-list"><span class="loading">Wird geladen…</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title" style="color:#f39c12">🔧 In Arbeit</h2>
|
||||||
|
</div>
|
||||||
|
<div id="feedback-inarbeit-list"><span class="loading">Wird geladen…</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title" style="color:var(--color-muted)">✅ Beantwortet</h2>
|
||||||
|
</div>
|
||||||
|
<div id="feedback-beantwortet-list"><span class="loading">Wird geladen…</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Feedback Antwort-Modal ── -->
|
||||||
|
<div class="modal-backdrop" id="feedbackAntwortModal">
|
||||||
|
<div class="modal">
|
||||||
|
<h2>✉️ Antwort senden</h2>
|
||||||
|
<input type="hidden" id="feedbackAntwortId">
|
||||||
|
<label>Antworttext</label>
|
||||||
|
<textarea id="feedbackAntwortText" rows="6" placeholder="Nachricht an den Nutzer…" style="width:100%;box-sizing:border-box;"></textarea>
|
||||||
|
<p style="font-size:0.8rem;color:var(--color-muted);margin-top:0.5rem;">Der Nutzer erhält diese Nachricht als Direktnachricht vom Support-Account.</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" onclick="closeFeedbackAntwort()">Abbrechen</button>
|
||||||
|
<button class="btn-save" onclick="submitFeedbackAntwort()">Absenden</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-error" id="feedbackAntwortError"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Aufgabengruppen ── -->
|
<!-- ── Aufgabengruppen ── -->
|
||||||
<div class="tab-panel" id="panel-aufgabengruppen">
|
<div class="tab-panel" id="panel-aufgabengruppen">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -644,6 +686,7 @@ async function init() {
|
|||||||
document.querySelectorAll('.superadmin-only').forEach(el => el.classList.remove('superadmin-only'));
|
document.querySelectorAll('.superadmin-only').forEach(el => el.classList.remove('superadmin-only'));
|
||||||
}
|
}
|
||||||
loadMeldungen();
|
loadMeldungen();
|
||||||
|
loadFeedback();
|
||||||
loadAdminGruppen();
|
loadAdminGruppen();
|
||||||
loadAdminToys();
|
loadAdminToys();
|
||||||
if (admin.rolle === 'SUPERADMIN') { loadAdmins(); loadTtlockConfig(); loadAllSubscriptions(); }
|
if (admin.rolle === 'SUPERADMIN') { loadAdmins(); loadTtlockConfig(); loadAllSubscriptions(); }
|
||||||
@@ -664,11 +707,114 @@ document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => {
|
|||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
|
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
|
||||||
localStorage.setItem('tab_admin', btn.dataset.tab);
|
localStorage.setItem('tab_admin', btn.dataset.tab);
|
||||||
|
if (btn.dataset.tab === 'feedback') loadFeedback();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Meldungen ─────────────────────────────────────────────────────────────
|
// ── Meldungen ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function loadFeedback() {
|
||||||
|
const r = await fetch('/admin/feedback');
|
||||||
|
if (!r.ok) return;
|
||||||
|
const data = await r.json();
|
||||||
|
renderFeedbackList('feedback-ungelesen-list', data.ungelesen, 'ungelesen');
|
||||||
|
renderFeedbackList('feedback-inarbeit-list', data.inArbeit, 'inArbeit');
|
||||||
|
renderFeedbackList('feedback-beantwortet-list', data.beantwortet, 'beantwortet');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFeedbackList(containerId, list, status) {
|
||||||
|
const el = document.getElementById(containerId);
|
||||||
|
const emptyTexts = { ungelesen: 'Keine ungelesenen Einträge.', inArbeit: 'Niemand arbeitet gerade an einem Eintrag.', beantwortet: 'Noch keine beantworteten Einträge.' };
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
el.innerHTML = '<span class="empty">' + (emptyTexts[status] || '') + '</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = list.map(f => {
|
||||||
|
const actions = status === 'ungelesen'
|
||||||
|
? `<button class="btn-item-edit" onclick="feedbackAnnehmen('${f.feedbackId}',event)">🔧 In Arbeit nehmen</button>`
|
||||||
|
: status === 'inArbeit'
|
||||||
|
? `<button class="btn-item-edit" onclick="openFeedbackAntwort('${f.feedbackId}',event)">✉️ Antworten & abschließen</button>`
|
||||||
|
: '';
|
||||||
|
const inArbeitBadge = f.inArbeitVonName
|
||||||
|
? `<span class="badge" style="background:rgba(243,156,18,0.15);color:#f39c12;">🔧 ${esc(f.inArbeitVonName)}</span>`
|
||||||
|
: '';
|
||||||
|
return `
|
||||||
|
<div class="item" id="fb-item-${f.feedbackId}">
|
||||||
|
<div class="item-row" onclick="toggleFbItem('${f.feedbackId}')">
|
||||||
|
<span class="item-text"><strong>${esc(f.name)}</strong> – ${esc(f.grund)}</span>
|
||||||
|
<span class="item-badges">
|
||||||
|
${inArbeitBadge}
|
||||||
|
<span class="badge badge-neutral">${esc(f.seite)}</span>
|
||||||
|
<span class="badge badge-neutral">${formatDate(f.eingegangen)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-detail">
|
||||||
|
<div class="item-detail-row">
|
||||||
|
<span class="item-detail-label">Von:</span><span class="item-detail-chip">${esc(f.name)}</span>
|
||||||
|
<span class="item-detail-label">Seite:</span><span class="item-detail-chip">${esc(f.seite)}</span>
|
||||||
|
<span class="item-detail-label">Grund:</span><span class="item-detail-chip">${esc(f.grund)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-detail-text">${esc(f.text)}</div>
|
||||||
|
${actions ? `<div class="item-action-btns">${actions}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFbItem(id) {
|
||||||
|
document.getElementById('fb-item-' + id)?.classList.toggle('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function feedbackAnnehmen(id, e) {
|
||||||
|
e?.stopPropagation();
|
||||||
|
const res = await fetch('/admin/feedback/' + id + '/annehmen', { method: 'PUT' });
|
||||||
|
if (res.status === 409) {
|
||||||
|
alert('Dieser Eintrag wird bereits von jemand anderem bearbeitet.');
|
||||||
|
}
|
||||||
|
loadFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFeedbackAntwort(id, e) {
|
||||||
|
e?.stopPropagation();
|
||||||
|
document.getElementById('feedbackAntwortId').value = id;
|
||||||
|
document.getElementById('feedbackAntwortText').value = '';
|
||||||
|
document.getElementById('feedbackAntwortError').style.display = 'none';
|
||||||
|
document.getElementById('feedbackAntwortModal').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFeedbackAntwort() {
|
||||||
|
document.getElementById('feedbackAntwortModal').classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitFeedbackAntwort() {
|
||||||
|
const id = document.getElementById('feedbackAntwortId').value;
|
||||||
|
const text = document.getElementById('feedbackAntwortText').value.trim();
|
||||||
|
const errEl = document.getElementById('feedbackAntwortError');
|
||||||
|
if (!text) { errEl.textContent = 'Bitte einen Text eingeben.'; errEl.style.display = 'block'; return; }
|
||||||
|
const res = await fetch('/admin/feedback/' + id + '/antworten', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ text })
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
closeFeedbackAntwort();
|
||||||
|
loadFeedback();
|
||||||
|
} else {
|
||||||
|
errEl.textContent = 'Fehler beim Senden.';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dt) {
|
||||||
|
if (!dt) return '';
|
||||||
|
return new Date(dt).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
|
||||||
async function loadMeldungen() {
|
async function loadMeldungen() {
|
||||||
const filter = document.getElementById('meldungFilter').value;
|
const filter = document.getElementById('meldungFilter').value;
|
||||||
const r = await fetch('/admin/meldungen' + (filter ? '?status=' + filter : ''));
|
const r = await fetch('/admin/meldungen' + (filter ? '?status=' + filter : ''));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Aufgaben – XXX The Game</title>
|
<title>Aufgaben – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>BDSM Game – Einladung – XXX The Game</title>
|
<title>BDSM Game – Einladung – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||||
<title>BDSM Game</title>
|
<title>BDSM Game – xXx Sphere</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>window.location.replace('/neubdsm.html');</script>
|
<script>window.location.replace('/neubdsm.html');</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>BDSM Game – Im Spiel – XXX The Game</title>
|
<title>BDSM Game – Im Spiel – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||||
<title>BDSM Game</title>
|
<title>BDSM Game – xXx Sphere</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>window.location.replace('/neubdsm.html');</script>
|
<script>window.location.replace('/neubdsm.html');</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||||
<title>BDSM Game</title>
|
<title>BDSM Game – xXx Sphere</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>window.location.replace('/neubdsm.html');</script>
|
<script>window.location.replace('/neubdsm.html');</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||||
<title>BDSM Game</title>
|
<title>BDSM Game – xXx Sphere</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>window.location.replace('/neubdsm.html');</script>
|
<script>window.location.replace('/neubdsm.html');</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
<meta http-equiv="refresh" content="0;url=/neubdsm.html">
|
||||||
<title>BDSM Game</title>
|
<title>BDSM Game – xXx Sphere</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>window.location.replace('/neubdsm.html');</script>
|
<script>window.location.replace('/neubdsm.html');</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Benachrichtigungen – XXX The Game</title>
|
<title>Benachrichtigungen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Profil – XXX The Game</title>
|
<title>Profil – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -539,7 +539,7 @@
|
|||||||
|
|
||||||
const isFriend = profile.friendStatus === 'FRIEND';
|
const isFriend = profile.friendStatus === 'FRIEND';
|
||||||
|
|
||||||
document.title = profile.name + ' – XXX The Game';
|
document.title = profile.name + ' – xXx Sphere';
|
||||||
renderHeader(profile);
|
renderHeader(profile);
|
||||||
|
|
||||||
// ── Galerie ──
|
// ── Galerie ──
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Community Votes – XXX The Game</title>
|
<title>Community Votes – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -188,20 +188,32 @@ body.app {
|
|||||||
.content { padding: 2rem 1.5rem; flex: 1; }
|
.content { padding: 2rem 1.5rem; flex: 1; }
|
||||||
|
|
||||||
/* ── Sidebar ── */
|
/* ── Sidebar ── */
|
||||||
.sidebar {
|
.sidebar-wrapper {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: stretch;
|
||||||
|
gap: 0.75rem;
|
||||||
|
z-index: 10;
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
border: 1px solid var(--color-secondary);
|
border: 1px solid var(--color-secondary);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-self: stretch;
|
overflow: hidden;
|
||||||
position: static;
|
}
|
||||||
|
|
||||||
|
.sidebar-scroll-area {
|
||||||
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 10;
|
|
||||||
transition: transform 0.25s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-logo-area {
|
.sidebar-logo-area {
|
||||||
@@ -257,7 +269,8 @@ body.app {
|
|||||||
|
|
||||||
.sidebar ul { list-style: none; padding: 0.5rem 0; }
|
.sidebar ul { list-style: none; padding: 0.5rem 0; }
|
||||||
|
|
||||||
.sidebar ul li a {
|
.sidebar ul li a,
|
||||||
|
.sidebar-footer ul li a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -270,13 +283,16 @@ body.app {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar ul li a:hover,
|
.sidebar ul li a:hover,
|
||||||
.sidebar ul li a.active {
|
.sidebar ul li a.active,
|
||||||
|
.sidebar-footer ul li a:hover,
|
||||||
|
.sidebar-footer ul li a.active {
|
||||||
background: var(--color-secondary);
|
background: var(--color-secondary);
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
border-left-color: var(--color-primary);
|
border-left-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
|
.sidebar ul li a .icon,
|
||||||
|
.sidebar-footer ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
|
||||||
|
|
||||||
.sidebar-profile-img {
|
.sidebar-profile-img {
|
||||||
width: 1.4rem;
|
width: 1.4rem;
|
||||||
@@ -344,22 +360,38 @@ body.app {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0; right: 0;
|
top: 0; right: 0;
|
||||||
width: 240px;
|
width: 240px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
max-height: 100vh;
|
gap: 0;
|
||||||
border-radius: 0;
|
background: var(--color-bg);
|
||||||
border: none;
|
|
||||||
border-left: 1px solid var(--color-secondary);
|
border-left: 1px solid var(--color-secondary);
|
||||||
box-shadow: none;
|
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-wrapper.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); }
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
flex: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); }
|
|
||||||
.sidebar-logo-area { display: none; }
|
.sidebar-logo-area { display: none; }
|
||||||
.sidebar-desktop-profile { display: none; }
|
.sidebar-desktop-profile { display: none; }
|
||||||
|
|
||||||
@@ -471,6 +503,19 @@ body.app {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Sidebar groups ── */
|
/* ── Sidebar groups ── */
|
||||||
|
.sidebar-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.sidebar-footer ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-group-toggle {
|
.sidebar-group-toggle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -523,10 +568,28 @@ body.app {
|
|||||||
padding: 0.55rem 1rem;
|
padding: 0.55rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Linker Platzhalter – gleiche Breite wie Sidebar */
|
/* Linker Bereich – Banner, gleiche Breite wie Sidebar */
|
||||||
.topbar-left {
|
.topbar-left {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
align-self: stretch;
|
||||||
|
margin: -0.55rem 0 -0.55rem -1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 11px 0 0 11px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0 0 5px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.topbar-left a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.topbar-banner {
|
||||||
|
height: 3.5rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Suche ── */
|
/* ── Suche ── */
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Einladungen – XXX The Game</title>
|
<title>Einladungen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Einstellungen – XXX The Game</title>
|
<title>Einstellungen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -588,7 +588,11 @@
|
|||||||
<p style="font-size:0.85rem;color:var(--color-muted);margin:0.75rem 0 1.25rem 0;">
|
<p style="font-size:0.85rem;color:var(--color-muted);margin:0.75rem 0 1.25rem 0;">
|
||||||
Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern.
|
Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-top:1rem;font-size:0.82rem;">
|
||||||
|
<a href="/help/ttlock.html" style="color:var(--color-muted);">❓ Hilfe zur TTLock-Integration</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="spiel-field">
|
<div class="spiel-field">
|
||||||
<div class="settings-row-label">Benutzername (E-Mail)</div>
|
<div class="settings-row-label">Benutzername (E-Mail)</div>
|
||||||
<input type="text" id="ttl-username" placeholder="E-Mail-Adresse bei TTLock"
|
<input type="text" id="ttl-username" placeholder="E-Mail-Adresse bei TTLock"
|
||||||
@@ -598,7 +602,8 @@
|
|||||||
<div class="spiel-field">
|
<div class="spiel-field">
|
||||||
<div class="settings-row-label">Passwort <span id="ttl-pw-hint" style="font-size:0.78rem;color:var(--color-muted);font-weight:400;"></span></div>
|
<div class="settings-row-label">Passwort <span id="ttl-pw-hint" style="font-size:0.78rem;color:var(--color-muted);font-weight:400;"></span></div>
|
||||||
<input type="password" id="ttl-password" placeholder="Leer lassen = unverändert"
|
<input type="password" id="ttl-password" placeholder="Leer lassen = unverändert"
|
||||||
style="margin-top:0.3rem;" autocomplete="new-password">
|
style="margin-top:0.3rem;" autocomplete="new-password"
|
||||||
|
onfocus="ttlPwFocus()" onblur="ttlPwBlur()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spiel-field">
|
<div class="spiel-field">
|
||||||
@@ -609,6 +614,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="ttl-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-bottom:0.5rem;"></div>
|
<div id="ttl-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-bottom:0.5rem;"></div>
|
||||||
|
<!-- Verbindungsstatus -->
|
||||||
|
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
|
||||||
|
<span style="font-size:0.82rem;color:var(--color-muted);">Verbindungsstatus:</span>
|
||||||
|
<span id="ttl-test-status" style="font-size:0.82rem;font-weight:600;"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;align-items:center;">
|
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;align-items:center;">
|
||||||
<button onclick="saveTtlockUserConfig()" style="width:auto;padding:0.5rem 1.25rem;margin:0;">Speichern</button>
|
<button onclick="saveTtlockUserConfig()" style="width:auto;padding:0.5rem 1.25rem;margin:0;">Speichern</button>
|
||||||
<button onclick="testTtlockConnection()" id="ttl-test-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔌 Verbindung testen</button>
|
<button onclick="testTtlockConnection()" id="ttl-test-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔌 Verbindung testen</button>
|
||||||
@@ -620,12 +631,23 @@
|
|||||||
|
|
||||||
<!-- Öffnen-Ergebnis -->
|
<!-- Öffnen-Ergebnis -->
|
||||||
<div id="ttl-open-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-top:0.5rem;"></div>
|
<div id="ttl-open-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-top:0.5rem;"></div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- TTLock Lade-Dialog -->
|
||||||
|
<div class="modal-backdrop" id="ttlLoadingModal">
|
||||||
|
<div class="modal" style="max-width:320px;text-align:center;padding:2rem 1.5rem;">
|
||||||
|
<div style="font-size:2rem;margin-bottom:0.75rem;">⏳</div>
|
||||||
|
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
|
||||||
|
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- TTLock Öffnen Modal -->
|
<!-- TTLock Öffnen Modal -->
|
||||||
<div class="modal-backdrop" id="ttlOpenModal">
|
<div class="modal-backdrop" id="ttlOpenModal">
|
||||||
<div class="modal" style="max-width:380px;text-align:center;">
|
<div class="modal" style="max-width:380px;text-align:center;">
|
||||||
@@ -1087,29 +1109,74 @@
|
|||||||
|
|
||||||
// ── TTLock ──────────────────────────────────────────────────────────────
|
// ── TTLock ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _ttlPasswordSet = false;
|
||||||
|
const TTL_PW_DOTS = '••••••';
|
||||||
|
|
||||||
async function loadTtlockUserConfig() {
|
async function loadTtlockUserConfig() {
|
||||||
const r = await fetch('/user/me/ttlock');
|
const r = await fetch('/user/me/ttlock');
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
const cfg = await r.json();
|
const cfg = await r.json();
|
||||||
document.getElementById('ttl-username').value = cfg.username || '';
|
document.getElementById('ttl-username').value = cfg.username || '';
|
||||||
document.getElementById('ttl-lockid').value = cfg.lockId != null ? cfg.lockId : '';
|
document.getElementById('ttl-lockid').value = cfg.lockId != null ? cfg.lockId : '';
|
||||||
document.getElementById('ttl-password').value = '';
|
_ttlPasswordSet = !!cfg.passwordSet;
|
||||||
document.getElementById('ttl-pw-hint').textContent =
|
const pwField = document.getElementById('ttl-password');
|
||||||
cfg.passwordSet ? '(gesetzt – leer lassen zum Beibehalten)' : '(noch nicht gesetzt)';
|
if (_ttlPasswordSet) {
|
||||||
|
pwField.value = TTL_PW_DOTS;
|
||||||
|
document.getElementById('ttl-pw-hint').textContent = '';
|
||||||
|
} else {
|
||||||
|
pwField.value = '';
|
||||||
|
document.getElementById('ttl-pw-hint').textContent = '(noch nicht gesetzt)';
|
||||||
|
}
|
||||||
|
renderTtlockTestStatus(cfg.testSuccessful);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTtlockTestStatus(ok) {
|
||||||
|
const el = document.getElementById('ttl-test-status');
|
||||||
|
if (ok) {
|
||||||
|
el.textContent = '✅ Verbindung erfolgreich getestet';
|
||||||
|
el.style.color = '#27ae60';
|
||||||
|
} else {
|
||||||
|
el.textContent = '⚠️ Noch nicht getestet';
|
||||||
|
el.style.color = 'var(--color-muted)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ttlPwFocus() {
|
||||||
|
const pwField = document.getElementById('ttl-password');
|
||||||
|
if (pwField.value === TTL_PW_DOTS) {
|
||||||
|
pwField.value = '';
|
||||||
|
document.getElementById('ttl-pw-hint').textContent = '(leer lassen = unverändert)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ttlPwBlur() {
|
||||||
|
const pwField = document.getElementById('ttl-password');
|
||||||
|
if (pwField.value === '' && _ttlPasswordSet) {
|
||||||
|
pwField.value = TTL_PW_DOTS;
|
||||||
|
document.getElementById('ttl-pw-hint').textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTtlockLoading() {
|
||||||
|
document.getElementById('ttlLoadingModal').classList.add('visible');
|
||||||
|
}
|
||||||
|
function hideTtlockLoading() {
|
||||||
|
document.getElementById('ttlLoadingModal').classList.remove('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testTtlockConnection() {
|
async function testTtlockConnection() {
|
||||||
const btn = document.getElementById('ttl-test-btn');
|
const btn = document.getElementById('ttl-test-btn');
|
||||||
const result = document.getElementById('ttl-test-result');
|
const result = document.getElementById('ttl-test-result');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '⏳ Teste…';
|
|
||||||
result.style.display = 'none';
|
result.style.display = 'none';
|
||||||
|
showTtlockLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/user/me/ttlock/test');
|
const r = await fetch('/user/me/ttlock/test');
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
renderTtlockTestStatus(true);
|
||||||
const battery = data.electricQuantity != null ? `${data.electricQuantity}%` : '–';
|
const battery = data.electricQuantity != null ? `${data.electricQuantity}%` : '–';
|
||||||
const state = data.state || '–';
|
const state = data.state || '–';
|
||||||
result.style.background = 'rgba(39,174,96,0.08)';
|
result.style.background = 'rgba(39,174,96,0.08)';
|
||||||
@@ -1138,6 +1205,8 @@
|
|||||||
result.style.background = 'rgba(231,76,60,0.08)';
|
result.style.background = 'rgba(231,76,60,0.08)';
|
||||||
result.style.borderColor = '#e74c3c';
|
result.style.borderColor = '#e74c3c';
|
||||||
result.innerHTML = `<div style="color:#e74c3c;">❌ Netzwerkfehler.</div>`;
|
result.innerHTML = `<div style="color:#e74c3c;">❌ Netzwerkfehler.</div>`;
|
||||||
|
} finally {
|
||||||
|
hideTtlockLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
result.style.display = '';
|
result.style.display = '';
|
||||||
@@ -1153,8 +1222,8 @@
|
|||||||
const btn = document.getElementById('ttl-open-btn');
|
const btn = document.getElementById('ttl-open-btn');
|
||||||
const errEl = document.getElementById('ttl-open-error');
|
const errEl = document.getElementById('ttl-open-error');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '⏳ …';
|
|
||||||
errEl.textContent = '';
|
errEl.textContent = '';
|
||||||
|
showTtlockLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/user/me/ttlock/open', { method: 'POST' });
|
const r = await fetch('/user/me/ttlock/open', { method: 'POST' });
|
||||||
@@ -1173,6 +1242,7 @@
|
|||||||
|
|
||||||
const pwdId = data.keyboardPwdId;
|
const pwdId = data.keyboardPwdId;
|
||||||
document.getElementById('ttl-open-pin').textContent = data.pin;
|
document.getElementById('ttl-open-pin').textContent = data.pin;
|
||||||
|
hideTtlockLoading();
|
||||||
|
|
||||||
const modal = document.getElementById('ttlOpenModal');
|
const modal = document.getElementById('ttlOpenModal');
|
||||||
modal.classList.add('visible');
|
modal.classList.add('visible');
|
||||||
@@ -1182,11 +1252,14 @@
|
|||||||
okBtn.removeEventListener('click', onOk);
|
okBtn.removeEventListener('click', onOk);
|
||||||
okBtn.disabled = true;
|
okBtn.disabled = true;
|
||||||
modal.classList.remove('visible');
|
modal.classList.remove('visible');
|
||||||
|
showTtlockLoading();
|
||||||
await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' });
|
await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' });
|
||||||
|
hideTtlockLoading();
|
||||||
okBtn.disabled = false;
|
okBtn.disabled = false;
|
||||||
};
|
};
|
||||||
okBtn.addEventListener('click', onOk);
|
okBtn.addEventListener('click', onOk);
|
||||||
} catch {
|
} catch {
|
||||||
|
hideTtlockLoading();
|
||||||
errEl.textContent = 'Netzwerkfehler.';
|
errEl.textContent = 'Netzwerkfehler.';
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -1198,9 +1271,10 @@
|
|||||||
const errEl = document.getElementById('ttl-error');
|
const errEl = document.getElementById('ttl-error');
|
||||||
errEl.textContent = '';
|
errEl.textContent = '';
|
||||||
const lockIdVal = document.getElementById('ttl-lockid').value.trim();
|
const lockIdVal = document.getElementById('ttl-lockid').value.trim();
|
||||||
|
const pwVal = document.getElementById('ttl-password').value;
|
||||||
const body = {
|
const body = {
|
||||||
username: document.getElementById('ttl-username').value.trim(),
|
username: document.getElementById('ttl-username').value.trim(),
|
||||||
password: document.getElementById('ttl-password').value,
|
password: pwVal === TTL_PW_DOTS ? '' : pwVal,
|
||||||
lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null
|
lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null
|
||||||
};
|
};
|
||||||
const r = await fetch('/user/me/ttlock', {
|
const r = await fetch('/user/me/ttlock', {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Entdecken – XXX The Game</title>
|
<title>Entdecken – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Feed – XXX The Game</title>
|
<title>Feed – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>xXx Games – Passwort vergessen</title>
|
<title>Passwort vergessen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Freunde – XXX The Game</title>
|
<title>Freunde – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Gruppe – XXX The Game</title>
|
<title>Gruppe – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
gruppeData = await res.json();
|
gruppeData = await res.json();
|
||||||
myRole = gruppeData.myRole;
|
myRole = gruppeData.myRole;
|
||||||
|
|
||||||
document.title = gruppeData.name + ' – XXX The Game';
|
document.title = gruppeData.name + ' – xXx Sphere';
|
||||||
document.getElementById('gruppeName').textContent = gruppeData.name;
|
document.getElementById('gruppeName').textContent = gruppeData.name;
|
||||||
document.getElementById('gruppeMeta').textContent =
|
document.getElementById('gruppeMeta').textContent =
|
||||||
gruppeData.memberCount + ' Mitglied' + (gruppeData.memberCount !== 1 ? 'er' : '') +
|
gruppeData.memberCount + ' Mitglied' + (gruppeData.memberCount !== 1 ? 'er' : '') +
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Gruppen – XXX The Game</title>
|
<title>Gruppen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
108
xxxthegame/src/main/resources/static/help/impressum.html
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Impressum – xXx Sphere</title>
|
||||||
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
.hilfe-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.hilfe-header h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 0 0 0.4rem 0;
|
||||||
|
}
|
||||||
|
.hilfe-header p {
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impressum-block {
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.impressum-block h2 {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-muted);
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
}
|
||||||
|
.impressum-block p,
|
||||||
|
.impressum-block address {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.8;
|
||||||
|
margin: 0;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
.impressum-block a {
|
||||||
|
color: var(--color-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.impressum-block a:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.impressum-block + .impressum-block {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<div class="hilfe-header">
|
||||||
|
<h1>📄 Impressum</h1>
|
||||||
|
<p>Angaben gemäß § 5 TMG</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="impressum-block">
|
||||||
|
<h2>Verantwortlich</h2>
|
||||||
|
<address>
|
||||||
|
Vorname Nachname<br>
|
||||||
|
Musterstraße 1<br>
|
||||||
|
12345 Musterstadt<br>
|
||||||
|
Deutschland
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="impressum-block">
|
||||||
|
<h2>Kontakt</h2>
|
||||||
|
<p>
|
||||||
|
E-Mail: <a href="mailto:kontakt@xxx-sphere.de">kontakt@xxx-sphere.de</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="impressum-block">
|
||||||
|
<h2>Hinweis</h2>
|
||||||
|
<p>
|
||||||
|
xXx Sphere ist ein privat betriebenes Projekt ohne kommerzielle Absicht.
|
||||||
|
Die Plattform richtet sich ausschließlich an volljährige Personen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="impressum-block">
|
||||||
|
<h2>Haftungsausschluss</h2>
|
||||||
|
<p>
|
||||||
|
Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links.
|
||||||
|
Für den Inhalt verlinkter Seiten sind ausschließlich deren Betreiber verantwortlich.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/js/icons.js"></script>
|
||||||
|
<script src="/js/sidebar.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
265
xxxthegame/src/main/resources/static/help/kontakt.html
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kontakt & Feedback – xXx Sphere</title>
|
||||||
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
.hilfe-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.hilfe-header h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 0 0 0.4rem 0;
|
||||||
|
}
|
||||||
|
.hilfe-header p {
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── E-Mail-Hinweis ── */
|
||||||
|
.mail-hint {
|
||||||
|
background: rgba(var(--color-primary-rgb, 120,80,200), 0.08);
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.mail-hint strong { color: var(--color-text); }
|
||||||
|
.mail-hint a { color: var(--color-text); }
|
||||||
|
|
||||||
|
/* ── Formular-Card ── */
|
||||||
|
.feedback-card {
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-bottom: 1.1rem;
|
||||||
|
}
|
||||||
|
.form-group:last-of-type {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.65rem 0.9rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.form-group input[readonly],
|
||||||
|
.form-group input:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 130px;
|
||||||
|
}
|
||||||
|
.form-group select option {
|
||||||
|
background: var(--color-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-counter {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.char-counter.warn { color: #e74c3c; }
|
||||||
|
|
||||||
|
.btn-send {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.btn-send:hover { opacity: 0.85; }
|
||||||
|
.btn-send:disabled { opacity: 0.45; cursor: default; }
|
||||||
|
|
||||||
|
.feedback-success {
|
||||||
|
display: none;
|
||||||
|
background: rgba(39,174,96,0.1);
|
||||||
|
border-left: 3px solid #27ae60;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.feedback-success strong { color: #27ae60; }
|
||||||
|
.feedback-error {
|
||||||
|
display: none;
|
||||||
|
background: rgba(231,76,60,0.08);
|
||||||
|
border-left: 3px solid #e74c3c;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.feedback-error strong { color: #e74c3c; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<div class="hilfe-header">
|
||||||
|
<h1>✉️ Kontakt & Feedback</h1>
|
||||||
|
<p>Hast du Fragen, Ideen oder einen Fehler gefunden? Schreib uns!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mail-hint">
|
||||||
|
<strong>Alternativ per E-Mail:</strong> Du kannst uns auch direkt schreiben –
|
||||||
|
<a href="mailto:kontakt@xxx-sphere.de">kontakt@xxx-sphere.de</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feedback-card">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fb-name">Name</label>
|
||||||
|
<input type="text" id="fb-name" readonly placeholder="Wird geladen…">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fb-seite">Seite</label>
|
||||||
|
<input type="text" id="fb-seite" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fb-grund">Kontaktgrund</label>
|
||||||
|
<select id="fb-grund">
|
||||||
|
<option value="Fehlermeldung">🐛 Fehlermeldung</option>
|
||||||
|
<option value="Feedback">💬 Feedback</option>
|
||||||
|
<option value="Idee & Verbesserungsvorschlag">💡 Idee & Verbesserungsvorschlag</option>
|
||||||
|
<option value="Nachfrage">❓ Nachfrage</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fb-text">Nachricht</label>
|
||||||
|
<textarea id="fb-text" maxlength="1000" placeholder="Beschreibe dein Anliegen…"></textarea>
|
||||||
|
<span class="char-counter" id="fb-counter">0 / 1000</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-send" id="fb-send" onclick="sendFeedback()">✉️ Absenden</button>
|
||||||
|
<div class="feedback-success" id="fb-success">
|
||||||
|
<strong>Vielen Dank!</strong> Deine Nachricht wurde erfolgreich übermittelt.
|
||||||
|
</div>
|
||||||
|
<div class="feedback-error" id="fb-error">
|
||||||
|
<strong>Fehler:</strong> Die Nachricht konnte nicht gesendet werden. Bitte versuche es später erneut.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/js/icons.js"></script>
|
||||||
|
<script src="/js/sidebar.js"></script>
|
||||||
|
<script>
|
||||||
|
// Name vorausfüllen
|
||||||
|
fetch('/login/me')
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(user => {
|
||||||
|
const field = document.getElementById('fb-name');
|
||||||
|
if (user && user.name) {
|
||||||
|
field.value = user.name;
|
||||||
|
} else {
|
||||||
|
field.value = 'Gast';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { document.getElementById('fb-name').value = 'Gast'; });
|
||||||
|
|
||||||
|
// Seite vorausfüllen
|
||||||
|
(function () {
|
||||||
|
const field = document.getElementById('fb-seite');
|
||||||
|
try {
|
||||||
|
const ref = document.referrer;
|
||||||
|
if (ref) {
|
||||||
|
const url = new URL(ref);
|
||||||
|
field.value = url.pathname;
|
||||||
|
} else {
|
||||||
|
field.value = 'Direkt aufgerufen';
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
field.value = 'Unbekannt';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Zeichenzähler
|
||||||
|
document.getElementById('fb-text').addEventListener('input', function () {
|
||||||
|
const len = this.value.length;
|
||||||
|
const counter = document.getElementById('fb-counter');
|
||||||
|
counter.textContent = len + ' / 1000';
|
||||||
|
counter.className = 'char-counter' + (len > 900 || len < 10 ? ' warn' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
async function sendFeedback() {
|
||||||
|
const btn = document.getElementById('fb-send');
|
||||||
|
const text = document.getElementById('fb-text').value.trim();
|
||||||
|
if (text.length < 10) { document.getElementById('fb-text').focus(); return; }
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
document.getElementById('fb-success').style.display = 'none';
|
||||||
|
document.getElementById('fb-error').style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/feedback', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: document.getElementById('fb-name').value,
|
||||||
|
seite: document.getElementById('fb-seite').value,
|
||||||
|
grund: document.getElementById('fb-grund').value,
|
||||||
|
text: text
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
document.getElementById('fb-success').style.display = 'block';
|
||||||
|
document.getElementById('fb-text').value = '';
|
||||||
|
document.getElementById('fb-counter').textContent = '0 / 1000';
|
||||||
|
} else {
|
||||||
|
document.getElementById('fb-error').style.display = 'block';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
document.getElementById('fb-error').style.display = 'block';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
221
xxxthegame/src/main/resources/static/help/overview.html
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hilfe-Übersicht – xXx Sphere</title>
|
||||||
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
.hilfe-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.hilfe-header h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 0 0 0.4rem 0;
|
||||||
|
}
|
||||||
|
.hilfe-header p {
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Kategorien-Grid ── */
|
||||||
|
.hilfe-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.hilfe-card {
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.hilfe-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
|
padding-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
.hilfe-card-desc {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.hilfe-card-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.hilfe-card-links a {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.hilfe-card-links a:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.hilfe-card-links a::before {
|
||||||
|
content: '›';
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Abschnitt-Überschrift ── */
|
||||||
|
.hilfe-section-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-muted);
|
||||||
|
margin: 1.75rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
.hilfe-section-label:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<div class="hilfe-header">
|
||||||
|
<h1>❓ Hilfe-Übersicht</h1>
|
||||||
|
<p>Hier findest du Anleitungen und Erklärungen zu allen Bereichen von xXx Sphere.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Einstellungen & Konto ── -->
|
||||||
|
<div class="hilfe-section-label">Einstellungen & Konto</div>
|
||||||
|
<div class="hilfe-grid">
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">⚙️ Allgemeine Einstellungen</div>
|
||||||
|
<div class="hilfe-card-desc">Profil, Benachrichtigungen, Datenschutz und weitere Kontoeinstellungen.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Profil bearbeiten</a>
|
||||||
|
<a href="#">Benachrichtigungen konfigurieren</a>
|
||||||
|
<a href="#">Passwort ändern</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">🔒 TTLock-Integration</div>
|
||||||
|
<div class="hilfe-card-desc">Verbinde deine physische Schlüsselbox mit xXx Sphere für automatische Code-Verwaltung.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="/help/ttlock.html#sec-intro">Was ist TTLock?</a>
|
||||||
|
<a href="/help/ttlock.html#sec-table">Voraussetzungen</a>
|
||||||
|
<a href="/help/ttlock.html#sec-howto">TTLock einrichten</a>
|
||||||
|
<a href="/help/ttlock.html#sec-faq1">Warum nur für Abonnenten?</a>
|
||||||
|
<a href="/help/ttlock.html#sec-faq2">Notfall-Öffnung</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">💳 Abonnements</div>
|
||||||
|
<div class="hilfe-card-desc">Informationen zu Premium-Funktionen und wie du dein Abonnement verwaltest.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Premium-Funktionen im Überblick</a>
|
||||||
|
<a href="#">Abonnement kündigen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Spiele ── -->
|
||||||
|
<div class="hilfe-section-label">Spiele</div>
|
||||||
|
<div class="hilfe-grid">
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">🔒 Chastity Game</div>
|
||||||
|
<div class="hilfe-card-desc">Alles rund um Schlösser, Keyholder, Karten und Aufgaben im Chastity Game.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Neues Lock starten</a>
|
||||||
|
<a href="#">Die Rolle als Keyholder</a>
|
||||||
|
<a href="#">Karten und Aufgaben</a>
|
||||||
|
<a href="#">TimeLock erklärt</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">⛓️ BDSM Game</div>
|
||||||
|
<div class="hilfe-card-desc">Sessions erstellen, Spieler einladen und Aufgaben verwalten.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Session starten</a>
|
||||||
|
<a href="#">Spieler einladen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">⚪ Vanilla Game</div>
|
||||||
|
<div class="hilfe-card-desc">Leichtere Spiele ohne strenge Regeln – für den entspannten Einstieg.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Vanilla-Session starten</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Community ── -->
|
||||||
|
<div class="hilfe-section-label">Community</div>
|
||||||
|
<div class="hilfe-grid">
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">👥 Gruppen</div>
|
||||||
|
<div class="hilfe-card-desc">Gruppen erstellen, beitreten und verwalten.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Gruppe erstellen</a>
|
||||||
|
<a href="#">Mitglieder verwalten</a>
|
||||||
|
<a href="#">Beiträge und Abstimmungen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">📰 Feed & Profil</div>
|
||||||
|
<div class="hilfe-card-desc">Beiträge teilen, Profile entdecken und die Community kennenlernen.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Feed nutzen</a>
|
||||||
|
<a href="#">Profil gestalten</a>
|
||||||
|
<a href="#">Personen suchen und folgen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">🏆 Community Votes</div>
|
||||||
|
<div class="hilfe-card-desc">Verifikationen bewerten und an Community-Abstimmungen teilnehmen.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Wie funktionieren Votes?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Sonstiges ── -->
|
||||||
|
<div class="hilfe-section-label">Sonstiges</div>
|
||||||
|
<div class="hilfe-grid">
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">🔐 Sicherheit & Datenschutz</div>
|
||||||
|
<div class="hilfe-card-desc">Wie deine Daten gespeichert werden und welche Sicherheitsmaßnahmen wir treffen.</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Datenspeicherung</a>
|
||||||
|
<a href="#">Passwort-Hashing</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-card">
|
||||||
|
<div class="hilfe-card-title">🐛 Fehler melden</div>
|
||||||
|
<div class="hilfe-card-desc">Hast du einen Fehler gefunden oder einen Verbesserungsvorschlag?</div>
|
||||||
|
<div class="hilfe-card-links">
|
||||||
|
<a href="#">Feedback senden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/js/icons.js"></script>
|
||||||
|
<script src="/js/sidebar.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
345
xxxthegame/src/main/resources/static/help/template.html
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SEITENTITEL – xXx Sphere</title>
|
||||||
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
/* ── Hilfe-Seite Basis ── */
|
||||||
|
.hilfe-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.hilfe-header h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 0 0 0.4rem 0;
|
||||||
|
}
|
||||||
|
.hilfe-header p {
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Abschnitte (Accordion) ── */
|
||||||
|
.hilfe-section {
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hilfe-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.hilfe-section-header:hover {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
.hilfe-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.hilfe-section-arrow {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.hilfe-section.open .hilfe-section-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.hilfe-section-body {
|
||||||
|
display: none;
|
||||||
|
padding: 0 1.25rem 1.25rem;
|
||||||
|
border-top: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
.hilfe-section.open .hilfe-section-body {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Fließtext in Abschnitten ── */
|
||||||
|
.hilfe-section-body p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0.9rem 0 0;
|
||||||
|
}
|
||||||
|
.hilfe-section-body p:first-child {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Schritt-für-Schritt Liste ── */
|
||||||
|
.hilfe-steps {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.hilfe-steps li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.85rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.hilfe-steps li .step-num {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Hinweis-Box ── */
|
||||||
|
.hilfe-hint {
|
||||||
|
background: rgba(var(--color-primary-rgb, 120,80,200), 0.08);
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.hilfe-hint strong {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Warn-Box ── */
|
||||||
|
.hilfe-warn {
|
||||||
|
background: rgba(231,76,60,0.08);
|
||||||
|
border-left: 3px solid #e74c3c;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.hilfe-warn strong {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info-Box (neutral) ── */
|
||||||
|
.hilfe-info {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Einfache Tabelle ── */
|
||||||
|
.hilfe-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.hilfe-table th {
|
||||||
|
text-align: left;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.4rem 0.75rem 0.4rem 0;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
.hilfe-table td {
|
||||||
|
color: var(--color-muted);
|
||||||
|
padding: 0.5rem 0.75rem 0.5rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.hilfe-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Trennlinie ── */
|
||||||
|
.hilfe-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-secondary);
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Inline-Badge ── */
|
||||||
|
.hilfe-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--color-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-muted);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- ── Kopfzeile ─────────────────────────────────────── -->
|
||||||
|
<div class="hilfe-header">
|
||||||
|
<h1>🔒 SEITENTITEL</h1>
|
||||||
|
<p>Kurze Beschreibung, worum es auf dieser Hilfeseite geht.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: Einfacher Fließtext ───────────────── -->
|
||||||
|
<div class="hilfe-section open" id="sec-intro">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-intro')">
|
||||||
|
<span class="hilfe-section-title">📖 Was ist das?</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>
|
||||||
|
Hier steht ein einleitender Text. Du kannst mehrere Absätze verwenden,
|
||||||
|
um das Thema zu erklären.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Zweiter Absatz mit weiteren Informationen. Links gehen so:
|
||||||
|
<a href="/neulock.html">neues Lock starten</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Hinweis-Box (lila) -->
|
||||||
|
<div class="hilfe-hint">
|
||||||
|
<strong>Hinweis:</strong> Hier steht ein wichtiger, aber freundlicher Hinweis.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: Schritt-für-Schritt ───────────────── -->
|
||||||
|
<div class="hilfe-section" id="sec-howto">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-howto')">
|
||||||
|
<span class="hilfe-section-title">🚀 So funktioniert es</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>Führe diese Schritte der Reihe nach aus:</p>
|
||||||
|
<ol class="hilfe-steps">
|
||||||
|
<li>
|
||||||
|
<span class="step-num">1</span>
|
||||||
|
<span>Erster Schritt – was der Nutzer hier tun muss.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">2</span>
|
||||||
|
<span>Zweiter Schritt – weitere Aktion mit Erklärung.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">3</span>
|
||||||
|
<span>Dritter Schritt – Abschluss oder Ergebnis.</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<!-- Warn-Box (rot) -->
|
||||||
|
<div class="hilfe-warn">
|
||||||
|
<strong>Achtung:</strong> Hier steht eine Warnung, z. B. dass eine Aktion nicht rückgängig gemacht werden kann.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: Tabelle ────────────────────────────── -->
|
||||||
|
<div class="hilfe-section" id="sec-table">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-table')">
|
||||||
|
<span class="hilfe-section-title">📋 Übersicht</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<table class="hilfe-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Funktion</th>
|
||||||
|
<th>Beschreibung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><span class="hilfe-badge">Beispiel A</span></td>
|
||||||
|
<td>Erklärung zu Funktion A.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class="hilfe-badge">Beispiel B</span></td>
|
||||||
|
<td>Erklärung zu Funktion B.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class="hilfe-badge">Beispiel C</span></td>
|
||||||
|
<td>Erklärung zu Funktion C.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: FAQ ────────────────────────────────── -->
|
||||||
|
<div class="hilfe-section" id="sec-faq1">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
|
||||||
|
<span class="hilfe-section-title">❓ Häufige Frage 1?</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>
|
||||||
|
Antwort auf die erste häufige Frage. Kann auch mehrere Absätze haben.
|
||||||
|
</p>
|
||||||
|
<div class="hilfe-info">
|
||||||
|
Neutrale Info-Box für ergänzende Details ohne Wertung.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hilfe-section" id="sec-faq2">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
|
||||||
|
<span class="hilfe-section-title">❓ Häufige Frage 2?</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>
|
||||||
|
Antwort auf die zweite häufige Frage.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/icons.js"></script>
|
||||||
|
<script src="/js/sidebar.js"></script>
|
||||||
|
<script>
|
||||||
|
function toggleSection(id) {
|
||||||
|
document.getElementById(id).classList.toggle('open');
|
||||||
|
}
|
||||||
|
function openFromHash() {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
if (!hash) return;
|
||||||
|
const el = document.getElementById(hash);
|
||||||
|
if (el && el.classList.contains('hilfe-section')) {
|
||||||
|
el.classList.add('open');
|
||||||
|
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', openFromHash);
|
||||||
|
window.addEventListener('hashchange', openFromHash);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
360
xxxthegame/src/main/resources/static/help/ttlock.html
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hilfe TTLock – xXx Sphere</title>
|
||||||
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
/* ── Hilfe-Seite Basis ── */
|
||||||
|
.hilfe-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.hilfe-header h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 0 0 0.4rem 0;
|
||||||
|
}
|
||||||
|
.hilfe-header p {
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Abschnitte (Accordion) ── */
|
||||||
|
.hilfe-section {
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hilfe-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.hilfe-section-header:hover {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
.hilfe-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.hilfe-section-arrow {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.hilfe-section.open .hilfe-section-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.hilfe-section-body {
|
||||||
|
display: none;
|
||||||
|
padding: 0 1.25rem 1.25rem;
|
||||||
|
border-top: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
.hilfe-section.open .hilfe-section-body {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Fließtext in Abschnitten ── */
|
||||||
|
.hilfe-section-body p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0.9rem 0 0;
|
||||||
|
}
|
||||||
|
.hilfe-section-body p:first-child {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Schritt-für-Schritt Liste ── */
|
||||||
|
.hilfe-steps {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.hilfe-steps li {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.85rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.hilfe-steps li::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.hilfe-steps li .step-num {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
}
|
||||||
|
.hilfe-steps li strong,
|
||||||
|
.hilfe-steps li em {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Hinweis-Box ── */
|
||||||
|
.hilfe-hint {
|
||||||
|
background: rgba(var(--color-primary-rgb, 120,80,200), 0.08);
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.hilfe-hint strong {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Warn-Box ── */
|
||||||
|
.hilfe-warn {
|
||||||
|
background: rgba(231,76,60,0.08);
|
||||||
|
border-left: 3px solid #e74c3c;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.hilfe-warn strong {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Info-Box (neutral) ── */
|
||||||
|
.hilfe-info {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--color-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Einfache Tabelle ── */
|
||||||
|
.hilfe-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.hilfe-table th {
|
||||||
|
text-align: left;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.4rem 0.75rem 0.4rem 0;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
.hilfe-table td {
|
||||||
|
color: var(--color-muted);
|
||||||
|
padding: 0.5rem 0.75rem 0.5rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.hilfe-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Trennlinie ── */
|
||||||
|
.hilfe-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-secondary);
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Inline-Badge ── */
|
||||||
|
.hilfe-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--color-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-muted);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="app">
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- ── Kopfzeile ─────────────────────────────────────── -->
|
||||||
|
<div class="hilfe-header">
|
||||||
|
<h1>🔒 TTLock</h1>
|
||||||
|
<p>Hilfe zur Einrichtung der Kommunikation mit einer TTLock-Schlüsselbox</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: Einfacher Fließtext ───────────────── -->
|
||||||
|
<div class="hilfe-section open" id="sec-intro">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-intro')">
|
||||||
|
<span class="hilfe-section-title">📖 Was ist das?</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>
|
||||||
|
<a href="https://ttlock.com/#/">TTLock</a> ist ein weit verbreitetes System für die Verwaltung von smarten Schlössern und Schlüsselboxen. Die Hardware kommuniziert in der Regel via Bluetooth, lässt sich aber über ein G2-Gateway auch aus der Ferne steuern.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Für Entwickler und Unternehmen bietet die TTLock Open Platform eine leistungsstarke REST-API. Damit lässt sich die Schlossverwaltung in eigene Anwendungen integrieren.
|
||||||
|
Diese API verwenden wir, um die Codes deiner Schlüsselbox zu steuern - kein nerviges manuelles Eintragen des generierten Schlüssels mehr.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
TTLock steht allen Premium-Abonenten zur Verfügung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: Tabelle ────────────────────────────── -->
|
||||||
|
<div class="hilfe-section" id="sec-table">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-table')">
|
||||||
|
<span class="hilfe-section-title">📋 Voraussetzungen</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<div class="hilfe-warn">
|
||||||
|
<strong>Achtung:</strong> Für die Verwendung ist zwingend ein G2-Gateway für die Kommunikation von TTLock-Server zu Deiner Schlüsselbox notwendig.
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-hint">
|
||||||
|
<strong>Hinweis:</strong> Für die Verwendung einer TTLock-Schlüsselbox in Spielen ist zwingend ein Premium-Abonement notwendig.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: Schritt-für-Schritt ───────────────── -->
|
||||||
|
<div class="hilfe-section" id="sec-howto">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-howto')">
|
||||||
|
<span class="hilfe-section-title">🚀 So funktioniert es</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>Führe diese Schritte der Reihe nach aus:</p>
|
||||||
|
<ol class="hilfe-steps">
|
||||||
|
<li>
|
||||||
|
<span class="step-num">1</span>
|
||||||
|
<span>App-Setup: Verbinde deine Schlüsselbox in der TTLock-App. Wichtig: Für die Fernsteuerung muss ein Gateway (G2) eingerichtet und aktiv sein.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">2</span>
|
||||||
|
<span>Fernzugriff aktivieren: Aktiviere die Funktion in den App-Einstellungen. Tipp: Schalte das WLAN an deinem Handy aus und versuche, die Box über mobile Daten zu öffnen. Funktioniert das? Dann ist alles bereit.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">3</span>
|
||||||
|
<span>Accounts verknüpfen: Trage deine TTLock-Zugangsdaten unter Einstellungen > TTLock ein.</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">4</span>
|
||||||
|
<span>Lock-ID hinterlegen: Gib die ID deiner Box an (zu finden unter MAC/ID). Wichtig: Nur den Teil hinter dem Schrägstrich nutzen (z.B. bei 00:11.../123456 nur 123456).</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">5</span>
|
||||||
|
<span>Verbindung testen: Klicke auf „Verbindung testen“. Erst nach einem grünen Licht ist das System aktiv.</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<!-- Hinweis-Box (lila) -->
|
||||||
|
<div class="hilfe-hint">
|
||||||
|
<strong>Hinweis:</strong> Wir speichern dein Passwort nicht im Klartext in der Datenbank sondern nur als MD5-Hash.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Abschnitt: FAQ ────────────────────────────────── -->
|
||||||
|
<div class="hilfe-section" id="sec-faq1">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
|
||||||
|
<span class="hilfe-section-title">❓ Warum steht dieser Dienst nur für Abonennten zur Verfügung?</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>
|
||||||
|
Die Verwendung der API von TTLock ist nur begrenzt kostenlos verwendbar. Ab bestimmten Kontingenten wird die Verwendung für uns kostenpflichtig.
|
||||||
|
</p>
|
||||||
|
<div class="hilfe-info">
|
||||||
|
Es gilt weiter der Grundsatz, XXX-Sphere soll niemanden reich machen - Die Abonemments dienen dazu die laufenden Kosten (Server, API-Schnittstellen etc.) zu decken.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hilfe-section" id="sec-faq2">
|
||||||
|
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
|
||||||
|
<span class="hilfe-section-title">❓ Hilfe - ich komme nicht mehr raus...Was machen Sachen?</span>
|
||||||
|
<span class="hilfe-section-arrow">▸</span>
|
||||||
|
</div>
|
||||||
|
<div class="hilfe-section-body">
|
||||||
|
<p>
|
||||||
|
Sollte sich der Schlüssel noch in der Box befinden und ihr nicht in einem aktiven Lock sein, besteht die Möglichkeit einen neuen Code für eine Notfallöffnung zu generieren:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ol class="hilfe-steps">
|
||||||
|
<li>
|
||||||
|
<span class="step-num">1</span>
|
||||||
|
<span>Öffne die Einstellungen</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">2</span>
|
||||||
|
<span>Navigiere zum Bereich '🔒 TTLock'</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="step-num">3</span>
|
||||||
|
<span>Drücke '🔒 Öffnen'</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
Der temporäre Code zum Öffnen wird euch angezeigt - damit lässt sich die Box dann öffnen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/icons.js"></script>
|
||||||
|
<script src="/js/sidebar.js"></script>
|
||||||
|
<script>
|
||||||
|
function toggleSection(id) {
|
||||||
|
document.getElementById(id).classList.toggle('open');
|
||||||
|
}
|
||||||
|
function openFromHash() {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
if (!hash) return;
|
||||||
|
const el = document.getElementById(hash);
|
||||||
|
if (el && el.classList.contains('hilfe-section')) {
|
||||||
|
el.classList.add('open');
|
||||||
|
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', openFromHash);
|
||||||
|
window.addEventListener('hashchange', openFromHash);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
xxxthegame/src/main/resources/static/img/banner.png
Normal file
|
After Width: | Height: | Size: 626 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>xXx Games</title>
|
<title>xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>BDSM Game – Info – XXX The Game</title>
|
<title>BDSM Game – Info – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Chastity Game – Info – XXX The Game</title>
|
<title>Chastity Game – Info – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vanilla Game – Info – XXX The Game</title>
|
<title>Vanilla Game – Info – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Lock-Einladung – XXX The Game</title>
|
<title>Lock-Einladung – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ window.ICONS = {
|
|||||||
SETTINGS: { type: 'emoji', value: '⚙️' },
|
SETTINGS: { type: 'emoji', value: '⚙️' },
|
||||||
LOGOUT: { type: 'emoji', value: '⏏️' },
|
LOGOUT: { type: 'emoji', value: '⏏️' },
|
||||||
PROFILE: { type: 'emoji', value: '👤' },
|
PROFILE: { type: 'emoji', value: '👤' },
|
||||||
|
HELP: { type: 'emoji', value: '❓' },
|
||||||
|
|
||||||
// ── Aufgaben / Items ──────────────────────────────────────────────────
|
// ── Aufgaben / Items ──────────────────────────────────────────────────
|
||||||
TOYS: { type: 'emoji', value: '➰' },
|
TOYS: { type: 'emoji', value: '➰' },
|
||||||
|
|||||||
@@ -77,25 +77,36 @@
|
|||||||
const adminCls = path === '/admin.html' ? ' class="active"' : '';
|
const adminCls = path === '/admin.html' ? ' class="active"' : '';
|
||||||
const adminItem = `<li id="navAdminLink" style="display:none"><a href="/admin.html"${adminCls}><span class="icon">${I('ADMIN') || '⚙'}</span> Administration</a></li>`;
|
const adminItem = `<li id="navAdminLink" style="display:none"><a href="/admin.html"${adminCls}><span class="icon">${I('ADMIN') || '⚙'}</span> Administration</a></li>`;
|
||||||
|
|
||||||
|
const footerLinks = [
|
||||||
|
{ href: '/help/kontakt.html', icon: '✉️', label: 'Kontakt & Feedback' },
|
||||||
|
{ href: '/help/impressum.html', icon: '📄', label: 'Impressum' },
|
||||||
|
];
|
||||||
|
const footerNav = footerLinks.map(({ href, icon, label }) => {
|
||||||
|
const cls = path === href ? ' class="active"' : '';
|
||||||
|
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}</a></li>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
document.body.insertAdjacentHTML('afterbegin', `
|
document.body.insertAdjacentHTML('afterbegin', `
|
||||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||||
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
||||||
<span class="burger-icon"><span></span><span></span><span></span></span>
|
<span class="burger-icon"><span></span><span></span><span></span></span>
|
||||||
</button>
|
</button>
|
||||||
<aside class="sidebar" id="sidebar">
|
<div class="sidebar-wrapper" id="sidebar">
|
||||||
<div class="sidebar-logo-area">
|
<aside class="sidebar">
|
||||||
<a href="/userhome.html"><img src="/img/logo.png" alt="Logo"></a>
|
<div class="sidebar-scroll-area">
|
||||||
|
<ul>
|
||||||
|
${socialNav}
|
||||||
|
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
||||||
|
${nav}
|
||||||
|
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;" id="navAdminDivider" style="display:none"></li>
|
||||||
|
${adminItem}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<ul>${footerNav}</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
</div>
|
||||||
${homeItem}
|
|
||||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
|
||||||
${socialNav}
|
|
||||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
|
||||||
${nav}
|
|
||||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;" id="navAdminDivider" style="display:none"></li>
|
|
||||||
${adminItem}
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Sidebar und .main in einen zentrierten App-Wrapper verschieben
|
// Sidebar und .main in einen zentrierten App-Wrapper verschieben
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
topbar.className = 'topbar';
|
topbar.className = 'topbar';
|
||||||
topbar.id = 'topbar';
|
topbar.id = 'topbar';
|
||||||
topbar.innerHTML = `
|
topbar.innerHTML = `
|
||||||
<div class="topbar-left"></div>
|
<div class="topbar-left">
|
||||||
|
<a href="/userhome.html"><img class="topbar-banner" src="/img/banner.png" alt="xXx Sphere"></a>
|
||||||
|
</div>
|
||||||
<div class="topbar-search-wrap">
|
<div class="topbar-search-wrap">
|
||||||
<span class="topbar-search-icon">${IC('SEARCH')}</span>
|
<span class="topbar-search-icon">${IC('SEARCH')}</span>
|
||||||
<input type="text" id="topbarSearchInput" placeholder="Suchen…" autocomplete="off" spellcheck="false">
|
<input type="text" id="topbarSearchInput" placeholder="Suchen…" autocomplete="off" spellcheck="false">
|
||||||
@@ -100,6 +102,10 @@
|
|||||||
<a href="/einstellungen.html" class="topbar-profile-link">
|
<a href="/einstellungen.html" class="topbar-profile-link">
|
||||||
<span>${IC('SETTINGS')}</span> Einstellungen
|
<span>${IC('SETTINGS')}</span> Einstellungen
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/help/overview.html" class="topbar-profile-link">
|
||||||
|
<span>${IC('HELP')}</span> Hilfe
|
||||||
|
</a>
|
||||||
|
<hr style="border:none;border-top:1px solid var(--color-secondary);margin:0;">
|
||||||
<a href="/login/logout" class="topbar-profile-link topbar-profile-link--danger">
|
<a href="/login/logout" class="topbar-profile-link topbar-profile-link--danger">
|
||||||
<span>${IC('LOGOUT')}</span> Abmelden
|
<span>${IC('LOGOUT')}</span> Abmelden
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Keyholder*In bestätigt – XXX The Game</title>
|
<title>Keyholder*In bestätigt – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Keyholder – XXX The Game</title>
|
<title>Keyholder – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>xXx Games – Login</title>
|
<title>Login – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Meine Locks – XXX The Game</title>
|
<title>Meine Locks – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -61,7 +61,26 @@
|
|||||||
.form-hint { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
|
.form-hint { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
|
||||||
.form-row input[type="text"],
|
.form-row input[type="text"],
|
||||||
.form-row input[type="number"],
|
.form-row input[type="number"],
|
||||||
.form-row select { width:100%; box-sizing:border-box; }
|
.form-row select {
|
||||||
|
width:100%; box-sizing:border-box;
|
||||||
|
padding:0.65rem 0.9rem;
|
||||||
|
border:1px solid var(--color-secondary);
|
||||||
|
border-radius:6px;
|
||||||
|
background:var(--color-secondary);
|
||||||
|
color:var(--color-text);
|
||||||
|
font-size:1rem;
|
||||||
|
outline:none;
|
||||||
|
appearance:none;
|
||||||
|
-webkit-appearance:none;
|
||||||
|
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||||
|
background-repeat:no-repeat;
|
||||||
|
background-position:right 0.9rem center;
|
||||||
|
padding-right:2.2rem;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:border-color 0.15s;
|
||||||
|
}
|
||||||
|
.form-row select:focus { border-color:var(--color-primary); }
|
||||||
|
.form-row select option { background:var(--color-card); }
|
||||||
.checkbox-row {
|
.checkbox-row {
|
||||||
display:flex; align-items:center; gap:0.6rem;
|
display:flex; align-items:center; gap:0.6rem;
|
||||||
margin-bottom:0.6rem; cursor:pointer;
|
margin-bottom:0.6rem; cursor:pointer;
|
||||||
@@ -157,8 +176,19 @@
|
|||||||
background:var(--color-card); border-radius:7px; padding:0.55rem 0.75rem;
|
background:var(--color-card); border-radius:7px; padding:0.55rem 0.75rem;
|
||||||
flex-wrap:wrap;
|
flex-wrap:wrap;
|
||||||
}
|
}
|
||||||
.wheel-item select { flex:1; min-width:150px; box-sizing:border-box; }
|
.wheel-item select {
|
||||||
.wheel-item input[type="number"] { width:80px; box-sizing:border-box; }
|
flex:1; min-width:150px; box-sizing:border-box;
|
||||||
|
padding:0.65rem 0.9rem; padding-right:2.2rem;
|
||||||
|
border:1px solid var(--color-secondary); border-radius:6px;
|
||||||
|
background:var(--color-secondary); color:var(--color-text);
|
||||||
|
font-size:1rem; outline:none;
|
||||||
|
appearance:none; -webkit-appearance:none;
|
||||||
|
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||||
|
background-repeat:no-repeat; background-position:right 0.9rem center;
|
||||||
|
cursor:pointer; transition:border-color 0.15s;
|
||||||
|
}
|
||||||
|
.wheel-item select:focus { border-color:var(--color-primary); }
|
||||||
|
.wheel-item select option { background:var(--color-card); }
|
||||||
.wheel-item input[type="text"] { flex:1; min-width:120px; box-sizing:border-box; }
|
.wheel-item input[type="text"] { flex:1; min-width:120px; box-sizing:border-box; }
|
||||||
|
|
||||||
.btn-remove { background:none; border:none; color:rgba(200,50,50,0.7); cursor:pointer; font-size:0.95rem; padding:0; margin:0; width:auto; }
|
.btn-remove { background:none; border:none; color:rgba(200,50,50,0.7); cursor:pointer; font-size:0.95rem; padding:0; margin:0; width:auto; }
|
||||||
@@ -290,6 +320,20 @@
|
|||||||
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Aufgaben (CardLock) -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-section-title">Aufgaben (optional)</div>
|
||||||
|
<div id="sectionCardTaskMode" style="display:none;margin-bottom:0.65rem;">
|
||||||
|
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
||||||
|
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||||
|
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-list" id="modalCardTaskList"></div>
|
||||||
|
<button class="btn-add" onclick="addTask()">+ Aufgabe hinzufügen</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ══ Zeit-Lock Bereich ══ -->
|
<!-- ══ Zeit-Lock Bereich ══ -->
|
||||||
@@ -324,24 +368,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Spinning Wheel -->
|
<!-- Glücksrad -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-section-title">Spinning Wheel (optional)</div>
|
<div class="form-section-title">Glücksrad (optional)</div>
|
||||||
<div class="wheel-list" id="wheelList"></div>
|
<div class="checkbox-row">
|
||||||
<button class="btn-add" onclick="addWheelEntry()">+ Eintrag hinzufügen</button>
|
<input type="checkbox" id="fSpinToggle" onchange="toggleWheel(this.checked)">
|
||||||
<div class="form-row" id="rowSpinsEvery" style="margin-top:0.75rem;display:none;">
|
<label for="fSpinToggle">Glücksrad aktivieren</label>
|
||||||
<label>Rad drehen alle</label>
|
</div>
|
||||||
<div class="time-picker">
|
<div id="wheelFields" style="display:none;">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'d')">−</button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
<div class="form-row" style="margin-top:0.5rem;">
|
||||||
<div class="tp-colon">:</div>
|
<label>Rad drehen alle</label>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')">−</button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
<div class="time-picker">
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'d')">−</button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')">−</button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
<div class="tp-colon">:</div>
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')">−</button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
||||||
|
<div class="tp-colon">:</div>
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')">−</button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-top:0.5rem;margin-bottom:0;">
|
<div class="form-row">
|
||||||
<label>Mindestdrehungen pro Tag (optional)</label>
|
<label>Mindestdrehungen pro Tag (optional)</label>
|
||||||
<div class="inline-row"><input type="number" id="fMinSpins" min="1" placeholder="–"> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
|
<div class="inline-row"><input type="number" id="fMinSpins" min="1" placeholder="–"> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:0.5rem;">
|
||||||
|
<div class="wheel-list" id="wheelList"></div>
|
||||||
|
<button class="btn-add" onclick="addWheelEntry()">+ Eintrag hinzufügen</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -354,7 +406,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="taskTimingFields" style="display:none;">
|
<div id="taskTimingFields" style="display:none;">
|
||||||
<div class="form-row" style="margin-top:0.5rem;">
|
<div class="form-row" style="margin-top:0.5rem;">
|
||||||
<label>Aufgabe alle</label>
|
<label>Aufgaben alle</label>
|
||||||
<div class="time-picker">
|
<div class="time-picker">
|
||||||
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'d')">−</button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'d')">−</button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
<div class="tp-colon">:</div>
|
<div class="tp-colon">:</div>
|
||||||
@@ -367,6 +419,19 @@
|
|||||||
<label>Mindestaufgaben pro Tag (optional)</label>
|
<label>Mindestaufgaben pro Tag (optional)</label>
|
||||||
<div class="inline-row"><input type="number" id="fMinTasks" min="1" placeholder="–"> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
|
<div class="inline-row"><input type="number" id="fMinTasks" min="1" placeholder="–"> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:0.85rem;">
|
||||||
|
<div id="sectionTaskMode" style="margin-bottom:0.65rem;">
|
||||||
|
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgaben?</div>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="modalTaskMode" value="RANDOM" checked> Zufall</label>
|
||||||
|
<label><input type="radio" name="modalTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||||
|
<label><input type="radio" name="modalTaskMode" value="COMMUNITY" > Community</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.4rem;">Aufgaben <span style="color:#e74c3c;font-size:0.85em;">*</span></div>
|
||||||
|
<div class="task-list" id="modalTaskList"></div>
|
||||||
|
<button class="btn-add" onclick="addTask()">+ Aufgabe hinzufügen</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -383,8 +448,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" id="rowPenaltyValue" style="display:none;">
|
<div class="form-row" id="rowPenaltyValue" style="display:none;">
|
||||||
<label id="lblPenaltyValue">Dauer (Minuten)</label>
|
<label>Dauer</label>
|
||||||
<div class="inline-row"><input type="number" id="fPenaltyValue" min="1" placeholder="Minuten"></div>
|
<div class="time-picker">
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'d')">−</button><input type="text" id="pv_d" value="0" readonly><button type="button" onclick="tpChange('pv',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
|
<div class="tp-colon">:</div>
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'h')">−</button><input type="text" id="pv_h" value="01" readonly><button type="button" onclick="tpChange('pv',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
||||||
|
<div class="tp-colon">:</div>
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'m')">−</button><input type="text" id="pv_m" value="00" readonly><button type="button" onclick="tpChange('pv',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,23 +491,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aufgaben-Modus (wenn Aufgaben vorhanden) -->
|
|
||||||
<div id="sectionTaskMode" style="display:none;" class="form-section">
|
|
||||||
<div class="form-section-title">Wer entscheidet über die Aufgabe?</div>
|
|
||||||
<div class="radio-group">
|
|
||||||
<label><input type="radio" name="modalTaskMode" value="RANDOM" checked> Zufall</label>
|
|
||||||
<label><input type="radio" name="modalTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
|
||||||
<label><input type="radio" name="modalTaskMode" value="COMMUNITY" > Community</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Aufgaben (immer, optional) -->
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="form-section-title">Aufgaben (optional)</div>
|
|
||||||
<div class="task-list" id="modalTaskList"></div>
|
|
||||||
<button class="btn-add" onclick="addTask()">+ Aufgabe hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="error-msg" id="modalError"></div>
|
<div class="error-msg" id="modalError"></div>
|
||||||
|
|
||||||
<div id="modalDiscardConfirm" style="display:none;background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.3);border-radius:8px;padding:0.6rem 0.9rem;margin-bottom:0.5rem;display:none;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap;">
|
<div id="modalDiscardConfirm" style="display:none;background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.3);border-radius:8px;padding:0.6rem 0.9rem;margin-bottom:0.5rem;display:none;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap;">
|
||||||
@@ -600,22 +654,28 @@
|
|||||||
function addWheelEntry(data) {
|
function addWheelEntry(data) {
|
||||||
const id = ++wheelCtr;
|
const id = ++wheelCtr;
|
||||||
const type = data?.type || 'ADD_TIME';
|
const type = data?.type || 'ADD_TIME';
|
||||||
const def = WHEEL_TYPES.find(t => t.value === type) || WHEEL_TYPES[0];
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'wheel-item';
|
div.className = 'wheel-item';
|
||||||
div.id = 'we-' + id;
|
div.id = 'we-' + id;
|
||||||
div.innerHTML = buildWheelItemHtml(id, type, data?.intVal, data?.stringVal);
|
div.innerHTML = buildWheelItemHtml(id, type, data?.intVal, data?.stringVal);
|
||||||
document.getElementById('wheelList').appendChild(div);
|
document.getElementById('wheelList').appendChild(div);
|
||||||
|
tpFromMinutes('wt' + id, data?.intVal || 60);
|
||||||
updateWheelFields(id);
|
updateWheelFields(id);
|
||||||
updateWheelTimingVisibility();
|
|
||||||
}
|
}
|
||||||
function buildWheelItemHtml(id, type, intVal, stringVal) {
|
function buildWheelItemHtml(id, type, intVal, stringVal) {
|
||||||
const opts = WHEEL_TYPES.map(t =>
|
const opts = WHEEL_TYPES.map(t =>
|
||||||
`<option value="${t.value}" ${t.value===type?'selected':''}>${esc(t.label)}</option>`
|
`<option value="${t.value}" ${t.value===type?'selected':''}>${esc(t.label)}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
return `<select onchange="updateWheelFields(${id})">${opts}</select>
|
return `<select onchange="updateWheelFields(${id})">${opts}</select>
|
||||||
<input type="number" id="we-int-${id}" placeholder="Min." min="1"
|
<div id="we-tp-${id}" style="display:none;">
|
||||||
value="${intVal||''}" style="display:none">
|
<div class="time-picker">
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'d')">−</button><input type="text" id="wt${id}_d" value="0" readonly><button type="button" onclick="tpChange('wt${id}',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
|
||||||
|
<div class="tp-colon">:</div>
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'h')">−</button><input type="text" id="wt${id}_h" value="01" readonly><button type="button" onclick="tpChange('wt${id}',1,'h')">+</button></div><span class="tp-label">Std</span></div>
|
||||||
|
<div class="tp-colon">:</div>
|
||||||
|
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'m')">−</button><input type="text" id="wt${id}_m" value="00" readonly><button type="button" onclick="tpChange('wt${id}',1,'m')">+</button></div><span class="tp-label">Min</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
|
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
|
||||||
value="${esc(stringVal||'')}" style="display:none">
|
value="${esc(stringVal||'')}" style="display:none">
|
||||||
<button class="btn-remove" onclick="removeWheelEntry(${id})" title="Entfernen">✕</button>`;
|
<button class="btn-remove" onclick="removeWheelEntry(${id})" title="Entfernen">✕</button>`;
|
||||||
@@ -624,27 +684,32 @@
|
|||||||
const sel = document.querySelector(`#we-${id} select`);
|
const sel = document.querySelector(`#we-${id} select`);
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
const def = WHEEL_TYPES.find(t => t.value === sel.value) || WHEEL_TYPES[0];
|
const def = WHEEL_TYPES.find(t => t.value === sel.value) || WHEEL_TYPES[0];
|
||||||
document.getElementById('we-int-' + id).style.display = def.hasInt ? '' : 'none';
|
document.getElementById('we-tp-' + id).style.display = def.hasInt ? '' : 'none';
|
||||||
document.getElementById('we-str-' + id).style.display = def.hasStr ? '' : 'none';
|
document.getElementById('we-str-' + id).style.display = def.hasStr ? '' : 'none';
|
||||||
if (def.intLabel) document.getElementById('we-int-' + id).placeholder = def.intLabel;
|
|
||||||
if (def.strLabel) document.getElementById('we-str-' + id).placeholder = def.strLabel;
|
|
||||||
}
|
}
|
||||||
function removeWheelEntry(id) {
|
function removeWheelEntry(id) {
|
||||||
document.getElementById('we-' + id)?.remove();
|
document.getElementById('we-' + id)?.remove();
|
||||||
updateWheelTimingVisibility();
|
|
||||||
}
|
}
|
||||||
function updateWheelTimingVisibility() {
|
function toggleWheel(on) {
|
||||||
const hasEntries = document.querySelectorAll('.wheel-item').length > 0;
|
document.getElementById('wheelFields').style.display = on ? '' : 'none';
|
||||||
document.getElementById('rowSpinsEvery').style.display = hasEntries ? '' : 'none';
|
if (!on) {
|
||||||
|
tpFromMinutes('se', 60);
|
||||||
|
document.getElementById('fMinSpins').value = '';
|
||||||
|
document.getElementById('wheelList').innerHTML = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function collectWheelEntries() {
|
function collectWheelEntries() {
|
||||||
return Array.from(document.querySelectorAll('.wheel-item')).map(item => {
|
return Array.from(document.querySelectorAll('#wheelList .wheel-item')).map(item => {
|
||||||
const id = item.id.replace('we-','');
|
const id = item.id.replace('we-','');
|
||||||
const sel = item.querySelector('select');
|
const sel = item.querySelector('select');
|
||||||
const def = WHEEL_TYPES.find(t => t.value === sel?.value);
|
const def = WHEEL_TYPES.find(t => t.value === sel?.value);
|
||||||
|
const d = parseInt(document.getElementById('wt' + id + '_d')?.value) || 0;
|
||||||
|
const h = parseInt(document.getElementById('wt' + id + '_h')?.value) || 0;
|
||||||
|
const m = parseInt(document.getElementById('wt' + id + '_m')?.value) || 0;
|
||||||
|
const minutes = d * 1440 + h * 60 + m;
|
||||||
return {
|
return {
|
||||||
type: sel?.value,
|
type: sel?.value,
|
||||||
intVal: def?.hasInt ? (parseInt(document.getElementById('we-int-'+id)?.value)||null) : null,
|
intVal: def?.hasInt ? (minutes > 0 ? minutes : null) : null,
|
||||||
stringVal: def?.hasStr ? (document.getElementById('we-str-'+id)?.value||null) : null,
|
stringVal: def?.hasStr ? (document.getElementById('we-str-'+id)?.value||null) : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -653,7 +718,11 @@
|
|||||||
// ── Aufgaben-Timing (TimeLock) ──
|
// ── Aufgaben-Timing (TimeLock) ──
|
||||||
function toggleTaskTiming(on) {
|
function toggleTaskTiming(on) {
|
||||||
document.getElementById('taskTimingFields').style.display = on ? '' : 'none';
|
document.getElementById('taskTimingFields').style.display = on ? '' : 'none';
|
||||||
if (!on) { tpFromMinutes('te', 480); document.getElementById('fMinTasks').value = ''; }
|
if (!on) {
|
||||||
|
tpFromMinutes('te', 480);
|
||||||
|
document.getElementById('fMinTasks').value = '';
|
||||||
|
document.getElementById('modalTaskList').innerHTML = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Strafmaß ──
|
// ── Strafmaß ──
|
||||||
@@ -661,8 +730,6 @@
|
|||||||
const type = document.getElementById('fPenaltyType').value;
|
const type = document.getElementById('fPenaltyType').value;
|
||||||
const needsVal = type === 'ADD' || type === 'FREEZE';
|
const needsVal = type === 'ADD' || type === 'FREEZE';
|
||||||
document.getElementById('rowPenaltyValue').style.display = needsVal ? '' : 'none';
|
document.getElementById('rowPenaltyValue').style.display = needsVal ? '' : 'none';
|
||||||
document.getElementById('lblPenaltyValue').textContent =
|
|
||||||
type === 'ADD' ? 'Minuten hinzufügen' : 'Einfrieren für (Minuten)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Aufgaben ──
|
// ── Aufgaben ──
|
||||||
@@ -682,16 +749,18 @@
|
|||||||
<button class="btn-remove" onclick="removeTask(${id})" title="Entfernen">✕</button>
|
<button class="btn-remove" onclick="removeTask(${id})" title="Entfernen">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea placeholder="Beschreibung (optional)…" maxlength="600" id="mt-desc-${id}">${descVal}</textarea>`;
|
<textarea placeholder="Beschreibung (optional)…" maxlength="600" id="mt-desc-${id}">${descVal}</textarea>`;
|
||||||
document.getElementById('modalTaskList').appendChild(div);
|
const containerId = currentModalType() === 'CARDLOCK' ? 'modalCardTaskList' : 'modalTaskList';
|
||||||
|
document.getElementById(containerId).appendChild(div);
|
||||||
updateTaskModeVisibility();
|
updateTaskModeVisibility();
|
||||||
}
|
}
|
||||||
function removeTask(id) { document.getElementById('mt-'+id)?.remove(); updateTaskModeVisibility(); }
|
function removeTask(id) { document.getElementById('mt-'+id)?.remove(); updateTaskModeVisibility(); }
|
||||||
function updateTaskModeVisibility() {
|
function updateTaskModeVisibility() {
|
||||||
const hasTasks = document.querySelectorAll('.task-item').length > 0;
|
|
||||||
const type = currentModalType();
|
const type = currentModalType();
|
||||||
// For CardLock: show mode only when task cards > 0 OR tasks exist
|
if (type === 'CARDLOCK') {
|
||||||
// For TimeLock: show mode when tasks exist OR timing is active
|
const hasCardTasks = document.querySelectorAll('#modalCardTaskList .task-item').length > 0;
|
||||||
document.getElementById('sectionTaskMode').style.display = hasTasks ? '' : 'none';
|
document.getElementById('sectionCardTaskMode').style.display = hasCardTasks ? '' : 'none';
|
||||||
|
}
|
||||||
|
// For TimeLock: sectionTaskMode is always visible when taskTimingFields is open
|
||||||
}
|
}
|
||||||
function collectTasks() {
|
function collectTasks() {
|
||||||
return Array.from(document.querySelectorAll('.task-item')).map(item => {
|
return Array.from(document.querySelectorAll('.task-item')).map(item => {
|
||||||
@@ -735,7 +804,8 @@
|
|||||||
document.getElementById('modalError').style.display = 'none';
|
document.getElementById('modalError').style.display = 'none';
|
||||||
document.getElementById('modalSaveBtn').disabled = false;
|
document.getElementById('modalSaveBtn').disabled = false;
|
||||||
document.getElementById('modalTaskList').innerHTML = '';
|
document.getElementById('modalTaskList').innerHTML = '';
|
||||||
document.getElementById('wheelList').innerHTML = '';
|
document.getElementById('fSpinToggle').checked = false;
|
||||||
|
toggleWheel(false);
|
||||||
document.getElementById('errGreen').style.display = 'none';
|
document.getElementById('errGreen').style.display = 'none';
|
||||||
taskCtr = 0; wheelCtr = 0;
|
taskCtr = 0; wheelCtr = 0;
|
||||||
|
|
||||||
@@ -768,11 +838,15 @@
|
|||||||
tpFromMinutes('tmax', template?.maxTimeInMinutes || 60);
|
tpFromMinutes('tmax', template?.maxTimeInMinutes || 60);
|
||||||
document.getElementById('fEndTimeVisible').checked = template?.endTimeVisible || false;
|
document.getElementById('fEndTimeVisible').checked = template?.endTimeVisible || false;
|
||||||
|
|
||||||
// Spinning Wheel
|
// Glücksrad
|
||||||
(template?.spinningWheelEntries || []).forEach(e => addWheelEntry(e));
|
const hasWheelEntries = !!(template?.spinningWheelEntries?.length);
|
||||||
updateWheelTimingVisibility();
|
document.getElementById('fSpinToggle').checked = hasWheelEntries;
|
||||||
if (template?.spinsEveryMinutes) tpFromMinutes('se', template.spinsEveryMinutes);
|
toggleWheel(hasWheelEntries);
|
||||||
document.getElementById('fMinSpins').value = template?.minSpinsPerDay || '';
|
if (hasWheelEntries) {
|
||||||
|
template.spinningWheelEntries.forEach(e => addWheelEntry(e));
|
||||||
|
if (template.spinsEveryMinutes) tpFromMinutes('se', template.spinsEveryMinutes);
|
||||||
|
document.getElementById('fMinSpins').value = template.minSpinsPerDay || '';
|
||||||
|
}
|
||||||
|
|
||||||
// Aufgaben-Timing
|
// Aufgaben-Timing
|
||||||
const hasTaskTiming = !!template?.taskEveryMinutes;
|
const hasTaskTiming = !!template?.taskEveryMinutes;
|
||||||
@@ -785,7 +859,7 @@
|
|||||||
|
|
||||||
// Strafmaß
|
// Strafmaß
|
||||||
document.getElementById('fPenaltyType').value = template?.penaltyType || '';
|
document.getElementById('fPenaltyType').value = template?.penaltyType || '';
|
||||||
document.getElementById('fPenaltyValue').value = template?.penaltyValue || '';
|
tpFromMinutes('pv', template?.penaltyValue || 60);
|
||||||
onPenaltyTypeChange();
|
onPenaltyTypeChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,7 +872,8 @@
|
|||||||
// Aufgaben
|
// Aufgaben
|
||||||
(template?.tasks||[]).forEach(t => addTask(t));
|
(template?.tasks||[]).forEach(t => addTask(t));
|
||||||
const mode = template?.taskMode || template?.taskCardMode || 'RANDOM';
|
const mode = template?.taskMode || template?.taskCardMode || 'RANDOM';
|
||||||
const radioEl = document.querySelector(`input[name="modalTaskMode"][value="${mode}"]`);
|
const radioName = type === 'CARDLOCK' ? 'modalCardTaskMode' : 'modalTaskMode';
|
||||||
|
const radioEl = document.querySelector(`input[name="${radioName}"][value="${mode}"]`);
|
||||||
if (radioEl) radioEl.checked = true;
|
if (radioEl) radioEl.checked = true;
|
||||||
updateTaskModeVisibility();
|
updateTaskModeVisibility();
|
||||||
|
|
||||||
@@ -853,6 +928,7 @@
|
|||||||
|
|
||||||
// ── Speichern ──
|
// ── Speichern ──
|
||||||
async function saveTemplate() {
|
async function saveTemplate() {
|
||||||
|
try {
|
||||||
document.getElementById('modalError').style.display = 'none';
|
document.getElementById('modalError').style.display = 'none';
|
||||||
clearErr('rowName');
|
clearErr('rowName');
|
||||||
const type = currentModalType();
|
const type = currentModalType();
|
||||||
@@ -915,7 +991,7 @@
|
|||||||
hygineOpeningEveryMinites: hygieneEvery,
|
hygineOpeningEveryMinites: hygieneEvery,
|
||||||
hygineOpeningDurationMinutes: hygieneDur,
|
hygineOpeningDurationMinutes: hygieneDur,
|
||||||
tasks, requiresVerification: document.getElementById('fRequiresVerification').checked,
|
tasks, requiresVerification: document.getElementById('fRequiresVerification').checked,
|
||||||
taskMode: document.querySelector('input[name="modalTaskMode"]:checked')?.value||'RANDOM',
|
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// TimeLock
|
// TimeLock
|
||||||
@@ -937,14 +1013,14 @@
|
|||||||
let spinsEvery = null, minSpinsPerDay = null;
|
let spinsEvery = null, minSpinsPerDay = null;
|
||||||
if (wheelEntries.length > 0) {
|
if (wheelEntries.length > 0) {
|
||||||
spinsEvery = tpToMinutes('se');
|
spinsEvery = tpToMinutes('se');
|
||||||
if (spinsEvery < 1) { showModalError('Spinning-Wheel-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); }
|
if (spinsEvery < 1) { showModalError('Glücksrad-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); }
|
||||||
const ms = parseInt(document.getElementById('fMinSpins').value);
|
const ms = parseInt(document.getElementById('fMinSpins').value);
|
||||||
minSpinsPerDay = isNaN(ms)||ms<1 ? null : ms;
|
minSpinsPerDay = isNaN(ms)||ms<1 ? null : ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validierung: Aufgaben-Timing + TASK-Wheel-Eintrag schließen sich aus
|
// Validierung: Aufgaben-Timing + TASK-Wheel-Eintrag schließen sich aus
|
||||||
if (hasTaskTiming && wheelEntries.some(e => e.type === 'TASK')) {
|
if (hasTaskTiming && wheelEntries.some(e => e.type === 'TASK')) {
|
||||||
showModalError('Aufgaben-Timing kann nicht mit TASK-Einträgen im Spinning-Wheel kombiniert werden.');
|
showModalError('Aufgaben-Timing kann nicht mit TASK-Einträgen im Glücksrad kombiniert werden.');
|
||||||
firstError = firstError || document.getElementById('modalError');
|
firstError = firstError || document.getElementById('modalError');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,17 +1041,17 @@
|
|||||||
if (spinsEvery && minSpinsPerDay) {
|
if (spinsEvery && minSpinsPerDay) {
|
||||||
const minSpinTime = spinsEvery * minSpinsPerDay;
|
const minSpinTime = spinsEvery * minSpinsPerDay;
|
||||||
if (minSpinTime > 24 * 60) {
|
if (minSpinTime > 24 * 60) {
|
||||||
showModalError('Spinning-Wheel-Konfiguration erfordert mehr als 24 Stunden pro Tag – bitte Intervall oder Min.-Anzahl reduzieren.');
|
showModalError('Glücksrad-Konfiguration erfordert mehr als 24 Stunden pro Tag – bitte Intervall oder Min.-Anzahl reduzieren.');
|
||||||
firstError = firstError || document.getElementById('modalError');
|
firstError = firstError || document.getElementById('modalError');
|
||||||
} else if (minSpinTime > 12 * 60) {
|
} else if (minSpinTime > 12 * 60) {
|
||||||
showModalError('⚠ Warnung: Spinning-Wheel-Konfiguration erfordert mehr als 12 Stunden pro Tag.');
|
showModalError('⚠ Warnung: Glücksrad-Konfiguration erfordert mehr als 12 Stunden pro Tag.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const penaltyType = document.getElementById('fPenaltyType').value || null;
|
const penaltyType = document.getElementById('fPenaltyType').value || null;
|
||||||
const penaltyRaw = parseInt(document.getElementById('fPenaltyValue').value);
|
const penaltyMinutes = tpToMinutes('pv');
|
||||||
const penaltyValue = penaltyType && (penaltyType==='ADD'||penaltyType==='FREEZE')
|
const penaltyValue = penaltyType && (penaltyType==='ADD'||penaltyType==='FREEZE')
|
||||||
? (isNaN(penaltyRaw)||penaltyRaw<1 ? null : penaltyRaw) : null;
|
? (penaltyMinutes < 1 ? null : penaltyMinutes) : null;
|
||||||
|
|
||||||
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
||||||
|
|
||||||
@@ -1003,6 +1079,11 @@
|
|||||||
if (res.ok) { closeModal(); resetList(); }
|
if (res.ok) { closeModal(); resetList(); }
|
||||||
else { showModalError('Fehler beim Speichern.'); btn.disabled=false; }
|
else { showModalError('Fehler beim Speichern.'); btn.disabled=false; }
|
||||||
} catch(e) { btn.disabled=false; }
|
} catch(e) { btn.disabled=false; }
|
||||||
|
} catch(e) {
|
||||||
|
console.error('saveTemplate exception:', e);
|
||||||
|
showModalError('Interner Fehler: ' + e.message);
|
||||||
|
document.getElementById('modalSaveBtn').disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Löschen ──
|
// ── Löschen ──
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Nachrichten – XXX The Game</title>
|
<title>Nachrichten – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -318,10 +318,12 @@
|
|||||||
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen">✕</button>
|
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/sidebar.js"></script>
|
<script src="/js/sidebar.js"></script>
|
||||||
<script src="/js/social-sidebar.js"></script>
|
<script src="/js/social-sidebar.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
const SUPPORT_USER_ID = 'dbf1e35a-e331-3211-9889-d0d21f386028';
|
||||||
|
|
||||||
let myId = null;
|
let myId = null;
|
||||||
let activePartnerId = null;
|
let activePartnerId = null;
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
@@ -411,8 +413,9 @@
|
|||||||
avatarEl.style.display = 'none';
|
avatarEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('threadInputWrap').style.display = '';
|
const isSupport = partnerId === SUPPORT_USER_ID;
|
||||||
document.getElementById('msgInput').focus();
|
document.getElementById('threadInputWrap').style.display = isSupport ? 'none' : '';
|
||||||
|
if (!isSupport) document.getElementById('msgInput').focus();
|
||||||
|
|
||||||
if (window.innerWidth <= 768) showThread();
|
if (window.innerWidth <= 768) showThread();
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>BDSM Game – Session einrichten – XXX The Game</title>
|
<title>BDSM Game – Session einrichten – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Neues Lock – XXX The Game</title>
|
<title>Neues Lock – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -304,6 +304,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- TTLock Lade-Overlay -->
|
||||||
|
<div class="modal-overlay" id="ttlLoadingOverlay">
|
||||||
|
<div class="modal-bg"></div>
|
||||||
|
<div class="modal-box" style="max-width:320px;text-align:center;gap:0.75rem;">
|
||||||
|
<div style="font-size:2rem;">⏳</div>
|
||||||
|
<div style="font-weight:600;">TTLock-Kommunikation läuft…</div>
|
||||||
|
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Entsperrcode-Modal -->
|
<!-- Entsperrcode-Modal -->
|
||||||
<div class="modal-overlay" id="unlockModal">
|
<div class="modal-overlay" id="unlockModal">
|
||||||
<div class="modal-bg"></div>
|
<div class="modal-bg"></div>
|
||||||
@@ -342,6 +352,7 @@
|
|||||||
let comboActiveIdx = -1;
|
let comboActiveIdx = -1;
|
||||||
let selectedLockControl = 'UNLOCK_CODE';
|
let selectedLockControl = 'UNLOCK_CODE';
|
||||||
let hasPaidSubscription = false;
|
let hasPaidSubscription = false;
|
||||||
|
let ttlockReady = false;
|
||||||
|
|
||||||
// ── Boot ──
|
// ── Boot ──
|
||||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||||
@@ -349,28 +360,35 @@
|
|||||||
myUserId = user.userId;
|
myUserId = user.userId;
|
||||||
myUserName = user.name;
|
myUserName = user.name;
|
||||||
|
|
||||||
// Subscription + Templates parallel laden
|
// Subscription + Templates + TTLock-Config parallel laden
|
||||||
try {
|
try {
|
||||||
const [cardTpls, timeTpls, subData] = await Promise.all([
|
const [cardTpls, timeTpls, subData, ttlCfg] = await Promise.all([
|
||||||
fetch('/cardlock/templates').then(r => r.ok ? r.json() : []),
|
fetch('/cardlock/templates').then(r => r.ok ? r.json() : []),
|
||||||
fetch('/timelock/templates').then(r => r.ok ? r.json() : []),
|
fetch('/timelock/templates').then(r => r.ok ? r.json() : []),
|
||||||
fetch('/subscription/me').then(r => r.ok ? r.json() : null)
|
fetch('/subscription/me').then(r => r.ok ? r.json() : null),
|
||||||
|
fetch('/user/me/ttlock').then(r => r.ok ? r.json() : null)
|
||||||
]);
|
]);
|
||||||
allTemplates = [
|
allTemplates = [
|
||||||
...cardTpls.map(t => ({ ...t, _type: 'cardlock' })),
|
...cardTpls.map(t => ({ ...t, _type: 'cardlock' })),
|
||||||
...timeTpls.map(t => ({ ...t, _type: 'timelock' }))
|
...timeTpls.map(t => ({ ...t, _type: 'timelock' }))
|
||||||
];
|
];
|
||||||
hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM');
|
hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM');
|
||||||
} catch { allTemplates = []; }
|
ttlockReady = !!(ttlCfg && ttlCfg.testSuccessful);
|
||||||
|
const ttlockTestOk = ttlockReady;
|
||||||
|
|
||||||
if (hasPaidSubscription) {
|
if (hasPaidSubscription && ttlockTestOk) {
|
||||||
const opt = document.getElementById('lcOptTtlock');
|
const opt = document.getElementById('lcOptTtlock');
|
||||||
opt.classList.remove('lc-disabled');
|
opt.classList.remove('lc-disabled');
|
||||||
opt.querySelector('input').disabled = false;
|
opt.querySelector('input').disabled = false;
|
||||||
document.getElementById('lcTtlockBadge').style.display = 'none';
|
document.getElementById('lcTtlockBadge').style.display = 'none';
|
||||||
document.getElementById('lcTtlockDesc').textContent =
|
document.getElementById('lcTtlockDesc').textContent =
|
||||||
'Steuert ein TTLock-Smartschloss direkt über die App-Integration.';
|
'Steuert ein TTLock-Smartschloss direkt über die App-Integration.';
|
||||||
}
|
} else if (hasPaidSubscription && !ttlockTestOk) {
|
||||||
|
document.getElementById('lcTtlockBadge').textContent = 'KONFIG';
|
||||||
|
document.getElementById('lcTtlockDesc').textContent =
|
||||||
|
'TTLock ist noch nicht konfiguriert. Bitte teste die Verbindung zuerst in den Einstellungen.';
|
||||||
|
}
|
||||||
|
} catch { allTemplates = []; }
|
||||||
|
|
||||||
if (allTemplates.length === 0) {
|
if (allTemplates.length === 0) {
|
||||||
document.querySelector('.content').innerHTML = `
|
document.querySelector('.content').innerHTML = `
|
||||||
@@ -583,7 +601,7 @@
|
|||||||
// ── LockControl-Auswahl ──
|
// ── LockControl-Auswahl ──
|
||||||
function selectLockControl(type) {
|
function selectLockControl(type) {
|
||||||
const ids = { UNLOCK_CODE: 'lcOptUnlockCode', TRUST: 'lcOptTrust', TTLOCK: 'lcOptTtlock' };
|
const ids = { UNLOCK_CODE: 'lcOptUnlockCode', TRUST: 'lcOptTrust', TTLOCK: 'lcOptTtlock' };
|
||||||
if (type === 'TTLOCK' && !hasPaidSubscription) return;
|
if (type === 'TTLOCK' && (!hasPaidSubscription || !ttlockReady)) return;
|
||||||
selectedLockControl = type;
|
selectedLockControl = type;
|
||||||
Object.entries(ids).forEach(([t, id]) => {
|
Object.entries(ids).forEach(([t, id]) => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
@@ -746,12 +764,18 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedLockControl === 'TTLOCK') {
|
||||||
|
document.getElementById('ttlLoadingOverlay').classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('ttlLoadingOverlay').classList.remove('open');
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errData = await res.json().catch(() => ({}));
|
const errData = await res.json().catch(() => ({}));
|
||||||
if (res.status === 409 && errData.error === 'active_lock_exists') {
|
if (res.status === 409 && errData.error === 'active_lock_exists') {
|
||||||
@@ -797,8 +821,9 @@
|
|||||||
const btn = document.getElementById('unlockModalBtn');
|
const btn = document.getElementById('unlockModalBtn');
|
||||||
btn.textContent = "🔒 Los geht's";
|
btn.textContent = "🔒 Los geht's";
|
||||||
btn.onclick = async () => {
|
btn.onclick = async () => {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '⏳ Neuer PIN wird gesetzt…';
|
document.getElementById('unlockModal').classList.remove('open');
|
||||||
|
document.getElementById('ttlLoadingOverlay').classList.add('open');
|
||||||
try {
|
try {
|
||||||
await fetch(`/keyholder/${lockType}/${lockId}/relock`, { method: 'POST' });
|
await fetch(`/keyholder/${lockType}/${lockId}/relock`, { method: 'POST' });
|
||||||
} catch { /* Fehler ignorieren – Weiterleitung trotzdem */ }
|
} catch { /* Fehler ignorieren – Weiterleitung trotzdem */ }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Personen suchen – XXX The Game</title>
|
<title>Personen suchen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Profil – XXX The Game</title>
|
<title>Profil – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>xXx Games – Neues Konto erstellen</title>
|
<title>Neues Konto erstellen – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>xXx Games – Neues Passwort</title>
|
<title>Neues Passwort – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vanilla Game – Neue Session – XXX The Game</title>
|
<title>Vanilla Game – Neue Session – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Toys – XXX The Game</title>
|
<title>Toys – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Code-Historie – XXX The Game</title>
|
<title>Code-Historie – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Home – XXX The Game</title>
|
<title>Home – xXx Sphere</title>
|
||||||
<link rel="stylesheet" href="/css/variables.css">
|
<link rel="stylesheet" href="/css/variables.css">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||