weitere änderungen an der Oberfläche, timelock hinzugefügt
This commit is contained in:
@@ -8,6 +8,8 @@ import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -36,13 +39,19 @@ public class AufgabeController {
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final AufgabenGruppeRepository gruppeRepository;
|
||||
private final ToyRepository toyRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final SubscriptionLimitService limitService;
|
||||
|
||||
public AufgabeController(AufgabeRepository aufgabeRepository,
|
||||
AufgabenGruppeRepository gruppeRepository,
|
||||
ToyRepository toyRepository) {
|
||||
ToyRepository toyRepository,
|
||||
UserRepository userRepository,
|
||||
SubscriptionLimitService limitService) {
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.toyRepository = toyRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.limitService = limitService;
|
||||
}
|
||||
|
||||
@GetMapping("/{aufgabeId}")
|
||||
@@ -53,7 +62,7 @@ public class AufgabeController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> create(@RequestBody Aufgabe aufgabe) {
|
||||
public ResponseEntity<Void> create(@RequestBody Aufgabe aufgabe, Principal principal) {
|
||||
if (aufgabe.getKurzText() == null || aufgabe.getText() == null || aufgabe.getLevel() == null || aufgabe.getGruppeId() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
@@ -61,7 +70,10 @@ public class AufgabeController {
|
||||
if (gruppeEntity == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (gruppeEntity.getAufgaben().size() >= 100) {
|
||||
var ownerOpt = userRepository.findByEmail(principal.getName());
|
||||
int limit = ownerOpt.map(u -> limitService.maxTasksPerGroup(u.getUserId()))
|
||||
.orElse(SubscriptionLimitService.STANDARD_MAX_TASKS_PER_GROUP);
|
||||
if (gruppeEntity.getAufgaben().size() >= limit) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
List<ToyEntity> toys = resolveToys(aufgabe.getBenoetigteToys());
|
||||
|
||||
@@ -34,6 +34,7 @@ import de.oaa.xxx.aufgaben.repository.FinisherRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.SperreRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
|
||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@@ -53,6 +54,7 @@ public class AufgabenGruppeController {
|
||||
private final UserRepository userRepository;
|
||||
private final GruppenAboRepository aboRepository;
|
||||
private final AufgabenGruppeService aufgabenGruppeService;
|
||||
private final SubscriptionLimitService limitService;
|
||||
|
||||
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
@@ -61,7 +63,8 @@ public class AufgabenGruppeController {
|
||||
FinisherRepository finisherRepository,
|
||||
UserRepository userRepository,
|
||||
GruppenAboRepository aboRepository,
|
||||
AufgabenGruppeService aufgabenGruppeService) {
|
||||
AufgabenGruppeService aufgabenGruppeService,
|
||||
SubscriptionLimitService limitService) {
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.strafeRepository = strafeRepository;
|
||||
@@ -70,6 +73,7 @@ public class AufgabenGruppeController {
|
||||
this.userRepository = userRepository;
|
||||
this.aboRepository = aboRepository;
|
||||
this.aufgabenGruppeService = aufgabenGruppeService;
|
||||
this.limitService = limitService;
|
||||
}
|
||||
|
||||
// ── Paginierte Listen ──
|
||||
@@ -132,7 +136,7 @@ public class AufgabenGruppeController {
|
||||
UserEntity user = resolveUser(principal);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
|
||||
if (gruppeRepository.countByUserId(user.getUserId()) >= 10) {
|
||||
if (gruppeRepository.countByUserId(user.getUserId()) >= limitService.maxTaskGroups(user.getUserId())) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.aufgaben.entity.ToyEntity;
|
||||
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
|
||||
import de.oaa.xxx.aufgaben.repository.ToyRepository;
|
||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
@@ -46,13 +47,16 @@ public class ToyController {
|
||||
private final ToyRepository toyRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GruppenAboRepository aboRepository;
|
||||
private final SubscriptionLimitService limitService;
|
||||
|
||||
public ToyController(ToyRepository toyRepository,
|
||||
UserRepository userRepository,
|
||||
GruppenAboRepository aboRepository) {
|
||||
GruppenAboRepository aboRepository,
|
||||
SubscriptionLimitService limitService) {
|
||||
this.toyRepository = toyRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.aboRepository = aboRepository;
|
||||
this.limitService = limitService;
|
||||
}
|
||||
|
||||
@GetMapping("/list/user")
|
||||
@@ -142,6 +146,11 @@ public class ToyController {
|
||||
.header("X-Error", "duplicate-name")
|
||||
.build();
|
||||
}
|
||||
if (toyRepository.countByUserId(user.getUserId()) >= limitService.maxToys(user.getUserId())) {
|
||||
return ResponseEntity.status(409)
|
||||
.header("X-Error", "limit-reached")
|
||||
.build();
|
||||
}
|
||||
ToyEntity entity = ToyEntity.create(toy);
|
||||
entity.setUserId(user.getUserId());
|
||||
toyRepository.save(entity);
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface ToyRepository extends JpaRepository<ToyEntity, UUID> {
|
||||
Page<ToyEntity> findByUserIdIsNull(Pageable pageable);
|
||||
|
||||
Page<ToyEntity> findByUserId(UUID userId, Pageable pageable);
|
||||
long countByUserId(UUID userId);
|
||||
|
||||
List<ToyEntity> findByUserId(UUID userId);
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ public class SecurityConfig {
|
||||
.requestMatchers("/einladungen.html").authenticated()
|
||||
.requestMatchers("/joinlock.html").authenticated()
|
||||
.requestMatchers("/benachrichtigungen.html").authenticated()
|
||||
.requestMatchers("/abonnements.html").authenticated()
|
||||
.requestMatchers("/gruppen/**").authenticated()
|
||||
.requestMatchers("/feed/**").authenticated()
|
||||
.requestMatchers("/notifications/**").authenticated()
|
||||
|
||||
@@ -2,10 +2,13 @@ package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateRepository;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -16,11 +19,17 @@ public class CardlockTemplateController {
|
||||
|
||||
private final CardlockTemplateRepository templateRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final TimeLockTemplateRepository timeLockTemplateRepository;
|
||||
private final SubscriptionLimitService limitService;
|
||||
|
||||
public CardlockTemplateController(CardlockTemplateRepository templateRepository,
|
||||
UserRepository userRepository) {
|
||||
UserRepository userRepository,
|
||||
TimeLockTemplateRepository timeLockTemplateRepository,
|
||||
SubscriptionLimitService limitService) {
|
||||
this.templateRepository = templateRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.timeLockTemplateRepository = timeLockTemplateRepository;
|
||||
this.limitService = limitService;
|
||||
}
|
||||
|
||||
record TemplateRequest(
|
||||
@@ -75,6 +84,11 @@ public class CardlockTemplateController {
|
||||
if (req.cardCountsMin() == null || req.cardCountsMin().isEmpty())
|
||||
return ResponseEntity.badRequest().build();
|
||||
|
||||
long totalTemplates = templateRepository.countByOwner(myId)
|
||||
+ timeLockTemplateRepository.countByOwner(myId);
|
||||
if (totalTemplates >= limitService.maxLockTemplates(myId))
|
||||
return ResponseEntity.status(409).header("X-Error", "limit-reached").build();
|
||||
|
||||
CardlockTemplateEntity t = new CardlockTemplateEntity();
|
||||
t.setOwner(myId);
|
||||
applyRequest(t, req);
|
||||
|
||||
@@ -7,4 +7,5 @@ import java.util.UUID;
|
||||
|
||||
public interface CardlockTemplateRepository extends JpaRepository<CardlockTemplateEntity, UUID> {
|
||||
List<CardlockTemplateEntity> findByOwner(UUID owner);
|
||||
long countByOwner(UUID owner);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.timelock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateRepository;
|
||||
import de.oaa.xxx.games.chastity.common.PenaltyType;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -18,11 +20,17 @@ public class TimeLockTemplateController {
|
||||
|
||||
private final TimeLockTemplateRepository templateRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final CardlockTemplateRepository cardlockTemplateRepository;
|
||||
private final SubscriptionLimitService limitService;
|
||||
|
||||
public TimeLockTemplateController(TimeLockTemplateRepository templateRepository,
|
||||
UserRepository userRepository) {
|
||||
UserRepository userRepository,
|
||||
CardlockTemplateRepository cardlockTemplateRepository,
|
||||
SubscriptionLimitService limitService) {
|
||||
this.templateRepository = templateRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.cardlockTemplateRepository = cardlockTemplateRepository;
|
||||
this.limitService = limitService;
|
||||
}
|
||||
|
||||
record TemplateRequest(
|
||||
@@ -85,6 +93,11 @@ public class TimeLockTemplateController {
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
if (req.maxTimeInMinutes() == null || req.maxTimeInMinutes() < 1) return ResponseEntity.badRequest().build();
|
||||
|
||||
long totalTemplates = templateRepository.countByOwner(myId)
|
||||
+ cardlockTemplateRepository.countByOwner(myId);
|
||||
if (totalTemplates >= limitService.maxLockTemplates(myId))
|
||||
return ResponseEntity.status(409).header("X-Error", "limit-reached").build();
|
||||
|
||||
TimeLockTemplateEntity t = new TimeLockTemplateEntity();
|
||||
t.setOwner(myId);
|
||||
applyRequest(t, req);
|
||||
|
||||
@@ -7,4 +7,5 @@ import java.util.UUID;
|
||||
|
||||
public interface TimeLockTemplateRepository extends JpaRepository<TimeLockTemplateEntity, UUID> {
|
||||
List<TimeLockTemplateEntity> findByOwner(UUID owner);
|
||||
long countByOwner(UUID owner);
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ public class SocialController {
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (myId.equals(body.receiverId())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (friendshipRepository.findExisting(myId, body.receiverId()).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.oaa.xxx.subscription;
|
||||
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/subscription")
|
||||
public class SubscriptionController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final SubscriptionLimitService limitService;
|
||||
|
||||
public SubscriptionController(UserRepository userRepository,
|
||||
SubscriptionLimitService limitService) {
|
||||
this.userRepository = userRepository;
|
||||
this.limitService = limitService;
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<Map<String, Object>> getMySubscription(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID userId = meOpt.get().getUserId();
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
limitService.getActiveSubscription(userId).ifPresentOrElse(sub -> {
|
||||
result.put("subscriptionType", sub.getSubscriptionType());
|
||||
result.put("subscribedAt", sub.getSubscribedAt());
|
||||
result.put("validUntil", sub.getValidUntil());
|
||||
result.put("cancellableFrom", sub.getCancellableFrom());
|
||||
}, () -> {
|
||||
result.put("subscriptionType", SubscriptionType.STANDARD);
|
||||
result.put("subscribedAt", null);
|
||||
result.put("validUntil", null);
|
||||
result.put("cancellableFrom", null);
|
||||
});
|
||||
|
||||
result.put("limits", Map.of(
|
||||
"maxLockTemplates", limitService.maxLockTemplates(userId),
|
||||
"maxTaskGroups", limitService.maxTaskGroups(userId),
|
||||
"maxTasksPerGroup", limitService.maxTasksPerGroup(userId),
|
||||
"maxToys", limitService.maxToys(userId)
|
||||
));
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.oaa.xxx.subscription;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Central service for subscription-based feature limits.
|
||||
* All limit constants are defined here – add new resource types as needed.
|
||||
*/
|
||||
@Service
|
||||
public class SubscriptionLimitService {
|
||||
|
||||
// ── Limits for STANDARD (no active subscription) ──
|
||||
public static final int STANDARD_MAX_LOCK_TEMPLATES = 6;
|
||||
public static final int STANDARD_MAX_TASK_GROUPS = 6;
|
||||
public static final int STANDARD_MAX_TASKS_PER_GROUP = 50;
|
||||
public static final int STANDARD_MAX_TOYS = 10;
|
||||
|
||||
private final UserSubscriptionRepository subscriptionRepository;
|
||||
|
||||
public SubscriptionLimitService(UserSubscriptionRepository subscriptionRepository) {
|
||||
this.subscriptionRepository = subscriptionRepository;
|
||||
}
|
||||
|
||||
/** Returns the active subscription for the user, or empty if none. */
|
||||
public Optional<UserSubscriptionEntity> getActiveSubscription(UUID userId) {
|
||||
return subscriptionRepository
|
||||
.findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc(userId, LocalDate.now());
|
||||
}
|
||||
|
||||
/** True if the user has an active (non-STANDARD) subscription. */
|
||||
public boolean hasActivePaidSubscription(UUID userId) {
|
||||
return getActiveSubscription(userId)
|
||||
.filter(s -> s.getSubscriptionType() != SubscriptionType.STANDARD)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/** Max total lock templates (cardlock + timelock combined) allowed for this user. */
|
||||
public int maxLockTemplates(UUID userId) {
|
||||
if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE;
|
||||
return STANDARD_MAX_LOCK_TEMPLATES;
|
||||
}
|
||||
|
||||
/** Max task groups the user may own. */
|
||||
public int maxTaskGroups(UUID userId) {
|
||||
if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE;
|
||||
return STANDARD_MAX_TASK_GROUPS;
|
||||
}
|
||||
|
||||
/** Max tasks per task group. */
|
||||
public int maxTasksPerGroup(UUID userId) {
|
||||
if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE;
|
||||
return STANDARD_MAX_TASKS_PER_GROUP;
|
||||
}
|
||||
|
||||
/** Max individual toys the user may create. */
|
||||
public int maxToys(UUID userId) {
|
||||
if (hasActivePaidSubscription(userId)) return Integer.MAX_VALUE;
|
||||
return STANDARD_MAX_TOYS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.oaa.xxx.subscription;
|
||||
|
||||
public enum SubscriptionType {
|
||||
STANDARD,
|
||||
PREMIUM
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.oaa.xxx.subscription;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "user_subscription")
|
||||
public class UserSubscriptionEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(name = "subscription_id")
|
||||
private UUID subscriptionId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(name = "subscribed_at", nullable = false)
|
||||
private LocalDate subscribedAt;
|
||||
|
||||
@Column(name = "valid_until", nullable = false)
|
||||
private LocalDate validUntil;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "subscription_type", nullable = false, length = 30)
|
||||
private SubscriptionType subscriptionType;
|
||||
|
||||
@Column(name = "cancellable_from")
|
||||
private LocalDate cancellableFrom;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.oaa.xxx.subscription;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UserSubscriptionRepository extends JpaRepository<UserSubscriptionEntity, UUID> {
|
||||
|
||||
Optional<UserSubscriptionEntity> findTopByUserIdAndValidUntilGreaterThanEqualOrderByValidUntilDesc(
|
||||
UUID userId, LocalDate today);
|
||||
}
|
||||
38
xxxthegame/src/main/resources/static/abonnements.html
Normal file
38
xxxthegame/src/main/resources/static/abonnements.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!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>Abonnements – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.page-hint { font-size:0.85rem; color:var(--color-muted); margin:0.25rem 0 1.5rem; }
|
||||
.coming-soon {
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
gap:1rem; padding:3rem 1rem; color:var(--color-muted); text-align:center;
|
||||
}
|
||||
.coming-soon .icon { font-size:3rem; }
|
||||
.coming-soon h2 { font-size:1.2rem; font-weight:600; color:var(--color-text); margin:0; }
|
||||
.coming-soon p { font-size:0.9rem; max-width:360px; margin:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1 style="margin:0 0 0.25rem;">⭐ Abonnements</h1>
|
||||
<p class="page-hint">Übersicht der verfügbaren Abo-Modelle</p>
|
||||
|
||||
<div class="coming-soon">
|
||||
<span class="icon">🚧</span>
|
||||
<h2>Demnächst verfügbar</h2>
|
||||
<p>Hier werden bald die verschiedenen Abo-Modelle beschrieben und abschließbar sein.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -762,7 +762,8 @@
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/card-display.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
|
||||
|
||||
@@ -431,7 +431,8 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -1603,6 +1603,7 @@
|
||||
});
|
||||
|
||||
</script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
border-radius: 14px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.invite-icon { font-size: 2.5rem; margin-bottom: 1rem; }
|
||||
.invite-title { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.5rem; }
|
||||
@@ -47,7 +46,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const einladungId = params.get('id');
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.game-overview { max-width: 700px; }
|
||||
.game-overview { }
|
||||
|
||||
.overview-section { margin-bottom: 2rem; }
|
||||
.overview-section h2 {
|
||||
@@ -331,7 +331,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const game = JSON.parse(sessionStorage.getItem('bdsm-session-game') || 'null');
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>Benachrichtigungen – XXX The Game</title>
|
||||
@@ -12,7 +12,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-width: 680px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notif-item {
|
||||
@@ -108,7 +108,6 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.25rem;
|
||||
max-width: 680px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -130,7 +129,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function fmtRelTime(isoStr) {
|
||||
|
||||
@@ -461,7 +461,8 @@
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/image-viewer.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
// ── State ──
|
||||
@@ -611,8 +612,12 @@
|
||||
|
||||
// Action buttons
|
||||
const actions = document.getElementById('profileActions');
|
||||
const isViewingOwnProfile = me && me.userId === profile.userId;
|
||||
if (isOwnProfile) {
|
||||
actions.innerHTML = `<a href="/profile.html" class="btn">Profil bearbeiten</a>`;
|
||||
} else if (isViewingOwnProfile) {
|
||||
// eigenes Profil im Preview-Modus – keine Aktionsbuttons anzeigen
|
||||
actions.innerHTML = '';
|
||||
} else {
|
||||
let html = '';
|
||||
if (profile.friendStatus === 'FRIEND') {
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function esc(s) {
|
||||
|
||||
@@ -150,24 +150,33 @@ button.secondary:hover {
|
||||
|
||||
/* ── App layout ── */
|
||||
body.app {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
padding: 1.5rem;
|
||||
padding: 1.5rem 1.5rem 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
min-height: calc(100vh - 3rem);
|
||||
padding-bottom: 1.5rem;
|
||||
width: 100%;
|
||||
max-width: calc(240px + 1.5rem + 93.75rem);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
@@ -188,10 +197,8 @@ body.app {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
max-height: calc(100vh - 3rem);
|
||||
align-self: stretch;
|
||||
position: static;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
transition: transform 0.25s ease;
|
||||
@@ -323,12 +330,18 @@ body.app {
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
body.app { padding: 0; }
|
||||
body.app {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@@ -355,6 +368,7 @@ body.app {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
min-height: 100vh;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.burger { display: flex; }
|
||||
@@ -490,3 +504,400 @@ body.app {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
TOP BAR
|
||||
═══════════════════════════════════════════ */
|
||||
.topbar {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: calc(240px + 1.5rem + 93.75rem);
|
||||
margin: 0 auto 1rem;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.55rem 1rem;
|
||||
}
|
||||
|
||||
/* Linker Platzhalter – gleiche Breite wie Sidebar */
|
||||
.topbar-left {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Suche ── */
|
||||
.topbar-search-wrap {
|
||||
flex: 1;
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topbar-search-wrap input {
|
||||
background: var(--color-secondary);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0.46rem 0.9rem 0.46rem 2.2rem;
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.topbar-search-wrap input:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.topbar-search-icon {
|
||||
position: absolute;
|
||||
left: 0.7rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-muted);
|
||||
font-size: 0.85rem;
|
||||
pointer-events: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.topbar-search-overlay {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
z-index: 600;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar-search-overlay.open { display: block; }
|
||||
|
||||
.topbar-search-hint {
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.topbar-search-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.6rem 1rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
transition: background 0.15s;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.topbar-search-result:last-child { border-bottom: none; }
|
||||
.topbar-search-result:hover { background: var(--color-secondary); }
|
||||
|
||||
.topbar-search-avatar {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-search-avatar--placeholder {
|
||||
background: var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* ── Rechter Bereich ── */
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.topbar-btn {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.4rem 0.55rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
transition: background 0.15s;
|
||||
width: auto;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.topbar-btn:hover { background: var(--color-secondary); }
|
||||
|
||||
.topbar-badge {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
border-radius: 9999px;
|
||||
padding: 0.05rem 0.3rem;
|
||||
min-width: 1rem;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.topbar-avatar {
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--color-secondary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.topbar-avatar-placeholder {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.topbar-username {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
max-width: 130px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.topbar-profile-btn { gap: 0.5rem; padding: 0.3rem 0.55rem; }
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
TOPBAR PANELS (Overlays)
|
||||
═══════════════════════════════════════════ */
|
||||
.topbar-panel {
|
||||
position: fixed;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
|
||||
z-index: 550;
|
||||
width: 360px;
|
||||
max-height: 500px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar-panel.open { display: flex; }
|
||||
|
||||
.topbar-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.7rem 1rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.92rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
background: var(--color-card);
|
||||
}
|
||||
|
||||
.topbar-panel-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
width: auto;
|
||||
line-height: 1;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.topbar-panel-close:hover { background: var(--color-secondary); color: var(--color-text); }
|
||||
|
||||
.topbar-panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.topbar-panel-footer {
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
padding: 0.55rem 1rem;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
background: var(--color-card);
|
||||
}
|
||||
|
||||
.topbar-panel-footer a {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.topbar-panel-footer a:hover { text-decoration: underline; }
|
||||
|
||||
.topbar-panel-hint {
|
||||
padding: 0.9rem 1rem;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
/* Einzel-Eintrag in Panel */
|
||||
.topbar-panel-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
transition: background 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topbar-panel-item:last-child { border-bottom: none; }
|
||||
.topbar-panel-item:hover { background: var(--color-secondary); }
|
||||
|
||||
.topbar-item-avatar {
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-item-avatar--placeholder {
|
||||
background: var(--color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.topbar-panel-item-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-panel-item-sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.topbar-item-badge {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
border-radius: 9999px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Benachrichtigungen */
|
||||
.topbar-notif-item--unread { background: rgba(var(--color-primary-rgb, 231,57,84), 0.04); }
|
||||
.topbar-notif-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.25rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.topbar-mark-all-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.topbar-mark-all-btn:hover { background: var(--color-secondary); }
|
||||
|
||||
/* Einladungen */
|
||||
.topbar-inv-card { align-items: center; }
|
||||
|
||||
.topbar-inv-btn {
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.78rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.topbar-inv-btn:hover { opacity: 0.85; }
|
||||
.topbar-inv-btn--decline { background: #c0392b; color: #fff; }
|
||||
.topbar-inv-btn--accept { background: var(--color-success, #27ae60); color: #fff; }
|
||||
|
||||
/* Profil-Panel */
|
||||
.topbar-profile-body { display: flex; flex-direction: column; }
|
||||
|
||||
.topbar-profile-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
padding: 1rem 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.topbar-profile-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
.topbar-profile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.6rem 1rem;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.topbar-profile-link:hover { background: var(--color-secondary); }
|
||||
.topbar-profile-link--danger { color: var(--color-primary); }
|
||||
|
||||
/* ── Mobile: Topbar ausblenden ── */
|
||||
@media (max-width: 768px) {
|
||||
.topbar { display: none; }
|
||||
}
|
||||
|
||||
@@ -324,7 +324,8 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||
|
||||
@@ -433,6 +433,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Abonnements -->
|
||||
<div class="settings-section" id="sec-abonnements">
|
||||
<div class="settings-section-header" onclick="toggleSection('sec-abonnements')">
|
||||
<span class="settings-section-title">⭐ Abonnements</span>
|
||||
<span class="settings-section-arrow">▸</span>
|
||||
</div>
|
||||
<div class="settings-section-body">
|
||||
<div class="settings-row">
|
||||
<div class="settings-row-info">
|
||||
<div class="settings-row-label">Aktuelles Abo</div>
|
||||
<div class="settings-row-desc" id="abo-type-desc">Wird geladen…</div>
|
||||
</div>
|
||||
<span id="abo-type-badge" style="font-weight:600;"></span>
|
||||
</div>
|
||||
<div id="abo-details" style="display:none;">
|
||||
<div class="settings-row">
|
||||
<div class="settings-row-info">
|
||||
<div class="settings-row-label">Gültig bis</div>
|
||||
</div>
|
||||
<span id="abo-valid-until" style="font-size:0.9rem;"></span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-row-info">
|
||||
<div class="settings-row-label">Kündbar ab</div>
|
||||
</div>
|
||||
<span id="abo-cancellable-from" style="font-size:0.9rem;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:0.75rem;">
|
||||
<a href="/abonnements.html" style="color:var(--color-primary);font-size:0.9rem;">
|
||||
Abo-Modelle ansehen & Abo abschließen →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datenschutz -->
|
||||
<div class="settings-section" id="sec-datenschutz">
|
||||
<div class="settings-section-header" onclick="toggleSection('sec-datenschutz')">
|
||||
@@ -601,7 +637,8 @@
|
||||
|
||||
<div class="save-toast" id="saveToast">✓ Gespeichert</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let myUserId = null;
|
||||
@@ -958,10 +995,31 @@
|
||||
showToast();
|
||||
}
|
||||
|
||||
// ── Abonnement laden ──
|
||||
async function loadSubscription() {
|
||||
const res = await fetch('/subscription/me');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const type = data.subscriptionType || 'STANDARD';
|
||||
const labels = { STANDARD: 'Standard', PREMIUM: 'Premium' };
|
||||
document.getElementById('abo-type-badge').textContent = labels[type] || type;
|
||||
if (type === 'STANDARD') {
|
||||
document.getElementById('abo-type-desc').textContent =
|
||||
'Kein kostenpflichtiges Abo aktiv · bis zu 6 Lock-Vorlagen, 6 Aufgabengruppen (je 50 Aufgaben), 10 Toys';
|
||||
} else {
|
||||
document.getElementById('abo-type-desc').textContent = 'Aktives Abo';
|
||||
document.getElementById('abo-details').style.display = '';
|
||||
const fmt = d => d ? new Date(d).toLocaleDateString('de-DE') : '–';
|
||||
document.getElementById('abo-valid-until').textContent = fmt(data.validUntil);
|
||||
document.getElementById('abo-cancellable-from').textContent = fmt(data.cancellableFrom);
|
||||
}
|
||||
}
|
||||
|
||||
loadGrunddaten();
|
||||
loadPrivacy();
|
||||
loadNotifications();
|
||||
loadBdsmDefaults();
|
||||
loadSubscription();
|
||||
openSectionFromHash();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -165,7 +165,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 0, totalPages = 1;
|
||||
|
||||
@@ -163,7 +163,8 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
// ── State ──
|
||||
|
||||
@@ -166,7 +166,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function switchTab(name, btn) {
|
||||
|
||||
@@ -295,7 +295,8 @@
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
// ── Generic modal helpers ──
|
||||
|
||||
@@ -136,7 +136,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function switchTab(name, btn) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<p>Informationen zum BDSM Game folgen hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<p>Informationen zum Chastity Game folgen hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<p>Informationen zum Vanilla Game folgen hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem 1.5rem;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.invite-icon { font-size: 3rem; margin-bottom: 1rem; }
|
||||
@@ -164,7 +163,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
48
xxxthegame/src/main/resources/static/js/icons.js
Normal file
48
xxxthegame/src/main/resources/static/js/icons.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Zentrale Icon-Verwaltung – XXX The Game
|
||||
* Alle Emojis und Symbole der App werden hier definiert.
|
||||
* Typen: emoji (Standard-Emoji), symbol (Unicode-Symbol), image (Pfad zu Bilddatei)
|
||||
*/
|
||||
window.ICONS = {
|
||||
// ── Navigation / Sidebar ──
|
||||
HOME: { type: 'emoji', value: '⊞' },
|
||||
VANILLA: { type: 'symbol', value: '♡' },
|
||||
BDSM: { type: 'symbol', value: '◆' },
|
||||
CHASTITY: { type: 'symbol', value: '⊗' },
|
||||
|
||||
// ── Aktionen ──
|
||||
PLAY_NEW: { type: 'symbol', value: '▷' },
|
||||
PLAY_ACTIVE: { type: 'symbol', value: '▶' },
|
||||
ACTIVE_LOCK: { type: 'emoji', value: '▶️' },
|
||||
WAITING: { type: 'emoji', value: '⏳' },
|
||||
CHECK: { type: 'symbol', value: '✓' },
|
||||
DISCOVER: { type: 'symbol', value: '⊙' },
|
||||
ARROW: { type: 'symbol', value: '▸' },
|
||||
|
||||
// ── Chastity Game ──
|
||||
NEW_LOCK: { type: 'emoji', value: '🆕' },
|
||||
LOCK: { type: 'emoji', value: '🔒' },
|
||||
KEY: { type: 'emoji', value: '🔑' },
|
||||
HISTORY: { type: 'emoji', value: '🔙' },
|
||||
VOTES: { type: 'emoji', value: '🗳️' },
|
||||
|
||||
// ── Social ──
|
||||
FEED: { type: 'emoji', value: '📰' },
|
||||
SEARCH: { type: 'emoji', value: '🔍' },
|
||||
FRIENDS: { type: 'emoji', value: '❤️' },
|
||||
MESSAGES: { type: 'emoji', value: '💬' },
|
||||
NOTIFICATIONS: { type: 'emoji', value: '🔔' },
|
||||
GROUPS: { type: 'emoji', value: '👥' },
|
||||
INVITATIONS: { type: 'emoji', value: '✨' },
|
||||
SETTINGS: { type: 'emoji', value: '⚙️' },
|
||||
LOGOUT: { type: 'symbol', value: '⏏' },
|
||||
PROFILE: { type: 'symbol', value: '◉' },
|
||||
|
||||
// ── Aufgaben / Items ──
|
||||
TOYS: { type: 'symbol', value: '◈' },
|
||||
};
|
||||
|
||||
/** Gibt nur den Wert (String) zurück – für einfache Einbindung in Templates */
|
||||
window.IC = function(key) {
|
||||
return (window.ICONS[key] || {}).value || '';
|
||||
};
|
||||
@@ -1,178 +1,198 @@
|
||||
(function () {
|
||||
const path = window.location.pathname;
|
||||
|
||||
const groups = [
|
||||
{
|
||||
label: 'Vanilla Game',
|
||||
icon: '♡',
|
||||
items: [
|
||||
{ href: '/sessionvanilla.html', icon: '▷', label: 'Neue Session' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'BDSM Game',
|
||||
icon: '◆',
|
||||
items: [
|
||||
{ href: '/neubdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: '⏳', label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' },
|
||||
{ href: '/toys.html', icon: '◈', label: 'Toys' },
|
||||
{ href: '/entdecken.html', icon: '⊙', label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Chastity Game',
|
||||
icon: '⊗',
|
||||
items: [
|
||||
{ href: '/neulock.html', icon: '🆕', label: 'Neues Lock', id: 'navChastityNeu' },
|
||||
{ href: '#', icon: '▶️', label: 'Aktives Lock', id: 'navChastityAktiv' },
|
||||
{ href: '/communityvotes.html', icon: '🗳️', label: 'Community Votes' },
|
||||
{ href: '/meine-locks.html', icon: '🔒', label: 'Meine Locks' },
|
||||
{ href: '/keyholder.html', icon: '🔑', label: 'Keyholder' },
|
||||
{ href: '/unlock-history.html', icon: '🔙', label: 'Code-Historie' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
const homeCls = path === '/userhome.html' ? ' class="active"' : '';
|
||||
const homeItem = `
|
||||
<li class="sidebar-mobile-only">
|
||||
<a href="/userhome.html"${homeCls}><span class="icon">⊞</span> Home</a>
|
||||
</li>`;
|
||||
|
||||
const nav = groups.map(({ label, icon, items }) => {
|
||||
const isOpen = items.some(item => item.href === path);
|
||||
const openCls = isOpen ? ' open' : '';
|
||||
const subItems = items.map(({ href, icon: iIcon, label: iLabel, id: iId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const idAt = iId ? ` id="${iId}"` : '';
|
||||
return `<li${idAt}><a href="${href}"${cls}><span class="icon">${iIcon}</span> ${iLabel}</a></li>`;
|
||||
}).join('');
|
||||
return `
|
||||
<li class="sidebar-group${openCls}">
|
||||
<a class="sidebar-group-toggle"><span class="icon">${icon}</span> ${label}<span class="sidebar-arrow">▸</span></a>
|
||||
<ul class="sidebar-sub">
|
||||
${subItems}
|
||||
</ul>
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
document.body.insertAdjacentHTML('afterbegin', `
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
||||
<span class="burger-icon"><span></span><span></span><span></span></span>
|
||||
</button>
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-logo-area">
|
||||
<a href="/userhome.html"><img src="/img/logo.png" alt="Logo"></a>
|
||||
</div>
|
||||
<ul>
|
||||
${homeItem}
|
||||
${nav}
|
||||
</ul>
|
||||
</aside>
|
||||
`);
|
||||
|
||||
// Sidebar und .main in einen zentrierten App-Wrapper verschieben
|
||||
const appWrapper = document.createElement('div');
|
||||
appWrapper.className = 'app-wrapper';
|
||||
const sidebarEl = document.getElementById('sidebar');
|
||||
const mainEl = document.querySelector('.main');
|
||||
document.body.insertBefore(appWrapper, sidebarEl);
|
||||
appWrapper.appendChild(sidebarEl);
|
||||
if (mainEl) appWrapper.appendChild(mainEl);
|
||||
|
||||
// Group toggle
|
||||
document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
toggle.closest('.sidebar-group').classList.toggle('open');
|
||||
});
|
||||
});
|
||||
|
||||
// "Im Spiel" und "Aktive Session" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
|
||||
const navNeu = document.getElementById('navBdsmNeu');
|
||||
const navAktiv = document.getElementById('navBdsmAktiv');
|
||||
const navImSpiel = document.getElementById('navBdsmImSpiel');
|
||||
const navCAktiv = document.getElementById('navChastityAktiv');
|
||||
if (navAktiv) navAktiv.style.display = 'none';
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
if (navCAktiv) navCAktiv.style.display = 'none';
|
||||
|
||||
// Session-Status prüfen
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(async user => {
|
||||
if (!user) return;
|
||||
|
||||
// BDSM Session-Status
|
||||
try {
|
||||
// Zuerst aktive Einladung prüfen (eigenesGeraet-Spieler)
|
||||
const aktivRes = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (aktivRes.ok) {
|
||||
const aktiv = await aktivRes.json();
|
||||
if (navNeu) navNeu.style.display = 'none';
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
if (navAktiv) {
|
||||
navAktiv.style.display = '';
|
||||
const ziel = aktiv.sessionId
|
||||
? '/bdsmingame.html'
|
||||
: `/neubdsm.html`;
|
||||
navAktiv.querySelector('a').href = ziel;
|
||||
}
|
||||
} else {
|
||||
// Dann laufende Host-Session prüfen
|
||||
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
const hasSession = sessionRes.status === 200;
|
||||
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
|
||||
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
|
||||
}
|
||||
} catch (_) { /* Menü bleibt im Standardzustand */ }
|
||||
|
||||
// Chastity Lock-Status
|
||||
try {
|
||||
const lockRes = await fetch('/keyholder/mylock');
|
||||
if (lockRes.ok) {
|
||||
const lockData = await lockRes.json();
|
||||
const lockId = lockData.lockId;
|
||||
if (navCAktiv) {
|
||||
navCAktiv.style.display = '';
|
||||
navCAktiv.querySelector('a').href = '/activelock.html?lockId=' + lockId;
|
||||
}
|
||||
}
|
||||
} catch (_) { /* Menü bleibt im Standardzustand */ }
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const burgerBtn = document.getElementById('burgerBtn');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
function openMenu() {
|
||||
sidebar.classList.add('open');
|
||||
overlay.classList.add('visible');
|
||||
burgerBtn.classList.add('open');
|
||||
burgerBtn.setAttribute('aria-label', 'Menü schließen');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('visible');
|
||||
burgerBtn.classList.remove('open');
|
||||
burgerBtn.setAttribute('aria-label', 'Menü öffnen');
|
||||
}
|
||||
|
||||
burgerBtn.addEventListener('click', () =>
|
||||
sidebar.classList.contains('open') ? closeMenu() : openMenu()
|
||||
);
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
sidebar.querySelectorAll('a:not(.sidebar-group-toggle)').forEach(l =>
|
||||
l.addEventListener('click', () => { if (window.innerWidth <= (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile').trim()) || 768)) closeMenu(); })
|
||||
);
|
||||
|
||||
// Social sidebar auf allen App-Seiten nachladen
|
||||
const s = document.createElement('script');
|
||||
s.src = '/js/social-sidebar.js';
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
(function () {
|
||||
const path = window.location.pathname;
|
||||
const I = window.IC || function() { return ''; };
|
||||
|
||||
const groups = [
|
||||
{
|
||||
label: 'Vanilla Game',
|
||||
icon: I('VANILLA'),
|
||||
items: [
|
||||
{ href: '/sessionvanilla.html', icon: I('PLAY_NEW'), label: 'Neue Session' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'BDSM Game',
|
||||
icon: I('BDSM'),
|
||||
items: [
|
||||
{ href: '/neubdsm.html', icon: I('PLAY_NEW'), label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: I('WAITING'), label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/bdsmingame.html', icon: I('PLAY_ACTIVE'), label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/aufgaben.html', icon: I('CHECK'), label: 'Aufgaben' },
|
||||
{ href: '/toys.html', icon: I('TOYS'), label: 'Toys' },
|
||||
{ href: '/entdecken.html', icon: I('DISCOVER'), label: 'Entdecken' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Chastity Game',
|
||||
icon: I('CHASTITY'),
|
||||
items: [
|
||||
{ href: '/neulock.html', icon: I('NEW_LOCK'), label: 'Neues Lock', id: 'navChastityNeu' },
|
||||
{ href: '#', icon: I('ACTIVE_LOCK'), label: 'Aktives Lock', id: 'navChastityAktiv' },
|
||||
{ href: '/communityvotes.html', icon: I('VOTES'), label: 'Community Votes' },
|
||||
{ href: '/meine-locks.html', icon: I('LOCK'), label: 'Meine Locks' },
|
||||
{ href: '/keyholder.html', icon: I('KEY'), label: 'Keyholder' },
|
||||
{ href: '/unlock-history.html', icon: I('HISTORY'), label: 'Code-Historie' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
const homeCls = path === '/userhome.html' ? ' class="active"' : '';
|
||||
const homeItem = `
|
||||
<li class="sidebar-mobile-only">
|
||||
<a href="/userhome.html"${homeCls}><span class="icon">${I('HOME')}</span> Home</a>
|
||||
</li>`;
|
||||
|
||||
// ── Community-Links (immer sichtbar, oberhalb der Spiele) ──
|
||||
const socialLinks = [
|
||||
{ href: '/feed.html', icon: I('FEED'), label: 'Feed', badgeId: null },
|
||||
{ href: '/freunde.html', icon: I('FRIENDS'), label: 'Freunde', badgeId: 'socialFriendsBadge'},
|
||||
{ href: '/nachrichten.html', icon: I('MESSAGES'), label: 'Nachrichten', badgeId: null },
|
||||
{ href: '/benachrichtigungen.html', icon: I('NOTIFICATIONS'), label: 'Benachrichtigungen', badgeId: null },
|
||||
{ href: '/gruppen.html', icon: I('GROUPS'), label: 'Gruppen', badgeId: 'socialGruppenBadge'},
|
||||
{ href: '/einladungen.html', icon: I('INVITATIONS'), label: 'Einladungen', badgeId: null },
|
||||
];
|
||||
const socialNav = socialLinks.map(({ href, icon, label, badgeId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const badge = badgeId ? `<span class="social-badge" id="${badgeId}" style="display:none;"></span>` : '';
|
||||
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
|
||||
}).join('');
|
||||
|
||||
const nav = groups.map(({ label, icon, items }) => {
|
||||
const isOpen = items.some(item => item.href === path);
|
||||
const openCls = isOpen ? ' open' : '';
|
||||
const subItems = items.map(({ href, icon: iIcon, label: iLabel, id: iId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const idAt = iId ? ` id="${iId}"` : '';
|
||||
return `<li${idAt}><a href="${href}"${cls}><span class="icon">${iIcon}</span> ${iLabel}</a></li>`;
|
||||
}).join('');
|
||||
return `
|
||||
<li class="sidebar-group${openCls}">
|
||||
<a class="sidebar-group-toggle"><span class="icon">${icon}</span> ${label}<span class="sidebar-arrow">${I('ARROW')}</span></a>
|
||||
<ul class="sidebar-sub">
|
||||
${subItems}
|
||||
</ul>
|
||||
</li>`;
|
||||
}).join('');
|
||||
|
||||
document.body.insertAdjacentHTML('afterbegin', `
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
<button class="burger" id="burgerBtn" aria-label="Menü öffnen">
|
||||
<span class="burger-icon"><span></span><span></span><span></span></span>
|
||||
</button>
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-logo-area">
|
||||
<a href="/userhome.html"><img src="/img/logo.png" alt="Logo"></a>
|
||||
</div>
|
||||
<ul>
|
||||
${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}
|
||||
</ul>
|
||||
</aside>
|
||||
`);
|
||||
|
||||
// Sidebar und .main in einen zentrierten App-Wrapper verschieben
|
||||
const appWrapper = document.createElement('div');
|
||||
appWrapper.className = 'app-wrapper';
|
||||
const sidebarEl = document.getElementById('sidebar');
|
||||
const mainEl = document.querySelector('.main');
|
||||
document.body.insertBefore(appWrapper, sidebarEl);
|
||||
appWrapper.appendChild(sidebarEl);
|
||||
if (mainEl) appWrapper.appendChild(mainEl);
|
||||
|
||||
// Group toggle
|
||||
document.querySelectorAll('.sidebar-group-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
toggle.closest('.sidebar-group').classList.toggle('open');
|
||||
});
|
||||
});
|
||||
|
||||
// "Im Spiel" und "Aktive Session" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
|
||||
const navNeu = document.getElementById('navBdsmNeu');
|
||||
const navAktiv = document.getElementById('navBdsmAktiv');
|
||||
const navImSpiel = document.getElementById('navBdsmImSpiel');
|
||||
const navCAktiv = document.getElementById('navChastityAktiv');
|
||||
if (navAktiv) navAktiv.style.display = 'none';
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
if (navCAktiv) navCAktiv.style.display = 'none';
|
||||
|
||||
// Session-Status prüfen
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(async user => {
|
||||
if (!user) return;
|
||||
|
||||
// BDSM Session-Status
|
||||
try {
|
||||
const aktivRes = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (aktivRes.ok) {
|
||||
const aktiv = await aktivRes.json();
|
||||
if (navNeu) navNeu.style.display = 'none';
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
if (navAktiv) {
|
||||
navAktiv.style.display = '';
|
||||
navAktiv.querySelector('a').href = aktiv.sessionId ? '/bdsmingame.html' : '/neubdsm.html';
|
||||
}
|
||||
} else {
|
||||
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
const hasSession = sessionRes.status === 200;
|
||||
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
|
||||
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Chastity Lock-Status
|
||||
try {
|
||||
const lockRes = await fetch('/keyholder/mylock');
|
||||
if (lockRes.ok) {
|
||||
const lockData = await lockRes.json();
|
||||
if (navCAktiv) {
|
||||
navCAktiv.style.display = '';
|
||||
navCAktiv.querySelector('a').href = '/activelock.html?lockId=' + lockData.lockId;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const burgerBtn = document.getElementById('burgerBtn');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
function openMenu() {
|
||||
sidebar.classList.add('open');
|
||||
overlay.classList.add('visible');
|
||||
burgerBtn.classList.add('open');
|
||||
burgerBtn.setAttribute('aria-label', 'Menü schließen');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('visible');
|
||||
burgerBtn.classList.remove('open');
|
||||
burgerBtn.setAttribute('aria-label', 'Menü öffnen');
|
||||
}
|
||||
|
||||
burgerBtn.addEventListener('click', () =>
|
||||
sidebar.classList.contains('open') ? closeMenu() : openMenu()
|
||||
);
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
sidebar.querySelectorAll('a:not(.sidebar-group-toggle)').forEach(l =>
|
||||
l.addEventListener('click', () => {
|
||||
if (window.innerWidth <= (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile').trim()) || 768))
|
||||
closeMenu();
|
||||
})
|
||||
);
|
||||
|
||||
// Topbar und Social-Sidebar nachladen
|
||||
function loadScript(src) {
|
||||
const s = document.createElement('script');
|
||||
s.src = src;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
loadScript('/js/topbar.js');
|
||||
loadScript('/js/social-sidebar.js');
|
||||
})();
|
||||
|
||||
@@ -1,195 +1,94 @@
|
||||
(function () {
|
||||
// Verhindert doppelte Ausführung (z.B. wenn sidebar.js nachladen UND direktes <script>-Tag vorhanden)
|
||||
if (document.querySelector('.social-sidebar')) return;
|
||||
|
||||
const path = window.location.pathname;
|
||||
|
||||
const links = [
|
||||
{ href: '/feed.html', icon: '📰', label: 'Feed', badgeId: null, mobileBadgeId: null },
|
||||
{ href: '/personen-suchen.html', icon: '🔍', label: 'Personen suchen', badgeId: null, mobileBadgeId: null },
|
||||
{ href: '/freunde.html', icon: '❤️', label: 'Freunde', badgeId: 'socialFriendsBadge', mobileBadgeId: 'socialMobileFriendsBadge' },
|
||||
{ href: '/nachrichten.html', icon: '📩', label: 'Nachrichten', badgeId: 'socialMsgBadge', mobileBadgeId: 'socialMobileMsgBadge' },
|
||||
{ href: '/benachrichtigungen.html', icon: '🔔', label: 'Benachrichtigungen', badgeId: 'socialNotifBadge', mobileBadgeId: 'socialMobileNotifBadge' },
|
||||
{ href: '/gruppen.html', icon: '👥', label: 'Gruppen', badgeId: 'socialGruppenBadge', mobileBadgeId: 'socialMobileGruppenBadge' },
|
||||
{ href: '/einladungen.html', icon: '✨', label: 'Einladungen', badgeId: 'socialInvBadge', mobileBadgeId: 'socialMobileInvBadge' },
|
||||
];
|
||||
|
||||
const profileActive = (path === '/benutzer.html' || path === '/profile.html') ? ' class="active"' : '';
|
||||
|
||||
// ── Rechte Desktop-Sidebar (kein Titel) ──
|
||||
const desktopItems = links.map(({ href, icon, label, badgeId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const badge = badgeId ? `<span class="social-badge" id="${badgeId}" style="display:none;"></span>` : '';
|
||||
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
|
||||
}).join('');
|
||||
|
||||
const aside = document.createElement('aside');
|
||||
aside.className = 'social-sidebar';
|
||||
aside.innerHTML = `
|
||||
<div class="social-sidebar-logo-area">
|
||||
<a href="/feed.html"><img src="/img/logo_community.png" alt="Logo"></a>
|
||||
</div>
|
||||
<ul>
|
||||
<li id="socialProfileItem">
|
||||
<a href="/benutzer.html"${profileActive}>
|
||||
<span class="icon" id="socialProfileIcon">◉</span>
|
||||
<span id="socialProfileName">Profil</span>
|
||||
</a>
|
||||
</li>
|
||||
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
||||
${desktopItems}
|
||||
<li style="margin-top:auto;"><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
|
||||
<li><a href="/einstellungen.html"${path === '/einstellungen.html' ? ' class="active"' : ''}><span class="icon">⚙️</span> Einstellungen</a></li>
|
||||
<li><a href="/login/logout"><span class="icon">⏏</span> Abmelden</a></li>
|
||||
</ul>`;
|
||||
|
||||
const appWrapper = document.querySelector('.app-wrapper');
|
||||
if (appWrapper) appWrapper.appendChild(aside);
|
||||
|
||||
// ── Mobile: Links + Profil ins Burger-Menü einhängen ──
|
||||
const sidebarUl = document.querySelector('.sidebar ul');
|
||||
if (sidebarUl) {
|
||||
const mobileLinks = links.map(({ href, icon, label, mobileBadgeId }) => {
|
||||
const cls = path === href ? ' class="active"' : '';
|
||||
const badge = mobileBadgeId
|
||||
? `<span class="social-badge" id="${mobileBadgeId}" style="display:none;"></span>`
|
||||
: '';
|
||||
return `<li class="sidebar-mobile-only"><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}${badge}</a></li>`;
|
||||
}).join('');
|
||||
|
||||
const mobileProfileActive = profileActive;
|
||||
const mobileProfile = `
|
||||
<li class="sidebar-mobile-only" id="socialMobileProfileItem">
|
||||
<a href="/benutzer.html"${mobileProfileActive}>
|
||||
<span class="icon" id="socialMobileProfileIcon">◉</span>
|
||||
<span id="socialMobileProfileName">Profil</span>
|
||||
</a>
|
||||
</li>`;
|
||||
|
||||
const sep = '<li class="sidebar-mobile-only"><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>';
|
||||
const mobileSettings = `<li class="sidebar-mobile-only"><a href="/einstellungen.html"${path === '/einstellungen.html' ? ' class="active"' : ''}><span class="icon">⚙️</span> Einstellungen</a></li>`;
|
||||
const logoutLi = sidebarUl.querySelector('a[href="/login/logout"]')?.closest('li');
|
||||
if (logoutLi) {
|
||||
logoutLi.insertAdjacentHTML('beforebegin', sep + mobileLinks + mobileProfile + mobileSettings);
|
||||
} else {
|
||||
sidebarUl.insertAdjacentHTML('beforeend', sep + mobileLinks + mobileProfile + mobileSettings);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Profil-Daten nachladen (Name + Avatar) ──
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(user => {
|
||||
if (!user) return;
|
||||
|
||||
function updateProfileEntry(nameId, iconId, itemId) {
|
||||
const nameEl = document.getElementById(nameId);
|
||||
if (nameEl) nameEl.textContent = user.name;
|
||||
|
||||
const iconEl = document.getElementById(iconId);
|
||||
if (iconEl && user.profilePicture) {
|
||||
iconEl.innerHTML = `<img src="data:image/png;base64,${user.profilePicture}" class="sidebar-profile-img" alt="">`;
|
||||
}
|
||||
const anchor = document.querySelector('#' + itemId + ' a');
|
||||
if (anchor) anchor.href = '/benutzer.html?userId=' + user.userId;
|
||||
}
|
||||
|
||||
updateProfileEntry('socialProfileName', 'socialProfileIcon', 'socialProfileItem');
|
||||
updateProfileEntry('socialMobileProfileName', 'socialMobileProfileIcon', 'socialMobileProfileItem');
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// ── Badge-Zähler ──
|
||||
function setBadge(ids, count) {
|
||||
ids.forEach(id => {
|
||||
if (!id) return;
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = count;
|
||||
el.style.display = count > 0 ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Ton abspielen ──
|
||||
// Browser erlauben audio.play() sobald der Nutzer mindestens einmal interagiert hat.
|
||||
let userHasInteracted = false;
|
||||
document.addEventListener('click', () => { userHasInteracted = true; }, { passive: true });
|
||||
document.addEventListener('keydown', () => { userHasInteracted = true; }, { passive: true });
|
||||
document.addEventListener('touchstart', () => { userHasInteracted = true; }, { passive: true });
|
||||
|
||||
function playSound(src) {
|
||||
if (!userHasInteracted) return;
|
||||
try {
|
||||
const audio = new Audio(src);
|
||||
audio.volume = 0.6;
|
||||
audio.play().catch(() => {});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ── Initiale Badge-Counts laden ──
|
||||
fetch('/social/friends/pending/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialFriendsBadge', 'socialMobileFriendsBadge'], n))
|
||||
.catch(() => {});
|
||||
|
||||
fetch('/social/messages/unread/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialMsgBadge', 'socialMobileMsgBadge'], n))
|
||||
.catch(() => {});
|
||||
|
||||
fetch('/notifications/unread/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialNotifBadge', 'socialMobileNotifBadge'], n))
|
||||
.catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/gruppen/requests/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/gruppen/reports/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0)
|
||||
]).then(([joins, reports]) => setBadge(['socialGruppenBadge', 'socialMobileGruppenBadge'], joins + reports))
|
||||
.catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/keyholder/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/bdsm/einladung/pending').then(r => r.ok ? r.json() : []).catch(() => [])
|
||||
]).then(([khInvs, lockeeInvs, bdsmInvs]) =>
|
||||
setBadge(['socialInvBadge', 'socialMobileInvBadge'], khInvs.length + lockeeInvs.length + bdsmInvs.length)
|
||||
).catch(() => {});
|
||||
|
||||
// ── SSE: Echtzeit-Push vom Server ──
|
||||
function connectSse() {
|
||||
const es = new EventSource('/events/stream');
|
||||
|
||||
es.addEventListener('DM', e => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
setBadge(['socialMsgBadge', 'socialMobileMsgBadge'], data.unreadCount || 0);
|
||||
// Nur Ton abspielen wenn nicht gerade auf der Nachrichten-Seite
|
||||
if (window.location.pathname !== '/nachrichten.html') {
|
||||
playSound('/audio/message.mp3');
|
||||
}
|
||||
// Nachrichten-Seite: sofortiges Laden neuer Nachrichten auslösen
|
||||
if (typeof window.__sseOnDm === 'function') window.__sseOnDm(data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
es.addEventListener('NOTIFICATION', e => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
setBadge(['socialNotifBadge', 'socialMobileNotifBadge'], data.unreadCount || 0);
|
||||
if (window.location.pathname !== '/benachrichtigungen.html') {
|
||||
playSound('/audio/notification.mp3');
|
||||
}
|
||||
if (typeof window.__sseOnNotification === 'function') window.__sseOnNotification(data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
// Nach 5 Sekunden neu verbinden
|
||||
setTimeout(connectSse, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
connectSse();
|
||||
})();
|
||||
(function () {
|
||||
// Badge + SSE service (kein Sidebar-Rendering mehr)
|
||||
|
||||
// ── Badge-Zähler ──
|
||||
function setBadge(ids, count, topbarType) {
|
||||
ids.forEach(id => {
|
||||
if (!id) return;
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.textContent = count;
|
||||
el.style.display = count > 0 ? '' : 'none';
|
||||
});
|
||||
if (topbarType && window.__topbarSetBadge) window.__topbarSetBadge(topbarType, count);
|
||||
}
|
||||
|
||||
// ── Ton abspielen ──
|
||||
let userHasInteracted = false;
|
||||
document.addEventListener('click', () => { userHasInteracted = true; }, { passive: true });
|
||||
document.addEventListener('keydown', () => { userHasInteracted = true; }, { passive: true });
|
||||
document.addEventListener('touchstart', () => { userHasInteracted = true; }, { passive: true });
|
||||
|
||||
function playSound(src) {
|
||||
if (!userHasInteracted) return;
|
||||
try {
|
||||
const audio = new Audio(src);
|
||||
audio.volume = 0.6;
|
||||
audio.play().catch(() => {});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ── Initiale Badge-Counts laden ──
|
||||
fetch('/social/friends/pending/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialFriendsBadge'], n, null))
|
||||
.catch(() => {});
|
||||
|
||||
fetch('/social/messages/unread/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialMsgBadge'], n, 'msg'))
|
||||
.catch(() => {});
|
||||
|
||||
fetch('/notifications/unread/count')
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialNotifBadge'], n, 'notif'))
|
||||
.catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/gruppen/requests/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/gruppen/reports/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0)
|
||||
]).then(([joins, reports]) => setBadge(['socialGruppenBadge'], joins + reports, null))
|
||||
.catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/keyholder/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/bdsm/einladung/pending').then(r => r.ok ? r.json() : []).catch(() => [])
|
||||
]).then(([khInvs, lockeeInvs, bdsmInvs]) =>
|
||||
setBadge(['socialInvBadge'], khInvs.length + lockeeInvs.length + bdsmInvs.length, 'inv')
|
||||
).catch(() => {});
|
||||
|
||||
// ── SSE: Echtzeit-Push vom Server ──
|
||||
function connectSse() {
|
||||
const es = new EventSource('/events/stream');
|
||||
|
||||
es.addEventListener('DM', e => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
setBadge(['socialMsgBadge'], data.unreadCount || 0, 'msg');
|
||||
if (window.location.pathname !== '/nachrichten.html') {
|
||||
playSound('/audio/message.mp3');
|
||||
}
|
||||
if (typeof window.__sseOnDm === 'function') window.__sseOnDm(data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
es.addEventListener('NOTIFICATION', e => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
setBadge(['socialNotifBadge'], data.unreadCount || 0, 'notif');
|
||||
if (window.location.pathname !== '/benachrichtigungen.html') {
|
||||
playSound('/audio/notification.mp3');
|
||||
}
|
||||
if (typeof window.__sseOnNotification === 'function') window.__sseOnNotification(data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
setTimeout(connectSse, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
connectSse();
|
||||
})();
|
||||
|
||||
420
xxxthegame/src/main/resources/static/js/topbar.js
Normal file
420
xxxthegame/src/main/resources/static/js/topbar.js
Normal file
@@ -0,0 +1,420 @@
|
||||
(function () {
|
||||
if (document.querySelector('.topbar')) return;
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '')
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ── Warten bis app-wrapper existiert (sidebar.js läuft synchron davor) ──
|
||||
function init() {
|
||||
const appWrapper = document.querySelector('.app-wrapper');
|
||||
if (!appWrapper) { setTimeout(init, 30); return; }
|
||||
injectHTML(appWrapper);
|
||||
loadProfile();
|
||||
setupSearch();
|
||||
setupOverlayButtons();
|
||||
loadInitialBadges();
|
||||
}
|
||||
setTimeout(init, 0);
|
||||
|
||||
// ── HTML Struktur ──
|
||||
function injectHTML(appWrapper) {
|
||||
const topbar = document.createElement('div');
|
||||
topbar.className = 'topbar';
|
||||
topbar.id = 'topbar';
|
||||
topbar.innerHTML = `
|
||||
<div class="topbar-left"></div>
|
||||
<div class="topbar-search-wrap">
|
||||
<span class="topbar-search-icon">${IC('SEARCH')}</span>
|
||||
<input type="text" id="topbarSearchInput" placeholder="Suchen…" autocomplete="off" spellcheck="false">
|
||||
<div class="topbar-search-overlay" id="topbarSearchOverlay"></div>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<button class="topbar-btn" id="topbarMsgBtn" title="Nachrichten">
|
||||
${IC('MESSAGES')}
|
||||
<span class="topbar-badge" id="topbarMsgBadge"></span>
|
||||
</button>
|
||||
<button class="topbar-btn" id="topbarNotifBtn" title="Benachrichtigungen">
|
||||
${IC('NOTIFICATIONS')}
|
||||
<span class="topbar-badge" id="topbarNotifBadge"></span>
|
||||
</button>
|
||||
<button class="topbar-btn" id="topbarInvBtn" title="Einladungen">
|
||||
${IC('INVITATIONS')}
|
||||
<span class="topbar-badge" id="topbarInvBadge"></span>
|
||||
</button>
|
||||
<button class="topbar-btn topbar-profile-btn" id="topbarProfileBtn">
|
||||
<span class="topbar-avatar-placeholder" id="topbarAvatarWrap">${IC('PROFILE')}</span>
|
||||
<span class="topbar-username" id="topbarUsername">…</span>
|
||||
</button>
|
||||
</div>`;
|
||||
appWrapper.insertAdjacentElement('beforebegin', topbar);
|
||||
|
||||
// Panel-Overlays am Ende von body einfügen
|
||||
document.body.insertAdjacentHTML('beforeend', `
|
||||
<div class="topbar-panel" id="topbarMsgPanel">
|
||||
<div class="topbar-panel-header">
|
||||
<span>${IC('MESSAGES')} Nachrichten</span>
|
||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||
</div>
|
||||
<div class="topbar-panel-body" id="topbarMsgBody"></div>
|
||||
<div class="topbar-panel-footer"><a href="/nachrichten.html">Alle Nachrichten →</a></div>
|
||||
</div>
|
||||
|
||||
<div class="topbar-panel" id="topbarNotifPanel">
|
||||
<div class="topbar-panel-header">
|
||||
<span>${IC('NOTIFICATIONS')} Benachrichtigungen</span>
|
||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||
</div>
|
||||
<div class="topbar-panel-body" id="topbarNotifBody"></div>
|
||||
<div class="topbar-panel-footer"><a href="/benachrichtigungen.html">Alle anzeigen →</a></div>
|
||||
</div>
|
||||
|
||||
<div class="topbar-panel" id="topbarInvPanel">
|
||||
<div class="topbar-panel-header">
|
||||
<span>${IC('INVITATIONS')} Einladungen</span>
|
||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||
</div>
|
||||
<div class="topbar-panel-body" id="topbarInvBody"></div>
|
||||
<div class="topbar-panel-footer"><a href="/einladungen.html">Alle anzeigen →</a></div>
|
||||
</div>
|
||||
|
||||
<div class="topbar-panel" id="topbarProfilePanel">
|
||||
<div class="topbar-panel-header">
|
||||
<span>Konto</span>
|
||||
<button class="topbar-panel-close" onclick="window.__topbarCloseAll()">✕</button>
|
||||
</div>
|
||||
<div class="topbar-panel-body topbar-profile-body">
|
||||
<div class="topbar-profile-card">
|
||||
<span id="topbarPanelAvatarWrap" style="font-size:2.5rem;line-height:1;">${IC('PROFILE')}</span>
|
||||
<div>
|
||||
<div id="topbarPanelName" style="font-weight:700;font-size:1rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr style="border:none;border-top:1px solid var(--color-secondary);margin:0;">
|
||||
<nav class="topbar-profile-nav">
|
||||
<a id="topbarProfileLink" href="/benutzer.html" class="topbar-profile-link">
|
||||
<span>${IC('PROFILE')}</span> Mein Profil
|
||||
</a>
|
||||
<a href="/einstellungen.html" class="topbar-profile-link">
|
||||
<span>${IC('SETTINGS')}</span> Einstellungen
|
||||
</a>
|
||||
<a href="/login/logout" class="topbar-profile-link topbar-profile-link--danger">
|
||||
<span>${IC('LOGOUT')}</span> Abmelden
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function IC(key) { return window.IC ? window.IC(key) : (window.ICONS?.[key]?.value || ''); }
|
||||
|
||||
// ── Profil laden ──
|
||||
function loadProfile() {
|
||||
fetch('/login/me')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(user => {
|
||||
if (!user) return;
|
||||
const nameEl = document.getElementById('topbarUsername');
|
||||
if (nameEl) nameEl.textContent = user.name;
|
||||
|
||||
const avatarWrap = document.getElementById('topbarAvatarWrap');
|
||||
if (avatarWrap && user.profilePicture) {
|
||||
avatarWrap.innerHTML = `<img src="data:image/png;base64,${user.profilePicture}" class="topbar-avatar" alt="">`;
|
||||
}
|
||||
const panelName = document.getElementById('topbarPanelName');
|
||||
if (panelName) panelName.textContent = user.name;
|
||||
|
||||
const panelAvatar = document.getElementById('topbarPanelAvatarWrap');
|
||||
if (panelAvatar && user.profilePicture) {
|
||||
panelAvatar.innerHTML = `<img src="data:image/png;base64,${user.profilePicture}" style="width:3rem;height:3rem;border-radius:50%;object-fit:cover;" alt="">`;
|
||||
}
|
||||
const profileLink = document.getElementById('topbarProfileLink');
|
||||
if (profileLink && user.userId) profileLink.href = '/benutzer.html?userId=' + user.userId;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// ── Suche ──
|
||||
function setupSearch() {
|
||||
const input = document.getElementById('topbarSearchInput');
|
||||
const overlay = document.getElementById('topbarSearchOverlay');
|
||||
if (!input || !overlay) return;
|
||||
|
||||
let timer;
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(timer);
|
||||
const q = input.value.trim();
|
||||
if (q.length < 2) { overlay.innerHTML = ''; overlay.classList.remove('open'); return; }
|
||||
overlay.innerHTML = '<div class="topbar-search-hint">Suche…</div>';
|
||||
overlay.classList.add('open');
|
||||
timer = setTimeout(() => doSearch(q, overlay), 300);
|
||||
});
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.topbar-search-wrap')) {
|
||||
overlay.classList.remove('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function doSearch(q, overlay) {
|
||||
try {
|
||||
const res = await fetch('/social/users/search?q=' + encodeURIComponent(q));
|
||||
if (!res.ok) { overlay.innerHTML = '<div class="topbar-search-hint">Fehler bei der Suche.</div>'; return; }
|
||||
const users = await res.json();
|
||||
if (!users || users.length === 0) {
|
||||
overlay.innerHTML = '<div class="topbar-search-hint">Keine Ergebnisse.</div>';
|
||||
return;
|
||||
}
|
||||
overlay.innerHTML = users.map(u => {
|
||||
const av = u.profilePicture
|
||||
? `<img src="data:image/png;base64,${esc(u.profilePicture)}" class="topbar-search-avatar" alt="">`
|
||||
: `<span class="topbar-search-avatar topbar-search-avatar--placeholder">${IC('PROFILE')}</span>`;
|
||||
return `<a href="/benutzer.html?userId=${esc(u.userId)}" class="topbar-search-result">
|
||||
${av}
|
||||
<span style="font-size:0.92rem;font-weight:600;">${esc(u.name)}</span>
|
||||
</a>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
overlay.innerHTML = '<div class="topbar-search-hint">Fehler bei der Suche.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Panel-Overlays ──
|
||||
let _activePanel = null;
|
||||
|
||||
function positionPanel(panel, btn) {
|
||||
const topbar = document.getElementById('topbar');
|
||||
const tRect = topbar ? topbar.getBoundingClientRect() : btn.getBoundingClientRect();
|
||||
panel.style.top = tRect.bottom + 'px';
|
||||
panel.style.right = Math.max(4, window.innerWidth - tRect.right) + 'px';
|
||||
panel.style.left = 'auto';
|
||||
}
|
||||
|
||||
function openPanel(panelId, btnId, loadFn) {
|
||||
const panel = document.getElementById(panelId);
|
||||
const btn = document.getElementById(btnId);
|
||||
if (!panel || !btn) return;
|
||||
if (_activePanel === panel && panel.classList.contains('open')) {
|
||||
closeAllPanels(); return;
|
||||
}
|
||||
closeAllPanels();
|
||||
positionPanel(panel, btn);
|
||||
panel.classList.add('open');
|
||||
_activePanel = panel;
|
||||
if (loadFn) loadFn();
|
||||
}
|
||||
|
||||
function closeAllPanels() {
|
||||
document.querySelectorAll('.topbar-panel.open').forEach(p => p.classList.remove('open'));
|
||||
_activePanel = null;
|
||||
}
|
||||
|
||||
window.__topbarCloseAll = closeAllPanels;
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.topbar-panel') && !e.target.closest('.topbar-btn'))
|
||||
closeAllPanels();
|
||||
});
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAllPanels(); });
|
||||
|
||||
function setupOverlayButtons() {
|
||||
const msgBtn = document.getElementById('topbarMsgBtn');
|
||||
const notifBtn = document.getElementById('topbarNotifBtn');
|
||||
const invBtn = document.getElementById('topbarInvBtn');
|
||||
const profileBtn = document.getElementById('topbarProfileBtn');
|
||||
if (msgBtn) msgBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarMsgPanel', 'topbarMsgBtn', loadMessages); });
|
||||
if (notifBtn) notifBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarNotifPanel', 'topbarNotifBtn', loadNotifications); });
|
||||
if (invBtn) invBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarInvPanel', 'topbarInvBtn', loadInvitations); });
|
||||
if (profileBtn) profileBtn.addEventListener('click', e => { e.stopPropagation(); openPanel('topbarProfilePanel', 'topbarProfileBtn', null); });
|
||||
}
|
||||
|
||||
// ── Nachrichten ──
|
||||
async function loadMessages() {
|
||||
const body = document.getElementById('topbarMsgBody');
|
||||
if (!body) return;
|
||||
body.innerHTML = '<div class="topbar-panel-hint">Wird geladen…</div>';
|
||||
try {
|
||||
const res = await fetch('/social/messages');
|
||||
if (!res.ok) { body.innerHTML = '<div class="topbar-panel-hint">Keine Nachrichten.</div>'; return; }
|
||||
const convos = await res.json();
|
||||
if (!convos.length) { body.innerHTML = '<div class="topbar-panel-hint">Noch keine Nachrichten.</div>'; return; }
|
||||
body.innerHTML = convos.slice(0, 7).map(c => {
|
||||
const av = c.partner?.profilePicture
|
||||
? `<img src="data:image/png;base64,${esc(c.partner.profilePicture)}" class="topbar-item-avatar" alt="">`
|
||||
: `<span class="topbar-item-avatar topbar-item-avatar--placeholder">${IC('PROFILE')}</span>`;
|
||||
const bold = c.unreadCount > 0 ? 'font-weight:700;' : '';
|
||||
const badge = c.unreadCount > 0
|
||||
? `<span class="topbar-item-badge">${c.unreadCount > 99 ? '99+' : c.unreadCount}</span>` : '';
|
||||
return `<a href="/nachrichten.html?userId=${esc(c.partner?.userId)}" class="topbar-panel-item">
|
||||
${av}
|
||||
<div class="topbar-panel-item-body">
|
||||
<div style="${bold}font-size:0.88rem;">${esc(c.partner?.name || '')}</div>
|
||||
<div class="topbar-panel-item-sub">${esc(c.lastMessage?.text || '')}</div>
|
||||
</div>
|
||||
${badge}
|
||||
</a>`;
|
||||
}).join('');
|
||||
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
|
||||
}
|
||||
|
||||
// ── Benachrichtigungen ──
|
||||
async function loadNotifications() {
|
||||
const body = document.getElementById('topbarNotifBody');
|
||||
if (!body) return;
|
||||
body.innerHTML = '<div class="topbar-panel-hint">Wird geladen…</div>';
|
||||
try {
|
||||
const res = await fetch('/notifications');
|
||||
if (!res.ok) { body.innerHTML = '<div class="topbar-panel-hint">Keine Benachrichtigungen.</div>'; return; }
|
||||
const notifs = await res.json();
|
||||
if (!notifs.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine neuen Benachrichtigungen.</div>'; return; }
|
||||
body.innerHTML = `<div style="padding:0.3rem 1rem;text-align:right;">
|
||||
<button onclick="window.__topbarMarkAllRead()" class="topbar-mark-all-btn">Alle gelesen</button>
|
||||
</div>`;
|
||||
notifs.forEach(n => {
|
||||
const el = document.createElement('div');
|
||||
const tag = n.targetUrl ? 'a' : 'div';
|
||||
const href = n.targetUrl ? `href="${esc(n.targetUrl)}"` : '';
|
||||
const unread = !n.read;
|
||||
el.innerHTML = `<${tag} ${href} class="topbar-panel-item topbar-notif-item${unread ? ' topbar-notif-item--unread' : ''}"
|
||||
onclick="window.__topbarMarkNotifRead('${esc(n.id)}')">
|
||||
${unread ? '<span class="topbar-notif-dot"></span>' : '<span style="width:7px;flex-shrink:0;"></span>'}
|
||||
<div class="topbar-panel-item-body">
|
||||
<div style="font-size:0.85rem;line-height:1.4;${unread ? 'font-weight:600;' : ''}">${esc(n.text)}</div>
|
||||
<div class="topbar-panel-item-sub">${n.sentAt ? new Date(n.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'}) : ''}</div>
|
||||
</div>
|
||||
</${tag}>`;
|
||||
body.appendChild(el.firstElementChild);
|
||||
});
|
||||
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
|
||||
}
|
||||
|
||||
window.__topbarMarkNotifRead = async function (id) {
|
||||
try {
|
||||
await fetch('/notifications/' + id + '/read', { method: 'POST' });
|
||||
const el = document.querySelector(`.topbar-notif-item--unread[onclick*="${id}"]`);
|
||||
if (el) el.classList.remove('topbar-notif-item--unread');
|
||||
const r = await fetch('/notifications/unread/count');
|
||||
if (r.ok) setTopbarBadge('notif', await r.json());
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
window.__topbarMarkAllRead = async function () {
|
||||
try {
|
||||
await fetch('/notifications/read-all', { method: 'POST' });
|
||||
setTopbarBadge('notif', 0);
|
||||
loadNotifications();
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
// ── Einladungen ──
|
||||
async function loadInvitations() {
|
||||
const body = document.getElementById('topbarInvBody');
|
||||
if (!body) return;
|
||||
body.innerHTML = '<div class="topbar-panel-hint">Wird geladen…</div>';
|
||||
try {
|
||||
const [lr, kr, br] = await Promise.all([
|
||||
fetch('/lockee/invitations/mine'),
|
||||
fetch('/keyholder/invitations/mine'),
|
||||
fetch('/bdsm/einladung/pending')
|
||||
]);
|
||||
const lockee = lr.ok ? await lr.json() : [];
|
||||
const kh = kr.ok ? await kr.json() : [];
|
||||
const bdsm = br.ok ? await br.json() : [];
|
||||
const all = [
|
||||
...lockee.map(i => ({ ...i, _type: 'lockee' })),
|
||||
...kh.map(i => ({ ...i, _type: 'keyholder' })),
|
||||
...bdsm.map(i => ({ ...i, _type: 'bdsm' }))
|
||||
];
|
||||
if (!all.length) { body.innerHTML = '<div class="topbar-panel-hint">Keine offenen Einladungen.</div>'; return; }
|
||||
body.innerHTML = '';
|
||||
all.forEach(inv => body.appendChild(buildInvCard(inv)));
|
||||
} catch (e) { body.innerHTML = '<div class="topbar-panel-hint">Fehler beim Laden.</div>'; }
|
||||
}
|
||||
|
||||
function buildInvCard(inv) {
|
||||
let typeIcon, typeName, line, declineUrl, declineMethod = 'DELETE', declineBody = null, acceptHtml;
|
||||
|
||||
if (inv._type === 'lockee') {
|
||||
typeIcon = IC('LOCK'); typeName = 'Lockee-Einladung'; line = inv.lockName || 'Lock';
|
||||
declineUrl = '/lockee/invitation/' + encodeURIComponent(inv.token);
|
||||
acceptHtml = `<a href="/einladungen.html" class="topbar-inv-btn topbar-inv-btn--accept">Details</a>`;
|
||||
} else if (inv._type === 'keyholder') {
|
||||
typeIcon = IC('KEY'); typeName = 'Keyholder-Einladung'; line = inv.lockName || 'Lock';
|
||||
declineUrl = '/keyholder/invitations/mine/' + encodeURIComponent(inv.token);
|
||||
acceptHtml = `<a href="/keyholder/invitation/${esc(inv.token)}" class="topbar-inv-btn topbar-inv-btn--accept">Annehmen</a>`;
|
||||
} else {
|
||||
typeIcon = IC('BDSM'); typeName = 'BDSM Game'; line = inv.senderName || 'Einladung';
|
||||
const id = inv.id || inv.einladungId || '';
|
||||
declineUrl = '/bdsm/einladung/' + encodeURIComponent(id) + '/antwort';
|
||||
declineMethod = 'PUT';
|
||||
declineBody = JSON.stringify({ annahme: false });
|
||||
acceptHtml = `<a href="/bdsm-einladung.html?id=${esc(id)}" class="topbar-inv-btn topbar-inv-btn--accept">Details</a>`;
|
||||
}
|
||||
|
||||
const senderPic = inv.senderAvatar || inv.lockOwnerAvatar;
|
||||
const av = senderPic
|
||||
? `<img src="data:image/png;base64,${esc(senderPic)}" class="topbar-item-avatar" alt="">`
|
||||
: `<span class="topbar-item-avatar topbar-item-avatar--placeholder">${IC('PROFILE')}</span>`;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'topbar-panel-item topbar-inv-card';
|
||||
div.innerHTML = `${av}
|
||||
<div class="topbar-panel-item-body">
|
||||
<div class="topbar-panel-item-sub">${typeIcon} ${typeName}</div>
|
||||
<div style="font-weight:600;font-size:0.88rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(line)}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.3rem;flex-shrink:0;">
|
||||
<button class="topbar-inv-btn topbar-inv-btn--decline"
|
||||
data-url="${esc(declineUrl)}"
|
||||
data-method="${declineMethod}"
|
||||
data-body="${esc(declineBody || '')}"
|
||||
onclick="window.__topbarDecline(this)">✕</button>
|
||||
${acceptHtml}
|
||||
</div>`;
|
||||
return div;
|
||||
}
|
||||
|
||||
window.__topbarDecline = async function (btn) {
|
||||
btn.disabled = true;
|
||||
const url = btn.dataset.url;
|
||||
const method = btn.dataset.method;
|
||||
const body = btn.dataset.body || null;
|
||||
try {
|
||||
const opts = { method };
|
||||
if (body) { opts.headers = { 'Content-Type': 'application/json' }; opts.body = body; }
|
||||
const res = await fetch(url, opts);
|
||||
if (res.ok || res.status === 204) {
|
||||
const card = btn.closest('.topbar-inv-card');
|
||||
if (card) card.remove();
|
||||
const remaining = document.getElementById('topbarInvBody')?.querySelectorAll('.topbar-inv-card').length || 0;
|
||||
setTopbarBadge('inv', remaining);
|
||||
} else { btn.disabled = false; }
|
||||
} catch (e) { btn.disabled = false; }
|
||||
};
|
||||
|
||||
// ── Badge-Verwaltung ──
|
||||
function setTopbarBadge(type, count) {
|
||||
const map = { msg: 'topbarMsgBadge', notif: 'topbarNotifBadge', inv: 'topbarInvBadge' };
|
||||
const el = document.getElementById(map[type]);
|
||||
if (!el) return;
|
||||
el.textContent = count > 99 ? '99+' : count;
|
||||
el.style.display = count > 0 ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
// Für social-sidebar.js zugänglich
|
||||
window.__topbarSetBadge = setTopbarBadge;
|
||||
|
||||
function loadInitialBadges() {
|
||||
fetch('/social/messages/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('msg', n)).catch(() => {});
|
||||
fetch('/notifications/unread/count').then(r => r.ok ? r.json() : 0).then(n => setTopbarBadge('notif', n)).catch(() => {});
|
||||
Promise.all([
|
||||
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/keyholder/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/bdsm/einladung/pending').then(r => r.ok ? r.json() : []).catch(() => [])
|
||||
]).then(([l, k, b]) => setTopbarBadge('inv', l.length + k.length + b.length)).catch(() => {});
|
||||
}
|
||||
})();
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>Keyholder*In bestätigt – XXX The Game</title>
|
||||
@@ -10,7 +10,7 @@
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content" style="max-width:480px;margin:4rem auto;text-align:center;">
|
||||
<div class="content" style="text-align:center;padding-top:3rem;">
|
||||
|
||||
<div id="msgOk" style="display:none;">
|
||||
<div style="font-size:3rem;margin-bottom:1rem;">🔐</div>
|
||||
|
||||
@@ -323,7 +323,8 @@
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/card-display.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>xXx Games – Login</title>
|
||||
@@ -10,7 +10,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<img src="icon.png" alt="Logo">
|
||||
<img src="/img/icon.png" alt="Logo">
|
||||
<h1>Bitte melde dich an</h1>
|
||||
|
||||
<label for="email">E-Mail</label>
|
||||
|
||||
@@ -16,13 +16,18 @@
|
||||
border-radius:10px; padding:1rem;
|
||||
}
|
||||
.template-card-header { display:flex; align-items:flex-start; justify-content:space-between; gap:0.75rem; margin-bottom:0.6rem; }
|
||||
.template-name { font-weight:700; font-size:1rem; }
|
||||
.template-type-badge {
|
||||
display:inline-block; font-size:0.68rem; font-weight:700; text-transform:uppercase;
|
||||
padding:0.1rem 0.5rem; border-radius:4px; margin-left:0.5rem; vertical-align:middle;
|
||||
.template-type-icon {
|
||||
position:relative; width:2.4rem; height:2.4rem; flex-shrink:0;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.badge-card { background:rgba(52,152,219,0.15); color:#3498db; }
|
||||
.badge-time { background:rgba(155,89,182,0.15); color:#9b59b6; }
|
||||
.template-type-icon .icon-base { font-size:2rem; line-height:1; }
|
||||
.template-type-icon img.icon-base { width:2rem; height:2rem; object-fit:contain; }
|
||||
.template-type-icon .icon-lock {
|
||||
position:absolute; bottom:-2px; right:-4px;
|
||||
font-size:1.8rem; line-height:1;
|
||||
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
|
||||
}
|
||||
.template-name { font-weight:700; font-size:1rem; }
|
||||
.template-meta { font-size:0.78rem; color:var(--color-muted); margin-top:0.2rem; line-height:1.5; }
|
||||
.template-actions { display:flex; gap:0.4rem; flex-shrink:0; }
|
||||
.template-actions button { margin:0; padding:0.3rem 0.75rem; font-size:0.82rem; width:auto; }
|
||||
@@ -434,8 +439,16 @@
|
||||
|
||||
<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;">
|
||||
<span style="font-size:0.88rem;color:#e74c3c;">Ungespeicherte Änderungen verwerfen?</span>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button onclick="cancelDiscard()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.3rem 0.8rem;font-size:0.82rem;width:auto;">Weiter bearbeiten</button>
|
||||
<button onclick="closeModal()" style="background:#c0392b;padding:0.3rem 0.8rem;font-size:0.82rem;width:auto;">Verwerfen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button onclick="closeModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">Abbrechen</button>
|
||||
<button id="modalCancelBtn" onclick="tryCloseModal()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">Abbrechen</button>
|
||||
<button id="modalSaveBtn" onclick="saveTemplate()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -454,7 +467,8 @@
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/card-display.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||
@@ -462,6 +476,7 @@
|
||||
// ── State ──
|
||||
let editId = null;
|
||||
let editType = null; // 'CARDLOCK' | 'TIMELOCK' – beim Bearbeiten fest
|
||||
let isDirty = false;
|
||||
let taskCtr = 0;
|
||||
let wheelCtr = 0;
|
||||
let pageNum = 0;
|
||||
@@ -789,11 +804,51 @@
|
||||
|
||||
alignModalToContent();
|
||||
document.getElementById('modalBackdrop').classList.add('open');
|
||||
document.getElementById('modalDiscardConfirm').style.display = 'none';
|
||||
|
||||
// Dirty-Tracking: erst nach dem nächsten Tick starten, damit die Initialisierung nicht feuert
|
||||
isDirty = false;
|
||||
setTimeout(() => {
|
||||
document.getElementById('modalBackdrop').querySelectorAll('input, textarea, select').forEach(el => {
|
||||
el.addEventListener('input', markDirty, { passive: true });
|
||||
el.addEventListener('change', markDirty, { passive: true });
|
||||
});
|
||||
}, 0);
|
||||
|
||||
document.getElementById('fName').focus();
|
||||
}
|
||||
|
||||
function closeModal() { document.getElementById('modalBackdrop').classList.remove('open'); editId = null; editType = null; }
|
||||
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) closeModal(); });
|
||||
function markDirty() { isDirty = true; }
|
||||
|
||||
function tryCloseModal() {
|
||||
if (isDirty) {
|
||||
const confirm = document.getElementById('modalDiscardConfirm');
|
||||
confirm.style.display = 'flex';
|
||||
confirm.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
function cancelDiscard() {
|
||||
document.getElementById('modalDiscardConfirm').style.display = 'none';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modalBackdrop').classList.remove('open');
|
||||
document.getElementById('modalDiscardConfirm').style.display = 'none';
|
||||
isDirty = false;
|
||||
editId = null;
|
||||
editType = null;
|
||||
}
|
||||
|
||||
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && document.getElementById('modalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault();
|
||||
tryCloseModal();
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', () => { if (document.getElementById('modalBackdrop').classList.contains('open')) alignModalToContent(); });
|
||||
|
||||
// ── Speichern ──
|
||||
@@ -962,9 +1017,9 @@
|
||||
function appendTemplateCard(t) {
|
||||
const list = document.getElementById('templateList');
|
||||
const isCard = t.lockType === 'CARDLOCK';
|
||||
const icon = isCard ? '🃏' : '⏱';
|
||||
const badgeClass = isCard ? 'badge-card' : 'badge-time';
|
||||
const badgeLabel = isCard ? 'Karten-Lock' : 'Zeit-Lock';
|
||||
const typeIcon = isCard
|
||||
? `<img src="img/card.png" class="icon-base" alt="Karten-Lock">`
|
||||
: `<span class="icon-base">🕐</span>`;
|
||||
|
||||
const hygText = t.hygineOpeningEveryMinites
|
||||
? `alle ${fmtMinutes(t.hygineOpeningEveryMinites)}, ${fmtMinutes(t.hygineOpeningDurationMinutes)} offen`
|
||||
@@ -973,17 +1028,22 @@
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'template-card';
|
||||
card.style.cursor = 'pointer';
|
||||
card.innerHTML = `
|
||||
<div class="template-card-header">
|
||||
<div>
|
||||
<div class="template-name">${esc(t.name || 'Ohne Namen')} <span class="template-type-badge ${badgeClass}">${icon} ${badgeLabel}</span></div>
|
||||
<div class="template-type-icon">
|
||||
${typeIcon}
|
||||
<span class="icon-lock">🔒</span>
|
||||
</div>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div class="template-name">${esc(t.name || 'Ohne Namen')}</div>
|
||||
<div class="template-meta">${metaLine}</div>
|
||||
</div>
|
||||
<div class="template-actions">
|
||||
<button onclick="editTemplate('${t.templateId}')" style="background:none;border:1px solid var(--color-secondary);color:var(--color-text);">✏ Bearbeiten</button>
|
||||
<button onclick="deleteTemplate('${t.lockType}','${t.templateId}','${esc(t.name||'')}')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;">✕ Löschen</button>
|
||||
<button onclick="event.stopPropagation();deleteTemplate('${t.lockType}','${t.templateId}','${esc(t.name||'')}')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;">✕ Löschen</button>
|
||||
</div>
|
||||
</div>`;
|
||||
card.addEventListener('click', () => editTemplate(t.templateId));
|
||||
list.appendChild(card);
|
||||
}
|
||||
|
||||
|
||||
@@ -318,7 +318,8 @@
|
||||
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let myId = null;
|
||||
|
||||
@@ -252,7 +252,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
// ── Constants ──
|
||||
const SPIELDAUER = [
|
||||
|
||||
@@ -277,7 +277,8 @@
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let myUserId = null;
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let debounceTimer;
|
||||
|
||||
@@ -346,7 +346,8 @@
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/image-viewer.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
let currentPicture = null;
|
||||
let currentPictureHq = null;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<img src="icon.png" alt="Logo">
|
||||
<img src="/img/icon.png" alt="Logo">
|
||||
<h1>Neues Konto erstellen</h1>
|
||||
|
||||
<label for="name">Name</label>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<p>Session-Setup für das Vanilla Game folgt hier.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 12;
|
||||
let userPage = 0, userTotalPages = 1, userLoading = false;
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const SOURCE_LABELS = {
|
||||
GREEN_CARD: 'Grüne Karte',
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
fetch('/login/me')
|
||||
.then(r => {
|
||||
|
||||
Reference in New Issue
Block a user