Skip to content

Commit

Permalink
Add sorting and searching functionality to bots page
Browse files Browse the repository at this point in the history
  • Loading branch information
chimpdev committed May 27, 2024
1 parent cf85396 commit 7e2a1c4
Show file tree
Hide file tree
Showing 12 changed files with 392 additions and 124 deletions.
50 changes: 50 additions & 0 deletions client/app/(bots)/bots/components/Hero/Drawer/Sorting.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import cn from '@/lib/cn';
import { nanoid } from 'nanoid';
import { IoMdCheckmarkCircle } from 'react-icons/io';
import { Drawer } from 'vaul';
import { TbBoxMultiple, TbSquareRoundedChevronUp } from 'react-icons/tb';
import { HiSortAscending, HiSortDescending } from 'react-icons/hi';
import { TiStar } from 'react-icons/ti';

export default function SortingDrawer({ openState, setOpenState, state, setState }) {
const sortings = {
'Votes': <TbSquareRoundedChevronUp />,
'Servers': <TbBoxMultiple />,
'Most Reviewed': <TiStar />,
'Newest': <HiSortAscending />,
'Oldest': <HiSortDescending />
};

return (
<Drawer.Root shouldScaleBackground={true} closeThreshold={0.5} open={openState} onOpenChange={setOpenState}>
<Drawer.Portal>
<Drawer.Content className='outline-none gap-y-1 p-4 z-[10001] bg-secondary flex flex-col rounded-t-3xl h-[85%] fixed bottom-0 left-0 right-0'>
<div className='mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-quaternary mb-8' />

{Object.keys(sortings).map(sort => (
<button
key={nanoid()}
onClick={() => {
setState(sort);
setOpenState(false);
}}
className={cn(
'flex items-center justify-between px-4 py-3 text-base font-medium rounded-lg disabled:pointer-events-none',
state === sort ? 'pointer-events-none bg-quaternary text-primary' : 'hover:bg-quaternary text-tertiary hover:text-primary'
)}
>
<span className='flex items-center gap-x-2'>
{sortings[sort]}
{sort}
</span>
{state === sort && <IoMdCheckmarkCircle />}
</button>
))}
</Drawer.Content>
<Drawer.Overlay className='fixed inset-0 bg-white/40 dark:bg-black/40 z-[10000]' />
</Drawer.Portal>
</Drawer.Root>
);
}
36 changes: 29 additions & 7 deletions client/app/(bots)/bots/components/Hero/PopularBots/Card/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { IoHeart } from 'react-icons/io5';
import { useMedia } from 'react-use';
import { BiSolidCategory } from 'react-icons/bi';
import useSearchStore from '@/stores/bots/search';
import { TiStar } from 'react-icons/ti';
import { HiSortAscending, HiSortDescending } from 'react-icons/hi';

