diff --git a/src/api/roadmap.ts b/src/api/roadmap.ts index 949f1dbba..2072a4fac 100644 --- a/src/api/roadmap.ts +++ b/src/api/roadmap.ts @@ -13,6 +13,7 @@ export type ListShowcaseRoadmapResponse = { | 'visibility' | 'createdAt' | 'topicCount' + | 'ratings' >[]; totalCount: number; totalPages: number; diff --git a/src/components/CustomRoadmap/CustomRoadmap.tsx b/src/components/CustomRoadmap/CustomRoadmap.tsx index bb9765b8a..fe3d66f45 100644 --- a/src/components/CustomRoadmap/CustomRoadmap.tsx +++ b/src/components/CustomRoadmap/CustomRoadmap.tsx @@ -18,7 +18,7 @@ export const allowedLinkTypes = [ 'roadmap.sh', 'official', 'roadmap', - 'feed' + 'feed', ] as const; export type AllowedLinkTypes = (typeof allowedLinkTypes)[number]; @@ -47,6 +47,7 @@ export type GetRoadmapResponse = RoadmapDocument & { canManage: boolean; creator?: CreatorType; team?: CreatorType; + unseenRatingCount: number; }; export function hideRoadmapLoader() { diff --git a/src/components/CustomRoadmap/CustomRoadmapRatings.tsx b/src/components/CustomRoadmap/CustomRoadmapRatings.tsx index bddc476b5..257014b11 100644 --- a/src/components/CustomRoadmap/CustomRoadmapRatings.tsx +++ b/src/components/CustomRoadmap/CustomRoadmapRatings.tsx @@ -1,15 +1,19 @@ -import { useState } from 'react'; +import { useState, type CSSProperties } from 'react'; import { Rating } from '../Rating/Rating'; import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal'; import { CustomRoadmapRatingsModal } from './CustomRoadmapRatingsModal'; +import { Star } from 'lucide-react'; +import { cn } from '../../lib/classname'; type CustomRoadmapRatingsProps = { roadmapSlug: string; ratings: RoadmapDocument['ratings']; + canManage?: boolean; + unseenRatingCount: number; }; export function CustomRoadmapRatings(props: CustomRoadmapRatingsProps) { - const { ratings, roadmapSlug } = props; + const { ratings, roadmapSlug, canManage, unseenRatingCount } = props; const average = ratings?.average || 0; const [isDetailsOpen, setIsDetailsOpen] = useState(false); @@ -18,22 +22,36 @@ export function CustomRoadmapRatings(props: CustomRoadmapRatingsProps) { <> {isDetailsOpen && ( { setIsDetailsOpen(false); }} ratings={ratings} + canManage={canManage} /> )}
- + + + +
diff --git a/src/components/CustomRoadmap/CustomRoadmapRatingsModal.tsx b/src/components/CustomRoadmap/CustomRoadmapRatingsModal.tsx index 65f626836..3babe647a 100644 --- a/src/components/CustomRoadmap/CustomRoadmapRatingsModal.tsx +++ b/src/components/CustomRoadmap/CustomRoadmapRatingsModal.tsx @@ -4,21 +4,71 @@ import { Modal } from '../Modal'; import { Rating } from '../Rating/Rating'; import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal'; import { RateRoadmapForm } from './RateRoadmapForm'; +import { cn } from '../../lib/classname'; +import { ListRoadmapRatings } from './ListRoadmapRatings'; + +type ActiveTab = 'ratings' | 'feedback'; type CustomRoadmapRatingsModalProps = { onClose: () => void; roadmapSlug: string; ratings: RoadmapDocument['ratings']; + canManage?: boolean; }; export function CustomRoadmapRatingsModal( props: CustomRoadmapRatingsModalProps, ) { - const { onClose, ratings, roadmapSlug } = props; + const { onClose, ratings, roadmapSlug, canManage = false } = props; + + const [activeTab, setActiveTab] = useState('ratings'); + const tabs: { + id: ActiveTab; + label: string; + }[] = [ + { + id: 'ratings', + label: 'Ratings', + }, + { + id: 'feedback', + label: 'Feedback', + }, + ]; return ( - + {canManage && ( +
+ {tabs.map((tab) => { + const isActive = tab.id === activeTab; + + return ( + + ); + })} +
+ )} + + {activeTab === 'ratings' && ( + + )} + {activeTab === 'feedback' && ( + + )}
); } diff --git a/src/components/CustomRoadmap/ListRoadmapRatings.tsx b/src/components/CustomRoadmap/ListRoadmapRatings.tsx new file mode 100644 index 000000000..90ccdb023 --- /dev/null +++ b/src/components/CustomRoadmap/ListRoadmapRatings.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react'; +import { httpGet } from '../../lib/http'; +import { useToast } from '../../hooks/use-toast'; +import { isLoggedIn } from '../../lib/jwt'; +import { Loader2, MessageCircle, ServerCrash } from 'lucide-react'; +import { Rating } from '../Rating/Rating'; + +export interface RoadmapRatingDocument { + _id?: string; + roadmapId: string; + userId: string; + rating: number; + feedback?: string; + + createdAt: Date; + updatedAt: Date; +} + +type ListRoadmapRatingsResponse = (RoadmapRatingDocument & { + name: string; + avatar: string; +})[]; + +type ListRoadmapRatingsProps = { + roadmapSlug: string; +}; + +export function ListRoadmapRatings(props: ListRoadmapRatingsProps) { + const { roadmapSlug } = props; + + const toast = useToast(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(''); + const [ratings, setRatings] = useState([]); + + const listRoadmapRatings = async () => { + const { response, error } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-list-roadmap-ratings/${roadmapSlug}`, + ); + + if (!response || error) { + setError(error?.message || 'Something went wrong'); + setIsLoading(false); + return; + } + + setRatings(response); + setError(''); + setIsLoading(false); + }; + + useEffect(() => { + if (!isLoggedIn()) { + return; + } + + listRoadmapRatings().then(); + }, []); + + if (error) { + return ( +
+ +

{error}

+
+ ); + } + + return ( +
+ {isLoading && ( +
+ +
+ )} + + {!isLoading && ratings.length > 0 && ( +
+ {ratings.map((rating) => { + const userAvatar = rating?.avatar + ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${rating.avatar}` + : '/images/default-avatar.png'; + + return ( +
+
+ {rating.name} + {rating.name} +
+ +
+ + + {rating.feedback && ( +

{rating.feedback}

+ )} +
+
+ ); + })} +
+ )} + + {!isLoading && ratings.length === 0 && ( +
+ +

No Feedbacks

+
+ )} +
+ ); +} diff --git a/src/components/CustomRoadmap/RateRoadmapForm.tsx b/src/components/CustomRoadmap/RateRoadmapForm.tsx index 3c5916358..df5ca423a 100644 --- a/src/components/CustomRoadmap/RateRoadmapForm.tsx +++ b/src/components/CustomRoadmap/RateRoadmapForm.tsx @@ -7,6 +7,7 @@ import { useToast } from '../../hooks/use-toast'; import { isLoggedIn } from '../../lib/jwt'; import { Loader2 } from 'lucide-react'; import { cn } from '../../lib/classname'; +import { showLoginPopup } from '../../lib/popup'; type GetMyRoadmapRatingResponse = { id?: string; @@ -77,13 +78,12 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) { return; } - toast.success('Rating successful'); - setUserRatingId(response.id); - setIsLoading(false); + window.location.reload(); }; useEffect(() => { if (!isLoggedIn() || !roadmapSlug) { + setIsLoading(false); return; } @@ -209,6 +209,11 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) {