From 490bcd6380a5072768c3e666383c996f955c1ffc Mon Sep 17 00:00:00 2001 From: CagatayColak Date: Tue, 10 Dec 2024 16:19:19 +0300 Subject: [PATCH 01/48] Add return questions, answers and followed tags to GET users/{userId} --- .../Controllers/UserController.java | 19 ++++++++++--------- .../Responses/UserProfileResponseDto.java | 5 ++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java index 0abad964..1318129d 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java @@ -42,16 +42,14 @@ public ResponseEntity> getUser() { List questions = questionService.findByAuthorId(user.getId()); List answers = answerService.findByAnsweredBy(user.getId()); selfProfileResponseDto.setFollowedTags( - tagService.getFollowedTags(user.getId()) - ); + tagService.getFollowedTags(user.getId())); selfProfileResponseDto.setReputationPoints(userService.calculateReputation(user)); selfProfileResponseDto.setQuestionCount((long) questions.size()); selfProfileResponseDto.setQuestions( - questions); + questions); selfProfileResponseDto.setAnswerCount((long) answers.size()); selfProfileResponseDto.setAnswers( - answers); - + answers); GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse( selfProfileResponseDto.getClass(), @@ -84,10 +82,13 @@ public ResponseEntity> getUserById( UserProfileResponseDto.class); userProfileResponseDto.setReputationPoints(userService.calculateReputation(user.get())); userProfileResponseDto.setSelfFollowing(userService.selfFollowing(user.get())); - userProfileResponseDto.setFollowedTags( - tagService.getFollowedTags(user.get().getId()) - ); - + List questions = questionService.findByAuthorId(id); + userProfileResponseDto.setQuestions(questions); + userProfileResponseDto.setQuestionCount((long) questions.size()); + List answers = answerService.findByAnsweredBy(id); + userProfileResponseDto.setAnswers(answers); + userProfileResponseDto.setAnswerCount((long) answers.size()); + userProfileResponseDto.setFollowedTags(tagService.getFollowedTags(id)); GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse( userProfileResponseDto.getClass(), diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java b/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java index 15d12b1a..06fb45b7 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java @@ -1,4 +1,5 @@ package com.group1.programminglanguagesforum.DTOs.Responses; + import com.group1.programminglanguagesforum.Entities.ExperienceLevel; import lombok.*; @@ -25,5 +26,7 @@ public class UserProfileResponseDto { private ExperienceLevel experienceLevel; @Builder.Default private List followedTags = new ArrayList<>(); - + private Long questionCount; + private List questions; + private List answers; } From 6d146ac1b4c87094079392f70b70ce617c560976 Mon Sep 17 00:00:00 2001 From: CagatayColak Date: Tue, 10 Dec 2024 16:20:02 +0300 Subject: [PATCH 02/48] Revert "Add return questions, answers and followed tags to GET users/{userId}" This reverts commit 490bcd6380a5072768c3e666383c996f955c1ffc. --- .../Controllers/UserController.java | 19 +++++++++---------- .../Responses/UserProfileResponseDto.java | 5 +---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java index 1318129d..0abad964 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java @@ -42,14 +42,16 @@ public ResponseEntity> getUser() { List questions = questionService.findByAuthorId(user.getId()); List answers = answerService.findByAnsweredBy(user.getId()); selfProfileResponseDto.setFollowedTags( - tagService.getFollowedTags(user.getId())); + tagService.getFollowedTags(user.getId()) + ); selfProfileResponseDto.setReputationPoints(userService.calculateReputation(user)); selfProfileResponseDto.setQuestionCount((long) questions.size()); selfProfileResponseDto.setQuestions( - questions); + questions); selfProfileResponseDto.setAnswerCount((long) answers.size()); selfProfileResponseDto.setAnswers( - answers); + answers); + GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse( selfProfileResponseDto.getClass(), @@ -82,13 +84,10 @@ public ResponseEntity> getUserById( UserProfileResponseDto.class); userProfileResponseDto.setReputationPoints(userService.calculateReputation(user.get())); userProfileResponseDto.setSelfFollowing(userService.selfFollowing(user.get())); - List questions = questionService.findByAuthorId(id); - userProfileResponseDto.setQuestions(questions); - userProfileResponseDto.setQuestionCount((long) questions.size()); - List answers = answerService.findByAnsweredBy(id); - userProfileResponseDto.setAnswers(answers); - userProfileResponseDto.setAnswerCount((long) answers.size()); - userProfileResponseDto.setFollowedTags(tagService.getFollowedTags(id)); + userProfileResponseDto.setFollowedTags( + tagService.getFollowedTags(user.get().getId()) + ); + GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse( userProfileResponseDto.getClass(), diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java b/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java index 06fb45b7..15d12b1a 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java @@ -1,5 +1,4 @@ package com.group1.programminglanguagesforum.DTOs.Responses; - import com.group1.programminglanguagesforum.Entities.ExperienceLevel; import lombok.*; @@ -26,7 +25,5 @@ public class UserProfileResponseDto { private ExperienceLevel experienceLevel; @Builder.Default private List followedTags = new ArrayList<>(); - private Long questionCount; - private List questions; - private List answers; + } From be1a66e5065cc54394b0372a66d6a7424a3c15eb Mon Sep 17 00:00:00 2001 From: CagatayColak Date: Tue, 10 Dec 2024 16:21:37 +0300 Subject: [PATCH 03/48] Reapply "Add return questions, answers and followed tags to GET users/{userId}" This reverts commit 6d146ac1b4c87094079392f70b70ce617c560976. --- .../Controllers/UserController.java | 19 ++++++++++--------- .../Responses/UserProfileResponseDto.java | 5 ++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java index 0abad964..1318129d 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/UserController.java @@ -42,16 +42,14 @@ public ResponseEntity> getUser() { List questions = questionService.findByAuthorId(user.getId()); List answers = answerService.findByAnsweredBy(user.getId()); selfProfileResponseDto.setFollowedTags( - tagService.getFollowedTags(user.getId()) - ); + tagService.getFollowedTags(user.getId())); selfProfileResponseDto.setReputationPoints(userService.calculateReputation(user)); selfProfileResponseDto.setQuestionCount((long) questions.size()); selfProfileResponseDto.setQuestions( - questions); + questions); selfProfileResponseDto.setAnswerCount((long) answers.size()); selfProfileResponseDto.setAnswers( - answers); - + answers); GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse( selfProfileResponseDto.getClass(), @@ -84,10 +82,13 @@ public ResponseEntity> getUserById( UserProfileResponseDto.class); userProfileResponseDto.setReputationPoints(userService.calculateReputation(user.get())); userProfileResponseDto.setSelfFollowing(userService.selfFollowing(user.get())); - userProfileResponseDto.setFollowedTags( - tagService.getFollowedTags(user.get().getId()) - ); - + List questions = questionService.findByAuthorId(id); + userProfileResponseDto.setQuestions(questions); + userProfileResponseDto.setQuestionCount((long) questions.size()); + List answers = answerService.findByAnsweredBy(id); + userProfileResponseDto.setAnswers(answers); + userProfileResponseDto.setAnswerCount((long) answers.size()); + userProfileResponseDto.setFollowedTags(tagService.getFollowedTags(id)); GenericApiResponse response = ApiResponseBuilder.buildSuccessResponse( userProfileResponseDto.getClass(), diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java b/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java index 15d12b1a..06fb45b7 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/DTOs/Responses/UserProfileResponseDto.java @@ -1,4 +1,5 @@ package com.group1.programminglanguagesforum.DTOs.Responses; + import com.group1.programminglanguagesforum.Entities.ExperienceLevel; import lombok.*; @@ -25,5 +26,7 @@ public class UserProfileResponseDto { private ExperienceLevel experienceLevel; @Builder.Default private List followedTags = new ArrayList<>(); - + private Long questionCount; + private List questions; + private List answers; } From a48ac3cacc96400a792bb904d0954212ec77c69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Tue, 10 Dec 2024 22:11:14 +0300 Subject: [PATCH 04/48] refactor(mobile): rename FollowButton to FollowUserButton --- mobile/app/(tabs)/profile.tsx | 4 ++-- mobile/app/question/[questionId].tsx | 4 ++-- mobile/components/{FollowButton.tsx => FollowUserButton.tsx} | 2 +- mobile/components/Profile.tsx | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename mobile/components/{FollowButton.tsx => FollowUserButton.tsx} (97%) diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx index dbac5ea5..e78b3bc6 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -1,5 +1,5 @@ import ErrorAlert from "@/components/ErrorAlert"; -import FollowButton from "@/components/FollowButton"; +import FollowUserButton from "@/components/FollowUserButton"; import { FullscreenLoading } from "@/components/FullscreenLoading"; import { QuestionCard } from "@/components/QuestionCard"; import { @@ -255,7 +255,7 @@ export function UserProfile({ userId }: { userId: string }) { ) ) : ( - data?.data && + data?.data && )} {/* diff --git a/mobile/app/question/[questionId].tsx b/mobile/app/question/[questionId].tsx index 7eb83e46..cb9315c7 100644 --- a/mobile/app/question/[questionId].tsx +++ b/mobile/app/question/[questionId].tsx @@ -1,7 +1,7 @@ import { Answers } from "@/components/Answers"; import ErrorAlert from "@/components/ErrorAlert"; -import FollowButton from "@/components/FollowButton"; +import FollowUserButton from "@/components/FollowUserButton"; import { FullscreenLoading } from "@/components/FullscreenLoading"; import { Button, @@ -259,7 +259,7 @@ export default function QuestionPage() { {token && selfProfile?.id !== question.author.id && ( - + )} diff --git a/mobile/components/FollowButton.tsx b/mobile/components/FollowUserButton.tsx similarity index 97% rename from mobile/components/FollowButton.tsx rename to mobile/components/FollowUserButton.tsx index b154937b..c675c2ef 100644 --- a/mobile/components/FollowButton.tsx +++ b/mobile/components/FollowUserButton.tsx @@ -6,7 +6,7 @@ import { import { useState } from "react"; import { Button, ButtonText } from "./ui/button"; -export default function FollowButton({ +export default function FollowUserButton({ profile, }: { profile: { id?: number; selfFollowing?: boolean }; diff --git a/mobile/components/Profile.tsx b/mobile/components/Profile.tsx index f1145cbc..9a5e0c79 100644 --- a/mobile/components/Profile.tsx +++ b/mobile/components/Profile.tsx @@ -1,7 +1,7 @@ import { UserSummary } from "@/services/api/programmingForumSchemas"; import useAuthStore from "@/services/auth"; import { Link } from "expo-router"; -import FollowButton from "./FollowButton"; +import FollowUserButton from "./FollowUserButton"; import { Text, View } from "./ui"; interface ProfileProps { @@ -18,7 +18,7 @@ export const Profile = ({ profile }: ProfileProps) => { {profile.username} - {profile.id !== selfProfile?.id && } + {profile.id !== selfProfile?.id && } ); }; From 574086780c86297e1ee6739bb63b84a962227e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Tue, 10 Dec 2024 22:18:54 +0300 Subject: [PATCH 05/48] fix(api): change tagId type from string to number --- .../services/api/programmingForumComponents.ts | 6 +++--- mobile/services/api/programmingForumSchemas.ts | 12 ++++++------ swagger/openapi.yml | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/mobile/services/api/programmingForumComponents.ts b/mobile/services/api/programmingForumComponents.ts index 63141bb8..64ed72cd 100644 --- a/mobile/services/api/programmingForumComponents.ts +++ b/mobile/services/api/programmingForumComponents.ts @@ -1787,7 +1787,7 @@ export const useCreateTag = ( }; export type GetTagDetailsPathParams = { - tagId: string; + tagId: number; }; export type GetTagDetailsError = Fetcher.ErrorWrapper<{ @@ -1850,7 +1850,7 @@ export const useGetTagDetails = ( }; export type FollowTagPathParams = { - tagId: string; + tagId: number; }; export type FollowTagError = Fetcher.ErrorWrapper< @@ -1900,7 +1900,7 @@ export const useFollowTag = ( }; export type UnfollowTagPathParams = { - tagId: string; + tagId: number; }; export type UnfollowTagError = Fetcher.ErrorWrapper< diff --git a/mobile/services/api/programmingForumSchemas.ts b/mobile/services/api/programmingForumSchemas.ts index 8c73d7f9..50f2bb46 100644 --- a/mobile/services/api/programmingForumSchemas.ts +++ b/mobile/services/api/programmingForumSchemas.ts @@ -212,13 +212,13 @@ export type AnswerDetails = { }; /** - * @example {"tagId":"python","name":"Python","description":"Python is a programming language.","questionCount":100,"followersCount":1000,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png","authors":["Guido van Rossum"],"inceptionYear":"1991","fileExtension":".py","officialWebsite":"https://www.python.org","stackExchangeTag":"python"} - * @example {"tagId":"java","name":"Java","type":"PROGRAMMING_LANGUAGE","description":"Java is a class-based, object-oriented programming language.","questionCount":200,"followersCount":800,"following":true,"photo":"https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/182px-Java_programming_language_logo.svg.png","authors":["James Gosling"],"inceptionYear":"1995","fileExtension":".java","officialWebsite":"https://www.java.com","stackExchangeTag":"java"} - * @example {"tagId":"react","name":"React","type":"SOFTWARE_LIBRARY","description":"React is a JavaScript library for building user interfaces.","questionCount":150,"followersCount":600,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/180px-React-icon.svg.png","officialWebsite":"https://reactjs.org","stackExchangeTag":"reactjs"} - * @example {"tagId":"oop","name":"Object-Oriented Programming","type":"PROGRAMMING_PARADIGM","description":"OOP is a programming paradigm based on objects containing data and code.","questionCount":80,"followersCount":400,"following":true,"photo":"https://example.com/oop-icon.png","stackExchangeTag":"oop"} + * @example {"tagId":1,"name":"Python","description":"Python is a programming language.","questionCount":100,"followersCount":1000,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png","authors":["Guido van Rossum"],"inceptionYear":"1991","fileExtension":".py","officialWebsite":"https://www.python.org","stackExchangeTag":"python"} + * @example {"tagId":2,"name":"Java","type":"PROGRAMMING_LANGUAGE","description":"Java is a class-based, object-oriented programming language.","questionCount":200,"followersCount":800,"following":true,"photo":"https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/182px-Java_programming_language_logo.svg.png","authors":["James Gosling"],"inceptionYear":"1995","fileExtension":".java","officialWebsite":"https://www.java.com","stackExchangeTag":"java"} + * @example {"tagId":3,"name":"React","type":"SOFTWARE_LIBRARY","description":"React is a JavaScript library for building user interfaces.","questionCount":150,"followersCount":600,"following":false,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/180px-React-icon.svg.png","officialWebsite":"https://reactjs.org","stackExchangeTag":"reactjs"} + * @example {"tagId":4,"name":"Object-Oriented Programming","type":"PROGRAMMING_PARADIGM","description":"OOP is a programming paradigm based on objects containing data and code.","questionCount":80,"followersCount":400,"following":true,"photo":"https://example.com/oop-icon.png","stackExchangeTag":"oop"} */ export type TagDetails = { - tagId: string; + tagId: number; name: string; tagType?: TagType; description: string; @@ -258,7 +258,7 @@ export type TagDetails = { }; /** - * @example {"tagId":"python","name":"Python","questionCount":100,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png"} + * @example {"tagId":1,"name":"Python","questionCount":100,"photo":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png"} */ export type TagSummary = { id?: string; diff --git a/swagger/openapi.yml b/swagger/openapi.yml index f619eefd..467787f1 100644 --- a/swagger/openapi.yml +++ b/swagger/openapi.yml @@ -844,7 +844,7 @@ paths: in: path required: true schema: - type: string + type: integer responses: "200": description: Successful response @@ -873,7 +873,7 @@ paths: in: path required: true schema: - type: string + type: integer responses: "200": description: Successfully followed the tag @@ -894,7 +894,7 @@ paths: in: path required: true schema: - type: string + type: integer responses: "200": description: Successfully unfollowed the tag @@ -1619,7 +1619,7 @@ components: - officialWebsite properties: tagId: - type: string + type: integer name: type: string tagType: @@ -1662,7 +1662,7 @@ components: items: $ref: "#/components/schemas/QuestionSummary" examples: - - tagId: "python" + - tagId: 1 name: "Python" description: "Python is a programming language." questionCount: 100 @@ -1675,7 +1675,7 @@ components: fileExtension: ".py" officialWebsite: "https://www.python.org" stackExchangeTag: "python" - - tagId: "java" + - tagId: 2 name: "Java" type: "PROGRAMMING_LANGUAGE" description: "Java is a class-based, object-oriented programming language." @@ -1689,7 +1689,7 @@ components: fileExtension: ".java" officialWebsite: "https://www.java.com" stackExchangeTag: "java" - - tagId: "react" + - tagId: 3 name: "React" type: "SOFTWARE_LIBRARY" description: "React is a JavaScript library for building user interfaces." @@ -1699,7 +1699,7 @@ components: photo: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/180px-React-icon.svg.png" officialWebsite: "https://reactjs.org" stackExchangeTag: "reactjs" - - tagId: "oop" + - tagId: 4 name: "Object-Oriented Programming" type: "PROGRAMMING_PARADIGM" description: "OOP is a programming paradigm based on objects containing data and code." @@ -1724,7 +1724,7 @@ components: type: string format: url examples: - - tagId: "python" + - tagId: 1 name: "Python" questionCount: 100 photo: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png" From c0ee4342b7d6cc70a0add9c53a8c4e35a3c82adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Tue, 10 Dec 2024 23:55:36 +0300 Subject: [PATCH 06/48] feat(mobile): add follow tag feature --- mobile/app/tags/[tagId].tsx | 39 +++++++---- mobile/components/FollowTagButton.tsx | 94 +++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 mobile/components/FollowTagButton.tsx diff --git a/mobile/app/tags/[tagId].tsx b/mobile/app/tags/[tagId].tsx index a81a264a..647b9fd7 100644 --- a/mobile/app/tags/[tagId].tsx +++ b/mobile/app/tags/[tagId].tsx @@ -1,4 +1,5 @@ import ErrorAlert from "@/components/ErrorAlert"; +import FollowTagButton from "@/components/FollowTagButton"; import { FullscreenLoading } from "@/components/FullscreenLoading"; import { QuestionList } from "@/components/QuestionsList"; import { @@ -30,13 +31,14 @@ import useAuthStore from "@/services/auth"; import { Link, router, useLocalSearchParams } from "expo-router"; import { ArrowLeftIcon, ChevronDownIcon, Plus } from "lucide-react-native"; import { useState } from "react"; +import { SvgUri } from "react-native-svg"; export default function TagPage() { const { tagId } = useLocalSearchParams<{ tagId: string }>(); const { data, isLoading, error } = useGetTagDetails( { - pathParams: { tagId: tagId! }, + pathParams: { tagId: Number(tagId) }, }, { enabled: !!tagId, @@ -68,11 +70,11 @@ export default function TagPage() { } return ( - + + + {tag?.logoImage && tag.logoImage.endsWith(".svg") && ( + + )} + + {tag?.logoImage && tag.logoImage.endsWith(".png") && ( + + )} + + {tag.name} + - {tag.name} {tag.description} @@ -155,7 +166,7 @@ export default function TagPage() { diff --git a/mobile/components/FollowTagButton.tsx b/mobile/components/FollowTagButton.tsx new file mode 100644 index 00000000..e97451fb --- /dev/null +++ b/mobile/components/FollowTagButton.tsx @@ -0,0 +1,94 @@ +import { + useFollowTag, + useGetTagDetails, + useUnfollowTag, +} from "@/services/api/programmingForumComponents"; +import { useEffect, useState } from "react"; +import { Button, ButtonText } from "./ui/button"; + +export default function FollowTagButton({ + tag, +}: { + tag: { id?: number; selfFollowing?: boolean }; +}) { + const { isLoading, data, error, refetch } = useGetTagDetails( + { + pathParams: { + tagId: tag.id!, + }, + }, + ); + + + const [optimisticFollowing, setOptimisticFollowing] = useState( + false + ); + + useEffect(() => { + setOptimisticFollowing(tag.selfFollowing ?? false); + }, [tag.selfFollowing]); + + const { mutateAsync: follow } = useFollowTag({ + onMutate: async () => { + setOptimisticFollowing(true); + }, + onSuccess: (data) => { + refetch().then(() => { + setOptimisticFollowing(true); + }); + console.log(data); + }, + onError: (error) => { + console.error(error); + setOptimisticFollowing(false); + }, + }); + + const { mutateAsync: unfollow } = useUnfollowTag({ + onMutate: async () => { + setOptimisticFollowing(false); + }, + onSuccess: () => { + refetch().then(() => { + setOptimisticFollowing(false); + }); + }, + onError: () => { + setOptimisticFollowing(true); + }, + }); + + const following = optimisticFollowing ?? data?.data?.following; + + return ( + + ); +} From 4742d696181ea230b37c8d03e0048e1dd2c18aee Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Thu, 12 Dec 2024 22:26:53 +0300 Subject: [PATCH 07/48] Add reputation points to profile page --- frontend/src/routes/profile.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index 13ef94f6..7e836162 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -167,6 +167,15 @@ export default function Profile() { ) )} + {profile.reputationPoints && ( +
+
+ + Reputation Points: {profile.reputationPoints} + +
+
+ )} {profile.followedTags && (
From ed029b14b92fdb6b1b92b0701e6691c3382b7073 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sat, 14 Dec 2024 11:18:46 +0300 Subject: [PATCH 08/48] Implement bookmarked page --- frontend/src/routes/bookmarked.tsx | 106 +++++++++++++++++++++++++++++ frontend/src/routes/index.tsx | 5 ++ 2 files changed, 111 insertions(+) create mode 100644 frontend/src/routes/bookmarked.tsx diff --git a/frontend/src/routes/bookmarked.tsx b/frontend/src/routes/bookmarked.tsx new file mode 100644 index 00000000..00be0cb4 --- /dev/null +++ b/frontend/src/routes/bookmarked.tsx @@ -0,0 +1,106 @@ +import { DifficultyFilter } from "@/components/DifficultyFilter"; +import { useGetBookmarkedQuestions } from "@/services/api/programmingForumComponents"; +import { + DifficultyLevel, + QuestionSummary, +} from "@/services/api/programmingForumSchemas"; +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import ErrorAlert from "../components/ErrorAlert"; +import InfiniteScroll from "../components/InfiniteScroll"; +import { QuestionCard } from "../components/QuestionCard"; + +export const BookmarkedQuestions = () => { + const [params] = useSearchParams(); + const [pageSize, setPageSize] = useState(20); + const [difficulty, setDifficulty] = useState(); + const [previousData, setPreviousData] = useState<{ + items: QuestionSummary[]; + totalItems: number; + }>({ + items: [], + totalItems: 0, + }); + + const { + data: resultList, + isLoading, + error, + } = useGetBookmarkedQuestions({}); + + useEffect(() => { + if (resultList?.data && !isLoading) { + setPreviousData(resultList.data as typeof previousData); + } + }, [resultList, isLoading]); + + if (error) { + return ; + } + + const resultListData = + (resultList?.data as typeof previousData) || previousData; + const questions = resultListData.items || []; + + const next = () => { + setPageSize(pageSize + 20); + }; + + return ( +
+
+

+ {questions.length + ? `Last ${resultListData.totalItems} bookmarked questions shown.` + : "No questions are bookmarked."} +

+
+ {!questions.length && ( +

Bookmark questions to view them here.

+ )} + +
+
+ pageSize + : false + } + isLoading={isLoading} + > + {questions.map((question) => ( + + ))} + +
+ {isLoading && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index fd77946f..8a869ef0 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -12,6 +12,7 @@ import QuestionRoute from "./question"; import { Search } from "./search"; import Signup from "./signup"; import TagPage from "./tag"; +import { BookmarkedQuestions } from "@/routes/bookmarked"; export const routes: RouteObject[] = [ { @@ -26,6 +27,10 @@ export const routes: RouteObject[] = [ path: "/login", Component: Login, }, + { + path: "/bookmarkedquestions", + Component: BookmarkedQuestions, + }, { path: "/logout", async action() { From 1ee28c5eb5897ffe2cf13b6fcd6256b24af4487d Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sat, 14 Dec 2024 11:48:42 +0300 Subject: [PATCH 09/48] Remove unused vars --- frontend/src/routes/bookmarked.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/routes/bookmarked.tsx b/frontend/src/routes/bookmarked.tsx index 00be0cb4..5d69c407 100644 --- a/frontend/src/routes/bookmarked.tsx +++ b/frontend/src/routes/bookmarked.tsx @@ -1,20 +1,15 @@ -import { DifficultyFilter } from "@/components/DifficultyFilter"; import { useGetBookmarkedQuestions } from "@/services/api/programmingForumComponents"; import { - DifficultyLevel, QuestionSummary, } from "@/services/api/programmingForumSchemas"; import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; import ErrorAlert from "../components/ErrorAlert"; import InfiniteScroll from "../components/InfiniteScroll"; import { QuestionCard } from "../components/QuestionCard"; export const BookmarkedQuestions = () => { - const [params] = useSearchParams(); const [pageSize, setPageSize] = useState(20); - const [difficulty, setDifficulty] = useState(); const [previousData, setPreviousData] = useState<{ items: QuestionSummary[]; totalItems: number; From 0bf9326c48d9da3862add190f3903b405e49b647 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sat, 14 Dec 2024 14:07:32 +0300 Subject: [PATCH 10/48] Add bookmarked page button to profile --- frontend/src/routes/profile.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index 13ef94f6..1d181414 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -152,9 +152,14 @@ export default function Profile() { {isPending ? "Saving..." : "Save"} ) : ( - +
+ + +
) ) : ( data?.data && ( From c89f78acbc17facc48c71cc10aace0fc07ff43a2 Mon Sep 17 00:00:00 2001 From: Nazire Date: Sat, 14 Dec 2024 17:15:30 +0300 Subject: [PATCH 11/48] Added default profile picture. --- frontend/src/assets/placeholder_profile.png | Bin 0 -> 2011 bytes frontend/src/components/AnswerCard.tsx | 4 +++- frontend/src/components/AnswerItem.tsx | 4 ++-- .../src/components/HighlightedQuestionCard.tsx | 3 ++- frontend/src/components/QuestionCard.tsx | 5 ++++- frontend/src/routes/profile.tsx | 2 +- frontend/src/routes/question.test.tsx | 3 ++- frontend/src/routes/question.tsx | 4 ++-- .../src/services/api/programmingForumSchemas.ts | 2 +- frontend/src/services/temporaryMocks.ts | 6 +++--- mobile/app/(tabs)/profile.tsx | 2 +- mobile/app/question/[questionId].tsx | 5 +++-- mobile/components/AnswerCard.tsx | 3 ++- mobile/components/AnswerItem.tsx | 4 ++-- mobile/components/QuestionCard.tsx | 3 ++- mobile/services/api/programmingForumSchemas.ts | 2 +- mobile/services/temporaryMocks.ts | 6 +++--- swagger/openapi.yml | 2 +- 18 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 frontend/src/assets/placeholder_profile.png diff --git a/frontend/src/assets/placeholder_profile.png b/frontend/src/assets/placeholder_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..09892098aa943af5fe95e7dd434a07fe03e7ccd1 GIT binary patch literal 2011 zcmV<12PF83P)D!y%g4vZ*x1<2%*?Q`u)x5;+S=OH)zz}HvdGBD zw6wIs!oteR%FWHqrlzLJ$;r^r(7(UGt*x!9s;a}o!>FjJ#>U3Iy}i%R&$YF+x3{;h zuCCtR-nO>3udlDMv9Z$9(zv*|ySuyA*4EqG+ZlWNivR!y*-1n}RCr$O+*gj>NDPM2 zpNco<96RTn9rwSi8F&oCZFlz#Qq{xgJ4+x4LZrx&UjP6A00000000000000000000 z0000000000h)13Iv~gIg9X6)(&Ma2$kko6>k^LchuGJIe1?9VT%ih&$-<6da;(GtY zRygU`V`YKSGPJdX%aOK)>O)}bd#Gv~*uA$6-S28s*y-CA`a9YL;;WKvrF0c*ZKyul z)*h=`7wH^W2Elqsjqd}?Xz*Ptp%PkVVMQxo5?Bs`i9Upe<)onxU}?Eo>Ro7Cj@nv* ze0H1D=vvOYdIRQ`ySe^?YRPg}s_G9&F3o|+E;KJf72K|Qva4ddGB4O%8ki>o6?HI`i*6#+w#5|Y?<y|LwN+|wJ-DOs*c9lZ(lz;YDS^$r~880p|x z??Sy~IVsik0el`?F2Y>=KFtcZ1R zaGELyc&axa?H!NJ`jhg5v~%-y@6vvAx%c&^lgbGsy=v!Q)n20Y000000000000000 z0000001(G=0_9$3(l{U9f4Ydqf&CDSql-`X!}G?Z(<>_@q?M!EmqlnRgo`h=qe`mH zp^F!L#dKMP=0-j66q)X!m2_UKU< z?=gcl`PrjD-Z_UQ!Oo*l-Y$R@ac4on7M&O-j9L_~HG+QG8x@#l(9NwQst~Om6uTU# z#^D0B?jkA@-9e`x4^-yi2NWuOs^gbXXX{ZRZwp;qmr8Y=vc4+;?DuSiV=6Wdp+vB! zYWn~xgnKHt51~MOqTGjPD@PoaW3S(TV1qAdpLGPU;{}n+CNZ&z1&l zAk)yBhI<>K$Szko#25&!NK0f~qZEphQ8{ z3aIf)#a^Mvnrf|~$}^REhB95M)P*|TX3iCPq&kn#$aR|+5*6uFl|Ix;s7M0Ewp3#a z)kYpw@J3LsOsPOxLcOd<@p>5y2p=fiLkJUGpQ800tayqjR`dizEyTFw96Gb|` z!YX_?aV~Q~zz%VD}8+~-=U|TkA>Od+oZ}G}nbJRjS zi+DxREJj@nPrRDb5Owl07n$q)ia(q8)f$NXg*KhY?6EB-U9U5WqIxO65S z|C%@HT<{BxaK1b6FYI^o5WlAp7RhA6Kem`8MTkGt$mX+;7JpOgV>ZtqfL8IK*!Dm6 zzQ>>P{QXCNTMXj=+Nnb$0D%|)f;hy>mx(1hlm3f?BA8dZ4V=5bdfd6DDtbSk-BC3w tPksUb00000000000000000000fEW70#Mag`cL)Fg002ovPDHLkV1gNB=am2e literal 0 HcmV?d00001 diff --git a/frontend/src/components/AnswerCard.tsx b/frontend/src/components/AnswerCard.tsx index 8de70cc8..2d70f055 100644 --- a/frontend/src/components/AnswerCard.tsx +++ b/frontend/src/components/AnswerCard.tsx @@ -2,6 +2,7 @@ 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; @@ -48,7 +49,8 @@ export const AnswerCard: React.FC = ({
{"Profile diff --git a/frontend/src/components/AnswerItem.tsx b/frontend/src/components/AnswerItem.tsx index 6e519d0d..f750d95d 100644 --- a/frontend/src/components/AnswerItem.tsx +++ b/frontend/src/components/AnswerItem.tsx @@ -6,6 +6,7 @@ import { ThumbsDown, ThumbsUp } 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; @@ -61,8 +62,7 @@ export const AnswerItem: React.FC = ({ > {"Profile> = ({ {author && ( {author.name} diff --git a/frontend/src/components/QuestionCard.tsx b/frontend/src/components/QuestionCard.tsx index de7e8b79..1fd2ea4e 100644 --- a/frontend/src/components/QuestionCard.tsx +++ b/frontend/src/components/QuestionCard.tsx @@ -4,6 +4,8 @@ import { DifficultyLevel } from "@/services/api/programmingForumSchemas"; import { ArrowRight, MessageSquare, Star, StarsIcon } from "lucide-react"; import React from "react"; import { Link } from "react-router-dom"; +import placeholderProfile from "@/assets/placeholder_profile.png"; + interface QuestionCardProps { id: number; @@ -57,7 +59,8 @@ export const QuestionCard = React.forwardRef( {author && ( {author.name} diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index 14504739..d7bb22d9 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -80,7 +80,7 @@ export default function Profile() {
diff --git a/frontend/src/routes/question.test.tsx b/frontend/src/routes/question.test.tsx index f35209c7..e35bf136 100644 --- a/frontend/src/routes/question.test.tsx +++ b/frontend/src/routes/question.test.tsx @@ -14,6 +14,7 @@ import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { routeConfig } from "."; import QuestionPage from "./question"; import { DifficultyBar } from "@/components/DifficultyBar"; +import placeholderProfile from "@/assets/placeholder_profile.png"; @@ -29,7 +30,7 @@ const mockQuestionData = vi.hoisted( username: "johndoe", name: "John Doe", reputationPoints: 100, - profilePicture: "https://example.com/profile.jpg", + profilePicture: placeholderProfile, }, likeCount: 10, commentCount: 5, diff --git a/frontend/src/routes/question.tsx b/frontend/src/routes/question.tsx index 554585fb..888ea944 100644 --- a/frontend/src/routes/question.tsx +++ b/frontend/src/routes/question.tsx @@ -12,6 +12,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { toast } from "@/components/ui/use-toast"; import { TagDetails } from "@/services/api/programmingForumSchemas"; +import placeholderProfile from "@/assets/placeholder_profile.png"; import { useDeleteQuestion as useDeleteQuestionById, @@ -225,8 +226,7 @@ export default function QuestionPage() { className="flex items-center gap-4" > {"Profile {`Profile = ({ {author.name} diff --git a/mobile/components/AnswerItem.tsx b/mobile/components/AnswerItem.tsx index 004b8edb..fd3c697b 100644 --- a/mobile/components/AnswerItem.tsx +++ b/mobile/components/AnswerItem.tsx @@ -8,6 +8,7 @@ import { ThumbsDown, ThumbsUp } from "lucide-react-native"; import React from "react"; import { View } from "react-native"; import { ContentWithSnippets } from "./ContentWithSnippets"; +import placeholderProfile from "@/assets/placeholder_profile.png"; interface AnswerItemProps { answer: AnswerDetails; @@ -71,8 +72,7 @@ export const AnswerItem: React.FC = ({ {answer.author?.name} = ({ {author.name Date: Sat, 14 Dec 2024 17:39:58 +0300 Subject: [PATCH 12/48] fixed test --- frontend/src/routes/question.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/routes/question.test.tsx b/frontend/src/routes/question.test.tsx index e35bf136..f35209c7 100644 --- a/frontend/src/routes/question.test.tsx +++ b/frontend/src/routes/question.test.tsx @@ -14,7 +14,6 @@ import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { routeConfig } from "."; import QuestionPage from "./question"; import { DifficultyBar } from "@/components/DifficultyBar"; -import placeholderProfile from "@/assets/placeholder_profile.png"; @@ -30,7 +29,7 @@ const mockQuestionData = vi.hoisted( username: "johndoe", name: "John Doe", reputationPoints: 100, - profilePicture: placeholderProfile, + profilePicture: "https://example.com/profile.jpg", }, likeCount: 10, commentCount: 5, From 2e044d72727fd2b429c12dfe727e8af28999cc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Sat, 14 Dec 2024 18:05:21 +0300 Subject: [PATCH 13/48] fix(backend): return follower count correctly in tags --- .../programminglanguagesforum/Services/TagService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java b/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java index 3647c828..cfb4c18c 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/TagService.java @@ -101,6 +101,8 @@ public GetTagDetailsResponseDto getTagDetails(Long tagId) { following = false; } + Long followerCount = (long) tagEntity.getFollowers().size(); + if (tagType == TagType.PROGRAMMING_LANGUAGE) { ProgrammingLanguagesTag languageTag = (ProgrammingLanguagesTag) tagEntity; GetProgrammingLanguageTagResponseDto responseDto = modelMapper.map(languageTag, @@ -108,6 +110,7 @@ public GetTagDetailsResponseDto getTagDetails(Long tagId) { responseDto.setTagType(tagType.toString()); responseDto.setRelatedQuestions(relatedQuestions); responseDto.setQuestionCount(questionCount); + responseDto.setFollowerCount(followerCount); responseDto.setFollowing(following); return responseDto; } else if (tagType == TagType.PROGRAMMING_PARADIGM) { @@ -117,6 +120,7 @@ public GetTagDetailsResponseDto getTagDetails(Long tagId) { responseDto.setTagType(tagType.toString()); responseDto.setRelatedQuestions(relatedQuestions); responseDto.setQuestionCount(questionCount); + responseDto.setFollowerCount(followerCount); responseDto.setFollowing(following); return responseDto; } @@ -128,6 +132,7 @@ public GetTagDetailsResponseDto getTagDetails(Long tagId) { .tagType(getTagType(tagEntity).toString()) .relatedQuestions(relatedQuestions) .questionCount(questionCount) + .followerCount(followerCount) .following(following) .build(); @@ -149,6 +154,7 @@ public Page searchTags(String q, Pageable pageable) { return tags.map(tag -> GetTagDetailsResponseDto.builder() .tagId(tag.getId()) .questionCount((long) questionRepository.findQuestionsByTagId(tag.getId()).size()) + .followerCount((long) tag.getFollowers().size()) .name(tag.getTagName()) .description(tag.getTagDescription()) .tagType(getTagType(tag).toString()) From c8c217925daf1889aa6840dffb5c894bd9c8e3ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Sat, 14 Dec 2024 18:13:32 +0300 Subject: [PATCH 14/48] fix backend tests --- .../programminglanguagesforum/Services/TagServiceTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java b/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java index 4b8fbf91..7a6a6b1f 100644 --- a/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java +++ b/backend/src/test/java/com/group1/programminglanguagesforum/Services/TagServiceTests.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; @@ -111,7 +112,7 @@ void testGetTagDetails_Success() { .reputationPoints(0L) .build(); - Tag mockTag = new Tag(1L, null, "Tag1", "Description1", null); + Tag mockTag = new Tag(1L, null, "Tag1", "Description1", new HashSet<>()); Question q1 = Question.builder() .id(1L) From 68825360390d00906311a0b561475a67f897d8ea Mon Sep 17 00:00:00 2001 From: Nazire Date: Sat, 14 Dec 2024 18:14:05 +0300 Subject: [PATCH 15/48] fixed based on review --- frontend/src/routes/profile.tsx | 3 ++- frontend/src/routes/question.test.tsx | 1 - mobile/app/(tabs)/profile.tsx | 5 ++--- mobile/app/question/[questionId].tsx | 9 ++++----- mobile/assets/images/placeholder_profile.png | Bin 0 -> 2011 bytes mobile/components/AnswerCard.tsx | 4 ++-- mobile/components/AnswerItem.tsx | 7 ++----- mobile/components/QuestionCard.tsx | 6 ++---- mobile/services/api/programmingForumSchemas.ts | 2 +- mobile/services/temporaryMocks.ts | 6 +++--- swagger/openapi.yml | 2 +- 11 files changed, 19 insertions(+), 26 deletions(-) create mode 100644 mobile/assets/images/placeholder_profile.png diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index d7bb22d9..1eb53a9f 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -19,6 +19,7 @@ import { Link, useParams } from "react-router-dom"; import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; +import placeholderProfile from "@/assets/placeholder_profile.png"; export default function Profile() { const { userId = "" } = useParams<{ userId: string }>(); @@ -80,7 +81,7 @@ export default function Profile() {
diff --git a/frontend/src/routes/question.test.tsx b/frontend/src/routes/question.test.tsx index f35209c7..f07d8fc8 100644 --- a/frontend/src/routes/question.test.tsx +++ b/frontend/src/routes/question.test.tsx @@ -17,7 +17,6 @@ import { DifficultyBar } from "@/components/DifficultyBar"; - const mockQuestionData = vi.hoisted( () => ({ diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx index 3e472ebe..bdbef758 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -37,6 +37,7 @@ import useAuthStore from "@/services/auth"; import { Link, router } from "expo-router"; import { ChevronDownIcon, Plus, Bookmark, MenuIcon, LogOutIcon } from "lucide-react-native"; import { useEffect, useState } from "react"; +import placeholderProfile from "@/assets/images/placeholder_profile.png"; export default function Profile() { return ; @@ -141,9 +142,7 @@ export function UserProfile({ userId }: { userId: string }) { {`Profile diff --git a/mobile/app/question/[questionId].tsx b/mobile/app/question/[questionId].tsx index 6ac3d6ea..4bc103b6 100644 --- a/mobile/app/question/[questionId].tsx +++ b/mobile/app/question/[questionId].tsx @@ -37,7 +37,7 @@ import { } from "lucide-react-native"; import { useEffect, useState } from "react"; import { ScrollView, View } from "react-native"; -import placeholderProfile from "@/assets/placeholder_profile.png"; +import placeholderProfile from "@/assets/images/placeholder_profile.png"; export default function QuestionPage() { const { questionId } = useLocalSearchParams(); @@ -259,10 +259,9 @@ export default function QuestionPage() { className="flex flex-row items-center gap-4" > {question.author.name}D!y%g4vZ*x1<2%*?Q`u)x5;+S=OH)zz}HvdGBD zw6wIs!oteR%FWHqrlzLJ$;r^r(7(UGt*x!9s;a}o!>FjJ#>U3Iy}i%R&$YF+x3{;h zuCCtR-nO>3udlDMv9Z$9(zv*|ySuyA*4EqG+ZlWNivR!y*-1n}RCr$O+*gj>NDPM2 zpNco<96RTn9rwSi8F&oCZFlz#Qq{xgJ4+x4LZrx&UjP6A00000000000000000000 z0000000000h)13Iv~gIg9X6)(&Ma2$kko6>k^LchuGJIe1?9VT%ih&$-<6da;(GtY zRygU`V`YKSGPJdX%aOK)>O)}bd#Gv~*uA$6-S28s*y-CA`a9YL;;WKvrF0c*ZKyul z)*h=`7wH^W2Elqsjqd}?Xz*Ptp%PkVVMQxo5?Bs`i9Upe<)onxU}?Eo>Ro7Cj@nv* ze0H1D=vvOYdIRQ`ySe^?YRPg}s_G9&F3o|+E;KJf72K|Qva4ddGB4O%8ki>o6?HI`i*6#+w#5|Y?<y|LwN+|wJ-DOs*c9lZ(lz;YDS^$r~880p|x z??Sy~IVsik0el`?F2Y>=KFtcZ1R zaGELyc&axa?H!NJ`jhg5v~%-y@6vvAx%c&^lgbGsy=v!Q)n20Y000000000000000 z0000001(G=0_9$3(l{U9f4Ydqf&CDSql-`X!}G?Z(<>_@q?M!EmqlnRgo`h=qe`mH zp^F!L#dKMP=0-j66q)X!m2_UKU< z?=gcl`PrjD-Z_UQ!Oo*l-Y$R@ac4on7M&O-j9L_~HG+QG8x@#l(9NwQst~Om6uTU# z#^D0B?jkA@-9e`x4^-yi2NWuOs^gbXXX{ZRZwp;qmr8Y=vc4+;?DuSiV=6Wdp+vB! zYWn~xgnKHt51~MOqTGjPD@PoaW3S(TV1qAdpLGPU;{}n+CNZ&z1&l zAk)yBhI<>K$Szko#25&!NK0f~qZEphQ8{ z3aIf)#a^Mvnrf|~$}^REhB95M)P*|TX3iCPq&kn#$aR|+5*6uFl|Ix;s7M0Ewp3#a z)kYpw@J3LsOsPOxLcOd<@p>5y2p=fiLkJUGpQ800tayqjR`dizEyTFw96Gb|` z!YX_?aV~Q~zz%VD}8+~-=U|TkA>Od+oZ}G}nbJRjS zi+DxREJj@nPrRDb5Owl07n$q)ia(q8)f$NXg*KhY?6EB-U9U5WqIxO65S z|C%@HT<{BxaK1b6FYI^o5WlAp7RhA6Kem`8MTkGt$mX+;7JpOgV>ZtqfL8IK*!Dm6 zzQ>>P{QXCNTMXj=+Nnb$0D%|)f;hy>mx(1hlm3f?BA8dZ4V=5bdfd6DDtbSk-BC3w tPksUb00000000000000000000fEW70#Mag`cL)Fg002ovPDHLkV1gNB=am2e literal 0 HcmV?d00001 diff --git a/mobile/components/AnswerCard.tsx b/mobile/components/AnswerCard.tsx index effa2550..55f44f10 100644 --- a/mobile/components/AnswerCard.tsx +++ b/mobile/components/AnswerCard.tsx @@ -5,7 +5,7 @@ import { ArrowRight, CornerDownRight, Star } from "lucide-react-native"; import React from "react"; import { View } from "react-native"; import { ContentWithSnippets } from "./ContentWithSnippets"; -import placeholderProfile from "@/assets/placeholder_profile.png"; +import placeholderProfile from "@/assets/images/placeholder_profile.png"; interface AnswerCardProps { id: string; @@ -47,7 +47,7 @@ export const AnswerCard: React.FC = ({ {author.name} diff --git a/mobile/components/AnswerItem.tsx b/mobile/components/AnswerItem.tsx index fd3c697b..3adf4e91 100644 --- a/mobile/components/AnswerItem.tsx +++ b/mobile/components/AnswerItem.tsx @@ -8,7 +8,7 @@ import { ThumbsDown, ThumbsUp } from "lucide-react-native"; import React from "react"; import { View } from "react-native"; import { ContentWithSnippets } from "./ContentWithSnippets"; -import placeholderProfile from "@/assets/placeholder_profile.png"; +import placeholderProfile from "@/assets/images/placeholder_profile.png"; interface AnswerItemProps { answer: AnswerDetails; @@ -70,10 +70,7 @@ export const AnswerItem: React.FC = ({ className="flex items-center gap-2" > {answer.author?.name} diff --git a/mobile/components/QuestionCard.tsx b/mobile/components/QuestionCard.tsx index bff6e427..c46b20e9 100644 --- a/mobile/components/QuestionCard.tsx +++ b/mobile/components/QuestionCard.tsx @@ -3,7 +3,7 @@ import { DifficultyLevel } from "@/services/api/programmingForumSchemas"; import { Link } from "expo-router"; import { ArrowRight, MessageSquare, Star, StarsIcon } from "lucide-react-native"; import React from "react"; -import placeholderProfile from "@/assets/placeholder_profile.png"; +import placeholderProfile from "@/assets/images/placeholder_profile.png"; interface QuestionCardProps { id: string; @@ -63,9 +63,7 @@ export const QuestionCard: React.FC = ({ {author.name diff --git a/mobile/services/api/programmingForumSchemas.ts b/mobile/services/api/programmingForumSchemas.ts index 77542aad..0e0b4664 100644 --- a/mobile/services/api/programmingForumSchemas.ts +++ b/mobile/services/api/programmingForumSchemas.ts @@ -96,7 +96,7 @@ export type UserProfileUpdate = { }; /** - * @example {"id":1,"username":"john_doe","reputationPoints":100,"profilePicture":"frontend\src\assets\placeholder_profile.png","name":"John Doe"} + * @example {"id":1,"username":"john_doe","reputationPoints":100,"profilePicture":"@/assets/images/placeholder_profile.png","name":"John Doe"} */ export type UserSummary = { id: number; diff --git a/mobile/services/temporaryMocks.ts b/mobile/services/temporaryMocks.ts index 96fe9c5b..c18cdd51 100644 --- a/mobile/services/temporaryMocks.ts +++ b/mobile/services/temporaryMocks.ts @@ -28,7 +28,7 @@ export const temporaryMocks = { id: 2, username: "john_doe", reputationPoints: 100, - profilePicture: "frontend\src\assets\placeholder_profile.png", + profilePicture: "@/assets/images/placeholder_profile.png", name: "John Doe", experienceLevel: "INTERMEDIATE" }, @@ -62,7 +62,7 @@ export const temporaryMocks = { id: 1, username: "jane_doe", reputationPoints: 150, - profilePicture: "frontend\src\assets\placeholder_profile.png", + profilePicture: "@/assets/images/placeholder_profile.png", name: "Jane Doe", experienceLevel: "EXPERT" }, @@ -83,7 +83,7 @@ export const temporaryMocks = { id: 2, username: "john_doe", reputationPoints: 100, - profilePicture: "frontend\src\assets\placeholder_profile.png", + profilePicture: "@/assets/images/placeholder_profile.png", name: "John Doe", experienceLevel: "INTERMEDIATE" }, diff --git a/swagger/openapi.yml b/swagger/openapi.yml index b76b58ad..c5f3a194 100644 --- a/swagger/openapi.yml +++ b/swagger/openapi.yml @@ -1354,7 +1354,7 @@ components: - id: 1 username: "john_doe" reputationPoints: 100 - profilePicture: "frontend\src\assets\placeholder_profile.png" + profilePicture: "https://placehold.co/640x640" name: "John Doe" DifficultyLevel: From ae392a5957f2f1ed8b3470451aaee4671f6d7ad7 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sat, 14 Dec 2024 21:25:00 +0300 Subject: [PATCH 16/48] Add bookmark page tests --- frontend/src/routes/bookmarked.test.tsx | 105 ++++++++++++++++++++++++ frontend/src/routes/profile.test.tsx | 2 +- 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 frontend/src/routes/bookmarked.test.tsx diff --git a/frontend/src/routes/bookmarked.test.tsx b/frontend/src/routes/bookmarked.test.tsx new file mode 100644 index 00000000..7b0de7c0 --- /dev/null +++ b/frontend/src/routes/bookmarked.test.tsx @@ -0,0 +1,105 @@ +import { + GetBookmarkedQuestionsError, + useGetBookmarkedQuestions, + } from "@/services/api/programmingForumComponents"; + import { QuestionDetails } from "@/services/api/programmingForumSchemas"; + import { testAccessibility } from "@/utils/test-accessibility"; + import { QueryObserverSuccessResult } from "@tanstack/react-query"; + import { render, screen } from "@testing-library/react"; + import { + createMemoryRouter, + MemoryRouter, + Route, + RouterProvider, + Routes, + } from "react-router-dom"; + import { beforeEach, describe, expect, it, vi } from "vitest"; + import { routeConfig } from "."; + import { BookmarkedQuestions } from "./bookmarked"; + + // Mock the useGetBookmarkedQuestions hook + vi.mock("@/services/api/programmingForumComponents", () => ({ + useGetBookmarkedQuestions: vi.fn(), + })); + + const mockQuestions: QuestionDetails[] = [ + { + id: 1, + title: "How to implement a binary tree in Python?", + content: "I'm struggling to understand the structure...", + author: { id: 1, name: "John Doe", username: "user1", profilePicture: "p", reputationPoints: 50}, + createdAt: "2024-12-01T12:00:00Z", + updatedAt: "2024-12-01T12:30:00Z", + tags: [{ id: "1", name: "Python" }], + likeCount: 10, + dislikeCount: 2, + commentCount: 4, + viewCount: 50, + bookmarked: true, + selfVoted: 1, + selfDifficultyVote: "MEDIUM", + easyCount: 5, + mediumCount: 10, + hardCount: 3, + }, + { + id: 2, + title: "What are closures in JavaScript?", + content: "Can someone explain closures with an example?", + author: { id: 2, name: "Jane Smith", username: "user2", profilePicture: "p", reputationPoints: 50}, + createdAt: "2024-12-02T10:00:00Z", + updatedAt: "2024-12-02T10:20:00Z", + tags: [{ id: "2", name: "JavaScript" }], + likeCount: 15, + dislikeCount: 1, + commentCount: 5, + viewCount: 70, + bookmarked: true, + selfVoted: 0, + selfDifficultyVote: "EASY", + easyCount: 8, + mediumCount: 6, + hardCount: 1, + }, + ]; + + describe("BookmarkedQuestions component", () => { + beforeEach(() => { + vi.mocked(useGetBookmarkedQuestions).mockReset(); + }); + + it("should have no accessibility violations", async () => { + const router = createMemoryRouter(routeConfig, { + initialEntries: ["/bookmarked"], + }); + + await testAccessibility(); + }); + + it("renders bookmarked questions correctly", () => { + vi.mocked(useGetBookmarkedQuestions).mockReturnValue({ + isLoading: false, + error: null, + data: { + data: { items: mockQuestions, totalItems: mockQuestions.length }, + }, + } as QueryObserverSuccessResult); + + render( + + + } /> + + , + ); + + expect( + screen.getByText(`Last ${mockQuestions.length} bookmarked questions shown.`), + ).toBeInTheDocument(); + + mockQuestions.forEach((question) => { + expect(screen.getByText(question.title)).toBeInTheDocument(); + }); + }); + }); + \ No newline at end of file diff --git a/frontend/src/routes/profile.test.tsx b/frontend/src/routes/profile.test.tsx index fc76cadc..82b185e4 100644 --- a/frontend/src/routes/profile.test.tsx +++ b/frontend/src/routes/profile.test.tsx @@ -126,7 +126,7 @@ describe("Profile component", () => { render(); - const editButton = screen.getByText("Edit profile"); + const editButton = screen.getByText("Edit Profile"); fireEvent.click(editButton); const bioField = screen.getByPlaceholderText("Bio"); From 9acbce7cb6953b85296bce0e12b8985a6e7f5293 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sat, 14 Dec 2024 22:19:38 +0300 Subject: [PATCH 17/48] Match latest changes in develop --- frontend/src/routes/bookmarked.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/routes/bookmarked.test.tsx b/frontend/src/routes/bookmarked.test.tsx index 7b0de7c0..cf2cbf5f 100644 --- a/frontend/src/routes/bookmarked.test.tsx +++ b/frontend/src/routes/bookmarked.test.tsx @@ -37,6 +37,7 @@ import { viewCount: 50, bookmarked: true, selfVoted: 1, + difficulty: "MEDIUM", selfDifficultyVote: "MEDIUM", easyCount: 5, mediumCount: 10, @@ -56,6 +57,7 @@ import { viewCount: 70, bookmarked: true, selfVoted: 0, + difficulty: "EASY", selfDifficultyVote: "EASY", easyCount: 8, mediumCount: 6, From 69b1cf3ca931b4c5655185dd8658f1eb1921bb2d Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sat, 14 Dec 2024 22:37:45 +0300 Subject: [PATCH 18/48] Fix issues from feedback --- .../routes/{bookmarked.test.tsx => bookmarks.test.tsx} | 8 ++++---- frontend/src/routes/{bookmarked.tsx => bookmarks.tsx} | 4 ++-- frontend/src/routes/index.tsx | 4 ++-- frontend/src/routes/profile.tsx | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename frontend/src/routes/{bookmarked.test.tsx => bookmarks.test.tsx} (93%) rename frontend/src/routes/{bookmarked.tsx => bookmarks.tsx} (95%) diff --git a/frontend/src/routes/bookmarked.test.tsx b/frontend/src/routes/bookmarks.test.tsx similarity index 93% rename from frontend/src/routes/bookmarked.test.tsx rename to frontend/src/routes/bookmarks.test.tsx index cf2cbf5f..f56ecfcc 100644 --- a/frontend/src/routes/bookmarked.test.tsx +++ b/frontend/src/routes/bookmarks.test.tsx @@ -15,7 +15,7 @@ import { } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { routeConfig } from "."; - import { BookmarkedQuestions } from "./bookmarked"; + import { BookmarkedQuestions } from "./bookmarks"; // Mock the useGetBookmarkedQuestions hook vi.mock("@/services/api/programmingForumComponents", () => ({ @@ -72,7 +72,7 @@ import { it("should have no accessibility violations", async () => { const router = createMemoryRouter(routeConfig, { - initialEntries: ["/bookmarked"], + initialEntries: ["/bookmarks"], }); await testAccessibility(); @@ -88,9 +88,9 @@ import { } as QueryObserverSuccessResult); render( - + - } /> + } /> , ); diff --git a/frontend/src/routes/bookmarked.tsx b/frontend/src/routes/bookmarks.tsx similarity index 95% rename from frontend/src/routes/bookmarked.tsx rename to frontend/src/routes/bookmarks.tsx index 5d69c407..9be92871 100644 --- a/frontend/src/routes/bookmarked.tsx +++ b/frontend/src/routes/bookmarks.tsx @@ -47,8 +47,8 @@ export const BookmarkedQuestions = () => {

{questions.length - ? `Last ${resultListData.totalItems} bookmarked questions shown.` - : "No questions are bookmarked."} + ? `You have ${resultListData.totalItems} bookmarked questions.` + : "You haven't bookmarked any questions."}

{!questions.length && ( diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 8a869ef0..cc042e4c 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -12,7 +12,7 @@ import QuestionRoute from "./question"; import { Search } from "./search"; import Signup from "./signup"; import TagPage from "./tag"; -import { BookmarkedQuestions } from "@/routes/bookmarked"; +import { BookmarkedQuestions } from "@/routes/bookmarks"; export const routes: RouteObject[] = [ { @@ -28,7 +28,7 @@ export const routes: RouteObject[] = [ Component: Login, }, { - path: "/bookmarkedquestions", + path: "/bookmarks", Component: BookmarkedQuestions, }, { diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index cf635cd6..bd457537 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -153,12 +153,12 @@ export default function Profile() { {isPending ? "Saving..." : "Save"} ) : ( -
+
) From 7794988981b95561769877a24379d7d293fcb16c Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sat, 14 Dec 2024 22:40:22 +0300 Subject: [PATCH 19/48] Fix test text field --- frontend/src/routes/bookmarks.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/bookmarks.test.tsx b/frontend/src/routes/bookmarks.test.tsx index f56ecfcc..2d0917bf 100644 --- a/frontend/src/routes/bookmarks.test.tsx +++ b/frontend/src/routes/bookmarks.test.tsx @@ -96,7 +96,7 @@ import { ); expect( - screen.getByText(`Last ${mockQuestions.length} bookmarked questions shown.`), + screen.getByText(`You have ${mockQuestions.length} bookmarked questions.`), ).toBeInTheDocument(); mockQuestions.forEach((question) => { From ebfd9704c90ff99f9287a4685bb1bdaccb6576ef Mon Sep 17 00:00:00 2001 From: Nazire Date: Sat, 14 Dec 2024 23:06:48 +0300 Subject: [PATCH 20/48] added link to tag page --- frontend/src/routes/question.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/question.tsx b/frontend/src/routes/question.tsx index 888ea944..9a0f4d34 100644 --- a/frontend/src/routes/question.tsx +++ b/frontend/src/routes/question.tsx @@ -300,8 +300,10 @@ export default function QuestionPage() { /> ) : (
- {question.tags.map((tag) => ( - {tag.name} + {question.tags.map((s) => ( + + {s.name} + ))}
)} From 5261e276f156ad63e8bd8cf4d8ac2e3dc64ccdca Mon Sep 17 00:00:00 2001 From: Nazire Date: Sat, 14 Dec 2024 23:10:26 +0300 Subject: [PATCH 21/48] added link to tag page --- frontend/src/routes/question.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/question.tsx b/frontend/src/routes/question.tsx index 9a0f4d34..1d29fb9c 100644 --- a/frontend/src/routes/question.tsx +++ b/frontend/src/routes/question.tsx @@ -301,10 +301,11 @@ export default function QuestionPage() { ) : (
{question.tags.map((s) => ( - + {s.name} - ))} + ))// Link to tag page + }
)} From fc09387f382ef8c427ac873dadfc28a71e48f5f7 Mon Sep 17 00:00:00 2001 From: Nazire Date: Sat, 14 Dec 2024 23:11:37 +0300 Subject: [PATCH 22/48] added link to tag page --- frontend/src/routes/question.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/routes/question.tsx b/frontend/src/routes/question.tsx index 1d29fb9c..35f892f6 100644 --- a/frontend/src/routes/question.tsx +++ b/frontend/src/routes/question.tsx @@ -304,8 +304,7 @@ export default function QuestionPage() { {s.name} - ))// Link to tag page - } + ))}
)} From 27fb4cc06d462182b16a0836f5da6f4d61688838 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Sun, 15 Dec 2024 16:52:58 +0300 Subject: [PATCH 23/48] Update reputation points and add test --- frontend/src/routes/profile.test.tsx | 2 ++ frontend/src/routes/profile.tsx | 13 ++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/frontend/src/routes/profile.test.tsx b/frontend/src/routes/profile.test.tsx index fc76cadc..350869bb 100644 --- a/frontend/src/routes/profile.test.tsx +++ b/frontend/src/routes/profile.test.tsx @@ -66,6 +66,7 @@ describe("Profile component", () => { answerCount: 5, followersCount: 50, followingCount: 30, + reputationPoints: 75, bio: "This is John's bio", country: "US", }, @@ -87,6 +88,7 @@ describe("Profile component", () => { expect(screen.getByText("5")).toBeInTheDocument(); expect(screen.getByText("50")).toBeInTheDocument(); expect(screen.getByText("30")).toBeInTheDocument(); + expect(screen.getByText("75")).toBeInTheDocument(); expect(screen.getAllByText("Questions").length).toBeGreaterThan(1); expect(screen.getAllByText("Answers").length).toBeGreaterThan(1); }); diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index 7e836162..ea9b9af2 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -100,6 +100,10 @@ export default function Profile() {
{profile.followingCount}
Following
+
+
{profile.reputationPoints}
+
Reputation Points
+
@@ -167,15 +171,6 @@ export default function Profile() { ) )}
- {profile.reputationPoints && ( -
-
- - Reputation Points: {profile.reputationPoints} - -
-
- )} {profile.followedTags && (
From e9cfbe311050b64b32c7941247b71251cf9d0b86 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 10:59:32 +0300 Subject: [PATCH 24/48] Add recommendation query --- .../Repositories/QuestionRepository.java | 16 ++++++++++++++++ .../Services/QuestionService.java | 12 ++++++++++-- .../Services/UserService.java | 6 ++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionRepository.java b/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionRepository.java index 42120529..58c541d2 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionRepository.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Repositories/QuestionRepository.java @@ -31,6 +31,22 @@ Page searchQuestions( @Param("difficulty") DifficultyLevel difficulty, Pageable pageable); + @Query("SELECT DISTINCT q FROM Question q " + + "LEFT JOIN q.tags t " + + "WHERE (:query IS NULL OR " + + " LOWER(q.title) LIKE LOWER(CONCAT('%', :query, '%')) OR " + + " LOWER(q.questionBody) LIKE LOWER(CONCAT('%', :query, '%'))) " + + "AND (:tagIds IS NULL OR t.id IN :tagIds) " + + "AND (:difficulty IS NULL OR q.difficulty = :difficulty) " + + "ORDER BY " + + " CASE WHEN :authorIds IS NOT NULL AND q.askedBy.id IN :authorIds THEN 1 ELSE 0 END DESC") + Page searchQuestionsByRecommended( + @Param("query") String query, + @Param("authorIds") List authorIds, + @Param("tagIds") List tagIds, + @Param("difficulty") DifficultyLevel difficulty, + Pageable pageable); + @Query("SELECT q FROM Question q WHERE q.askedBy.id = :author") List findByAuthorId(@Param("author") Long authorId); 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 6db81f66..a9613530 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java @@ -218,7 +218,9 @@ public Page searchQuestions( String tagIdsStr, DifficultyLevel difficulty, int page, - int pageSize) { + int pageSize, + String sort, + User currentUser) { List tagIds = null; if (tagIdsStr != null && !tagIdsStr.isEmpty()) { @@ -228,7 +230,13 @@ public Page searchQuestions( } PageRequest pageable = PageRequest.of(page - 1, pageSize); - return questionRepository.searchQuestions(query, tagIds, difficulty, pageable); + if (Objects.equals(sort, "default") || Objects.equals(currentUser, null)) { + return questionRepository.searchQuestions(query, tagIds, difficulty, pageable); + } else { + List authorIds = getFollowingIds(currentUser); + return questionRepository.searchQuestionsByRecommended(query, authorIds, tagIds, difficulty, pageable); + } + } public static QuestionSummaryDto mapToQuestionSummary(Question question) { diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java b/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java index 646ba34e..3fe8307c 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java @@ -107,4 +107,10 @@ public Page searchUsers(String query, int page, int pageSize) { public List getFollowing(User user) { return user.getFollowing().stream().toList(); } + + public List getFollowingIds(User user) { + return user.getFollowing().stream() + .map(User::getId) // Map each User to its ID + .collect(Collectors.toList()); // Collect the IDs into a List + } } From 95643445d8d9d01a2871916405a2772f4d82400d Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 11:10:35 +0300 Subject: [PATCH 25/48] Resolve import issues --- .../Controllers/QuestionController.java | 7 +++++-- .../Services/QuestionService.java | 5 ++++- .../programminglanguagesforum/Services/UserService.java | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java index 352590c0..f0ad2b7d 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java @@ -31,6 +31,7 @@ import com.group1.programminglanguagesforum.DTOs.Responses.QuestionSummaryDto; import com.group1.programminglanguagesforum.Entities.DifficultyLevel; import com.group1.programminglanguagesforum.Entities.Question; +import com.group1.programminglanguagesforum.Entities.User; import com.group1.programminglanguagesforum.Exceptions.ExceptionResponseHandler; import com.group1.programminglanguagesforum.Exceptions.UnauthorizedAccessException; import com.group1.programminglanguagesforum.Services.QuestionDifficultyRateService; @@ -123,9 +124,11 @@ public ResponseEntity>> searchQuestions( @RequestParam(required = false) String tags, @RequestParam(required = false) DifficultyLevel difficulty, @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "20") int pageSize) { + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(defaultValue = "recommended") String sort, + @RequestParam(required = false) User currentUser) { - Page questionPage = questionService.searchQuestions(query, tags, difficulty, page, pageSize); + Page questionPage = questionService.searchQuestions(query, tags, difficulty, page, pageSize, sort, currentUser); List questionSummaries = questionPage.getContent().stream() .map(QuestionService::mapToQuestionSummary) 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 a9613530..e546aa59 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java @@ -10,6 +10,7 @@ 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; @@ -233,7 +234,9 @@ public Page searchQuestions( if (Objects.equals(sort, "default") || Objects.equals(currentUser, null)) { return questionRepository.searchQuestions(query, tagIds, difficulty, pageable); } else { - List authorIds = getFollowingIds(currentUser); + 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); } diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java b/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java index 3fe8307c..46e18d54 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor From 9ee87282a8671da4cf36acda3ca7239eab0393d1 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 13:06:59 +0300 Subject: [PATCH 26/48] Add api info --- .../Controllers/QuestionController.java | 6 +++--- .../Services/QuestionService.java | 8 +++++--- .../src/services/api/programmingForumComponents.ts | 14 ++++++++++++++ swagger/openapi.yml | 12 ++++++++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java index f0ad2b7d..5304b0c0 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java @@ -125,10 +125,10 @@ public ResponseEntity>> searchQuestions( @RequestParam(required = false) DifficultyLevel difficulty, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize, - @RequestParam(defaultValue = "recommended") String sort, - @RequestParam(required = false) User currentUser) { + @RequestParam(defaultValue = "recommended") String sortBy, + @RequestParam(defaultValue = "-1") Long currentUserId) { - Page questionPage = questionService.searchQuestions(query, tags, difficulty, page, pageSize, sort, currentUser); + Page questionPage = questionService.searchQuestions(query, tags, difficulty, page, pageSize, sortBy, currentUserId); List questionSummaries = questionPage.getContent().stream() .map(QuestionService::mapToQuestionSummary) 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 e546aa59..2d88d4f4 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java @@ -43,6 +43,7 @@ public class QuestionService { private final TagService tagService; private final BookmarkRepository bookmarkRepository; private final VoteRepository voteRepository; + private final UserService userService; private final QuestionDifficultyRateService questionDifficultyRateService; public Optional findById(Long id) { @@ -220,8 +221,8 @@ public Page searchQuestions( DifficultyLevel difficulty, int page, int pageSize, - String sort, - User currentUser) { + String sortBy, + Long currentUserId) { List tagIds = null; if (tagIdsStr != null && !tagIdsStr.isEmpty()) { @@ -231,9 +232,10 @@ public Page searchQuestions( } PageRequest pageable = PageRequest.of(page - 1, pageSize); - if (Objects.equals(sort, "default") || Objects.equals(currentUser, null)) { + if (Objects.equals(sortBy, "default") || currentUserId == -1) { return questionRepository.searchQuestions(query, tagIds, difficulty, pageable); } else { + User currentUser = userService.getUserById(currentUserId).get(); List authorIds = currentUser.getFollowing().stream() .map(User::getId) // Map each User to its ID .collect(Collectors.toList()); // Collect the IDs into a List diff --git a/frontend/src/services/api/programmingForumComponents.ts b/frontend/src/services/api/programmingForumComponents.ts index 63141bb8..490f4b48 100644 --- a/frontend/src/services/api/programmingForumComponents.ts +++ b/frontend/src/services/api/programmingForumComponents.ts @@ -1978,6 +1978,20 @@ export type SearchQuestionsQueryParams = { * @default 20 */ pageSize?: number; + /** + * Sorting type + * + * @default "recommended" + */ + sortBy: string; + /** + * Current user Id + * + * @default -1 + */ + currentUserId?: number; + + }; export type SearchQuestionsError = Fetcher.ErrorWrapper<{ diff --git a/swagger/openapi.yml b/swagger/openapi.yml index c5f3a194..86ed232c 100644 --- a/swagger/openapi.yml +++ b/swagger/openapi.yml @@ -938,6 +938,18 @@ paths: schema: type: integer default: 20 + - name: sortBy + in: query + description: Sorting type + schema: + type: string + default: "recommended" + - name: currentUserId + in: query + description: Current user id + schema: + type: integer + default: -1 responses: "200": description: Successful response From 0cb47524cd7fa775ac8d5ce4faaba38e8cc5187d Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 13:42:58 +0300 Subject: [PATCH 27/48] Integrate with frontend --- frontend/src/components/SearchBar.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index ac352793..2f8efd71 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -2,6 +2,7 @@ import { Hash, MessageSquare, Search } from "lucide-react"; import { useEffect, useId, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "./ui/button"; +import useAuthStore from "@/services/auth"; import { DropdownMenu, DropdownMenuContent, @@ -23,6 +24,7 @@ const searchTypes = [ { id: "questions", label: "Questions", icon: MessageSquare }, ] as const; + export const SearchBar = () => { const id = useId(); const [params] = useSearchParams(); @@ -35,6 +37,8 @@ export const SearchBar = () => { const inputRef = useRef(null); const navigate = useNavigate(); + const { selfProfile } = useAuthStore(); + // Get current search type info const currentSearchType = searchTypes.find((type) => type.id === searchType); const SearchTypeIcon = currentSearchType?.icon || Hash; @@ -97,6 +101,10 @@ export const SearchBar = () => { const params = new URLSearchParams(); params.append("type", searchType); params.append("q", search); + // Safely append currentUserId only if selfProfile is available + if (selfProfile?.id) { + params.append("currentUserId", selfProfile.id.toString()); + } navigate("/search?" + params.toString()); }; From b1b4606bba5e50cdd1b68ec9f52b91aa5ffd2f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Mon, 16 Dec 2024 13:44:35 +0300 Subject: [PATCH 28/48] fix(mobile): update new backend URL --- mobile/services/api/programmingForumFetcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/services/api/programmingForumFetcher.ts b/mobile/services/api/programmingForumFetcher.ts index f1c8d7d8..4fe0448c 100644 --- a/mobile/services/api/programmingForumFetcher.ts +++ b/mobile/services/api/programmingForumFetcher.ts @@ -13,7 +13,7 @@ const baseUrl = process.env.NODE_ENV === "development" ? `http://${new URL(Constants.default.experienceUrl).hostname}:5173/api/v1` : - "https://programming-languages-forum-ahwzj.ondigitalocean.app/api/v1"; + "https://programming-languages-forum-psrb6.ondigitalocean.app/api/v1"; console.log(baseUrl); From c8d0bb05de1343d8e864a3f6ff12fb1fb36c41d1 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 13:48:08 +0300 Subject: [PATCH 29/48] Add sortBy to parameters --- frontend/src/components/SearchBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 2f8efd71..8260bfe7 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -105,6 +105,7 @@ export const SearchBar = () => { if (selfProfile?.id) { params.append("currentUserId", selfProfile.id.toString()); } + params.append("sortBy", "recommended"); navigate("/search?" + params.toString()); }; From 44e34535af570b86a3726ef7b12350335e9fb9b7 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 13:53:12 +0300 Subject: [PATCH 30/48] Fix sortBy --- frontend/src/services/api/programmingForumComponents.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/services/api/programmingForumComponents.ts b/frontend/src/services/api/programmingForumComponents.ts index 490f4b48..16a6d8b4 100644 --- a/frontend/src/services/api/programmingForumComponents.ts +++ b/frontend/src/services/api/programmingForumComponents.ts @@ -1983,15 +1983,13 @@ export type SearchQuestionsQueryParams = { * * @default "recommended" */ - sortBy: string; + sortBy?: string; /** * Current user Id * * @default -1 */ currentUserId?: number; - - }; export type SearchQuestionsError = Fetcher.ErrorWrapper<{ From a7bc9268db7782e8a58c00109e48aebdb4ca958f Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 14:16:00 +0300 Subject: [PATCH 31/48] Use sr-only for non-button non-anchor elements --- frontend/src/components/FullscreenLoading.tsx | 2 +- frontend/src/components/SearchQuestionsList.tsx | 2 +- frontend/src/components/SearchTagsList.tsx | 2 +- frontend/src/components/Tags.tsx | 2 +- frontend/src/routes/bookmarks.tsx | 2 +- frontend/src/routes/tag.tsx | 13 ++++--------- 6 files changed, 9 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/FullscreenLoading.tsx b/frontend/src/components/FullscreenLoading.tsx index 246b562f..e58f3ca9 100644 --- a/frontend/src/components/FullscreenLoading.tsx +++ b/frontend/src/components/FullscreenLoading.tsx @@ -24,9 +24,9 @@ export const FullscreenLoading = ({ )} > + Loading {takingLong && (
This is taking a while... diff --git a/frontend/src/components/SearchQuestionsList.tsx b/frontend/src/components/SearchQuestionsList.tsx index b5dc4046..af717e14 100644 --- a/frontend/src/components/SearchQuestionsList.tsx +++ b/frontend/src/components/SearchQuestionsList.tsx @@ -102,9 +102,9 @@ export const SearchQuestionsList = () => { {isLoading && (
+ Loading
)}
diff --git a/frontend/src/components/SearchTagsList.tsx b/frontend/src/components/SearchTagsList.tsx index c2bf9eab..2b9f752f 100644 --- a/frontend/src/components/SearchTagsList.tsx +++ b/frontend/src/components/SearchTagsList.tsx @@ -81,9 +81,9 @@ export const SearchTagsList = () => { {isLoading && (
+ Loading
)}
diff --git a/frontend/src/components/Tags.tsx b/frontend/src/components/Tags.tsx index d9f498d7..9e3adbcf 100644 --- a/frontend/src/components/Tags.tsx +++ b/frontend/src/components/Tags.tsx @@ -88,9 +88,9 @@ export default function TagsPage() { {isLoading && (
+ Loading
)}
diff --git a/frontend/src/routes/bookmarks.tsx b/frontend/src/routes/bookmarks.tsx index 9be92871..360a2e3c 100644 --- a/frontend/src/routes/bookmarks.tsx +++ b/frontend/src/routes/bookmarks.tsx @@ -90,9 +90,9 @@ export const BookmarkedQuestions = () => { {isLoading && (
+ Loading
)}
diff --git a/frontend/src/routes/tag.tsx b/frontend/src/routes/tag.tsx index 6604933d..0b623ca1 100644 --- a/frontend/src/routes/tag.tsx +++ b/frontend/src/routes/tag.tsx @@ -125,12 +125,9 @@ export default function TagPage() {
{tag.fileExtension && ( -
+
- + File extension for ${tag.name} is ${tag.fileExtension} {tag.fileExtension}
)} @@ -146,14 +143,12 @@ export default function TagPage() { )} {tag.inceptionYear && ( -
+
Created in {new Date(tag.inceptionYear).toLocaleDateString()} + Inception year for ${tag.name} is ${tag.inceptionYear}
)} From 80dce92c8e20f814d5e0d4a1c6458470426acfc2 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 14:33:35 +0300 Subject: [PATCH 32/48] Make loading messages visible --- frontend/src/components/FullscreenLoading.tsx | 6 +++++- frontend/src/components/SearchQuestionsList.tsx | 4 +++- frontend/src/components/SearchTagsList.tsx | 4 +++- frontend/src/components/Tags.tsx | 4 +++- frontend/src/routes/bookmarks.tsx | 4 +++- frontend/src/routes/profile.test.tsx | 2 +- frontend/src/routes/search.test.tsx | 2 +- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/FullscreenLoading.tsx b/frontend/src/components/FullscreenLoading.tsx index e58f3ca9..4d815ee7 100644 --- a/frontend/src/components/FullscreenLoading.tsx +++ b/frontend/src/components/FullscreenLoading.tsx @@ -26,7 +26,11 @@ export const FullscreenLoading = ({ - Loading + {!takingLong && ( +
+ Loading... +
+ )} {takingLong && (
This is taking a while... diff --git a/frontend/src/components/SearchQuestionsList.tsx b/frontend/src/components/SearchQuestionsList.tsx index af717e14..684ac11b 100644 --- a/frontend/src/components/SearchQuestionsList.tsx +++ b/frontend/src/components/SearchQuestionsList.tsx @@ -104,7 +104,9 @@ export const SearchQuestionsList = () => { - Loading +
+ Loading... +
)}
diff --git a/frontend/src/components/SearchTagsList.tsx b/frontend/src/components/SearchTagsList.tsx index 2b9f752f..0aad2f39 100644 --- a/frontend/src/components/SearchTagsList.tsx +++ b/frontend/src/components/SearchTagsList.tsx @@ -83,7 +83,9 @@ export const SearchTagsList = () => { - Loading +
+ Loading... +
)}
diff --git a/frontend/src/components/Tags.tsx b/frontend/src/components/Tags.tsx index 9e3adbcf..a1de0918 100644 --- a/frontend/src/components/Tags.tsx +++ b/frontend/src/components/Tags.tsx @@ -90,7 +90,9 @@ export default function TagsPage() { - Loading +
+ Loading... +
)} diff --git a/frontend/src/routes/bookmarks.tsx b/frontend/src/routes/bookmarks.tsx index 360a2e3c..d0e3c7ee 100644 --- a/frontend/src/routes/bookmarks.tsx +++ b/frontend/src/routes/bookmarks.tsx @@ -92,7 +92,9 @@ export const BookmarkedQuestions = () => { - Loading +
+ Loading... +
)} diff --git a/frontend/src/routes/profile.test.tsx b/frontend/src/routes/profile.test.tsx index 82b185e4..bdfd4e64 100644 --- a/frontend/src/routes/profile.test.tsx +++ b/frontend/src/routes/profile.test.tsx @@ -53,7 +53,7 @@ describe("Profile component", () => { render(); - expect(screen.getByLabelText(/loading/i)).toBeInTheDocument(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); }); it("displays user data correctly", async () => { diff --git a/frontend/src/routes/search.test.tsx b/frontend/src/routes/search.test.tsx index fad28c4c..3056df0b 100644 --- a/frontend/src/routes/search.test.tsx +++ b/frontend/src/routes/search.test.tsx @@ -55,7 +55,7 @@ describe("Search component", () => { , ); - expect(screen.getByLabelText(/loading/i)).toBeInTheDocument(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); }); it("renders tags correctly", () => { From 5acc44ca369416e6f65643c7ac2f2ac1039ce454 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 15:42:05 +0300 Subject: [PATCH 33/48] Get User from UserContextService --- .../Controllers/QuestionController.java | 6 ++---- .../Services/QuestionService.java | 14 +++++++++----- .../Services/UserService.java | 6 ------ frontend/src/components/SearchBar.tsx | 8 -------- .../src/services/api/programmingForumComponents.ts | 10 ++-------- .../src/services/api/programmingForumSchemas.ts | 2 +- swagger/openapi.yml | 8 +------- 7 files changed, 15 insertions(+), 39 deletions(-) diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java index 5304b0c0..96ed50ad 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Controllers/QuestionController.java @@ -31,7 +31,6 @@ import com.group1.programminglanguagesforum.DTOs.Responses.QuestionSummaryDto; import com.group1.programminglanguagesforum.Entities.DifficultyLevel; import com.group1.programminglanguagesforum.Entities.Question; -import com.group1.programminglanguagesforum.Entities.User; import com.group1.programminglanguagesforum.Exceptions.ExceptionResponseHandler; import com.group1.programminglanguagesforum.Exceptions.UnauthorizedAccessException; import com.group1.programminglanguagesforum.Services.QuestionDifficultyRateService; @@ -125,10 +124,9 @@ public ResponseEntity>> searchQuestions( @RequestParam(required = false) DifficultyLevel difficulty, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize, - @RequestParam(defaultValue = "recommended") String sortBy, - @RequestParam(defaultValue = "-1") Long currentUserId) { + @RequestParam(defaultValue = "recommended") String sortBy) { - Page questionPage = questionService.searchQuestions(query, tags, difficulty, page, pageSize, sortBy, currentUserId); + Page questionPage = questionService.searchQuestions(query, tags, difficulty, page, pageSize, sortBy); List questionSummaries = questionPage.getContent().stream() .map(QuestionService::mapToQuestionSummary) 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 2d88d4f4..e4bc9b81 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java @@ -43,7 +43,6 @@ public class QuestionService { private final TagService tagService; private final BookmarkRepository bookmarkRepository; private final VoteRepository voteRepository; - private final UserService userService; private final QuestionDifficultyRateService questionDifficultyRateService; public Optional findById(Long id) { @@ -221,8 +220,7 @@ public Page searchQuestions( DifficultyLevel difficulty, int page, int pageSize, - String sortBy, - Long currentUserId) { + String sortBy) { List tagIds = null; if (tagIdsStr != null && !tagIdsStr.isEmpty()) { @@ -230,12 +228,18 @@ public Page searchQuestions( .map(Long::parseLong) .collect(Collectors.toList()); } + User currentUser; + try { + currentUser = userContextService.getCurrentUser(); + } catch (UnauthorizedAccessException e) { + currentUser = null; + } PageRequest pageable = PageRequest.of(page - 1, pageSize); - if (Objects.equals(sortBy, "default") || currentUserId == -1) { + if (Objects.equals(sortBy, "default") || Objects.equals(currentUser, null)) { return questionRepository.searchQuestions(query, tagIds, difficulty, pageable); } else { - User currentUser = userService.getUserById(currentUserId).get(); + List authorIds = currentUser.getFollowing().stream() .map(User::getId) // Map each User to its ID .collect(Collectors.toList()); // Collect the IDs into a List diff --git a/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java b/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java index 46e18d54..118f6a23 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/UserService.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -109,9 +108,4 @@ public List getFollowing(User user) { return user.getFollowing().stream().toList(); } - public List getFollowingIds(User user) { - return user.getFollowing().stream() - .map(User::getId) // Map each User to its ID - .collect(Collectors.toList()); // Collect the IDs into a List - } } diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 8260bfe7..5ef44760 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -2,7 +2,6 @@ import { Hash, MessageSquare, Search } from "lucide-react"; import { useEffect, useId, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "./ui/button"; -import useAuthStore from "@/services/auth"; import { DropdownMenu, DropdownMenuContent, @@ -24,7 +23,6 @@ const searchTypes = [ { id: "questions", label: "Questions", icon: MessageSquare }, ] as const; - export const SearchBar = () => { const id = useId(); const [params] = useSearchParams(); @@ -37,8 +35,6 @@ export const SearchBar = () => { const inputRef = useRef(null); const navigate = useNavigate(); - const { selfProfile } = useAuthStore(); - // Get current search type info const currentSearchType = searchTypes.find((type) => type.id === searchType); const SearchTypeIcon = currentSearchType?.icon || Hash; @@ -101,10 +97,6 @@ export const SearchBar = () => { const params = new URLSearchParams(); params.append("type", searchType); params.append("q", search); - // Safely append currentUserId only if selfProfile is available - if (selfProfile?.id) { - params.append("currentUserId", selfProfile.id.toString()); - } params.append("sortBy", "recommended"); navigate("/search?" + params.toString()); }; diff --git a/frontend/src/services/api/programmingForumComponents.ts b/frontend/src/services/api/programmingForumComponents.ts index 16a6d8b4..b05bc956 100644 --- a/frontend/src/services/api/programmingForumComponents.ts +++ b/frontend/src/services/api/programmingForumComponents.ts @@ -1980,16 +1980,10 @@ export type SearchQuestionsQueryParams = { pageSize?: number; /** * Sorting type - * - * @default "recommended" + * + * @default recommended */ sortBy?: string; - /** - * Current user Id - * - * @default -1 - */ - currentUserId?: number; }; export type SearchQuestionsError = Fetcher.ErrorWrapper<{ diff --git a/frontend/src/services/api/programmingForumSchemas.ts b/frontend/src/services/api/programmingForumSchemas.ts index 59344b8f..4080d5ec 100644 --- a/frontend/src/services/api/programmingForumSchemas.ts +++ b/frontend/src/services/api/programmingForumSchemas.ts @@ -96,7 +96,7 @@ export type UserProfileUpdate = { }; /** - * @example {"id":1,"username":"john_doe","reputationPoints":100,"profilePicture":"frontend\src\assets\placeholder_profile.png","name":"John Doe"} + * @example {"id":1,"username":"john_doe","reputationPoints":100,"profilePicture":"https://placehold.co/640x640","name":"John Doe"} */ export type UserSummary = { id: number; diff --git a/swagger/openapi.yml b/swagger/openapi.yml index 86ed232c..499f1a48 100644 --- a/swagger/openapi.yml +++ b/swagger/openapi.yml @@ -938,18 +938,12 @@ paths: schema: type: integer default: 20 - - name: sortBy + - name: sortBy in: query description: Sorting type schema: type: string default: "recommended" - - name: currentUserId - in: query - description: Current user id - schema: - type: integer - default: -1 responses: "200": description: Successful response From 1286231b5a0493f51a43e5f59da3cbffcb65cfb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Mon, 16 Dec 2024 16:06:51 +0300 Subject: [PATCH 34/48] feat(mobile): add reputation and followed tags to profile page --- mobile/.expo/types/router.d.ts | 2 +- mobile/app/(tabs)/profile.tsx | 80 ++++++++++++++++------------- mobile/app/{ => me}/bookmarks.tsx | 25 ++------- mobile/app/tags/[tagId].tsx | 4 +- mobile/components/Feed.tsx | 5 +- mobile/components/QuestionCard.tsx | 24 +++++---- mobile/components/QuestionsList.tsx | 27 +++++++++- 7 files changed, 95 insertions(+), 72 deletions(-) rename mobile/app/{ => me}/bookmarks.tsx (66%) diff --git a/mobile/.expo/types/router.d.ts b/mobile/.expo/types/router.d.ts index 961123f3..1d7ea96f 100644 --- a/mobile/.expo/types/router.d.ts +++ b/mobile/.expo/types/router.d.ts @@ -6,7 +6,7 @@ export * from 'expo-router'; declare module 'expo-router' { export namespace ExpoRouter { export interface __routes extends Record { - StaticRoutes: `/` | `/(tabs)` | `/(tabs)/` | `/(tabs)/profile` | `/(tabs)/search` | `/_sitemap` | `/home` | `/login` | `/logout` | `/profile` | `/question/new` | `/search` | `/signup`; + StaticRoutes: `/` | `/(tabs)` | `/(tabs)/` | `/(tabs)/profile` | `/(tabs)/search` | `/_sitemap` | `/home` | `/login` | `/logout` | `/profile` | `/question/new` | `/search` | `/signup` | `/me` | `/me/bookmarks`; DynamicRoutes: `/question/${Router.SingleRoutePart}` | `/question/${Router.SingleRoutePart}/answer` | `/tags/${Router.SingleRoutePart}` | `/users/${Router.SingleRoutePart}`; DynamicRouteTemplate: `/question/[questionId]` | `/question/[questionId]/answer` | `/tags/[tagId]` | `/users/[userId]`; } diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx index 34b72b56..27e78e3c 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -1,8 +1,9 @@ import ErrorAlert from "@/components/ErrorAlert"; import FollowUserButton from "@/components/FollowUserButton"; import { FullscreenLoading } from "@/components/FullscreenLoading"; -import { QuestionCard } from "@/components/QuestionCard"; import { + Badge, + BadgeText, Button, ButtonText, HStack, @@ -10,6 +11,7 @@ import { Image, Input, InputField, + Pressable, ScrollView, Select, SelectBackdrop, @@ -32,12 +34,13 @@ import { useGetUserProfile, useUpdateUserProfile, } from "@/services/api/programmingForumComponents"; -import { ExperienceLevel } from "@/services/api/programmingForumSchemas"; +import { ExperienceLevel, QuestionSummary } from "@/services/api/programmingForumSchemas"; import useAuthStore from "@/services/auth"; import { Link, router } from "expo-router"; import { ChevronDownIcon, Plus, Bookmark, MenuIcon, LogOutIcon } from "lucide-react-native"; import { useEffect, useState } from "react"; import placeholderProfile from "@/assets/images/placeholder_profile.png"; +import { QuestionList } from "@/components/QuestionsList"; export default function Profile() { return ; @@ -123,7 +126,7 @@ export function UserProfile({ userId }: { userId: string }) { ) }} > - router.push(`/bookmarks`)}> + router.push(`me/bookmarks`)}> Bookmarks @@ -140,7 +143,7 @@ export function UserProfile({ userId }: { userId: string }) { - + {`Profile - + {editing ? ( <> Experience: {profile.experienceLevel?.toString() || "Unknown"} + + Reputation Points:{" "} + 0 ? "text-green-700" : "text-gray-500" + } + > + {(profile.reputationPoints ?? 0) > 0 ? `+${profile.reputationPoints ?? 0}` : profile.reputationPoints ?? 0} + + + + + {profile.followedTags?.map((tag) => ( + router.push(`/tags/${tag.id}`)}> + + {tag.name} + + + ))} + + )} @@ -257,7 +281,7 @@ export function UserProfile({ userId }: { userId: string }) { data?.data && )} - {/* + { - */} + } {activeTab === "questions" ? ( @@ -288,37 +312,23 @@ export function UserProfile({ userId }: { userId: string }) { )} - - {profile?.questions?.map((question) => ( - { + return (b.upvoteCount - b.downvoteCount) - (a.upvoteCount - a.downvoteCount); + }) || [] + } /> - ))} - ) : ( - - Answers - - {profile?.answers?.map((answer) => ( - - ))} - - + ({ + id: answer.question.id ?? 0, + title: answer.question.title ?? "", + content: answer.content ?? "", + upvoteCount: answer.upvoteCount, + downvoteCount: answer.downvoteCount, + } as QuestionSummary)) || [] } + /> )}
diff --git a/mobile/app/bookmarks.tsx b/mobile/app/me/bookmarks.tsx similarity index 66% rename from mobile/app/bookmarks.tsx rename to mobile/app/me/bookmarks.tsx index 7558f75c..b150ef04 100644 --- a/mobile/app/bookmarks.tsx +++ b/mobile/app/me/bookmarks.tsx @@ -1,6 +1,6 @@ import ErrorAlert from "@/components/ErrorAlert"; import { FullscreenLoading } from "@/components/FullscreenLoading"; -import { QuestionCard } from "@/components/QuestionCard"; +import { QuestionList } from "@/components/QuestionsList"; import { Button, HStack, @@ -55,25 +55,10 @@ export default function BookmarkedQuestionsPage() { Bookmarked Questions - - - {questions - .sort((a: QuestionSummary, b: QuestionSummary) => - a.createdAt < b.createdAt ? 1 : -1 - ) - .map((question: QuestionSummary) => ( - - ))} - - + + )} diff --git a/mobile/app/tags/[tagId].tsx b/mobile/app/tags/[tagId].tsx index 647b9fd7..48d99f4d 100644 --- a/mobile/app/tags/[tagId].tsx +++ b/mobile/app/tags/[tagId].tsx @@ -1,7 +1,7 @@ import ErrorAlert from "@/components/ErrorAlert"; import FollowTagButton from "@/components/FollowTagButton"; import { FullscreenLoading } from "@/components/FullscreenLoading"; -import { QuestionList } from "@/components/QuestionsList"; +import { QuestionListSearch } from "@/components/QuestionsList"; import { Button, ButtonGroup, @@ -164,7 +164,7 @@ export default function TagPage() { - { @@ -11,7 +10,7 @@ export const Feed = () => { Latest Questions - +
); }; diff --git a/mobile/components/QuestionCard.tsx b/mobile/components/QuestionCard.tsx index c46b20e9..a8009d7e 100644 --- a/mobile/components/QuestionCard.tsx +++ b/mobile/components/QuestionCard.tsx @@ -89,16 +89,20 @@ export const QuestionCard: React.FC = ({ {content} - - - {votes} votes - - - - {answerCount} answers - + {votes !== undefined && ( + + + {votes} votes + + )} + {answerCount !== undefined && ( + + + {answerCount} answers + + )} {difficulty && ( diff --git a/mobile/components/QuestionsList.tsx b/mobile/components/QuestionsList.tsx index c373469f..66970b50 100644 --- a/mobile/components/QuestionsList.tsx +++ b/mobile/components/QuestionsList.tsx @@ -6,8 +6,33 @@ import { useSearchQuestions, SearchQuestionsResponse, } from "@/services/api/programmingForumComponents"; +import { QuestionSummary } from "@/services/api/programmingForumSchemas"; interface QuestionListProps { + questions: QuestionSummary[]; +} + +export const QuestionList: React.FC = ({ questions }) => { + return ( + + {questions.map((question) => ( + + ))} + + ); +} + +interface QuestionListSearchProps { searchQueryParams?: string; pageSize?: number; difficultyFilter?: "EASY" | "MEDIUM" | "HARD"; @@ -15,7 +40,7 @@ interface QuestionListProps { sortBy?: "RECENT" | "TOP_RATED"; } -export const QuestionList: React.FC = ({ +export const QuestionListSearch: React.FC = ({ searchQueryParams = "", pageSize = 10, difficultyFilter, From 4cae53345f5d11547d23eeffc1cf096d6f43ca1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asl=C4=B1=20G=C3=B6k?= Date: Mon, 16 Dec 2024 17:34:58 +0300 Subject: [PATCH 35/48] add glossary and tag type pages --- frontend/src/components/NavbarLayout.tsx | 1 + frontend/src/components/SubtypeCard.tsx | 51 +++++++ frontend/src/components/TagCard.tsx | 17 ++- frontend/src/components/TagType.tsx | 181 +++++++++++++++++++++++ frontend/src/routes/glossary.tsx | 89 +++++++++++ frontend/src/routes/index.tsx | 10 ++ frontend/src/routes/tag.test.tsx | 18 +-- frontend/src/routes/tag.tsx | 20 ++- 8 files changed, 365 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/SubtypeCard.tsx create mode 100644 frontend/src/components/TagType.tsx create mode 100644 frontend/src/routes/glossary.tsx diff --git a/frontend/src/components/NavbarLayout.tsx b/frontend/src/components/NavbarLayout.tsx index b3964039..19a96377 100644 --- a/frontend/src/components/NavbarLayout.tsx +++ b/frontend/src/components/NavbarLayout.tsx @@ -32,6 +32,7 @@ import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"; const links = [ { name: "Home", path: "/" }, + { name: "Glossary", path: "/glossary" }, //add glossary page to the navbar { name: "Tags", path: "/tags" }, ] as const; diff --git a/frontend/src/components/SubtypeCard.tsx b/frontend/src/components/SubtypeCard.tsx new file mode 100644 index 00000000..92f0c085 --- /dev/null +++ b/frontend/src/components/SubtypeCard.tsx @@ -0,0 +1,51 @@ +import { Card } from "@/components/ui/card"; +import { ArrowRight, Tags } from "lucide-react"; +import React from "react"; +import { Link } from "react-router-dom"; + +interface TagSubtypeCardProps { + tagSubtype: { + typeId: string; + tagCount: number; + description: string; // Added description field + }; +} + +export const TagSubtypeCard = React.forwardRef< + HTMLDivElement, + TagSubtypeCardProps +>(({ tagSubtype }, ref) => { + return ( + +
+ {/* Subtype Name */} +

+ {tagSubtype.typeId} +

+ + {/* Description */} +

{tagSubtype.description}

+ + {/* Number of Tags */} +
+ + {tagSubtype.tagCount} tags +
+ + {/* Navigation Link */} +
+ + View tag type page + + +
+
+
+ ); +}); diff --git a/frontend/src/components/TagCard.tsx b/frontend/src/components/TagCard.tsx index 6e7682a8..cee0d471 100644 --- a/frontend/src/components/TagCard.tsx +++ b/frontend/src/components/TagCard.tsx @@ -33,12 +33,17 @@ export const TagCard = React.forwardRef( {tag.questionCount} questions - {tag.tagType && ( -
- - {tag.tagType} -
- )} + + {tag.tagType && ( +
+ + {tag.tagType} +
+ )} +
{tag.logoImage && ( diff --git a/frontend/src/components/TagType.tsx b/frontend/src/components/TagType.tsx new file mode 100644 index 00000000..4272fcfb --- /dev/null +++ b/frontend/src/components/TagType.tsx @@ -0,0 +1,181 @@ +import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; +import { TagCard } from "@/components/TagCard"; +import { useSearchTags } from "@/services/api/programmingForumComponents"; +import { TagDetails } from "@/services/api/programmingForumSchemas"; +import ErrorAlert from "@/components/ErrorAlert"; +import InfiniteScroll from "@/components/InfiniteScroll"; + +export default function SubtypePage() { + const { typeId } = useParams<{ typeId: string }>(); + const [tags, setTags] = useState([]); + const [tagTypeId, setTagTypeId] = useState(); + + const [pageSize, setPageSize] = useState(20); + const [previousData, setPreviousData] = useState<{ + items: TagDetails[]; + totalItems: number; + }>({ items: [], totalItems: 0 }); + + const { + data: tagSearchData, + isLoading, + error, + } = useSearchTags( + { + queryParams: { q: "", pageSize }, + }, + { enabled: true }, + ); + + useEffect(() => { + if (tagSearchData?.data) { + const availableTags = (tagSearchData.data as { items: TagDetails[] }) + .items; + + // Filter tags by the typeId + const filteredTags = availableTags.filter( + (tag) => tag.tagType === typeId, + ); + + setTags(filteredTags); + + // Set the tag type name and description + if (filteredTags.length > 0) { + setTagTypeId(typeId); + //setTagDescription(`Description for ${typeId} tags.`); // Replace with actual description if available + } else { + setTagTypeId("Unknown Tag Type"); + //setTagDescription("No description available for this tag type."); + } + } + }, [tagSearchData, typeId]); + + useEffect(() => { + if (tagSearchData?.data && !isLoading) { + setPreviousData(tagSearchData.data as typeof previousData); + } + }, [tagSearchData, isLoading]); + + const searchResultData = + (tagSearchData?.data as { + items?: TagDetails[]; + totalItems?: number; + }) || previousData; + + const next = () => { + setPageSize(pageSize + 20); + }; + + //descriptions for the tag types + const getDescription = (TypeId: string) => { + switch (TypeId) { + case "Programming Language": + return ( +

+ A programming language is a formal system used to communicate + instructions to a machine, particularly a computer. It consists of + syntax, semantics, and rules that allow developers to write software + and algorithms. Programming languages enable the creation of + programs that can execute tasks ranging from simple calculations to + complex data processing and system management. Well-known examples + include Python, Java, and C++. +

+ ); + case "Programming Paradigm": + return ( +

+ A programming paradigm refers to a fundamental style or approach to + programming that influences how software is structured and + developed. It encompasses the methodologies and principles that + guide the design and implementation of programs, including + object-oriented programming, functional programming, and procedural + programming. Different paradigms offer distinct ways of thinking + about and solving problems in software development. +

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

+ A computer science term 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 + structure, machine learning, and artificial intelligence. These + terms form the foundation of communication and learning within the + computer science discipline. +

+ ); + case "Software Library": + return ( +

+ A software library is a collection of pre-written code, functions, + and resources that developers can use to perform common tasks + without having to write code from scratch. Libraries are designed to + provide reusable components for tasks such as data manipulation, + user interface design, and network communication. Popular software + libraries include jQuery for web development, TensorFlow for machine + learning, and NumPy for numerical computations. +

+ ); + } + }; + + const description = typeId ? ( + getDescription(typeId) + ) : ( +

Loading description...

+ ); + + if (error) { + return ; + } + + return ( +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {/* Header */} +

{tagTypeId}

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

+ Tags in This Category: +

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

+ No related tags found for this tag type. +

+ )} +
+
+ + )} +
+ ); +} diff --git a/frontend/src/routes/glossary.tsx b/frontend/src/routes/glossary.tsx new file mode 100644 index 00000000..d05b2453 --- /dev/null +++ b/frontend/src/routes/glossary.tsx @@ -0,0 +1,89 @@ +import { TagSubtypeCard } from "@/components/SubtypeCard"; +import { useSearchTags } from "@/services/api/programmingForumComponents"; +import { TagDetails } from "@/services/api/programmingForumSchemas"; +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import ErrorAlert from "@/components/ErrorAlert"; + +export default function GlossaryPage() { + const [tagCounts, setTagCounts] = useState< + { typeId: string; tagCount: number; description: string }[] // Added description field + >([]); + const [, setAvailableTags] = useState([]); + + const { + data: tagSearchData, + isLoading, + error, + } = useSearchTags( + { queryParams: { q: "", pageSize: 400 } }, + { enabled: true }, + ); + + useEffect(() => { + if (tagSearchData?.data) { + // Extract available tags + const tags = (tagSearchData.data as { items: TagDetails[] }).items; + setAvailableTags(tags); + + // Define the 4 tag types with descriptions from wikidata + const tagTypes = [ + { + typeId: "Programming Language", + description: "language for communicating instructions to a machine", + }, + { + typeId: "Software Library", + description: + "collection of non-volatile resources used by computer programs, often for software development", + }, + { + typeId: "Programming Paradigm", + description: + "category of programming languages according to what methodology of designing and implementing programs their features support", + }, + { + typeId: "Computer Science Topic", + description: + "technical term; word or phrase that is part of computer science terminology", + }, + ]; + + // group and count tags by tagType + const counts = tagTypes.map((type) => { + const filteredTags = tags.filter((tag) => tag.tagType === type.typeId); + return { + typeId: type.typeId, + tagCount: filteredTags.length, + description: type.description, + }; + }); + + setTagCounts(counts); + } + }, [tagSearchData]); + + if (error) { + return ; + } + + return ( +
+

Explore Various Tag Types

+
+ {tagCounts.map((tagCount) => ( + + ))} + + {isLoading && ( +
+ +
+ )} +
+
+ ); +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index cc042e4c..a89fddd0 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -13,6 +13,8 @@ import { Search } from "./search"; import Signup from "./signup"; import TagPage from "./tag"; import { BookmarkedQuestions } from "@/routes/bookmarks"; +import Glossary from "./glossary"; +import TagTypePage from "@/components/TagType"; export const routes: RouteObject[] = [ { @@ -66,6 +68,14 @@ export const routes: RouteObject[] = [ path: "/tags/new", Component: CreateTagPage, }, + { + path: "/glossary", + Component: Glossary, + }, + { + path: "/tagtype/:typeId", + Component: TagTypePage, + }, ]; export const routeConfig: RouteObject[] = [ diff --git a/frontend/src/routes/tag.test.tsx b/frontend/src/routes/tag.test.tsx index 38ce66b7..a19b0493 100644 --- a/frontend/src/routes/tag.test.tsx +++ b/frontend/src/routes/tag.test.tsx @@ -100,14 +100,14 @@ describe("TagPage", () => { token: "mock-token", }); - render( - - - } /> - - , - ); + render( + + + } /> + + , + ); - expect(screen.getByRole("button", { name: /follow/i })).toBeInTheDocument(); - }); + expect(screen.getByRole("button", { name: /follow/i })).toBeInTheDocument(); + }); }); diff --git a/frontend/src/routes/tag.tsx b/frontend/src/routes/tag.tsx index 6604933d..0fe839a6 100644 --- a/frontend/src/routes/tag.tsx +++ b/frontend/src/routes/tag.tsx @@ -106,7 +106,8 @@ export default function TagPage() { tagId: tagId!, following: tag.following, }} - />)} + /> + )}
{tag.logoImage && ( )} - {tag.tagType && ( -
- - {tag.tagType} -
- )} + + {tag.tagType && ( +
+ + {tag.tagType} +
+ )} +
From a103f21d17b18999a3e2231121fe48d2e6a28cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asl=C4=B1=20G=C3=B6k?= Date: Mon, 16 Dec 2024 17:49:53 +0300 Subject: [PATCH 36/48] add glossary test --- frontend/src/routes/glossary.test.tsx | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 frontend/src/routes/glossary.test.tsx diff --git a/frontend/src/routes/glossary.test.tsx b/frontend/src/routes/glossary.test.tsx new file mode 100644 index 00000000..3a84833e --- /dev/null +++ b/frontend/src/routes/glossary.test.tsx @@ -0,0 +1,79 @@ +// glossary.test.tsx +import { useSearchTags } from "@/services/api/programmingForumComponents"; +import { TagDetails } from "@/services/api/programmingForumSchemas"; +import { render, screen } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import GlossaryPage from "./glossary"; + +const mockTagData = vi.hoisted( + () => + ({ + tagId: "1", + name: "javascript", + description: + "A popular programming language primarily used for web development.", + questionCount: 50, + followerCount: 1000, + logoImage: "https://example.com/logo.jpg", + officialWebsite: "https://example.com", + //createdAt: "2023-01-01T00:00:00Z", + }) satisfies TagDetails, +); + +vi.mock("@/services/api/programmingForumComponents", () => ({ + useSearchTags: vi.fn(() => ({ + data: { data: { items: [mockTagData] } }, + isLoading: false, + error: null, + })), +})); + +describe("GlossaryPage", () => { + beforeEach(() => { + (useSearchTags as Mock).mockReturnValue({ + data: { data: { items: [mockTagData] } }, + isLoading: false, + error: null, + }); + }); + + it("renders glossary title correctly", () => { + render( + + + } /> + + , + ); + + // Check if glossary title is rendered + expect(screen.getByText(/Explore Various Tag Types/i)).toBeInTheDocument(); + }); + + it("renders tag counts and descriptions correctly", () => { + render( + + + } /> + + , + ); + }); + + it("renders error alert when there is an error", () => { + (useSearchTags as Mock).mockReturnValue({ + data: null, + isLoading: false, + error: new Error("Something went wrong"), + }); + + render( + + + } /> + + , + ); + }); +}); From 2922edaf7a6dc1eb5f532b32ae93c8251340d98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asl=C4=B1=20G=C3=B6k?= Date: Mon, 16 Dec 2024 17:51:42 +0300 Subject: [PATCH 37/48] update glossary.tsx --- frontend/src/routes/glossary.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/routes/glossary.tsx b/frontend/src/routes/glossary.tsx index d05b2453..7807c279 100644 --- a/frontend/src/routes/glossary.tsx +++ b/frontend/src/routes/glossary.tsx @@ -1,3 +1,4 @@ +//glossary page to display tag types import { TagSubtypeCard } from "@/components/SubtypeCard"; import { useSearchTags } from "@/services/api/programmingForumComponents"; import { TagDetails } from "@/services/api/programmingForumSchemas"; From fb0b52820b7832435d1244ee80c759c2f40258b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asl=C4=B1=20G=C3=B6k?= Date: Mon, 16 Dec 2024 17:54:34 +0300 Subject: [PATCH 38/48] add comments on changed files --- frontend/src/components/SubtypeCard.tsx | 1 + frontend/src/components/TagType.tsx | 2 +- frontend/src/routes/index.tsx | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/SubtypeCard.tsx b/frontend/src/components/SubtypeCard.tsx index 92f0c085..f8ac9a3e 100644 --- a/frontend/src/components/SubtypeCard.tsx +++ b/frontend/src/components/SubtypeCard.tsx @@ -3,6 +3,7 @@ import { ArrowRight, Tags } from "lucide-react"; import React from "react"; import { Link } from "react-router-dom"; +//new card component for tag types to display in glossary interface TagSubtypeCardProps { tagSubtype: { typeId: string; diff --git a/frontend/src/components/TagType.tsx b/frontend/src/components/TagType.tsx index 4272fcfb..8caac1e3 100644 --- a/frontend/src/components/TagType.tsx +++ b/frontend/src/components/TagType.tsx @@ -149,7 +149,7 @@ export default function SubtypePage() { {/* Render the description based on typeId */}
{description}
- {/* Tags in this Category Section */} + {/* Tags in this type */}

Tags in This Category:

diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index a89fddd0..6db81ff3 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -69,11 +69,11 @@ export const routes: RouteObject[] = [ Component: CreateTagPage, }, { - path: "/glossary", + path: "/glossary", //added glossary route Component: Glossary, }, { - path: "/tagtype/:typeId", + path: "/tagtype/:typeId", //added tagtype pages' routes Component: TagTypePage, }, ]; From 13744f8c9cf516db2936e0f4b691be26932467df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Ya=C5=9Far?= Date: Mon, 16 Dec 2024 18:14:18 +0300 Subject: [PATCH 39/48] feat(mobile): update user profile components --- mobile/app/(tabs)/profile.tsx | 333 +--------------------------- mobile/app/users/[userId].tsx | 13 +- mobile/components/UserProfile.tsx | 346 ++++++++++++++++++++++++++++++ 3 files changed, 353 insertions(+), 339 deletions(-) create mode 100644 mobile/components/UserProfile.tsx diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx index 27e78e3c..812022f9 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -1,336 +1,5 @@ -import ErrorAlert from "@/components/ErrorAlert"; -import FollowUserButton from "@/components/FollowUserButton"; -import { FullscreenLoading } from "@/components/FullscreenLoading"; -import { - Badge, - BadgeText, - Button, - ButtonText, - HStack, - Icon, - Image, - Input, - InputField, - Pressable, - ScrollView, - Select, - SelectBackdrop, - SelectContent, - SelectDragIndicator, - SelectDragIndicatorWrapper, - SelectIcon, - SelectInput, - SelectItem, - SelectPortal, - SelectTrigger, - Text, - Textarea, - TextareaInput, - VStack, -} from "@/components/ui"; -import { Menu, MenuItem, MenuItemLabel, MenuSeparator } from "@/components/ui/menu"; - -import { - useGetUserProfile, - useUpdateUserProfile, -} from "@/services/api/programmingForumComponents"; -import { ExperienceLevel, QuestionSummary } from "@/services/api/programmingForumSchemas"; -import useAuthStore from "@/services/auth"; -import { Link, router } from "expo-router"; -import { ChevronDownIcon, Plus, Bookmark, MenuIcon, LogOutIcon } from "lucide-react-native"; -import { useEffect, useState } from "react"; -import placeholderProfile from "@/assets/images/placeholder_profile.png"; -import { QuestionList } from "@/components/QuestionsList"; +import UserProfile from "@/components/UserProfile"; export default function Profile() { return ; } - -export function UserProfile({ userId }: { userId: string }) { - const { selfProfile, fetchProfile } = useAuthStore(); - const me = userId === "me" || userId === selfProfile?.id?.toString(); - const [editing, setEditing] = useState(false); - const [activeTab, setActiveTab] = useState<"questions" | "answers">( - "questions" - ); - - const [country, setCountry] = useState(""); - const [bio, setBio] = useState(""); - const [experienceLevel, setExperienceLevel] = - useState("BEGINNER"); - - useEffect(() => { - if (!me) { - setEditing(false); - } - }, [me]); - - const { isLoading, data, error, refetch } = useGetUserProfile( - { - pathParams: { - userId: me ? ("me" as unknown as number) : parseInt(userId || "0"), - }, - }, - { - enabled: me || !isNaN(Number(userId)), - } - ); - - const { mutateAsync, isPending } = useUpdateUserProfile({ - onSuccess: () => { - fetchProfile(); - refetch().then(() => { - setEditing(false); - }); - }, - onError: () => { - setEditing(false); - }, - }); - - useEffect(() => { - if (data?.data) { - setCountry(data.data.country || ""); - setBio(data.data.bio || ""); - setExperienceLevel(data.data.experienceLevel || "BEGINNER"); - } - }, [data?.data]); - - if (!me && isNaN(Number(userId))) { - return Invalid user id; - } - if (isLoading) { - return ; - } - if (error) { - return ; - } - - const profile = data!.data; - - return ( - - - - - {me ? "My Profile" : "Profile"} - - - { - return ( - - - ) - }} - > - router.push(`me/bookmarks`)}> - - Bookmarks - - - - - router.push(`/logout`)}> - - Logout - - - - - - - - - {`Profile - - - {profile.questionCount} - Questions - - - {profile.answerCount} - Answers - - - {profile.followersCount} - Followers - - - {profile.followingCount} - Following - - - - - - {editing ? ( - <> - - - - - - - ) : ( - <> - {profile.username} - - {profile.bio ?? "Empty bio."} - - - Experience: {profile.experienceLevel?.toString() || "Unknown"} - - - Reputation Points:{" "} - 0 ? "text-green-700" : "text-gray-500" - } - > - {(profile.reputationPoints ?? 0) > 0 ? `+${profile.reputationPoints ?? 0}` : profile.reputationPoints ?? 0} - - - - - {profile.followedTags?.map((tag) => ( - router.push(`/tags/${tag.id}`)}> - - {tag.name} - - - ))} - - - - )} - - {me ? ( - editing ? ( - - ) : ( - - ) - ) : ( - data?.data && - )} - - { - - - - } - - {activeTab === "questions" ? ( - - - Questions - {me && ( - - - - )} - - { - return (b.upvoteCount - b.downvoteCount) - (a.upvoteCount - a.downvoteCount); - }) || [] - } - /> - - ) : ( - ({ - id: answer.question.id ?? 0, - title: answer.question.title ?? "", - content: answer.content ?? "", - upvoteCount: answer.upvoteCount, - downvoteCount: answer.downvoteCount, - } as QuestionSummary)) || [] } - /> - )} - - - ); -} diff --git a/mobile/app/users/[userId].tsx b/mobile/app/users/[userId].tsx index 955e6a20..1ba25945 100644 --- a/mobile/app/users/[userId].tsx +++ b/mobile/app/users/[userId].tsx @@ -1,8 +1,7 @@ -import { Text } from "@/components/ui"; -import { View } from "react-native"; +import UserProfile from "@/components/UserProfile"; +import { useLocalSearchParams } from "expo-router"; -export default function User() { - return - User - -} +export default function ProfilePage() { + const { userId } = useLocalSearchParams<{ userId: string }>(); + return ; +} \ No newline at end of file diff --git a/mobile/components/UserProfile.tsx b/mobile/components/UserProfile.tsx new file mode 100644 index 00000000..26b1eff9 --- /dev/null +++ b/mobile/components/UserProfile.tsx @@ -0,0 +1,346 @@ +import ErrorAlert from "@/components/ErrorAlert"; +import FollowUserButton from "@/components/FollowUserButton"; +import { FullscreenLoading } from "@/components/FullscreenLoading"; +import { + Badge, + BadgeText, + Button, + ButtonText, + HStack, + Icon, + Image, + Input, + InputField, + Pressable, + ScrollView, + Select, + SelectBackdrop, + SelectContent, + SelectDragIndicator, + SelectDragIndicatorWrapper, + SelectIcon, + SelectInput, + SelectItem, + SelectPortal, + SelectTrigger, + Text, + Textarea, + TextareaInput, + VStack, +} from "@/components/ui"; +import { Menu, MenuItem, MenuItemLabel, MenuSeparator } from "@/components/ui/menu"; + +import { + useGetUserProfile, + useUpdateUserProfile, +} from "@/services/api/programmingForumComponents"; +import { ExperienceLevel, QuestionSummary } from "@/services/api/programmingForumSchemas"; +import useAuthStore from "@/services/auth"; +import { Link, router } from "expo-router"; +import { ChevronDownIcon, Plus, Bookmark, MenuIcon, LogOutIcon, ArrowLeftIcon } from "lucide-react-native"; +import { useEffect, useState } from "react"; +import placeholderProfile from "@/assets/images/placeholder_profile.png"; +import { QuestionList } from "@/components/QuestionsList"; + +export default function UserProfile({ userId }: { userId: string }) { + const { selfProfile, fetchProfile } = useAuthStore(); + const me = userId === "me" || userId === selfProfile?.id?.toString(); + const [editing, setEditing] = useState(false); + const [activeTab, setActiveTab] = useState<"questions" | "answers">( + "questions" + ); + + const [country, setCountry] = useState(""); + const [bio, setBio] = useState(""); + const [experienceLevel, setExperienceLevel] = + useState("BEGINNER"); + + useEffect(() => { + if (!me) { + setEditing(false); + } + }, [me]); + + const { isLoading, data, error, refetch } = useGetUserProfile( + { + pathParams: { + userId: me ? ("me" as unknown as number) : parseInt(userId || "0"), + }, + }, + { + enabled: me || !isNaN(Number(userId)), + } + ); + + const { mutateAsync, isPending } = useUpdateUserProfile({ + onSuccess: () => { + fetchProfile(); + refetch().then(() => { + setEditing(false); + }); + }, + onError: () => { + setEditing(false); + }, + }); + + useEffect(() => { + if (data?.data) { + setCountry(data.data.country || ""); + setBio(data.data.bio || ""); + setExperienceLevel(data.data.experienceLevel || "BEGINNER"); + } + }, [data?.data]); + + if (!me && isNaN(Number(userId))) { + return Invalid user id; + } + if (isLoading) { + return ; + } + if (error) { + return ; + } + + const profile = data!.data; + + return ( + + + + + + {me ? "My Profile" : profile.username + "'s Profile" } + + + {me ? ( + { + return ( + + + ) + }} + > + router.push(`me/bookmarks`)}> + + Bookmarks + + + + + router.push(`/logout`)}> + + Logout + + + ) : + + } + + + + + + {`Profile + + + {profile.questionCount} + Questions + + + {profile.answerCount} + Answers + + + {profile.followersCount} + Followers + + + {profile.followingCount} + Following + + + + + + {editing ? ( + <> + + + + + + + ) : ( + <> + {profile.firstName + " " + profile.lastName} + + {profile.bio ?? "Empty bio."} + + + Experience: {profile.experienceLevel?.toString() || "Unknown"} + + + Reputation Points:{" "} + 0 ? "text-green-700" : "text-gray-500" + } + > + {(profile.reputationPoints ?? 0) > 0 ? `+${profile.reputationPoints ?? 0}` : profile.reputationPoints ?? 0} + + + + + {profile.followedTags?.map((tag) => ( + router.push(`/tags/${tag.id}`)}> + + {tag.name} + + + ))} + + + + )} + + {me ? ( + editing ? ( + + ) : ( + + ) + ) : ( + profile && + )} + + { + + + + } + + {activeTab === "questions" ? ( + + + Questions + {me && ( + + + + )} + + { + return (b.upvoteCount - b.downvoteCount) - (a.upvoteCount - a.downvoteCount); + }) || [] + } + /> + + ) : ( + + Answers + ({ + id: answer.question.id ?? 0, + title: answer.question.title ?? "", + content: answer.content ?? "", + upvoteCount: answer.upvoteCount, + downvoteCount: answer.downvoteCount, + } as QuestionSummary)) || [] } + /> + + )} + + + ); +} From 0c1d2c9b5b3ccc008601c520f624cc13554ac891 Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 18:22:44 +0300 Subject: [PATCH 40/48] Update sortBy parameter and conditions --- .../programminglanguagesforum/Services/QuestionService.java | 3 +-- frontend/src/components/SearchQuestionsList.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) 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 e4bc9b81..6408b490 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java @@ -236,10 +236,9 @@ public Page searchQuestions( } PageRequest pageable = PageRequest.of(page - 1, pageSize); - if (Objects.equals(sortBy, "default") || Objects.equals(currentUser, null)) { + 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 diff --git a/frontend/src/components/SearchQuestionsList.tsx b/frontend/src/components/SearchQuestionsList.tsx index b5dc4046..e93d703c 100644 --- a/frontend/src/components/SearchQuestionsList.tsx +++ b/frontend/src/components/SearchQuestionsList.tsx @@ -32,6 +32,7 @@ export const SearchQuestionsList = () => { q: params.get("q") ?? "", pageSize, ...(difficulty && { difficulty }), + sortBy: params.get("sortBy") ?? "recommended" }, }); From 0cc94c68b95acbf9fdf6e1c33446ebb841f2673b Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 18:25:39 +0300 Subject: [PATCH 41/48] Fix typo --- .../programminglanguagesforum/Services/QuestionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6408b490..35efcf9e 100644 --- a/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java +++ b/backend/src/main/java/com/group1/programminglanguagesforum/Services/QuestionService.java @@ -236,7 +236,7 @@ public Page searchQuestions( } PageRequest pageable = PageRequest.of(page - 1, pageSize); - if (Objects.equals(sortBy, "default") || Objects.equals(sortBy, null) || Objects.equals(currentUser, null) || ) { + 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() From 5a522b45707519e53d788ef7e7233c1e18cf51de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Mon, 16 Dec 2024 18:28:39 +0300 Subject: [PATCH 42/48] fix(frontend): bookmarks not being shown also changed the tag subtype page title to look nicer --- frontend/src/components/TagType.tsx | 12 +- frontend/src/routes/bookmarks.test.tsx | 211 +++++++++++++----------- frontend/src/routes/bookmarks.tsx | 80 +++------ frontend/src/services/temporaryMocks.ts | 6 +- 4 files changed, 146 insertions(+), 163 deletions(-) diff --git a/frontend/src/components/TagType.tsx b/frontend/src/components/TagType.tsx index 4272fcfb..2bb5fae2 100644 --- a/frontend/src/components/TagType.tsx +++ b/frontend/src/components/TagType.tsx @@ -1,11 +1,11 @@ -import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; -import { Loader2 } from "lucide-react"; +import ErrorAlert from "@/components/ErrorAlert"; +import InfiniteScroll from "@/components/InfiniteScroll"; import { TagCard } from "@/components/TagCard"; import { useSearchTags } from "@/services/api/programmingForumComponents"; import { TagDetails } from "@/services/api/programmingForumSchemas"; -import ErrorAlert from "@/components/ErrorAlert"; -import InfiniteScroll from "@/components/InfiniteScroll"; +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; export default function SubtypePage() { const { typeId } = useParams<{ typeId: string }>(); @@ -151,7 +151,7 @@ export default function SubtypePage() { {/* Tags in this Category Section */}

- Tags in This Category: + Tags in Category

{/* Infinite Scroll for displaying Related Tags */} diff --git a/frontend/src/routes/bookmarks.test.tsx b/frontend/src/routes/bookmarks.test.tsx index 2d0917bf..4514de02 100644 --- a/frontend/src/routes/bookmarks.test.tsx +++ b/frontend/src/routes/bookmarks.test.tsx @@ -1,107 +1,120 @@ import { - GetBookmarkedQuestionsError, - useGetBookmarkedQuestions, - } from "@/services/api/programmingForumComponents"; - import { QuestionDetails } from "@/services/api/programmingForumSchemas"; - import { testAccessibility } from "@/utils/test-accessibility"; - import { QueryObserverSuccessResult } from "@tanstack/react-query"; - import { render, screen } from "@testing-library/react"; - import { - createMemoryRouter, - MemoryRouter, - Route, - RouterProvider, - Routes, - } from "react-router-dom"; - import { beforeEach, describe, expect, it, vi } from "vitest"; - import { routeConfig } from "."; - import { BookmarkedQuestions } from "./bookmarks"; - - // Mock the useGetBookmarkedQuestions hook - vi.mock("@/services/api/programmingForumComponents", () => ({ - useGetBookmarkedQuestions: vi.fn(), - })); - - const mockQuestions: QuestionDetails[] = [ - { + GetBookmarkedQuestionsError, + useGetBookmarkedQuestions, +} from "@/services/api/programmingForumComponents"; +import { QuestionDetails } from "@/services/api/programmingForumSchemas"; +import { testAccessibility } from "@/utils/test-accessibility"; +import { QueryObserverSuccessResult } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import { + createMemoryRouter, + MemoryRouter, + Route, + RouterProvider, + Routes, +} from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { routeConfig } from "."; +import { BookmarkedQuestions } from "./bookmarks"; + +// Mock the useGetBookmarkedQuestions hook +vi.mock("@/services/api/programmingForumComponents", () => ({ + useGetBookmarkedQuestions: vi.fn(), +})); + +const mockQuestions: QuestionDetails[] = [ + { + id: 1, + title: "How to implement a binary tree in Python?", + content: "I'm struggling to understand the structure...", + author: { id: 1, - title: "How to implement a binary tree in Python?", - content: "I'm struggling to understand the structure...", - author: { id: 1, name: "John Doe", username: "user1", profilePicture: "p", reputationPoints: 50}, - createdAt: "2024-12-01T12:00:00Z", - updatedAt: "2024-12-01T12:30:00Z", - tags: [{ id: "1", name: "Python" }], - likeCount: 10, - dislikeCount: 2, - commentCount: 4, - viewCount: 50, - bookmarked: true, - selfVoted: 1, - difficulty: "MEDIUM", - selfDifficultyVote: "MEDIUM", - easyCount: 5, - mediumCount: 10, - hardCount: 3, + name: "John Doe", + username: "user1", + profilePicture: "p", + reputationPoints: 50, }, - { + createdAt: "2024-12-01T12:00:00Z", + updatedAt: "2024-12-01T12:30:00Z", + tags: [{ id: "1", name: "Python" }], + likeCount: 10, + dislikeCount: 2, + commentCount: 4, + viewCount: 50, + bookmarked: true, + selfVoted: 1, + difficulty: "MEDIUM", + selfDifficultyVote: "MEDIUM", + easyCount: 5, + mediumCount: 10, + hardCount: 3, + }, + { + id: 2, + title: "What are closures in JavaScript?", + content: "Can someone explain closures with an example?", + author: { id: 2, - title: "What are closures in JavaScript?", - content: "Can someone explain closures with an example?", - author: { id: 2, name: "Jane Smith", username: "user2", profilePicture: "p", reputationPoints: 50}, - createdAt: "2024-12-02T10:00:00Z", - updatedAt: "2024-12-02T10:20:00Z", - tags: [{ id: "2", name: "JavaScript" }], - likeCount: 15, - dislikeCount: 1, - commentCount: 5, - viewCount: 70, - bookmarked: true, - selfVoted: 0, - difficulty: "EASY", - selfDifficultyVote: "EASY", - easyCount: 8, - mediumCount: 6, - hardCount: 1, + name: "Jane Smith", + username: "user2", + profilePicture: "p", + reputationPoints: 50, }, - ]; - - describe("BookmarkedQuestions component", () => { - beforeEach(() => { - vi.mocked(useGetBookmarkedQuestions).mockReset(); - }); - - it("should have no accessibility violations", async () => { - const router = createMemoryRouter(routeConfig, { - initialEntries: ["/bookmarks"], - }); - - await testAccessibility(); + createdAt: "2024-12-02T10:00:00Z", + updatedAt: "2024-12-02T10:20:00Z", + tags: [{ id: "2", name: "JavaScript" }], + likeCount: 15, + dislikeCount: 1, + commentCount: 5, + viewCount: 70, + bookmarked: true, + selfVoted: 0, + difficulty: "EASY", + selfDifficultyVote: "EASY", + easyCount: 8, + mediumCount: 6, + hardCount: 1, + }, +]; + +describe("BookmarkedQuestions component", () => { + beforeEach(() => { + vi.mocked(useGetBookmarkedQuestions).mockReset(); + }); + + it("should have no accessibility violations", async () => { + const router = createMemoryRouter(routeConfig, { + initialEntries: ["/bookmarks"], }); - - it("renders bookmarked questions correctly", () => { - vi.mocked(useGetBookmarkedQuestions).mockReturnValue({ - isLoading: false, - error: null, - data: { - data: { items: mockQuestions, totalItems: mockQuestions.length }, - }, - } as QueryObserverSuccessResult); - - render( - - - } /> - - , - ); - - expect( - screen.getByText(`You have ${mockQuestions.length} bookmarked questions.`), - ).toBeInTheDocument(); - - mockQuestions.forEach((question) => { - expect(screen.getByText(question.title)).toBeInTheDocument(); - }); + + await testAccessibility(); + }); + + it("renders bookmarked questions correctly", () => { + vi.mocked(useGetBookmarkedQuestions).mockReturnValue({ + isLoading: false, + error: null, + data: { + data: mockQuestions, + }, + } as QueryObserverSuccessResult); + + render( + + + } /> + + , + ); + + expect( + screen.getByText( + `You have ${mockQuestions.length} bookmarked questions.`, + ), + ).toBeInTheDocument(); + + mockQuestions.forEach((question) => { + expect(screen.getByText(question.title)).toBeInTheDocument(); }); }); - \ No newline at end of file +}); diff --git a/frontend/src/routes/bookmarks.tsx b/frontend/src/routes/bookmarks.tsx index 9be92871..b7ea52f5 100644 --- a/frontend/src/routes/bookmarks.tsx +++ b/frontend/src/routes/bookmarks.tsx @@ -1,28 +1,14 @@ import { useGetBookmarkedQuestions } from "@/services/api/programmingForumComponents"; -import { - QuestionSummary, -} from "@/services/api/programmingForumSchemas"; +import { QuestionSummary } from "@/services/api/programmingForumSchemas"; import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; import ErrorAlert from "../components/ErrorAlert"; -import InfiniteScroll from "../components/InfiniteScroll"; import { QuestionCard } from "../components/QuestionCard"; export const BookmarkedQuestions = () => { - const [pageSize, setPageSize] = useState(20); - const [previousData, setPreviousData] = useState<{ - items: QuestionSummary[]; - totalItems: number; - }>({ - items: [], - totalItems: 0, - }); + const [previousData, setPreviousData] = useState([]); - const { - data: resultList, - isLoading, - error, - } = useGetBookmarkedQuestions({}); + const { data: resultList, isLoading, error } = useGetBookmarkedQuestions({}); useEffect(() => { if (resultList?.data && !isLoading) { @@ -36,56 +22,40 @@ export const BookmarkedQuestions = () => { const resultListData = (resultList?.data as typeof previousData) || previousData; - const questions = resultListData.items || []; - - const next = () => { - setPageSize(pageSize + 20); - }; + const questions = resultListData || []; return (

{questions.length - ? `You have ${resultListData.totalItems} bookmarked questions.` + ? `You have ${resultListData.length} bookmarked questions.` : "You haven't bookmarked any questions."}

- {!questions.length && ( -

Bookmark questions to view them here.

- )} + {!questions.length &&

Bookmark questions to view them here.

}
- pageSize - : false - } - isLoading={isLoading} - > - {questions.map((question) => ( - - ))} - + {questions.map((question) => ( + + ))}
{isLoading && (
diff --git a/frontend/src/services/temporaryMocks.ts b/frontend/src/services/temporaryMocks.ts index 9b9014b7..e8e06cba 100644 --- a/frontend/src/services/temporaryMocks.ts +++ b/frontend/src/services/temporaryMocks.ts @@ -28,7 +28,7 @@ export const temporaryMocks = { id: 2, username: "john_doe", reputationPoints: 100, - profilePicture: "frontend\src\assets\placeholder_profile.png", + profilePicture: "frontend\\src\\assets\\placeholder_profile.png", name: "John Doe", experienceLevel: "INTERMEDIATE", }, @@ -62,7 +62,7 @@ export const temporaryMocks = { id: 1, username: "jane_doe", reputationPoints: 150, - profilePicture: "frontend\src\assets\placeholder_profile.png", + profilePicture: "frontend\\src\\assets\\placeholder_profile.png", name: "Jane Doe", experienceLevel: "EXPERT", }, @@ -83,7 +83,7 @@ export const temporaryMocks = { id: 2, username: "john_doe", reputationPoints: 100, - profilePicture: "frontend\src\assets\placeholder_profile.png", + profilePicture: "frontend\\src\\assets\\placeholder_profile.png", name: "John Doe", experienceLevel: "INTERMEDIATE", }, From e3493f1fa336b1a22da6299b38cb3470b02efdfc Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 18:37:12 +0300 Subject: [PATCH 43/48] Add loading while taking long --- frontend/src/components/FullscreenLoading.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/FullscreenLoading.tsx b/frontend/src/components/FullscreenLoading.tsx index 4d815ee7..b5de17bc 100644 --- a/frontend/src/components/FullscreenLoading.tsx +++ b/frontend/src/components/FullscreenLoading.tsx @@ -33,6 +33,7 @@ export const FullscreenLoading = ({ )} {takingLong && (
+ Loading... This is taking a while...
)} From 48fb101ceb12d21a4e11e939c15cbd8279743b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Mon, 16 Dec 2024 18:45:54 +0300 Subject: [PATCH 44/48] feat(frontend): use recommended questions in the home page --- frontend/src/routes/feed.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/feed.tsx b/frontend/src/routes/feed.tsx index 9020665f..e4b04488 100644 --- a/frontend/src/routes/feed.tsx +++ b/frontend/src/routes/feed.tsx @@ -34,7 +34,11 @@ export const Feed = () => { isLoading: isQuestionsLoading, error: questionsError, } = useSearchQuestions({ - queryParams: { q: params.get("q") ?? "" }, // Fetch default questions + queryParams: { + pageSize: 9, + q: params.get("q") ?? "", + sortBy: "recommended", + }, // Fetch default questions }); // Fetch related exercises using your useExercismSearch hook @@ -105,7 +109,7 @@ export const Feed = () => { {/* Questions Section */}
-

Latest Questions

+

Recommended Questions

{questions.length === 0 ? ( From b803c0fed5fae1fd957a04ed9a2e66ba2f877a2d Mon Sep 17 00:00:00 2001 From: ozdentarikcan Date: Mon, 16 Dec 2024 19:04:58 +0300 Subject: [PATCH 45/48] Add loading screen --- frontend/src/components/TagType.tsx | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/TagType.tsx b/frontend/src/components/TagType.tsx index fa5d1cf9..e32f945c 100644 --- a/frontend/src/components/TagType.tsx +++ b/frontend/src/components/TagType.tsx @@ -134,15 +134,6 @@ export default function SubtypePage() { return (
- {isLoading ? ( -
- -
- ) : ( - <> {/* Header */}

{tagTypeId}

@@ -165,17 +156,18 @@ export default function SubtypePage() { } isLoading={isLoading} > - {tags.length > 0 ? ( - tags.map((tag) => ) - ) : ( -

- No related tags found for this tag type. -

- )} + {tags?.map((tag) => ) + } + {isLoading && ( +
+ +
+ )}
- - )}
); } From c2b233394a91d32d98b44ff2f10b7347af31eee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Mon, 16 Dec 2024 19:07:16 +0300 Subject: [PATCH 46/48] feat(frontend): improve UX for difficulty bar --- frontend/package.json | 3 + frontend/src/components/Answers.tsx | 7 + frontend/src/components/DifficultyBar.tsx | 348 +++++++++++--------- frontend/src/components/ui/collapsible.tsx | 9 + frontend/src/components/ui/toggle-group.tsx | 59 ++++ frontend/src/components/ui/toggle.tsx | 45 +++ frontend/src/routes/question.tsx | 99 +++--- frontend/yarn.lock | 198 +++++++++++ 8 files changed, 564 insertions(+), 204 deletions(-) create mode 100644 frontend/src/components/ui/collapsible.tsx create mode 100644 frontend/src/components/ui/toggle-group.tsx create mode 100644 frontend/src/components/ui/toggle.tsx diff --git a/frontend/package.json b/frontend/package.json index 677a9431..687a520e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -33,6 +34,8 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-toggle": "^1.1.1", + "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", "@tailwindcss/typography": "^0.5.15", "@tanstack/react-query": "^5.35.1", diff --git a/frontend/src/components/Answers.tsx b/frontend/src/components/Answers.tsx index fc9e42e7..5c490211 100644 --- a/frontend/src/components/Answers.tsx +++ b/frontend/src/components/Answers.tsx @@ -69,6 +69,13 @@ export function Answers({ questionId }: AnswersProps) { onDownvote={() => handleVote(answer.id, -1)} /> ))} + {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 80f6125b..e4f5007a 100644 --- a/frontend/src/components/DifficultyBar.tsx +++ b/frontend/src/components/DifficultyBar.tsx @@ -1,161 +1,187 @@ -import React, {useState} from "react"; -import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip"; -import { useRateQuestion} from "@/services/api/programmingForumComponents"; -import { Button } from "@/components/ui/button"; -import useAuthStore from "@/services/auth"; - - -type DifficultyBarProps = { - easyCount: number; - mediumCount: number; - hardCount: number; - questionId: number; -}; - -export const DifficultyBar: React.FC = ({ - easyCount, - mediumCount, - hardCount, - questionId, -}) => { - const { token } = useAuthStore(); // Fetch authentication status - const [votedDifficulty, setVotedDifficulty] = useState(null); // Track user's vote - const [localCounts, setLocalCounts] = useState({ - easy: easyCount, - medium: mediumCount, - hard: hardCount, - }); - - const { mutateAsync: rateQuestion } = useRateQuestion(); - - // Helper to calculate percentage - const calculatePercentage = (count: number) => - totalVotes > 0 ? ((count / totalVotes) * 100).toFixed(1) : "0"; - - const handleVote = async (difficulty: "EASY" | "MEDIUM" | "HARD") => { - if (!token) { - alert("You must be logged in to vote!"); - return; - } - - try { - const response = await rateQuestion({ - pathParams: { id: questionId }, - body: { difficulty: difficulty }, - }); - - const { easyCount, mediumCount, hardCount } = response.data; - - setLocalCounts({ - easy: easyCount ?? 0, - medium: mediumCount ?? 0, - hard: hardCount ?? 0, - }); - - setVotedDifficulty(difficulty); - } catch (error) { - console.error("Failed to vote:", error); - alert("There was an issue submitting your vote. Please try again."); - } - }; - - const totalVotes = localCounts.easy + localCounts.medium + localCounts.hard; - - const getHighestVotedDifficulty = () => { - const counts = [ - { level: "Easy", count: localCounts.easy }, - { level: "Medium", count: localCounts.medium }, - { level: "Hard", count: localCounts.hard }, - ]; - const highest = counts.reduce((prev, current) => //any equality -> pick higher difficulty level - prev.count > current.count ? prev : current - ); - return highest.level; - }; - - return ( -
- - {/* Voting Form */} -
-

Vote for Difficulty Level:

-
- - - -
-
- - {/* Difficulty Bar */} -
-

- The community finds this question: {getHighestVotedDifficulty()} difficulty. -

-
- Easy: {localCounts.easy} votes,{" "} - Medium: {localCounts.medium} votes,{" "} - Hard: {localCounts.hard} votes -
-
- {/* Easy Section */} - - -
-
- - {localCounts.easy} Easy votes - -
- - {/* Medium Section */} - - -
-
- - {localCounts.medium} Medium votes - -
- - {/* Hard Section */} - - -
-
- - {localCounts.hard} Hard votes - -
-
-
-
-
- ); -}; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useRateQuestion } from "@/services/api/programmingForumComponents"; +import useAuthStore from "@/services/auth"; +import React, { useState } from "react"; +import { Card } from "./ui/card"; +import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group"; + +type DifficultyBarProps = { + easyCount: number; + mediumCount: number; + hardCount: number; + questionId: number; +}; + +export const DifficultyBar: React.FC = ({ + easyCount, + mediumCount, + hardCount, + questionId, +}) => { + const { token } = useAuthStore(); // Fetch authentication status + const [votedDifficulty, setVotedDifficulty] = useState(null); // Track user's vote + const [localCounts, setLocalCounts] = useState({ + easy: easyCount, + medium: mediumCount, + hard: hardCount, + }); + + const { mutateAsync: rateQuestion } = useRateQuestion(); + + // Helper to calculate percentage + const calculatePercentage = (count: number) => + totalVotes > 0 ? ((count / totalVotes) * 100).toFixed(1) : "0"; + + const handleVote = async (difficulty: "EASY" | "MEDIUM" | "HARD") => { + if (!token) { + alert("You must be logged in to vote!"); + return; + } + + try { + const response = await rateQuestion({ + pathParams: { id: questionId }, + body: { difficulty: difficulty }, + }); + + const { easyCount, mediumCount, hardCount } = response.data; + + setLocalCounts({ + easy: easyCount ?? 0, + medium: mediumCount ?? 0, + hard: hardCount ?? 0, + }); + + setVotedDifficulty(difficulty); + } catch (error) { + console.error("Failed to vote:", error); + alert("There was an issue submitting your vote. Please try again."); + } + }; + + const totalVotes = localCounts.easy + localCounts.medium + localCounts.hard; + + const getHighestVotedDifficulty = () => { + const counts = [ + { level: "Easy", count: localCounts.easy }, + { level: "Medium", count: localCounts.medium }, + { level: "Hard", count: localCounts.hard }, + ]; + const highest = counts.reduce( + ( + prev, + current, //any equality -> pick higher difficulty level + ) => (prev.count > current.count ? prev : current), + ); + return highest.level; + }; + + return ( + +

Difficulty voting

+ + {/* Voting Form */} +
+ Vote for Difficulty Level: + + val && handleVote(val as "EASY" | "MEDIUM" | "HARD") + } + type="single" + size="lg" + variant="outline" + > + + Easy + + + Medium + + + Hard + + +
+ + {/* Difficulty Bar */} +
+

+ The community finds this question:{" "} + {getHighestVotedDifficulty()} difficulty. +

+
+ Easy: {localCounts.easy} votes,{" "} + Medium: {localCounts.medium} votes,{" "} + Hard: {localCounts.hard} votes +
+
+ {/* Easy Section */} + + +
+
+ {localCounts.easy} Easy votes +
+ + {/* Medium Section */} + + +
+
+ {localCounts.medium} Medium votes +
+ + {/* Hard Section */} + + +
+
+ {localCounts.hard} Hard votes +
+
+
+
+
+ ); +}; diff --git a/frontend/src/components/ui/collapsible.tsx b/frontend/src/components/ui/collapsible.tsx new file mode 100644 index 00000000..5c28cbcc --- /dev/null +++ b/frontend/src/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/frontend/src/components/ui/toggle-group.tsx b/frontend/src/components/ui/toggle-group.tsx new file mode 100644 index 00000000..35ba3feb --- /dev/null +++ b/frontend/src/components/ui/toggle-group.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; +import { type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; +import { toggleVariants } from "@/components/ui/toggle"; + +const ToggleGroupContext = React.createContext< + VariantProps +>({ + size: "default", + variant: "default", +}); + +const ToggleGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, children, ...props }, ref) => ( + + + {children} + + +)); + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; + +const ToggleGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, children, variant, size, ...props }, ref) => { + const context = React.useContext(ToggleGroupContext); + + return ( + + {children} + + ); +}); + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; + +export { ToggleGroup, ToggleGroupItem }; diff --git a/frontend/src/components/ui/toggle.tsx b/frontend/src/components/ui/toggle.tsx new file mode 100644 index 00000000..b45b9084 --- /dev/null +++ b/frontend/src/components/ui/toggle.tsx @@ -0,0 +1,45 @@ +"use client"; + +import * as React from "react"; +import * as TogglePrimitive from "@radix-ui/react-toggle"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const toggleVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-10 px-3 min-w-10", + sm: "h-9 px-2.5 min-w-9", + lg: "h-11 px-5 min-w-11", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +const Toggle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, ...props }, ref) => ( + +)); + +Toggle.displayName = TogglePrimitive.Root.displayName; + +export { Toggle, toggleVariants }; diff --git a/frontend/src/routes/question.tsx b/frontend/src/routes/question.tsx index 35f892f6..d4a54336 100644 --- a/frontend/src/routes/question.tsx +++ b/frontend/src/routes/question.tsx @@ -1,34 +1,34 @@ import LinkIcon from "@/assets/Icon/General/Link.svg?react"; +import placeholderProfile from "@/assets/placeholder_profile.png"; import { Answers } from "@/components/Answers"; +import BookmarkButton from "@/components/BookmarkButton"; import { ContentWithSnippets } from "@/components/ContentWithSnippets"; import { CreateAnswerForm } from "@/components/CreateAnswerForm"; import { DifficultyBar } from "@/components/DifficultyBar"; import ErrorAlert from "@/components/ErrorAlert"; import { ExerciseCard } from "@/components/ExerciseCard"; import FollowButton from "@/components/FollowButton"; -import BookmarkButton from "@/components/BookmarkButton"; import { FullscreenLoading } from "@/components/FullscreenLoading"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { toast } from "@/components/ui/use-toast"; import { TagDetails } from "@/services/api/programmingForumSchemas"; -import placeholderProfile from "@/assets/placeholder_profile.png"; +import { MultiSelect } from "@/components/multi-select"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { useDeleteQuestion as useDeleteQuestionById, useDownvoteQuestion, useGetQuestionDetails, - useUpvoteQuestion, - useUpdateQuestion, useSearchTags, + useUpdateQuestion, + useUpvoteQuestion, } from "@/services/api/programmingForumComponents"; -import { MultiSelect } from "@/components/multi-select"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; import useAuthStore from "@/services/auth"; import { convertTagToTrack, useExercismSearch } from "@/services/exercism"; import { Flag, MessageSquare, ThumbsDown, ThumbsUp, Trash } from "lucide-react"; -import { useEffect,useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Link, useParams } from "react-router-dom"; export default function QuestionPage() { @@ -97,25 +97,27 @@ export default function QuestionPage() { { queryParams: { q: "", pageSize: 1000 } }, { enabled: true }, ); - + useEffect(() => { if (tagSearchData?.data) { const tagsData = (tagSearchData.data as { items: TagDetails[] }).items; setAvailableTags(tagsData); } }, [tagSearchData]); - - const question = data! || {}; const [isEditing, setIsEditing] = useState(false); // To toggle edit mode const [isPreviewMode, setIsPreviewMode] = useState(false); // Preview toggle for description - + const titleRef = useRef(null); const contentRef = useRef(null); - const [tags, setTags] = useState(question.tags?.map((tag) => Number(tag.id)) || []); // Tag IDs state - const [availableTags, setAvailableTags] = useState<{ tagId: string; name: string }[]>([]); // Available tags + const [tags, setTags] = useState( + question.tags?.map((tag) => Number(tag.id)) || [], + ); // Tag IDs state + const [availableTags, setAvailableTags] = useState< + { tagId: string; name: string }[] + >([]); // Available tags const { mutateAsync: updateQuestion, isPending } = useUpdateQuestion({ onSuccess: () => { @@ -123,7 +125,6 @@ export default function QuestionPage() { setIsEditing(false); }, }); - const saveChanges = async () => { try { @@ -141,11 +142,9 @@ export default function QuestionPage() { description: "The question has been updated successfully.", }); setIsEditing(false); - } catch (err) {console.error( - "Failed to save changes", - err - ); - toast({ + } catch (err) { + console.error("Failed to save changes", err); + toast({ variant: "destructive", title: "Failed to save changes", description: "An error occurred while updating the question.", @@ -175,14 +174,15 @@ export default function QuestionPage() { {/* Left Column: Question and Answers */}
- {isEditing ? ( - - ) : ( -

{question.title}

- )} + {isEditing ? ( + + ) : ( +

{question.title}

+ )}
@@ -226,8 +230,7 @@ export default function QuestionPage() { className="flex items-center gap-4" > {"Profile @@ -292,7 +295,7 @@ export default function QuestionPage() { label: tag.name || "Loading...", }))} value={tags.map((tag) => String(tag))} - onValueChange={(selectedIds) =>{ + onValueChange={(selectedIds) => { const selectedTags = selectedIds.map((id) => Number(id)); // Convert back to numbers setTags(selectedTags); }} @@ -301,13 +304,12 @@ export default function QuestionPage() { ) : (
{question.tags.map((s) => ( - + {s.name} ))}
)} - Asked: {new Date(question.createdAt).toLocaleDateString()} @@ -318,19 +320,31 @@ export default function QuestionPage() { {isEditing ? (
- -
{isPreviewMode ? (
- +
) : ( -