Skip to content

Commit

Permalink
feat(reviews): implement actual pagination functionality (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
chimpdev authored Feb 4, 2025
1 parent 4e601be commit 877e16c
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 71 deletions.
29 changes: 19 additions & 10 deletions client/app/(bots)/bots/[id]/components/Tabs/Reviews.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TbLoader } from 'react-icons/tb';
import { TiStarFullOutline, TiStarHalfOutline, TiStarOutline } from 'react-icons/ti';
import { toast } from 'sonner';
import createReview from '@/lib/request/bots/createReview';
import fetchReviews from '@/lib/request/bots/fetchReviews';
import LoginButton from '@/app/(bots)/bots/[id]/components/Tabs/LoginButton';
import { RiErrorWarningFill } from 'react-icons/ri';
import cn from '@/lib/cn';
Expand All @@ -20,8 +21,9 @@ import Image from 'next/image';
export default function Reviews({ bot }) {
const [page, setPage] = useState(1);
const limit = 6;
const maxPages = bot.reviews.length / limit;
const [reviews, setReviews] = useState(bot.reviews.slice(0, limit));
const [reviews, setReviews] = useState([]);
const [totalReviews, setTotalReviews] = useState(0);
const [reviewsLoading, setReviewsLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [selectedRating, setSelectedRating] = useState(0);
const [reviewSubmitted, setReviewSubmitted] = useState(false);
Expand All @@ -32,12 +34,16 @@ export default function Reviews({ bot }) {
const language = useLanguageStore(state => state.language);

useEffect(() => {
const start = (page - 1) * limit;
const end = start + limit;
setReviews(bot.reviews.slice(start, end));
setReviewsLoading(true);

// eslint-disable-next-line
}, [page]);
fetchReviews(bot.id, page, limit)
.then(data => {
setReviews(data.reviews);
setTotalReviews(data.total);
})
.catch(error => toast.error(error))
.finally(() => setReviewsLoading(false));
}, [bot.id, page, limit]);

const calcRating = rating => {
const totalReviews = bot.reviews.length;
Expand Down Expand Up @@ -68,6 +74,8 @@ export default function Reviews({ bot }) {
});
}

const showPagination = !reviewsLoading && totalReviews > limit;

return (
<div className='flex flex-col px-8 lg:w-[70%] lg:px-0'>
<h1 className='text-xl font-semibold'>
Expand Down Expand Up @@ -317,14 +325,15 @@ export default function Reviews({ bot }) {
</div>
))}