export default function Card({ data }) {
export default function Card({ data, overridedSort }) {
const storedSort = useSearchStore(state => state.sort);
const sort = overridedSort || storedSort;
const category = useSearchStore(state => state.category);
const isMobile = useMedia('(max-width: 420px)', false);

Expand All @@ -22,10 +26,15 @@ export default function Card({ data }) {
condition: data.owner?.premium === true && !isMobile,
transform: () => 'Premium'
},
{
icon: TbSquareRoundedChevronUp,
value: data.votes,
condition: sort === 'Votes'
},
{
icon: TbBoxMultiple,
value: data.servers,
condition: true,
condition: sort === 'Servers',
transform: value => {
const serversFormatter = new Intl.NumberFormat('en-US', {
style: 'decimal',
Expand All @@ -37,9 +46,22 @@ export default function Card({ data }) {
}
},
{
icon: TbSquareRoundedChevronUp,
value: data.votes,
condition: true
icon: TiStar,
value: data.reviews,
condition: sort === 'Most Reviewed',
transform: value => `${formatter.format(value)} Time Reviewed`
},
{
icon: HiSortAscending,
value: data.created_at,
condition: sort === 'Newest',
transform: date => new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
},
{
icon: HiSortDescending,
value: data.created_at,
condition: sort === 'Oldest',
transform: date => new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
];

Expand Down Expand Up @@ -67,7 +89,7 @@ export default function Card({ data }) {
height={64}
className='absolute top-[-25px] left-4 bg-secondary group-hover:bg-tertiary border-[4px] border-[rgba(var(--bg-secondary))] group-hover:border-[rgba(var(--bg-tertiary))] transition-colors rounded-3xl'
/>

<div className='flex flex-col px-4 pt-12'>
<div className='flex items-center'>
<span className='text-lg font-semibold truncate'>
Expand Down Expand Up @@ -109,7 +131,7 @@ export default function Card({ data }) {

<div className='flex items-center px-2.5 py-1 mt-3 text-sm font-medium rounded-full gap-x-1 w-max text-secondary bg-quaternary'>
<BiSolidCategory />
{category || data.categories[0]}
{category === 'All' ? data.categories[0] : category}
</div>
</div>
</div>
Expand Down
60 changes: 48 additions & 12 deletions client/app/(bots)/bots/components/Hero/PopularBots/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ import ErrorState from '@/app/components/ErrorState';
import { BsEmojiAngry } from 'react-icons/bs';
import { toast } from 'sonner';
import Pagination from '@/app/components/Pagination';
import { AnimatePresence } from 'framer-motion';

export default function PopularBots() {
const { loading, bots, fetchBots, total: totalBots, limit, page, setPage, category } = useSearchStore(useShallow(state => ({
const { loading, bots, fetchBots, total: totalBots, limit, page, setPage, category, setSearch, setCategory, setSort } = useSearchStore(useShallow(state => ({
loading: state.loading,
bots: state.bots,
fetchBots: state.fetchBots,
total: state.total,
limit: state.limit,
page: state.page,
setPage: state.setPage,
category: state.category
category: state.category,
setSearch: state.setSearch,
setCategory: state.setCategory,
setSort: state.setSort
})));

useEffect(() => {
Expand All @@ -28,18 +32,50 @@ export default function PopularBots() {
}, []);

const showPagination = !loading && totalBots > limit;


const stateVariants = {
hidden: {
opacity: 0
},
visible: {
opacity: 1
},
exit: {
opacity: 0
}
};

return (
!loading && bots.length <= 0 ? (
<ErrorState
title={
<div className='flex items-center gap-x-2'>
<BsEmojiAngry />
It{'\''}s quiet in here...
</div>
}
message='We couldn’t find any bots.'
/>
<AnimatePresence>
<motion.div
className='flex flex-col gap-y-2'
variants={stateVariants}
initial='hidden'
animate='visible'
exit='hidden'
>
<ErrorState
title={
<div className='flex items-center gap-x-2'>
<BsEmojiAngry />
It{'\''}s quiet in here...
</div>
}
message={'There are no bots to display. Maybe that\'s a sign to create one?'}
/>

<button className='text-tertiary hover:underline hover:text-primary' onClick={() => {
setSearch('');
fetchBots('');
setCategory('All');
setSort('Votes');
setPage(1);
}}>
Reset Search
</button>
</motion.div>
</AnimatePresence>
) : (
<>
<motion.div
Expand Down
96 changes: 96 additions & 0 deletions client/app/(bots)/bots/components/Hero/SearchInput/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { FiSearch, FiX } from 'react-icons/fi';
import { motion } from 'framer-motion';
import useSearchStore from '@/stores/bots/search';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { useShallow } from 'zustand/react/shallow';

export default function SearchInput() {
const [value, setValue] = useState('');
const { loading, search, fetchBots } = useSearchStore(useShallow(state => ({
loading: state.loading,
search: state.search,
fetchBots: state.fetchBots
})));

function validateValue(throwError = true) {
function returnError(message) {
if (throwError) toast.error(message);
return false;
}

if (!value) return returnError('Please enter a valid search query to find bots.');

const trimmedValue = value.trim();
if (trimmedValue.length <= 0) return returnError('Please enter a valid search query to find bots.');
if (trimmedValue.length < 3) return returnError('The search query is too short. Please enter a minimum of 3 characters.');
if (trimmedValue.length > 100) return returnError('The search query is too long. Please enter a maximum of 100 characters.');

return trimmedValue;
}

const sequenceTransition = {
duration: 0.25,
type: 'spring',
stiffness: 260,
damping: 20
};

useEffect(() => {
setValue(search);
}, [search]);

return (
<div className='flex mt-8 gap-x-2'>
<div className='relative flex items-center w-full'>
<motion.input
type="text"
placeholder="Search for a bot by id, description, or category..."
value={value}
disabled={loading}
className='w-full p-3 font-medium rounded-md outline-none disabled:pointer-events-none disabled:opacity-70 text-secondary placeholder-placeholder bg-secondary hover:bg-tertiary focus-visible:bg-tertiary'
onChange={event => setValue(event.target.value)}
autoComplete='off'
maxLength={100}
spellCheck='false'
onKeyUp={event => {
if (event.key === 'Enter') {
const validatedValue = validateValue();
if (validatedValue) fetchBots(validatedValue);
}
}}
initial={{ opacity: 0, y: -25 }}
animate={{ opacity: 1, y: 0 }}
transition={{ ...sequenceTransition, delay: 0.3 }}
/>

{search && (
<motion.button
className='absolute right-2 p-1.5 rounded-md text-secondary hover:bg-tertiary'
onClick={() => {
setValue('');
fetchBots('');
}}
disabled={loading}
>
<FiX className='text-xl' />
</motion.button>
)}
</div>

<motion.button
className='p-3 rounded-md bg-secondary text-secondary hover:bg-tertiary disabled:pointer-events-none disabled:opacity-70'
onClick={() => {
const validatedValue = validateValue();
if (validatedValue) fetchBots(validatedValue);
}}
disabled={loading || validateValue(false) === false}
initial={{ opacity: 0, y: -25 }}
animate={{ opacity: 1, y: 0 }}
transition={{ ...sequenceTransition, delay: 0.3 }}
>
<FiSearch className='text-xl' />
</motion.button>
</div>
);
}
87 changes: 0 additions & 87 deletions client/app/(bots)/bots/components/Hero/TopCategories/index.jsx

This file was deleted.

Loading

0 comments on commit 7e2a1c4

Please sign in to comment.