Social Features weiterentwickelt
This commit is contained in:
@@ -50,6 +50,11 @@ public class SecurityConfig {
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/freunde.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/nachrichten.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/benutzer.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/gruppen.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/gruppe.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/feed.html")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/gruppen/**")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/feed/**")).authenticated()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.html")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/css/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/js/**")).permitAll()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.oaa.xxx.config;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Converter
|
||||
public class StringListConverter implements AttributeConverter<List<String>, String> {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(List<String> list) {
|
||||
if (list == null || list.isEmpty()) return null;
|
||||
try {
|
||||
return mapper.writeValueAsString(list);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> convertToEntityAttribute(String json) {
|
||||
if (json == null || json.isBlank()) return List.of();
|
||||
try {
|
||||
if (!json.startsWith("[")) {
|
||||
// Legacy: single base64 string
|
||||
return List.of(json);
|
||||
}
|
||||
return mapper.readValue(json, new TypeReference<>() {});
|
||||
} catch (Exception e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,10 @@ public class ThemeController {
|
||||
@Value("${app.theme.color-success:#2ecc71}")
|
||||
private String colorSuccess;
|
||||
|
||||
/** Mobile breakpoint in px (unitless integer). Used by sidebar.js and lightbox layout. */
|
||||
@Value("${app.theme.breakpoint-mobile:768}")
|
||||
private int breakpointMobile;
|
||||
|
||||
@GetMapping(value = "/css/variables.css", produces = "text/css")
|
||||
public String variables() {
|
||||
return """
|
||||
@@ -43,7 +47,8 @@ public class ThemeController {
|
||||
--color-text: %s;
|
||||
--color-muted: %s;
|
||||
--color-success: %s;
|
||||
--breakpoint-mobile: %d;
|
||||
}
|
||||
""".formatted(colorBg, colorCard, colorPrimary, colorSecondary, colorText, colorMuted, colorSuccess);
|
||||
""".formatted(colorBg, colorCard, colorPrimary, colorSecondary, colorText, colorMuted, colorSuccess, breakpointMobile);
|
||||
}
|
||||
}
|
||||
|
||||
405
xxxthegame/src/main/java/de/oaa/xxx/feed/FeedController.java
Normal file
405
xxxthegame/src/main/java/de/oaa/xxx/feed/FeedController.java
Normal file
@@ -0,0 +1,405 @@
|
||||
package de.oaa.xxx.feed;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
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.feed.dto.FeedItemDto;
|
||||
import de.oaa.xxx.feed.dto.FeedPostRequest;
|
||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
||||
import de.oaa.xxx.feed.entity.FeedPostLikeEntity;
|
||||
import de.oaa.xxx.feed.entity.FeedPostOptionEntity;
|
||||
import de.oaa.xxx.feed.entity.FeedPostVoteEntity;
|
||||
import de.oaa.xxx.feed.repository.FeedPostLikeRepository;
|
||||
import de.oaa.xxx.feed.repository.FeedPostOptionRepository;
|
||||
import de.oaa.xxx.feed.repository.FeedPostRepository;
|
||||
import de.oaa.xxx.feed.repository.FeedPostVoteRepository;
|
||||
import de.oaa.xxx.gruppe.BeitragTyp;
|
||||
import de.oaa.xxx.gruppe.dto.UmfrageOptionDto;
|
||||
import de.oaa.xxx.gruppe.entity.GruppenbeitragEntity;
|
||||
import de.oaa.xxx.gruppe.entity.UmfrageStimmeEntity;
|
||||
import de.oaa.xxx.gruppe.repository.GruppeRepository;
|
||||
import de.oaa.xxx.gruppe.repository.GruppenbeitragLikeRepository;
|
||||
import de.oaa.xxx.gruppe.repository.GruppenbeitragRepository;
|
||||
import de.oaa.xxx.gruppe.repository.GruppenmitgliedRepository;
|
||||
import de.oaa.xxx.gruppe.repository.UmfrageOptionRepository;
|
||||
import de.oaa.xxx.gruppe.repository.UmfrageStimmeRepository;
|
||||
import de.oaa.xxx.social.entity.FriendshipEntity;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/feed")
|
||||
public class FeedController {
|
||||
|
||||
private final FeedPostRepository feedPostRepository;
|
||||
private final FeedPostLikeRepository feedPostLikeRepository;
|
||||
private final FeedPostOptionRepository feedPostOptionRepository;
|
||||
private final FeedPostVoteRepository feedPostVoteRepository;
|
||||
private final FriendshipRepository friendshipRepository;
|
||||
private final GruppenmitgliedRepository mitgliedRepository;
|
||||
private final GruppenbeitragRepository gruppenbeitragRepository;
|
||||
private final UmfrageOptionRepository umfrageOptionRepository;
|
||||
private final UmfrageStimmeRepository umfrageStimmeRepository;
|
||||
private final GruppenbeitragLikeRepository gruppenbeitragLikeRepository;
|
||||
private final GruppeRepository gruppeRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public FeedController(FeedPostRepository feedPostRepository,
|
||||
FeedPostLikeRepository feedPostLikeRepository,
|
||||
FeedPostOptionRepository feedPostOptionRepository,
|
||||
FeedPostVoteRepository feedPostVoteRepository,
|
||||
FriendshipRepository friendshipRepository,
|
||||
GruppenmitgliedRepository mitgliedRepository,
|
||||
GruppenbeitragRepository gruppenbeitragRepository,
|
||||
UmfrageOptionRepository umfrageOptionRepository,
|
||||
UmfrageStimmeRepository umfrageStimmeRepository,
|
||||
GruppenbeitragLikeRepository gruppenbeitragLikeRepository,
|
||||
GruppeRepository gruppeRepository,
|
||||
KommentarRepository kommentarRepository,
|
||||
UserRepository userRepository) {
|
||||
this.feedPostRepository = feedPostRepository;
|
||||
this.feedPostLikeRepository = feedPostLikeRepository;
|
||||
this.feedPostOptionRepository = feedPostOptionRepository;
|
||||
this.feedPostVoteRepository = feedPostVoteRepository;
|
||||
this.friendshipRepository = friendshipRepository;
|
||||
this.mitgliedRepository = mitgliedRepository;
|
||||
this.gruppenbeitragRepository = gruppenbeitragRepository;
|
||||
this.umfrageOptionRepository = umfrageOptionRepository;
|
||||
this.umfrageStimmeRepository = umfrageStimmeRepository;
|
||||
this.gruppenbeitragLikeRepository = gruppenbeitragLikeRepository;
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record FeedPage(List<FeedItemDto> posts, boolean hasMore) {}
|
||||
record VoteRequest(UUID optionId) {}
|
||||
|
||||
// ── POST /feed/posts ──
|
||||
|
||||
@PostMapping("/posts")
|
||||
public ResponseEntity<FeedItemDto> createPost(@RequestBody FeedPostRequest req, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (req.text() == null || req.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
BeitragTyp typ;
|
||||
try {
|
||||
typ = BeitragTyp.valueOf(req.beitragTyp());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
FeedPostEntity post = new FeedPostEntity();
|
||||
post.setPostId(UUID.randomUUID());
|
||||
post.setAuthorId(myId);
|
||||
post.setText(req.text().trim());
|
||||
post.setBeitragTyp(typ);
|
||||
post.setMultiChoice(typ == BeitragTyp.UMFRAGE ? req.multiChoice() : null);
|
||||
post.setBilder(req.bilder() != null ? req.bilder() : List.of());
|
||||
post.setPublic(req.isPublic());
|
||||
post.setCreatedAt(LocalDateTime.now());
|
||||
feedPostRepository.save(post);
|
||||
|
||||
if (typ == BeitragTyp.UMFRAGE && req.optionen() != null) {
|
||||
for (int i = 0; i < req.optionen().size(); i++) {
|
||||
String optText = req.optionen().get(i);
|
||||
if (optText == null || optText.isBlank()) continue;
|
||||
FeedPostOptionEntity opt = new FeedPostOptionEntity();
|
||||
opt.setOptionId(UUID.randomUUID());
|
||||
opt.setPostId(post.getPostId());
|
||||
opt.setText(optText.trim());
|
||||
opt.setReihenfolge(i);
|
||||
feedPostOptionRepository.save(opt);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.status(201).body(toFeedItemDtoFromPost(post, myId));
|
||||
}
|
||||
|
||||
// ── GET /feed/mine ──
|
||||
|
||||
@GetMapping("/mine")
|
||||
public ResponseEntity<FeedPage> getMyFeed(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
// Collect friend IDs
|
||||
List<UUID> friendIds = friendshipRepository
|
||||
.findFriends(myId, FriendshipEntity.Status.ACCEPTED)
|
||||
.stream()
|
||||
.map(f -> f.getSenderId().equals(myId) ? f.getReceiverId() : f.getSenderId())
|
||||
.toList();
|
||||
|
||||
List<UUID> authorIds = new ArrayList<>(friendIds);
|
||||
authorIds.add(myId);
|
||||
|
||||
// Collect group IDs
|
||||
List<UUID> gruppeIds = mitgliedRepository.findByUserId(myId)
|
||||
.stream()
|
||||
.map(m -> m.getGruppeId())
|
||||
.toList();
|
||||
|
||||
LocalDateTime since = LocalDateTime.now().minusDays(90);
|
||||
|
||||
// Fetch feed posts from friends + self
|
||||
List<FeedPostEntity> feedPosts = feedPostRepository
|
||||
.findByAuthorIdInAndCreatedAtAfterOrderByCreatedAtDesc(authorIds, since);
|
||||
|
||||
// Fetch gruppe posts
|
||||
List<GruppenbeitragEntity> gruppePosts = gruppeIds.isEmpty() ? List.of() :
|
||||
gruppenbeitragRepository.findByGruppeIdInAndCreatedAtAfterOrderByCreatedAtDesc(gruppeIds, since);
|
||||
|
||||
// Merge, convert, sort
|
||||
List<FeedItemDto> merged = Stream.concat(
|
||||
feedPosts.stream().map(p -> toFeedItemDtoFromPost(p, myId)),
|
||||
gruppePosts.stream().map(b -> toFeedItemDtoFromGruppe(b, myId))
|
||||
).sorted(Comparator.comparing(FeedItemDto::createdAt).reversed()).toList();
|
||||
|
||||
int from = page * size;
|
||||
int to = Math.min(from + size, merged.size());
|
||||
List<FeedItemDto> items = from < merged.size() ? merged.subList(from, to) : List.of();
|
||||
boolean hasMore = to < merged.size();
|
||||
|
||||
return ResponseEntity.ok(new FeedPage(items, hasMore));
|
||||
}
|
||||
|
||||
// ── GET /feed/public ──
|
||||
|
||||
@GetMapping("/public")
|
||||
public ResponseEntity<FeedPage> getPublicFeed(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
Slice<FeedPostEntity> slice = feedPostRepository
|
||||
.findByIsPublicTrueOrderByCreatedAtDesc(PageRequest.of(page, size));
|
||||
|
||||
List<FeedItemDto> items = slice.getContent().stream()
|
||||
.map(p -> toFeedItemDtoFromPost(p, myId))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(new FeedPage(items, slice.hasNext()));
|
||||
}
|
||||
|
||||
// ── GET /feed/user/{userId} ──
|
||||
|
||||
@GetMapping("/user/{userId}")
|
||||
public ResponseEntity<FeedPage> getUserPosts(@PathVariable UUID userId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
PageRequest pageable = PageRequest.of(page, size);
|
||||
List<FeedPostEntity> posts;
|
||||
if (myId.equals(userId)) {
|
||||
posts = feedPostRepository.findByAuthorIdOrderByCreatedAtDesc(userId, pageable);
|
||||
} else {
|
||||
posts = feedPostRepository.findByAuthorIdAndIsPublicTrueOrderByCreatedAtDesc(userId, pageable);
|
||||
}
|
||||
|
||||
// Check if there's a next page
|
||||
PageRequest nextPageable = PageRequest.of(page + 1, size);
|
||||
List<FeedPostEntity> nextPage;
|
||||
if (myId.equals(userId)) {
|
||||
nextPage = feedPostRepository.findByAuthorIdOrderByCreatedAtDesc(userId, nextPageable);
|
||||
} else {
|
||||
nextPage = feedPostRepository.findByAuthorIdAndIsPublicTrueOrderByCreatedAtDesc(userId, nextPageable);
|
||||
}
|
||||
|
||||
List<FeedItemDto> items = posts.stream()
|
||||
.map(p -> toFeedItemDtoFromPost(p, myId))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(new FeedPage(items, !nextPage.isEmpty()));
|
||||
}
|
||||
|
||||
// ── POST /feed/posts/{id}/like ──
|
||||
|
||||
@PostMapping("/posts/{id}/like")
|
||||
public ResponseEntity<Void> toggleLike(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (feedPostRepository.findById(id).isEmpty()) return ResponseEntity.notFound().build();
|
||||
|
||||
var existing = feedPostLikeRepository.findByPostIdAndUserId(id, myId);
|
||||
if (existing.isPresent()) {
|
||||
feedPostLikeRepository.delete(existing.get());
|
||||
} else {
|
||||
FeedPostLikeEntity like = new FeedPostLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
like.setPostId(id);
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
feedPostLikeRepository.save(like);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── POST /feed/posts/{id}/vote ──
|
||||
|
||||
@PostMapping("/posts/{id}/vote")
|
||||
public ResponseEntity<Void> vote(@PathVariable UUID id,
|
||||
@RequestBody VoteRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
var postOpt = feedPostRepository.findById(id);
|
||||
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
FeedPostEntity post = postOpt.get();
|
||||
|
||||
var optOpt = feedPostOptionRepository.findById(req.optionId());
|
||||
if (optOpt.isEmpty() || !optOpt.get().getPostId().equals(id))
|
||||
return ResponseEntity.badRequest().build();
|
||||
|
||||
boolean isMultiChoice = Boolean.TRUE.equals(post.getMultiChoice());
|
||||
|
||||
var existingVote = feedPostVoteRepository.findByOptionIdAndUserId(req.optionId(), myId);
|
||||
if (existingVote.isPresent()) {
|
||||
feedPostVoteRepository.delete(existingVote.get());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
if (!isMultiChoice) {
|
||||
List<FeedPostVoteEntity> existing = feedPostVoteRepository.findByPostIdAndUserId(id, myId);
|
||||
feedPostVoteRepository.deleteAll(existing);
|
||||
}
|
||||
|
||||
FeedPostVoteEntity vote = new FeedPostVoteEntity();
|
||||
vote.setStimmeId(UUID.randomUUID());
|
||||
vote.setOptionId(req.optionId());
|
||||
vote.setPostId(id);
|
||||
vote.setUserId(myId);
|
||||
feedPostVoteRepository.save(vote);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── DELETE /feed/posts/{id} ──
|
||||
|
||||
@DeleteMapping("/posts/{id}")
|
||||
public ResponseEntity<Void> deletePost(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
var postOpt = feedPostRepository.findById(id);
|
||||
if (postOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
FeedPostEntity post = postOpt.get();
|
||||
|
||||
if (!post.getAuthorId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
feedPostVoteRepository.deleteByPostId(id);
|
||||
feedPostOptionRepository.deleteByPostId(id);
|
||||
feedPostLikeRepository.deleteByPostId(id);
|
||||
var kommentare = kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("FEED_POST", id);
|
||||
kommentarRepository.deleteAll(kommentare);
|
||||
feedPostRepository.delete(post);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private UUID resolveMyId(Principal principal) {
|
||||
if (principal == null) return null;
|
||||
return userRepository.findByEmail(principal.getName())
|
||||
.map(UserEntity::getUserId)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private FeedItemDto toFeedItemDtoFromPost(FeedPostEntity p, UUID myId) {
|
||||
UserEntity author = userRepository.findById(p.getAuthorId()).orElse(null);
|
||||
long likeCount = feedPostLikeRepository.countByPostId(p.getPostId());
|
||||
boolean likedByMe = feedPostLikeRepository.findByPostIdAndUserId(p.getPostId(), myId).isPresent();
|
||||
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("FEED_POST", p.getPostId());
|
||||
|
||||
List<UmfrageOptionDto> optionen = List.of();
|
||||
List<UUID> myVoteOptionIds = List.of();
|
||||
if (p.getBeitragTyp() == BeitragTyp.UMFRAGE) {
|
||||
optionen = feedPostOptionRepository.findByPostIdOrderByReihenfolge(p.getPostId())
|
||||
.stream()
|
||||
.map(o -> new UmfrageOptionDto(o.getOptionId(), o.getText(), o.getReihenfolge(),
|
||||
feedPostVoteRepository.countByOptionId(o.getOptionId())))
|
||||
.toList();
|
||||
myVoteOptionIds = feedPostVoteRepository.findByPostIdAndUserId(p.getPostId(), myId)
|
||||
.stream()
|
||||
.map(FeedPostVoteEntity::getOptionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return new FeedItemDto(
|
||||
p.getPostId(), "FEED",
|
||||
null, null,
|
||||
p.getAuthorId(),
|
||||
author != null ? author.getName() : "Unbekannt",
|
||||
author != null ? author.getProfilePicture() : null,
|
||||
p.getBeitragTyp().name(), p.getText(), p.getMultiChoice(), p.getBilder(),
|
||||
p.getCreatedAt(),
|
||||
likeCount, likedByMe, kommentarCount,
|
||||
optionen, myVoteOptionIds,
|
||||
p.isPublic()
|
||||
);
|
||||
}
|
||||
|
||||
private FeedItemDto toFeedItemDtoFromGruppe(GruppenbeitragEntity b, UUID myId) {
|
||||
UserEntity author = userRepository.findById(b.getAuthorId()).orElse(null);
|
||||
long likeCount = gruppenbeitragLikeRepository.countByBeitragId(b.getBeitragId());
|
||||
boolean likedByMe = gruppenbeitragLikeRepository.findByBeitragIdAndUserId(b.getBeitragId(), myId).isPresent();
|
||||
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("GROUP_POST", b.getBeitragId());
|
||||
String gruppeName = gruppeRepository.findById(b.getGruppeId())
|
||||
.map(g -> g.getName())
|
||||
.orElse("Gruppe");
|
||||
|
||||
List<UmfrageOptionDto> optionen = List.of();
|
||||
List<UUID> myVoteOptionIds = List.of();
|
||||
if (b.getBeitragTyp() == BeitragTyp.UMFRAGE) {
|
||||
optionen = umfrageOptionRepository.findByBeitragIdOrderByReihenfolge(b.getBeitragId())
|
||||
.stream()
|
||||
.map(o -> new UmfrageOptionDto(o.getOptionId(), o.getText(), o.getReihenfolge(),
|
||||
umfrageStimmeRepository.countByOptionId(o.getOptionId())))
|
||||
.toList();
|
||||
myVoteOptionIds = umfrageStimmeRepository.findByBeitragIdAndUserId(b.getBeitragId(), myId)
|
||||
.stream()
|
||||
.map(UmfrageStimmeEntity::getOptionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return new FeedItemDto(
|
||||
b.getBeitragId(), "GROUP",
|
||||
b.getGruppeId(), gruppeName,
|
||||
b.getAuthorId(),
|
||||
author != null ? author.getName() : "Unbekannt",
|
||||
author != null ? author.getProfilePicture() : null,
|
||||
b.getBeitragTyp().name(), b.getText(), b.getMultiChoice(), b.getBilder(),
|
||||
b.getCreatedAt(),
|
||||
likeCount, likedByMe, kommentarCount,
|
||||
optionen, myVoteOptionIds,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.oaa.xxx.feed.dto;
|
||||
|
||||
import de.oaa.xxx.gruppe.dto.UmfrageOptionDto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record FeedItemDto(
|
||||
UUID postId,
|
||||
String postType, // "FEED" | "GROUP"
|
||||
UUID gruppeId,
|
||||
String gruppeName,
|
||||
UUID authorId,
|
||||
String authorName,
|
||||
String authorPicture,
|
||||
String beitragTyp,
|
||||
String text,
|
||||
Boolean multiChoice,
|
||||
List<String> bilder,
|
||||
LocalDateTime createdAt,
|
||||
long likeCount,
|
||||
boolean likedByMe,
|
||||
long kommentarCount,
|
||||
List<UmfrageOptionDto> optionen,
|
||||
List<UUID> myVoteOptionIds,
|
||||
boolean isPublic
|
||||
) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.oaa.xxx.feed.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record FeedPostRequest(
|
||||
String beitragTyp,
|
||||
String text,
|
||||
Boolean multiChoice,
|
||||
List<String> optionen,
|
||||
List<String> bilder,
|
||||
boolean isPublic
|
||||
) {}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.oaa.xxx.feed.entity;
|
||||
|
||||
import de.oaa.xxx.config.StringListConverter;
|
||||
import de.oaa.xxx.gruppe.BeitragTyp;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "feed_post")
|
||||
public class FeedPostEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID postId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID authorId;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String text;
|
||||
|
||||
@Convert(converter = StringListConverter.class)
|
||||
@Column(name = "bild", columnDefinition = "MEDIUMTEXT")
|
||||
private List<String> bilder;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10)
|
||||
private BeitragTyp beitragTyp;
|
||||
|
||||
@Column
|
||||
private Boolean multiChoice;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean isPublic;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getPostId() { return postId; }
|
||||
public void setPostId(UUID postId) { this.postId = postId; }
|
||||
|
||||
public UUID getAuthorId() { return authorId; }
|
||||
public void setAuthorId(UUID authorId) { this.authorId = authorId; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public List<String> getBilder() { return bilder; }
|
||||
public void setBilder(List<String> bilder) { this.bilder = bilder; }
|
||||
|
||||
public BeitragTyp getBeitragTyp() { return beitragTyp; }
|
||||
public void setBeitragTyp(BeitragTyp beitragTyp) { this.beitragTyp = beitragTyp; }
|
||||
|
||||
public Boolean getMultiChoice() { return multiChoice; }
|
||||
public void setMultiChoice(Boolean multiChoice) { this.multiChoice = multiChoice; }
|
||||
|
||||
public boolean isPublic() { return isPublic; }
|
||||
public void setPublic(boolean aPublic) { isPublic = aPublic; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.oaa.xxx.feed.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "feed_post_like", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = {"postId", "userId"})
|
||||
})
|
||||
public class FeedPostLikeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID likeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID postId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime likedAt;
|
||||
|
||||
public UUID getLikeId() { return likeId; }
|
||||
public void setLikeId(UUID likeId) { this.likeId = likeId; }
|
||||
|
||||
public UUID getPostId() { return postId; }
|
||||
public void setPostId(UUID postId) { this.postId = postId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public LocalDateTime getLikedAt() { return likedAt; }
|
||||
public void setLikedAt(LocalDateTime likedAt) { this.likedAt = likedAt; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.oaa.xxx.feed.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "feed_post_option")
|
||||
public class FeedPostOptionEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID optionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID postId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String text;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int reihenfolge;
|
||||
|
||||
public UUID getOptionId() { return optionId; }
|
||||
public void setOptionId(UUID optionId) { this.optionId = optionId; }
|
||||
|
||||
public UUID getPostId() { return postId; }
|
||||
public void setPostId(UUID postId) { this.postId = postId; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public int getReihenfolge() { return reihenfolge; }
|
||||
public void setReihenfolge(int reihenfolge) { this.reihenfolge = reihenfolge; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.oaa.xxx.feed.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "feed_post_vote")
|
||||
public class FeedPostVoteEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID stimmeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID optionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID postId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
public UUID getStimmeId() { return stimmeId; }
|
||||
public void setStimmeId(UUID stimmeId) { this.stimmeId = stimmeId; }
|
||||
|
||||
public UUID getOptionId() { return optionId; }
|
||||
public void setOptionId(UUID optionId) { this.optionId = optionId; }
|
||||
|
||||
public UUID getPostId() { return postId; }
|
||||
public void setPostId(UUID postId) { this.postId = postId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.oaa.xxx.feed.repository;
|
||||
|
||||
import de.oaa.xxx.feed.entity.FeedPostLikeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FeedPostLikeRepository extends JpaRepository<FeedPostLikeEntity, UUID> {
|
||||
|
||||
Optional<FeedPostLikeEntity> findByPostIdAndUserId(UUID postId, UUID userId);
|
||||
|
||||
long countByPostId(UUID postId);
|
||||
|
||||
@Transactional
|
||||
void deleteByPostId(UUID postId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.oaa.xxx.feed.repository;
|
||||
|
||||
import de.oaa.xxx.feed.entity.FeedPostOptionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FeedPostOptionRepository extends JpaRepository<FeedPostOptionEntity, UUID> {
|
||||
|
||||
List<FeedPostOptionEntity> findByPostIdOrderByReihenfolge(UUID postId);
|
||||
|
||||
@Transactional
|
||||
void deleteByPostId(UUID postId);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.oaa.xxx.feed.repository;
|
||||
|
||||
import de.oaa.xxx.feed.entity.FeedPostEntity;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FeedPostRepository extends JpaRepository<FeedPostEntity, UUID> {
|
||||
|
||||
Slice<FeedPostEntity> findByIsPublicTrueOrderByCreatedAtDesc(Pageable pageable);
|
||||
|
||||
List<FeedPostEntity> findByAuthorIdInAndCreatedAtAfterOrderByCreatedAtDesc(List<UUID> authorIds, LocalDateTime since);
|
||||
|
||||
List<FeedPostEntity> findByAuthorIdAndIsPublicTrueOrderByCreatedAtDesc(UUID authorId, Pageable pageable);
|
||||
|
||||
List<FeedPostEntity> findByAuthorIdOrderByCreatedAtDesc(UUID authorId, Pageable pageable);
|
||||
|
||||
@Transactional
|
||||
void deleteByAuthorId(UUID authorId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.oaa.xxx.feed.repository;
|
||||
|
||||
import de.oaa.xxx.feed.entity.FeedPostVoteEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FeedPostVoteRepository extends JpaRepository<FeedPostVoteEntity, UUID> {
|
||||
|
||||
List<FeedPostVoteEntity> findByPostIdAndUserId(UUID postId, UUID userId);
|
||||
|
||||
Optional<FeedPostVoteEntity> findByOptionIdAndUserId(UUID optionId, UUID userId);
|
||||
|
||||
long countByOptionId(UUID optionId);
|
||||
|
||||
@Transactional
|
||||
void deleteByPostId(UUID postId);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.gruppe;
|
||||
|
||||
public enum AnfrageStatus {
|
||||
AUSSTEHEND, GENEHMIGT, ABGELEHNT
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.gruppe;
|
||||
|
||||
public enum BeitragTyp {
|
||||
TEXT, UMFRAGE
|
||||
}
|
||||
482
xxxthegame/src/main/java/de/oaa/xxx/gruppe/GruppeController.java
Normal file
482
xxxthegame/src/main/java/de/oaa/xxx/gruppe/GruppeController.java
Normal file
@@ -0,0 +1,482 @@
|
||||
package de.oaa.xxx.gruppe;
|
||||
|
||||
import de.oaa.xxx.gruppe.dto.*;
|
||||
import de.oaa.xxx.gruppe.entity.*;
|
||||
import de.oaa.xxx.gruppe.repository.*;
|
||||
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.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/gruppen")
|
||||
public class GruppeController {
|
||||
|
||||
private final GruppeRepository gruppeRepository;
|
||||
private final GruppenmitgliedRepository mitgliedRepository;
|
||||
private final BeitrittsanfrageRepository anfrageRepository;
|
||||
private final GruppenbeitragRepository beitragRepository;
|
||||
private final UmfrageOptionRepository optionRepository;
|
||||
private final UmfrageStimmeRepository stimmeRepository;
|
||||
private final GruppenbeitragLikeRepository likeRepository;
|
||||
private final BeitragMeldungRepository meldungRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public GruppeController(GruppeRepository gruppeRepository,
|
||||
GruppenmitgliedRepository mitgliedRepository,
|
||||
BeitrittsanfrageRepository anfrageRepository,
|
||||
GruppenbeitragRepository beitragRepository,
|
||||
UmfrageOptionRepository optionRepository,
|
||||
UmfrageStimmeRepository stimmeRepository,
|
||||
GruppenbeitragLikeRepository likeRepository,
|
||||
BeitragMeldungRepository meldungRepository,
|
||||
KommentarRepository kommentarRepository,
|
||||
UserRepository userRepository) {
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.mitgliedRepository = mitgliedRepository;
|
||||
this.anfrageRepository = anfrageRepository;
|
||||
this.beitragRepository = beitragRepository;
|
||||
this.optionRepository = optionRepository;
|
||||
this.stimmeRepository = stimmeRepository;
|
||||
this.likeRepository = likeRepository;
|
||||
this.meldungRepository = meldungRepository;
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record CreateGruppeRequest(String name, String beschreibung, String bild, boolean isPrivate) {}
|
||||
record JoinRequest(String nachricht) {}
|
||||
record UpdateGruppeRequest(String name, String beschreibung, String bild, Boolean isPrivate) {}
|
||||
|
||||
// ── GET /gruppe/search?q= ──
|
||||
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<List<GruppeDto>> search(@RequestParam String q, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
List<GruppeDto> result = gruppeRepository.findByNameContainingIgnoreCase(q)
|
||||
.stream()
|
||||
.limit(30)
|
||||
.map(g -> toDto(g, myId))
|
||||
.sorted((a, b) -> {
|
||||
LocalDateTime la = beitragRepository.findFirstByGruppeIdOrderByCreatedAtDesc(a.gruppeId())
|
||||
.map(GruppenbeitragEntity::getCreatedAt).orElse(a.createdAt());
|
||||
LocalDateTime lb = beitragRepository.findFirstByGruppeIdOrderByCreatedAtDesc(b.gruppeId())
|
||||
.map(GruppenbeitragEntity::getCreatedAt).orElse(b.createdAt());
|
||||
return lb.compareTo(la);
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── GET /gruppe/mine ──
|
||||
|
||||
@GetMapping("/mine")
|
||||
public ResponseEntity<List<GruppeDto>> mine(Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
List<GruppeDto> result = mitgliedRepository.findByUserId(myId)
|
||||
.stream()
|
||||
.map(m -> gruppeRepository.findById(m.getGruppeId()).orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.map(g -> toDto(g, myId))
|
||||
.sorted((a, b) -> {
|
||||
LocalDateTime la = beitragRepository.findFirstByGruppeIdOrderByCreatedAtDesc(a.gruppeId())
|
||||
.map(GruppenbeitragEntity::getCreatedAt).orElse(a.createdAt());
|
||||
LocalDateTime lb = beitragRepository.findFirstByGruppeIdOrderByCreatedAtDesc(b.gruppeId())
|
||||
.map(GruppenbeitragEntity::getCreatedAt).orElse(b.createdAt());
|
||||
return lb.compareTo(la);
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── GET /gruppe/{id} ──
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<GruppeDto> getGruppe(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
return gruppeRepository.findById(id)
|
||||
.map(g -> ResponseEntity.ok(toDto(g, myId)))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── POST /gruppe ──
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<GruppeDto> createGruppe(@RequestBody CreateGruppeRequest req, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
GruppeEntity gruppe = new GruppeEntity();
|
||||
gruppe.setGruppeId(UUID.randomUUID());
|
||||
gruppe.setName(req.name().trim());
|
||||
gruppe.setBeschreibung(req.beschreibung());
|
||||
gruppe.setBild(req.bild());
|
||||
gruppe.setPrivate(req.isPrivate());
|
||||
gruppe.setCreatedAt(LocalDateTime.now());
|
||||
gruppe.setCreatedByUserId(myId);
|
||||
gruppeRepository.save(gruppe);
|
||||
|
||||
GruppenmitgliedEntity admin = new GruppenmitgliedEntity();
|
||||
admin.setMitgliedId(UUID.randomUUID());
|
||||
admin.setGruppeId(gruppe.getGruppeId());
|
||||
admin.setUserId(myId);
|
||||
admin.setRolle(GruppenRolle.ADMIN);
|
||||
admin.setJoinedAt(LocalDateTime.now());
|
||||
mitgliedRepository.save(admin);
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(gruppe, myId));
|
||||
}
|
||||
|
||||
// ── PUT /gruppe/{id} ──
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<GruppeDto> updateGruppe(@PathVariable UUID id,
|
||||
@RequestBody UpdateGruppeRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
var gruppeOpt = gruppeRepository.findById(id);
|
||||
if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
GruppeEntity gruppe = gruppeOpt.get();
|
||||
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
if (req.name() != null && !req.name().isBlank()) gruppe.setName(req.name().trim());
|
||||
if (req.beschreibung() != null) gruppe.setBeschreibung(req.beschreibung());
|
||||
if (req.bild() != null) gruppe.setBild(req.bild());
|
||||
if (req.isPrivate() != null) gruppe.setPrivate(req.isPrivate());
|
||||
gruppeRepository.save(gruppe);
|
||||
|
||||
return ResponseEntity.ok(toDto(gruppe, myId));
|
||||
}
|
||||
|
||||
// ── DELETE /gruppe/{id} ──
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteGruppe(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
var gruppeOpt = gruppeRepository.findById(id);
|
||||
if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
// Cascade delete
|
||||
List<GruppenbeitragEntity> beitraege = beitragRepository.findByGruppeIdOrderByCreatedAtDesc(id);
|
||||
for (GruppenbeitragEntity b : beitraege) {
|
||||
UUID bid = b.getBeitragId();
|
||||
meldungRepository.deleteByBeitragId(bid);
|
||||
stimmeRepository.deleteByBeitragId(bid);
|
||||
optionRepository.deleteByBeitragId(bid);
|
||||
likeRepository.deleteByBeitragId(bid);
|
||||
// Kommentare on GROUP_POST
|
||||
List<KommentarEntity> kommentare = kommentarRepository
|
||||
.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("GROUP_POST", bid);
|
||||
for (KommentarEntity k : kommentare) {
|
||||
kommentarRepository.delete(k);
|
||||
}
|
||||
}
|
||||
beitragRepository.deleteByGruppeId(id);
|
||||
anfrageRepository.deleteByGruppeId(id);
|
||||
mitgliedRepository.deleteByGruppeId(id);
|
||||
gruppeRepository.deleteById(id);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── POST /gruppe/{id}/join ──
|
||||
|
||||
@PostMapping("/{id}/join")
|
||||
public ResponseEntity<Void> join(@PathVariable UUID id,
|
||||
@RequestBody(required = false) JoinRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
var gruppeOpt = gruppeRepository.findById(id);
|
||||
if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
GruppeEntity gruppe = gruppeOpt.get();
|
||||
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isPresent())
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
if (gruppe.isPrivate()) {
|
||||
// Check no pending request already
|
||||
var existingReq = anfrageRepository.findByGruppeIdAndUserId(id, myId);
|
||||
if (existingReq.isPresent() && existingReq.get().getStatus() == AnfrageStatus.AUSSTEHEND)
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
BeitrittsanfrageEntity anfrage = new BeitrittsanfrageEntity();
|
||||
anfrage.setAnfrageId(UUID.randomUUID());
|
||||
anfrage.setGruppeId(id);
|
||||
anfrage.setUserId(myId);
|
||||
anfrage.setNachricht(req != null ? req.nachricht() : null);
|
||||
anfrage.setAngefragtAt(LocalDateTime.now());
|
||||
anfrage.setStatus(AnfrageStatus.AUSSTEHEND);
|
||||
anfrageRepository.save(anfrage);
|
||||
return ResponseEntity.status(201).build();
|
||||
} else {
|
||||
GruppenmitgliedEntity mitglied = new GruppenmitgliedEntity();
|
||||
mitglied.setMitgliedId(UUID.randomUUID());
|
||||
mitglied.setGruppeId(id);
|
||||
mitglied.setUserId(myId);
|
||||
mitglied.setRolle(GruppenRolle.MITGLIED);
|
||||
mitglied.setJoinedAt(LocalDateTime.now());
|
||||
mitgliedRepository.save(mitglied);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE /gruppe/{id}/leave ──
|
||||
|
||||
@DeleteMapping("/{id}/leave")
|
||||
public ResponseEntity<Void> leave(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
mitgliedRepository.deleteByGruppeIdAndUserId(id, myId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── GET /gruppe/{id}/members ──
|
||||
|
||||
@GetMapping("/{id}/members")
|
||||
public ResponseEntity<List<Map<String, Object>>> getMembers(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty())
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
List<Map<String, Object>> result = mitgliedRepository.findByGruppeId(id).stream()
|
||||
.map(m -> {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("mitgliedId", m.getMitgliedId());
|
||||
map.put("userId", m.getUserId());
|
||||
map.put("rolle", m.getRolle().name());
|
||||
map.put("joinedAt", m.getJoinedAt());
|
||||
userRepository.findById(m.getUserId()).ifPresent(u -> {
|
||||
map.put("userName", u.getName());
|
||||
map.put("userPicture", u.getProfilePicture());
|
||||
});
|
||||
return map;
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── DELETE /gruppe/{id}/members/{userId} ──
|
||||
|
||||
@DeleteMapping("/{id}/members/{userId}")
|
||||
public ResponseEntity<Void> removeMember(@PathVariable UUID id,
|
||||
@PathVariable UUID userId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
mitgliedRepository.deleteByGruppeIdAndUserId(id, userId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── POST /gruppe/{id}/members/{userId}/promote ──
|
||||
|
||||
@PostMapping("/{id}/members/{userId}/promote")
|
||||
public ResponseEntity<Void> promote(@PathVariable UUID id,
|
||||
@PathVariable UUID userId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var m = mitgliedRepository.findFirstByGruppeIdAndUserId(id, userId);
|
||||
if (m.isEmpty()) return ResponseEntity.notFound().build();
|
||||
m.get().setRolle(GruppenRolle.ADMIN);
|
||||
mitgliedRepository.save(m.get());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── GET /gruppe/{id}/requests ──
|
||||
|
||||
@GetMapping("/{id}/requests")
|
||||
public ResponseEntity<List<BeitrittsanfrageDto>> getRequests(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
List<BeitrittsanfrageDto> dtos = anfrageRepository
|
||||
.findByGruppeIdAndStatus(id, AnfrageStatus.AUSSTEHEND)
|
||||
.stream()
|
||||
.map(a -> {
|
||||
UserEntity u = userRepository.findById(a.getUserId()).orElse(null);
|
||||
return new BeitrittsanfrageDto(a.getAnfrageId(), a.getGruppeId(), a.getUserId(),
|
||||
u != null ? u.getName() : "Unbekannt",
|
||||
u != null ? u.getProfilePicture() : null,
|
||||
a.getNachricht(), a.getAngefragtAt());
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
// ── POST /gruppe/{id}/requests/{reqId}/approve ──
|
||||
|
||||
@PostMapping("/{id}/requests/{reqId}/approve")
|
||||
public ResponseEntity<Void> approveRequest(@PathVariable UUID id,
|
||||
@PathVariable UUID reqId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var anfOpt = anfrageRepository.findById(reqId);
|
||||
if (anfOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
BeitrittsanfrageEntity anfrage = anfOpt.get();
|
||||
anfrage.setStatus(AnfrageStatus.GENEHMIGT);
|
||||
anfrageRepository.save(anfrage);
|
||||
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, anfrage.getUserId()).isEmpty()) {
|
||||
GruppenmitgliedEntity mitglied = new GruppenmitgliedEntity();
|
||||
mitglied.setMitgliedId(UUID.randomUUID());
|
||||
mitglied.setGruppeId(id);
|
||||
mitglied.setUserId(anfrage.getUserId());
|
||||
mitglied.setRolle(GruppenRolle.MITGLIED);
|
||||
mitglied.setJoinedAt(LocalDateTime.now());
|
||||
mitgliedRepository.save(mitglied);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── DELETE /gruppe/{id}/requests/{reqId} ──
|
||||
|
||||
@DeleteMapping("/{id}/requests/{reqId}")
|
||||
public ResponseEntity<Void> rejectRequest(@PathVariable UUID id,
|
||||
@PathVariable UUID reqId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (!isAdmin(id, myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var anfOpt = anfrageRepository.findById(reqId);
|
||||
if (anfOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
BeitrittsanfrageEntity anfrage = anfOpt.get();
|
||||
anfrage.setStatus(AnfrageStatus.ABGELEHNT);
|
||||
anfrageRepository.save(anfrage);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── GET /gruppen/reports/pending/count ──
|
||||
|
||||
@GetMapping("/reports/pending/count")
|
||||
public ResponseEntity<Long> pendingReportsCount(Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
long count = mitgliedRepository.findByUserId(myId).stream()
|
||||
.filter(m -> m.getRolle() == GruppenRolle.ADMIN)
|
||||
.mapToLong(m -> {
|
||||
List<GruppenbeitragEntity> beitraege = beitragRepository.findByGruppeIdOrderByCreatedAtDesc(m.getGruppeId());
|
||||
List<UUID> ids = beitraege.stream().map(GruppenbeitragEntity::getBeitragId).toList();
|
||||
return ids.isEmpty() ? 0L : meldungRepository.findByBeitragIdIn(ids).size();
|
||||
})
|
||||
.sum();
|
||||
return ResponseEntity.ok(count);
|
||||
}
|
||||
|
||||
// ── GET /gruppen/requests/pending/count ──
|
||||
|
||||
@GetMapping("/requests/pending/count")
|
||||
public ResponseEntity<Long> pendingRequestCount(Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
long count = mitgliedRepository.findByUserId(myId).stream()
|
||||
.filter(m -> m.getRolle() == GruppenRolle.ADMIN)
|
||||
.mapToLong(m -> anfrageRepository.findByGruppeIdAndStatus(m.getGruppeId(), AnfrageStatus.AUSSTEHEND).size())
|
||||
.sum();
|
||||
return ResponseEntity.ok(count);
|
||||
}
|
||||
|
||||
// ── GET /gruppe/requests/mine ──
|
||||
|
||||
@GetMapping("/requests/mine")
|
||||
public ResponseEntity<List<Map<String, Object>>> myRequests(Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
List<Map<String, Object>> result = anfrageRepository.findByUserIdAndStatus(myId, AnfrageStatus.AUSSTEHEND)
|
||||
.stream()
|
||||
.map(a -> {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("anfrageId", a.getAnfrageId());
|
||||
map.put("gruppeId", a.getGruppeId());
|
||||
map.put("nachricht", a.getNachricht());
|
||||
map.put("angefragtAt", a.getAngefragtAt());
|
||||
gruppeRepository.findById(a.getGruppeId()).ifPresent(g -> map.put("gruppeName", g.getName()));
|
||||
return map;
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── DELETE /gruppe/{id}/requests/mine (withdraw own request) ──
|
||||
|
||||
@DeleteMapping("/{id}/requests/mine")
|
||||
public ResponseEntity<Void> withdrawRequest(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
anfrageRepository.findByGruppeIdAndUserId(id, myId).ifPresent(a -> {
|
||||
if (a.getStatus() == AnfrageStatus.AUSSTEHEND) {
|
||||
anfrageRepository.delete(a);
|
||||
}
|
||||
});
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private UUID resolveMyId(Principal principal) {
|
||||
if (principal == null) return null;
|
||||
return userRepository.findByEmail(principal.getName())
|
||||
.map(UserEntity::getUserId)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private boolean isAdmin(UUID gruppeId, UUID userId) {
|
||||
return mitgliedRepository.findFirstByGruppeIdAndUserId(gruppeId, userId)
|
||||
.map(m -> m.getRolle() == GruppenRolle.ADMIN)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
GruppeDto toDto(GruppeEntity g, UUID myId) {
|
||||
long memberCount = mitgliedRepository.countByGruppeId(g.getGruppeId());
|
||||
long postCount = beitragRepository.findByGruppeIdOrderByCreatedAtDesc(g.getGruppeId()).size();
|
||||
String myRole = mitgliedRepository.findFirstByGruppeIdAndUserId(g.getGruppeId(), myId)
|
||||
.map(m -> m.getRolle().name())
|
||||
.orElse(null);
|
||||
String myRequestStatus = null;
|
||||
if (myRole == null) {
|
||||
myRequestStatus = anfrageRepository.findByGruppeIdAndUserId(g.getGruppeId(), myId)
|
||||
.filter(a -> a.getStatus() == AnfrageStatus.AUSSTEHEND)
|
||||
.map(a -> a.getStatus().name())
|
||||
.orElse(null);
|
||||
}
|
||||
return new GruppeDto(g.getGruppeId(), g.getName(), g.getBeschreibung(), g.getBild(),
|
||||
g.isPrivate(), g.getCreatedAt(), memberCount, postCount, myRole, myRequestStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.gruppe;
|
||||
|
||||
public enum GruppenRolle {
|
||||
ADMIN, MITGLIED
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package de.oaa.xxx.gruppe;
|
||||
|
||||
import de.oaa.xxx.gruppe.dto.*;
|
||||
import de.oaa.xxx.gruppe.entity.*;
|
||||
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.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
public class GruppenbeitragController {
|
||||
|
||||
private final GruppeRepository gruppeRepository;
|
||||
private final GruppenmitgliedRepository mitgliedRepository;
|
||||
private final GruppenbeitragRepository beitragRepository;
|
||||
private final UmfrageOptionRepository optionRepository;
|
||||
private final UmfrageStimmeRepository stimmeRepository;
|
||||
private final GruppenbeitragLikeRepository likeRepository;
|
||||
private final BeitragMeldungRepository meldungRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public GruppenbeitragController(GruppeRepository gruppeRepository,
|
||||
GruppenmitgliedRepository mitgliedRepository,
|
||||
GruppenbeitragRepository beitragRepository,
|
||||
UmfrageOptionRepository optionRepository,
|
||||
UmfrageStimmeRepository stimmeRepository,
|
||||
GruppenbeitragLikeRepository likeRepository,
|
||||
BeitragMeldungRepository meldungRepository,
|
||||
KommentarRepository kommentarRepository,
|
||||
UserRepository userRepository) {
|
||||
this.gruppeRepository = gruppeRepository;
|
||||
this.mitgliedRepository = mitgliedRepository;
|
||||
this.beitragRepository = beitragRepository;
|
||||
this.optionRepository = optionRepository;
|
||||
this.stimmeRepository = stimmeRepository;
|
||||
this.likeRepository = likeRepository;
|
||||
this.meldungRepository = meldungRepository;
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record CreateBeitragRequest(String beitragTyp, String text, Boolean multiChoice, List<String> optionen, List<String> bilder) {}
|
||||
record PostsPage(List<GruppenbeitragDto> posts, boolean hasMore) {}
|
||||
record VoteRequest(UUID optionId) {}
|
||||
record ReportRequest(String grund) {}
|
||||
|
||||
// ── GET /gruppen/{id}/posts/{postId} ──
|
||||
|
||||
@GetMapping("/gruppen/{id}/posts/{postId}")
|
||||
public ResponseEntity<GruppenbeitragDto> getPost(@PathVariable UUID id,
|
||||
@PathVariable UUID postId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty())
|
||||
return ResponseEntity.status(403).build();
|
||||
return beitragRepository.findById(postId)
|
||||
.map(b -> ResponseEntity.ok(toDto(b, myId)))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── GET /gruppen/{id}/posts ──
|
||||
|
||||
@GetMapping("/gruppen/{id}/posts")
|
||||
public ResponseEntity<PostsPage> getPosts(@PathVariable UUID id,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty())
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
Slice<GruppenbeitragEntity> slice = beitragRepository
|
||||
.findByGruppeIdOrderByCreatedAtDesc(id, PageRequest.of(page, size));
|
||||
List<GruppenbeitragDto> posts = slice.getContent().stream()
|
||||
.map(b -> toDto(b, myId))
|
||||
.toList();
|
||||
return ResponseEntity.ok(new PostsPage(posts, slice.hasNext()));
|
||||
}
|
||||
|
||||
// ── POST /gruppen/{id}/posts ──
|
||||
|
||||
@PostMapping("/gruppen/{id}/posts")
|
||||
public ResponseEntity<GruppenbeitragDto> createPost(@PathVariable UUID id,
|
||||
@RequestBody CreateBeitragRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty())
|
||||
return ResponseEntity.status(403).build();
|
||||
if (gruppeRepository.findById(id).isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (req.text() == null || req.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
BeitragTyp typ;
|
||||
try {
|
||||
typ = BeitragTyp.valueOf(req.beitragTyp());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
GruppenbeitragEntity beitrag = new GruppenbeitragEntity();
|
||||
beitrag.setBeitragId(UUID.randomUUID());
|
||||
beitrag.setGruppeId(id);
|
||||
beitrag.setAuthorId(myId);
|
||||
beitrag.setBeitragTyp(typ);
|
||||
beitrag.setText(req.text().trim());
|
||||
beitrag.setMultiChoice(typ == BeitragTyp.UMFRAGE ? req.multiChoice() : null);
|
||||
beitrag.setBilder(req.bilder() != null ? req.bilder() : List.of());
|
||||
beitrag.setCreatedAt(LocalDateTime.now());
|
||||
beitragRepository.save(beitrag);
|
||||
|
||||
if (typ == BeitragTyp.UMFRAGE && req.optionen() != null) {
|
||||
for (int i = 0; i < req.optionen().size(); i++) {
|
||||
String optText = req.optionen().get(i);
|
||||
if (optText == null || optText.isBlank()) continue;
|
||||
UmfrageOptionEntity opt = new UmfrageOptionEntity();
|
||||
opt.setOptionId(UUID.randomUUID());
|
||||
opt.setBeitragId(beitrag.getBeitragId());
|
||||
opt.setText(optText.trim());
|
||||
opt.setReihenfolge(i);
|
||||
optionRepository.save(opt);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(beitrag, myId));
|
||||
}
|
||||
|
||||
// ── DELETE /gruppen/{id}/posts/{postId} ──
|
||||
|
||||
@DeleteMapping("/gruppen/{id}/posts/{postId}")
|
||||
public ResponseEntity<Void> deletePost(@PathVariable UUID id,
|
||||
@PathVariable UUID postId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
var bOpt = beitragRepository.findById(postId);
|
||||
if (bOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
GruppenbeitragEntity beitrag = bOpt.get();
|
||||
|
||||
boolean isAuthor = beitrag.getAuthorId().equals(myId);
|
||||
boolean isAdmin = mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId)
|
||||
.map(m -> m.getRolle() == GruppenRolle.ADMIN)
|
||||
.orElse(false);
|
||||
if (!isAuthor && !isAdmin) return ResponseEntity.status(403).build();
|
||||
|
||||
deleteBeitragCascade(beitrag);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── POST /gruppen/{id}/posts/{postId}/like ──
|
||||
|
||||
@PostMapping("/gruppen/{id}/posts/{postId}/like")
|
||||
public ResponseEntity<Void> toggleLike(@PathVariable UUID id,
|
||||
@PathVariable UUID postId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty())
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
var existing = likeRepository.findByBeitragIdAndUserId(postId, myId);
|
||||
if (existing.isPresent()) {
|
||||
likeRepository.delete(existing.get());
|
||||
} else {
|
||||
GruppenbeitragLikeEntity like = new GruppenbeitragLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
like.setBeitragId(postId);
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
likeRepository.save(like);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── POST /gruppen/{id}/posts/{postId}/vote ──
|
||||
|
||||
@PostMapping("/gruppen/{id}/posts/{postId}/vote")
|
||||
public ResponseEntity<Void> vote(@PathVariable UUID id,
|
||||
@PathVariable UUID postId,
|
||||
@RequestBody VoteRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty())
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
var bOpt = beitragRepository.findById(postId);
|
||||
if (bOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
GruppenbeitragEntity beitrag = bOpt.get();
|
||||
|
||||
var optOpt = optionRepository.findById(req.optionId());
|
||||
if (optOpt.isEmpty() || !optOpt.get().getBeitragId().equals(postId))
|
||||
return ResponseEntity.badRequest().build();
|
||||
|
||||
boolean isMultiChoice = Boolean.TRUE.equals(beitrag.getMultiChoice());
|
||||
|
||||
var existingVote = stimmeRepository.findByOptionIdAndUserId(req.optionId(), myId);
|
||||
if (existingVote.isPresent()) {
|
||||
// Toggle: remove vote
|
||||
stimmeRepository.delete(existingVote.get());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
if (!isMultiChoice) {
|
||||
// Single-choice: remove all existing votes on this poll
|
||||
List<UmfrageStimmeEntity> existing = stimmeRepository.findByBeitragIdAndUserId(postId, myId);
|
||||
stimmeRepository.deleteAll(existing);
|
||||
}
|
||||
|
||||
UmfrageStimmeEntity stimme = new UmfrageStimmeEntity();
|
||||
stimme.setStimmeId(UUID.randomUUID());
|
||||
stimme.setOptionId(req.optionId());
|
||||
stimme.setBeitragId(postId);
|
||||
stimme.setUserId(myId);
|
||||
stimmeRepository.save(stimme);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// ── POST /gruppen/{id}/posts/{postId}/report ──
|
||||
|
||||
@PostMapping("/gruppen/{id}/posts/{postId}/report")
|
||||
public ResponseEntity<Void> report(@PathVariable UUID id,
|
||||
@PathVariable UUID postId,
|
||||
@RequestBody ReportRequest req,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty())
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
if (beitragRepository.findById(postId).isEmpty()) return ResponseEntity.notFound().build();
|
||||
|
||||
var existing = meldungRepository.findByBeitragIdAndMelderId(postId, myId);
|
||||
if (existing.isPresent()) return ResponseEntity.status(409).build();
|
||||
|
||||
BeitragMeldungEntity meldung = new BeitragMeldungEntity();
|
||||
meldung.setMeldungId(UUID.randomUUID());
|
||||
meldung.setBeitragId(postId);
|
||||
meldung.setMelderId(myId);
|
||||
meldung.setGrund(req.grund());
|
||||
meldung.setGemeldetAt(LocalDateTime.now());
|
||||
meldungRepository.save(meldung);
|
||||
return ResponseEntity.status(201).build();
|
||||
}
|
||||
|
||||
// ── GET /gruppen/{id}/reports ──
|
||||
|
||||
@GetMapping("/gruppen/{id}/reports")
|
||||
public ResponseEntity<List<BeitragMeldungDto>> getReports(@PathVariable UUID id, Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId)
|
||||
.map(m -> m.getRolle() != GruppenRolle.ADMIN).orElse(true))
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
List<UUID> beitragIds = beitragRepository.findByGruppeIdOrderByCreatedAtDesc(id)
|
||||
.stream().map(GruppenbeitragEntity::getBeitragId).toList();
|
||||
|
||||
List<BeitragMeldungDto> dtos = meldungRepository.findByBeitragIdIn(beitragIds)
|
||||
.stream()
|
||||
.map(m -> {
|
||||
UserEntity melder = userRepository.findById(m.getMelderId()).orElse(null);
|
||||
return new BeitragMeldungDto(m.getMeldungId(), m.getBeitragId(), m.getMelderId(),
|
||||
melder != null ? melder.getName() : "Unbekannt",
|
||||
m.getGrund(), m.getGemeldetAt());
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
// ── DELETE /gruppen/{id}/reports/{meldungId} ──
|
||||
|
||||
@DeleteMapping("/gruppen/{id}/reports/{meldungId}")
|
||||
public ResponseEntity<Void> dismissReport(@PathVariable UUID id,
|
||||
@PathVariable UUID meldungId,
|
||||
Principal principal) {
|
||||
UUID myId = resolveMyId(principal);
|
||||
if (myId == null) return ResponseEntity.status(401).build();
|
||||
if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId)
|
||||
.map(m -> m.getRolle() != GruppenRolle.ADMIN).orElse(true))
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
meldungRepository.deleteById(meldungId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private UUID resolveMyId(Principal principal) {
|
||||
if (principal == null) return null;
|
||||
return userRepository.findByEmail(principal.getName())
|
||||
.map(UserEntity::getUserId)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void deleteBeitragCascade(GruppenbeitragEntity beitrag) {
|
||||
UUID bid = beitrag.getBeitragId();
|
||||
meldungRepository.deleteByBeitragId(bid);
|
||||
stimmeRepository.deleteByBeitragId(bid);
|
||||
optionRepository.deleteByBeitragId(bid);
|
||||
likeRepository.deleteByBeitragId(bid);
|
||||
var kommentare = kommentarRepository
|
||||
.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("GROUP_POST", bid);
|
||||
kommentarRepository.deleteAll(kommentare);
|
||||
beitragRepository.delete(beitrag);
|
||||
}
|
||||
|
||||
private GruppenbeitragDto toDto(GruppenbeitragEntity b, UUID myId) {
|
||||
UserEntity author = userRepository.findById(b.getAuthorId()).orElse(null);
|
||||
long likeCount = likeRepository.countByBeitragId(b.getBeitragId());
|
||||
boolean likedByMe = likeRepository.findByBeitragIdAndUserId(b.getBeitragId(), myId).isPresent();
|
||||
long kommentarCount = kommentarRepository
|
||||
.countByTargetTypeAndTargetId("GROUP_POST", b.getBeitragId());
|
||||
boolean reported = meldungRepository.findByBeitragIdAndMelderId(b.getBeitragId(), myId).isPresent();
|
||||
|
||||
List<UmfrageOptionDto> optionen = List.of();
|
||||
List<UUID> myVoteOptionIds = List.of();
|
||||
if (b.getBeitragTyp() == BeitragTyp.UMFRAGE) {
|
||||
optionen = optionRepository.findByBeitragIdOrderByReihenfolge(b.getBeitragId())
|
||||
.stream()
|
||||
.map(o -> new UmfrageOptionDto(o.getOptionId(), o.getText(), o.getReihenfolge(),
|
||||
stimmeRepository.countByOptionId(o.getOptionId())))
|
||||
.toList();
|
||||
myVoteOptionIds = stimmeRepository.findByBeitragIdAndUserId(b.getBeitragId(), myId)
|
||||
.stream()
|
||||
.map(UmfrageStimmeEntity::getOptionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return new GruppenbeitragDto(
|
||||
b.getBeitragId(), b.getGruppeId(), b.getAuthorId(),
|
||||
author != null ? author.getName() : "Unbekannt",
|
||||
author != null ? author.getProfilePicture() : null,
|
||||
b.getBeitragTyp().name(), b.getText(), b.getMultiChoice(), b.getBilder(), b.getCreatedAt(),
|
||||
likeCount, likedByMe, kommentarCount, optionen, myVoteOptionIds, reported);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.oaa.xxx.gruppe.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record BeitragMeldungDto(
|
||||
UUID meldungId,
|
||||
UUID beitragId,
|
||||
UUID melderId,
|
||||
String melderName,
|
||||
String grund,
|
||||
LocalDateTime gemeldetAt
|
||||
) {}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.oaa.xxx.gruppe.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record BeitrittsanfrageDto(
|
||||
UUID anfrageId,
|
||||
UUID gruppeId,
|
||||
UUID userId,
|
||||
String userName,
|
||||
String userPicture,
|
||||
String nachricht,
|
||||
LocalDateTime angefragtAt
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.oaa.xxx.gruppe.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record GruppeDto(
|
||||
UUID gruppeId,
|
||||
String name,
|
||||
String beschreibung,
|
||||
String bild,
|
||||
boolean isPrivate,
|
||||
LocalDateTime createdAt,
|
||||
long memberCount,
|
||||
long postCount,
|
||||
String myRole,
|
||||
String myRequestStatus
|
||||
) {}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.oaa.xxx.gruppe.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record GruppenbeitragDto(
|
||||
UUID beitragId,
|
||||
UUID gruppeId,
|
||||
UUID authorId,
|
||||
String authorName,
|
||||
String authorPicture,
|
||||
String beitragTyp,
|
||||
String text,
|
||||
Boolean multiChoice,
|
||||
List<String> bilder,
|
||||
LocalDateTime createdAt,
|
||||
long likeCount,
|
||||
boolean likedByMe,
|
||||
long kommentarCount,
|
||||
List<UmfrageOptionDto> optionen,
|
||||
List<UUID> myVoteOptionIds,
|
||||
boolean reported
|
||||
) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.oaa.xxx.gruppe.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record UmfrageOptionDto(
|
||||
UUID optionId,
|
||||
String text,
|
||||
int reihenfolge,
|
||||
long stimmenCount
|
||||
) {}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "beitrag_meldung",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"beitragId", "melderId"}))
|
||||
public class BeitragMeldungEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID meldungId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID beitragId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID melderId;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String grund;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime gemeldetAt;
|
||||
|
||||
public UUID getMeldungId() { return meldungId; }
|
||||
public void setMeldungId(UUID meldungId) { this.meldungId = meldungId; }
|
||||
|
||||
public UUID getBeitragId() { return beitragId; }
|
||||
public void setBeitragId(UUID beitragId) { this.beitragId = beitragId; }
|
||||
|
||||
public UUID getMelderId() { return melderId; }
|
||||
public void setMelderId(UUID melderId) { this.melderId = melderId; }
|
||||
|
||||
public String getGrund() { return grund; }
|
||||
public void setGrund(String grund) { this.grund = grund; }
|
||||
|
||||
public LocalDateTime getGemeldetAt() { return gemeldetAt; }
|
||||
public void setGemeldetAt(LocalDateTime gemeldetAt) { this.gemeldetAt = gemeldetAt; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import de.oaa.xxx.gruppe.AnfrageStatus;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "beitrittsanfrage")
|
||||
public class BeitrittsanfrageEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID anfrageId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID gruppeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String nachricht;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime angefragtAt;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 15)
|
||||
private AnfrageStatus status;
|
||||
|
||||
public UUID getAnfrageId() { return anfrageId; }
|
||||
public void setAnfrageId(UUID anfrageId) { this.anfrageId = anfrageId; }
|
||||
|
||||
public UUID getGruppeId() { return gruppeId; }
|
||||
public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public String getNachricht() { return nachricht; }
|
||||
public void setNachricht(String nachricht) { this.nachricht = nachricht; }
|
||||
|
||||
public LocalDateTime getAngefragtAt() { return angefragtAt; }
|
||||
public void setAngefragtAt(LocalDateTime angefragtAt) { this.angefragtAt = angefragtAt; }
|
||||
|
||||
public AnfrageStatus getStatus() { return status; }
|
||||
public void setStatus(AnfrageStatus status) { this.status = status; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gruppe")
|
||||
public class GruppeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID gruppeId;
|
||||
|
||||
@Column(length = 100, nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String beschreibung;
|
||||
|
||||
@Column(columnDefinition = "MEDIUMTEXT")
|
||||
private String bild;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean isPrivate = false;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID createdByUserId;
|
||||
|
||||
public UUID getGruppeId() { return gruppeId; }
|
||||
public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getBeschreibung() { return beschreibung; }
|
||||
public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
|
||||
|
||||
public String getBild() { return bild; }
|
||||
public void setBild(String bild) { this.bild = bild; }
|
||||
|
||||
public boolean isPrivate() { return isPrivate; }
|
||||
public void setPrivate(boolean aPrivate) { isPrivate = aPrivate; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public UUID getCreatedByUserId() { return createdByUserId; }
|
||||
public void setCreatedByUserId(UUID createdByUserId) { this.createdByUserId = createdByUserId; }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import de.oaa.xxx.config.StringListConverter;
|
||||
import de.oaa.xxx.gruppe.BeitragTyp;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gruppe_beitrag")
|
||||
public class GruppenbeitragEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID beitragId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID gruppeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID authorId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10)
|
||||
private BeitragTyp beitragTyp;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
private String text;
|
||||
|
||||
@Column
|
||||
private Boolean multiChoice;
|
||||
|
||||
@Convert(converter = StringListConverter.class)
|
||||
@Column(name = "bild", columnDefinition = "MEDIUMTEXT")
|
||||
private List<String> bilder;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getBeitragId() { return beitragId; }
|
||||
public void setBeitragId(UUID beitragId) { this.beitragId = beitragId; }
|
||||
|
||||
public UUID getGruppeId() { return gruppeId; }
|
||||
public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; }
|
||||
|
||||
public UUID getAuthorId() { return authorId; }
|
||||
public void setAuthorId(UUID authorId) { this.authorId = authorId; }
|
||||
|
||||
public BeitragTyp getBeitragTyp() { return beitragTyp; }
|
||||
public void setBeitragTyp(BeitragTyp beitragTyp) { this.beitragTyp = beitragTyp; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public Boolean getMultiChoice() { return multiChoice; }
|
||||
public void setMultiChoice(Boolean multiChoice) { this.multiChoice = multiChoice; }
|
||||
|
||||
public List<String> getBilder() { return bilder; }
|
||||
public void setBilder(List<String> bilder) { this.bilder = bilder; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gruppe_beitrag_like",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"beitragId", "userId"}))
|
||||
public class GruppenbeitragLikeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID likeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID beitragId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime likedAt;
|
||||
|
||||
public UUID getLikeId() { return likeId; }
|
||||
public void setLikeId(UUID likeId) { this.likeId = likeId; }
|
||||
|
||||
public UUID getBeitragId() { return beitragId; }
|
||||
public void setBeitragId(UUID beitragId) { this.beitragId = beitragId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public LocalDateTime getLikedAt() { return likedAt; }
|
||||
public void setLikedAt(LocalDateTime likedAt) { this.likedAt = likedAt; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import de.oaa.xxx.gruppe.GruppenRolle;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "gruppe_mitglied",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"gruppeId", "userId"}))
|
||||
public class GruppenmitgliedEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID mitgliedId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID gruppeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10)
|
||||
private GruppenRolle rolle;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime joinedAt;
|
||||
|
||||
public UUID getMitgliedId() { return mitgliedId; }
|
||||
public void setMitgliedId(UUID mitgliedId) { this.mitgliedId = mitgliedId; }
|
||||
|
||||
public UUID getGruppeId() { return gruppeId; }
|
||||
public void setGruppeId(UUID gruppeId) { this.gruppeId = gruppeId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public GruppenRolle getRolle() { return rolle; }
|
||||
public void setRolle(GruppenRolle rolle) { this.rolle = rolle; }
|
||||
|
||||
public LocalDateTime getJoinedAt() { return joinedAt; }
|
||||
public void setJoinedAt(LocalDateTime joinedAt) { this.joinedAt = joinedAt; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "umfrage_option")
|
||||
public class UmfrageOptionEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID optionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID beitragId;
|
||||
|
||||
@Column(length = 200, nullable = false)
|
||||
private String text;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int reihenfolge;
|
||||
|
||||
public UUID getOptionId() { return optionId; }
|
||||
public void setOptionId(UUID optionId) { this.optionId = optionId; }
|
||||
|
||||
public UUID getBeitragId() { return beitragId; }
|
||||
public void setBeitragId(UUID beitragId) { this.beitragId = beitragId; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public int getReihenfolge() { return reihenfolge; }
|
||||
public void setReihenfolge(int reihenfolge) { this.reihenfolge = reihenfolge; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.gruppe.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "umfrage_stimme",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"optionId", "userId"}))
|
||||
public class UmfrageStimmeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID stimmeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID optionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID beitragId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
public UUID getStimmeId() { return stimmeId; }
|
||||
public void setStimmeId(UUID stimmeId) { this.stimmeId = stimmeId; }
|
||||
|
||||
public UUID getOptionId() { return optionId; }
|
||||
public void setOptionId(UUID optionId) { this.optionId = optionId; }
|
||||
|
||||
public UUID getBeitragId() { return beitragId; }
|
||||
public void setBeitragId(UUID beitragId) { this.beitragId = beitragId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.entity.BeitragMeldungEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface BeitragMeldungRepository extends JpaRepository<BeitragMeldungEntity, UUID> {
|
||||
|
||||
List<BeitragMeldungEntity> findByBeitragId(UUID beitragId);
|
||||
|
||||
List<BeitragMeldungEntity> findByBeitragIdIn(Collection<UUID> beitragIds);
|
||||
|
||||
Optional<BeitragMeldungEntity> findByBeitragIdAndMelderId(UUID beitragId, UUID melderId);
|
||||
|
||||
@Transactional
|
||||
void deleteByBeitragId(UUID beitragId);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.AnfrageStatus;
|
||||
import de.oaa.xxx.gruppe.entity.BeitrittsanfrageEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface BeitrittsanfrageRepository extends JpaRepository<BeitrittsanfrageEntity, UUID> {
|
||||
|
||||
List<BeitrittsanfrageEntity> findByGruppeIdAndStatus(UUID gruppeId, AnfrageStatus status);
|
||||
|
||||
List<BeitrittsanfrageEntity> findByUserIdAndStatus(UUID userId, AnfrageStatus status);
|
||||
|
||||
Optional<BeitrittsanfrageEntity> findByGruppeIdAndUserId(UUID gruppeId, UUID userId);
|
||||
|
||||
@Transactional
|
||||
void deleteByGruppeId(UUID gruppeId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.entity.GruppeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GruppeRepository extends JpaRepository<GruppeEntity, UUID> {
|
||||
|
||||
List<GruppeEntity> findByNameContainingIgnoreCase(String name);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.entity.GruppenbeitragLikeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GruppenbeitragLikeRepository extends JpaRepository<GruppenbeitragLikeEntity, UUID> {
|
||||
|
||||
Optional<GruppenbeitragLikeEntity> findByBeitragIdAndUserId(UUID beitragId, UUID userId);
|
||||
|
||||
long countByBeitragId(UUID beitragId);
|
||||
|
||||
@Transactional
|
||||
void deleteByBeitragId(UUID beitragId);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.entity.GruppenbeitragEntity;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GruppenbeitragRepository extends JpaRepository<GruppenbeitragEntity, UUID> {
|
||||
|
||||
Slice<GruppenbeitragEntity> findByGruppeIdOrderByCreatedAtDesc(UUID gruppeId, Pageable pageable);
|
||||
|
||||
List<GruppenbeitragEntity> findByGruppeIdOrderByCreatedAtDesc(UUID gruppeId);
|
||||
|
||||
Optional<GruppenbeitragEntity> findFirstByGruppeIdOrderByCreatedAtDesc(UUID gruppeId);
|
||||
|
||||
List<GruppenbeitragEntity> findByGruppeIdInAndCreatedAtAfterOrderByCreatedAtDesc(List<UUID> gruppeIds, LocalDateTime since);
|
||||
|
||||
@Transactional
|
||||
void deleteByGruppeId(UUID gruppeId);
|
||||
|
||||
@Transactional
|
||||
void deleteByAuthorId(UUID authorId);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.entity.GruppenmitgliedEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GruppenmitgliedRepository extends JpaRepository<GruppenmitgliedEntity, UUID> {
|
||||
|
||||
Optional<GruppenmitgliedEntity> findFirstByGruppeIdAndUserId(UUID gruppeId, UUID userId);
|
||||
|
||||
List<GruppenmitgliedEntity> findByGruppeId(UUID gruppeId);
|
||||
|
||||
List<GruppenmitgliedEntity> findByUserId(UUID userId);
|
||||
|
||||
@Transactional
|
||||
void deleteByGruppeIdAndUserId(UUID gruppeId, UUID userId);
|
||||
|
||||
long countByGruppeId(UUID gruppeId);
|
||||
|
||||
@Transactional
|
||||
void deleteByGruppeId(UUID gruppeId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.entity.UmfrageOptionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UmfrageOptionRepository extends JpaRepository<UmfrageOptionEntity, UUID> {
|
||||
|
||||
List<UmfrageOptionEntity> findByBeitragIdOrderByReihenfolge(UUID beitragId);
|
||||
|
||||
@Transactional
|
||||
void deleteByBeitragId(UUID beitragId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.oaa.xxx.gruppe.repository;
|
||||
|
||||
import de.oaa.xxx.gruppe.entity.UmfrageStimmeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UmfrageStimmeRepository extends JpaRepository<UmfrageStimmeEntity, UUID> {
|
||||
|
||||
List<UmfrageStimmeEntity> findByBeitragIdAndUserId(UUID beitragId, UUID userId);
|
||||
|
||||
Optional<UmfrageStimmeEntity> findByOptionIdAndUserId(UUID optionId, UUID userId);
|
||||
|
||||
long countByOptionId(UUID optionId);
|
||||
|
||||
@Transactional
|
||||
void deleteByBeitragId(UUID beitragId);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package de.oaa.xxx.social;
|
||||
|
||||
import de.oaa.xxx.social.dto.KommentarDto;
|
||||
import de.oaa.xxx.social.entity.KommentarEntity;
|
||||
import de.oaa.xxx.social.entity.KommentarLikeEntity;
|
||||
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.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/social/kommentare")
|
||||
public class KommentarController {
|
||||
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final KommentarLikeRepository likeRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public KommentarController(KommentarRepository kommentarRepository,
|
||||
KommentarLikeRepository likeRepository,
|
||||
UserRepository userRepository) {
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.likeRepository = likeRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record CreateKommentarRequest(String targetType, UUID targetId, String text) {}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<KommentarDto>> getKommentare(
|
||||
@RequestParam String targetType,
|
||||
@RequestParam UUID targetId,
|
||||
Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<KommentarDto> dtos = kommentarRepository
|
||||
.findByTargetTypeAndTargetIdOrderByCreatedAtAsc(targetType, targetId)
|
||||
.stream()
|
||||
.map(k -> toDto(k, myId))
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<KommentarDto> createKommentar(@RequestBody CreateKommentarRequest request, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (request.text() == null || request.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
if (request.text().length() > 500) return ResponseEntity.badRequest().build();
|
||||
if (!List.of("PINNWAND", "IMAGE", "KOMMENTAR", "GROUP_POST").contains(request.targetType())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
KommentarEntity entity = new KommentarEntity();
|
||||
entity.setKommentarId(UUID.randomUUID());
|
||||
entity.setAuthorId(myId);
|
||||
entity.setTargetType(request.targetType());
|
||||
entity.setTargetId(request.targetId());
|
||||
entity.setText(request.text().trim());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
kommentarRepository.save(entity);
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(entity, myId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{kommentarId}")
|
||||
public ResponseEntity<Void> deleteKommentar(@PathVariable UUID kommentarId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var kOpt = kommentarRepository.findById(kommentarId);
|
||||
if (kOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!kOpt.get().getAuthorId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
// Delete nested replies and their likes
|
||||
List<KommentarEntity> replies = kommentarRepository
|
||||
.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("KOMMENTAR", kommentarId);
|
||||
for (KommentarEntity reply : replies) {
|
||||
likeRepository.deleteByKommentarId(reply.getKommentarId());
|
||||
kommentarRepository.delete(reply);
|
||||
}
|
||||
likeRepository.deleteByKommentarId(kommentarId);
|
||||
kommentarRepository.delete(kOpt.get());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{kommentarId}/like")
|
||||
public ResponseEntity<Void> toggleLike(@PathVariable UUID kommentarId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (kommentarRepository.findById(kommentarId).isEmpty()) return ResponseEntity.notFound().build();
|
||||
|
||||
var existing = likeRepository.findByKommentarIdAndUserId(kommentarId, myId);
|
||||
if (existing.isPresent()) {
|
||||
likeRepository.delete(existing.get());
|
||||
} else {
|
||||
KommentarLikeEntity like = new KommentarLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
like.setKommentarId(kommentarId);
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
likeRepository.save(like);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private KommentarDto toDto(KommentarEntity k, UUID myId) {
|
||||
UserEntity author = userRepository.findById(k.getAuthorId()).orElse(null);
|
||||
String authorName = author != null ? author.getName() : "Unbekannt";
|
||||
String authorPic = author != null ? author.getProfilePicture() : null;
|
||||
long likeCount = likeRepository.countByKommentarId(k.getKommentarId());
|
||||
boolean likedByMe = likeRepository.findByKommentarIdAndUserId(k.getKommentarId(), myId).isPresent();
|
||||
long replyCount = kommentarRepository.countByTargetTypeAndTargetId("KOMMENTAR", k.getKommentarId());
|
||||
return new KommentarDto(k.getKommentarId(), k.getAuthorId(), authorName, authorPic,
|
||||
k.getTargetType(), k.getTargetId(), k.getText(), k.getCreatedAt(),
|
||||
likeCount, likedByMe, replyCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package de.oaa.xxx.social;
|
||||
|
||||
import de.oaa.xxx.social.dto.PinnwandEintragDto;
|
||||
import de.oaa.xxx.social.entity.PinnwandEintragEntity;
|
||||
import de.oaa.xxx.social.entity.PinnwandLikeEntity;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
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.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/social/pinnwand")
|
||||
public class PinnwandController {
|
||||
|
||||
private final PinnwandEintragRepository eintragRepository;
|
||||
private final PinnwandLikeRepository likeRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public PinnwandController(PinnwandEintragRepository eintragRepository,
|
||||
PinnwandLikeRepository likeRepository,
|
||||
KommentarRepository kommentarRepository,
|
||||
UserRepository userRepository) {
|
||||
this.eintragRepository = eintragRepository;
|
||||
this.likeRepository = likeRepository;
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record CreateEintragRequest(UUID profilUserId, String text) {}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<PinnwandEintragDto>> getEintraege(@RequestParam UUID userId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
List<PinnwandEintragDto> dtos = eintragRepository
|
||||
.findByProfilUserIdOrderByCreatedAtDesc(userId)
|
||||
.stream()
|
||||
.map(e -> toDto(e, myId))
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PinnwandEintragDto> createEintrag(@RequestBody CreateEintragRequest request, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (request.text() == null || request.text().isBlank()) return ResponseEntity.badRequest().build();
|
||||
if (request.text().length() > 1000) return ResponseEntity.badRequest().build();
|
||||
|
||||
PinnwandEintragEntity entity = new PinnwandEintragEntity();
|
||||
entity.setEintragId(UUID.randomUUID());
|
||||
entity.setProfilUserId(request.profilUserId());
|
||||
entity.setAuthorId(myId);
|
||||
entity.setText(request.text().trim());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
eintragRepository.save(entity);
|
||||
|
||||
return ResponseEntity.status(201).body(toDto(entity, myId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{eintragId}")
|
||||
public ResponseEntity<Void> deleteEintrag(@PathVariable UUID eintragId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var eintragOpt = eintragRepository.findById(eintragId);
|
||||
if (eintragOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var eintrag = eintragOpt.get();
|
||||
|
||||
// Author or profile owner may delete
|
||||
if (!eintrag.getAuthorId().equals(myId) && !eintrag.getProfilUserId().equals(myId)) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
likeRepository.deleteByEintragId(eintragId);
|
||||
// Delete comments on this entry (via KommentarRepository)
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", eintragId)
|
||||
.forEach(k -> kommentarRepository.deleteById(k.getKommentarId()));
|
||||
eintragRepository.delete(eintrag);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{eintragId}/like")
|
||||
public ResponseEntity<Void> toggleLike(@PathVariable UUID eintragId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (eintragRepository.findById(eintragId).isEmpty()) return ResponseEntity.notFound().build();
|
||||
|
||||
var existing = likeRepository.findByEintragIdAndUserId(eintragId, myId);
|
||||
if (existing.isPresent()) {
|
||||
likeRepository.delete(existing.get());
|
||||
} else {
|
||||
PinnwandLikeEntity like = new PinnwandLikeEntity();
|
||||
like.setLikeId(UUID.randomUUID());
|
||||
like.setEintragId(eintragId);
|
||||
like.setUserId(myId);
|
||||
like.setLikedAt(LocalDateTime.now());
|
||||
likeRepository.save(like);
|
||||
}
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private PinnwandEintragDto toDto(PinnwandEintragEntity e, UUID myId) {
|
||||
UserEntity author = userRepository.findById(e.getAuthorId()).orElse(null);
|
||||
String authorName = author != null ? author.getName() : "Unbekannt";
|
||||
String authorPic = author != null ? author.getProfilePicture() : null;
|
||||
long likeCount = likeRepository.countByEintragId(e.getEintragId());
|
||||
boolean likedByMe = likeRepository.findByEintragIdAndUserId(e.getEintragId(), myId).isPresent();
|
||||
long kommentarCount = kommentarRepository.countByTargetTypeAndTargetId("PINNWAND", e.getEintragId());
|
||||
return new PinnwandEintragDto(e.getEintragId(), e.getProfilUserId(), e.getAuthorId(),
|
||||
authorName, authorPic, e.getText(), e.getCreatedAt(), likeCount, likedByMe, kommentarCount);
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,21 @@ public class SocialController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/friends/user/{userId}")
|
||||
public ResponseEntity<List<UserProfile>> getFriendsOfUser(@PathVariable UUID userId, Principal principal) {
|
||||
if (userRepository.findByEmail(principal.getName()).isEmpty()) return ResponseEntity.status(401).build();
|
||||
List<UserProfile> profiles = friendshipRepository.findFriends(userId, Status.ACCEPTED).stream()
|
||||
.map(f -> {
|
||||
UUID friendId = f.getSenderId().equals(userId) ? f.getReceiverId() : f.getSenderId();
|
||||
return userRepository.findById(friendId)
|
||||
.map(u -> new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), null))
|
||||
.orElse(null);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
return ResponseEntity.ok(profiles);
|
||||
}
|
||||
|
||||
@GetMapping("/friends")
|
||||
public ResponseEntity<List<FriendshipDto>> getFriends(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
@@ -259,7 +274,9 @@ public class SocialController {
|
||||
status = "PENDING_RECEIVED";
|
||||
}
|
||||
}
|
||||
return new UserProfile(user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(), status);
|
||||
return new UserProfile(user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(),
|
||||
status, user.getAlter(), user.getGroesse(), user.getGewicht(),
|
||||
user.getGeschlecht(), user.getNeigung(), user.getBeziehungsstatus(), user.getBeschreibung());
|
||||
}
|
||||
|
||||
private MessageDto toMessageDto(MessageEntity m) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record KommentarDto(
|
||||
UUID kommentarId,
|
||||
UUID authorId,
|
||||
String authorName,
|
||||
String authorPicture,
|
||||
String targetType,
|
||||
UUID targetId,
|
||||
String text,
|
||||
LocalDateTime createdAt,
|
||||
long likeCount,
|
||||
boolean likedByMe,
|
||||
long replyCount
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record PinnwandEintragDto(
|
||||
UUID eintragId,
|
||||
UUID profilUserId,
|
||||
UUID authorId,
|
||||
String authorName,
|
||||
String authorPicture,
|
||||
String text,
|
||||
LocalDateTime createdAt,
|
||||
long likeCount,
|
||||
boolean likedByMe,
|
||||
long kommentarCount
|
||||
) {}
|
||||
@@ -1,5 +1,27 @@
|
||||
package de.oaa.xxx.social.dto;
|
||||
|
||||
import de.oaa.xxx.user.Beziehungsstatus;
|
||||
import de.oaa.xxx.user.Geschlecht;
|
||||
import de.oaa.xxx.user.Neigung;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {}
|
||||
public record UserProfile(
|
||||
UUID userId,
|
||||
String name,
|
||||
String profilePicture,
|
||||
String profilePictureHq,
|
||||
String friendStatus,
|
||||
Integer alter,
|
||||
Integer groesse,
|
||||
Integer gewicht,
|
||||
Geschlecht geschlecht,
|
||||
Neigung neigung,
|
||||
Beziehungsstatus beziehungsstatus,
|
||||
String beschreibung
|
||||
) {
|
||||
/** Compact constructor for contexts where profile details are not needed (friend list etc.) */
|
||||
public UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {
|
||||
this(userId, name, profilePicture, profilePictureHq, friendStatus, null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "kommentar")
|
||||
public class KommentarEntity {
|
||||
|
||||
/** targetType values: PINNWAND, IMAGE, KOMMENTAR */
|
||||
@Id
|
||||
@Column
|
||||
private UUID kommentarId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID authorId;
|
||||
|
||||
@Column(length = 20, nullable = false)
|
||||
private String targetType;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID targetId;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
private String text;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getKommentarId() { return kommentarId; }
|
||||
public void setKommentarId(UUID kommentarId) { this.kommentarId = kommentarId; }
|
||||
|
||||
public UUID getAuthorId() { return authorId; }
|
||||
public void setAuthorId(UUID authorId) { this.authorId = authorId; }
|
||||
|
||||
public String getTargetType() { return targetType; }
|
||||
public void setTargetType(String targetType) { this.targetType = targetType; }
|
||||
|
||||
public UUID getTargetId() { return targetId; }
|
||||
public void setTargetId(UUID targetId) { this.targetId = targetId; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "kommentar_like")
|
||||
public class KommentarLikeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID likeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID kommentarId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime likedAt;
|
||||
|
||||
public UUID getLikeId() { return likeId; }
|
||||
public void setLikeId(UUID likeId) { this.likeId = likeId; }
|
||||
|
||||
public UUID getKommentarId() { return kommentarId; }
|
||||
public void setKommentarId(UUID kommentarId) { this.kommentarId = kommentarId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public LocalDateTime getLikedAt() { return likedAt; }
|
||||
public void setLikedAt(LocalDateTime likedAt) { this.likedAt = likedAt; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pinnwand_eintrag")
|
||||
public class PinnwandEintragEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID eintragId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID profilUserId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID authorId;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
private String text;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public UUID getEintragId() { return eintragId; }
|
||||
public void setEintragId(UUID eintragId) { this.eintragId = eintragId; }
|
||||
|
||||
public UUID getProfilUserId() { return profilUserId; }
|
||||
public void setProfilUserId(UUID profilUserId) { this.profilUserId = profilUserId; }
|
||||
|
||||
public UUID getAuthorId() { return authorId; }
|
||||
public void setAuthorId(UUID authorId) { this.authorId = authorId; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public void setText(String text) { this.text = text; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.oaa.xxx.social.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pinnwand_like")
|
||||
public class PinnwandLikeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID likeId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID eintragId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime likedAt;
|
||||
|
||||
public UUID getLikeId() { return likeId; }
|
||||
public void setLikeId(UUID likeId) { this.likeId = likeId; }
|
||||
|
||||
public UUID getEintragId() { return eintragId; }
|
||||
public void setEintragId(UUID eintragId) { this.eintragId = eintragId; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public LocalDateTime getLikedAt() { return likedAt; }
|
||||
public void setLikedAt(LocalDateTime likedAt) { this.likedAt = likedAt; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.KommentarLikeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface KommentarLikeRepository extends JpaRepository<KommentarLikeEntity, UUID> {
|
||||
|
||||
long countByKommentarId(UUID kommentarId);
|
||||
|
||||
Optional<KommentarLikeEntity> findByKommentarIdAndUserId(UUID kommentarId, UUID userId);
|
||||
|
||||
void deleteByKommentarId(UUID kommentarId);
|
||||
|
||||
void deleteByUserId(UUID userId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.KommentarEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface KommentarRepository extends JpaRepository<KommentarEntity, UUID> {
|
||||
|
||||
List<KommentarEntity> findByTargetTypeAndTargetIdOrderByCreatedAtAsc(String targetType, UUID targetId);
|
||||
|
||||
long countByTargetTypeAndTargetId(String targetType, UUID targetId);
|
||||
|
||||
void deleteByAuthorId(UUID authorId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.PinnwandEintragEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PinnwandEintragRepository extends JpaRepository<PinnwandEintragEntity, UUID> {
|
||||
|
||||
List<PinnwandEintragEntity> findByProfilUserIdOrderByCreatedAtDesc(UUID profilUserId);
|
||||
|
||||
void deleteByProfilUserId(UUID profilUserId);
|
||||
|
||||
void deleteByAuthorId(UUID authorId);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.oaa.xxx.social.repository;
|
||||
|
||||
import de.oaa.xxx.social.entity.PinnwandLikeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PinnwandLikeRepository extends JpaRepository<PinnwandLikeEntity, UUID> {
|
||||
|
||||
long countByEintragId(UUID eintragId);
|
||||
|
||||
Optional<PinnwandLikeEntity> findByEintragIdAndUserId(UUID eintragId, UUID userId);
|
||||
|
||||
void deleteByEintragId(UUID eintragId);
|
||||
|
||||
void deleteByUserId(UUID userId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
public enum Beziehungsstatus {
|
||||
SINGLE("single"),
|
||||
IN_EINER_BEZIEHUNG("in einer Beziehung"),
|
||||
VERHEIRATET("verheiratet"),
|
||||
IN_EINER_OFFENEN_BEZIEHUNG("in einer offenen Beziehung"),
|
||||
IN_EINER_OFFENEN_EHE("in einer offenen Ehe");
|
||||
|
||||
private final String label;
|
||||
|
||||
Beziehungsstatus(String label) { this.label = label; }
|
||||
|
||||
public String getLabel() { return label; }
|
||||
}
|
||||
13
xxxthegame/src/main/java/de/oaa/xxx/user/Geschlecht.java
Normal file
13
xxxthegame/src/main/java/de/oaa/xxx/user/Geschlecht.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
public enum Geschlecht {
|
||||
WEIBLICH("weiblich"),
|
||||
DIVERS("divers"),
|
||||
MAENNLICH("männlich");
|
||||
|
||||
private final String label;
|
||||
|
||||
Geschlecht(String label) { this.label = label; }
|
||||
|
||||
public String getLabel() { return label; }
|
||||
}
|
||||
16
xxxthegame/src/main/java/de/oaa/xxx/user/Neigung.java
Normal file
16
xxxthegame/src/main/java/de/oaa/xxx/user/Neigung.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
public enum Neigung {
|
||||
DEVOT("devot"),
|
||||
EHER_DEVOT("eher devot"),
|
||||
SWITCHER("Switcher"),
|
||||
EHER_DOMINANT("eher dominant"),
|
||||
DOMINANT("dominant"),
|
||||
KEINES("keines");
|
||||
|
||||
private final String label;
|
||||
|
||||
Neigung(String label) { this.label = label; }
|
||||
|
||||
public String getLabel() { return label; }
|
||||
}
|
||||
@@ -9,6 +9,13 @@ public class User {
|
||||
private String email;
|
||||
private String password;
|
||||
private String profilePicture;
|
||||
private Integer alter;
|
||||
private Integer groesse;
|
||||
private Integer gewicht;
|
||||
private Geschlecht geschlecht;
|
||||
private Neigung neigung;
|
||||
private Beziehungsstatus beziehungsstatus;
|
||||
private String beschreibung;
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
@@ -25,6 +32,27 @@ public class User {
|
||||
public String getProfilePicture() { return profilePicture; }
|
||||
public void setProfilePicture(String profilePicture) { this.profilePicture = profilePicture; }
|
||||
|
||||
public Integer getAlter() { return alter; }
|
||||
public void setAlter(Integer alter) { this.alter = alter; }
|
||||
|
||||
public Integer getGroesse() { return groesse; }
|
||||
public void setGroesse(Integer groesse) { this.groesse = groesse; }
|
||||
|
||||
public Integer getGewicht() { return gewicht; }
|
||||
public void setGewicht(Integer gewicht) { this.gewicht = gewicht; }
|
||||
|
||||
public Geschlecht getGeschlecht() { return geschlecht; }
|
||||
public void setGeschlecht(Geschlecht geschlecht) { this.geschlecht = geschlecht; }
|
||||
|
||||
public Neigung getNeigung() { return neigung; }
|
||||
public void setNeigung(Neigung neigung) { this.neigung = neigung; }
|
||||
|
||||
public Beziehungsstatus getBeziehungsstatus() { return beziehungsstatus; }
|
||||
public void setBeziehungsstatus(Beziehungsstatus beziehungsstatus) { this.beziehungsstatus = beziehungsstatus; }
|
||||
|
||||
public String getBeschreibung() { return beschreibung; }
|
||||
public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User[userId=" + userId + ", name=" + name + ", email=" + email + "]";
|
||||
|
||||
@@ -33,6 +33,10 @@ import de.oaa.xxx.session.repository.MitspielerRepository;
|
||||
import de.oaa.xxx.session.repository.SessionRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
|
||||
import de.oaa.xxx.social.repository.ProfileImageRepository;
|
||||
import de.oaa.xxx.social.repository.PinnwandEintragRepository;
|
||||
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
|
||||
import de.oaa.xxx.social.repository.KommentarRepository;
|
||||
import de.oaa.xxx.social.repository.KommentarLikeRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
@RestController
|
||||
@@ -57,6 +61,10 @@ public class UserController {
|
||||
private final PasswordResetRepository passwordResetRepository;
|
||||
private final ProfileImageRepository profileImageRepository;
|
||||
private final ProfileImageLikeRepository profileImageLikeRepository;
|
||||
private final PinnwandEintragRepository pinnwandEintragRepository;
|
||||
private final PinnwandLikeRepository pinnwandLikeRepository;
|
||||
private final KommentarRepository kommentarRepository;
|
||||
private final KommentarLikeRepository kommentarLikeRepository;
|
||||
|
||||
public UserController(UserRepository userRepository,
|
||||
RegistrationRepository registrationRepository,
|
||||
@@ -73,7 +81,11 @@ public class UserController {
|
||||
EmailChangeRepository emailChangeRepository,
|
||||
PasswordResetRepository passwordResetRepository,
|
||||
ProfileImageRepository profileImageRepository,
|
||||
ProfileImageLikeRepository profileImageLikeRepository) {
|
||||
ProfileImageLikeRepository profileImageLikeRepository,
|
||||
PinnwandEintragRepository pinnwandEintragRepository,
|
||||
PinnwandLikeRepository pinnwandLikeRepository,
|
||||
KommentarRepository kommentarRepository,
|
||||
KommentarLikeRepository kommentarLikeRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.registrationRepository = registrationRepository;
|
||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||
@@ -90,10 +102,16 @@ public class UserController {
|
||||
this.passwordResetRepository = passwordResetRepository;
|
||||
this.profileImageRepository = profileImageRepository;
|
||||
this.profileImageLikeRepository = profileImageLikeRepository;
|
||||
this.pinnwandEintragRepository = pinnwandEintragRepository;
|
||||
this.pinnwandLikeRepository = pinnwandLikeRepository;
|
||||
this.kommentarRepository = kommentarRepository;
|
||||
this.kommentarLikeRepository = kommentarLikeRepository;
|
||||
}
|
||||
|
||||
record ProfilePictureRequest(String picture, String pictureHq) {}
|
||||
record NameChangeRequest(String name) {}
|
||||
record ProfileRequest(Integer alter, Integer groesse, Integer gewicht,
|
||||
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
|
||||
|
||||
@PutMapping("/me/picture")
|
||||
public ResponseEntity<Void> updateProfilePicture(@RequestBody ProfilePictureRequest request, Principal principal) {
|
||||
@@ -105,6 +123,25 @@ public class UserController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/profile")
|
||||
public ResponseEntity<Void> updateProfile(@RequestBody ProfileRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var user = userOpt.get();
|
||||
if (request.beschreibung() != null && request.beschreibung().length() > 600) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
user.setAlter(request.alter());
|
||||
user.setGroesse(request.groesse());
|
||||
user.setGewicht(request.gewicht());
|
||||
user.setGeschlecht(request.geschlecht());
|
||||
user.setNeigung(request.neigung());
|
||||
user.setBeziehungsstatus(request.beziehungsstatus());
|
||||
user.setBeschreibung(request.beschreibung());
|
||||
userRepository.save(user);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PutMapping("/me/name")
|
||||
public ResponseEntity<Void> updateName(@RequestBody NameChangeRequest request, Principal principal) {
|
||||
String newName = request.name();
|
||||
@@ -169,10 +206,31 @@ public class UserController {
|
||||
var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId);
|
||||
for (var img : profileImages) {
|
||||
profileImageLikeRepository.deleteByImageId(img.getImageId());
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("IMAGE", img.getImageId())
|
||||
.forEach(k -> {
|
||||
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
|
||||
kommentarRepository.delete(k);
|
||||
});
|
||||
}
|
||||
profileImageRepository.deleteAll(profileImages);
|
||||
profileImageLikeRepository.deleteByUserId(userId);
|
||||
|
||||
// 5c. Delete pinnwand entries (authored by or on user's wall) + their likes/comments
|
||||
var ownWallEntries = pinnwandEintragRepository.findByProfilUserIdOrderByCreatedAtDesc(userId);
|
||||
for (var e : ownWallEntries) {
|
||||
pinnwandLikeRepository.deleteByEintragId(e.getEintragId());
|
||||
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", e.getEintragId())
|
||||
.forEach(k -> {
|
||||
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
|
||||
kommentarRepository.delete(k);
|
||||
});
|
||||
}
|
||||
pinnwandEintragRepository.deleteAll(ownWallEntries);
|
||||
pinnwandEintragRepository.deleteByAuthorId(userId);
|
||||
pinnwandLikeRepository.deleteByUserId(userId);
|
||||
kommentarRepository.deleteByAuthorId(userId);
|
||||
kommentarLikeRepository.deleteByUserId(userId);
|
||||
|
||||
// 6. Delete user
|
||||
userRepository.delete(user);
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package de.oaa.xxx.user;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -27,6 +24,30 @@ public class UserEntity {
|
||||
@Column(columnDefinition = "MEDIUMTEXT")
|
||||
private String profilePictureHq;
|
||||
|
||||
@Column(name = "benutzer_alter")
|
||||
private Integer alter;
|
||||
|
||||
@Column
|
||||
private Integer groesse;
|
||||
|
||||
@Column
|
||||
private Integer gewicht;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 20)
|
||||
private Geschlecht geschlecht;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 20)
|
||||
private Neigung neigung;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 30)
|
||||
private Beziehungsstatus beziehungsstatus;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String beschreibung;
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
@@ -45,6 +66,27 @@ public class UserEntity {
|
||||
public String getProfilePictureHq() { return profilePictureHq; }
|
||||
public void setProfilePictureHq(String profilePictureHq) { this.profilePictureHq = profilePictureHq; }
|
||||
|
||||
public Integer getAlter() { return alter; }
|
||||
public void setAlter(Integer alter) { this.alter = alter; }
|
||||
|
||||
public Integer getGroesse() { return groesse; }
|
||||
public void setGroesse(Integer groesse) { this.groesse = groesse; }
|
||||
|
||||
public Integer getGewicht() { return gewicht; }
|
||||
public void setGewicht(Integer gewicht) { this.gewicht = gewicht; }
|
||||
|
||||
public Geschlecht getGeschlecht() { return geschlecht; }
|
||||
public void setGeschlecht(Geschlecht geschlecht) { this.geschlecht = geschlecht; }
|
||||
|
||||
public Neigung getNeigung() { return neigung; }
|
||||
public void setNeigung(Neigung neigung) { this.neigung = neigung; }
|
||||
|
||||
public Beziehungsstatus getBeziehungsstatus() { return beziehungsstatus; }
|
||||
public void setBeziehungsstatus(Beziehungsstatus beziehungsstatus) { this.beziehungsstatus = beziehungsstatus; }
|
||||
|
||||
public String getBeschreibung() { return beschreibung; }
|
||||
public void setBeschreibung(String beschreibung) { this.beschreibung = beschreibung; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserEntity[userId=" + userId + ", name=" + name + ", email=" + email + "]";
|
||||
@@ -56,6 +98,13 @@ public class UserEntity {
|
||||
user.setName(name);
|
||||
user.setUserId(userId);
|
||||
user.setProfilePicture(profilePicture);
|
||||
user.setAlter(alter);
|
||||
user.setGroesse(groesse);
|
||||
user.setGewicht(gewicht);
|
||||
user.setGeschlecht(geschlecht);
|
||||
user.setNeigung(neigung);
|
||||
user.setBeziehungsstatus(beziehungsstatus);
|
||||
user.setBeschreibung(beschreibung);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
655
xxxthegame/src/main/resources/static/feed.html
Normal file
655
xxxthegame/src/main/resources/static/feed.html
Normal file
@@ -0,0 +1,655 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Feed – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.tabs { display:flex; gap:0; margin-bottom:1.5rem; border-bottom:1px solid var(--color-secondary); }
|
||||
.tab-btn { background:none; border:none; border-bottom:3px solid transparent; border-radius:0; padding:0.6rem 1.25rem; font-size:0.95rem; font-weight:600; color:var(--color-muted); cursor:pointer; margin-bottom:-1px; transition:color 0.15s,border-color 0.15s; }
|
||||
.tab-btn:hover { color:var(--color-text); background:none; }
|
||||
.tab-btn.active { color:var(--color-primary); border-bottom-color:var(--color-primary); }
|
||||
.tab-panel { display:none; }
|
||||
.tab-panel.active { display:block; }
|
||||
|
||||
.post-compose { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; padding:1rem; margin-bottom:1rem; transition:border-color 0.15s; }
|
||||
.post-compose.drag-over { border-color:var(--color-primary); background:rgba(var(--color-primary-rgb,180,0,60),0.06); }
|
||||
.compose-type { display:flex; gap:1.5rem; margin-bottom:0.75rem; }
|
||||
.compose-type label { display:flex; align-items:center; gap:0.4rem; font-size:0.9rem; cursor:pointer; }
|
||||
.post-compose textarea { width:100%; padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:70px; box-sizing:border-box; }
|
||||
.post-compose textarea:focus { border-color:var(--color-primary); }
|
||||
.compose-thumbs { display:none; flex-wrap:wrap; gap:0.5rem; margin-top:0.5rem; }
|
||||
.compose-thumb { position:relative; width:64px; height:64px; flex-shrink:0; }
|
||||
.compose-thumb img { width:64px; height:64px; object-fit:cover; border-radius:6px; display:block; }
|
||||
.compose-thumb-remove { position:absolute; top:-5px; right:-5px; background:rgba(0,0,0,0.7); border:none; color:#fff; width:18px; height:18px; border-radius:50%; font-size:0.65rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; margin:0; width:auto; line-height:1; }
|
||||
.umfrage-options { margin-top:0.5rem; }
|
||||
.umfrage-option-row { display:flex; gap:0.5rem; margin-bottom:0.4rem; }
|
||||
.umfrage-option-row input { flex:1; }
|
||||
.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; }
|
||||
.privacy-toggle { font-size:0.85rem; display:flex; align-items:center; gap:0.4rem; }
|
||||
.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; transition:border-color 0.15s,color 0.15s; }
|
||||
.compose-action-btn:hover { border-color:var(--color-primary); color:var(--color-primary); background:none; }
|
||||
label.compose-action-btn { display:inline-flex; align-items:center; }
|
||||
|
||||
.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; }
|
||||
.post-avatar { width:36px; height:36px; border-radius:50%; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:0.95rem; flex-shrink:0; overflow:hidden; }
|
||||
.post-avatar img { width:100%; height:100%; object-fit:cover; }
|
||||
.post-author { font-weight:600; font-size:0.9rem; }
|
||||
.post-meta { font-size:0.75rem; color:var(--color-muted); }
|
||||
.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:400px; object-fit:contain; border-radius:6px; margin-top:0.5rem; display:block; }
|
||||
.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; }
|
||||
.post-action-btn.active { color:var(--color-primary); }
|
||||
.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; }
|
||||
|
||||
.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); }
|
||||
.umfrage-option-bar.voted { border-color:var(--color-primary); }
|
||||
.umfrage-bar-fill { position:absolute; inset:0; background:rgba(var(--color-primary-rgb,180,0,60),0.15); transition:width 0.4s; }
|
||||
.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; }
|
||||
|
||||
.gruppe-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.75rem; color:var(--color-muted); background:var(--color-secondary); border-radius:4px; padding:0.15rem 0.45rem; margin-top:0.1rem; }
|
||||
.gruppe-badge a { color:inherit; text-decoration:none; }
|
||||
.gruppe-badge a:hover { color:var(--color-primary); }
|
||||
|
||||
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
|
||||
.sentinel { height:1px; }
|
||||
|
||||
/* Lightbox */
|
||||
.lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:300; 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; 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 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); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" id="tabMine" onclick="switchTab('mine', this)">Mein Feed</button>
|
||||
<button class="tab-btn" id="tabPublic" onclick="switchTab('public', this)">Öffentlicher Feed</button>
|
||||
</div>
|
||||
|
||||
<!-- Mein Feed -->
|
||||
<div class="tab-panel active" id="tab-mine">
|
||||
<div class="post-compose" id="compose">
|
||||
<div class="compose-type">
|
||||
<label><input type="radio" name="beitragTyp" value="TEXT" checked onchange="toggleUmfrage()"> Text</label>
|
||||
<label><input type="radio" name="beitragTyp" value="UMFRAGE" onchange="toggleUmfrage()"> Umfrage</label>
|
||||
</div>
|
||||
<textarea id="composeText" placeholder="Was möchtest du teilen?" rows="3"></textarea>
|
||||
<div class="compose-thumbs" id="composeThumbs"></div>
|
||||
<div class="umfrage-options" id="umfrageOptions" style="display:none;">
|
||||
<div id="optionList"></div>
|
||||
<button onclick="addOption()" style="width:auto; margin:0; padding:0.3rem 0.75rem; font-size:0.8rem; margin-top:0.4rem;">+ Option</button>
|
||||
</div>
|
||||
<div class="compose-footer">
|
||||
<div style="display:flex;gap:1rem;align-items:center;flex-wrap:wrap;">
|
||||
<label class="multi-toggle" id="multiChoiceRow" style="display:none;">
|
||||
<input type="checkbox" id="multiChoice"> Multi-Choice
|
||||
</label>
|
||||
<label class="privacy-toggle">
|
||||
<input type="checkbox" id="isPublic"> Öffentlich
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<button type="button" class="compose-action-btn" onclick="toggleEmojiPicker(this,'composeText')" title="Emoji einfügen">😊</button>
|
||||
<label class="compose-action-btn" title="Fotos hinzufügen">📷
|
||||
<input type="file" id="composeBildFile" accept="image/*" multiple style="display:none;" onchange="selectComposeBilder(this)">
|
||||
</label>
|
||||
<button onclick="submitPost()" style="width:auto; margin:0;">Veröffentlichen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mineFeed"></div>
|
||||
<p class="empty-hint" id="mineEmpty" style="display:none;">Noch keine Beiträge. Schreib den ersten!</p>
|
||||
<div class="sentinel" id="mineSentinel"></div>
|
||||
</div>
|
||||
|
||||
<!-- Öffentlicher Feed -->
|
||||
<div class="tab-panel" id="tab-public">
|
||||
<div id="publicFeed"></div>
|
||||
<p class="empty-hint" id="publicEmpty" style="display:none;">Noch keine öffentlichen Beiträge.</p>
|
||||
<div class="sentinel" id="publicSentinel"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Lightbox -->
|
||||
<div class="lightbox" id="postLightbox">
|
||||
<div class="lb-layout">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
// ── State ──
|
||||
let myUserId = null;
|
||||
let activeLbPostId = null;
|
||||
let activeLbPostType = null;
|
||||
|
||||
const feedState = {
|
||||
mine: { page:0, hasMore:true, loading:false, loaded:false },
|
||||
public: { page:0, hasMore:true, loading:false, loaded:false }
|
||||
};
|
||||
|
||||
let composeBilderArr = [];
|
||||
|
||||
// ── Boot ──
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
|
||||
if (user) {
|
||||
myUserId = user.userId;
|
||||
loadFeed('mine');
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// ── Tab switching ──
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
if (!feedState[name].loaded) loadFeed(name);
|
||||
}
|
||||
|
||||
// ── Feed loading ──
|
||||
async function loadFeed(tab) {
|
||||
const state = feedState[tab];
|
||||
if (state.loading || !state.hasMore) return;
|
||||
state.loading = true;
|
||||
state.loaded = true;
|
||||
try {
|
||||
const endpoint = tab === 'mine' ? '/feed/mine' : '/feed/public';
|
||||
const res = await fetch(`${endpoint}?page=${state.page}&size=10`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const feedEl = document.getElementById(tab + 'Feed');
|
||||
if (state.page === 0 && data.posts.length === 0) {
|
||||
document.getElementById(tab + 'Empty').style.display = '';
|
||||
}
|
||||
data.posts.forEach(p => feedEl.insertAdjacentHTML('beforeend', renderPostCard(p, tab)));
|
||||
state.hasMore = data.hasMore;
|
||||
state.page++;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Infinite Scroll ──
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(e => {
|
||||
if (!e.isIntersecting) return;
|
||||
if (e.target.id === 'mineSentinel') loadFeed('mine');
|
||||
if (e.target.id === 'publicSentinel') loadFeed('public');
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Render post card ──
|
||||
function renderPostCard(p, tab) {
|
||||
const avatarHtml = p.authorPicture
|
||||
? `<img src="data:image/png;base64,${p.authorPicture}" alt="">`
|
||||
: '◉';
|
||||
const privacyLabel = !p.isPublic ? ' <span style="font-size:0.7rem;color:var(--color-muted);">🔒</span>' : '';
|
||||
const groupBadge = p.postType === 'GROUP' && p.gruppeId
|
||||
? `<span class="gruppe-badge" onclick="event.stopPropagation()">👥 <a href="/gruppe.html?id=${p.gruppeId}" onclick="event.stopPropagation()">${esc(p.gruppeName)}</a></span>`
|
||||
: '';
|
||||
const bildHtml = bilderCarousel(p.bilder, p.postId);
|
||||
|
||||
let umfrageHtml = '';
|
||||
if (p.beitragTyp === 'UMFRAGE' && p.optionen && p.optionen.length > 0) {
|
||||
const totalVotes = p.optionen.reduce((s, o) => s + o.stimmenCount, 0);
|
||||
umfrageHtml = '<div style="margin-top:0.5rem;">' + p.optionen.map(o => {
|
||||
const pct = totalVotes > 0 ? Math.round(o.stimmenCount / totalVotes * 100) : 0;
|
||||
const voted = p.myVoteOptionIds && p.myVoteOptionIds.includes(o.optionId);
|
||||
return `<div class="umfrage-option-bar${voted ? ' voted' : ''}" onclick="event.stopPropagation(); votePost('${p.postId}','${o.optionId}','${tab}','${p.postType}')">
|
||||
<div class="umfrage-bar-fill" style="width:${pct}%"></div>
|
||||
<div class="umfrage-bar-content"><span>${esc(o.text)}</span><span>${pct}%</span></div>
|
||||
</div>`;
|
||||
}).join('') + `<div class="umfrage-total">${totalVotes} Stimme${totalVotes !== 1 ? 'n' : ''}</div></div>`;
|
||||
}
|
||||
|
||||
const canDelete = p.postType === 'FEED' && p.authorId === myUserId;
|
||||
const deleteBtn = canDelete
|
||||
? `<button class="post-action-btn post-delete" onclick="event.stopPropagation(); deletePost('${p.postId}')">🗑</button>`
|
||||
: '';
|
||||
|
||||
const gruppeIdAttr = p.gruppeId ? ` data-gruppe-id="${p.gruppeId}"` : '';
|
||||
return `<div class="post-card" id="pc-${p.postId}"${gruppeIdAttr} onclick="openLb('${p.postId}','${p.postType}')" style="cursor:pointer;">
|
||||
<div class="post-header">
|
||||
<div class="post-avatar">${avatarHtml}</div>
|
||||
<div>
|
||||
<div class="post-author"><a href="/benutzer.html?userId=${p.authorId}" style="color:inherit;text-decoration:none;" onclick="event.stopPropagation()">${esc(p.authorName)}</a>${privacyLabel}</div>
|
||||
<div class="post-meta">${fmtDate(p.createdAt)}${groupBadge}</div>
|
||||
</div>
|
||||
${deleteBtn}
|
||||
</div>
|
||||
<div class="post-text">${esc(p.text)}</div>
|
||||
${bildHtml}
|
||||
${umfrageHtml}
|
||||
<div class="post-actions">
|
||||
<button class="post-action-btn${p.likedByMe ? ' active' : ''}" id="lk-${p.postId}" onclick="event.stopPropagation(); likePost('${p.postId}','${p.postType}')">
|
||||
♥ <span id="lkc-${p.postId}">${p.likeCount}</span>
|
||||
</button>
|
||||
<button class="post-action-btn" onclick="event.stopPropagation(); openLb('${p.postId}','${p.postType}')">
|
||||
💬 <span id="kc-${p.postId}">${p.kommentarCount}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Compose ──
|
||||
function toggleUmfrage() {
|
||||
const isUmfrage = document.querySelector('input[name="beitragTyp"]:checked').value === 'UMFRAGE';
|
||||
document.getElementById('umfrageOptions').style.display = isUmfrage ? '' : 'none';
|
||||
document.getElementById('multiChoiceRow').style.display = isUmfrage ? '' : 'none';
|
||||
if (isUmfrage && document.getElementById('optionList').children.length === 0) {
|
||||
addOption(); addOption();
|
||||
}
|
||||
}
|
||||
|
||||
function addOption() {
|
||||
const list = document.getElementById('optionList');
|
||||
const idx = list.children.length;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'umfrage-option-row';
|
||||
row.innerHTML = `<input type="text" placeholder="Option ${idx + 1}" maxlength="100">
|
||||
<button onclick="this.parentElement.remove()">✕</button>`;
|
||||
list.appendChild(row);
|
||||
}
|
||||
|
||||
function selectComposeBilder(input) {
|
||||
[...input.files].forEach(f => { if (f.type.startsWith('image/')) processImageFile(f); });
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function processImageFile(file) {
|
||||
if (!file || !file.type.startsWith('image/')) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const maxSize = 1024;
|
||||
const canvas = document.createElement('canvas');
|
||||
const scale = Math.min(maxSize / img.width, maxSize / img.height, 1);
|
||||
canvas.width = Math.round(img.width * scale);
|
||||
canvas.height = Math.round(img.height * scale);
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
const data = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];
|
||||
composeBilderArr.push(data);
|
||||
renderComposeThumbs();
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function renderComposeThumbs() {
|
||||
const container = document.getElementById('composeThumbs');
|
||||
container.innerHTML = '';
|
||||
composeBilderArr.forEach((b, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'compose-thumb';
|
||||
div.innerHTML = `<img src="data:image/jpeg;base64,${b}" alt="">
|
||||
<button class="compose-thumb-remove" onclick="removeThumb(${i})" title="Entfernen">✕</button>`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
container.style.display = composeBilderArr.length > 0 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function removeThumb(idx) {
|
||||
composeBilderArr.splice(idx, 1);
|
||||
renderComposeThumbs();
|
||||
}
|
||||
|
||||
// ── Drag & Drop ──
|
||||
const compose = document.getElementById('compose');
|
||||
compose.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
if ([...e.dataTransfer.items].some(i => i.type.startsWith('image/')))
|
||||
compose.classList.add('drag-over');
|
||||
});
|
||||
compose.addEventListener('dragleave', e => {
|
||||
if (!compose.contains(e.relatedTarget)) compose.classList.remove('drag-over');
|
||||
});
|
||||
compose.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
compose.classList.remove('drag-over');
|
||||
[...e.dataTransfer.files]
|
||||
.filter(f => f.type.startsWith('image/'))
|
||||
.forEach(f => processImageFile(f));
|
||||
});
|
||||
|
||||
async function submitPost() {
|
||||
const text = document.getElementById('composeText').value.trim();
|
||||
if (!text) return;
|
||||
const beitragTyp = document.querySelector('input[name="beitragTyp"]:checked').value;
|
||||
const multiChoice = document.getElementById('multiChoice').checked;
|
||||
const isPublic = document.getElementById('isPublic').checked;
|
||||
|
||||
let optionen = [];
|
||||
if (beitragTyp === 'UMFRAGE') {
|
||||
optionen = Array.from(document.getElementById('optionList').querySelectorAll('input'))
|
||||
.map(i => i.value.trim()).filter(v => v);
|
||||
if (optionen.length < 2) { alert('Mindestens 2 Optionen erforderlich.'); return; }
|
||||
}
|
||||
|
||||
const res = await fetch('/feed/posts', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ beitragTyp, text, multiChoice, optionen, bilder: [...composeBilderArr], isPublic })
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const post = await res.json();
|
||||
|
||||
// Reset compose
|
||||
document.getElementById('composeText').value = '';
|
||||
composeBilderArr = [];
|
||||
renderComposeThumbs();
|
||||
document.querySelector('input[name="beitragTyp"][value="TEXT"]').checked = true;
|
||||
toggleUmfrage();
|
||||
document.getElementById('multiChoice').checked = false;
|
||||
document.getElementById('isPublic').checked = false;
|
||||
document.getElementById('optionList').innerHTML = '';
|
||||
|
||||
// Prepend to mine feed
|
||||
document.getElementById('mineEmpty').style.display = 'none';
|
||||
document.getElementById('mineFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'mine'));
|
||||
|
||||
if (isPublic) {
|
||||
document.getElementById('publicEmpty').style.display = 'none';
|
||||
document.getElementById('publicFeed').insertAdjacentHTML('afterbegin', renderPostCard(post, 'public'));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Like ──
|
||||
async function likePost(postId, postType) {
|
||||
let likeEndpoint;
|
||||
if (postType === 'GROUP') {
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
const gruppeId = card?.dataset?.gruppeId;
|
||||
if (!gruppeId) return;
|
||||
likeEndpoint = `/gruppen/${gruppeId}/posts/${postId}/like`;
|
||||
} else {
|
||||
likeEndpoint = `/feed/posts/${postId}/like`;
|
||||
}
|
||||
await fetch(likeEndpoint, { method: 'POST' });
|
||||
const btn = document.getElementById('lk-' + postId);
|
||||
const lc = document.getElementById('lkc-' + postId);
|
||||
const was = btn.classList.contains('active');
|
||||
btn.classList.toggle('active', !was);
|
||||
lc.textContent = parseInt(lc.textContent) + (was ? -1 : 1);
|
||||
}
|
||||
|
||||
// ── Vote ──
|
||||
async function votePost(postId, optionId, tab, postType) {
|
||||
if (postType === 'GROUP') {
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
const gruppeId = card?.dataset?.gruppeId;
|
||||
if (!gruppeId) return;
|
||||
await fetch(`/gruppen/${gruppeId}/posts/${postId}/vote`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ optionId })
|
||||
});
|
||||
} else {
|
||||
await fetch('/feed/posts/' + postId + '/vote', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ optionId })
|
||||
});
|
||||
}
|
||||
reloadPost(postId, tab);
|
||||
}
|
||||
|
||||
async function reloadPost(postId, tab) {
|
||||
const state = feedState[tab];
|
||||
state.page = 0; state.hasMore = true; state.loaded = false;
|
||||
document.getElementById(tab + 'Feed').innerHTML = '';
|
||||
document.getElementById(tab + 'Empty').style.display = 'none';
|
||||
await loadFeed(tab);
|
||||
}
|
||||
|
||||
// ── Delete ──
|
||||
async function deletePost(postId) {
|
||||
if (!confirm('Post löschen?')) return;
|
||||
const res = await fetch('/feed/posts/' + postId, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
document.getElementById('pc-' + postId)?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lightbox ──
|
||||
function openLb(postId, postType) {
|
||||
activeLbPostId = postId;
|
||||
activeLbPostType = postType;
|
||||
const card = document.getElementById('pc-' + postId);
|
||||
if (card) {
|
||||
const clone = card.cloneNode(true);
|
||||
clone.querySelectorAll('.post-actions').forEach(el => el.remove());
|
||||
document.getElementById('lbPostBody').innerHTML = clone.innerHTML;
|
||||
}
|
||||
loadLbComments(postId, postType);
|
||||
document.getElementById('postLightbox').classList.add('open');
|
||||
}
|
||||
|
||||
function closeLb() {
|
||||
document.getElementById('postLightbox').classList.remove('open');
|
||||
activeLbPostId = null;
|
||||
activeLbPostType = null;
|
||||
}
|
||||
|
||||
document.getElementById('postLightbox').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('postLightbox')) closeLb();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && document.getElementById('postLightbox').classList.contains('open')) closeLb();
|
||||
});
|
||||
|
||||
async function loadLbComments(postId, postType) {
|
||||
const targetType = postType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
|
||||
const res = await fetch(`/social/kommentare?targetType=${targetType}&targetId=${postId}`);
|
||||
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('');
|
||||
}
|
||||
|
||||
async function postLbComment() {
|
||||
if (!activeLbPostId) return;
|
||||
const input = document.getElementById('lbCommentInput');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
const targetType = activeLbPostType === 'GROUP' ? 'GROUP_POST' : 'FEED_POST';
|
||||
await fetch('/social/kommentare', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ targetType, targetId: activeLbPostId, text })
|
||||
});
|
||||
input.value = '';
|
||||
await loadLbComments(activeLbPostId, activeLbPostType);
|
||||
const kcEl = document.getElementById('kc-' + activeLbPostId);
|
||||
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);
|
||||
}
|
||||
|
||||
async function deleteKommentar(kommentarId, targetType, targetId) {
|
||||
await fetch('/social/kommentare/' + kommentarId, { method: 'DELETE' });
|
||||
await loadLbComments(targetId, activeLbPostType);
|
||||
}
|
||||
|
||||
// ── 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';
|
||||
}
|
||||
});
|
||||
|
||||
// ── 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' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -88,6 +88,31 @@
|
||||
|
||||
.empty-hint { color: var(--color-muted); font-size: 0.9rem; margin-top: 0.5rem; }
|
||||
|
||||
/* ── Confirm dialog ── */
|
||||
.dialog-backdrop {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 200;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.dialog-backdrop.visible { display: flex; }
|
||||
.dialog {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
||||
}
|
||||
.dialog h3 { color: var(--color-primary); font-size: 1.05rem; margin-bottom: 0.75rem; }
|
||||
.dialog p { color: var(--color-muted); font-size: 0.88rem; margin-bottom: 1.25rem; }
|
||||
.dialog-actions { display: flex; gap: 0.75rem; }
|
||||
.dialog-actions button { flex: 1; margin-top: 0; }
|
||||
|
||||
.tab-badge {
|
||||
display: inline-block;
|
||||
background: var(--color-primary);
|
||||
@@ -128,6 +153,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm remove dialog -->
|
||||
<div class="dialog-backdrop" id="removeDialog">
|
||||
<div class="dialog">
|
||||
<h3>Freundschaft beenden</h3>
|
||||
<p id="removeDialogText">Möchtest du diese Freundschaft wirklich beenden?</p>
|
||||
<div class="dialog-actions">
|
||||
<button class="secondary" onclick="closeRemoveDialog()">Abbrechen</button>
|
||||
<button id="removeConfirmBtn" style="background:#c0392b;" onclick="confirmRemove()">Entfernen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
@@ -253,24 +290,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFriend(friendshipId, btn) {
|
||||
let pendingRemoveFriendshipId = null;
|
||||
let pendingRemoveBtn = null;
|
||||
|
||||
function removeFriend(friendshipId, btn) {
|
||||
pendingRemoveFriendshipId = friendshipId;
|
||||
pendingRemoveBtn = btn;
|
||||
document.getElementById('removeDialogText').textContent =
|
||||
'Möchtest du ' + (btn.closest('.user-item').querySelector('.user-name')?.textContent || 'diese Person') + ' wirklich aus deiner Freundesliste entfernen?';
|
||||
document.getElementById('removeDialog').classList.add('visible');
|
||||
}
|
||||
|
||||
function closeRemoveDialog() {
|
||||
document.getElementById('removeDialog').classList.remove('visible');
|
||||
pendingRemoveFriendshipId = null;
|
||||
pendingRemoveBtn = null;
|
||||
}
|
||||
|
||||
async function confirmRemove() {
|
||||
if (!pendingRemoveFriendshipId) return;
|
||||
const btn = document.getElementById('removeConfirmBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
await fetch('/social/friends/reject', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ friendshipId })
|
||||
body: JSON.stringify({ friendshipId: pendingRemoveFriendshipId })
|
||||
});
|
||||
document.getElementById('friend-' + friendshipId)?.remove();
|
||||
document.getElementById('friend-' + pendingRemoveFriendshipId)?.remove();
|
||||
const list = document.getElementById('friendsList');
|
||||
if (list.children.length === 0) {
|
||||
document.getElementById('friendsEmpty').style.display = '';
|
||||
}
|
||||
closeRemoveDialog();
|
||||
} catch (e) {
|
||||
if (pendingRemoveBtn) pendingRemoveBtn.disabled = false;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Entfernen';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('removeDialog').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('removeDialog')) closeRemoveDialog();
|
||||
});
|
||||
|
||||
loadFriends();
|
||||
loadPending();
|
||||
</script>
|
||||
|
||||
1179
xxxthegame/src/main/resources/static/gruppe.html
Normal file
1179
xxxthegame/src/main/resources/static/gruppe.html
Normal file
File diff suppressed because it is too large
Load Diff
403
xxxthegame/src/main/resources/static/gruppen.html
Normal file
403
xxxthegame/src/main/resources/static/gruppen.html
Normal file
@@ -0,0 +1,403 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gruppen – XXX The Game</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.tabs { display:flex; gap:0; margin-bottom:1.5rem; border-bottom:1px solid var(--color-secondary); }
|
||||
.tab-btn { background:none; border:none; border-bottom:3px solid transparent; border-radius:0; padding:0.6rem 1.25rem; font-size:0.95rem; font-weight:600; color:var(--color-muted); cursor:pointer; margin-bottom:-1px; transition:color 0.15s,border-color 0.15s; }
|
||||
.tab-btn:hover { color:var(--color-text); background:none; }
|
||||
.tab-btn.active { color:var(--color-primary); border-bottom-color:var(--color-primary); }
|
||||
.tab-panel { display:none; }
|
||||
.tab-panel.active { display:block; }
|
||||
|
||||
.gruppe-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:1rem; margin-top:0.5rem; }
|
||||
.gruppe-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; }
|
||||
.gruppe-card-img { width:100%; height:110px; object-fit:cover; background:var(--color-secondary); display:flex; align-items:center; justify-content:center; font-size:2.5rem; }
|
||||
.gruppe-card-img img { width:100%; height:100%; object-fit:cover; }
|
||||
.gruppe-card-body { padding:0.85rem; flex:1; display:flex; flex-direction:column; gap:0.4rem; }
|
||||
.gruppe-card-name { font-weight:700; font-size:1rem; }
|
||||
.gruppe-card-meta { font-size:0.78rem; color:var(--color-muted); }
|
||||
.gruppe-card-actions { display:flex; gap:0.5rem; flex-wrap:wrap; margin-top:auto; padding-top:0.5rem; }
|
||||
.gruppe-card-actions button, .gruppe-card-actions a.btn { margin-top:0; padding:0.3rem 0.7rem; font-size:0.8rem; width:auto; }
|
||||
|
||||
.role-badge { font-size:0.7rem; font-weight:700; padding:0.1rem 0.4rem; border-radius:4px; background:var(--color-primary); color:#fff; display:inline-block; }
|
||||
.role-badge.mitglied { background:var(--color-secondary); color:var(--color-text); }
|
||||
|
||||
.search-row { display:flex; gap:0.75rem; margin-bottom:1rem; }
|
||||
.search-row input { flex:1; }
|
||||
.search-row button { white-space:nowrap; width:auto; margin-top:0; }
|
||||
|
||||
.anfrage-list { list-style:none; margin:0; padding:0; }
|
||||
.anfrage-item { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:0.75rem 0; border-bottom:1px solid var(--color-secondary); }
|
||||
.anfrage-item:last-child { border-bottom:none; }
|
||||
.anfrage-name { font-weight:600; }
|
||||
.anfrage-status { font-size:0.8rem; color:var(--color-muted); }
|
||||
|
||||
.empty-hint { color:var(--color-muted); font-size:0.9rem; margin-top:0.5rem; }
|
||||
|
||||
/* Dialog */
|
||||
.dialog-backdrop { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:200; align-items:center; justify-content:center; }
|
||||
.dialog-backdrop.visible { display:flex; }
|
||||
.dialog { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:12px; padding:1.75rem; width:100%; max-width:420px; box-shadow:0 8px 32px rgba(0,0,0,0.6); max-height:90vh; overflow-y:auto; }
|
||||
.dialog h3 { color:var(--color-primary); font-size:1.1rem; margin-bottom:1.25rem; }
|
||||
.dialog label { display:block; font-size:0.8rem; color:#aaa; margin-bottom:0.3rem; margin-top:1rem; }
|
||||
.dialog input, .dialog textarea { width:100%; box-sizing:border-box; }
|
||||
.dialog textarea { width:100%; padding:0.6rem 0.85rem; border:1px solid var(--color-secondary); border-radius:6px; background:var(--color-secondary); color:var(--color-text); font-size:0.95rem; outline:none; transition:border-color 0.2s; resize:vertical; min-height:80px; box-sizing:border-box; }
|
||||
.dialog textarea:focus { border-color:var(--color-primary); }
|
||||
.dialog-actions { display:flex; justify-content:flex-end; gap:0.75rem; margin-top:1.5rem; }
|
||||
.dialog-actions button { flex:none; margin:0; padding:0.55rem 1.1rem; font-size:0.9rem; width:auto; }
|
||||
.toggle-row { display:flex; align-items:center; gap:0.75rem; margin-top:0.75rem; }
|
||||
.toggle-row label { margin:0; font-size:0.9rem; color:var(--color-text); }
|
||||
|
||||
.img-preview { width:100%; max-height:140px; object-fit:cover; border-radius:6px; margin-top:0.5rem; display:none; }
|
||||
|
||||
.card-notif { font-size:0.75rem; font-weight:700; color:#fff; background:#e67e22; border-radius:4px; padding:0.15rem 0.45rem; display:none; width:fit-content; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.25rem;">
|
||||
<h1 style="margin:0;">Gruppen</h1>
|
||||
<button onclick="openCreateDialog()" style="width:auto; padding:0.4rem 1rem; font-size:0.9rem; margin:0;">+ Erstellen</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="switchTab('mine', this)">Meine Gruppen</button>
|
||||
<button class="tab-btn" onclick="switchTab('discover', this)">Entdecken</button>
|
||||
<button class="tab-btn" onclick="switchTab('requests', this)">Meine Anfragen</button>
|
||||
</div>
|
||||
|
||||
<!-- Meine Gruppen -->
|
||||
<div class="tab-panel active" id="tab-mine">
|
||||
<div class="gruppe-grid" id="mineGrid"></div>
|
||||
<p class="empty-hint" id="mineEmpty" style="display:none;">Du bist noch in keiner Gruppe.</p>
|
||||
</div>
|
||||
|
||||
<!-- Entdecken -->
|
||||
<div class="tab-panel" id="tab-discover">
|
||||
<div class="search-row">
|
||||
<input type="text" id="searchInput" placeholder="Gruppenname suchen…" onkeydown="if(event.key==='Enter')doSearch()">
|
||||
<button onclick="doSearch()">Suchen</button>
|
||||
</div>
|
||||
<div class="gruppe-grid" id="discoverGrid"></div>
|
||||
<p class="empty-hint" id="discoverHint">Gib einen Suchbegriff ein.</p>
|
||||
</div>
|
||||
|
||||
<!-- Meine Anfragen -->
|
||||
<div class="tab-panel" id="tab-requests">
|
||||
<ul class="anfrage-list" id="requestsList"></ul>
|
||||
<p class="empty-hint" id="requestsEmpty" style="display:none;">Keine ausstehenden Anfragen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gruppe erstellen Dialog -->
|
||||
<div class="dialog-backdrop" id="createDialog">
|
||||
<div class="dialog">
|
||||
<h3>Gruppe erstellen</h3>
|
||||
<label>Name *</label>
|
||||
<input type="text" id="createName" maxlength="100" placeholder="Gruppenname">
|
||||
<label>Beschreibung</label>
|
||||
<textarea id="createDesc" maxlength="1000" placeholder="Worum geht es in dieser Gruppe?"></textarea>
|
||||
<label>Bild (optional)</label>
|
||||
<input type="file" id="createBildFile" accept="image/*" onchange="previewBild(this,'createBildPreview','createBildData')">
|
||||
<img id="createBildPreview" class="img-preview" alt="">
|
||||
<input type="hidden" id="createBildData">
|
||||
<div class="toggle-row">
|
||||
<input type="checkbox" id="createPrivate">
|
||||
<label for="createPrivate">Private Gruppe (Beitritt nur per Anfrage)</label>
|
||||
</div>
|
||||
<p class="message error" id="createError" style="display:none; margin-top:0.75rem;"></p>
|
||||
<div class="dialog-actions">
|
||||
<button class="secondary" onclick="closeCreateDialog()">Abbrechen</button>
|
||||
<button onclick="createGruppe()">Erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beitrittsanfrage Dialog -->
|
||||
<div class="dialog-backdrop" id="joinDialog">
|
||||
<div class="dialog">
|
||||
<h3>Beitrittsanfrage senden</h3>
|
||||
<p id="joinDialogGroupName" style="font-weight:600; margin-bottom:0.5rem;"></p>
|
||||
<label>Nachricht (optional)</label>
|
||||
<textarea id="joinNachricht" placeholder="Warum möchtest du beitreten?"></textarea>
|
||||
<p class="message error" id="joinError" style="display:none; margin-top:0.75rem;"></p>
|
||||
<div class="dialog-actions">
|
||||
<button class="secondary" onclick="closeJoinDialog()">Abbrechen</button>
|
||||
<button onclick="sendJoinRequest()">Anfrage senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
if (name === 'mine') loadMine();
|
||||
if (name === 'requests') loadRequests();
|
||||
}
|
||||
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
||||
|
||||
function gruppeCard(g, showJoin = false) {
|
||||
const img = g.bild
|
||||
? `<div class="gruppe-card-img"><img src="data:image/jpeg;base64,${g.bild}" alt=""></div>`
|
||||
: `<div class="gruppe-card-img">👥</div>`;
|
||||
const roleBadge = g.myRole
|
||||
? `<span class="role-badge ${g.myRole === 'ADMIN' ? '' : 'mitglied'}">${g.myRole === 'ADMIN' ? 'Admin' : 'Mitglied'}</span>`
|
||||
: '';
|
||||
const privBadge = g.isPrivate ? ' 🔒' : '';
|
||||
let actions = '';
|
||||
if (showJoin && !g.myRole) {
|
||||
if (g.myRequestStatus === 'AUSSTEHEND') {
|
||||
actions = `<button disabled style="opacity:0.6;" onclick="event.stopPropagation()">Anfrage ausstehend</button>`;
|
||||
} else if (g.isPrivate) {
|
||||
actions = `<button onclick="event.stopPropagation(); openJoinDialog('${g.gruppeId}','${esc(g.name)}')">Anfrage senden</button>`;
|
||||
} else {
|
||||
actions = `<button onclick="event.stopPropagation(); joinGruppe('${g.gruppeId}', this)">Beitreten</button>`;
|
||||
}
|
||||
}
|
||||
return `
|
||||
<div class="gruppe-card" id="gc-${g.gruppeId}" onclick="location.href='/gruppe.html?gruppeId=${g.gruppeId}'" style="cursor:pointer;">
|
||||
${img}
|
||||
<div class="gruppe-card-body">
|
||||
<div class="gruppe-card-name">${esc(g.name)}${privBadge} ${roleBadge}</div>
|
||||
<div class="gruppe-card-meta">${g.memberCount} Mitglied${g.memberCount !== 1 ? 'er' : ''} · ${g.postCount} Beiträge</div>
|
||||
${g.beschreibung ? `<div style="font-size:0.82rem;color:var(--color-muted);">${esc(g.beschreibung.substring(0,80))}${g.beschreibung.length>80?'…':''}</div>` : ''}
|
||||
<div class="card-notif" id="notif-${g.gruppeId}"></div>
|
||||
${actions ? `<div class="gruppe-card-actions">${actions}</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function loadMine() {
|
||||
try {
|
||||
const res = await fetch('/gruppen/mine');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const grid = document.getElementById('mineGrid');
|
||||
grid.innerHTML = '';
|
||||
if (data.length === 0) { document.getElementById('mineEmpty').style.display = ''; return; }
|
||||
document.getElementById('mineEmpty').style.display = 'none';
|
||||
data.forEach(g => grid.insertAdjacentHTML('beforeend', gruppeCard(g)));
|
||||
loadAdminBadges(data);
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function loadAdminBadges(groups) {
|
||||
const adminGroups = groups.filter(g => g.myRole === 'ADMIN');
|
||||
await Promise.all(adminGroups.map(async g => {
|
||||
const [reqRes, repRes] = await Promise.all([
|
||||
fetch('/gruppen/' + g.gruppeId + '/requests'),
|
||||
fetch('/gruppen/' + g.gruppeId + '/reports')
|
||||
]);
|
||||
const reqs = reqRes.ok ? await reqRes.json() : [];
|
||||
const reps = repRes.ok ? await repRes.json() : [];
|
||||
const total = reqs.length + reps.length;
|
||||
if (total === 0) return;
|
||||
const el = document.getElementById('notif-' + g.gruppeId);
|
||||
if (!el) return;
|
||||
const parts = [];
|
||||
if (reqs.length > 0) parts.push(reqs.length + ' Anfrage' + (reqs.length !== 1 ? 'n' : ''));
|
||||
if (reps.length > 0) parts.push(reps.length + ' Meldung' + (reps.length !== 1 ? 'en' : ''));
|
||||
el.textContent = '🔔 ' + parts.join(' · ');
|
||||
el.style.display = '';
|
||||
}));
|
||||
}
|
||||
|
||||
async function doSearch() {
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
if (!q) return;
|
||||
try {
|
||||
const res = await fetch('/gruppen/search?q=' + encodeURIComponent(q));
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const grid = document.getElementById('discoverGrid');
|
||||
const hint = document.getElementById('discoverHint');
|
||||
grid.innerHTML = '';
|
||||
if (data.length === 0) { hint.textContent = 'Keine Gruppen gefunden.'; hint.style.display = ''; return; }
|
||||
hint.style.display = 'none';
|
||||
data.forEach(g => grid.insertAdjacentHTML('beforeend', gruppeCard(g, true)));
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function joinGruppe(gruppeId, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const res = await fetch('/gruppen/' + gruppeId + '/join', { method: 'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
|
||||
if (res.ok || res.status === 201) {
|
||||
btn.textContent = 'Beigetreten ✓';
|
||||
setTimeout(loadMine, 500);
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Beitreten';
|
||||
}
|
||||
} catch(e) { btn.disabled = false; btn.textContent = 'Beitreten'; }
|
||||
}
|
||||
|
||||
async function loadRequests() {
|
||||
try {
|
||||
const reqRes = await fetch('/gruppen/requests/mine');
|
||||
if (!reqRes.ok) { document.getElementById('requestsEmpty').style.display = ''; return; }
|
||||
const data = await reqRes.json();
|
||||
const list = document.getElementById('requestsList');
|
||||
list.innerHTML = '';
|
||||
if (data.length === 0) { document.getElementById('requestsEmpty').style.display = ''; return; }
|
||||
document.getElementById('requestsEmpty').style.display = 'none';
|
||||
data.forEach(r => {
|
||||
list.insertAdjacentHTML('beforeend', `
|
||||
<li class="anfrage-item" id="req-${r.anfrageId}">
|
||||
<div>
|
||||
<div class="anfrage-name">${esc(r.gruppeName)}</div>
|
||||
<div class="anfrage-status">Ausstehend seit ${fmtDate(r.angefragtAt)}</div>
|
||||
${r.nachricht ? `<div style="font-size:0.82rem;color:var(--color-muted);margin-top:0.2rem;">"${esc(r.nachricht)}"</div>` : ''}
|
||||
</div>
|
||||
<button class="secondary" onclick="withdrawRequest('${r.gruppeId}', '${r.anfrageId}', this)">Zurückziehen</button>
|
||||
</li>`);
|
||||
});
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function withdrawRequest(gruppeId, anfrageId, btn) {
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await fetch('/gruppen/' + gruppeId + '/requests/mine', { method: 'DELETE' });
|
||||
document.getElementById('req-' + anfrageId)?.remove();
|
||||
if (document.getElementById('requestsList').children.length === 0)
|
||||
document.getElementById('requestsEmpty').style.display = '';
|
||||
} catch(e) { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '';
|
||||
return new Date(iso).toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric' });
|
||||
}
|
||||
|
||||
// ── Create dialog ──
|
||||
|
||||
function openCreateDialog() {
|
||||
document.getElementById('createName').value = '';
|
||||
document.getElementById('createDesc').value = '';
|
||||
document.getElementById('createPrivate').checked = false;
|
||||
document.getElementById('createBildData').value = '';
|
||||
document.getElementById('createBildPreview').style.display = 'none';
|
||||
document.getElementById('createBildFile').value = '';
|
||||
document.getElementById('createDialog').classList.add('visible');
|
||||
}
|
||||
function closeCreateDialog() { document.getElementById('createDialog').classList.remove('visible'); document.getElementById('createError').style.display = 'none'; }
|
||||
|
||||
function showCreateError(text) {
|
||||
const el = document.getElementById('createError');
|
||||
el.textContent = text;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
async function createGruppe() {
|
||||
const name = document.getElementById('createName').value.trim();
|
||||
if (!name) { showCreateError('Bitte einen Namen eingeben.'); return; }
|
||||
document.getElementById('createError').style.display = 'none';
|
||||
const body = {
|
||||
name,
|
||||
beschreibung: document.getElementById('createDesc').value.trim() || null,
|
||||
bild: document.getElementById('createBildData').value || null,
|
||||
isPrivate: document.getElementById('createPrivate').checked
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/gruppen', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
||||
if (res.ok || res.status === 201) {
|
||||
const g = await res.json();
|
||||
closeCreateDialog();
|
||||
window.location.href = '/gruppe.html?gruppeId=' + g.gruppeId;
|
||||
} else {
|
||||
showCreateError('Fehler beim Erstellen der Gruppe.');
|
||||
}
|
||||
} catch(e) { showCreateError('Fehler: ' + e.message); }
|
||||
}
|
||||
|
||||
// ── Join dialog ──
|
||||
|
||||
let pendingJoinGruppeId = null;
|
||||
function openJoinDialog(gruppeId, name) {
|
||||
pendingJoinGruppeId = gruppeId;
|
||||
document.getElementById('joinDialogGroupName').textContent = name;
|
||||
document.getElementById('joinNachricht').value = '';
|
||||
document.getElementById('joinDialog').classList.add('visible');
|
||||
}
|
||||
function closeJoinDialog() { document.getElementById('joinDialog').classList.remove('visible'); pendingJoinGruppeId = null; }
|
||||
|
||||
async function sendJoinRequest() {
|
||||
if (!pendingJoinGruppeId) return;
|
||||
document.getElementById('joinError').style.display = 'none';
|
||||
const nachricht = document.getElementById('joinNachricht').value.trim() || null;
|
||||
try {
|
||||
const res = await fetch('/gruppen/' + pendingJoinGruppeId + '/join', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ nachricht })
|
||||
});
|
||||
if (res.ok || res.status === 201) {
|
||||
closeJoinDialog();
|
||||
doSearch();
|
||||
} else {
|
||||
const el = document.getElementById('joinError');
|
||||
el.textContent = 'Fehler beim Senden der Anfrage.';
|
||||
el.style.display = 'block';
|
||||
}
|
||||
} catch(e) {
|
||||
const el = document.getElementById('joinError');
|
||||
el.textContent = 'Fehler: ' + e.message;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image preview ──
|
||||
|
||||
function previewBild(input, previewId, dataId) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const original = new Image();
|
||||
original.onload = () => {
|
||||
const MAX = 256;
|
||||
let w = original.width, h = original.height;
|
||||
if (w > MAX || h > MAX) {
|
||||
if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
|
||||
else { w = Math.round(w * MAX / h); h = MAX; }
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(original, 0, 0, w, h);
|
||||
const scaled = canvas.toDataURL('image/jpeg', 0.85);
|
||||
const img = document.getElementById(previewId);
|
||||
img.src = scaled;
|
||||
img.style.display = 'block';
|
||||
document.getElementById(dataId).value = scaled.split(',')[1];
|
||||
};
|
||||
original.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
document.getElementById('createDialog').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('createDialog')) closeCreateDialog();
|
||||
});
|
||||
document.getElementById('joinDialog').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('joinDialog')) closeJoinDialog();
|
||||
});
|
||||
|
||||
loadMine();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -136,7 +136,7 @@
|
||||
);
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
sidebar.querySelectorAll('a:not([href="/login/logout"]):not(.sidebar-group-toggle)').forEach(l =>
|
||||
l.addEventListener('click', () => { if (window.innerWidth <= 768) closeMenu(); })
|
||||
l.addEventListener('click', () => { if (window.innerWidth <= (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-mobile').trim()) || 768)) closeMenu(); })
|
||||
);
|
||||
|
||||
// Social sidebar auf allen App-Seiten nachladen
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
const path = window.location.pathname;
|
||||
|
||||
const links = [
|
||||
{ href: '/feed.html', icon: '📰', label: 'Feed', badgeId: null, mobileBadgeId: null },
|
||||
{ href: '/personen-suchen.html', icon: '⊕', label: 'Personen suchen', badgeId: null, mobileBadgeId: null },
|
||||
{ href: '/freunde.html', icon: '♡', label: 'Freunde', badgeId: 'socialFriendsBadge', mobileBadgeId: 'socialMobileFriendsBadge' },
|
||||
{ href: '/nachrichten.html', icon: '✉', label: 'Nachrichten', badgeId: 'socialMsgBadge', mobileBadgeId: 'socialMobileMsgBadge' },
|
||||
{ href: '/gruppen.html', icon: '👥', label: 'Gruppen', badgeId: 'socialGruppenBadge', mobileBadgeId: 'socialMobileGruppenBadge' },
|
||||
];
|
||||
|
||||
const profileActive = (path === '/benutzer.html' || path === '/profile.html') ? ' class="active"' : '';
|
||||
@@ -108,4 +110,10 @@
|
||||
.then(r => r.ok ? r.json() : 0)
|
||||
.then(n => setBadge(['socialMsgBadge', 'socialMobileMsgBadge'], n))
|
||||
.catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fetch('/gruppen/requests/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0),
|
||||
fetch('/gruppen/reports/pending/count').then(r => r.ok ? r.json() : 0).catch(() => 0)
|
||||
]).then(([joins, reports]) => setBadge(['socialGruppenBadge', 'socialMobileGruppenBadge'], joins + reports))
|
||||
.catch(() => {});
|
||||
})();
|
||||
|
||||
@@ -168,6 +168,60 @@
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── Profile extras ── */
|
||||
.profile-extras-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.profile-extras-grid .full-col {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
select:focus { border-color: var(--color-primary); }
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 6px;
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
transition: border-color 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
textarea:focus { border-color: var(--color-primary); }
|
||||
|
||||
.char-count {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-muted);
|
||||
text-align: right;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.char-count.over { color: var(--color-primary); }
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
@@ -237,6 +291,60 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Freiwillige Profilangaben -->
|
||||
<div class="gallery-section-label" style="margin-top:1.25rem;">Freiwillige Angaben</div>
|
||||
<div class="profile-extras-grid">
|
||||
<div>
|
||||
<label>Alter</label>
|
||||
<input type="number" id="profileAlter" min="18" max="99" placeholder="—">
|
||||
</div>
|
||||
<div>
|
||||
<label>Größe (cm)</label>
|
||||
<input type="number" id="profileGroesse" min="100" max="250" placeholder="—">
|
||||
</div>
|
||||
<div>
|
||||
<label>Gewicht (kg)</label>
|
||||
<input type="number" id="profileGewicht" min="30" max="300" placeholder="—">
|
||||
</div>
|
||||
<div>
|
||||
<label>Geschlecht</label>
|
||||
<select id="profileGeschlecht">
|
||||
<option value="">— keine Angabe —</option>
|
||||
<option value="WEIBLICH">weiblich</option>
|
||||
<option value="DIVERS">divers</option>
|
||||
<option value="MAENNLICH">männlich</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="full-col">
|
||||
<label>Neigung</label>
|
||||
<select id="profileNeigung">
|
||||
<option value="">— keine Angabe —</option>
|
||||
<option value="DEVOT">devot</option>
|
||||
<option value="EHER_DEVOT">eher devot</option>
|
||||
<option value="SWITCHER">Switcher</option>
|
||||
<option value="EHER_DOMINANT">eher dominant</option>
|
||||
<option value="DOMINANT">dominant</option>
|
||||
<option value="KEINES">keines</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="full-col">
|
||||
<label>Beziehungsstatus</label>
|
||||
<select id="profileBeziehungsstatus">
|
||||
<option value="">— keine Angabe —</option>
|
||||
<option value="SINGLE">single</option>
|
||||
<option value="IN_EINER_BEZIEHUNG">in einer Beziehung</option>
|
||||
<option value="VERHEIRATET">verheiratet</option>
|
||||
<option value="IN_EINER_OFFENEN_BEZIEHUNG">in einer offenen Beziehung</option>
|
||||
<option value="IN_EINER_OFFENEN_EHE">in einer offenen Ehe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="full-col">
|
||||
<label>Über mich</label>
|
||||
<textarea id="profileBeschreibung" maxlength="600" placeholder="Erzähl etwas über dich…" oninput="updateCharCount()"></textarea>
|
||||
<div class="char-count" id="charCount">0 / 600</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-delete-row">
|
||||
<button class="btn-delete" onclick="openDeleteDialog()">Konto löschen</button>
|
||||
</div>
|
||||
@@ -303,6 +411,7 @@
|
||||
<script>
|
||||
let currentPicture = null;
|
||||
let currentPictureHq = null;
|
||||
let pictureDirty = false;
|
||||
|
||||
fetch('/login/me')
|
||||
.then(r => {
|
||||
@@ -317,6 +426,17 @@
|
||||
currentPicture = user.profilePicture;
|
||||
renderPicture(currentPicture);
|
||||
}
|
||||
// Fill optional profile fields
|
||||
if (user.alter) document.getElementById('profileAlter').value = user.alter;
|
||||
if (user.groesse) document.getElementById('profileGroesse').value = user.groesse;
|
||||
if (user.gewicht) document.getElementById('profileGewicht').value = user.gewicht;
|
||||
if (user.geschlecht) document.getElementById('profileGeschlecht').value = user.geschlecht;
|
||||
if (user.neigung) document.getElementById('profileNeigung').value = user.neigung;
|
||||
if (user.beziehungsstatus) document.getElementById('profileBeziehungsstatus').value = user.beziehungsstatus;
|
||||
if (user.beschreibung) {
|
||||
document.getElementById('profileBeschreibung').value = user.beschreibung;
|
||||
updateCharCount();
|
||||
}
|
||||
myUserId = user.userId;
|
||||
loadOwnGallery();
|
||||
})
|
||||
@@ -331,6 +451,7 @@
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
[currentPicture, currentPictureHq] = await Promise.all([toBase64(file, 96), toBase64(file, 1024)]);
|
||||
pictureDirty = true;
|
||||
renderPicture(currentPicture);
|
||||
});
|
||||
|
||||
@@ -339,30 +460,69 @@
|
||||
el.innerHTML = `<img src="data:image/png;base64,${base64}" alt="Profilbild">`;
|
||||
}
|
||||
|
||||
function updateCharCount() {
|
||||
const ta = document.getElementById('profileBeschreibung');
|
||||
const el = document.getElementById('charCount');
|
||||
const len = ta.value.length;
|
||||
el.textContent = len + ' / 600';
|
||||
el.classList.toggle('over', len > 600);
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
const btn = document.getElementById('saveBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird gespeichert…';
|
||||
hideMessage();
|
||||
|
||||
const beschreibung = document.getElementById('profileBeschreibung').value.trim();
|
||||
if (beschreibung.length > 600) {
|
||||
showMessage('Die Beschreibung darf maximal 600 Zeichen lang sein.', 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Profil speichern';
|
||||
return;
|
||||
}
|
||||
|
||||
const toNullOrInt = id => {
|
||||
const v = document.getElementById(id).value;
|
||||
return v ? parseInt(v, 10) : null;
|
||||
};
|
||||
const toNullOrStr = id => {
|
||||
const v = document.getElementById(id).value;
|
||||
return v || null;
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/user/me/picture', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ picture: currentPicture, pictureHq: currentPictureHq })
|
||||
});
|
||||
if (response.ok) {
|
||||
window.location.href = '/userhome.html';
|
||||
const [picRes, profileRes] = await Promise.all([
|
||||
pictureDirty ? fetch('/user/me/picture', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ picture: currentPicture, pictureHq: currentPictureHq })
|
||||
}) : Promise.resolve({ ok: true }),
|
||||
fetch('/user/me/profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
alter: toNullOrInt('profileAlter'),
|
||||
groesse: toNullOrInt('profileGroesse'),
|
||||
gewicht: toNullOrInt('profileGewicht'),
|
||||
geschlecht: toNullOrStr('profileGeschlecht'),
|
||||
neigung: toNullOrStr('profileNeigung'),
|
||||
beziehungsstatus: toNullOrStr('profileBeziehungsstatus'),
|
||||
beschreibung: beschreibung || null
|
||||
})
|
||||
})
|
||||
]);
|
||||
if (picRes.ok && profileRes.ok) {
|
||||
showMessage('Gespeichert!', 'success');
|
||||
} else {
|
||||
showMessage(`Fehler: HTTP ${response.status}`, 'error');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Profil speichern';
|
||||
showMessage(`Fehler beim Speichern.`, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage('Server nicht erreichbar.', 'error');
|
||||
console.error(err);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Profil speichern';
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user