diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionDifficultyRateRepository.java b/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionDifficultyRateRepository.java index 63ec5b5..9a8edfc 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionDifficultyRateRepository.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionDifficultyRateRepository.java @@ -15,5 +15,5 @@ public interface QuestionDifficultyRateRepository extends JpaRepository findByQuestionAndUser(Question question, User user); - long countByDifficulty(DifficultyLevel difficultyLevel); + long countByDifficultyAndQuestion(DifficultyLevel difficultyLevel, Question question); } diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionDifficultyRateService.java b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionDifficultyRateService.java index 2f59d1d..86e62a7 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionDifficultyRateService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionDifficultyRateService.java @@ -8,23 +8,22 @@ import com.group1.programminglanguagesforum.Exceptions.UnauthorizedAccessException; import com.group1.programminglanguagesforum.Repositories.QuestionDifficultyRateRepository; import com.group1.programminglanguagesforum.Repositories.QuestionRepository; +import java.util.NoSuchElementException; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; -import java.util.NoSuchElementException; -import java.util.Optional; - @Service @RequiredArgsConstructor public class QuestionDifficultyRateService { + private final QuestionDifficultyRateRepository questionDifficultyRateRepository; private final UserContextService userContextService; private final QuestionRepository questionRepository; - public QuestionRateResponseDto rateQuestion(Long id, DifficultyLevel difficultyLevel) throws UnauthorizedAccessException { - Question question = questionRepository.findById(id).orElseThrow(() -> new NoSuchElementException("Question not found")); + Question question = questionRepository.findById(id).orElseThrow(() -> new NoSuchElementException("Question not found")); User user = userContextService.getCurrentUser(); Optional questionDifficultyRateOptional = getQuestionDifficultyRate(question, user); QuestionDifficultyRate questionDifficultyRate = questionDifficultyRateOptional.orElseGet(() -> { @@ -35,7 +34,21 @@ public QuestionRateResponseDto rateQuestion(Long id, DifficultyLevel difficultyL }); questionDifficultyRate.setDifficulty(difficultyLevel); questionDifficultyRateRepository.save(questionDifficultyRate); - QuestionRateCounts result = getResult(); + QuestionRateCounts result = getResult(id); + + DifficultyLevel newDifficulty = question.getDifficulty(); + + if (result.easyCount() > result.mediumCount() && result.easyCount() > result.hardCount()) { + newDifficulty = DifficultyLevel.EASY; + } else if (result.mediumCount() > result.easyCount() && result.mediumCount() > result.hardCount()) { + newDifficulty = DifficultyLevel.MEDIUM; + } else if (result.hardCount() > result.easyCount() && result.hardCount() > result.mediumCount()) { + newDifficulty = DifficultyLevel.HARD; + } + + question.setDifficulty(newDifficulty); + questionRepository.save(question); + return QuestionRateResponseDto.builder() .questionId(id) .easyCount(result.easyCount()) @@ -44,8 +57,6 @@ public QuestionRateResponseDto rateQuestion(Long id, DifficultyLevel difficultyL .totalCount(result.easyCount() + result.mediumCount() + result.hardCount()) .build(); - - } public Optional getQuestionDifficultyRate(Question question, User user) { @@ -53,13 +64,15 @@ public Optional getQuestionDifficultyRate(Question quest } @NonNull - public QuestionRateCounts getResult() { - long easyCount = questionDifficultyRateRepository.countByDifficulty(DifficultyLevel.EASY); - long mediumCount = questionDifficultyRateRepository.countByDifficulty(DifficultyLevel.MEDIUM); - long hardCount = questionDifficultyRateRepository.countByDifficulty(DifficultyLevel.HARD); + public QuestionRateCounts getResult(Long questionId) { + Question question = questionRepository.findById(questionId).orElseThrow(() -> new NoSuchElementException("Question not found")); + long easyCount = questionDifficultyRateRepository.countByDifficultyAndQuestion(DifficultyLevel.EASY, question); + long mediumCount = questionDifficultyRateRepository.countByDifficultyAndQuestion(DifficultyLevel.MEDIUM, question); + long hardCount = questionDifficultyRateRepository.countByDifficultyAndQuestion(DifficultyLevel.HARD, question); return new QuestionRateCounts(easyCount, mediumCount, hardCount); } public record QuestionRateCounts(long easyCount, long mediumCount, long hardCount) { + } } diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java index 35efcf9..31298f7 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java @@ -1,21 +1,5 @@ package com.group1.programminglanguagesforum.Services; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.Objects; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; - import com.group1.programminglanguagesforum.DTOs.Requests.CreateQuestionRequestDto; import com.group1.programminglanguagesforum.DTOs.Requests.UpdateQuestionRequestDto; import com.group1.programminglanguagesforum.DTOs.Responses.AuthorDto; @@ -32,244 +16,258 @@ import com.group1.programminglanguagesforum.Repositories.BookmarkRepository; import com.group1.programminglanguagesforum.Repositories.QuestionRepository; import com.group1.programminglanguagesforum.Repositories.VoteRepository; - +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class QuestionService { - private final QuestionRepository questionRepository; - private final UserContextService userContextService; - private final TagService tagService; - private final BookmarkRepository bookmarkRepository; - private final VoteRepository voteRepository; - private final QuestionDifficultyRateService questionDifficultyRateService; - public Optional findById(Long id) { - return questionRepository.findById(id); - } + private final QuestionRepository questionRepository; + private final UserContextService userContextService; + private final TagService tagService; + private final BookmarkRepository bookmarkRepository; + private final VoteRepository voteRepository; + private final QuestionDifficultyRateService questionDifficultyRateService; - public List findByAuthorId(Long authorId) { - return questionRepository.findByAuthorId(authorId).stream() - .map(QuestionService::mapToQuestionSummary) - .collect(Collectors.toList()); - } + public Optional findById(Long id) { + return questionRepository.findById(id); + } - public CreateQuestionResponseDto createQuestion(CreateQuestionRequestDto dto) - throws UnauthorizedAccessException { - List tagIds = dto.getTagIds(); - Set existingTags = new HashSet<>(tagService.findAllByIdIn(tagIds)); - User currentUser = userContextService.getCurrentUser(); - Date date = new Date(); - Question question = Question.builder() - .title(dto.getTitle()) - .questionBody(dto.getContent()) - .difficulty(dto.getDifficulty()) - .askedBy(currentUser) - .createdAt(date) - .updatedAt(date) - .likeCount(0L) - .commentCount(0L) - .tags(existingTags) - .votes(new ArrayList<>()) - .build(); - questionRepository.save(question); - List tags = existingTags.stream().map(tag -> TagDto.builder() - .id(tag.getId()) - .name(tag.getTagName()) - .build()).toList(); + public List findByAuthorId(Long authorId) { + return questionRepository.findByAuthorId(authorId).stream() + .map(QuestionService::mapToQuestionSummary) + .collect(Collectors.toList()); + } - return CreateQuestionResponseDto.builder() - .id(question.getId()) - .title(question.getTitle()) - .content(question.getQuestionBody()) - .difficulty(question.getDifficulty()) - .tags(tags) - .author(AuthorDto.builder() - .id(currentUser.getId()) - .username(currentUser.getUsername()) - .reputationPoints(currentUser.getReputationPoints()) - .name(currentUser.getFirstName() + " " + currentUser.getLastName()) - .build()) - .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) - .updatedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getUpdatedAt())) - .upvoteCount(0L) - .downvoteCount(0L) - .build(); + public CreateQuestionResponseDto createQuestion(CreateQuestionRequestDto dto) + throws UnauthorizedAccessException { + List tagIds = dto.getTagIds(); + Set existingTags = new HashSet<>(tagService.findAllByIdIn(tagIds)); + User currentUser = userContextService.getCurrentUser(); + Date date = new Date(); + Question question = Question.builder() + .title(dto.getTitle()) + .questionBody(dto.getContent()) + .difficulty(dto.getDifficulty()) + .askedBy(currentUser) + .createdAt(date) + .updatedAt(date) + .likeCount(0L) + .commentCount(0L) + .tags(existingTags) + .votes(new ArrayList<>()) + .build(); + questionRepository.save(question); + List tags = existingTags.stream().map(tag -> TagDto.builder() + .id(tag.getId()) + .name(tag.getTagName()) + .build()).toList(); - } + return CreateQuestionResponseDto.builder() + .id(question.getId()) + .title(question.getTitle()) + .content(question.getQuestionBody()) + .difficulty(question.getDifficulty()) + .tags(tags) + .author(AuthorDto.builder() + .id(currentUser.getId()) + .username(currentUser.getUsername()) + .reputationPoints(currentUser.getReputationPoints()) + .name(currentUser.getFirstName() + " " + currentUser.getLastName()) + .build()) + .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) + .updatedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getUpdatedAt())) + .upvoteCount(0L) + .downvoteCount(0L) + .build(); - public GetQuestionDetailsResponseDto getQuestion(Long id) throws NoSuchElementException { - User currentUser; - try { - currentUser = userContextService.getCurrentUser(); - } catch (UnauthorizedAccessException e) { - currentUser = null; - } + } - Optional questionOptional = questionRepository.findById(id); - if (questionOptional.isEmpty()) { - throw new NoSuchElementException("Question not found"); - } + public GetQuestionDetailsResponseDto getQuestion(Long id) throws NoSuchElementException { + User currentUser; + try { + currentUser = userContextService.getCurrentUser(); + } catch (UnauthorizedAccessException e) { + currentUser = null; + } - Question question = questionOptional.get(); - boolean selfQuestion = (currentUser != null - && currentUser.getId().equals(question.getAskedBy().getId())); - boolean isBookmarked = (currentUser != null - && bookmarkRepository.existsByUserAndQuestion(currentUser, question)); - Integer selfVoted = (currentUser != null - && voteRepository.findByUserAndQuestion(currentUser, question).isPresent() - ? voteRepository.findByUserAndQuestion(currentUser, question).get() - .isUpvote() ? 1 : -1 - : 0); - QuestionDifficultyRateService.QuestionRateCounts record = questionDifficultyRateService.getResult(); - long easyCount = record.easyCount(); - long mediumCount = record.mediumCount(); - long hardCount = record.hardCount(); - DifficultyLevel lastRatedDifficulty = questionDifficultyRateService - .getQuestionDifficultyRate(questionOptional.get(), currentUser) - .map(QuestionDifficultyRate::getDifficulty).orElse(null); + Optional questionOptional = questionRepository.findById(id); + if (questionOptional.isEmpty()) { + throw new NoSuchElementException("Question not found"); + } - return GetQuestionDetailsResponseDto.builder() - .id(question.getId()) - .title(question.getTitle()) - .content(question.getQuestionBody()) - .difficulty(question.getDifficulty()) - .selfDifficultyVote(lastRatedDifficulty) - .likeCount(question.getUpvoteCount()) - .dislikeCount(question.getDownvoteCount()) - .commentCount(question.getCommentCount()) - .selfQuestion(selfQuestion) - .selfVoted(selfVoted) - .easyCount(easyCount) - .mediumCount(mediumCount) - .hardCount(hardCount) - .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) - .updatedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getUpdatedAt())) - .author(AuthorDto.builder() - .id(question.getAskedBy().getId()) - .username(question.getAskedBy().getUsername()) - .reputationPoints(question.getAskedBy().getReputationPoints()) - .name(question.getAskedBy().getFirstName() + " " - + question.getAskedBy().getLastName()) - .build()) - .rating(0L) - .tags(question.getTags().stream().map(tag -> TagDto.builder() - .id(tag.getId()) - .name(tag.getTagName()) - .build()).toList()) - .answerCount((long) question.getAnswers().size()) - .bookmarked(isBookmarked) - .build(); + Question question = questionOptional.get(); + boolean selfQuestion = (currentUser != null + && currentUser.getId().equals(question.getAskedBy().getId())); + boolean isBookmarked = (currentUser != null + && bookmarkRepository.existsByUserAndQuestion(currentUser, question)); + Integer selfVoted = (currentUser != null + && voteRepository.findByUserAndQuestion(currentUser, question).isPresent() + ? voteRepository.findByUserAndQuestion(currentUser, question).get() + .isUpvote() ? 1 : -1 + : 0); + QuestionDifficultyRateService.QuestionRateCounts record = questionDifficultyRateService.getResult(question.getId()); + long easyCount = record.easyCount(); + long mediumCount = record.mediumCount(); + long hardCount = record.hardCount(); + DifficultyLevel lastRatedDifficulty = questionDifficultyRateService + .getQuestionDifficultyRate(questionOptional.get(), currentUser) + .map(QuestionDifficultyRate::getDifficulty).orElse(null); - } + return GetQuestionDetailsResponseDto.builder() + .id(question.getId()) + .title(question.getTitle()) + .content(question.getQuestionBody()) + .difficulty(question.getDifficulty()) + .selfDifficultyVote(lastRatedDifficulty) + .likeCount(question.getUpvoteCount()) + .dislikeCount(question.getDownvoteCount()) + .commentCount(question.getCommentCount()) + .selfQuestion(selfQuestion) + .selfVoted(selfVoted) + .easyCount(easyCount) + .mediumCount(mediumCount) + .hardCount(hardCount) + .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) + .updatedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getUpdatedAt())) + .author(AuthorDto.builder() + .id(question.getAskedBy().getId()) + .username(question.getAskedBy().getUsername()) + .reputationPoints(question.getAskedBy().getReputationPoints()) + .name(question.getAskedBy().getFirstName() + " " + + question.getAskedBy().getLastName()) + .build()) + .rating(0L) + .tags(question.getTags().stream().map(tag -> TagDto.builder() + .id(tag.getId()) + .name(tag.getTagName()) + .build()).toList()) + .answerCount((long) question.getAnswers().size()) + .bookmarked(isBookmarked) + .build(); - public String deleteQuestion(Long id) { - Optional questionOptional = questionRepository.findById(id); - if (questionOptional.isEmpty()) { - throw new NoSuchElementException("Question not found"); - } - Question question = questionOptional.get(); - questionRepository.delete(question); - return "Question deleted successfully"; - } + } - public CreateQuestionResponseDto updateQuestion(Long id, UpdateQuestionRequestDto dto) { - Optional questionOptional = questionRepository.findById(id); - if (questionOptional.isEmpty()) { - throw new NoSuchElementException("Question not found"); - } - Date date = new Date(); - Question question = questionOptional.get(); - List tagIds = dto.getTags(); - Set existingTags = new HashSet<>(tagService.findAllByIdIn(tagIds)); - question.setTitle(dto.getTitle()); - question.setQuestionBody(dto.getContent()); - question.setTags(existingTags); - question.setUpdatedAt(date); - question.setDifficulty(dto.getDifficulty()); - questionRepository.save(question); - List tags = existingTags.stream().map(tag -> TagDto.builder() - .id(tag.getId()) - .name(tag.getTagName()) - .build()).toList(); - return CreateQuestionResponseDto.builder() - .id(question.getId()) - .title(question.getTitle()) - .content(question.getQuestionBody()) - .difficulty(question.getDifficulty()) - .tags(tags) - .author(AuthorDto.builder() - .id(question.getAskedBy().getId()) - .username(question.getAskedBy().getUsername()) - .reputationPoints(question.getAskedBy().getReputationPoints()) - .name(question.getAskedBy().getFirstName() + " " - + question.getAskedBy().getLastName()) - .build()) - .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) - .updatedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getUpdatedAt())) - .upvoteCount(question.getUpvoteCount()) - .downvoteCount(question.getDownvoteCount()) - .build(); + public String deleteQuestion(Long id) { + Optional questionOptional = questionRepository.findById(id); + if (questionOptional.isEmpty()) { + throw new NoSuchElementException("Question not found"); + } + Question question = questionOptional.get(); + questionRepository.delete(question); + return "Question deleted successfully"; + } + public CreateQuestionResponseDto updateQuestion(Long id, UpdateQuestionRequestDto dto) { + Optional questionOptional = questionRepository.findById(id); + if (questionOptional.isEmpty()) { + throw new NoSuchElementException("Question not found"); } + Date date = new Date(); + Question question = questionOptional.get(); + List tagIds = dto.getTags(); + Set existingTags = new HashSet<>(tagService.findAllByIdIn(tagIds)); + question.setTitle(dto.getTitle()); + question.setQuestionBody(dto.getContent()); + question.setTags(existingTags); + question.setUpdatedAt(date); + question.setDifficulty(dto.getDifficulty()); + questionRepository.save(question); + List tags = existingTags.stream().map(tag -> TagDto.builder() + .id(tag.getId()) + .name(tag.getTagName()) + .build()).toList(); + return CreateQuestionResponseDto.builder() + .id(question.getId()) + .title(question.getTitle()) + .content(question.getQuestionBody()) + .difficulty(question.getDifficulty()) + .tags(tags) + .author(AuthorDto.builder() + .id(question.getAskedBy().getId()) + .username(question.getAskedBy().getUsername()) + .reputationPoints(question.getAskedBy().getReputationPoints()) + .name(question.getAskedBy().getFirstName() + " " + + question.getAskedBy().getLastName()) + .build()) + .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) + .updatedAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getUpdatedAt())) + .upvoteCount(question.getUpvoteCount()) + .downvoteCount(question.getDownvoteCount()) + .build(); - public Page searchQuestions( - String query, - String tagIdsStr, - DifficultyLevel difficulty, - int page, - int pageSize, - String sortBy) { + } - List tagIds = null; - if (tagIdsStr != null && !tagIdsStr.isEmpty()) { - tagIds = Arrays.stream(tagIdsStr.split(",")) - .map(Long::parseLong) - .collect(Collectors.toList()); - } - User currentUser; - try { - currentUser = userContextService.getCurrentUser(); - } catch (UnauthorizedAccessException e) { - currentUser = null; - } + public Page searchQuestions( + String query, + String tagIdsStr, + DifficultyLevel difficulty, + int page, + int pageSize, + String sortBy) { - PageRequest pageable = PageRequest.of(page - 1, pageSize); - if (Objects.equals(sortBy, "default") || Objects.equals(sortBy, null) || Objects.equals(currentUser, null)) { - return questionRepository.searchQuestions(query, tagIds, difficulty, pageable); - } else { - List authorIds = currentUser.getFollowing().stream() - .map(User::getId) // Map each User to its ID - .collect(Collectors.toList()); // Collect the IDs into a List - return questionRepository.searchQuestionsByRecommended(query, authorIds, tagIds, difficulty, pageable); - } - + List tagIds = null; + if (tagIdsStr != null && !tagIdsStr.isEmpty()) { + tagIds = Arrays.stream(tagIdsStr.split(",")) + .map(Long::parseLong) + .collect(Collectors.toList()); + } + User currentUser; + try { + currentUser = userContextService.getCurrentUser(); + } catch (UnauthorizedAccessException e) { + currentUser = null; } - public static QuestionSummaryDto mapToQuestionSummary(Question question) { - return QuestionSummaryDto.builder() - .id(question.getId()) - .title(question.getTitle()) - .content(question.getQuestionBody()) - .difficulty(question.getDifficulty()) - .upvoteCount(question.getUpvoteCount()) - .downvoteCount(question.getDownvoteCount()) - .answerCount((long) question.getAnswers().size()) - .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) - .author(AuthorDto.builder() - .id(question.getAskedBy().getId()) - .username(question.getAskedBy().getUsername()) - .name(question.getAskedBy().getFirstName() + " " - + question.getAskedBy().getLastName()) - .reputationPoints(question.getAskedBy().getReputationPoints()) - .build()) - .tags(question.getTags().stream() - .map(tag -> TagDto.builder() - .id(tag.getId()) - .name(tag.getTagName()) - .build()) - .collect(Collectors.toList())) - .build(); + PageRequest pageable = PageRequest.of(page - 1, pageSize); + if (Objects.equals(sortBy, "default") || Objects.equals(sortBy, null) || Objects.equals(currentUser, null)) { + return questionRepository.searchQuestions(query, tagIds, difficulty, pageable); + } else { + List authorIds = currentUser.getFollowing().stream() + .map(User::getId) // Map each User to its ID + .collect(Collectors.toList()); // Collect the IDs into a List + return questionRepository.searchQuestionsByRecommended(query, authorIds, tagIds, difficulty, pageable); } + + } + + public static QuestionSummaryDto mapToQuestionSummary(Question question) { + return QuestionSummaryDto.builder() + .id(question.getId()) + .title(question.getTitle()) + .content(question.getQuestionBody()) + .difficulty(question.getDifficulty()) + .upvoteCount(question.getUpvoteCount()) + .downvoteCount(question.getDownvoteCount()) + .answerCount((long) question.getAnswers().size()) + .createdAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(question.getCreatedAt())) + .author(AuthorDto.builder() + .id(question.getAskedBy().getId()) + .username(question.getAskedBy().getUsername()) + .name(question.getAskedBy().getFirstName() + " " + + question.getAskedBy().getLastName()) + .reputationPoints(question.getAskedBy().getReputationPoints()) + .build()) + .tags(question.getTags().stream() + .map(tag -> TagDto.builder() + .id(tag.getId()) + .name(tag.getTagName()) + .build()) + .collect(Collectors.toList())) + .build(); + } } diff --git a/frontend/src/components/AnswerCard.tsx b/frontend/src/components/AnswerCard.tsx index 2d70f05..450832f 100644 --- a/frontend/src/components/AnswerCard.tsx +++ b/frontend/src/components/AnswerCard.tsx @@ -1,8 +1,8 @@ +import placeholderProfile from "@/assets/placeholder_profile.png"; import { Card } from "@/components/ui/card"; import { ArrowRight, CornerDownRight, Star } from "lucide-react"; import React from "react"; import { Link } from "react-router-dom"; -import placeholderProfile from "@/assets/placeholder_profile.png"; interface AnswerCardProps { id: number; @@ -49,8 +49,7 @@ export const AnswerCard: React.FC = ({
{"Profile diff --git a/frontend/src/components/AnswerItem.tsx b/frontend/src/components/AnswerItem.tsx index f750d95..3d03411 100644 --- a/frontend/src/components/AnswerItem.tsx +++ b/frontend/src/components/AnswerItem.tsx @@ -1,26 +1,31 @@ +import placeholderProfile from "@/assets/placeholder_profile.png"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { useDeleteAnswer } from "@/services/api/programmingForumComponents"; import { AnswerDetails } from "@/services/api/programmingForumSchemas"; import useAuthStore from "@/services/auth"; -import { ThumbsDown, ThumbsUp } from "lucide-react"; +import { ThumbsDown, ThumbsUp, Trash2 } from "lucide-react"; import React from "react"; import { Link } from "react-router-dom"; import { ContentWithSnippets } from "./ContentWithSnippets"; -import placeholderProfile from "@/assets/placeholder_profile.png"; interface AnswerItemProps { answer: AnswerDetails; onUpvote: () => void; onDownvote: () => void; + onDelete: () => void; } export const AnswerItem: React.FC = ({ answer, onUpvote, onDownvote, + onDelete, }) => { - const { token } = useAuthStore(); + const { token, selfProfile } = useAuthStore(); + const isSelfAnswer = answer.author?.id === selfProfile?.id; + const { mutateAsync: deleteAnswer } = useDeleteAnswer(); return (
@@ -55,23 +60,43 @@ export const AnswerItem: React.FC = ({
)}
-
- - {"Profile - {answer.author?.name} - - - Answered: {new Date(answer.createdAt || "").toLocaleDateString()} - +
+
+ + {"Profile + + {answer.author?.name} + + + + Answered:{" "} + {new Date(answer.createdAt || "").toLocaleDateString()} + +
+ {isSelfAnswer && ( + + )}
diff --git a/frontend/src/components/Answers.test.tsx b/frontend/src/components/Answers.test.tsx index 99247c3..f9ef1a6 100644 --- a/frontend/src/components/Answers.test.tsx +++ b/frontend/src/components/Answers.test.tsx @@ -21,6 +21,9 @@ vi.mock("@/services/api/programmingForumComponents", () => ({ useGetQuestionAnswers: vi.fn(), useUpvoteAnswer: vi.fn(), useDownvoteAnswer: vi.fn(), + useDeleteAnswer: vi.fn().mockReturnValue({ + mutateAsync: vi.fn(), + }), })); // Mock the auth store diff --git a/frontend/src/components/Answers.tsx b/frontend/src/components/Answers.tsx index 5c49021..a9f09af 100644 --- a/frontend/src/components/Answers.tsx +++ b/frontend/src/components/Answers.tsx @@ -67,11 +67,11 @@ export function Answers({ questionId }: AnswersProps) { answer={answer} onUpvote={() => handleVote(answer.id, 1)} onDownvote={() => handleVote(answer.id, -1)} + onDelete={() => refetch()} /> ))} {answers.length === 0 && ( - {" "} This question doesn't have an answer yet. Contribute to the discussion by answering this question. diff --git a/frontend/src/components/DifficultyBar.tsx b/frontend/src/components/DifficultyBar.tsx index 9b70276..6371ca7 100644 --- a/frontend/src/components/DifficultyBar.tsx +++ b/frontend/src/components/DifficultyBar.tsx @@ -80,6 +80,9 @@ export const DifficultyBar: React.FC = ({ current, //any equality -> pick higher difficulty level ) => (prev.count > current.count ? prev : current), ); + if (counts.reduce((prev, current) => prev + current.count, 0) === 0) { + return "-"; + } return highest.level; }; diff --git a/frontend/src/components/SubtypeCard.tsx b/frontend/src/components/SubtypeCard.tsx index f8ac9a3..2dff6f7 100644 --- a/frontend/src/components/SubtypeCard.tsx +++ b/frontend/src/components/SubtypeCard.tsx @@ -18,22 +18,24 @@ export const TagSubtypeCard = React.forwardRef< >(({ tagSubtype }, ref) => { return ( -
- {/* Subtype Name */} -

- {tagSubtype.typeId} -

+
+
+ {/* Subtype Name */} +

+ {tagSubtype.typeId} +

- {/* Description */} -

{tagSubtype.description}

+ {/* Description */} +

{tagSubtype.description}

- {/* Number of Tags */} -
- - {tagSubtype.tagCount} tags + {/* Number of Tags */} +
+ + {tagSubtype.tagCount} tags +
{/* Navigation Link */} diff --git a/frontend/src/components/TagType.tsx b/frontend/src/components/TagType.tsx index e32f945..d1f0da8 100644 --- a/frontend/src/components/TagType.tsx +++ b/frontend/src/components/TagType.tsx @@ -95,10 +95,10 @@ export default function SubtypePage() { about and solving problems in software development.

); - case "Computer Science Term": + case "Computer Science Topic": return (

- A computer science term is a word or phrase that is part of the + A computer science topic is a word or phrase that is part of the technical vocabulary of computer science. These terms represent concepts, theories, tools, or techniques that are essential to understanding the field. Examples include terms like algorithm, data @@ -134,40 +134,39 @@ export default function SubtypePage() { return (

- {/* Header */} -

{tagTypeId}

- - {/* Render the description based on typeId */} -
{description}
- - {/* Tags in this type */} -

- Tags in Category -

- - {/* Infinite Scroll for displaying Related Tags */} -
- pageSize - : false - } - isLoading={isLoading} - > - {tags?.map((tag) => ) - } - - {isLoading && ( -
- -
- )} + {/* Header */} +

{tagTypeId}

+ + {/* Render the description based on typeId */} +
{description}
+ + {/* Tags in this type */} +

+ Tags in Category +

+ + {/* Infinite Scroll for displaying Related Tags */} +
+ pageSize + : false + } + isLoading={isLoading} + > + {tags?.map((tag) => )} + + {isLoading && ( +
+
+ )} +
); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 8b14640..ef7de35 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -104,7 +104,7 @@ blockquote { } code { - @apply relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold; + @apply relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold; } @layer base { diff --git a/frontend/src/routes/home.tsx b/frontend/src/routes/home.tsx index e8e2dbe..7217df9 100644 --- a/frontend/src/routes/home.tsx +++ b/frontend/src/routes/home.tsx @@ -5,7 +5,7 @@ export function IndexRoute() { <>

- Welcome to Programming Languages Forum + Welcome to the Programming Languages Forum

diff --git a/frontend/src/routes/question.tsx b/frontend/src/routes/question.tsx index 2fd8e29..2f9d796 100644 --- a/frontend/src/routes/question.tsx +++ b/frontend/src/routes/question.tsx @@ -27,7 +27,14 @@ import { } from "@/services/api/programmingForumComponents"; import useAuthStore from "@/services/auth"; import { convertTagToTrack, useExercismSearch } from "@/services/exercism"; -import { Flag, MessageSquare, ThumbsDown, ThumbsUp, Trash } from "lucide-react"; +import { + Flag, + MessageSquare, + Pencil, + ThumbsDown, + ThumbsUp, + Trash, +} from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Link, useParams } from "react-router-dom"; @@ -112,9 +119,14 @@ export default function QuestionPage() { const titleRef = useRef(null); const contentRef = useRef(null); - const [tags, setTags] = useState( - question.tags?.map((tag) => Number(tag.id)) || [], - ); // Tag IDs state + const [tags, setTags] = useState([]); + + useEffect(() => { + if (!isEditing && question.tags) { + setTags(question.tags.map((t) => Number(t.id))); + } + }, [question?.tags, isEditing]); + const [availableTags, setAvailableTags] = useState< { tagId: string; name: string }[] >([]); // Available tags @@ -220,6 +232,15 @@ export default function QuestionPage() { }} /> )} + {!isEditing && selfProfile?.id === question.author.id && ( + + )}
@@ -294,6 +315,7 @@ export default function QuestionPage() { value: String(tag.tagId), label: tag.name || "Loading...", }))} + defaultValue={tags.map((tag) => String(tag))} value={tags.map((tag) => String(tag))} onValueChange={(selectedIds) => { const selectedTags = selectedIds.map((id) => Number(id)); // Convert back to numbers @@ -318,8 +340,8 @@ export default function QuestionPage() { {/* Question Content */} {isEditing ? ( -
-
+ <> +
{isPreviewMode ? ( -
+
) : (