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..812022f9 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -1,326 +1,5 @@ -import ErrorAlert from "@/components/ErrorAlert"; -import FollowUserButton from "@/components/FollowUserButton"; -import { FullscreenLoading } from "@/components/FullscreenLoading"; -import { QuestionCard } from "@/components/QuestionCard"; -import { - Button, - ButtonText, - HStack, - Icon, - Image, - Input, - InputField, - 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 } 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 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(`/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"} - - - )} - - {me ? ( - editing ? ( - - ) : ( - - ) - ) : ( - data?.data && - )} - - {/* - - - - */} - - {activeTab === "questions" ? ( - - - Questions - {me && ( - - - - )} - - - {profile?.questions?.map((question) => ( - - ))} - - - ) : ( - - Answers - - {profile?.answers?.map((answer) => ( - - ))} - - - )} - - - ); -} 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() { - - User - -} +export default function ProfilePage() { + const { userId } = useLocalSearchParams<{ userId: string }>(); + return ; +} \ No newline at end of file diff --git a/mobile/components/Feed.tsx b/mobile/components/Feed.tsx index 1e213b43..77ffaad4 100644 --- a/mobile/components/Feed.tsx +++ b/mobile/components/Feed.tsx @@ -1,7 +1,6 @@ import { TagList } from "@/components/TagList"; import { Divider, Text, VStack } from "@/components/ui"; -import { QuestionList } from "./QuestionsList"; -import { Divide } from "lucide-react-native"; +import { QuestionListSearch } from "./QuestionsList"; export const Feed = () => { @@ -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, 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)) || [] } + /> + + )} + + + ); +}