Skip to content

Commit

Permalink
Merge pull request #702 from bounswe/develop
Browse files Browse the repository at this point in the history
Develop to Main
  • Loading branch information
atakanyasar authored Dec 16, 2024
2 parents dbb2333 + 4139649 commit 33c4561
Show file tree
Hide file tree
Showing 23 changed files with 608 additions and 392 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ public interface QuestionDifficultyRateRepository extends JpaRepository<Question

Optional<QuestionDifficultyRate> findByQuestionAndUser(Question question, User user);

long countByDifficulty(DifficultyLevel difficultyLevel);
long countByDifficultyAndQuestion(DifficultyLevel difficultyLevel, Question question);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<QuestionDifficultyRate> questionDifficultyRateOptional = getQuestionDifficultyRate(question, user);
QuestionDifficultyRate questionDifficultyRate = questionDifficultyRateOptional.orElseGet(() -> {
Expand All @@ -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())
Expand All @@ -44,22 +57,22 @@ public QuestionRateResponseDto rateQuestion(Long id, DifficultyLevel difficultyL
.totalCount(result.easyCount() + result.mediumCount() + result.hardCount())
.build();



}

public Optional<QuestionDifficultyRate> getQuestionDifficultyRate(Question question, User user) {
return questionDifficultyRateRepository.findByQuestionAndUser(question, user);
}

@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) {

}
}

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions frontend/src/components/AnswerCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -49,8 +49,7 @@ export const AnswerCard: React.FC<AnswerCardProps> = ({
<div className="flex items-center justify-between">
<Link to={`/users/${author.id}`} className="h-10 w-10">
<img
src={author?.profilePicture ||
placeholderProfile}
src={author?.profilePicture || placeholderProfile}
alt={"Profile picture"}
className="rounded-full object-cover"
/>
Expand Down
65 changes: 45 additions & 20 deletions frontend/src/components/AnswerItem.tsx
Original file line number Diff line number Diff line change
@@ -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<AnswerItemProps> = ({
answer,
onUpvote,
onDownvote,
onDelete,
}) => {
const { token } = useAuthStore();
const { token, selfProfile } = useAuthStore();

const isSelfAnswer = answer.author?.id === selfProfile?.id;
const { mutateAsync: deleteAnswer } = useDeleteAnswer();
return (
<Card className="border-none bg-neutral-100 px-6 py-8 shadow-sm">
<div className="flex flex-col gap-4">
Expand Down Expand Up @@ -55,23 +60,43 @@ export const AnswerItem: React.FC<AnswerItemProps> = ({
</div>
)}
</div>
<div className="flex flex-col items-end gap-1">
<Link
to={`/users/${answer.author?.id}`}
className="flex items-center gap-2"
>
<img
src={
answer.author?.profilePicture || placeholderProfile
}
alt={"Profile picture"}
className="h-8 w-8 rounded-full object-cover"
/>
<span className="text-sm font-medium">{answer.author?.name}</span>
</Link>
<span className="text-xs text-gray-700">
Answered: {new Date(answer.createdAt || "").toLocaleDateString()}
</span>
<div className="flex items-center gap-4">
<div className="flex flex-col items-end gap-1">
<Link
to={`/users/${answer.author?.id}`}
className="flex items-center gap-2"
>
<img
src={answer.author?.profilePicture || placeholderProfile}
alt={"Profile picture"}
className="h-8 w-8 rounded-full object-cover"
/>
<span className="text-sm font-medium">
{answer.author?.name}
</span>
</Link>
<span className="text-xs text-gray-700">
Answered:{" "}
{new Date(answer.createdAt || "").toLocaleDateString()}
</span>
</div>
{isSelfAnswer && (
<Button
variant="destructive"
size="icon"
onClick={() => {
deleteAnswer({
pathParams: {
answerId: answer.id,
},
}).then(() => {
onDelete();
});
}}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/Answers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
<span>
{" "}
This question doesn't have an answer yet. Contribute to the discussion
by answering this question.
</span>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/DifficultyBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export const DifficultyBar: React.FC<DifficultyBarProps> = ({
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;
};

Expand Down
26 changes: 14 additions & 12 deletions frontend/src/components/SubtypeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,24 @@ export const TagSubtypeCard = React.forwardRef<
>(({ tagSubtype }, ref) => {
return (
<Card
className="flex-1 border-none bg-neutral-100 px-6 py-8 shadow-sm"
className="flex flex-1 flex-col border-none bg-neutral-100 px-6 py-8 shadow-sm"
ref={ref}
>
<div className="flex flex-col gap-6">
{/* Subtype Name */}
<h1 className="text-xl font-semibold text-gray-800">
{tagSubtype.typeId}
</h1>
<div className="flex flex-1 flex-col justify-between ">
<div className="flex flex-col gap-6">
{/* Subtype Name */}
<h1 className="text-xl font-semibold text-gray-800">
{tagSubtype.typeId}
</h1>

{/* Description */}
<p className="text-sm text-gray-600">{tagSubtype.description}</p>
{/* Description */}
<p className="text-sm text-gray-600">{tagSubtype.description}</p>

{/* Number of Tags */}
<div className="flex items-center gap-2 text-sm text-gray-700">
<Tags className="h-5 w-5" />
<span>{tagSubtype.tagCount} tags</span>
{/* Number of Tags */}
<div className="flex items-center gap-2 text-sm text-gray-700">
<Tags className="h-5 w-5" />
<span>{tagSubtype.tagCount} tags</span>
</div>
</div>

{/* Navigation Link */}
Expand Down
69 changes: 34 additions & 35 deletions frontend/src/components/TagType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ export default function SubtypePage() {
about and solving problems in software development.
</p>
);
case "Computer Science Term":
case "Computer Science Topic":
return (
<p>
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
Expand Down Expand Up @@ -134,40 +134,39 @@ export default function SubtypePage() {

return (
<div className="container py-8">
{/* Header */}
<h1 className="mb-4 text-4xl font-bold text-gray-800">{tagTypeId}</h1>

{/* Render the description based on typeId */}
<div className="mb-6 text-lg text-gray-700">{description}</div>

{/* Tags in this type */}
<h2 className="mb-4 text-2xl font-semibold text-gray-800">
Tags in Category
</h2>

{/* Infinite Scroll for displaying Related Tags */}
<div className="grid grid-cols-3 gap-4">
<InfiniteScroll
next={next}
hasMore={
searchResultData.totalItems
? searchResultData.totalItems > pageSize
: false
}
isLoading={isLoading}
>
{tags?.map((tag) => <TagCard key={tag.tagId} tag={tag} />)
}
</InfiniteScroll>
{isLoading && (
<div className="col-span-3 flex w-full items-center justify-center">
<Loader2
aria-label="Loading"
className="h-16 w-16 animate-spin text-primary"
/>
</div>
)}
{/* Header */}
<h1 className="mb-4 text-4xl font-bold text-gray-800">{tagTypeId}</h1>

{/* Render the description based on typeId */}
<div className="mb-6 text-lg text-gray-700">{description}</div>

{/* Tags in this type */}
<h2 className="mb-4 text-2xl font-semibold text-gray-800">
Tags in Category
</h2>

{/* Infinite Scroll for displaying Related Tags */}
<div className="grid grid-cols-3 gap-4">
<InfiniteScroll
next={next}
hasMore={
searchResultData.totalItems
? searchResultData.totalItems > pageSize
: false
}
isLoading={isLoading}
>
{tags?.map((tag) => <TagCard key={tag.tagId} tag={tag} />)}
</InfiniteScroll>
{isLoading && (
<div className="col-span-3 flex w-full items-center justify-center">
<Loader2
aria-label="Loading"
className="h-16 w-16 animate-spin text-primary"
/>
</div>
)}
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function IndexRoute() {
<>
<div className="container flex flex-col gap-2 py-8">
<h1 className="mb-4 text-4xl font-bold">
Welcome to Programming Languages Forum
Welcome to the Programming Languages Forum
</h1>
<Feed />
</div>
Expand Down
Loading

0 comments on commit 33c4561

Please sign in to comment.