{maxPages > 1 && (
{showPagination > 1 && (
<div className='flex w-full items-center justify-center'>
<Pagination
page={page}
setPage={setPage}
loading={loading}
total={bot.reviews.length}
loading={loading || reviewsLoading}
total={totalReviews}
limit={limit}
disableAnimation={true}
/>
</div>
)}
Expand Down
168 changes: 109 additions & 59 deletions client/app/(servers)/servers/[id]/components/Tabs/Reviews.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TbLoader } from 'react-icons/tb';
import { TiStarFullOutline, TiStarHalfOutline, TiStarOutline } from 'react-icons/ti';
import { toast } from 'sonner';
import createReview from '@/lib/request/servers/createReview';
import fetchReviews from '@/lib/request/servers/fetchReviews';
import LoginButton from '@/app/(servers)/servers/[id]/components/Tabs/LoginButton';
import { RiErrorWarningFill } from 'react-icons/ri';
import cn from '@/lib/cn';
Expand All @@ -20,8 +21,9 @@ import Image from 'next/image';
export default function Reviews({ server }) {
const [page, setPage] = useState(1);
const limit = 6;
const maxPages = server.reviews.length / limit;
const [reviews, setReviews] = useState(server.reviews.slice(0, limit));
const [reviews, setReviews] = useState([]);
const [totalReviews, setTotalReviews] = useState(0);
const [reviewsLoading, setReviewsLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [selectedRating, setSelectedRating] = useState(0);
const [reviewSubmitted, setReviewSubmitted] = useState(false);
Expand All @@ -32,12 +34,16 @@ export default function Reviews({ server }) {
const language = useLanguageStore(state => state.language);

useEffect(() => {
const start = (page - 1) * limit;
const end = start + limit;
setReviews(server.reviews.slice(start, end));
setReviewsLoading(true);

// eslint-disable-next-line
}, [page]);
fetchReviews(server.id, page, limit)
.then(data => {
setReviews(data.reviews);
setTotalReviews(data.total);
})
.catch(error => toast.error(error))
.finally(() => setReviewsLoading(false));
}, [server.id, page, limit]);

const calcRating = rating => {
const totalReviews = server.reviews.length;
Expand Down Expand Up @@ -68,6 +74,8 @@ export default function Reviews({ server }) {
});
}

const showPagination = !reviewsLoading && totalReviews > limit;

return (
<div className='flex flex-col px-8 lg:w-[70%] lg:px-0'>
<h1 className='text-xl font-semibold'>
Expand Down Expand Up @@ -244,7 +252,7 @@ export default function Reviews({ server }) {
<button
onClick={submitReview}
className='mt-4 flex items-center justify-center gap-x-1.5 rounded-lg bg-black px-4 py-2 text-sm font-semibold text-white hover:bg-black/70 disabled:pointer-events-none disabled:opacity-70 dark:bg-white dark:text-black dark:hover:bg-white/70'
disabled={selectedRating === 0 || loading || reviewSubmitted || review.length < config.reviewsMinCharacters}
disabled={selectedRating === 0 || loading || reviewSubmitted || review.length < config.reviewsMinCharacters || reviewsLoading}
>
{loading && <TbLoader className='animate-spin' />}

Expand All @@ -260,72 +268,114 @@ export default function Reviews({ server }) {
)}
</div>

{reviews.map(review => (
<div className='mt-8 flex w-full flex-col gap-y-4 sm:flex-row' key={review._id}>
<div className='flex w-full gap-x-4 sm:w-[35%]'>
<Link
href={`/profile/u/${review.user.id}`}
className='transition-opacity hover:opacity-70'
>
<UserAvatar
id={review.user.id}
hash={review.user.avatar}
size={64}
width={48}
height={48}
className='size-[48px] rounded-2xl'
/>
</Link>

<div className='flex flex-col gap-y-1'>
{reviewsLoading ? (
new Array(6).fill(null).map((_, index) => (
<div
className='mt-8 flex w-full flex-col gap-y-4 sm:flex-row'
key={`review-loading-${index}`}
>
<div className='flex w-full gap-x-4 sm:w-[35%]'>
<div className='size-[48px] min-h-[48px] min-w-[48px] animate-pulse rounded-2xl bg-quaternary'>
&thinsp;
</div>

<div className='flex w-full flex-col gap-y-1'>
<div className='flex w-full max-w-[100px] animate-pulse items-center rounded-xl bg-quaternary text-base mobile:max-w-[150px] sm:max-w-[100px] lg:max-w-[160px]'>
&thinsp;
</div>

<div className='flex items-center text-sm font-semibold text-tertiary'>
<span className='h-[15px] w-full max-w-[50px] animate-pulse rounded-xl bg-quaternary' />
<TiStarFullOutline className='ml-2 animate-pulse text-[rgba(var(--bg-quaternary))]' />
</div>
</div>
</div>

<div className='flex w-full max-w-[440px] flex-1 flex-col justify-between gap-y-2 whitespace-pre-wrap break-words font-medium text-secondary sm:gap-y-0'>
<span className='h-[15px] w-full max-w-[150px] animate-pulse rounded-xl bg-quaternary text-xs font-medium text-tertiary'>
&thinsp;
</span>

<div className='mt-2 flex w-full flex-col gap-y-2'>
<span className='h-[20px] w-full max-w-full animate-pulse rounded-xl bg-quaternary text-xs font-medium text-tertiary'>
&thinsp;
</span>
<span className='h-[20px] w-full max-w-[65%] animate-pulse rounded-xl bg-quaternary text-xs font-medium text-tertiary'>
&thinsp;
</span>
</div>
</div>
</div>
))
) : (
reviews.map(review => (
<div className='mt-8 flex w-full flex-col gap-y-4 sm:flex-row' key={review._id}>
<div className='flex w-full gap-x-4 sm:w-[35%]'>
<Link
href={`/profile/u/${review.user.id}`}
className='flex items-center text-base font-semibold transition-opacity hover:opacity-70'
className='transition-opacity hover:opacity-70'
>
<span className='max-w-[100px] truncate mobile:max-w-[150px] sm:max-w-[100px] lg:max-w-[160px]'>
{review.user.username}
</span>
<UserAvatar
id={review.user.id}
hash={review.user.avatar}
size={64}
width={48}
height={48}
className='size-[48px] rounded-2xl'
/>
</Link>

<div className='flex items-center text-sm font-semibold text-tertiary'>
<span className='text-xl text-primary'>{review.rating}</span>/5 <TiStarFullOutline className='ml-2 text-yellow-500' />
<div className='flex flex-col gap-y-1'>
<Link
href={`/profile/u/${review.user.id}`}
className='flex items-center text-base font-semibold transition-opacity hover:opacity-70'
>
<span className='max-w-[100px] truncate mobile:max-w-[150px] sm:max-w-[100px] lg:max-w-[160px]'>
{review.user.username}
</span>
</Link>

<div className='flex items-center text-sm font-semibold text-tertiary'>
<span className='text-xl text-primary'>{review.rating}</span>/5 <TiStarFullOutline className='ml-2 text-yellow-500' />
</div>
</div>
</div>
</div>

<ReportableArea
type='review'
active={user?.id !== review.user.id}
metadata={{
reviewer: {
id: review.user.id,
username: review.user.username,
avatar: review.user.avatar
},
rating: review.rating,
content: review.content
}}
identifier={`server-${server.id}-review-${review._id}`}
>
<div className='flex w-full max-w-[440px] flex-1 flex-col justify-between gap-y-2 whitespace-pre-wrap break-words font-medium text-secondary sm:gap-y-0'>
<span className='text-xs font-medium text-tertiary'>
{new Date(review.createdAt).toLocaleDateString(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' })}
</span>
<ReportableArea
type='review'
active={user?.id !== review.user.id}
metadata={{
reviewer: {
id: review.user.id,
username: review.user.username,
avatar: review.user.avatar
},
rating: review.rating,
content: review.content
}}
identifier={`server-${server.id}-review-${review._id}`}
>
<div className='flex w-full max-w-[440px] flex-1 flex-col justify-between gap-y-2 whitespace-pre-wrap break-words font-medium text-secondary sm:gap-y-0'>
<span className='text-xs font-medium text-tertiary'>
{new Date(review.createdAt).toLocaleDateString(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' })}
</span>

{review.content}
</div>
</ReportableArea>
</div>
))}
{review.content}
</div>
</ReportableArea>
</div>
))
)}

{maxPages > 1 && (
{showPagination && (
<div className='flex w-full items-center justify-center'>
<Pagination
page={page}
setPage={setPage}
loading={loading}
total={server.reviews.length}
loading={loading || reviewsLoading}
total={totalReviews}
limit={limit}
disableAnimation={true}
/>
</div>
)}
Expand Down
19 changes: 19 additions & 0 deletions client/lib/request/bots/fetchReviews.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import config from '@/config';
import axios from 'axios';

export default function fetchReviews(id, page, limit) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const baseURL = `${config.api.url}/bots/${id}/reviews`;
const url = new URL(baseURL);
if (page) url.searchParams.append('page', page);
if (limit) url.searchParams.append('limit', limit);

try {
const response = await axios.get(url, { withCredentials: true });
resolve(response.data);
} catch (error) {
reject(error instanceof axios.AxiosError ? (error.response?.data?.error || error.message) : error.message);
}
});
}
19 changes: 19 additions & 0 deletions client/lib/request/servers/fetchReviews.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import config from '@/config';
import axios from 'axios';

export default function fetchReviews(id, page, limit) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const baseURL = `${config.api.url}/servers/${id}/reviews`;
const url = new URL(baseURL);
if (page) url.searchParams.append('page', page);
if (limit) url.searchParams.append('limit', limit);

try {
const response = await axios.get(url, { withCredentials: true });
resolve(response.data);
} catch (error) {
reject(error instanceof axios.AxiosError ? (error.response?.data?.error || error.message) : error.message);
}
});
}
Loading

0 comments on commit 877e16c

Please sign in to comment.