Chastity game angefangen
This commit is contained in:
@@ -18,6 +18,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -28,6 +31,7 @@ import java.util.UUID;
|
||||
@Transactional
|
||||
public class AboController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AboController.class);
|
||||
private static final int DEFAULT_PAGE_SIZE = 5;
|
||||
private static final int DISCOVER_PAGE_SIZE = 10;
|
||||
|
||||
@@ -105,6 +109,7 @@ public class AboController {
|
||||
abo.setUserId(user.getUserId());
|
||||
abo.setAufgabenGruppe(gruppe);
|
||||
aboRepository.save(abo);
|
||||
LOGGER.info("User {} hat Gruppe {} abonniert", user.getUserId(), gruppenId);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@@ -119,6 +124,7 @@ public class AboController {
|
||||
if (gruppe == null) return ResponseEntity.noContent().build();
|
||||
|
||||
aboRepository.deleteByUserIdAndAufgabenGruppe(user.getUserId(), gruppe);
|
||||
LOGGER.info("User {} hat Abo auf Gruppe {} beendet", user.getUserId(), gruppenId);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ public class AufgabeController {
|
||||
List<ToyEntity> toys = resolveToys(aufgabe.getBenoetigteToys());
|
||||
AufgabeEntity entity = AufgabeEntity.create(aufgabe, gruppeEntity, toys);
|
||||
aufgabeRepository.save(entity);
|
||||
LOGGER.debug("Aufgabe {} '{}' in Gruppe {} erstellt", entity.getAufgabeId(), entity.getKurzText(), aufgabe.getGruppeId());
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getAufgabeId()).toUri()
|
||||
).build();
|
||||
@@ -88,6 +89,7 @@ public class AufgabeController {
|
||||
entity.setBenoetigtPassiv(aufgabe.getBenoetigtPassiv());
|
||||
entity.setBenoetigteToys(resolveToys(aufgabe.getBenoetigteToys()));
|
||||
aufgabeRepository.save(entity);
|
||||
LOGGER.debug("Aufgabe {} aktualisiert", aufgabeId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ public class AufgabenGruppeController {
|
||||
entity.setUserId(user.getUserId());
|
||||
entity.setPrivateGruppe(true);
|
||||
gruppeRepository.save(entity);
|
||||
LOGGER.debug("User {} hat AufgabenGruppe '{}' ({}) erstellt", user.getUserId(), entity.getName(), entity.getGruppenId());
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getGruppenId()).toUri()
|
||||
).build();
|
||||
@@ -179,6 +180,7 @@ public class AufgabenGruppeController {
|
||||
entity.setBild(Base64.getDecoder().decode(gruppe.getBild()));
|
||||
}
|
||||
gruppeRepository.save(entity);
|
||||
LOGGER.debug("User {} hat AufgabenGruppe {} aktualisiert", user.getUserId(), gruppeId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -293,6 +295,7 @@ public class AufgabenGruppeController {
|
||||
finisherRepository.save(fc);
|
||||
}
|
||||
|
||||
LOGGER.info("User {} hat AufgabenGruppe {} kopiert (Quelle: {})", user.getUserId(), copy.getGruppenId(), gruppeId);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ public class FavoritController {
|
||||
if (existing.isEmpty()) {
|
||||
entity = FavoritEntity.fromFavorit(favorit, userId);
|
||||
favoritRepository.save(entity);
|
||||
LOGGER.debug("User {} hat AufgabenGruppe {} als Favorit gespeichert", userId, favorit.getAufgabenGruppeId());
|
||||
} else {
|
||||
entity = existing.get(0);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ public class FinisherController {
|
||||
List<ToyEntity> toys = resolveToys(finisher.getBenoetigteToys());
|
||||
FinisherEntity entity = FinisherEntity.create(finisher, gruppeEntity, toys);
|
||||
finisherRepository.save(entity);
|
||||
LOGGER.debug("Finisher {} '{}' in Gruppe {} erstellt", entity.getFinisherId(), entity.getKurzText(), finisher.getGruppeId());
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getFinisherId()).toUri()
|
||||
).build();
|
||||
@@ -87,6 +88,7 @@ public class FinisherController {
|
||||
entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv());
|
||||
entity.setBenoetigteToys(resolveToys(finisher.getBenoetigteToys()));
|
||||
finisherRepository.save(entity);
|
||||
LOGGER.debug("Finisher {} aktualisiert", finisherId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ public class SperreController {
|
||||
List<ToyEntity> toys = resolveToys(sperre.getBenoetigteToys());
|
||||
SperreEntity entity = SperreEntity.create(sperre, gruppeEntity, toys);
|
||||
sperreRepository.save(entity);
|
||||
LOGGER.debug("Sperre {} '{}' in Gruppe {} erstellt", entity.getSperreId(), entity.getKurzText(), sperre.getGruppeId());
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getSperreId()).toUri()
|
||||
).build();
|
||||
@@ -89,6 +90,7 @@ public class SperreController {
|
||||
entity.setSperreFuer(sperre.getSperreFuer());
|
||||
entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys()));
|
||||
sperreRepository.save(entity);
|
||||
LOGGER.debug("Sperre {} aktualisiert", sperreId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ public class StrafeController {
|
||||
List<ToyEntity> toys = resolveToys(strafe.getBenoetigteToys());
|
||||
StrafeEntity entity = StrafeEntity.create(strafe, gruppeEntity, toys);
|
||||
strafeRepository.save(entity);
|
||||
LOGGER.debug("Strafe {} '{}' in Gruppe {} erstellt", entity.getStrafeId(), entity.getKurzText(), strafe.getGruppeId());
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getStrafeId()).toUri()
|
||||
).build();
|
||||
@@ -88,6 +89,7 @@ public class StrafeController {
|
||||
entity.setBenoetigtPassiv(strafe.getBenoetigtPassiv());
|
||||
entity.setBenoetigteToys(resolveToys(strafe.getBenoetigteToys()));
|
||||
strafeRepository.save(entity);
|
||||
LOGGER.debug("Strafe {} aktualisiert", strafeId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ public class ToyController {
|
||||
ToyEntity entity = ToyEntity.create(toy);
|
||||
entity.setUserId(user.getUserId());
|
||||
toyRepository.save(entity);
|
||||
LOGGER.debug("User {} hat Toy {} '{}' erstellt", user.getUserId(), entity.getToyId(), entity.getName());
|
||||
return ResponseEntity.created(
|
||||
ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(entity.getToyId()).toUri()
|
||||
).build();
|
||||
@@ -175,6 +176,7 @@ public class ToyController {
|
||||
copy.setUserId(user.getUserId());
|
||||
copy.setBild(source.getBild());
|
||||
toyRepository.save(copy);
|
||||
LOGGER.debug("User {} hat System-Toy {} kopiert (Kopie: {})", user.getUserId(), toyId, copy.getToyId());
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@@ -206,6 +208,7 @@ public class ToyController {
|
||||
entity.setBild(Base64.getDecoder().decode(toy.getBild()));
|
||||
}
|
||||
toyRepository.save(entity);
|
||||
LOGGER.debug("User {} hat Toy {} aktualisiert", user.getUserId(), toyId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ public class SecurityConfig {
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionvanilla.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsm.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionchastity.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionchastityingame.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtasks.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtoys.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmingame.html")).authenticated()
|
||||
@@ -75,6 +76,7 @@ public class SecurityConfig {
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/password-reset/request")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/password-reset/confirm")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/email-change/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/keyholder/invitation/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/filler")).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
@@ -46,10 +46,15 @@ import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/feed")
|
||||
public class FeedController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FeedController.class);
|
||||
|
||||
private final FeedPostRepository feedPostRepository;
|
||||
private final FeedPostLikeRepository feedPostLikeRepository;
|
||||
private final FeedPostOptionRepository feedPostOptionRepository;
|
||||
@@ -120,6 +125,7 @@ public class FeedController {
|
||||
post.setPublic(req.isPublic());
|
||||
post.setCreatedAt(LocalDateTime.now());
|
||||
feedPostRepository.save(post);
|
||||
LOGGER.info("User {} hat Feed-Post {} erstellt (Typ: {}, public: {})", myId, post.getPostId(), typ, post.isPublic());
|
||||
|
||||
if (typ == BeitragTyp.UMFRAGE && req.optionen() != null) {
|
||||
for (int i = 0; i < req.optionen().size(); i++) {
|
||||
@@ -250,6 +256,7 @@ public class FeedController {
|
||||
var existing = feedPostLikeRepository.findByPostIdAndUserId(id, myId);
|
||||
if (existing.isPresent()) {
|
||||
feedPostLikeRepository.delete(existing.get());
|
||||
LOGGER.debug("User {} hat Like auf Feed-Post {} entfernt", myId, id);
|
||||
} else {
|
||||
FeedPostLikeEntity like = new FeedPostLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
@@ -257,6 +264,7 @@ public class FeedController {
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
feedPostLikeRepository.save(like);
|
||||
LOGGER.debug("User {} hat Feed-Post {} geliked", myId, id);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
@@ -297,6 +305,7 @@ public class FeedController {
|
||||
vote.setPostId(id);
|
||||
vote.setUserId(myId);
|
||||
feedPostVoteRepository.save(vote);
|
||||
LOGGER.debug("User {} hat für Option {} in Feed-Post {} gestimmt", myId, req.optionId(), id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -319,6 +328,7 @@ public class FeedController {
|
||||
var kommentare = kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("FEED_POST", id);
|
||||
kommentarRepository.deleteAll(kommentare);
|
||||
feedPostRepository.delete(post);
|
||||
LOGGER.info("User {} hat Feed-Post {} gelöscht", myId, id);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockRepository;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationEntity;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
|
||||
public class CardLockService extends ProcessLock {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CardLockService.class);
|
||||
private final CardLockEntity lock;
|
||||
private VerificationRepository verificationRepository;
|
||||
private VerificationVoteRepository verificationVoteRepository;
|
||||
private CardLockRepository cardLockRepository;
|
||||
|
||||
public CardLockService(CardLockEntity lock, VerificationRepository verificationRepository, VerificationVoteRepository verificationVoteRepository, CardLockRepository cardLockRepository) {
|
||||
this.lock = lock;
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
this.cardLockRepository = cardLockRepository;
|
||||
}
|
||||
|
||||
public String getNextCard() {
|
||||
LOGGER.debug("New Card requested by user {}", lock.getLockee());
|
||||
var cards = lock.getAvailableCards();
|
||||
if (!cards.isEmpty()) {
|
||||
var card = cards.get(new Random().nextInt(cards.size()));
|
||||
LOGGER.debug("Card drafted: {}", card);
|
||||
card.get().processCard(this);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String doubleUp() {
|
||||
var cards = lock.getAvailableCards();
|
||||
LOGGER.debug("Double up {} cards", cards.size());
|
||||
lock.getAvailableCards().addAll(cards);
|
||||
LOGGER.debug("Now {} cards", lock.getAvailableCards().size());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String reset() {
|
||||
LOGGER.debug("Reset to initial cards");
|
||||
lock.setAvailableCards(lock.getInitialCards());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String unlock() {
|
||||
this.lock.setUnlockTime(LocalDateTime.now());
|
||||
boolean valid = true;
|
||||
if (!this.lock.isTestLock()) {
|
||||
if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
|
||||
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
|
||||
.filter(verification -> isValid(verification))
|
||||
.map(verification -> verification.getVerificationTime().toLocalDate())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
LocalDate current = this.lock.getStartTime().toLocalDate().plusDays(1);
|
||||
LocalDate last = this.lock.getUnlockTime().toLocalDate();
|
||||
|
||||
while (!current.isAfter(last)) {
|
||||
if (!verifications.contains(current)) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
current = current.plusDays(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
|
||||
}
|
||||
// XP berechnen
|
||||
// Lock löschen
|
||||
|
||||
lock.setUnlockTime(LocalDateTime.now());
|
||||
LOGGER.debug("Unlocked at {}", lock.getUnlockTime());
|
||||
return this.lock.getUnlockCode();
|
||||
}
|
||||
|
||||
private boolean isValid(VerificationEntity entity) {
|
||||
int count = 0;
|
||||
for (VerificationVoteEntity vote : verificationVoteRepository.findAllByVerificationId(entity.getVerficationId())) {
|
||||
if (vote.isUpvote()) {
|
||||
count++;
|
||||
} else {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
return count >= 0;
|
||||
}
|
||||
|
||||
public String freeze() {
|
||||
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0);
|
||||
freeze(multiplier);
|
||||
return "";
|
||||
}
|
||||
|
||||
private String freeze(double multiplier) {
|
||||
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
||||
lock.setFrozenUntill(frozenTill);
|
||||
LOGGER.debug("Frozen until {}", lock.getFrozenUntill());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String task() {
|
||||
LOGGER.debug("Apply random task");
|
||||
var tasks = lock.getTasks();
|
||||
if (!tasks.isEmpty()) {
|
||||
task(tasks.get(new Random().nextInt(tasks.size())));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String task(Task task) {
|
||||
LOGGER.debug("Apply task {}", task);
|
||||
lock.setCurrentTask(task.getText());
|
||||
if (task.getMinutes() != null) {
|
||||
freeze(task.getMinutes());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String clearTask() {
|
||||
LOGGER.debug("Clear task");
|
||||
lock.setFrozenUntill(null);
|
||||
lock.setCurrentTask(null);
|
||||
return "";
|
||||
}
|
||||
|
||||
public String redCard() {
|
||||
return "";
|
||||
}
|
||||
|
||||
public String yellowCard() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class CodeCreator {
|
||||
|
||||
private static final String CHARS_AN = "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
|
||||
private static final String CHARS_N = "0123456789";
|
||||
|
||||
public static String createNumeric(int digits) {
|
||||
return create(digits, CHARS_N);
|
||||
}
|
||||
|
||||
public static String createAlphanumericCode(int digits) {
|
||||
return create(digits, CHARS_AN);
|
||||
}
|
||||
|
||||
private static String create(int digits, String chars) {
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < digits; i++) {
|
||||
sb.append(chars.charAt(new Random().nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -12,38 +12,34 @@ import de.oaa.xxx.games.chastity.cardlock.CardEnum;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
|
||||
public class KeyholdeCardLock {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeyholdeCardLock.class);
|
||||
public class KeyholderCardLock {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeyholderCardLock.class);
|
||||
|
||||
private CardLockEntity lock;
|
||||
|
||||
|
||||
public void addCards(List<CardEnum> cards) {
|
||||
lock.getAvailableCards().addAll(cards);
|
||||
}
|
||||
|
||||
|
||||
public void freeze() {
|
||||
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0);
|
||||
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
||||
lock.setFrozenUntill(frozenTill);
|
||||
LOGGER.debug("Frozen by keyholder untill %s", lock.getFrozenUntill());
|
||||
LOGGER.debug("Frozen by keyholder until {}", lock.getFrozenUntill());
|
||||
}
|
||||
|
||||
public void freeze(LocalDateTime untill) {
|
||||
lock.setFrozenUntill(untill);
|
||||
LOGGER.debug("Frozen by keyholder untill %s", lock.getFrozenUntill());
|
||||
public void freeze(LocalDateTime until) {
|
||||
lock.setFrozenUntill(until);
|
||||
LOGGER.debug("Frozen by keyholder until {}", lock.getFrozenUntill());
|
||||
}
|
||||
|
||||
|
||||
public void task() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void task(Task task) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void penalty() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
public class KeyholderController {
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "keyholder_invitation")
|
||||
public class KeyholderInvitationEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID lockId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID keyholderUserId;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String token;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getId() { return id; }
|
||||
|
||||
public UUID getLockId() { return lockId; }
|
||||
public void setLockId(UUID lockId) { this.lockId = lockId; }
|
||||
|
||||
public UUID getKeyholderUserId() { return keyholderUserId; }
|
||||
public void setKeyholderUserId(UUID keyholderUserId) { this.keyholderUserId = keyholderUserId; }
|
||||
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface KeyholderInvitationRepository extends JpaRepository<KeyholderInvitationEntity, UUID> {
|
||||
Optional<KeyholderInvitationEntity> findByToken(String token);
|
||||
@Transactional
|
||||
void deleteByLockId(UUID lockId);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
public class PrcoessTimedLock {
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardEnum;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
|
||||
public class ProcessCardLock extends ProcessLock {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ProcessCardLock.class);
|
||||
private final CardLockEntity lock;
|
||||
|
||||
public ProcessCardLock(CardLockEntity lock) {
|
||||
this.lock = lock;
|
||||
}
|
||||
|
||||
public String getNextCard() {
|
||||
LOGGER.debug("New Card requested by user %s", lock.getLockee().toString());
|
||||
if (lock.getAvailableCards().size() > 0) {
|
||||
var card = lock.getAvailableCards().get(new Random().nextInt(lock.getAvailableCards().size()));
|
||||
LOGGER.debug("Card drafted: %s", card.toString());
|
||||
card.get().processCard(this);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String doubleUp() {
|
||||
List<CardEnum> cards = lock.getAvailableCards();
|
||||
LOGGER.debug("Double up %i cards", cards.size());
|
||||
lock.getAvailableCards().addAll(cards);
|
||||
LOGGER.debug("Now %i cards", cards.size());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String reset() {
|
||||
LOGGER.debug("Reset to initial cards");
|
||||
lock.setAvailableCards(lock.getInitialCards());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String unlock() {
|
||||
// Verifications prüfen
|
||||
// Historie eintragen
|
||||
// XP berechnen
|
||||
// Lock löschen
|
||||
|
||||
lock.setUnlockTime(LocalDateTime.now());
|
||||
LOGGER.debug("Unlocked at " + lock.getUnlockTime());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String freeze() {
|
||||
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0);
|
||||
freeze(multiplier);
|
||||
return "";
|
||||
}
|
||||
|
||||
private String freeze(double multiplier) {
|
||||
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
||||
lock.setFrozenUntill(frozenTill);
|
||||
LOGGER.debug("Frozen untill %s", lock.getFrozenUntill());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String task() {
|
||||
LOGGER.debug("Apply random task");
|
||||
if (lock.getTasks().size() > 0) {
|
||||
task(lock.getTasks().get(new Random().nextInt(lock.getTasks().size())));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String task(Task task) {
|
||||
LOGGER.debug("Apply task %s", task);
|
||||
lock.setCurrentTask(task.getText());
|
||||
if (task.getMinutes() != null) {
|
||||
freeze(task.getMinutes());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String clearTask() {
|
||||
LOGGER.debug("Clear task");
|
||||
lock.setFrozenUntill(null);
|
||||
lock.setCurrentTask(null);
|
||||
return "";
|
||||
}
|
||||
|
||||
public String redCard() {
|
||||
return "";
|
||||
}
|
||||
|
||||
public String yellowCard() {
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.games.chastity;
|
||||
|
||||
public class ProcessTimedLock extends ProcessLock {
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public interface Card {
|
||||
|
||||
public CardDTO processCard(ProcessCardLock lock);
|
||||
public CardDTO processCard(CardLockService lock);
|
||||
}
|
||||
|
||||
@@ -2,29 +2,49 @@ package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
public enum CardEnum {
|
||||
|
||||
ROT {
|
||||
RED {
|
||||
@Override
|
||||
public Card get() {
|
||||
return new RedCard();
|
||||
}
|
||||
},
|
||||
GRUEN {
|
||||
GREEN {
|
||||
@Override
|
||||
public Card get() {
|
||||
return new GreenCard();
|
||||
}
|
||||
},
|
||||
GELB {
|
||||
YELLOW {
|
||||
@Override
|
||||
public Card get() {
|
||||
return new YellowCard();
|
||||
}
|
||||
},
|
||||
AUFGABE {
|
||||
TASK {
|
||||
@Override
|
||||
public Card get() {
|
||||
return new TaskCard();
|
||||
}
|
||||
},
|
||||
FREEZE {
|
||||
@Override
|
||||
public Card get() {
|
||||
return new FreezeCard();
|
||||
}
|
||||
},
|
||||
RESET {
|
||||
@Override
|
||||
public Card get() {
|
||||
return new ResetCard();
|
||||
}
|
||||
},
|
||||
DOUBLE_UP {
|
||||
@Override
|
||||
public Card get() {
|
||||
return new DoubleUpCard();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public abstract Card get();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Converter
|
||||
public class CardEnumListConverter implements AttributeConverter<List<CardEnum>, String> {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(List<CardEnum> list) {
|
||||
if (list == null || list.isEmpty()) return null;
|
||||
try {
|
||||
return mapper.writeValueAsString(list.stream().map(Enum::name).toList());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CardEnum> convertToEntityAttribute(String json) {
|
||||
if (json == null || json.isBlank()) return new ArrayList<>();
|
||||
try {
|
||||
List<String> names = mapper.readValue(json, new TypeReference<>() {});
|
||||
return new ArrayList<>(names.stream().map(CardEnum::valueOf).toList());
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.KeyholderInvitationEntity;
|
||||
import de.oaa.xxx.games.chastity.KeyholderInvitationRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.mail.Email;
|
||||
import de.oaa.xxx.mail.MailService;
|
||||
import de.oaa.xxx.mail.MailTemplateService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/keyholder")
|
||||
public class CardLockController {
|
||||
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final KeyholderInvitationRepository invitationRepository;
|
||||
private final MailService mailService;
|
||||
private final MailTemplateService mailTemplateService;
|
||||
|
||||
@Value("${app.base-url:http://localhost:8080}")
|
||||
private String baseUrl;
|
||||
|
||||
public CardLockController(CardlockRepository cardlockRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderInvitationRepository invitationRepository,
|
||||
MailService mailService,
|
||||
MailTemplateService mailTemplateService) {
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.invitationRepository = invitationRepository;
|
||||
this.mailService = mailService;
|
||||
this.mailTemplateService = mailTemplateService;
|
||||
}
|
||||
|
||||
record CreateCardLockRequest(
|
||||
String name,
|
||||
UUID keyholder,
|
||||
List<CardEnum> initialCards,
|
||||
Integer pickEveryMinute,
|
||||
boolean accumulatePicks,
|
||||
boolean showRemainingCards,
|
||||
LocalDateTime latestOpeningtime,
|
||||
Integer hygineOpeningDurationMinutes,
|
||||
Integer hygineOpeningEveryMinites,
|
||||
List<Task> tasks,
|
||||
boolean requiresVerification,
|
||||
boolean testLock,
|
||||
Integer unlockCodeLines
|
||||
) {}
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
|
||||
private String generateUnlockCode(int lines) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < lines; i++) {
|
||||
sb.append(RNG.nextInt(10));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@PostMapping("/cardlock")
|
||||
public ResponseEntity<Map<String, Object>> createCardLock(
|
||||
@RequestBody CreateCardLockRequest req,
|
||||
Principal principal) {
|
||||
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var me = meOpt.get();
|
||||
UUID myId = me.getUserId();
|
||||
|
||||
if (req.initialCards() == null || req.initialCards().isEmpty()
|
||||
|| req.pickEveryMinute() == null || req.pickEveryMinute() < 1)
|
||||
return ResponseEntity.badRequest().build();
|
||||
|
||||
int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1)
|
||||
? req.unlockCodeLines() : 5;
|
||||
String unlockCode = generateUnlockCode(codeLines);
|
||||
|
||||
CardLockEntity lock = new CardLockEntity();
|
||||
lock.setName(req.name());
|
||||
lock.setLockee(myId);
|
||||
lock.setKeyholder(null); // set only after invitation is confirmed
|
||||
lock.setInitialCards(req.initialCards());
|
||||
lock.setPickEveryMinute(req.pickEveryMinute());
|
||||
lock.setAccumulatePicks(req.accumulatePicks());
|
||||
lock.setShowRemainingCards(req.showRemainingCards());
|
||||
lock.setLatestOpeningtime(req.latestOpeningtime());
|
||||
lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes());
|
||||
lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites());
|
||||
lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
|
||||
lock.setRequiresVerification(req.requiresVerification());
|
||||
lock.setTestLock(req.testLock());
|
||||
lock.setUnlockCodeLines(codeLines);
|
||||
lock.setUnlockCode(unlockCode);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
lock.setStartTime(now);
|
||||
lock.setAvailableCards(new ArrayList<>(req.initialCards()));
|
||||
lock.setOpenPicks(0);
|
||||
lock.setNextCardIn(now.plusMinutes(req.pickEveryMinute()));
|
||||
lock.setHygineOpeningtime(now);
|
||||
|
||||
cardlockRepository.save(lock);
|
||||
|
||||
boolean keyholderPending = false;
|
||||
if (req.keyholder() != null) {
|
||||
var khOpt = userRepository.findById(req.keyholder());
|
||||
if (khOpt.isPresent()) {
|
||||
var kh = khOpt.get();
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
KeyholderInvitationEntity inv = new KeyholderInvitationEntity();
|
||||
inv.setLockId(lock.getLockId());
|
||||
inv.setKeyholderUserId(kh.getUserId());
|
||||
inv.setToken(token);
|
||||
inv.setCreatedAt(now);
|
||||
invitationRepository.save(inv);
|
||||
|
||||
String confirmLink = baseUrl + "/keyholder/invitation/" + token;
|
||||
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
|
||||
Email email = new Email();
|
||||
email.setEmailAdresse(kh.getEmail());
|
||||
email.setTitel("Einladung als Keyholder*In – " + lockName);
|
||||
email.setText(mailTemplateService.buildKeyholderInvitationMail(
|
||||
kh.getName(), me.getName(), lockName, confirmLink));
|
||||
mailService.send(email);
|
||||
|
||||
keyholderPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"lockId", lock.getLockId().toString(),
|
||||
"unlockCode", unlockCode,
|
||||
"keyholderPending", keyholderPending
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/mylock")
|
||||
public ResponseEntity<Map<String, Object>> getMyActiveLock(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var locks = cardlockRepository.findByLockee(meOpt.get().getUserId());
|
||||
var active = locks.stream()
|
||||
.filter(l -> l.getUnlockTime() == null)
|
||||
.findFirst();
|
||||
if (active.isEmpty()) return ResponseEntity.noContent().build();
|
||||
return ResponseEntity.ok(Map.of("lockId", active.get().getLockId().toString()));
|
||||
}
|
||||
|
||||
@GetMapping("/cardlock/{lockId}")
|
||||
public ResponseEntity<Map<String, Object>> getLock(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = cardlockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
Map<String, Long> cardCounts = new LinkedHashMap<>();
|
||||
if (l.getAvailableCards() != null) {
|
||||
l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum));
|
||||
}
|
||||
long totalCards = l.getAvailableCards() != null ? l.getAvailableCards().size() : 0;
|
||||
|
||||
// Hygiene-Berechnung
|
||||
boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null;
|
||||
boolean hygieneOpeningDue = false;
|
||||
long hygieneMinutesRemaining = 0;
|
||||
if (hygieneEnabled) {
|
||||
LocalDateTime base = l.getLastHygineOpening() != null
|
||||
? l.getLastHygineOpening()
|
||||
: l.getStartTime();
|
||||
if (base != null) {
|
||||
LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites());
|
||||
hygieneMinutesRemaining = ChronoUnit.MINUTES.between(LocalDateTime.now(), nextHygiene);
|
||||
hygieneOpeningDue = hygieneMinutesRemaining <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("lockId", l.getLockId().toString());
|
||||
result.put("name", l.getName() != null ? l.getName() : "");
|
||||
result.put("showRemainingCards", l.isShowRemainingCards());
|
||||
result.put("availableCardCounts", cardCounts);
|
||||
result.put("totalCards", totalCards);
|
||||
result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0);
|
||||
result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : "");
|
||||
result.put("hygieneEnabled", hygieneEnabled);
|
||||
result.put("hygieneOpeningDue", hygieneOpeningDue);
|
||||
result.put("hygieneMinutesRemaining", hygieneMinutesRemaining);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@DeleteMapping("/cardlock/{lockId}")
|
||||
public ResponseEntity<Void> deleteLock(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = cardlockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!lockOpt.get().getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
invitationRepository.deleteByLockId(lockId);
|
||||
cardlockRepository.deleteById(lockId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/invitation/{token}")
|
||||
public void confirmInvitation(@PathVariable String token,
|
||||
jakarta.servlet.http.HttpServletResponse response) throws Exception {
|
||||
var invOpt = invitationRepository.findByToken(token);
|
||||
if (invOpt.isEmpty()) {
|
||||
response.sendRedirect("/keyholder-invitation-confirmed.html?status=invalid");
|
||||
return;
|
||||
}
|
||||
var inv = invOpt.get();
|
||||
var lockOpt = cardlockRepository.findById(inv.getLockId());
|
||||
if (lockOpt.isEmpty()) {
|
||||
response.sendRedirect("/keyholder-invitation-confirmed.html?status=invalid");
|
||||
return;
|
||||
}
|
||||
var lock = lockOpt.get();
|
||||
lock.setKeyholder(inv.getKeyholderUserId());
|
||||
cardlockRepository.save(lock);
|
||||
invitationRepository.delete(inv);
|
||||
response.sendRedirect("/keyholder-invitation-confirmed.html?status=ok");
|
||||
}
|
||||
}
|
||||
@@ -5,36 +5,98 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "card_lock")
|
||||
public class CardLockEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID lockId;
|
||||
|
||||
@Column
|
||||
private String name;
|
||||
|
||||
// Settings
|
||||
@Column(nullable = false)
|
||||
private UUID lockee;
|
||||
@Column
|
||||
private UUID keyholder;
|
||||
@Convert(converter = CardEnumListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<CardEnum> initialCards;
|
||||
@Column
|
||||
private Integer pickEveryMinute;
|
||||
@Column
|
||||
private boolean accumulatePicks;
|
||||
@Column
|
||||
private boolean showRemainingCards;
|
||||
@Column
|
||||
private LocalDateTime latestOpeningtime;
|
||||
@Column
|
||||
private LocalDateTime frozenUntill;
|
||||
@Column
|
||||
private Integer hygineOpeningDurationMinutes;
|
||||
@Column
|
||||
private Integer hygineOpeningEveryMinites;
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasks;
|
||||
@Column
|
||||
private boolean requiresVerification;
|
||||
@Column
|
||||
private boolean testLock;
|
||||
@Column
|
||||
private Integer unlockCodeLines;
|
||||
|
||||
// State
|
||||
@Column
|
||||
private LocalDateTime startTime;
|
||||
@Column
|
||||
private LocalDateTime nextCardIn;
|
||||
@Column
|
||||
private Integer openPicks;
|
||||
@Convert(converter = CardEnumListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<CardEnum> availableCards;
|
||||
@Column
|
||||
private LocalDateTime lastHygineOpening;
|
||||
@Column
|
||||
private LocalDateTime hygineOpeningtime; // If null, not while hygine opening
|
||||
@Column
|
||||
private LocalDateTime unlockTime;
|
||||
@Column
|
||||
private String currentTask;
|
||||
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasksInQueue;
|
||||
|
||||
@Column
|
||||
private String unlockCode;
|
||||
|
||||
public UUID getLockId() {
|
||||
return lockId;
|
||||
}
|
||||
|
||||
public void setLockId(UUID lockId) {
|
||||
this.lockId = lockId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public UUID getLockee() {
|
||||
return lockee;
|
||||
@@ -44,6 +106,14 @@ public class CardLockEntity {
|
||||
this.lockee = lockee;
|
||||
}
|
||||
|
||||
public UUID getKeyholder() {
|
||||
return keyholder;
|
||||
}
|
||||
|
||||
public void setKeyholder(UUID keyholder) {
|
||||
this.keyholder = keyholder;
|
||||
}
|
||||
|
||||
public List<CardEnum> getInitialCards() {
|
||||
return initialCards;
|
||||
}
|
||||
@@ -188,4 +258,35 @@ public class CardLockEntity {
|
||||
this.tasksInQueue = tasksInQueue;
|
||||
}
|
||||
|
||||
public boolean isRequiresVerification() {
|
||||
return requiresVerification;
|
||||
}
|
||||
|
||||
public void setRequiresVerification(boolean requiresVerification) {
|
||||
this.requiresVerification = requiresVerification;
|
||||
}
|
||||
|
||||
public boolean isTestLock() {
|
||||
return testLock;
|
||||
}
|
||||
|
||||
public void setTestLock(boolean testLock) {
|
||||
this.testLock = testLock;
|
||||
}
|
||||
|
||||
public String getUnlockCode() {
|
||||
return unlockCode;
|
||||
}
|
||||
|
||||
public void setUnlockCode(String unlockCode) {
|
||||
this.unlockCode = unlockCode;
|
||||
}
|
||||
|
||||
public Integer getUnlockCodeLines() {
|
||||
return unlockCodeLines;
|
||||
}
|
||||
|
||||
public void setUnlockCodeLines(Integer unlockCodeLines) {
|
||||
this.unlockCodeLines = unlockCodeLines;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CardLockRepository extends JpaRepository<CardLockEntity, UUID> {
|
||||
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
public class CardlockRepository {
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CardlockRepository extends JpaRepository<CardLockEntity, UUID> {
|
||||
|
||||
List<CardLockEntity> findByLockee(UUID lockee);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public class DoubleUpCard implements Card {
|
||||
|
||||
@Override
|
||||
public CardDTO processCard(ProcessCardLock lock) {
|
||||
public CardDTO processCard(CardLockService lock) {
|
||||
return new CardDTO(lock.doubleUp(), "img/card_red.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public class FreezeCard implements Card {
|
||||
|
||||
@Override
|
||||
public CardDTO processCard(ProcessCardLock lock) {
|
||||
public CardDTO processCard(CardLockService lock) {
|
||||
return new CardDTO(lock.freeze(), "img/card_freeze.png");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public class GreenCard implements Card {
|
||||
|
||||
@Override
|
||||
public CardDTO processCard(ProcessCardLock lock) {
|
||||
public CardDTO processCard(CardLockService lock) {
|
||||
return new CardDTO(lock.unlock(), "img/card_green.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public class RedCard implements Card {
|
||||
|
||||
@Override
|
||||
public CardDTO processCard(ProcessCardLock lock) {
|
||||
public CardDTO processCard(CardLockService lock) {
|
||||
return new CardDTO(lock.redCard(), "img/card_red.png");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public class ResetCard implements Card {
|
||||
|
||||
@Override
|
||||
public CardDTO processCard(ProcessCardLock lock) {
|
||||
public CardDTO processCard(CardLockService lock) {
|
||||
return new CardDTO(lock.reset(), "img/card_reset.png");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public class TaskCard implements Card {
|
||||
|
||||
@Override
|
||||
public CardDTO processCard(ProcessCardLock lock) {
|
||||
public CardDTO processCard(CardLockService lock) {
|
||||
return new CardDTO(lock.task(), "img/card_task.png");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.ProcessCardLock;
|
||||
import de.oaa.xxx.games.chastity.CardLockService;
|
||||
|
||||
public class YellowCard implements Card {
|
||||
|
||||
@Override
|
||||
public CardDTO processCard(ProcessCardLock lock) {
|
||||
public CardDTO processCard(CardLockService lock) {
|
||||
return new CardDTO(lock.yellowCard(), "img/card_yellow.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import java.util.UUID;
|
||||
import de.oaa.xxx.games.chastity.LockType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@@ -14,6 +18,7 @@ import jakarta.persistence.Table;
|
||||
public class LockHistoryEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID historyId;
|
||||
@Column(nullable = false)
|
||||
@@ -22,6 +27,7 @@ public class LockHistoryEntity {
|
||||
private LocalDateTime startTime;
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime endTime;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private LockType type;
|
||||
@Column
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.games.chastity.tasks;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Converter
|
||||
public class TaskListConverter implements AttributeConverter<List<Task>, String> {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(List<Task> list) {
|
||||
if (list == null || list.isEmpty()) return null;
|
||||
try {
|
||||
return mapper.writeValueAsString(list);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> convertToEntityAttribute(String json) {
|
||||
if (json == null || json.isBlank()) return new ArrayList<>();
|
||||
try {
|
||||
return new ArrayList<>(mapper.readValue(json, new TypeReference<List<Task>>() {}));
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,8 @@ package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -24,7 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.aufgaben.controller.AboController;
|
||||
import de.oaa.xxx.games.chastity.CodeCreator;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@@ -32,15 +28,14 @@ import de.oaa.xxx.user.UserRepository;
|
||||
@Transactional
|
||||
public class VerificationController {
|
||||
|
||||
private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
|
||||
|
||||
private final VerificationRepository verificationRepository;
|
||||
private final VerificationVoteRepository verificationVoteRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public VerificationController(VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository, UserRepository userRepository,
|
||||
AboController aboController) {
|
||||
VerificationVoteRepository verificationVoteRepository, UserRepository userRepository) {
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
this.userRepository = userRepository;
|
||||
@@ -48,40 +43,36 @@ public class VerificationController {
|
||||
|
||||
@GetMapping("/{verificationId}")
|
||||
public ResponseEntity<VerificationDTO> get(@PathVariable UUID verificationId) {
|
||||
return verificationRepository.findById(verificationId).map(entity -> ResponseEntity.ok(entity.toVerification()))
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public ResponseEntity<List<VerificationDTO>> getAll(@RequestParam(value = "page", defaultValue = "1") int page,
|
||||
@RequestParam(value = "size", defaultValue = "10") int size) {
|
||||
var paging = PageRequest.of(page, size, Sort.by("verificationTime").descending());
|
||||
Page<VerificationEntity> result = verificationRepository.findAllByImageIsNotNull(paging);
|
||||
return ResponseEntity.ok(result.stream().map(entity -> entity.toVerification()).toList());
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public ResponseEntity<VerificationDTO> createVerification() {
|
||||
VerificationEntity verification = new VerificationEntity();
|
||||
verification.setVerficationId(UUID.randomUUID());
|
||||
verification.setCode(createCode());
|
||||
verification.setVerificationTime(LocalDateTime.now());
|
||||
verificationRepository.save(verification);
|
||||
return ResponseEntity.ok(verification.toVerification());
|
||||
}
|
||||
|
||||
@GetMapping("/{verificationId}")
|
||||
public ResponseEntity<VerificationDTO> getVerification(@PathVariable UUID verificationId) {
|
||||
Optional<VerificationEntity> optional = verificationRepository.findById(verificationId);
|
||||
var optional = verificationRepository.findById(verificationId);
|
||||
if (optional.isEmpty()) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
var dto = optional.get().toVerification();
|
||||
dto.votes().addAll(verificationVoteRepository.findAllByVerificationId(verificationId).stream()
|
||||
.map(entity -> entity.toVerificationVote()).collect(Collectors.toList()));
|
||||
verificationVoteRepository.findAllByVerificationId(verificationId).stream()
|
||||
.map(VerificationVoteEntity::toVerificationVote)
|
||||
.forEach(dto.votes()::add);
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public ResponseEntity<List<VerificationDTO>> getAll(
|
||||
@RequestParam(value = "page", defaultValue = "1") int page,
|
||||
@RequestParam(value = "size", defaultValue = "10") int size) {
|
||||
var paging = PageRequest.of(page, size, Sort.by("verificationTime").descending());
|
||||
Page<VerificationEntity> result = verificationRepository.findAllByImageIsNotNull(paging);
|
||||
return ResponseEntity.ok(result.stream().map(VerificationEntity::toVerification).toList());
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public ResponseEntity<VerificationDTO> createVerification() {
|
||||
var verification = new VerificationEntity();
|
||||
verification.setVerficationId(UUID.randomUUID());
|
||||
verification.setCode(CodeCreator.createAlphanumericCode(6));
|
||||
verification.setVerificationTime(LocalDateTime.now());
|
||||
verificationRepository.save(verification);
|
||||
return ResponseEntity.ok(verification.toVerification());
|
||||
}
|
||||
|
||||
@PutMapping("/{verificationId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID verificationId, @RequestBody VerificationDTO dto,
|
||||
Principal principal) {
|
||||
@@ -93,13 +84,11 @@ public class VerificationController {
|
||||
if (entity == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
var codeErstelltAm = entity.getVerificationTime();
|
||||
var vorEinerStunde = LocalDateTime.now().minusHours(1);
|
||||
if (codeErstelltAm.isBefore(vorEinerStunde)) {
|
||||
if (entity.getVerificationTime().isBefore(LocalDateTime.now().minusHours(1))) {
|
||||
return ResponseEntity.status(HttpStatus.GONE).build();
|
||||
}
|
||||
if (dto.image() != null) {
|
||||
entity.setImage(Base64.getDecoder().decode(dto.image()));
|
||||
entity.setImage(dto.image());
|
||||
}
|
||||
verificationRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
@@ -112,23 +101,16 @@ public class VerificationController {
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
var verification = verificationRepository.findById(verificationId).orElse(null);
|
||||
if (verification == null) {
|
||||
if (!verificationRepository.existsById(verificationId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
var vote = new VerificationVoteEntity();
|
||||
vote.setVoteId(UUID.randomUUID());
|
||||
vote.setUserId(dto.userId());
|
||||
vote.setVerificationId(verificationId);
|
||||
vote.setUserId(user.getUserId());
|
||||
vote.setUpvote(dto.upvote());
|
||||
verificationVoteRepository.save(vote);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
private String createCode() {
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(CHARS.charAt(new Random().nextInt(CHARS.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
@@ -15,39 +16,56 @@ public class VerificationEntity {
|
||||
@Id
|
||||
@Column
|
||||
private UUID verficationId;
|
||||
@Column
|
||||
private UUID lockId;
|
||||
@Column(nullable = false)
|
||||
private String code;
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime verificationTime;
|
||||
@Column(columnDefinition = "BLOB")
|
||||
@Column(columnDefinition = "BLOB")
|
||||
private byte[] image;
|
||||
|
||||
|
||||
public UUID getVerficationId() {
|
||||
return verficationId;
|
||||
}
|
||||
|
||||
public void setVerficationId(UUID verficationId) {
|
||||
this.verficationId = verficationId;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public LocalDateTime getVerificationTime() {
|
||||
return verificationTime;
|
||||
}
|
||||
|
||||
public void setVerificationTime(LocalDateTime verificationTime) {
|
||||
this.verificationTime = verificationTime;
|
||||
}
|
||||
|
||||
public byte[] getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setImage(byte[] image) {
|
||||
this.image = image;
|
||||
}
|
||||
public VerificationDTO toVerification() {
|
||||
return new VerificationDTO(verficationId, code, verificationTime,image, null);
|
||||
|
||||
public UUID getLockId() {
|
||||
return lockId;
|
||||
}
|
||||
|
||||
public void setLockId(UUID lockId) {
|
||||
this.lockId = lockId;
|
||||
}
|
||||
|
||||
public VerificationDTO toVerification() {
|
||||
return new VerificationDTO(verficationId, code, verificationTime, image, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
public interface VerificationRepository extends JpaRepository<VerificationEntity, UUID> {
|
||||
|
||||
org.springframework.data.domain.Page<VerificationEntity> findAllByImageIsNotNull(Pageable pageable);
|
||||
|
||||
java.util.List<VerificationEntity> findByLockId(UUID lockId);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public class VerificationVoteEntity {
|
||||
@Column
|
||||
private UUID voteId;
|
||||
@Column(nullable = false)
|
||||
private UUID verificationId;
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
@Column(nullable = false)
|
||||
private boolean upvote;
|
||||
@@ -25,6 +27,12 @@ public class VerificationVoteEntity {
|
||||
public void setVoteId(UUID voteId) {
|
||||
this.voteId = voteId;
|
||||
}
|
||||
public UUID getVerificationId() {
|
||||
return verificationId;
|
||||
}
|
||||
public void setVerificationId(UUID verificationId) {
|
||||
this.verificationId = verificationId;
|
||||
}
|
||||
public UUID getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import de.oaa.xxx.social.entity.KommentarEntity;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -18,6 +20,8 @@ import java.util.*;
|
||||
@RequestMapping("/gruppen")
|
||||
public class GruppeController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(GruppeController.class);
|
||||
|
||||
private final GruppeRepository gruppeRepository;
|
||||
private final GruppenmitgliedRepository mitgliedRepository;
|
||||
private final BeitrittsanfrageRepository anfrageRepository;
|
||||
@@ -137,6 +141,7 @@ public class GruppeController {
|
||||
admin.setRolle(GruppenRolle.ADMIN);
|
||||
admin.setJoinedAt(LocalDateTime.now());
|
||||
mitgliedRepository.save(admin);
|
||||
LOGGER.info("User {} hat Gruppe '{}' ({}) erstellt", myId, gruppe.getName(), gruppe.getGruppeId());
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(gruppe, myId));
|
||||
}
|
||||
@@ -161,6 +166,7 @@ public class GruppeController {
|
||||
if (req.bild() != null) gruppe.setBild(req.bild());
|
||||
if (req.isPrivate() != null) gruppe.setPrivate(req.isPrivate());
|
||||
gruppeRepository.save(gruppe);
|
||||
LOGGER.debug("User {} hat Gruppe {} aktualisiert", myId, id);
|
||||
|
||||
return ResponseEntity.ok(toDto(gruppe, myId));
|
||||
}
|
||||
@@ -195,6 +201,7 @@ public class GruppeController {
|
||||
anfrageRepository.deleteByGruppeId(id);
|
||||
mitgliedRepository.deleteByGruppeId(id);
|
||||
gruppeRepository.deleteById(id);
|
||||
LOGGER.info("User {} hat Gruppe {} gelöscht", myId, id);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
@@ -229,6 +236,7 @@ public class GruppeController {
|
||||
anfrage.setAngefragtAt(LocalDateTime.now());
|
||||
anfrage.setStatus(AnfrageStatus.AUSSTEHEND);
|
||||
anfrageRepository.save(anfrage);
|
||||
LOGGER.info("User {} hat Beitrittsanfrage für private Gruppe {} gestellt", myId, id);
|
||||
return ResponseEntity.status(201).build();
|
||||
} else {
|
||||
GruppenmitgliedEntity mitglied = new GruppenmitgliedEntity();
|
||||
@@ -238,6 +246,7 @@ public class GruppeController {
|
||||
mitglied.setRolle(GruppenRolle.MITGLIED);
|
||||
mitglied.setJoinedAt(LocalDateTime.now());
|
||||
mitgliedRepository.save(mitglied);
|
||||
LOGGER.info("User {} ist Gruppe {} beigetreten", myId, id);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
}
|
||||
@@ -250,6 +259,7 @@ public class GruppeController {
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
mitgliedRepository.deleteByGruppeIdAndUserId(id, myId);
|
||||
LOGGER.info("User {} hat Gruppe {} verlassen", myId, id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -290,6 +300,7 @@ public class GruppeController {
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
mitgliedRepository.deleteByGruppeIdAndUserId(id, userId);
|
||||
LOGGER.warn("Admin {} hat User {} aus Gruppe {} entfernt", myId, userId, id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -307,6 +318,7 @@ public class GruppeController {
|
||||
if (m.isEmpty()) return ResponseEntity.notFound().build();
|
||||
m.get().setRolle(GruppenRolle.ADMIN);
|
||||
mitgliedRepository.save(m.get());
|
||||
LOGGER.info("Admin {} hat User {} in Gruppe {} zum Admin befördert", myId, userId, id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -347,6 +359,7 @@ public class GruppeController {
|
||||
BeitrittsanfrageEntity anfrage = anfOpt.get();
|
||||
anfrage.setStatus(AnfrageStatus.GENEHMIGT);
|
||||
anfrageRepository.save(anfrage);
|
||||
LOGGER.info("Admin {} hat Beitrittsanfrage {} (User: {}) für Gruppe {} genehmigt", myId, reqId, anfrage.getUserId(), id);
|
||||
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, anfrage.getUserId()).isEmpty()) {
|
||||
GruppenmitgliedEntity mitglied = new GruppenmitgliedEntity();
|
||||
@@ -376,6 +389,7 @@ public class GruppeController {
|
||||
BeitrittsanfrageEntity anfrage = anfOpt.get();
|
||||
anfrage.setStatus(AnfrageStatus.ABGELEHNT);
|
||||
anfrageRepository.save(anfrage);
|
||||
LOGGER.debug("Admin {} hat Beitrittsanfrage {} für Gruppe {} abgelehnt", myId, reqId, id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -443,6 +457,7 @@ public class GruppeController {
|
||||
anfrageRepository.findByGruppeIdAndUserId(id, myId).ifPresent(a -> {
|
||||
if (a.getStatus() == AnfrageStatus.AUSSTEHEND) {
|
||||
anfrageRepository.delete(a);
|
||||
LOGGER.debug("User {} hat eigene Beitrittsanfrage für Gruppe {} zurückgezogen", myId, id);
|
||||
}
|
||||
});
|
||||
return ResponseEntity.noContent().build();
|
||||
|
||||
@@ -6,6 +6,8 @@ import de.oaa.xxx.gruppe.repository.*;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -18,6 +20,8 @@ import java.util.*;
|
||||
@RestController
|
||||
public class GruppenbeitragController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(GruppenbeitragController.class);
|
||||
|
||||
private final GruppeRepository gruppeRepository;
|
||||
private final GruppenmitgliedRepository mitgliedRepository;
|
||||
private final GruppenbeitragRepository beitragRepository;
|
||||
@@ -118,6 +122,7 @@ public class GruppenbeitragController {
|
||||
beitrag.setBilder(req.bilder() != null ? req.bilder() : List.of());
|
||||
beitrag.setCreatedAt(LocalDateTime.now());
|
||||
beitragRepository.save(beitrag);
|
||||
LOGGER.debug("User {} hat Beitrag {} (Typ: {}) in Gruppe {} erstellt", myId, beitrag.getBeitragId(), typ, id);
|
||||
|
||||
if (typ == BeitragTyp.UMFRAGE && req.optionen() != null) {
|
||||
for (int i = 0; i < req.optionen().size(); i++) {
|
||||
@@ -155,6 +160,7 @@ public class GruppenbeitragController {
|
||||
if (!isAuthor && !isAdmin) return ResponseEntity.status(403).build();
|
||||
|
||||
deleteBeitragCascade(beitrag);
|
||||
LOGGER.info("User {} hat Beitrag {} aus Gruppe {} gelöscht", myId, postId, id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -172,6 +178,7 @@ public class GruppenbeitragController {
|
||||
var existing = likeRepository.findByBeitragIdAndUserId(postId, myId);
|
||||
if (existing.isPresent()) {
|
||||
likeRepository.delete(existing.get());
|
||||
LOGGER.debug("User {} hat Like auf Beitrag {} entfernt", myId, postId);
|
||||
} else {
|
||||
GruppenbeitragLikeEntity like = new GruppenbeitragLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
@@ -179,6 +186,7 @@ public class GruppenbeitragController {
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
likeRepository.save(like);
|
||||
LOGGER.debug("User {} hat Beitrag {} geliked", myId, postId);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
@@ -224,6 +232,7 @@ public class GruppenbeitragController {
|
||||
stimme.setBeitragId(postId);
|
||||
stimme.setUserId(myId);
|
||||
stimmeRepository.save(stimme);
|
||||
LOGGER.debug("User {} hat für Option {} in Beitrag {} gestimmt", myId, req.optionId(), postId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -251,6 +260,7 @@ public class GruppenbeitragController {
|
||||
meldung.setGrund(req.grund());
|
||||
meldung.setGemeldetAt(LocalDateTime.now());
|
||||
meldungRepository.save(meldung);
|
||||
LOGGER.warn("User {} hat Beitrag {} in Gruppe {} gemeldet (Grund: {})", myId, postId, id, req.grund());
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@@ -292,6 +302,7 @@ public class GruppenbeitragController {
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
meldungRepository.deleteById(meldungId);
|
||||
LOGGER.debug("Admin {} hat Meldung {} in Gruppe {} abgewiesen", myId, meldungId, id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,55 @@ public class MailTemplateService {
|
||||
);
|
||||
}
|
||||
|
||||
public String buildKeyholderInvitationMail(String keyholderName, String lockeeName, String lockName, String confirmLink) {
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<body style="margin:0; padding:2rem; background:%s; font-family:'Segoe UI',Arial,sans-serif; color:%s;">
|
||||
<div style="max-width:460px; margin:0 auto; background:%s; border:1px solid %s; border-radius:12px; padding:2.5rem; box-shadow:0 8px 32px rgba(0,0,0,0.5);">
|
||||
|
||||
<h1 style="color:%s; text-align:center; margin:0 0 1.5rem 0; font-size:1.6rem;">XXX The Game</h1>
|
||||
|
||||
<p style="color:%s; margin:0 0 0.75rem 0;">Moin %s,</p>
|
||||
<p style="color:%s; margin:0 0 0.5rem 0;">
|
||||
<strong style="color:%s;">%s</strong> hat dich als Keyholder*In für das Chastity-Lock
|
||||
<strong style="color:%s;">%s</strong> eingetragen.
|
||||
</p>
|
||||
<p style="color:%s; margin:0 0 2rem 0;">
|
||||
Wenn du die Keyholder*In-Rolle annehmen möchtest, klicke auf den Button. Das Lock startet erst nach deiner Bestätigung mit dir als Keyholder*In.
|
||||
</p>
|
||||
|
||||
<div style="text-align:center; margin:0 0 2rem 0;">
|
||||
<a href="%s"
|
||||
style="display:inline-block; padding:0.75rem 2.5rem; background:%s; color:#ffffff;
|
||||
border-radius:6px; text-decoration:none; font-weight:600; font-size:1rem;">
|
||||
Keyholder*In-Rolle annehmen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr style="border:none; border-top:1px solid %s; margin:0 0 1.5rem 0;">
|
||||
|
||||
<p style="color:%s; font-size:0.85em; margin:0;">
|
||||
Falls du die Rolle nicht annehmen möchtest, kannst du diese E-Mail einfach ignorieren.
|
||||
Das Lock läuft dann als Self-Lock weiter.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""".formatted(
|
||||
colorBg, colorText,
|
||||
colorCard, colorSecondary,
|
||||
colorPrimary,
|
||||
colorText, keyholderName,
|
||||
colorText, colorPrimary, lockeeName,
|
||||
colorPrimary, lockName,
|
||||
colorText,
|
||||
confirmLink, colorPrimary,
|
||||
colorSecondary,
|
||||
colorMuted
|
||||
);
|
||||
}
|
||||
|
||||
public String buildActivationMail(String name, String activationLink, String activatePageUrl, String uuid) {
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -48,6 +48,7 @@ public class SperreController {
|
||||
public ResponseEntity<Void> sperren(@RequestBody SperreCallback callback) {
|
||||
try {
|
||||
new SperreVerarbeiten().sperreAnwenden(callback, sessionRepository, mitspielerRepository, aktiveSperreRepository);
|
||||
LOGGER.info("Sperre angewandt für Session {}", callback.getSessionId());
|
||||
return ResponseEntity.status(201).build();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
@@ -101,6 +102,7 @@ public class SperreController {
|
||||
List<AktiveSperreEntity> aktiveLocks = aktiveSperreRepository.findAktiveLocks(callback.getSpielerId());
|
||||
SperreVerarbeiten verarbeiten = new SperreVerarbeiten();
|
||||
aktiveLocks.forEach(lock -> verarbeiten.sperreVerlaengern(lock, callback.getFaktor(), aktiveSperreRepository));
|
||||
LOGGER.debug("Sperren für Spieler {} um Faktor {} verlängert ({} Sperren)", callback.getSpielerId(), callback.getFaktor(), aktiveLocks.size());
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
|
||||
@@ -7,6 +7,8 @@ import de.oaa.xxx.social.repository.KommentarLikeRepository;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -19,6 +21,8 @@ import java.util.UUID;
|
||||
@RequestMapping("/social/kommentare")
|
||||
public class KommentarController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KommentarController.class);
|
||||
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final KommentarLikeRepository likeRepository;
|
||||
private final UserRepository userRepository;
|
||||
@@ -70,6 +74,7 @@ public class KommentarController {
|
||||
entity.setText(request.text().trim());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
kommentarRepository.save(entity);
|
||||
LOGGER.debug("User {} hat Kommentar {} auf {} {} erstellt", myId, entity.getKommentarId(), request.targetType(), request.targetId());
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(entity, myId));
|
||||
}
|
||||
@@ -93,6 +98,7 @@ public class KommentarController {
|
||||
}
|
||||
likeRepository.deleteByKommentarId(kommentarId);
|
||||
kommentarRepository.delete(kOpt.get());
|
||||
LOGGER.debug("User {} hat Kommentar {} gelöscht", myId, kommentarId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -107,6 +113,7 @@ public class KommentarController {
|
||||
var existing = likeRepository.findByKommentarIdAndUserId(kommentarId, myId);
|
||||
if (existing.isPresent()) {
|
||||
likeRepository.delete(existing.get());
|
||||
LOGGER.debug("User {} hat Like auf Kommentar {} entfernt", myId, kommentarId);
|
||||
} else {
|
||||
KommentarLikeEntity like = new KommentarLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
@@ -114,6 +121,7 @@ public class KommentarController {
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
likeRepository.save(like);
|
||||
LOGGER.debug("User {} hat Kommentar {} geliked", myId, kommentarId);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import de.oaa.xxx.social.repository.PinnwandEintragRepository;
|
||||
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -20,6 +22,8 @@ import java.util.UUID;
|
||||
@RequestMapping("/social/pinnwand")
|
||||
public class PinnwandController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PinnwandController.class);
|
||||
|
||||
private final PinnwandEintragRepository eintragRepository;
|
||||
private final PinnwandLikeRepository likeRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
@@ -67,6 +71,7 @@ public class PinnwandController {
|
||||
entity.setText(request.text().trim());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
eintragRepository.save(entity);
|
||||
LOGGER.debug("User {} hat Pinnwand-Eintrag {} auf Profil {} erstellt", myId, entity.getEintragId(), request.profilUserId());
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(entity, myId));
|
||||
}
|
||||
@@ -91,6 +96,7 @@ public class PinnwandController {
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", eintragId)
|
||||
.forEach(k -> kommentarRepository.deleteById(k.getKommentarId()));
|
||||
eintragRepository.delete(eintrag);
|
||||
LOGGER.debug("User {} hat Pinnwand-Eintrag {} gelöscht", myId, eintragId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -105,6 +111,7 @@ public class PinnwandController {
|
||||
var existing = likeRepository.findByEintragIdAndUserId(eintragId, myId);
|
||||
if (existing.isPresent()) {
|
||||
likeRepository.delete(existing.get());
|
||||
LOGGER.debug("User {} hat Like auf Pinnwand-Eintrag {} entfernt", myId, eintragId);
|
||||
} else {
|
||||
PinnwandLikeEntity like = new PinnwandLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
@@ -112,6 +119,7 @@ public class PinnwandController {
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
likeRepository.save(like);
|
||||
LOGGER.debug("User {} hat Pinnwand-Eintrag {} geliked", myId, eintragId);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import de.oaa.xxx.social.entity.ProfileImageLikeEntity;
|
||||
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -18,6 +20,7 @@ import java.util.UUID;
|
||||
@RequestMapping("/social/profile-images")
|
||||
public class ProfileImageController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ProfileImageController.class);
|
||||
private static final int MAX_IMAGES_PER_USER = 20;
|
||||
|
||||
private final ProfileImageRepository profileImageRepository;
|
||||
@@ -53,6 +56,7 @@ public class ProfileImageController {
|
||||
entity.setImageData(request.imageData());
|
||||
entity.setUploadedAt(LocalDateTime.now());
|
||||
profileImageRepository.save(entity);
|
||||
LOGGER.debug("User {} hat Profilbild {} hochgeladen", myId, entity.getImageId());
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(entity, myId));
|
||||
}
|
||||
@@ -80,6 +84,7 @@ public class ProfileImageController {
|
||||
|
||||
profileImageLikeRepository.deleteByImageId(imageId);
|
||||
profileImageRepository.delete(imgOpt.get());
|
||||
LOGGER.info("User {} hat Profilbild {} gelöscht", myId, imageId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -94,6 +99,7 @@ public class ProfileImageController {
|
||||
var existing = profileImageLikeRepository.findByImageIdAndUserId(imageId, myId);
|
||||
if (existing.isPresent()) {
|
||||
profileImageLikeRepository.delete(existing.get());
|
||||
LOGGER.debug("User {} hat Like auf Profilbild {} entfernt", myId, imageId);
|
||||
} else {
|
||||
ProfileImageLikeEntity like = new ProfileImageLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
@@ -101,6 +107,7 @@ public class ProfileImageController {
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
profileImageLikeRepository.save(like);
|
||||
LOGGER.debug("User {} hat Profilbild {} geliked", myId, imageId);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.social.repository.MessageRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -23,6 +25,8 @@ import java.util.*;
|
||||
@RequestMapping("/social")
|
||||
public class SocialController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SocialController.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final FriendshipRepository friendshipRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
@@ -86,6 +90,7 @@ public class SocialController {
|
||||
f.setStatus(Status.PENDING);
|
||||
f.setCreatedAt(LocalDateTime.now());
|
||||
friendshipRepository.save(f);
|
||||
LOGGER.info("User {} hat Freundschaftsanfrage an User {} gesendet", myId, body.receiverId());
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
@@ -102,6 +107,7 @@ public class SocialController {
|
||||
|
||||
f.setStatus(Status.ACCEPTED);
|
||||
friendshipRepository.save(f);
|
||||
LOGGER.info("User {} hat Freundschaftsanfrage {} angenommen", myId, body.friendshipId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -118,6 +124,7 @@ public class SocialController {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
friendshipRepository.delete(f);
|
||||
LOGGER.info("User {} hat Freundschaft/Anfrage {} gelöscht", myId, body.friendshipId());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -202,6 +209,7 @@ public class SocialController {
|
||||
msg.setText(body.text().trim());
|
||||
msg.setSentAt(LocalDateTime.now());
|
||||
messageRepository.save(msg);
|
||||
LOGGER.debug("User {} hat Nachricht an User {} gesendet", myId, body.receiverId());
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ public class UserController {
|
||||
user.get().setProfilePicture(request.picture());
|
||||
user.get().setProfilePictureHq(request.pictureHq());
|
||||
userRepository.save(user.get());
|
||||
LOGGER.debug("User {} hat Profilbild aktualisiert", user.get().getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -139,6 +140,7 @@ public class UserController {
|
||||
user.setBeziehungsstatus(request.beziehungsstatus());
|
||||
user.setBeschreibung(request.beschreibung());
|
||||
userRepository.save(user);
|
||||
LOGGER.info("User {} hat Profil aktualisiert", user.getUserId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -153,6 +155,7 @@ public class UserController {
|
||||
if (user.isEmpty()) return ResponseEntity.status(401).build();
|
||||
user.get().setName(newName);
|
||||
userRepository.save(user.get());
|
||||
LOGGER.info("User {} hat Namen zu '{}' geändert", user.get().getUserId(), newName);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -223,207 +223,6 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* ── Like button (inline) ── */
|
||||
.btn-like {
|
||||
background: none;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 20px;
|
||||
padding: 0.2rem 0.65rem;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-like:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
.btn-like.liked { border-color: var(--color-primary); color: var(--color-primary); }
|
||||
|
||||
/* ── Lightbox ── */
|
||||
.lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.88);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.lightbox.open { display: flex; }
|
||||
|
||||
.lightbox-close {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(0,0,0,0.55);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 502;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.lightbox-close:hover { background: rgba(180,30,30,0.8); }
|
||||
|
||||
/* Layout wrapper – fixed size */
|
||||
.lb-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
height: min(78vh, 660px);
|
||||
max-width: calc(100vw - 4rem);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Image side – fixed preferred width so all images show at the same size */
|
||||
.lb-image-side {
|
||||
width: 660px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lb-image-box {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#lightboxImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Arrow + like overlay on image bottom */
|
||||
.lb-image-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.6));
|
||||
border-radius: 0 0 12px 12px;
|
||||
padding: 2rem 0.75rem 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.lb-nav-btn {
|
||||
background: rgba(0,0,0,0.35);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
padding: 0.3rem 0.75rem;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.lb-nav-btn:hover { background: rgba(0,0,0,0.65); }
|
||||
.lb-nav-btn:disabled { opacity: 0.25; cursor: default; }
|
||||
|
||||
.lb-overlay-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lb-counter {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255,255,255,0.75);
|
||||
}
|
||||
|
||||
/* Comments panel – same height as image box */
|
||||
.lb-comments-panel {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lb-comments-header {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.7rem 1rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lb-comments-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.65rem 0.75rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-secondary) transparent;
|
||||
}
|
||||
|
||||
.lb-comment-write {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lb-comment-write input {
|
||||
flex: 1;
|
||||
padding: 0.4rem 0.65rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.lb-comment-write button {
|
||||
width: auto;
|
||||
padding: 0.4rem 0.7rem;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Narrow layout – toggled by JS reading --breakpoint-mobile */
|
||||
.lb-layout.narrow {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
width: calc(100vw - 1rem);
|
||||
max-width: calc(100vw - 1rem);
|
||||
}
|
||||
.lb-layout.narrow .lb-image-side { width: 100%; flex-shrink: 0; }
|
||||
.lb-layout.narrow .lb-image-box { height: min(45vh, 360px); flex: none; }
|
||||
.lb-layout.narrow .lb-comments-panel { width: 100%; max-height: 40vh; flex-shrink: 0; }
|
||||
|
||||
/* ── Pinnwand ── */
|
||||
.pinnwand-write {
|
||||
display: flex;
|
||||
@@ -504,77 +303,13 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
.btn-text:hover { color: var(--color-text); }
|
||||
|
||||
.btn-delete-small {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(200,50,50,0.6);
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
.btn-delete-small:hover { color: var(--color-primary); }
|
||||
|
||||
/* ── Comments ── */
|
||||
/* ── Comments (section container) ── */
|
||||
.comments-section {
|
||||
margin-top: 0.65rem;
|
||||
padding-top: 0.65rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
flex: 1;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.65rem;
|
||||
}
|
||||
|
||||
.comment-author { font-size: 0.8rem; font-weight: 600; color: var(--color-text); }
|
||||
.comment-time { font-size: 0.7rem; color: var(--color-muted); margin-left: 0.4rem; }
|
||||
.comment-text { font-size: 0.85rem; color: rgba(255,255,255,0.75); margin-top: 0.2rem; line-height: 1.45; white-space: pre-wrap; }
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.3rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.replies-section {
|
||||
margin-top: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 2px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
.comment-write {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.comment-write input { flex: 1; padding: 0.4rem 0.75rem; font-size: 0.85rem; }
|
||||
.comment-write button { width: auto; padding: 0.4rem 0.75rem; font-size: 0.82rem; white-space: nowrap; }
|
||||
|
||||
/* ── Profil Tabs ── */
|
||||
.profil-tabs { display:flex; gap:0; margin-bottom:1.25rem; border-bottom:1px solid var(--color-secondary); }
|
||||
.profil-tab-btn { background:none; border:none; border-bottom:3px solid transparent; border-radius:0; padding:0.6rem 1.25rem; font-size:0.9rem; font-weight:600; color:var(--color-muted); cursor:pointer; margin-bottom:-1px; transition:color 0.15s,border-color 0.15s; }
|
||||
@@ -591,14 +326,11 @@
|
||||
.post-author { font-weight:600; font-size:0.9rem; }
|
||||
.post-date { font-size:0.75rem; color:var(--color-muted); margin-left:auto; }
|
||||
.post-text { font-size:0.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
|
||||
.post-bild { width:100%; max-height:360px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
|
||||
.post-carousel { position:relative; margin-top:0.5rem; }
|
||||
.car-slide { display:none; }
|
||||
.car-slide.active { display:block; }
|
||||
.car-btn { position:absolute; top:50%; transform:translateY(-50%); background:rgba(0,0,0,0.55); border:none; color:#fff; font-size:2.2rem; width:auto; min-width:2.4rem; height:3.2rem; border-radius:8px; cursor:pointer; z-index:5; display:flex; align-items:center; justify-content:center; padding:0 0.5rem; margin:0; line-height:1; }
|
||||
.car-prev { left:0.3rem; }
|
||||
.car-next { right:0.3rem; }
|
||||
.car-indicator { text-align:center; font-size:0.75rem; color:var(--color-muted); margin-top:0.25rem; }
|
||||
.post-bild { width:100%; max-height:360px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; transition:opacity 0.2s; }
|
||||
.post-bild-wrap { position:relative; cursor:pointer; display:block; }
|
||||
.post-bild-wrap:hover .post-bild { opacity:0.82; }
|
||||
.post-bild-hover-icon { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; opacity:0; transition:opacity 0.2s; pointer-events:none; font-size:1.6rem; }
|
||||
.post-bild-wrap:hover .post-bild-hover-icon { opacity:1; }
|
||||
.post-actions { display:flex; gap:1rem; margin-top:0.75rem; align-items:center; flex-wrap:wrap; }
|
||||
.post-action-btn { background:none; border:none; color:var(--color-muted); cursor:pointer; font-size:0.85rem; padding:0; display:flex; align-items:center; gap:0.3rem; margin:0; width:auto; }
|
||||
.post-action-btn:hover { color:var(--color-primary); background:none; }
|
||||
@@ -612,18 +344,23 @@
|
||||
.umfrage-bar-content { position:relative; display:flex; justify-content:space-between; padding:0.45rem 0.75rem; font-size:0.88rem; }
|
||||
.umfrage-total { font-size:0.78rem; color:var(--color-muted); margin-top:0.3rem; }
|
||||
|
||||
/* ── Post Lightbox ── */
|
||||
.post-lb { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:400; align-items:center; justify-content:center; }
|
||||
.post-lb.open { display:flex; }
|
||||
.post-lb-layout { display:flex; max-width:860px; width:95vw; max-height:88vh; background:var(--color-card); border-radius:12px; overflow:hidden; position:relative; }
|
||||
.post-lb-close { position:absolute; top:0.6rem; right:0.6rem; background:rgba(0,0,0,0.55); border:none; color:#fff; font-size:1.1rem; width:2rem; height:2rem; border-radius:50%; cursor:pointer; z-index:10; display:flex; align-items:center; justify-content:center; padding:0; margin:0; }
|
||||
.post-lb-body { flex:1; overflow-y:auto; padding:1.25rem; border-right:1px solid var(--color-secondary); min-width:0; }
|
||||
.post-lb-comments { width:300px; flex-shrink:0; display:flex; flex-direction:column; }
|
||||
.post-lb-comments-list { flex:1; overflow-y:auto; padding:0.75rem; }
|
||||
.post-lb-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; gap:0.5rem; }
|
||||
.post-lb-compose input { flex:1; font-size:0.85rem; padding:0.35rem 0.6rem; height:auto; }
|
||||
.post-lb-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
|
||||
@media(max-width:650px) { .post-lb-layout { flex-direction:column; max-height:95vh; } .post-lb-body { border-right:none; border-bottom:1px solid var(--color-secondary); } .post-lb-comments { width:100%; } }
|
||||
/* ── Post / Bild Lightbox ── */
|
||||
.lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:400; align-items:center; justify-content:center; }
|
||||
.lightbox.open { display:flex; }
|
||||
.lb-layout { display:flex; max-width:920px; width:95vw; max-height:90vh; background:var(--color-card); border-radius:12px; overflow:hidden; position:relative; }
|
||||
.lb-close { position:absolute; top:0.6rem; right:0.6rem; background:rgba(0,0,0,0.55); border:none; color:#fff; font-size:1.1rem; width:2rem; height:2rem; border-radius:50%; cursor:pointer; z-index:10; display:flex; align-items:center; justify-content:center; padding:0; margin:0; }
|
||||
.lb-post-side { flex:1; overflow-y:auto; padding:1.25rem; border-right:1px solid var(--color-secondary); min-width:0; }
|
||||
.lb-comments-panel { width:300px; flex-shrink:0; display:flex; flex-direction:column; }
|
||||
.lb-comments-header { font-size:0.78rem; font-weight:600; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.06em; padding:0.7rem 1rem; border-bottom:1px solid var(--color-secondary); flex-shrink:0; }
|
||||
.lb-comments-list { flex:1; overflow-y:auto; padding:0.75rem; }
|
||||
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; flex-direction:column; gap:0.5rem; flex-shrink:0; }
|
||||
.lb-comment-compose textarea { width:100%; font-size:0.85rem; padding:0.35rem 0.6rem; resize:none; background:var(--color-secondary); border:1px solid var(--color-secondary); border-radius:6px; color:var(--color-text); font-family:inherit; outline:none; transition:border-color 0.2s; box-sizing:border-box; }
|
||||
.lb-comment-compose textarea:focus { border-color:var(--color-primary); }
|
||||
.lb-comment-compose-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
|
||||
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
|
||||
.lb-img-nav { display:flex; gap:0.75rem; align-items:center; justify-content:center; margin-top:0.75rem; flex-wrap:wrap; }
|
||||
.compose-action-btn { background:none; border:1px solid var(--color-secondary); color:var(--color-muted); border-radius:6px; padding:0.35rem 0.6rem; font-size:0.95rem; cursor:pointer; margin:0; width:auto; }
|
||||
@media (max-width:650px) { .lb-layout { flex-direction:column; max-height:95vh; } .lb-post-side { border-right:none; border-bottom:1px solid var(--color-secondary); max-height:55vh; } .lb-comments-panel { width:100%; } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -694,60 +431,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox -->
|
||||
<div class="lightbox" id="lightbox">
|
||||
<button class="lightbox-close" onclick="closeLightbox()">✕</button>
|
||||
|
||||
<!-- Post / Bild Lightbox -->
|
||||
<div class="lightbox" id="postLightbox">
|
||||
<div class="lb-layout">
|
||||
<!-- Image side -->
|
||||
<div class="lb-image-side">
|
||||
<div class="lb-image-box">
|
||||
<img id="lightboxImg" src="" alt="">
|
||||
<div class="lb-image-overlay">
|
||||
<button class="lb-nav-btn" id="lbPrev" onclick="galleryPrev()" style="display:none;">←</button>
|
||||
<div class="lb-overlay-center">
|
||||
<span class="lb-counter" id="lbCounter" style="display:none;"></span>
|
||||
<button class="btn-like" id="lbLikeBtn" onclick="toggleGalleryLike()" style="display:none;">
|
||||
♥ <span id="lbLikeCount">0</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="lb-nav-btn" id="lbNext" onclick="galleryNext()" style="display:none;">→</button>
|
||||
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||
<div class="lb-post-side" id="lbPostBody"></div>
|
||||
<div class="lb-comments-panel">
|
||||
<div class="lb-comments-header">Kommentare</div>
|
||||
<div class="lb-comments-list" id="lbCommentsList"></div>
|
||||
<div class="lb-comment-compose">
|
||||
<textarea id="lbCommentInput" placeholder="Kommentar schreiben…" maxlength="500" rows="3"
|
||||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();postLbComment()}"></textarea>
|
||||
<div class="lb-comment-compose-actions">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
||||
<button onclick="postLbComment()">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments panel (always visible for gallery images) -->
|
||||
<div class="lb-comments-panel" id="lbCommentsPanel" style="display:none;">
|
||||
<div class="lb-comments-header">Kommentare</div>
|
||||
<div class="lb-comments-list" id="lbComments"></div>
|
||||
<div class="lb-comment-write" style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<input type="text" id="lbCommentInput" placeholder="Kommentar schreiben…" maxlength="500"
|
||||
onkeydown="if(event.key==='Enter') postLbComment()" style="flex:1;">
|
||||
<button type="button" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.35rem 0.6rem;font-size:0.95rem;cursor:pointer;margin:0;width:auto;">😊</button>
|
||||
<button onclick="postLbComment()">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Lightbox -->
|
||||
<div class="post-lb" id="postLightbox">
|
||||
<div class="post-lb-layout">
|
||||
<button class="post-lb-close" onclick="closePostLb()">✕</button>
|
||||
<div class="post-lb-body" id="postLbBody"></div>
|
||||
<div class="post-lb-comments">
|
||||
<div class="lb-comments-header" style="font-size:0.78rem;font-weight:600;color:var(--color-muted);text-transform:uppercase;letter-spacing:0.06em;padding:0.7rem 1rem;border-bottom:1px solid var(--color-secondary);">Kommentare</div>
|
||||
<div class="post-lb-comments-list" id="postLbComments"></div>
|
||||
<div class="post-lb-compose" style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<input type="text" id="postLbCommentInput" placeholder="Kommentar schreiben…" maxlength="500"
|
||||
onkeydown="if(event.key==='Enter') postLbFeedComment()" style="flex:1;">
|
||||
<button type="button" onclick="toggleEmojiPicker(this,'postLbCommentInput')" title="Emoji" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.35rem 0.6rem;font-size:0.95rem;cursor:pointer;margin:0;width:auto;">😊</button>
|
||||
<button onclick="postLbFeedComment()">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/image-viewer.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
@@ -766,14 +471,13 @@
|
||||
let friendsOffset = 0;
|
||||
const FRIENDS_PAGE = 5;
|
||||
|
||||
let lbMode = null; // 'avatar' | 'gallery'
|
||||
let lbImageIndex = 0;
|
||||
|
||||
// Posts tab state
|
||||
let postsPage = 0;
|
||||
let postsHasMore = true;
|
||||
let postsLoading = false;
|
||||
let activeLbPostId = null;
|
||||
let postsPage = 0;
|
||||
let postsHasMore = true;
|
||||
let postsLoading = false;
|
||||
let activeLbPostId = null;
|
||||
let activeLbMode = null; // 'post' | 'image'
|
||||
let activeLbImageIdx = null;
|
||||
|
||||
// ── Label maps ──
|
||||
const GESCHLECHT_LABEL = { WEIBLICH: 'weiblich', DIVERS: 'divers', MAENNLICH: 'männlich' };
|
||||
@@ -944,7 +648,7 @@
|
||||
const strip = document.getElementById('friendsStrip');
|
||||
strip.innerHTML = page.map(f => {
|
||||
const pic = f.profilePicture
|
||||
? `<img src="${f.profilePicture}" alt="${esc(f.name)}">`
|
||||
? `<img src="data:image/png;base64,${f.profilePicture}" alt="${esc(f.name)}">`
|
||||
: `<div class="friend-avatar-placeholder">👤</div>`;
|
||||
return `<div class="friend-thumb" onclick="window.location='/benutzer.html?userId=${f.userId}'">
|
||||
${pic}
|
||||
@@ -977,124 +681,57 @@
|
||||
img.likeCount += img.likedByMe ? 1 : -1;
|
||||
btn.className = 'btn-like' + (img.likedByMe ? ' liked' : '');
|
||||
btn.textContent = '♥ ' + img.likeCount;
|
||||
if (lbMode === 'gallery' && lbImageIndex === idx) syncLbLike();
|
||||
if (activeLbMode === 'image' && activeLbImageIdx === idx) renderLbImageBody();
|
||||
}
|
||||
|
||||
// ── Lightbox ──
|
||||
const lbLayout = () => document.querySelector('#lightbox .lb-layout');
|
||||
const mobileBP = () => parseInt(getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile').trim()) || 768;
|
||||
|
||||
function updateLbLayout() {
|
||||
const el = lbLayout();
|
||||
if (el) el.classList.toggle('narrow', window.innerWidth <= mobileBP());
|
||||
}
|
||||
window.addEventListener('resize', updateLbLayout);
|
||||
|
||||
// ── Bild-Lightbox (Avatar = imageViewer, Galerie = shared lightbox) ──
|
||||
function openAvatarLightbox() {
|
||||
if (!avatarSrc) return;
|
||||
lbMode = 'avatar';
|
||||
document.getElementById('lightboxImg').src = avatarSrc;
|
||||
document.getElementById('lbPrev').style.display = 'none';
|
||||
document.getElementById('lbNext').style.display = 'none';
|
||||
document.getElementById('lbCounter').style.display = 'none';
|
||||
document.getElementById('lbLikeBtn').style.display = 'none';
|
||||
document.getElementById('lbCommentsPanel').style.display = 'none';
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
updateLbLayout();
|
||||
imageViewer.open({ images: [{ src: avatarSrc }] });
|
||||
}
|
||||
|
||||
function renderLbImageBody() {
|
||||
const img = allImages[activeLbImageIdx];
|
||||
const total = allImages.length;
|
||||
const prevDisabled = activeLbImageIdx === 0 ? 'disabled' : '';
|
||||
const nextDisabled = activeLbImageIdx === total - 1 ? 'disabled' : '';
|
||||
const likeClass = img.likedByMe ? ' active' : '';
|
||||
document.getElementById('lbPostBody').innerHTML = `
|
||||
<img src="data:image/jpeg;base64,${img.imageData}" alt=""
|
||||
style="width:100%;max-height:360px;object-fit:contain;border-radius:8px;display:block;">
|
||||
<div class="lb-img-nav">
|
||||
<button class="post-action-btn" ${prevDisabled} onclick="lbGalleryNav(-1)">← Zurück</button>
|
||||
${!isOwnProfile ? `<button class="post-action-btn${likeClass}" id="lbImgLikeBtn" onclick="lbToggleImageLike()">
|
||||
♥ <span id="lbImgLikeCount">${img.likeCount}</span></button>` : ''}
|
||||
<span style="font-size:0.8rem;color:var(--color-muted);">${activeLbImageIdx + 1} / ${total}</span>
|
||||
<button class="post-action-btn" ${nextDisabled} onclick="lbGalleryNav(1)">Weiter →</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function openGallery(index) {
|
||||
lbMode = 'gallery';
|
||||
lbImageIndex = index;
|
||||
document.getElementById('lbPrev').style.display = '';
|
||||
document.getElementById('lbNext').style.display = '';
|
||||
document.getElementById('lbCounter').style.display = '';
|
||||
document.getElementById('lbCommentsPanel').style.display = '';
|
||||
if (!isOwnProfile) document.getElementById('lbLikeBtn').style.display = '';
|
||||
showLbImage();
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
updateLbLayout();
|
||||
activeLbMode = 'image';
|
||||
activeLbImageIdx = index;
|
||||
activeLbPostId = null;
|
||||
renderLbImageBody();
|
||||
loadLbComments();
|
||||
document.getElementById('postLightbox').classList.add('open');
|
||||
}
|
||||
|
||||
function showLbImage() {
|
||||
const img = allImages[lbImageIndex];
|
||||
document.getElementById('lightboxImg').src = 'data:image/jpeg;base64,' + img.imageData;
|
||||
document.getElementById('lbCounter').textContent = (lbImageIndex + 1) + ' / ' + allImages.length;
|
||||
document.getElementById('lbPrev').disabled = lbImageIndex === 0;
|
||||
document.getElementById('lbNext').disabled = lbImageIndex === allImages.length - 1;
|
||||
syncLbLike();
|
||||
function lbGalleryNav(dir) {
|
||||
activeLbImageIdx = Math.max(0, Math.min(activeLbImageIdx + dir, allImages.length - 1));
|
||||
renderLbImageBody();
|
||||
loadLbComments();
|
||||
}
|
||||
|
||||
function syncLbLike() {
|
||||
const img = allImages[lbImageIndex];
|
||||
const btn = document.getElementById('lbLikeBtn');
|
||||
btn.className = 'btn-like' + (img.likedByMe ? ' liked' : '');
|
||||
document.getElementById('lbLikeCount').textContent = img.likeCount;
|
||||
}
|
||||
|
||||
function galleryPrev() {
|
||||
if (lbImageIndex > 0) { lbImageIndex--; showLbImage(); }
|
||||
}
|
||||
|
||||
function galleryNext() {
|
||||
if (lbImageIndex < allImages.length - 1) { lbImageIndex++; showLbImage(); }
|
||||
}
|
||||
|
||||
async function toggleGalleryLike() {
|
||||
const img = allImages[lbImageIndex];
|
||||
async function lbToggleImageLike() {
|
||||
const img = allImages[activeLbImageIdx];
|
||||
await fetch('/social/profile-images/' + img.imageId + '/like', { method: 'POST' });
|
||||
img.likedByMe = !img.likedByMe;
|
||||
img.likeCount += img.likedByMe ? 1 : -1;
|
||||
syncLbLike();
|
||||
renderLbImageBody();
|
||||
renderGallery();
|
||||
try {
|
||||
await fetch('/social/profile-images/' + img.imageId + '/like', { method: 'POST' });
|
||||
} catch {
|
||||
img.likedByMe = !img.likedByMe;
|
||||
img.likeCount += img.likedByMe ? 1 : -1;
|
||||
syncLbLike();
|
||||
renderGallery();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLbComments() {
|
||||
const imageId = allImages[lbImageIndex].imageId;
|
||||
const res = await fetch('/social/kommentare?targetType=IMAGE&targetId=' + imageId);
|
||||
const comments = await res.json();
|
||||
document.getElementById('lbComments').innerHTML = comments.length === 0
|
||||
? '<p style="color:var(--color-muted);font-size:0.82rem;margin-bottom:0.4rem;">Noch keine Kommentare.</p>'
|
||||
: comments.map(k => renderKommentarHtml(k, 'IMAGE', imageId, true)).join('');
|
||||
}
|
||||
|
||||
async function postLbComment() {
|
||||
const input = document.getElementById('lbCommentInput');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
const imageId = allImages[lbImageIndex].imageId;
|
||||
await fetch('/social/kommentare', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ targetType: 'IMAGE', targetId: imageId, text })
|
||||
});
|
||||
input.value = '';
|
||||
await loadLbComments();
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
document.getElementById('lightbox').classList.remove('open');
|
||||
lbMode = null;
|
||||
}
|
||||
|
||||
document.getElementById('lightbox').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('lightbox')) closeLightbox();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!document.getElementById('lightbox').classList.contains('open')) return;
|
||||
if (e.key === 'Escape') closeLightbox();
|
||||
if (e.key === 'ArrowLeft') galleryPrev();
|
||||
if (e.key === 'ArrowRight') galleryNext();
|
||||
});
|
||||
|
||||
// ── Pinnwand ──
|
||||
async function loadPinnwand() {
|
||||
const res = await fetch('/social/pinnwand?userId=' + targetUserId);
|
||||
@@ -1166,13 +803,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// loadComments und renderKommentarHtml kommen aus shared.js
|
||||
async function loadComments(targetId, targetType) {
|
||||
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${targetId}`);
|
||||
const comments = await res.json();
|
||||
const section = document.getElementById('comments-' + targetId);
|
||||
section.innerHTML = (comments.length === 0
|
||||
? '<p style="color:var(--color-muted);font-size:0.82rem;margin-bottom:0.4rem;">Noch keine Kommentare.</p>'
|
||||
: comments.map(k => renderKommentarHtml(k, targetType, targetId, false)).join(''))
|
||||
: comments.map(k => renderKommentarHtml(k, targetType, targetId, { myUserId, showReplies: true })).join(''))
|
||||
+ `<div class="comment-write">
|
||||
<input type="text" id="ci-${targetId}" placeholder="Kommentar schreiben…" maxlength="500"
|
||||
onkeydown="if(event.key==='Enter') postComment('${targetId}','${targetType}')">
|
||||
@@ -1180,27 +818,6 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderKommentarHtml(k, targetType, targetId, inLb) {
|
||||
const avatarHtml = k.authorPicture ? `<img src="data:image/png;base64,${k.authorPicture}" alt="">` : '◉';
|
||||
const canDelete = k.authorId === myUserId;
|
||||
const replyLabel = k.replyCount > 0 ? `Antworten (${k.replyCount})` : 'Antworten';
|
||||
return `<div class="comment-item" id="kom-${k.kommentarId}">
|
||||
<div class="entry-avatar" style="width:26px;height:26px;font-size:0.85rem;">${avatarHtml}</div>
|
||||
<div class="comment-body">
|
||||
<span class="comment-author">${esc(k.authorName)}</span>
|
||||
<span class="comment-time">${fmtDate(k.createdAt)}</span>
|
||||
<div class="comment-text">${esc(k.text)}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="btn-like ${k.likedByMe ? 'liked' : ''}" id="like-k-${k.kommentarId}"
|
||||
onclick="toggleKommentarLike('${k.kommentarId}')">♥ <span id="lc-k-${k.kommentarId}">${k.likeCount}</span></button>
|
||||
<button class="btn-text" onclick="toggleReplies('${k.kommentarId}')">${replyLabel}</button>
|
||||
${canDelete ? `<button class="btn-delete-small" onclick="deleteKommentar('${k.kommentarId}','${targetType}','${targetId}',${inLb})">✕</button>` : ''}
|
||||
</div>
|
||||
<div class="replies-section" id="replies-${k.kommentarId}" style="display:none;"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function postComment(targetId, targetType) {
|
||||
const input = document.getElementById('ci-' + targetId);
|
||||
const text = input.value.trim();
|
||||
@@ -1213,81 +830,15 @@
|
||||
await loadComments(targetId, targetType);
|
||||
}
|
||||
|
||||
async function deleteKommentar(kommentarId, targetType, targetId, inLb) {
|
||||
async function deleteKommentar(kommentarId, targetType, targetId) {
|
||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||||
if (inLb) await loadLbComments();
|
||||
else await loadComments(targetId, targetType);
|
||||
}
|
||||
|
||||
async function toggleKommentarLike(kommentarId) {
|
||||
await fetch('/social/kommentare/' + kommentarId + '/like', { method: 'POST' });
|
||||
const btn = document.getElementById('like-k-' + kommentarId);
|
||||
const lc = document.getElementById('lc-k-' + kommentarId);
|
||||
const was = btn.classList.contains('liked');
|
||||
btn.classList.toggle('liked', !was);
|
||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||
}
|
||||
|
||||
// ── Replies ──
|
||||
async function toggleReplies(kommentarId) {
|
||||
const section = document.getElementById('replies-' + kommentarId);
|
||||
if (section.style.display === 'none') {
|
||||
section.style.display = '';
|
||||
await loadReplies(kommentarId);
|
||||
if (document.getElementById('postLightbox')?.classList.contains('open')) {
|
||||
await loadLbComments();
|
||||
} else {
|
||||
section.style.display = 'none';
|
||||
await loadComments(targetId, targetType);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReplies(kommentarId) {
|
||||
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
|
||||
const replies = await res.json();
|
||||
const section = document.getElementById('replies-' + kommentarId);
|
||||
section.innerHTML = (replies.length === 0
|
||||
? '<p style="color:var(--color-muted);font-size:0.78rem;margin-bottom:0.35rem;">Noch keine Antworten.</p>'
|
||||
: replies.map(r => renderReplyHtml(r, kommentarId)).join(''))
|
||||
+ `<div class="comment-write">
|
||||
<input type="text" id="ri-${kommentarId}" placeholder="Antwort schreiben…" maxlength="500"
|
||||
onkeydown="if(event.key==='Enter') postReply('${kommentarId}')">
|
||||
<button onclick="postReply('${kommentarId}')">Senden</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderReplyHtml(r, parentId) {
|
||||
const avatarHtml = r.authorPicture ? `<img src="data:image/png;base64,${r.authorPicture}" alt="">` : '◉';
|
||||
const canDelete = r.authorId === myUserId;
|
||||
return `<div class="comment-item" id="kom-${r.kommentarId}" style="margin-bottom:0.35rem;">
|
||||
<div class="entry-avatar" style="width:22px;height:22px;font-size:0.75rem;">${avatarHtml}</div>
|
||||
<div class="comment-body" style="padding:0.35rem 0.55rem;">
|
||||
<span class="comment-author">${esc(r.authorName)}</span>
|
||||
<span class="comment-time">${fmtDate(r.createdAt)}</span>
|
||||
<div class="comment-text">${esc(r.text)}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="btn-like ${r.likedByMe ? 'liked' : ''}" id="like-k-${r.kommentarId}"
|
||||
onclick="toggleKommentarLike('${r.kommentarId}')">♥ <span id="lc-k-${r.kommentarId}">${r.likeCount}</span></button>
|
||||
${canDelete ? `<button class="btn-delete-small" onclick="deleteReply('${r.kommentarId}','${parentId}')">✕</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function postReply(kommentarId) {
|
||||
const input = document.getElementById('ri-' + kommentarId);
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
await fetch('/social/kommentare', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ targetType: 'KOMMENTAR', targetId: kommentarId, text })
|
||||
});
|
||||
input.value = '';
|
||||
await loadReplies(kommentarId);
|
||||
}
|
||||
|
||||
async function deleteReply(replyId, parentId) {
|
||||
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
|
||||
await loadReplies(parentId);
|
||||
}
|
||||
|
||||
// ── Friend actions ──
|
||||
async function addFriend() {
|
||||
const btn = document.getElementById('friendActionBtn');
|
||||
@@ -1339,38 +890,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function bilderCarousel(bilder) {
|
||||
if (!bilder || bilder.length === 0) return '';
|
||||
if (bilder.length === 1) {
|
||||
return `<div style="margin-top:0.5rem;"><img class="post-bild" src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>`;
|
||||
}
|
||||
const slides = bilder.map((b, i) =>
|
||||
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
|
||||
).join('');
|
||||
return `<div class="post-carousel">
|
||||
${slides}
|
||||
<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">‹</button>
|
||||
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">›</button>
|
||||
<div class="car-indicator"><span class="car-cur">1</span>/${bilder.length}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function carNav(btn, dir) {
|
||||
const car = btn.closest('.post-carousel');
|
||||
const slides = Array.from(car.querySelectorAll('.car-slide'));
|
||||
const cur = slides.findIndex(s => s.classList.contains('active'));
|
||||
slides[cur].classList.remove('active');
|
||||
const next = (cur + dir + slides.length) % slides.length;
|
||||
slides[next].classList.add('active');
|
||||
const ind = car.querySelector('.car-cur');
|
||||
if (ind) ind.textContent = next + 1;
|
||||
}
|
||||
// bilderCarousel und carNav kommen aus shared.js
|
||||
|
||||
function renderProfilPostCard(p) {
|
||||
const avatarHtml = p.authorPicture
|
||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
||||
: '◉';
|
||||
const bildHtml = bilderCarousel(p.bilder);
|
||||
const bildRaw = bilderCarousel(p.bilder);
|
||||
const bildHtml = bildRaw
|
||||
? `<div class="post-bild-wrap" data-post-id="${p.postId}">${bildRaw}<div class="post-bild-hover-icon">💬</div></div>`
|
||||
: '';
|
||||
const privacyLabel = p.isPublic ? '' : '<span style="font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem;">🔒 privat</span>';
|
||||
|
||||
let umfrageHtml = '';
|
||||
@@ -1443,43 +972,74 @@
|
||||
if (res.ok) document.getElementById('pp-' + postId)?.remove();
|
||||
}
|
||||
|
||||
// ── Post Lightbox ──
|
||||
// ── Lightbox (Post + Bild) ──
|
||||
function openPostLb(postId) {
|
||||
activeLbPostId = postId;
|
||||
activeLbMode = 'post';
|
||||
activeLbPostId = postId;
|
||||
activeLbImageIdx = null;
|
||||
const card = document.getElementById('pp-' + postId);
|
||||
if (card) document.getElementById('postLbBody').innerHTML = card.innerHTML;
|
||||
loadPostLbComments(postId);
|
||||
if (card) {
|
||||
const clone = card.cloneNode(true);
|
||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||
}
|
||||
loadLbComments();
|
||||
document.getElementById('postLightbox').classList.add('open');
|
||||
}
|
||||
|
||||
function closePostLb() {
|
||||
function closeLb() {
|
||||
document.getElementById('postLightbox').classList.remove('open');
|
||||
activeLbPostId = null;
|
||||
activeLbMode = null;
|
||||
activeLbPostId = null;
|
||||
activeLbImageIdx = null;
|
||||
}
|
||||
|
||||
async function loadPostLbComments(postId) {
|
||||
const res = await fetch(`/social/kommentare?targetType=FEED_POST&targetId=${postId}`);
|
||||
async function loadLbComments() {
|
||||
let targetType, targetId;
|
||||
if (activeLbMode === 'image') {
|
||||
targetType = 'IMAGE';
|
||||
targetId = allImages[activeLbImageIdx].imageId;
|
||||
} else {
|
||||
targetType = 'FEED_POST';
|
||||
targetId = activeLbPostId;
|
||||
}
|
||||
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${targetId}`);
|
||||
const comments = await res.json();
|
||||
document.getElementById('postLbComments').innerHTML = comments.length === 0
|
||||
document.getElementById('lbCommentsList').innerHTML = comments.length === 0
|
||||
? '<p style="color:var(--color-muted);font-size:0.82rem;margin:0.4rem;">Noch keine Kommentare.</p>'
|
||||
: comments.map(k => renderKommentarHtml(k, 'FEED_POST', postId, true)).join('');
|
||||
: comments.map(k => renderKommentarHtml(k, targetType, targetId, { myUserId, showReplies: false })).join('');
|
||||
}
|
||||
|
||||
async function postLbFeedComment() {
|
||||
if (!activeLbPostId) return;
|
||||
const input = document.getElementById('postLbCommentInput');
|
||||
async function postLbComment() {
|
||||
const input = document.getElementById('lbCommentInput');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
let targetType, targetId;
|
||||
if (activeLbMode === 'image') {
|
||||
targetType = 'IMAGE';
|
||||
targetId = allImages[activeLbImageIdx].imageId;
|
||||
} else {
|
||||
if (!activeLbPostId) return;
|
||||
targetType = 'FEED_POST';
|
||||
targetId = activeLbPostId;
|
||||
}
|
||||
await fetch('/social/kommentare', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ targetType: 'FEED_POST', targetId: activeLbPostId, text })
|
||||
body: JSON.stringify({ targetType, targetId, text })
|
||||
});
|
||||
input.value = '';
|
||||
await loadPostLbComments(activeLbPostId);
|
||||
await loadLbComments();
|
||||
}
|
||||
|
||||
document.getElementById('postLightbox').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('postLightbox')) closePostLb();
|
||||
if (e.target === document.getElementById('postLightbox')) closeLb();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && document.getElementById('postLightbox').classList.contains('open')) closeLb();
|
||||
if (activeLbMode === 'image') {
|
||||
if (e.key === 'ArrowLeft') lbGalleryNav(-1);
|
||||
if (e.key === 'ArrowRight') lbGalleryNav(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Infinite scroll for profil posts
|
||||
@@ -1487,68 +1047,14 @@
|
||||
if (entries[0].isIntersecting) loadProfilPosts();
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
// ── Helpers ──
|
||||
function esc(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
.replace(/"/g,'"').replace(/\n/g,'<br>');
|
||||
}
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' });
|
||||
}
|
||||
|
||||
// ── Emoji Picker ──
|
||||
const EMOJIS = ['😊','😂','❤️','😍','🔥','👍','🥰','😎','🤔','😘','💕','🎉','✨','💋','😈','🫦','🍑','🍆','🔞','🥵','😭','😢','😤','🙄','🤦','🤷','🙏','💪','😏','🤩'];
|
||||
let emojiTarget = null;
|
||||
|
||||
function toggleEmojiPicker(btn, targetId) {
|
||||
emojiTarget = document.getElementById(targetId);
|
||||
let picker = document.getElementById('emojiPicker');
|
||||
if (!picker) {
|
||||
picker = document.createElement('div');
|
||||
picker.id = 'emojiPicker';
|
||||
picker.style.cssText = 'position:fixed;z-index:1000;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.5rem;display:flex;flex-wrap:wrap;gap:0.2rem;max-width:260px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
|
||||
EMOJIS.forEach(em => {
|
||||
const b = document.createElement('button');
|
||||
b.textContent = em;
|
||||
b.style.cssText = 'background:none;border:none;font-size:1.3rem;cursor:pointer;padding:0.2rem;margin:0;width:auto;line-height:1;';
|
||||
b.onclick = e => { e.stopPropagation(); insertEmoji(em); };
|
||||
picker.appendChild(b);
|
||||
});
|
||||
document.body.appendChild(picker);
|
||||
}
|
||||
if (picker.style.display === 'flex') { picker.style.display = 'none'; return; }
|
||||
picker.style.display = 'flex';
|
||||
requestAnimationFrame(() => {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const ph = picker.offsetHeight, pw = picker.offsetWidth;
|
||||
let top = rect.top - ph - 8;
|
||||
let left = rect.left;
|
||||
if (top < 8) top = rect.bottom + 8;
|
||||
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
|
||||
picker.style.top = top + 'px';
|
||||
picker.style.left = left + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function insertEmoji(emoji) {
|
||||
if (!emojiTarget) return;
|
||||
const start = emojiTarget.selectionStart ?? emojiTarget.value.length;
|
||||
const end = emojiTarget.selectionEnd ?? start;
|
||||
emojiTarget.value = emojiTarget.value.slice(0, start) + emoji + emojiTarget.value.slice(end);
|
||||
emojiTarget.selectionStart = emojiTarget.selectionEnd = start + emoji.length;
|
||||
emojiTarget.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
if (picker && picker.style.display === 'flex' && !picker.contains(e.target) && !e.target.closest('[onclick*="toggleEmojiPicker"]')) {
|
||||
picker.style.display = 'none';
|
||||
}
|
||||
// Klick auf Post-Bilder → Post-Lightbox öffnen
|
||||
document.getElementById('profilPostsFeed').addEventListener('click', e => {
|
||||
const wrap = e.target.closest('.post-bild-wrap');
|
||||
if (!wrap) return;
|
||||
openPostLb(wrap.dataset.postId);
|
||||
});
|
||||
|
||||
// esc, fmtDate, toggleEmojiPicker, insertEmoji kommen aus shared.js
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -51,14 +51,7 @@
|
||||
.post-delete { margin-left:auto; }
|
||||
.post-delete:hover { color:#c0392b !important; }
|
||||
|
||||
/* Carousel */
|
||||
.post-carousel { position:relative; margin-top:0.5rem; }
|
||||
.car-slide { display:none; }
|
||||
.car-slide.active { display:block; }
|
||||
.car-btn { position:absolute; top:50%; transform:translateY(-50%); background:rgba(0,0,0,0.55); border:none; color:#fff; font-size:2.2rem; width:auto; min-width:2.4rem; height:3.2rem; border-radius:8px; cursor:pointer; z-index:5; display:flex; align-items:center; justify-content:center; padding:0 0.5rem; margin:0; line-height:1; }
|
||||
.car-prev { left:0.3rem; }
|
||||
.car-next { right:0.3rem; }
|
||||
.car-indicator { text-align:center; font-size:0.75rem; color:var(--color-muted); margin-top:0.25rem; }
|
||||
/* Carousel – Stile kommen aus shared.js */
|
||||
|
||||
.umfrage-option-bar { margin:0.3rem 0; cursor:pointer; border-radius:6px; overflow:hidden; border:1px solid var(--color-secondary); position:relative; transition:border-color 0.15s; }
|
||||
.umfrage-option-bar:hover { border-color:var(--color-primary); }
|
||||
@@ -83,23 +76,14 @@
|
||||
.lb-comments-panel { width:300px; flex-shrink:0; display:flex; flex-direction:column; }
|
||||
.lb-comments-header { font-size:0.78rem; font-weight:600; color:var(--color-muted); text-transform:uppercase; letter-spacing:0.06em; padding:0.7rem 1rem; border-bottom:1px solid var(--color-secondary); flex-shrink:0; }
|
||||
.lb-comments-list { flex:1; overflow-y:auto; padding:0.75rem; }
|
||||
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; gap:0.5rem; flex-shrink:0; align-items:center; }
|
||||
.lb-comment-compose input { flex:1; font-size:0.85rem; padding:0.35rem 0.6rem; height:auto; }
|
||||
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; flex-direction:column; gap:0.5rem; flex-shrink:0; }
|
||||
.lb-comment-compose textarea { width:100%; font-size:0.85rem; padding:0.35rem 0.6rem; resize:none; background:var(--color-secondary); border:1px solid var(--color-secondary); border-radius:6px; color:var(--color-text); font-family:inherit; outline:none; transition:border-color 0.2s; box-sizing:border-box; }
|
||||
.lb-comment-compose textarea:focus { border-color:var(--color-primary); }
|
||||
.lb-comment-compose-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
|
||||
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
|
||||
@media (max-width:650px) { .lb-layout { flex-direction:column; max-height:95vh; } .lb-post-side { border-right:none; border-bottom:1px solid var(--color-secondary); max-height:55vh; } .lb-comments-panel { width:100%; } }
|
||||
|
||||
.comment-item { display:flex; gap:0.5rem; margin-bottom:0.5rem; }
|
||||
.comment-avatar { width:28px; height:28px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.75rem; flex-shrink:0; overflow:hidden; }
|
||||
.comment-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
.comment-body { flex:1; background:rgba(255,255,255,0.04); border-radius:6px; padding:0.5rem 0.65rem; }
|
||||
.comment-author { font-size:0.8rem; font-weight:600; }
|
||||
.comment-text { font-size:0.85rem; white-space:pre-wrap; word-break:break-word; margin-top:0.2rem; }
|
||||
.comment-date { font-size:0.72rem; color:var(--color-muted); margin-left:0.4rem; }
|
||||
.comment-actions { display:flex; gap:0.4rem; margin-top:0.3rem; align-items:center; }
|
||||
.btn-like { background:none; border:1px solid rgba(255,255,255,0.15); border-radius:20px; padding:0.2rem 0.65rem; color:var(--color-muted); font-size:0.78rem; cursor:pointer; display:inline-flex; align-items:center; gap:0.3rem; margin:0; width:auto; transition:border-color 0.15s, color 0.15s; }
|
||||
.btn-like:hover, .btn-like.liked { border-color:var(--color-primary); color:var(--color-primary); }
|
||||
.btn-delete-small { background:none; border:none; color:rgba(200,50,50,0.6); font-size:0.78rem; cursor:pointer; margin:0; width:auto; padding:0; }
|
||||
.btn-delete-small:hover { color:var(--color-primary); }
|
||||
/* Comment + Like-Stile kommen aus shared.js */
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -166,15 +150,18 @@
|
||||
<div class="lb-comments-header">Kommentare</div>
|
||||
<div class="lb-comments-list" id="lbCommentsList"></div>
|
||||
<div class="lb-comment-compose">
|
||||
<input type="text" id="lbCommentInput" placeholder="Kommentar schreiben…" maxlength="500"
|
||||
onkeydown="if(event.key==='Enter') postLbComment()">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
||||
<button onclick="postLbComment()">Senden</button>
|
||||
<textarea id="lbCommentInput" placeholder="Kommentar schreiben…" maxlength="500" rows="3"
|
||||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();postLbComment()}"></textarea>
|
||||
<div class="lb-comment-compose-actions">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
||||
<button onclick="postLbComment()">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
@@ -241,33 +228,7 @@
|
||||
observer.observe(document.getElementById('mineSentinel'));
|
||||
observer.observe(document.getElementById('publicSentinel'));
|
||||
|
||||
// ── Carousel ──
|
||||
function bilderCarousel(bilder, postId) {
|
||||
if (!bilder || bilder.length === 0) return '';
|
||||
if (bilder.length === 1) {
|
||||
return `<div style="margin-top:0.5rem;"><img class="post-bild" src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>`;
|
||||
}
|
||||
const slides = bilder.map((b, i) =>
|
||||
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
|
||||
).join('');
|
||||
return `<div class="post-carousel">
|
||||
${slides}
|
||||
<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">‹</button>
|
||||
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">›</button>
|
||||
<div class="car-indicator"><span class="car-cur">1</span>/${bilder.length}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function carNav(btn, dir) {
|
||||
const car = btn.closest('.post-carousel');
|
||||
const slides = Array.from(car.querySelectorAll('.car-slide'));
|
||||
const cur = slides.findIndex(s => s.classList.contains('active'));
|
||||
slides[cur].classList.remove('active');
|
||||
const next = (cur + dir + slides.length) % slides.length;
|
||||
slides[next].classList.add('active');
|
||||
const ind = car.querySelector('.car-cur');
|
||||
if (ind) ind.textContent = next + 1;
|
||||
}
|
||||
// bilderCarousel und carNav kommen aus shared.js
|
||||
|
||||
// ── Render post card ──
|
||||
function renderPostCard(p, tab) {
|
||||
@@ -533,7 +494,7 @@
|
||||
const comments = await res.json();
|
||||
document.getElementById('lbCommentsList').innerHTML = comments.length === 0
|
||||
? '<p style="color:var(--color-muted);font-size:0.82rem;margin:0.4rem;">Noch keine Kommentare.</p>'
|
||||
: comments.map(k => renderKommentarHtml(k, targetType, postId)).join('');
|
||||
: comments.map(k => renderKommentarHtml(k, targetType, postId, { myUserId })).join('');
|
||||
}
|
||||
|
||||
async function postLbComment() {
|
||||
@@ -552,104 +513,16 @@
|
||||
if (kcEl) kcEl.textContent = parseInt(kcEl.textContent) + 1;
|
||||
}
|
||||
|
||||
function renderKommentarHtml(k, targetType, targetId) {
|
||||
const avatarHtml = k.authorPicture
|
||||
? `<img src="data:image/png;base64,${k.authorPicture}" alt="">`
|
||||
: '◉';
|
||||
const canDelete = k.authorId === myUserId;
|
||||
return `<div class="comment-item" id="kom-${k.kommentarId}">
|
||||
<div class="comment-avatar">${avatarHtml}</div>
|
||||
<div class="comment-body">
|
||||
<span class="comment-author">${esc(k.authorName)}</span>
|
||||
<span class="comment-date">${fmtDate(k.createdAt)}</span>
|
||||
<div class="comment-text">${esc(k.text)}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="btn-like${k.likedByMe ? ' liked' : ''}" onclick="likeKommentar('${k.kommentarId}')">
|
||||
♥ <span id="lkk-${k.kommentarId}">${k.likeCount}</span>
|
||||
</button>
|
||||
${canDelete ? `<button class="btn-delete-small" onclick="deleteKommentar('${k.kommentarId}','${targetType}','${targetId}')">✕</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function likeKommentar(kommentarId) {
|
||||
await fetch('/social/kommentare/' + kommentarId + '/like', { method: 'POST' });
|
||||
const btn = document.getElementById('lkk-' + kommentarId)?.parentElement;
|
||||
const lc = document.getElementById('lkk-' + kommentarId);
|
||||
if (!btn || !lc) return;
|
||||
const was = btn.classList.contains('liked');
|
||||
btn.classList.toggle('liked', !was);
|
||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||
}
|
||||
// renderKommentarHtml und toggleKommentarLike kommen aus shared.js
|
||||
|
||||
async function deleteKommentar(kommentarId, targetType, targetId) {
|
||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||||
await loadLbComments(targetId, activeLbPostType);
|
||||
}
|
||||
|
||||
// ── Emoji Picker ──
|
||||
const EMOJIS = ['😊','😂','❤️','😍','🔥','👍','🥰','😎','🤔','😘','💕','🎉','✨','💋','😈','🫦','🍑','🍆','🔞','🥵','😭','😢','😤','🙄','🤦','🤷','🙏','💪','😏','🤩'];
|
||||
let emojiTarget = null;
|
||||
// toggleEmojiPicker, insertEmoji kommen aus shared.js
|
||||
|
||||
function toggleEmojiPicker(btn, targetId) {
|
||||
emojiTarget = document.getElementById(targetId);
|
||||
let picker = document.getElementById('emojiPicker');
|
||||
if (!picker) {
|
||||
picker = document.createElement('div');
|
||||
picker.id = 'emojiPicker';
|
||||
picker.style.cssText = 'position:fixed;z-index:1000;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.5rem;display:flex;flex-wrap:wrap;gap:0.2rem;max-width:260px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
|
||||
EMOJIS.forEach(em => {
|
||||
const b = document.createElement('button');
|
||||
b.textContent = em;
|
||||
b.style.cssText = 'background:none;border:none;font-size:1.3rem;cursor:pointer;padding:0.2rem;margin:0;width:auto;line-height:1;';
|
||||
b.onclick = e => { e.stopPropagation(); insertEmoji(em); };
|
||||
picker.appendChild(b);
|
||||
});
|
||||
document.body.appendChild(picker);
|
||||
}
|
||||
if (picker.style.display === 'flex') { picker.style.display = 'none'; return; }
|
||||
picker.style.display = 'flex';
|
||||
requestAnimationFrame(() => {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const ph = picker.offsetHeight, pw = picker.offsetWidth;
|
||||
let top = rect.top - ph - 8;
|
||||
let left = rect.left;
|
||||
if (top < 8) top = rect.bottom + 8;
|
||||
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
|
||||
picker.style.top = top + 'px';
|
||||
picker.style.left = left + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function insertEmoji(emoji) {
|
||||
if (!emojiTarget) return;
|
||||
const start = emojiTarget.selectionStart ?? emojiTarget.value.length;
|
||||
const end = emojiTarget.selectionEnd ?? start;
|
||||
emojiTarget.value = emojiTarget.value.slice(0, start) + emoji + emojiTarget.value.slice(end);
|
||||
emojiTarget.selectionStart = emojiTarget.selectionEnd = start + emoji.length;
|
||||
emojiTarget.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
if (picker && picker.style.display === 'flex' && !picker.contains(e.target) && !e.target.closest('[onclick*="toggleEmojiPicker"]')) {
|
||||
picker.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// ── Helpers ──
|
||||
function esc(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
.replace(/"/g,'"').replace(/\n/g,'<br>');
|
||||
}
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' });
|
||||
}
|
||||
// esc, fmtDate kommen aus shared.js
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -43,14 +43,6 @@
|
||||
.umfrage-option-row button { width:auto; margin:0; padding:0.3rem 0.6rem; font-size:0.8rem; }
|
||||
.compose-footer { display:flex; justify-content:space-between; align-items:center; margin-top:0.75rem; flex-wrap:wrap; gap:0.5rem; }
|
||||
.multi-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
|
||||
/* Carousel */
|
||||
.post-carousel { position:relative; margin-top:0.5rem; }
|
||||
.car-slide { display:none; }
|
||||
.car-slide.active { display:block; }
|
||||
.car-btn { position:absolute; top:50%; transform:translateY(-50%); background:rgba(0,0,0,0.55); border:none; color:#fff; font-size:2.2rem; width:auto; min-width:2.4rem; height:3.2rem; border-radius:8px; cursor:pointer; z-index:5; display:flex; align-items:center; justify-content:center; padding:0 0.5rem; margin:0; line-height:1; }
|
||||
.car-prev { left:0.3rem; }
|
||||
.car-next { right:0.3rem; }
|
||||
.car-indicator { text-align:center; font-size:0.75rem; color:var(--color-muted); margin-top:0.25rem; }
|
||||
|
||||
.post-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:0.9rem; }
|
||||
.post-header { display:flex; align-items:center; gap:0.7rem; margin-bottom:0.6rem; }
|
||||
@@ -76,13 +68,6 @@
|
||||
|
||||
/* Kommentare */
|
||||
.comments-section { margin-top:0.75rem; border-top:1px solid var(--color-secondary); padding-top:0.75rem; }
|
||||
.comment-item { display:flex; gap:0.5rem; margin-bottom:0.5rem; }
|
||||
.comment-avatar { width:28px; height:28px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.75rem; flex-shrink:0; overflow:hidden; }
|
||||
.comment-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
.comment-body { flex:1; }
|
||||
.comment-author { font-weight:600; font-size:0.82rem; }
|
||||
.comment-text { font-size:0.85rem; white-space:pre-wrap; word-break:break-word; }
|
||||
.comment-date { font-size:0.72rem; color:var(--color-muted); }
|
||||
.comment-compose { display:flex; gap:0.5rem; margin-top:0.5rem; }
|
||||
.comment-compose input { flex:1; font-size:0.85rem; padding:0.35rem 0.6rem; height:auto; }
|
||||
.comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
|
||||
@@ -133,8 +118,10 @@
|
||||
.lb-post-side { flex:1; overflow-y:auto; padding:1.25rem; border-right:1px solid var(--color-secondary); min-width:0; }
|
||||
.lb-comments-panel { width:300px; flex-shrink:0; display:flex; flex-direction:column; }
|
||||
.lb-comments-list { flex:1; overflow-y:auto; padding:0.75rem; }
|
||||
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; gap:0.5rem; }
|
||||
.lb-comment-compose input { flex:1; font-size:0.85rem; padding:0.35rem 0.6rem; height:auto; }
|
||||
.lb-comment-compose { padding:0.75rem; border-top:1px solid var(--color-secondary); display:flex; flex-direction:column; gap:0.5rem; flex-shrink:0; }
|
||||
.lb-comment-compose textarea { width:100%; font-size:0.85rem; padding:0.35rem 0.6rem; resize:none; background:var(--color-secondary); border:1px solid var(--color-secondary); border-radius:6px; color:var(--color-text); font-family:inherit; outline:none; transition:border-color 0.2s; box-sizing:border-box; }
|
||||
.lb-comment-compose textarea:focus { border-color:var(--color-primary); }
|
||||
.lb-comment-compose-actions { display:flex; gap:0.5rem; justify-content:flex-end; }
|
||||
.lb-comment-compose button { width:auto; margin:0; padding:0.35rem 0.75rem; font-size:0.8rem; }
|
||||
@media (max-width:650px) {
|
||||
.lb-layout { flex-direction:column; max-height:95vh; }
|
||||
@@ -294,15 +281,19 @@
|
||||
<div class="lb-post-side" id="lbPostContent"></div>
|
||||
<div class="lb-comments-panel">
|
||||
<div class="lb-comments-list" id="lbCommentsList"></div>
|
||||
<div class="lb-comment-compose" style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<input type="text" id="lbCommentInput" placeholder="Kommentieren…" onkeydown="if(event.key==='Enter')submitLbComment()" style="flex:1;">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
||||
<button onclick="submitLbComment()">Senden</button>
|
||||
<div class="lb-comment-compose">
|
||||
<textarea id="lbCommentInput" placeholder="Kommentieren…" maxlength="500" rows="3"
|
||||
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();submitLbComment()}"></textarea>
|
||||
<div class="lb-comment-compose-actions">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'lbCommentInput')" title="Emoji">😊</button>
|
||||
<button onclick="submitLbComment()">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
@@ -355,8 +346,7 @@
|
||||
let currentPage = 0;
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||
function fmtDate(iso) { if (!iso) return ''; return new Date(iso).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' }); }
|
||||
// esc, fmtDate kommen aus shared.js
|
||||
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
@@ -466,32 +456,6 @@
|
||||
btn.style.display = data.hasMore ? '' : 'none';
|
||||
}
|
||||
|
||||
function bilderCarousel(bilder) {
|
||||
if (!bilder || bilder.length === 0) return '';
|
||||
if (bilder.length === 1) {
|
||||
return `<div style="margin-top:0.5rem;"><img class="post-bild" src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>`;
|
||||
}
|
||||
const slides = bilder.map((b, i) =>
|
||||
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
|
||||
).join('');
|
||||
return `<div class="post-carousel">
|
||||
${slides}
|
||||
<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">‹</button>
|
||||
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">›</button>
|
||||
<div class="car-indicator"><span class="car-cur">1</span>/${bilder.length}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function carNav(btn, dir) {
|
||||
const car = btn.closest('.post-carousel');
|
||||
const slides = Array.from(car.querySelectorAll('.car-slide'));
|
||||
const cur = slides.findIndex(s => s.classList.contains('active'));
|
||||
slides[cur].classList.remove('active');
|
||||
const next = (cur + dir + slides.length) % slides.length;
|
||||
slides[next].classList.add('active');
|
||||
const ind = car.querySelector('.car-cur');
|
||||
if (ind) ind.textContent = next + 1;
|
||||
}
|
||||
|
||||
function renderPostCard(p) {
|
||||
const canDelete = (myRole === 'ADMIN' || p.authorId === myId);
|
||||
@@ -593,17 +557,6 @@
|
||||
]);
|
||||
}
|
||||
|
||||
function renderComment(k) {
|
||||
const av = k.authorPicture ? `<img src="data:image/png;base64,${k.authorPicture}" alt="">` : '◉';
|
||||
return `<div class="comment-item">
|
||||
<div class="comment-avatar">${av}</div>
|
||||
<div class="comment-body">
|
||||
<span class="comment-author">${esc(k.authorName)}</span>
|
||||
<span class="comment-date" style="margin-left:0.4rem;">${fmtDate(k.createdAt)}</span>
|
||||
<div class="comment-text">${esc(k.text)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Compose image ──
|
||||
|
||||
@@ -1044,6 +997,11 @@
|
||||
if (post) { post.likeCount = newCount; post.likedByMe = !isActive; }
|
||||
}
|
||||
|
||||
async function deleteKommentar(kommentarId) {
|
||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||||
await loadLbComments();
|
||||
}
|
||||
|
||||
async function loadLbComments() {
|
||||
if (!lbPostId) return;
|
||||
const list = document.getElementById('lbCommentsList');
|
||||
@@ -1051,7 +1009,7 @@
|
||||
const res = await fetch('/social/kommentare?targetType=GROUP_POST&targetId=' + lbPostId);
|
||||
if (!res.ok) return;
|
||||
const kmts = await res.json();
|
||||
kmts.forEach(k => list.insertAdjacentHTML('beforeend', renderComment(k)));
|
||||
kmts.forEach(k => list.insertAdjacentHTML('beforeend', renderKommentarHtml(k, 'GROUP_POST', lbPostId, { myUserId: myId })));
|
||||
list.scrollTop = list.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -1123,55 +1081,6 @@
|
||||
});
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePostDialog(); });
|
||||
|
||||
// ── Emoji Picker ──
|
||||
const EMOJIS = ['😊','😂','❤️','😍','🔥','👍','🥰','😎','🤔','😘','💕','🎉','✨','💋','😈','🫦','🍑','🍆','🔞','🥵','😭','😢','😤','🙄','🤦','🤷','🙏','💪','😏','🤩'];
|
||||
let emojiTarget = null;
|
||||
|
||||
function toggleEmojiPicker(btn, targetId) {
|
||||
emojiTarget = document.getElementById(targetId);
|
||||
let picker = document.getElementById('emojiPicker');
|
||||
if (!picker) {
|
||||
picker = document.createElement('div');
|
||||
picker.id = 'emojiPicker';
|
||||
picker.style.cssText = 'position:fixed;z-index:1000;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.5rem;display:flex;flex-wrap:wrap;gap:0.2rem;max-width:260px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
|
||||
EMOJIS.forEach(em => {
|
||||
const b = document.createElement('button');
|
||||
b.textContent = em;
|
||||
b.style.cssText = 'background:none;border:none;font-size:1.3rem;cursor:pointer;padding:0.2rem;margin:0;width:auto;line-height:1;';
|
||||
b.onclick = e => { e.stopPropagation(); insertEmoji(em); };
|
||||
picker.appendChild(b);
|
||||
});
|
||||
document.body.appendChild(picker);
|
||||
}
|
||||
if (picker.style.display === 'flex') { picker.style.display = 'none'; return; }
|
||||
picker.style.display = 'flex';
|
||||
requestAnimationFrame(() => {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const ph = picker.offsetHeight, pw = picker.offsetWidth;
|
||||
let top = rect.top - ph - 8;
|
||||
let left = rect.left;
|
||||
if (top < 8) top = rect.bottom + 8;
|
||||
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
|
||||
picker.style.top = top + 'px';
|
||||
picker.style.left = left + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function insertEmoji(emoji) {
|
||||
if (!emojiTarget) return;
|
||||
const start = emojiTarget.selectionStart ?? emojiTarget.value.length;
|
||||
const end = emojiTarget.selectionEnd ?? start;
|
||||
emojiTarget.value = emojiTarget.value.slice(0, start) + emoji + emojiTarget.value.slice(end);
|
||||
emojiTarget.selectionStart = emojiTarget.selectionEnd = start + emoji.length;
|
||||
emojiTarget.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
if (picker && picker.style.display === 'flex' && !picker.contains(e.target) && !e.target.closest('[onclick*="toggleEmojiPicker"]')) {
|
||||
picker.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
BIN
xxxthegame/src/main/resources/static/img/card_doubleup.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_doubleup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
BIN
xxxthegame/src/main/resources/static/img/card_freeze.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_freeze.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
BIN
xxxthegame/src/main/resources/static/img/card_red.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
BIN
xxxthegame/src/main/resources/static/img/card_reset.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_reset.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
xxxthegame/src/main/resources/static/img/card_task.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_task.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
BIN
xxxthegame/src/main/resources/static/img/card_yellow.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_yellow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
237
xxxthegame/src/main/resources/static/js/image-viewer.js
Normal file
237
xxxthegame/src/main/resources/static/js/image-viewer.js
Normal file
@@ -0,0 +1,237 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// image-viewer.js – Universelle Bild-Lightbox
|
||||
//
|
||||
// Einbinden: <script src="/js/shared.js"></script> (vorher)
|
||||
// <script src="/js/image-viewer.js"></script>
|
||||
//
|
||||
// Zwei Modi:
|
||||
// Modus A – Nur Bild (kein Like, keine Kommentare):
|
||||
// imageViewer.open({ images: [{ src }] })
|
||||
//
|
||||
// Modus B – Galerie mit Like + Kommentare:
|
||||
// imageViewer.open({
|
||||
// images: [{ src, id, likedByMe, likeCount }],
|
||||
// index: 0,
|
||||
// showLike: true,
|
||||
// showComments: true,
|
||||
// myUserId: '...',
|
||||
// onLike: async (img) => {} // optional; sonst POST /social/profile-images/{id}/like
|
||||
// })
|
||||
//
|
||||
// Globale Instanz: window.imageViewer
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class ImageViewer {
|
||||
constructor() {
|
||||
this._cfg = null;
|
||||
this._idx = 0;
|
||||
this.isOpen = false;
|
||||
this._injectStyles();
|
||||
this._injectHTML();
|
||||
this._bindEvents();
|
||||
}
|
||||
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
open(cfg) {
|
||||
this._cfg = cfg;
|
||||
this._idx = cfg.index || 0;
|
||||
this.isOpen = true;
|
||||
|
||||
const multi = cfg.images.length > 1;
|
||||
const showLike = !!cfg.showLike;
|
||||
const showCom = !!cfg.showComments;
|
||||
|
||||
this._q('ivPrev').style.display = multi ? '' : 'none';
|
||||
this._q('ivNext').style.display = multi ? '' : 'none';
|
||||
this._q('ivCounter').style.display = multi ? '' : 'none';
|
||||
this._q('ivLikeBtn').style.display = showLike ? '' : 'none';
|
||||
this._q('ivComments').style.display = showCom ? '' : 'none';
|
||||
|
||||
this._render();
|
||||
this._q('imageViewer').classList.add('open');
|
||||
this._updateLayout();
|
||||
}
|
||||
|
||||
close() {
|
||||
this._q('imageViewer').classList.remove('open');
|
||||
this.isOpen = false;
|
||||
this._cfg = null;
|
||||
}
|
||||
|
||||
/** Kommentare im offenen Viewer neu laden (z.B. nach externem Löschen) */
|
||||
reloadComments() {
|
||||
if (this.isOpen && this._cfg?.showComments) this._loadComments();
|
||||
}
|
||||
|
||||
// ── Internes Rendering ────────────────────────────────────────────────────
|
||||
|
||||
_q(id) { return document.getElementById(id); }
|
||||
|
||||
_render() {
|
||||
const img = this._cfg.images[this._idx];
|
||||
this._q('ivImg').src = img.src;
|
||||
|
||||
const total = this._cfg.images.length;
|
||||
this._q('ivCounter').textContent = `${this._idx + 1} / ${total}`;
|
||||
this._q('ivPrev').disabled = this._idx === 0;
|
||||
this._q('ivNext').disabled = this._idx === total - 1;
|
||||
|
||||
if (this._cfg.showLike) this._syncLike();
|
||||
if (this._cfg.showComments) this._loadComments();
|
||||
}
|
||||
|
||||
_syncLike() {
|
||||
const img = this._cfg.images[this._idx];
|
||||
const btn = this._q('ivLikeBtn');
|
||||
btn.className = 'btn-like' + (img.likedByMe ? ' liked' : '');
|
||||
this._q('ivLikeCount').textContent = img.likeCount;
|
||||
}
|
||||
|
||||
async _loadComments() {
|
||||
const img = this._cfg.images[this._idx];
|
||||
const res = await fetch(`/social/kommentare?targetType=IMAGE&targetId=${img.id}`);
|
||||
const comments = await res.json();
|
||||
const myUserId = this._cfg.myUserId || null;
|
||||
this._q('ivCommentsList').innerHTML = comments.length === 0
|
||||
? '<p style="color:var(--color-muted);font-size:0.82rem;margin-bottom:0.4rem;">Noch keine Kommentare.</p>'
|
||||
: comments.map(k => renderKommentarHtml(k, 'IMAGE', img.id, { myUserId, showReplies: true })).join('');
|
||||
}
|
||||
|
||||
async _postComment() {
|
||||
const input = this._q('ivCommentInput');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
const img = this._cfg.images[this._idx];
|
||||
await fetch('/social/kommentare', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ targetType: 'IMAGE', targetId: img.id, text })
|
||||
});
|
||||
input.value = '';
|
||||
await this._loadComments();
|
||||
}
|
||||
|
||||
async _toggleLike() {
|
||||
const img = this._cfg.images[this._idx];
|
||||
const onLike = this._cfg.onLike;
|
||||
img.likedByMe = !img.likedByMe;
|
||||
img.likeCount += img.likedByMe ? 1 : -1;
|
||||
this._syncLike();
|
||||
try {
|
||||
if (onLike) await onLike(img);
|
||||
else await fetch('/social/profile-images/' + img.id + '/like', { method: 'POST' });
|
||||
} catch {
|
||||
img.likedByMe = !img.likedByMe;
|
||||
img.likeCount += img.likedByMe ? 1 : -1;
|
||||
this._syncLike();
|
||||
}
|
||||
}
|
||||
|
||||
_prev() { if (this._idx > 0) { this._idx--; this._render(); } }
|
||||
_next() { if (this._idx < this._cfg.images.length - 1) { this._idx++; this._render(); } }
|
||||
|
||||
_updateLayout() {
|
||||
const el = this._q('ivLayout');
|
||||
if (!el) return;
|
||||
const bp = parseInt(getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--breakpoint-mobile').trim()) || 768;
|
||||
el.classList.toggle('iv-narrow', window.innerWidth <= bp);
|
||||
}
|
||||
|
||||
// ── CSS + HTML Injection ──────────────────────────────────────────────────
|
||||
|
||||
_injectStyles() {
|
||||
if (document.getElementById('iv-styles')) return;
|
||||
const s = document.createElement('style');
|
||||
s.id = 'iv-styles';
|
||||
s.textContent = `
|
||||
#imageViewer{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.88);z-index:500;align-items:center;justify-content:center;padding:2rem}
|
||||
#imageViewer.open{display:flex}
|
||||
#ivLayout{display:flex;flex-direction:row;gap:1rem;height:min(78vh,660px);max-width:calc(100vw - 4rem);align-items:stretch}
|
||||
#ivImageSide{width:660px;flex-shrink:1;min-width:0;display:flex;flex-direction:column}
|
||||
.iv-image-box{flex:1;position:relative;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center}
|
||||
#ivImg{width:100%;height:100%;object-fit:contain;display:block}
|
||||
.iv-overlay{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(transparent,rgba(0,0,0,0.6));border-radius:0 0 12px 12px;padding:2rem 0.75rem 0.6rem;display:flex;align-items:center;justify-content:space-between;gap:0.5rem}
|
||||
.iv-nav-btn{background:rgba(0,0,0,0.35);border:1px solid rgba(255,255,255,0.2);border-radius:6px;color:#fff;padding:0.3rem 0.75rem;cursor:pointer;margin:0;width:auto;font-size:1rem;flex-shrink:0;transition:background 0.15s}
|
||||
.iv-nav-btn:hover{background:rgba(0,0,0,0.65)}
|
||||
.iv-nav-btn:disabled{opacity:.25;cursor:default}
|
||||
.iv-overlay-center{display:flex;align-items:center;gap:0.6rem;flex:1;justify-content:center}
|
||||
#ivCounter{font-size:0.8rem;color:rgba(255,255,255,0.75)}
|
||||
.iv-close{position:fixed;top:1rem;right:1rem;background:rgba(0,0,0,0.55);border:1px solid rgba(255,255,255,0.2);color:#fff;font-size:1.1rem;width:2.2rem;height:2.2rem;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;margin:0;z-index:502;transition:background 0.15s}
|
||||
.iv-close:hover{background:rgba(180,30,30,0.8)}
|
||||
#ivComments{background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;width:280px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden}
|
||||
.iv-comments-header{font-size:0.78rem;font-weight:600;color:var(--color-muted);text-transform:uppercase;letter-spacing:0.06em;padding:0.7rem 1rem;border-bottom:1px solid var(--color-secondary);flex-shrink:0}
|
||||
#ivCommentsList{flex:1;overflow-y:auto;padding:0.65rem 0.75rem;scrollbar-width:thin;scrollbar-color:var(--color-secondary) transparent}
|
||||
.iv-comment-compose{display:flex;gap:0.4rem;padding:0.65rem 0.75rem;border-top:1px solid var(--color-secondary);flex-shrink:0;align-items:center}
|
||||
.iv-comment-compose input{flex:1;padding:0.4rem 0.65rem;font-size:0.85rem}
|
||||
.iv-comment-compose button{width:auto;padding:0.4rem 0.7rem;font-size:0.82rem;white-space:nowrap}
|
||||
#ivLayout.iv-narrow{flex-direction:column;height:auto;max-height:90vh;overflow-y:auto;width:calc(100vw - 1rem);max-width:calc(100vw - 1rem)}
|
||||
#ivLayout.iv-narrow #ivImageSide{width:100%;flex-shrink:0}
|
||||
#ivLayout.iv-narrow .iv-image-box{height:min(45vh,360px);flex:none}
|
||||
#ivLayout.iv-narrow #ivComments{width:100%;max-height:40vh;flex-shrink:0}
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
_injectHTML() {
|
||||
if (document.getElementById('imageViewer')) return;
|
||||
const div = document.createElement('div');
|
||||
div.id = 'imageViewer';
|
||||
div.innerHTML = `
|
||||
<button class="iv-close" id="ivClose">✕</button>
|
||||
<div id="ivLayout">
|
||||
<div id="ivImageSide">
|
||||
<div class="iv-image-box">
|
||||
<img id="ivImg" src="" alt="">
|
||||
<div class="iv-overlay">
|
||||
<button class="iv-nav-btn" id="ivPrev">←</button>
|
||||
<div class="iv-overlay-center">
|
||||
<span id="ivCounter"></span>
|
||||
<button class="btn-like" id="ivLikeBtn">♥ <span id="ivLikeCount">0</span></button>
|
||||
</div>
|
||||
<button class="iv-nav-btn" id="ivNext">→</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ivComments">
|
||||
<div class="iv-comments-header">Kommentare</div>
|
||||
<div id="ivCommentsList"></div>
|
||||
<div class="iv-comment-compose">
|
||||
<input type="text" id="ivCommentInput" placeholder="Kommentar schreiben…" maxlength="500">
|
||||
<button type="button" onclick="toggleEmojiPicker(this,'ivCommentInput')" title="Emoji"
|
||||
style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);border-radius:6px;padding:0.35rem 0.6rem;font-size:0.95rem;cursor:pointer;margin:0;width:auto;">😊</button>
|
||||
<button id="ivCommentSend">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
const init = () => {
|
||||
this._q('ivClose').addEventListener('click', () => this.close());
|
||||
this._q('imageViewer').addEventListener('click', e => {
|
||||
if (e.target === this._q('imageViewer')) this.close();
|
||||
});
|
||||
this._q('ivPrev').addEventListener('click', () => this._prev());
|
||||
this._q('ivNext').addEventListener('click', () => this._next());
|
||||
this._q('ivLikeBtn').addEventListener('click', () => this._toggleLike());
|
||||
this._q('ivCommentSend').addEventListener('click', () => this._postComment());
|
||||
this._q('ivCommentInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') this._postComment();
|
||||
});
|
||||
window.addEventListener('resize', () => this._updateLayout());
|
||||
};
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!this.isOpen) return;
|
||||
if (e.key === 'Escape') this.close();
|
||||
if (e.key === 'ArrowLeft') this._prev();
|
||||
if (e.key === 'ArrowRight') this._next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.imageViewer = new ImageViewer();
|
||||
237
xxxthegame/src/main/resources/static/js/shared.js
Normal file
237
xxxthegame/src/main/resources/static/js/shared.js
Normal file
@@ -0,0 +1,237 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// shared.js – Gemeinsame Helfer & Komponenten
|
||||
// Einbinden: <script src="/js/shared.js"></script>
|
||||
// (vor allen Seiten-Skripten, nach CSS-Links)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── CSS-Injection (Comment + Carousel) ────────────────────────────────────────
|
||||
(function injectSharedStyles() {
|
||||
if (document.getElementById('shared-styles')) return;
|
||||
const s = document.createElement('style');
|
||||
s.id = 'shared-styles';
|
||||
s.textContent = `
|
||||
/* ── Karussell ── */
|
||||
.post-carousel{position:relative;margin-top:0.5rem}
|
||||
.car-slide{display:none}
|
||||
.car-slide.active{display:block}
|
||||
.car-btn{position:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,0.55);border:none;color:#fff;font-size:2.2rem;width:auto;min-width:2.4rem;height:3.2rem;border-radius:8px;cursor:pointer;z-index:5;display:flex;align-items:center;justify-content:center;padding:0 0.5rem;margin:0;line-height:1}
|
||||
.car-prev{left:0.3rem}
|
||||
.car-next{right:0.3rem}
|
||||
.car-indicator{text-align:center;font-size:0.75rem;color:var(--color-muted);margin-top:0.25rem}
|
||||
|
||||
/* ── Like / Löschen-Buttons ── */
|
||||
.btn-like{background:none;border:1px solid rgba(255,255,255,0.15);border-radius:20px;padding:0.2rem 0.65rem;color:var(--color-muted);font-size:0.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:0.3rem;margin:0;width:auto;transition:border-color 0.15s,color 0.15s}
|
||||
.btn-like:hover,.btn-like.liked{border-color:var(--color-primary);color:var(--color-primary)}
|
||||
.btn-delete-small{background:none;border:none;color:rgba(200,50,50,0.6);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0}
|
||||
.btn-delete-small:hover{color:var(--color-primary)}
|
||||
.btn-text{background:none;border:none;color:var(--color-muted);font-size:0.78rem;cursor:pointer;margin:0;width:auto;padding:0;text-decoration:underline;text-decoration-color:rgba(255,255,255,0.2)}
|
||||
.btn-text:hover{color:var(--color-text)}
|
||||
|
||||
/* ── Kommentare ── */
|
||||
.comment-item{display:flex;gap:0.5rem;margin-bottom:0.5rem}
|
||||
.comment-avatar{width:28px;height:28px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:0.75rem;flex-shrink:0;overflow:hidden}
|
||||
.comment-avatar img{width:100%;height:100%;object-fit:cover}
|
||||
.comment-body{flex:1;background:rgba(255,255,255,0.04);border-radius:6px;padding:0.5rem 0.65rem}
|
||||
.comment-author{font-size:0.8rem;font-weight:600;color:var(--color-text)}
|
||||
.comment-date{font-size:0.72rem;color:var(--color-muted);margin-left:0.4rem}
|
||||
.comment-text{font-size:0.85rem;color:rgba(255,255,255,0.75);margin-top:0.2rem;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
||||
.comment-actions{display:flex;gap:0.4rem;margin-top:0.3rem;align-items:center}
|
||||
.replies-section{margin-top:0.5rem;padding-left:0.5rem;border-left:2px solid rgba(255,255,255,0.06)}
|
||||
.comment-write{display:flex;gap:0.4rem;margin-top:0.5rem}
|
||||
.comment-write input{flex:1;padding:0.4rem 0.75rem;font-size:0.85rem}
|
||||
.comment-write button{width:auto;padding:0.4rem 0.75rem;font-size:0.82rem;white-space:nowrap}
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
|
||||
// ── HTML-Escape ────────────────────────────────────────────────────────────────
|
||||
function esc(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// ── Datum-Format ──────────────────────────────────────────────────────────────
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
// ── Emoji-Picker ──────────────────────────────────────────────────────────────
|
||||
const EMOJIS = ['😊','😂','❤️','😍','🔥','👍','🥰','😎','🤔','😘','💕','🎉','✨','💋','😈','🫦','🍑','🍆','🔞','🥵','😭','😢','😤','🙄','🤦','🤷','🙏','💪','😏','🤩'];
|
||||
let _emojiTarget = null;
|
||||
|
||||
function toggleEmojiPicker(btn, targetId) {
|
||||
_emojiTarget = document.getElementById(targetId);
|
||||
let picker = document.getElementById('sharedEmojiPicker');
|
||||
if (!picker) {
|
||||
picker = document.createElement('div');
|
||||
picker.id = 'sharedEmojiPicker';
|
||||
picker.style.cssText = 'position:fixed;z-index:9000;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.5rem;display:flex;flex-wrap:wrap;gap:0.2rem;max-width:260px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
|
||||
EMOJIS.forEach(em => {
|
||||
const b = document.createElement('button');
|
||||
b.textContent = em;
|
||||
b.style.cssText = 'background:none;border:none;font-size:1.3rem;cursor:pointer;padding:0.2rem;margin:0;width:auto;line-height:1;';
|
||||
b.onclick = e => { e.stopPropagation(); insertEmoji(em); };
|
||||
picker.appendChild(b);
|
||||
});
|
||||
document.body.appendChild(picker);
|
||||
}
|
||||
if (picker.style.display === 'flex') { picker.style.display = 'none'; return; }
|
||||
picker.style.display = 'flex';
|
||||
requestAnimationFrame(() => {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const ph = picker.offsetHeight, pw = picker.offsetWidth;
|
||||
let top = rect.top - ph - 8;
|
||||
let left = rect.left;
|
||||
if (top < 8) top = rect.bottom + 8;
|
||||
if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
|
||||
picker.style.top = top + 'px';
|
||||
picker.style.left = left + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
function insertEmoji(emoji) {
|
||||
if (!_emojiTarget) return;
|
||||
const start = _emojiTarget.selectionStart ?? _emojiTarget.value.length;
|
||||
const end = _emojiTarget.selectionEnd ?? start;
|
||||
_emojiTarget.value = _emojiTarget.value.slice(0, start) + emoji + _emojiTarget.value.slice(end);
|
||||
_emojiTarget.selectionStart = _emojiTarget.selectionEnd = start + emoji.length;
|
||||
_emojiTarget.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const picker = document.getElementById('sharedEmojiPicker');
|
||||
if (picker && picker.style.display === 'flex'
|
||||
&& !picker.contains(e.target)
|
||||
&& !e.target.closest('[onclick*="toggleEmojiPicker"]')) {
|
||||
picker.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// ── Bild-Karussell ────────────────────────────────────────────────────────────
|
||||
function bilderCarousel(bilder) {
|
||||
if (!bilder || bilder.length === 0) return '';
|
||||
if (bilder.length === 1) {
|
||||
return `<div style="margin-top:0.5rem;"><img class="post-bild" src="data:image/jpeg;base64,${bilder[0]}" alt=""></div>`;
|
||||
}
|
||||
const slides = bilder.map((b, i) =>
|
||||
`<div class="car-slide${i === 0 ? ' active' : ''}"><img class="post-bild" src="data:image/jpeg;base64,${b}" alt=""></div>`
|
||||
).join('');
|
||||
return `<div class="post-carousel">
|
||||
${slides}
|
||||
<button class="car-btn car-prev" onclick="event.stopPropagation();carNav(this,-1)">‹</button>
|
||||
<button class="car-btn car-next" onclick="event.stopPropagation();carNav(this,1)">›</button>
|
||||
<div class="car-indicator"><span class="car-cur">1</span>/${bilder.length}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function carNav(btn, dir) {
|
||||
const car = btn.closest('.post-carousel');
|
||||
const slides = Array.from(car.querySelectorAll('.car-slide'));
|
||||
const cur = slides.findIndex(s => s.classList.contains('active'));
|
||||
slides[cur].classList.remove('active');
|
||||
const next = (cur + dir + slides.length) % slides.length;
|
||||
slides[next].classList.add('active');
|
||||
const ind = car.querySelector('.car-cur');
|
||||
if (ind) ind.textContent = next + 1;
|
||||
}
|
||||
|
||||
// ── Kommentar-Rendering ───────────────────────────────────────────────────────
|
||||
// opts: { myUserId, showReplies }
|
||||
// Seite muss definieren: deleteKommentar(kommentarId, targetType, targetId)
|
||||
function renderKommentarHtml(k, targetType, targetId, opts) {
|
||||
const { myUserId = null, showReplies = false } = opts || {};
|
||||
const avatarHtml = k.authorPicture
|
||||
? `<img src="data:image/png;base64,${k.authorPicture}" alt="">`
|
||||
: '◉';
|
||||
const canDelete = k.authorId === myUserId;
|
||||
const replyLabel = k.replyCount > 0 ? `Antworten (${k.replyCount})` : 'Antworten';
|
||||
return `<div class="comment-item" id="kom-${k.kommentarId}">
|
||||
<div class="comment-avatar">${avatarHtml}</div>
|
||||
<div class="comment-body">
|
||||
<span class="comment-author">${esc(k.authorName)}</span>
|
||||
<span class="comment-date">${fmtDate(k.createdAt)}</span>
|
||||
<div class="comment-text">${esc(k.text)}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="btn-like${k.likedByMe ? ' liked' : ''}" id="lk-kom-${k.kommentarId}"
|
||||
onclick="toggleKommentarLike('${k.kommentarId}')">♥ <span id="lkc-kom-${k.kommentarId}">${k.likeCount}</span></button>
|
||||
${showReplies ? `<button class="btn-text" onclick="toggleReplies('${k.kommentarId}')">${replyLabel}</button>` : ''}
|
||||
${canDelete ? `<button class="btn-delete-small" onclick="deleteKommentar('${k.kommentarId}','${targetType}','${targetId}')">✕</button>` : ''}
|
||||
</div>
|
||||
${showReplies ? `<div class="replies-section" id="replies-${k.kommentarId}" style="display:none;"></div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderReplyHtml(r, parentId) {
|
||||
const avatarHtml = r.authorPicture
|
||||
? `<img src="data:image/png;base64,${r.authorPicture}" alt="">`
|
||||
: '◉';
|
||||
const canDelete = typeof window.myUserId !== 'undefined' && r.authorId === window.myUserId;
|
||||
return `<div class="comment-item" id="kom-${r.kommentarId}" style="margin-bottom:0.35rem;">
|
||||
<div class="comment-avatar" style="width:22px;height:22px;font-size:0.75rem;">${avatarHtml}</div>
|
||||
<div class="comment-body" style="padding:0.35rem 0.55rem;">
|
||||
<span class="comment-author">${esc(r.authorName)}</span>
|
||||
<span class="comment-date">${fmtDate(r.createdAt)}</span>
|
||||
<div class="comment-text">${esc(r.text)}</div>
|
||||
<div class="comment-actions">
|
||||
<button class="btn-like${r.likedByMe ? ' liked' : ''}" id="lk-kom-${r.kommentarId}"
|
||||
onclick="toggleKommentarLike('${r.kommentarId}')">♥ <span id="lkc-kom-${r.kommentarId}">${r.likeCount}</span></button>
|
||||
${canDelete ? `<button class="btn-delete-small" onclick="deleteReply('${r.kommentarId}','${parentId}')">✕</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function toggleKommentarLike(kommentarId) {
|
||||
await fetch('/social/kommentare/' + kommentarId + '/like', { method: 'POST' });
|
||||
const btn = document.getElementById('lk-kom-' + kommentarId);
|
||||
const lc = document.getElementById('lkc-kom-' + kommentarId);
|
||||
if (!btn || !lc) return;
|
||||
const was = btn.classList.contains('liked');
|
||||
btn.classList.toggle('liked', !was);
|
||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||
}
|
||||
|
||||
async function toggleReplies(kommentarId) {
|
||||
const section = document.getElementById('replies-' + kommentarId);
|
||||
if (section.style.display === 'none') {
|
||||
section.style.display = '';
|
||||
await loadReplies(kommentarId);
|
||||
} else {
|
||||
section.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReplies(kommentarId) {
|
||||
const res = await fetch(`/social/kommentare?targetType=KOMMENTAR&targetId=${kommentarId}`);
|
||||
const replies = await res.json();
|
||||
const section = document.getElementById('replies-' + kommentarId);
|
||||
section.innerHTML = (replies.length === 0
|
||||
? '<p style="color:var(--color-muted);font-size:0.78rem;margin-bottom:0.35rem;">Noch keine Antworten.</p>'
|
||||
: replies.map(r => renderReplyHtml(r, kommentarId)).join(''))
|
||||
+ `<div class="comment-write">
|
||||
<input type="text" id="ri-${kommentarId}" placeholder="Antwort schreiben…" maxlength="500"
|
||||
onkeydown="if(event.key==='Enter') postReply('${kommentarId}')">
|
||||
<button onclick="postReply('${kommentarId}')">Senden</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function postReply(kommentarId) {
|
||||
const input = document.getElementById('ri-' + kommentarId);
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
await fetch('/social/kommentare', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ targetType: 'KOMMENTAR', targetId: kommentarId, text })
|
||||
});
|
||||
input.value = '';
|
||||
await loadReplies(kommentarId);
|
||||
}
|
||||
|
||||
async function deleteReply(replyId, parentId) {
|
||||
await fetch('/social/kommentare/' + replyId, { method: 'DELETE' });
|
||||
await loadReplies(parentId);
|
||||
}
|
||||
@@ -27,7 +27,8 @@
|
||||
icon: '⊗',
|
||||
items: [
|
||||
{ href: '/infochastity.html', icon: 'ℹ', label: 'Info' },
|
||||
{ href: '/sessionchastity.html', icon: '▷', label: 'Neue Session' },
|
||||
{ href: '/sessionchastity.html', icon: '▷', label: 'Neue Session', id: 'navChastityNeu' },
|
||||
{ href: '#', icon: '▶', label: 'Aktive Session', id: 'navChastityAktiv' },
|
||||
]
|
||||
},
|
||||
];
|
||||
@@ -93,9 +94,12 @@
|
||||
});
|
||||
|
||||
// "Im Spiel" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
|
||||
const navNeu = document.getElementById('navBdsmNeu');
|
||||
const navImSpiel = document.getElementById('navBdsmImSpiel');
|
||||
const navNeu = document.getElementById('navBdsmNeu');
|
||||
const navImSpiel = document.getElementById('navBdsmImSpiel');
|
||||
const navCNeu = document.getElementById('navChastityNeu');
|
||||
const navCAktiv = document.getElementById('navChastityAktiv');
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
if (navCAktiv) navCAktiv.style.display = 'none';
|
||||
|
||||
// Session-Status prüfen
|
||||
fetch('/login/me')
|
||||
@@ -103,13 +107,27 @@
|
||||
.then(async user => {
|
||||
if (!user) return;
|
||||
|
||||
// Session-Status prüfen und Menü anpassen
|
||||
// BDSM Session-Status
|
||||
try {
|
||||
const sessionRes = await fetch(`/session?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 (navCNeu) navCNeu.style.display = 'none';
|
||||
if (navCAktiv) {
|
||||
navCAktiv.style.display = '';
|
||||
navCAktiv.querySelector('a').href = '/sessionchastityingame.html?lockId=' + lockId;
|
||||
}
|
||||
}
|
||||
} catch (_) { /* Menü bleibt im Standardzustand */ }
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Keyholder*In bestätigt – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content" style="max-width:480px;margin:4rem auto;text-align:center;">
|
||||
|
||||
<div id="msgOk" style="display:none;">
|
||||
<div style="font-size:3rem;margin-bottom:1rem;">🔐</div>
|
||||
<h2 style="margin-bottom:0.75rem;">Keyholder*In-Rolle angenommen!</h2>
|
||||
<p style="color:var(--color-muted);margin-bottom:2rem;line-height:1.6;">
|
||||
Du hast die Keyholder*In-Rolle erfolgreich bestätigt.<br>
|
||||
Das Lock läuft ab sofort mit dir als Keyholder*In.
|
||||
</p>
|
||||
<a href="/userhome.html" style="display:inline-block;padding:0.65rem 1.75rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="msgInvalid" style="display:none;">
|
||||
<div style="font-size:3rem;margin-bottom:1rem;">❌</div>
|
||||
<h2 style="margin-bottom:0.75rem;">Link ungültig</h2>
|
||||
<p style="color:var(--color-muted);margin-bottom:2rem;line-height:1.6;">
|
||||
Dieser Bestätigungslink ist abgelaufen oder wurde bereits verwendet.
|
||||
</p>
|
||||
<a href="/userhome.html" style="display:inline-block;padding:0.65rem 1.75rem;background:var(--color-primary);color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">
|
||||
Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const status = new URLSearchParams(window.location.search).get('status');
|
||||
document.getElementById(status === 'ok' ? 'msgOk' : 'msgInvalid').style.display = '';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -407,11 +407,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/image-viewer.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
let currentPicture = null;
|
||||
let currentPictureHq = null;
|
||||
let pictureDirty = false;
|
||||
let allGalleryImages = [];
|
||||
|
||||
fetch('/login/me')
|
||||
.then(r => {
|
||||
@@ -637,19 +640,34 @@
|
||||
}
|
||||
|
||||
function renderOwnGallery(images) {
|
||||
allGalleryImages = images;
|
||||
const grid = document.getElementById('ownGallery');
|
||||
if (images.length === 0) {
|
||||
grid.innerHTML = '<p style="color:var(--color-muted);font-size:0.85rem;grid-column:1/-1;">Noch keine Bilder hochgeladen.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = images.map(img => `
|
||||
<div class="own-thumb">
|
||||
grid.innerHTML = images.map((img, i) => `
|
||||
<div class="own-thumb" onclick="openGalleryViewer(${i})" style="cursor:pointer;">
|
||||
<img src="data:image/jpeg;base64,${img.imageData}" alt="Galerie-Bild">
|
||||
<button class="own-thumb-delete" onclick="deleteGalleryImage('${img.imageId}', event)" title="Bild löschen">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openGalleryViewer(index) {
|
||||
imageViewer.open({
|
||||
images: allGalleryImages.map(img => ({
|
||||
src: 'data:image/jpeg;base64,' + img.imageData,
|
||||
id: img.imageId,
|
||||
likedByMe: false,
|
||||
likeCount: 0
|
||||
})),
|
||||
index,
|
||||
showLike: false,
|
||||
showComments: false
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteGalleryImage(imageId, event) {
|
||||
event.stopPropagation();
|
||||
const res = await fetch('/social/profile-images/' + imageId, { method: 'DELETE' });
|
||||
|
||||
@@ -6,14 +6,941 @@
|
||||
<title>Chastity Game – Neue Session – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.form-section {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-section-title {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
.form-row:last-child { margin-bottom: 0; }
|
||||
.form-row label {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.form-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-muted);
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.form-row input[type="text"],
|
||||
.form-row input[type="number"],
|
||||
.form-row input[type="datetime-local"],
|
||||
.form-row select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox-row:last-child { margin-bottom: 0; }
|
||||
.checkbox-row input[type="checkbox"] {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
.checkbox-row label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Karten-Grid */
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.card-count-item {
|
||||
background: var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 0.7rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
text-align: center;
|
||||
}
|
||||
.card-count-item img {
|
||||
width: 54px;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, opacity 0.15s;
|
||||
}
|
||||
.card-count-item img:hover { transform: scale(1.07); opacity: 0.85; }
|
||||
.card-count-item label {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
/* Stepper */
|
||||
.card-range-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-range-row .range-label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-muted);
|
||||
width: 24px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-muted);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
height: 28px;
|
||||
}
|
||||
.stepper button {
|
||||
width: 24px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: var(--color-card);
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.stepper button:hover { background: var(--color-primary); color: #fff; }
|
||||
.stepper input[type="text"] {
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-muted);
|
||||
border-right: 1px solid var(--color-muted);
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
padding: 0;
|
||||
background: var(--color-card);
|
||||
color: var(--color-text);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.stepper input[type="text"]:focus { outline: none; background: var(--color-secondary); }
|
||||
|
||||
/* Karten-Info-Dialog */
|
||||
.card-info-dialog {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-info-dialog.open { display: flex; }
|
||||
.card-info-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
}
|
||||
.card-info-box {
|
||||
position: relative;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem 1.5rem 1.25rem;
|
||||
max-width: 320px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
z-index: 1;
|
||||
}
|
||||
.card-info-box img {
|
||||
width: 90px;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.card-info-box h3 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.card-info-box p {
|
||||
margin: 0;
|
||||
font-size: 0.88rem;
|
||||
color: var(--color-muted);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card-info-close {
|
||||
margin-top: 0.25rem;
|
||||
width: auto;
|
||||
padding: 0.45rem 1.4rem;
|
||||
}
|
||||
|
||||
/* Aufgaben */
|
||||
.task-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.75rem; }
|
||||
.task-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 80px auto;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
background: var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 0.65rem 0.75rem;
|
||||
}
|
||||
.task-item input[type="text"] { width: 100%; box-sizing: border-box; }
|
||||
.task-item input[type="number"] { width: 100%; box-sizing: border-box; text-align: center; }
|
||||
.task-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(200,50,50,0.7);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
.task-remove:hover { color: #e74c3c; background: none; }
|
||||
.btn-add {
|
||||
background: none;
|
||||
border: 1px dashed var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-add:hover { border-color: var(--color-primary); color: var(--color-primary); background: none; }
|
||||
|
||||
.hygiene-fields { display: none; }
|
||||
.form-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 0.5rem; }
|
||||
.form-actions button { width: auto; padding: 0.65rem 1.5rem; }
|
||||
.error-msg { color: #e74c3c; font-size: 0.85rem; margin-top: 0.4rem; display: none; }
|
||||
|
||||
.field-error input,
|
||||
.field-error .combo-wrap input[type="text"],
|
||||
.field-error .stepper {
|
||||
border-color: #e74c3c !important;
|
||||
}
|
||||
.field-error .stepper { box-shadow: 0 0 0 1px #e74c3c; border-radius: 6px; }
|
||||
.field-error-msg {
|
||||
font-size: 0.78rem;
|
||||
color: #e74c3c;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
.required-star { color: #e74c3c; margin-left: 0.15em; }
|
||||
|
||||
.inline-number { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.inline-number input { width: 90px !important; flex-shrink: 0; }
|
||||
.inline-number span { font-size: 0.9rem; color: var(--color-text); }
|
||||
|
||||
/* Keyholder Combobox */
|
||||
.combo-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.combo-wrap input[type="text"] {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.combo-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 3px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
z-index: 200;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
||||
}
|
||||
.combo-dropdown.open { display: block; }
|
||||
.combo-option {
|
||||
padding: 0.55rem 0.85rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.combo-option:hover,
|
||||
.combo-option.active { background: var(--color-secondary); }
|
||||
.combo-option .combo-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-muted);
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
.combo-empty {
|
||||
padding: 0.55rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<h1>Chastity Game – Neue Session</h1>
|
||||
<p>Session-Setup für das Chastity Game folgt hier.</p>
|
||||
|
||||
<h2 style="margin-bottom:1.25rem;">🔒 Neue Chastity-Session</h2>
|
||||
|
||||
<!-- 1. Grundeinstellungen -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Grundeinstellungen</div>
|
||||
|
||||
<div class="form-row" id="rowLockName">
|
||||
<label for="lockName">Name der Session<span class="required-star">*</span></label>
|
||||
<input type="text" id="lockName" placeholder="z.B. Wochenend-Lock" maxlength="100" required
|
||||
oninput="clearFieldError('rowLockName')">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="keyholderInput">Keyholder*In</label>
|
||||
<div class="combo-wrap" id="keyholderCombo">
|
||||
<input type="text" id="keyholderInput" placeholder="Freund suchen…" autocomplete="off">
|
||||
<div class="combo-dropdown" id="keyholderDropdown"></div>
|
||||
<input type="hidden" id="keyholderValue">
|
||||
</div>
|
||||
<div class="form-hint">Ohne Keyholder läuft das Lock als Self-Lock – du hast selbst jederzeit die Kontrolle.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="unlockCodeLines">Zeilen des Entsperrcodes</label>
|
||||
<div class="inline-number">
|
||||
<input type="number" id="unlockCodeLines" min="1" max="20" value="5">
|
||||
<span>Zeilen</span>
|
||||
</div>
|
||||
<div class="form-hint">Aus wie vielen Zeilen der Entsperrcode bestehen soll.</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="testLock">
|
||||
<label for="testLock">Test-Lock <span class="form-hint">(kein echter Lock, zum Ausprobieren)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Karten -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Karten-Konfiguration</div>
|
||||
|
||||
<div class="form-row" style="margin-bottom:0.75rem;">
|
||||
<label>Startdeck – Anzahl je Kartentyp</label>
|
||||
<div class="form-hint">Das Deck wird aus diesen Karten zusammengestellt. Mindestens eine grüne Karte ist Pflicht<span class="required-star">*</span>.</div>
|
||||
</div>
|
||||
|
||||
<div class="cards-grid" id="cardsGrid"></div>
|
||||
<div class="field-error-msg" id="errorGreenCard" style="display:none; margin-bottom:0.5rem;">Es muss mindestens eine Grüne Karte im Deck sein (Min ≥ 1).</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Karte ziehen alle X Minuten</label>
|
||||
<div class="inline-number">
|
||||
<input type="number" id="pickEvery" min="1" max="99999" value="60">
|
||||
<span>Minuten</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="accumulatePicks">
|
||||
<label for="accumulatePicks">Picks akkumulieren
|
||||
<span class="form-hint">(nicht genutzte Züge bleiben erhalten)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="showRemaining">
|
||||
<label for="showRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Zeitlimits -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Zeitlimits (optional)</div>
|
||||
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<label for="latestOpening">Spätestes Öffnungsdatum</label>
|
||||
<input type="datetime-local" id="latestOpening">
|
||||
<div class="form-hint">Das Lock öffnet spätestens zu diesem Zeitpunkt automatisch.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Hygiene -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Hygiene-Öffnungen (optional)</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="hygieneToggle" onchange="toggleHygiene(this.checked)">
|
||||
<label for="hygieneToggle">Regelmäßige Hygiene-Öffnungen aktivieren</label>
|
||||
</div>
|
||||
|
||||
<div class="hygiene-fields" id="hygieneFields">
|
||||
<div class="form-row">
|
||||
<label>Hygiene-Öffnung alle X Stunden</label>
|
||||
<div class="inline-number">
|
||||
<input type="number" id="hygieneEveryHours" min="1" max="999" value="24">
|
||||
<span>Stunden</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<label>Dauer der Öffnung</label>
|
||||
<div class="inline-number">
|
||||
<input type="number" id="hygieneDuration" min="1" max="9999" value="30">
|
||||
<span>Minuten</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. Aufgaben -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Aufgaben (optional)</div>
|
||||
<div class="form-hint" style="margin-bottom:0.75rem;">
|
||||
Aufgaben werden zufällig bei Aufgaben-Karten zugeteilt. Jede Aufgabe hat eine Beschreibung und eine Dauer in Minuten.
|
||||
</div>
|
||||
<div class="task-list" id="taskList"></div>
|
||||
<button class="btn-add" onclick="addTask()">+ Aufgabe hinzufügen</button>
|
||||
</div>
|
||||
|
||||
<!-- 6. Sicherheit -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Sicherheit</div>
|
||||
<div class="checkbox-row">
|
||||
<input type="checkbox" id="requiresVerification">
|
||||
<label for="requiresVerification">Verifikation erforderlich
|
||||
<span class="form-hint">(Community muss bestätigen, dass Aufgabe erfüllt wurde)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-msg" id="errorMsg"></div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button onclick="history.back()" style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">Abbrechen</button>
|
||||
<button onclick="createSession()">🔒 Session starten</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entsperrcode-Modal -->
|
||||
<div class="card-info-dialog" id="unlockModal">
|
||||
<div class="card-info-overlay"></div>
|
||||
<div class="card-info-box" style="max-width:380px;">
|
||||
<div style="font-size:2rem;">🔒</div>
|
||||
<h3 style="margin:0;">Dein Entsperrcode</h3>
|
||||
<p id="unlockModalHint" style="color:var(--color-muted);font-size:0.85rem;text-align:center;margin:0;">
|
||||
Stelle die Kombination deines Tresors auf den folgenden Code ein und verschließe deinen Schlüssel in diesem.
|
||||
</p>
|
||||
<div id="unlockCodeDisplay" style="
|
||||
font-family: monospace;
|
||||
font-size: 2rem;
|
||||
letter-spacing: 0.3em;
|
||||
background: var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--color-primary);
|
||||
line-height: 1.8;
|
||||
word-break: break-all;
|
||||
"></div>
|
||||
<div id="unlockModalCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);text-align:center;font-family:monospace;"></div>
|
||||
<div id="unlockKeyholderHint" style="display:none;background:var(--color-secondary);border-radius:8px;padding:0.75rem 1rem;font-size:0.85rem;color:var(--color-muted);text-align:center;line-height:1.5;">
|
||||
⏳ Die eingetragene Keyholder*In wurde per E-Mail benachrichtigt und muss die Rolle noch bestätigen.
|
||||
Bis zur Bestätigung läuft das Lock als Self-Lock.
|
||||
</div>
|
||||
<button id="unlockModalBtn" onclick="" style="width:100%;margin-top:0.25rem;">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Karten-Info-Dialog -->
|
||||
<div class="card-info-dialog" id="cardInfoDialog">
|
||||
<div class="card-info-overlay" onclick="closeCardInfo()"></div>
|
||||
<div class="card-info-box">
|
||||
<img id="cardInfoImg" src="" alt="">
|
||||
<h3 id="cardInfoTitle"></h3>
|
||||
<p id="cardInfoDesc"></p>
|
||||
<button class="card-info-close" onclick="closeCardInfo()">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let myUserId = null;
|
||||
|
||||
// ── Boot ──
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
|
||||
if (!user) { window.location.href = '/login.html'; return; }
|
||||
myUserId = user.userId;
|
||||
loadKeyholderOptions(user.userId);
|
||||
});
|
||||
|
||||
let allFriends = [];
|
||||
let comboActiveIdx = -1;
|
||||
|
||||
async function loadKeyholderOptions(myId) {
|
||||
try {
|
||||
allFriends = await fetch('/social/friends/user/' + myId).then(r => r.ok ? r.json() : []);
|
||||
} catch { allFriends = []; }
|
||||
setupKeyholderCombo();
|
||||
}
|
||||
|
||||
function setupKeyholderCombo() {
|
||||
const input = document.getElementById('keyholderInput');
|
||||
const dropdown = document.getElementById('keyholderDropdown');
|
||||
const hidden = document.getElementById('keyholderValue');
|
||||
|
||||
function renderDropdown(query) {
|
||||
const q = query.toLowerCase().trim();
|
||||
const filtered = q
|
||||
? allFriends.filter(f => f.name.toLowerCase().includes(q))
|
||||
: allFriends;
|
||||
|
||||
dropdown.innerHTML = '';
|
||||
comboActiveIdx = -1;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
dropdown.innerHTML = `<div class="combo-empty">${q ? 'Keine Freunde gefunden.' : 'Keine Freunde vorhanden.'}</div>`;
|
||||
} else {
|
||||
filtered.forEach((f, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'combo-option';
|
||||
div.dataset.id = f.userId;
|
||||
div.dataset.name = f.name;
|
||||
div.dataset.idx = i;
|
||||
div.textContent = f.name;
|
||||
div.addEventListener('mousedown', e => {
|
||||
e.preventDefault();
|
||||
selectKeyholder(f.userId, f.name);
|
||||
});
|
||||
dropdown.appendChild(div);
|
||||
});
|
||||
}
|
||||
dropdown.classList.add('open');
|
||||
}
|
||||
|
||||
function selectKeyholder(id, name) {
|
||||
hidden.value = id;
|
||||
input.value = name;
|
||||
dropdown.classList.remove('open');
|
||||
updateTestLock();
|
||||
}
|
||||
|
||||
function clearKeyholder() {
|
||||
hidden.value = '';
|
||||
updateTestLock();
|
||||
}
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearKeyholder();
|
||||
renderDropdown(input.value);
|
||||
});
|
||||
|
||||
input.addEventListener('focus', () => {
|
||||
renderDropdown(input.value);
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
// slight delay so mousedown on option fires first
|
||||
setTimeout(() => {
|
||||
dropdown.classList.remove('open');
|
||||
// if text doesn't match selected, clear hidden value and text
|
||||
if (!hidden.value) input.value = '';
|
||||
}, 150);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', e => {
|
||||
const opts = dropdown.querySelectorAll('.combo-option');
|
||||
if (!dropdown.classList.contains('open')) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
comboActiveIdx = Math.min(comboActiveIdx + 1, opts.length - 1);
|
||||
opts.forEach((o, i) => o.classList.toggle('active', i === comboActiveIdx));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
comboActiveIdx = Math.max(comboActiveIdx - 1, 0);
|
||||
opts.forEach((o, i) => o.classList.toggle('active', i === comboActiveIdx));
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const active = dropdown.querySelector('.combo-option.active');
|
||||
if (active) selectKeyholder(active.dataset.id, active.dataset.name);
|
||||
else dropdown.classList.remove('open');
|
||||
} else if (e.key === 'Escape') {
|
||||
dropdown.classList.remove('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Hygiene Toggle ──
|
||||
function toggleHygiene(on) {
|
||||
document.getElementById('hygieneFields').style.display = on ? 'block' : 'none';
|
||||
if (!on) {
|
||||
document.getElementById('hygieneEveryHours').value = '24';
|
||||
document.getElementById('hygieneDuration').value = '30';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aufgaben ──
|
||||
let taskCounter = 0;
|
||||
function addTask() {
|
||||
const id = ++taskCounter;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'task-item';
|
||||
div.id = 'task-' + id;
|
||||
div.innerHTML = `
|
||||
<input type="text" placeholder="Aufgaben-Beschreibung…" maxlength="300" id="task-text-${id}">
|
||||
<input type="number" min="1" max="9999" placeholder="Min." id="task-min-${id}" title="Dauer in Minuten">
|
||||
<button class="task-remove" onclick="removeTask(${id})" title="Entfernen">✕</button>`;
|
||||
document.getElementById('taskList').appendChild(div);
|
||||
}
|
||||
|
||||
function removeTask(id) {
|
||||
document.getElementById('task-' + id)?.remove();
|
||||
}
|
||||
|
||||
function collectTasks() {
|
||||
return Array.from(document.querySelectorAll('.task-item')).map(item => {
|
||||
const id = item.id.replace('task-', '');
|
||||
const text = document.getElementById('task-text-' + id)?.value.trim();
|
||||
const mins = parseInt(document.getElementById('task-min-' + id)?.value);
|
||||
return text ? { text, minutes: isNaN(mins) ? null : mins } : null;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
// ── Karten-Definitionen ──
|
||||
const CARD_DEFS = [
|
||||
{ id: 'RED', img: '/img/card_red.png', name: 'Rote Karte', desc: 'Verlängert die Sperrzeit. Je nach Konfiguration werden Minuten oder Stunden auf den Timer addiert.', defMin: 3, defMax: 5 },
|
||||
{ id: 'GREEN', img: '/img/card_green.png', name: 'Grüne Karte', desc: 'Verkürzt die Sperrzeit. Eine grüne Karte bringt dich dem Öffnen näher.', defMin: 1, defMax: 2 },
|
||||
{ id: 'YELLOW', img: '/img/card_yellow.png', name: 'Gelbe Karte', desc: 'Neutrales Ereignis – keine Zeitveränderung, aber es passiert trotzdem etwas.', defMin: 1, defMax: 2 },
|
||||
{ id: 'TASK', img: '/img/card_task.png', name: 'Aufgabe', desc: 'Teilt dir eine zufällige Aufgabe aus der Aufgabenliste zu. Die Aufgabe muss erfüllt werden.', defMin: 1, defMax: 2 },
|
||||
{ id: 'FREEZE', img: '/img/card_freeze.png', name: 'Freeze', desc: 'Friert das Lock für eine festgelegte Zeit ein – in diesem Zeitraum können keine Karten gezogen werden.', defMin: 0, defMax: 1 },
|
||||
{ id: 'RESET', img: '/img/card_reset.png', name: 'Reset', desc: 'Setzt das Kartendeck auf den Ausgangszustand zurück. Alle bisher gezogenen Karten kommen wieder rein.', defMin: 0, defMax: 1 },
|
||||
{ id: 'DOUBLE_UP', img: '/img/card_doubleup.png', name: 'Double Up', desc: 'Verdoppelt den Effekt der nächsten gezogenen Karte.', defMin: 0, defMax: 1 },
|
||||
];
|
||||
renderCardsGrid();
|
||||
|
||||
// ── Karten-Grid rendern ──
|
||||
function renderCardsGrid() {
|
||||
const grid = document.getElementById('cardsGrid');
|
||||
grid.innerHTML = '';
|
||||
CARD_DEFS.forEach(c => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'card-count-item';
|
||||
item.innerHTML = `
|
||||
<img src="${c.img}" alt="${c.name}" title="Klicken für Details" onclick="openCardInfo('${c.id}')">
|
||||
<label>${c.name}</label>
|
||||
<div class="card-range-row">
|
||||
<span class="range-label">Min</span>
|
||||
${stepperHtml('min_' + c.id, c.defMin)}
|
||||
</div>
|
||||
<div class="card-range-row">
|
||||
<span class="range-label">Max</span>
|
||||
${stepperHtml('max_' + c.id, c.defMax)}
|
||||
</div>`;
|
||||
grid.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function stepperHtml(id, val) {
|
||||
return `<div class="stepper">
|
||||
<button type="button" onclick="stepperChange('${id}',-1)">−</button>
|
||||
<input type="text" id="${id}" value="${val}" onchange="stepperClamp('${id}')">
|
||||
<button type="button" onclick="stepperChange('${id}',1)">+</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function stepperChange(id, delta) {
|
||||
const el = document.getElementById(id);
|
||||
const val = Math.max(0, (parseInt(el.value) || 0) + delta);
|
||||
el.value = val;
|
||||
}
|
||||
|
||||
function stepperClamp(id) {
|
||||
const el = document.getElementById(id);
|
||||
const val = parseInt(el.value);
|
||||
el.value = isNaN(val) || val < 0 ? 0 : val;
|
||||
}
|
||||
|
||||
// ── Karten-Info-Dialog ──
|
||||
function openCardInfo(cardId) {
|
||||
const c = CARD_DEFS.find(x => x.id === cardId);
|
||||
if (!c) return;
|
||||
document.getElementById('cardInfoImg').src = c.img;
|
||||
document.getElementById('cardInfoImg').alt = c.name;
|
||||
document.getElementById('cardInfoTitle').textContent = c.name;
|
||||
document.getElementById('cardInfoDesc').textContent = c.desc;
|
||||
document.getElementById('cardInfoDialog').classList.add('open');
|
||||
}
|
||||
|
||||
function closeCardInfo() {
|
||||
document.getElementById('cardInfoDialog').classList.remove('open');
|
||||
}
|
||||
|
||||
// ── Karten-Array aufbauen (zufällig zwischen min und max) ──
|
||||
function buildInitialCards() {
|
||||
// Validate first
|
||||
for (const c of CARD_DEFS) {
|
||||
const minVal = parseInt(document.getElementById('min_' + c.id).value) || 0;
|
||||
const maxVal = parseInt(document.getElementById('max_' + c.id).value) || 0;
|
||||
if (minVal > maxVal) {
|
||||
showError(`Min-Wert darf nicht größer als Max-Wert sein (${c.name}).`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const cards = [];
|
||||
CARD_DEFS.forEach(c => {
|
||||
const minVal = parseInt(document.getElementById('min_' + c.id).value) || 0;
|
||||
const maxVal = parseInt(document.getElementById('max_' + c.id).value) || 0;
|
||||
const n = minVal + Math.floor(Math.random() * (maxVal - minVal + 1));
|
||||
for (let i = 0; i < n; i++) cards.push(c.id);
|
||||
});
|
||||
return cards;
|
||||
}
|
||||
|
||||
// ── Datetime → LocalDateTime String ──
|
||||
function toLocalDateTime(val) {
|
||||
if (!val) return null;
|
||||
return val.length === 16 ? val + ':00' : val;
|
||||
}
|
||||
|
||||
// ── Fehler-Highlighting ──
|
||||
function setFieldError(rowId, msg) {
|
||||
const row = document.getElementById(rowId);
|
||||
if (!row) return;
|
||||
row.classList.add('field-error');
|
||||
let el = row.querySelector('.field-error-msg');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.className = 'field-error-msg';
|
||||
row.appendChild(el);
|
||||
}
|
||||
el.textContent = msg;
|
||||
}
|
||||
|
||||
function clearFieldError(rowId) {
|
||||
const row = document.getElementById(rowId);
|
||||
if (!row) return;
|
||||
row.classList.remove('field-error');
|
||||
row.querySelector('.field-error-msg')?.remove();
|
||||
}
|
||||
|
||||
// ── Validierung & Absenden ──
|
||||
async function createSession() {
|
||||
document.getElementById('errorMsg').style.display = 'none';
|
||||
let firstError = null;
|
||||
|
||||
// Name Pflichtfeld
|
||||
const nameVal = document.getElementById('lockName').value.trim();
|
||||
if (!nameVal) {
|
||||
setFieldError('rowLockName', 'Name der Session ist ein Pflichtfeld.');
|
||||
firstError = firstError || document.getElementById('rowLockName');
|
||||
} else {
|
||||
clearFieldError('rowLockName');
|
||||
}
|
||||
|
||||
// Min > Max Prüfung
|
||||
const initialCards = buildInitialCards();
|
||||
if (initialCards === null) return; // buildInitialCards zeigt eigene Fehlermeldung
|
||||
|
||||
// Mindestens eine grüne Karte
|
||||
const greenMin = parseInt(document.getElementById('min_GREEN').value) || 0;
|
||||
const greenErr = document.getElementById('errorGreenCard');
|
||||
const greenItem = document.getElementById('min_GREEN')?.closest('.card-count-item');
|
||||
if (greenMin < 1) {
|
||||
greenErr.style.display = '';
|
||||
if (greenItem) greenItem.style.outline = '2px solid #e74c3c';
|
||||
firstError = firstError || greenErr;
|
||||
} else {
|
||||
greenErr.style.display = 'none';
|
||||
if (greenItem) greenItem.style.outline = '';
|
||||
}
|
||||
|
||||
// Aufgaben-Karten ohne Aufgaben
|
||||
const hasTaskDefs = (parseInt(document.getElementById('min_TASK').value) || 0) > 0
|
||||
|| (parseInt(document.getElementById('max_TASK').value) || 0) > 0;
|
||||
if (hasTaskDefs && collectTasks().length === 0) {
|
||||
showError('Es sind Aufgaben-Karten im Stapel, aber keine Aufgaben definiert. Bitte mindestens eine Aufgabe hinzufügen.');
|
||||
firstError = firstError || document.getElementById('errorMsg');
|
||||
}
|
||||
|
||||
const pickEvery = parseInt(document.getElementById('pickEvery').value);
|
||||
if (isNaN(pickEvery) || pickEvery < 1) {
|
||||
showError('Bitte einen gültigen Kartenzieh-Intervall eingeben.');
|
||||
firstError = firstError || document.getElementById('errorMsg');
|
||||
}
|
||||
|
||||
if (initialCards.length === 0) {
|
||||
showError('Das Deck muss mindestens eine Karte enthalten.');
|
||||
firstError = firstError || document.getElementById('errorMsg');
|
||||
}
|
||||
|
||||
const hygieneOn = document.getElementById('hygieneToggle').checked;
|
||||
const hygieneHours = hygieneOn ? parseInt(document.getElementById('hygieneEveryHours').value) : null;
|
||||
const hygieneDur = hygieneOn ? parseInt(document.getElementById('hygieneDuration').value) : null;
|
||||
if (hygieneOn && (!hygieneHours || hygieneHours < 1)) {
|
||||
showError('Bitte das Hygiene-Intervall in Stunden angeben.');
|
||||
firstError = firstError || document.getElementById('errorMsg');
|
||||
}
|
||||
if (hygieneOn && (!hygieneDur || hygieneDur < 1)) {
|
||||
showError('Bitte die Dauer der Hygiene-Öffnung angeben.');
|
||||
firstError = firstError || document.getElementById('errorMsg');
|
||||
}
|
||||
|
||||
if (firstError) {
|
||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return;
|
||||
}
|
||||
|
||||
const keyholderVal = document.getElementById('keyholderValue').value;
|
||||
|
||||
const body = {
|
||||
name: nameVal,
|
||||
keyholder: keyholderVal || null,
|
||||
initialCards,
|
||||
pickEveryMinute: pickEvery,
|
||||
accumulatePicks: document.getElementById('accumulatePicks').checked,
|
||||
showRemainingCards: document.getElementById('showRemaining').checked,
|
||||
latestOpeningtime: toLocalDateTime(document.getElementById('latestOpening').value),
|
||||
hygineOpeningEveryMinites: hygieneOn ? hygieneHours * 60 : null,
|
||||
hygineOpeningDurationMinutes: hygieneOn ? hygieneDur : null,
|
||||
tasks: collectTasks(),
|
||||
unlockCodeLines: parseInt(document.getElementById('unlockCodeLines').value) || 5,
|
||||
requiresVerification: document.getElementById('requiresVerification').checked,
|
||||
testLock: document.getElementById('testLock').checked,
|
||||
};
|
||||
|
||||
const res = await fetch('/keyholder/cardlock', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
showError(res.status === 400 ? 'Ungültige Eingabe. Bitte alle Pflichtfelder prüfen.' : 'Fehler beim Erstellen der Session.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
showUnlockCodeModal(data.unlockCode, data.lockId, data.keyholderPending);
|
||||
}
|
||||
|
||||
function showUnlockCodeModal(code, lockId, keyholderPending) {
|
||||
const display = document.getElementById('unlockCodeDisplay');
|
||||
display.textContent = code;
|
||||
|
||||
if (keyholderPending) {
|
||||
document.getElementById('unlockKeyholderHint').style.display = '';
|
||||
}
|
||||
|
||||
const url = '/sessionchastityingame.html?lockId=' + lockId
|
||||
+ (keyholderPending ? '&keyholderPending=1' : '');
|
||||
|
||||
const btn = document.getElementById('unlockModalBtn');
|
||||
btn.onclick = () => startCodeScramble(code, url);
|
||||
|
||||
document.getElementById('unlockModal').classList.add('open');
|
||||
}
|
||||
|
||||
function startCodeScramble(realCode, url) {
|
||||
const display = document.getElementById('unlockCodeDisplay');
|
||||
const btn = document.getElementById('unlockModalBtn');
|
||||
const hint = document.getElementById('unlockModalHint');
|
||||
const countdown = document.getElementById('unlockModalCountdown');
|
||||
|
||||
const len = realCode.length;
|
||||
const DURATION = 3 * 60; // seconds
|
||||
let remaining = DURATION;
|
||||
let stopped = false;
|
||||
|
||||
function randomCode() {
|
||||
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||||
}
|
||||
|
||||
function finish() {
|
||||
stopped = true;
|
||||
clearInterval(scrambleInterval);
|
||||
clearInterval(countdownInterval);
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// update hint text and button
|
||||
if (hint) hint.style.display = 'none';
|
||||
countdown.style.display = '';
|
||||
document.querySelector('#unlockModal h3').textContent = 'Nun vergessen wir den Code…';
|
||||
btn.textContent = 'Abbrechen';
|
||||
btn.onclick = finish;
|
||||
|
||||
function updateCountdown() {
|
||||
const m = Math.floor(remaining / 60);
|
||||
const s = remaining % 60;
|
||||
countdown.textContent = `${m}:${String(s).padStart(2, '0')}`;
|
||||
}
|
||||
updateCountdown();
|
||||
|
||||
const scrambleInterval = setInterval(() => { if (!stopped) display.textContent = randomCode(); }, 1000);
|
||||
const countdownInterval = setInterval(() => {
|
||||
if (stopped) return;
|
||||
remaining--;
|
||||
updateCountdown();
|
||||
if (remaining <= 0) finish();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function updateTestLock() {
|
||||
const hasKeyholder = !!document.getElementById('keyholderValue').value;
|
||||
const cb = document.getElementById('testLock');
|
||||
cb.disabled = hasKeyholder;
|
||||
if (hasKeyholder) cb.checked = false;
|
||||
cb.closest('.checkbox-row').style.opacity = hasKeyholder ? '0.4' : '';
|
||||
cb.closest('.checkbox-row').style.pointerEvents = hasKeyholder ? 'none' : '';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const el = document.getElementById('errorMsg');
|
||||
el.textContent = msg;
|
||||
el.style.display = '';
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
447
xxxthegame/src/main/resources/static/sessionchastityingame.html
Normal file
447
xxxthegame/src/main/resources/static/sessionchastityingame.html
Normal file
@@ -0,0 +1,447 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chastity Game – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.keyholder-pending-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
background: rgba(233,69,96,0.12);
|
||||
border: 1px solid rgba(233,69,96,0.35);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.keyholder-pending-banner .icon { font-size: 1.4rem; flex-shrink: 0; }
|
||||
|
||||
/* ── Karten-Anzeige ── */
|
||||
.cards-panel {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.cards-panel-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
/* showRemainingCards = false: only total count */
|
||||
.cards-total {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.cards-total img {
|
||||
width: 38px;
|
||||
height: auto;
|
||||
}
|
||||
.cards-total-count {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.cards-total-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
/* showRemainingCards = true: per-type grid */
|
||||
.cards-by-type {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.card-type-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
background: var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 0.8rem;
|
||||
min-width: 68px;
|
||||
}
|
||||
.card-type-item img {
|
||||
width: 40px;
|
||||
height: auto;
|
||||
}
|
||||
.card-type-count {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.card-type-name {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-muted);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* ── Nächste Karte Panel ── */
|
||||
.nextcard-panel {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.nextcard-panel-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.nextcard-countdown {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ── Hygiene-Panel ── */
|
||||
.hygiene-panel {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.hygiene-panel-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.hygiene-countdown {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.btn-hygiene {
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
padding: 0.5rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ── Lock beenden Button ── */
|
||||
.btn-lock-beenden {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(200,50,50,0.45);
|
||||
color: rgba(200,50,50,0.7);
|
||||
font-size: 0.82rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-top: 1.5rem;
|
||||
width: auto;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.btn-lock-beenden:hover {
|
||||
background: rgba(200,50,50,0.12);
|
||||
color: #e74c3c;
|
||||
border-color: #e74c3c;
|
||||
}
|
||||
|
||||
/* ── Warn-Modal ── */
|
||||
.warn-modal-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.warn-modal-backdrop.open { display: flex; }
|
||||
.warn-modal-box {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 2rem 1.75rem 1.5rem;
|
||||
max-width: 360px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.warn-modal-box h3 { margin: 0; font-size: 1.05rem; }
|
||||
.warn-modal-box p { margin: 0; font-size: 0.88rem; color: var(--color-muted); line-height: 1.55; }
|
||||
.warn-modal-actions { display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 0.25rem; }
|
||||
.warn-modal-actions .btn-cancel {
|
||||
background: none;
|
||||
border: 1px solid var(--color-secondary);
|
||||
color: var(--color-muted);
|
||||
padding: 0.5rem 1.1rem;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
font-size: 0.88rem;
|
||||
width: auto;
|
||||
}
|
||||
.warn-modal-actions .btn-danger {
|
||||
background: #c0392b;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 0.5rem 1.1rem;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
font-size: 0.88rem;
|
||||
width: auto;
|
||||
}
|
||||
.warn-modal-actions .btn-danger:hover { background: #e74c3c; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<h2 style="margin-bottom:1.25rem;">🔒 Chastity Session</h2>
|
||||
|
||||
<div class="keyholder-pending-banner" id="keyholderPendingBanner" style="display:none;">
|
||||
<span class="icon">⏳</span>
|
||||
<div>
|
||||
<strong>Keyholder*In-Bestätigung ausstehend</strong><br>
|
||||
Die eingetragene Keyholder*In wurde per E-Mail eingeladen und muss die Rolle noch bestätigen.
|
||||
Bis zur Bestätigung läuft das Lock als Self-Lock – du hast selbst die Kontrolle.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nächste Karte Panel -->
|
||||
<div class="nextcard-panel" id="nextcardPanel" style="display:none;">
|
||||
<div>
|
||||
<div class="nextcard-panel-title">Nächste Karte ziehbar in</div>
|
||||
<div class="nextcard-countdown" id="nextcardCountdown">–</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hygiene-Panel -->
|
||||
<div class="hygiene-panel" id="hygienePanel" style="display:none;">
|
||||
<div>
|
||||
<div class="hygiene-panel-title">Nächste Hygiene-Öffnung</div>
|
||||
<div class="hygiene-countdown" id="hygieneCountdown">–</div>
|
||||
</div>
|
||||
<button class="btn-hygiene" id="hygieneBtn" style="display:none;">🚿 Hygiene-Öffnung</button>
|
||||
</div>
|
||||
|
||||
<!-- Karten-Panel -->
|
||||
<div class="cards-panel" id="cardsPanel" style="display:none;">
|
||||
<div class="cards-panel-title">Verbleibende Karten</div>
|
||||
<div id="cardsDisplay"></div>
|
||||
</div>
|
||||
|
||||
<div id="lockContent" style="color:var(--color-muted);font-size:0.95rem;">
|
||||
Wird geladen…
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; margin-top:2rem;">
|
||||
<button class="btn-lock-beenden" onclick="lockBeendenFragen()">🔓 Lock beenden</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warn-Modal -->
|
||||
<div class="warn-modal-backdrop" id="warnModal">
|
||||
<div class="warn-modal-box">
|
||||
<h3>Lock wirklich beenden?</h3>
|
||||
<p>
|
||||
Wenn du das Lock beendest, wird der <strong>Entsperrcode unwiderruflich gelöscht</strong>.
|
||||
Der Code kann danach nicht wiederhergestellt werden.
|
||||
</p>
|
||||
<div class="warn-modal-actions">
|
||||
<button class="btn-cancel" onclick="closeWarnModal()">Abbrechen</button>
|
||||
<button class="btn-danger" onclick="lockLoeschen()">Ja, Lock beenden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/shared.js"></script>
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
const CARD_LABELS = {
|
||||
RED: { name: 'Rot', img: '/img/card_red.png' },
|
||||
GREEN: { name: 'Grün', img: '/img/card_green.png' },
|
||||
YELLOW: { name: 'Gelb', img: '/img/card_yellow.png' },
|
||||
TASK: { name: 'Aufgabe', img: '/img/card_task.png' },
|
||||
FREEZE: { name: 'Freeze', img: '/img/card_freeze.png' },
|
||||
RESET: { name: 'Reset', img: '/img/card_reset.png' },
|
||||
DOUBLE_UP: { name: 'Double Up', img: '/img/card_doubleup.png' },
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const lockId = params.get('lockId');
|
||||
const khPending = params.get('keyholderPending') === '1';
|
||||
|
||||
if (khPending) {
|
||||
document.getElementById('keyholderPendingBanner').style.display = '';
|
||||
}
|
||||
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
|
||||
if (!user) { window.location.href = '/login.html'; return; }
|
||||
if (lockId) loadLock();
|
||||
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
||||
});
|
||||
|
||||
async function loadLock() {
|
||||
const res = await fetch('/keyholder/cardlock/' + lockId);
|
||||
if (!res.ok) {
|
||||
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
|
||||
return;
|
||||
}
|
||||
const lock = await res.json();
|
||||
document.getElementById('lockContent').textContent = '';
|
||||
renderNextCardPanel(lock);
|
||||
renderHygienePanel(lock);
|
||||
renderCardsPanel(lock);
|
||||
}
|
||||
|
||||
function renderNextCardPanel(lock) {
|
||||
const panel = document.getElementById('nextcardPanel');
|
||||
const countdown = document.getElementById('nextcardCountdown');
|
||||
panel.style.display = '';
|
||||
|
||||
if (lock.openPicks > 0) {
|
||||
document.querySelector('.nextcard-panel-title').textContent = 'Karten ziehbar';
|
||||
countdown.textContent = lock.openPicks + '× verfügbar';
|
||||
countdown.style.color = 'var(--color-primary)';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lock.nextCardIn) return;
|
||||
|
||||
const target = new Date(lock.nextCardIn);
|
||||
function tick() {
|
||||
const diffMs = target - Date.now();
|
||||
if (diffMs <= 0) {
|
||||
countdown.textContent = 'Jetzt ziehbar';
|
||||
countdown.style.color = 'var(--color-primary)';
|
||||
return;
|
||||
}
|
||||
const totalSec = Math.floor(diffMs / 1000);
|
||||
const h = Math.floor(totalSec / 3600);
|
||||
const m = Math.floor((totalSec % 3600) / 60);
|
||||
const s = totalSec % 60;
|
||||
countdown.textContent = h > 0
|
||||
? `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`
|
||||
: `${m}:${String(s).padStart(2,'0')}`;
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
function renderHygienePanel(lock) {
|
||||
if (!lock.hygieneEnabled) return;
|
||||
const panel = document.getElementById('hygienePanel');
|
||||
const countdown = document.getElementById('hygieneCountdown');
|
||||
const btn = document.getElementById('hygieneBtn');
|
||||
panel.style.display = '';
|
||||
|
||||
if (lock.hygieneOpeningDue) {
|
||||
countdown.textContent = 'Jetzt fällig';
|
||||
countdown.style.color = 'var(--color-primary)';
|
||||
btn.style.display = '';
|
||||
} else {
|
||||
btn.style.display = 'none';
|
||||
// tick down from server-provided remaining minutes
|
||||
let totalSeconds = lock.hygieneMinutesRemaining * 60;
|
||||
function tick() {
|
||||
if (totalSeconds <= 0) {
|
||||
countdown.textContent = 'Jetzt fällig';
|
||||
countdown.style.color = 'var(--color-primary)';
|
||||
btn.style.display = '';
|
||||
return;
|
||||
}
|
||||
const h = Math.floor(totalSeconds / 3600);
|
||||
const m = Math.floor((totalSeconds % 3600) / 60);
|
||||
const s = totalSeconds % 60;
|
||||
countdown.textContent = h > 0
|
||||
? `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`
|
||||
: `${m}:${String(s).padStart(2,'0')}`;
|
||||
totalSeconds--;
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCardsPanel(lock) {
|
||||
const panel = document.getElementById('cardsPanel');
|
||||
const display = document.getElementById('cardsDisplay');
|
||||
panel.style.display = '';
|
||||
|
||||
if (lock.showRemainingCards) {
|
||||
// Show each card type with its count
|
||||
const counts = lock.availableCardCounts || {};
|
||||
const items = Object.entries(counts)
|
||||
.filter(([, n]) => n > 0)
|
||||
.map(([type, n]) => {
|
||||
const info = CARD_LABELS[type] || { name: type, img: '/img/card.png' };
|
||||
return `<div class="card-type-item">
|
||||
<img src="${info.img}" alt="${info.name}">
|
||||
<span class="card-type-name">${info.name}</span>
|
||||
<span class="card-type-count">${n}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
display.innerHTML = `<div class="cards-by-type">${items || '<span style="color:var(--color-muted);font-size:0.85rem;">Keine Karten mehr im Stapel.</span>'}</div>`;
|
||||
} else {
|
||||
// Show only total count with generic card image
|
||||
display.innerHTML = `
|
||||
<div class="cards-total">
|
||||
<img src="/img/card.png" alt="Karten">
|
||||
<div>
|
||||
<div class="cards-total-count">${lock.totalCards}</div>
|
||||
<div class="cards-total-label">Karten verbleiben</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lock beenden ──
|
||||
function lockBeendenFragen() {
|
||||
document.getElementById('warnModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeWarnModal() {
|
||||
document.getElementById('warnModal').classList.remove('open');
|
||||
}
|
||||
|
||||
async function lockLoeschen() {
|
||||
closeWarnModal();
|
||||
try {
|
||||
await fetch('/keyholder/cardlock/' + lockId, { method: 'DELETE' });
|
||||
} catch (_) { /* ignorieren */ }
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeWarnModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user