Refactor feedback modal

feat/discover
Kamran Ahmed 5 months ago
parent 875a7c4c62
commit fa8dd8d865
  1. 57
      src/components/CustomRoadmap/CustomRoadmapRatingsModal.tsx
  2. 97
      src/components/CustomRoadmap/ListRoadmapRatings.tsx
  3. 4
      src/components/CustomRoadmap/RateRoadmapForm.tsx
  4. 5
      src/components/CustomRoadmap/SkeletonRoadmapHeader.tsx
  5. 4
      src/lib/date.ts

@ -21,7 +21,10 @@ export function CustomRoadmapRatingsModal(
) { ) {
const { onClose, ratings, roadmapSlug, canManage = false } = props; const { onClose, ratings, roadmapSlug, canManage = false } = props;
const [activeTab, setActiveTab] = useState<ActiveTab>('ratings'); const [activeTab, setActiveTab] = useState<ActiveTab>(
canManage ? 'feedback' : 'ratings',
);
const tabs: { const tabs: {
id: ActiveTab; id: ActiveTab;
label: string; label: string;
@ -38,36 +41,38 @@ export function CustomRoadmapRatingsModal(
return ( return (
<Modal onClose={onClose} bodyClassName="bg-transparent shadow-none"> <Modal onClose={onClose} bodyClassName="bg-transparent shadow-none">
{canManage && ( {/*{canManage && (*/}
<div className="-mx-4 mb-4 flex items-center gap-4 border-b px-4"> {/* <div className="mb-1 flex items-center gap-1">*/}
{tabs.map((tab) => { {/* {tabs.map((tab) => {*/}
const isActive = tab.id === activeTab; {/* const isActive = tab.id === activeTab;*/}
return ( {/* return (*/}
<button {/* <button*/}
key={tab.id} {/* key={tab.id}*/}
onClick={() => { {/* onClick={() => {*/}
setActiveTab(tab.id); {/* setActiveTab(tab.id);*/}
}} {/* }}*/}
className={cn( {/* className={cn('rounded-md bg-white px-3 py-2 text-sm', {*/}
'py-2 text-sm', {/* 'bg-gray-100 font-medium text-black': isActive,*/}
isActive {/* 'text-gray-500 hover:text-gray-700': !isActive,*/}
? 'border-b-2 border-black font-medium' {/* })}*/}
: 'text-gray-500 hover:text-gray-700', {/* >*/}
)} {/* {tab.label}*/}
> {/* </button>*/}
{tab.label} {/* );*/}
</button> {/* })}*/}
); {/* </div>*/}
})} {/*)}*/}
</div>
)}
{activeTab === 'ratings' && ( {activeTab === 'ratings' && (
<RateRoadmapForm ratings={ratings} roadmapSlug={roadmapSlug} canManage={canManage} /> <RateRoadmapForm
ratings={ratings}
roadmapSlug={roadmapSlug}
canManage={canManage}
/>
)} )}
{activeTab === 'feedback' && ( {activeTab === 'feedback' && (
<ListRoadmapRatings roadmapSlug={roadmapSlug} /> <ListRoadmapRatings ratings={ratings} roadmapSlug={roadmapSlug} />
)} )}
</Modal> </Modal>
); );

@ -4,6 +4,10 @@ import { useToast } from '../../hooks/use-toast';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { Loader2, MessageCircle, ServerCrash } from 'lucide-react'; import { Loader2, MessageCircle, ServerCrash } from 'lucide-react';
import { Rating } from '../Rating/Rating'; import { Rating } from '../Rating/Rating';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { getRelativeTimeString } from '../../lib/date.ts';
import { cn } from '../../lib/classname.ts';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal.tsx';
export interface RoadmapRatingDocument { export interface RoadmapRatingDocument {
_id?: string; _id?: string;
@ -23,12 +27,18 @@ type ListRoadmapRatingsResponse = (RoadmapRatingDocument & {
type ListRoadmapRatingsProps = { type ListRoadmapRatingsProps = {
roadmapSlug: string; roadmapSlug: string;
ratings: RoadmapDocument['ratings'];
}; };
export function ListRoadmapRatings(props: ListRoadmapRatingsProps) { export function ListRoadmapRatings(props: ListRoadmapRatingsProps) {
const { roadmapSlug } = props; const { roadmapSlug, ratings: ratingSummary } = props;
const totalWhoRated = Object.keys(ratingSummary.breakdown || {}).reduce(
(acc, key) => acc + ratingSummary.breakdown[key as any],
0,
);
const averageRating = ratingSummary.average;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [ratings, setRatings] = useState<ListRoadmapRatingsResponse>([]); const [ratings, setRatings] = useState<ListRoadmapRatingsResponse>([]);
@ -59,7 +69,7 @@ export function ListRoadmapRatings(props: ListRoadmapRatingsProps) {
if (error) { if (error) {
return ( return (
<div className="flex flex-col items-center justify-center py-10"> <div className="flex flex-col items-center justify-center bg-white py-10">
<ServerCrash className="size-12 text-red-500" /> <ServerCrash className="size-12 text-red-500" />
<p className="mt-3 text-lg text-red-500">{error}</p> <p className="mt-3 text-lg text-red-500">{error}</p>
</div> </div>
@ -67,41 +77,64 @@ export function ListRoadmapRatings(props: ListRoadmapRatingsProps) {
} }
return ( return (
<div> <div className="relative min-h-[100px] rounded-lg bg-white p-2 overflow-hidden">
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin stroke-[3px]" /> <Spinner isDualRing={false} />
</div> </div>
)} )}
{!isLoading && ratings.length > 0 && ( {!isLoading && ratings.length > 0 && (
<div className="flex flex-col gap-2"> <div>
{ratings.map((rating) => { <div className='text-sm px-2 py-1.5 text-yellow-900 mb-2 rounded-lg bg-yellow-50 flex items-center gap-1 justify-center'>
const userAvatar = rating?.avatar <span>Rated <span className="font-medium">{averageRating.toFixed(1)}</span></span>
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${rating.avatar}` <Rating starSize={15} rating={averageRating} readOnly />
: '/images/default-avatar.png'; by <span className="font-medium">{totalWhoRated} user{totalWhoRated > 1 && 's'}</span>
</div>
return (
<div key={rating._id} className="rounded-md border p-2"> <div className="flex flex-col">
<div className="flex items-center gap-2"> {ratings.map((rating) => {
<img const userAvatar = rating?.avatar
src={userAvatar} ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${rating.avatar}`
alt={rating.name} : '/images/default-avatar.png';
className="h-6 w-6 rounded-full"
/> const isLastRating =
<span className="text-lg font-medium">{rating.name}</span> ratings[ratings.length - 1]._id === rating._id;
</div>
return (
<div className="mt-2.5"> <div
<Rating rating={rating.rating} readOnly /> key={rating._id}
className={cn('px-2 py-2.5', {
{rating.feedback && ( 'border-b': !isLastRating,
<p className="mt-2 text-gray-500">{rating.feedback}</p> })}
)} >
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<img
src={userAvatar}
alt={rating.name}
className="h-4 w-4 rounded-full"
/>
<span className="text-sm font-medium">{rating.name}</span>
</div>
<span className="text-xs text-gray-400">
{getRelativeTimeString(rating.createdAt)}
</span>
</div>
<div className="mt-2.5">
<Rating rating={rating.rating} readOnly />
{rating.feedback && (
<p className="mt-2 text-sm text-gray-500">
{rating.feedback}
</p>
)}
</div>
</div> </div>
</div> );
); })}
})} </div>
</div> </div>
)} )}

@ -107,7 +107,7 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{showRatingsBreakdown && !isRatingRoadmap && ( {showRatingsBreakdown && !isRatingRoadmap && (
<> <>
<ul className="mt-4 flex flex-col gap-1 rounded-lg bg-white p-5"> <ul className="flex flex-col gap-1 rounded-lg bg-white p-5">
{ratingsKeys.map((rating) => { {ratingsKeys.map((rating) => {
const percentage = const percentage =
totalRatings <= 0 totalRatings <= 0
@ -134,7 +134,7 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) {
</div> </div>
<span className="w-[35px] shrink-0 text-xs text-gray-500"> <span className="w-[35px] shrink-0 text-xs text-gray-500">
{percentage}% {parseInt(`${percentage}`, 10)}%
</span> </span>
</li> </li>
); );

@ -17,9 +17,8 @@ export function SkeletonRoadmapHeader() {
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[85px]" /> <div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[85px]" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-7 w-[60.52px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[139.71px]" /> <div className="h-7 w-[60.52px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[92px]" />
<div className="h-7 w-[71.48px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[100.34px]" /> <div className="h-7 w-[71.48px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[139px]" />
<div className="h-7 w-[32px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[89.73px]" />
</div> </div>
</div> </div>

@ -1,11 +1,11 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export function getRelativeTimeString( export function getRelativeTimeString(
date: string, date: string | Date,
isTimed: boolean = false, isTimed: boolean = false,
): string { ): string {
if (!Intl?.RelativeTimeFormat) { if (!Intl?.RelativeTimeFormat) {
return date; return date.toString();
} }
const rtf = new Intl.RelativeTimeFormat('en', { const rtf = new Intl.RelativeTimeFormat('en', {

Loading…
Cancel
Save