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

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

@ -107,7 +107,7 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) {
<div className="flex flex-col gap-3">
{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) => {
const percentage =
totalRatings <= 0
@ -134,7 +134,7 @@ export function RateRoadmapForm(props: RateRoadmapFormProps) {
</div>
<span className="w-[35px] shrink-0 text-xs text-gray-500">
{percentage}%
{parseInt(`${percentage}`, 10)}%
</span>
</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>
<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-[71.48px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[100.34px]" />
<div className="h-7 w-[32px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[89.73px]" />
<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-[139px]" />
</div>
</div>

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

Loading…
Cancel
Save