feat: makers page base filters, maker card skeleton, update mocks api

This commit is contained in:
MTG2000
2022-09-05 14:46:37 +03:00
parent d9a5db2a9d
commit e727cc6aa9
12 changed files with 224 additions and 70 deletions

View File

@@ -9,6 +9,7 @@ interface Props {
className?: string
size?: "sm" | 'md' | 'lg'
variant?: 'blank' | 'fill'
isDisabled?: boolean
}
@@ -32,7 +33,8 @@ const IconButton = React.forwardRef<any, PropsWithChildren<Props>>(({
children,
onClick = () => { },
onKeyDown,
variant = 'blank'
variant = 'blank',
isDisabled,
}, ref) => {
if (href)
@@ -57,12 +59,16 @@ const IconButton = React.forwardRef<any, PropsWithChildren<Props>>(({
ref={ref}
type='button'
className={`
${className}
${sizeToPadding[size]}
${baseBtnStyles[variant]}
active:scale-95 rounded-full ${className}`}
active:scale-95 rounded-full
${isDisabled && "opacity-60"}
`}
style={{ lineHeight: 0 }}
onClick={onClick}
onKeyDown={onKeyDown}
disabled={isDisabled}
>
{children}
</button>

View File

@@ -122,7 +122,15 @@ export default function BasicSelectInput<T extends Record<string, any>, IsMulti
}
function getOptionComponent<T extends Record<string, any>>(renderOption: Props<T>['renderOption'], labelField: Props<T>['labelField']) {
const _render = renderOption ?? ((option) => <div className={`flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 ${option.isSelected ? "bg-gray-100 text-gray-800" : "hover:bg-gray-50"} cursor-pointer`}>
const _render = renderOption ?? ((option) => <div
className={`
flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 cursor-pointer
${!(option.isSelected || option.isFocused) ?
"hover:bg-gray-50"
:
option.isSelected ? "bg-gray-100 text-gray-800" : "bg-gray-50"
}
`}>
{option.data[labelField]}
</div>)

View File

@@ -7,6 +7,18 @@ import useEmblaCarousel from 'embla-carousel-react'
import { useCallback, useEffect, useState } from "react";
const headerLinks = [
{
title:
<>
<p className="text-body1 font-bolder text-white">The Long Night tournament is coming!!!</p>
<p className="text-body3 font-medium text-white mt-8">1st Oct - 31st Nov, 2022</p>
</>,
img: "https://imagedelivery.net/wyrwp3c-j0gDDUWgnE7lig/1d5d2c86-fe46-4478-6909-bb3c425c0d00/public",
link: {
content: "Register Now",
url: "/tournaments/12",
},
},
{
title: <p className="text-body1 font-bolder text-white">Explore a fun directory of lightning web apps</p>,
img: Assets.Images_ExploreHeader1,
@@ -15,18 +27,6 @@ const headerLinks = [
url: "https://form.jotform.com/220301236112030",
},
},
{
title:
<>
<p className="text-body1 font-bolder text-white">Take part in BOLT🔩FUNs Shock the Web 2 </p>
<p className="text-body3 font-medium text-white mt-8">16th - 19th June, 2022</p>
</>,
img: Assets.Images_ExploreHeader2,
link: {
content: "Register Now",
url: "https://bolt.fun/hackathons/shock-the-web-2/",
},
},
];
@@ -72,7 +72,7 @@ export default function Header() {
{headerLinks[0].title}
</div>
<Button href={headerLinks[0].link.url} newTab color="white" className="mt-24">
<Button href={headerLinks[0].link.url} color="white" className="mt-24">
{headerLinks[0].link.content}
</Button>
</div>

View File

@@ -0,0 +1,39 @@
import Card from 'src/Components/Card/Card';
import Badge from 'src/Components/Badge/Badge';
import Skeleton from 'react-loading-skeleton';
export default function MakerCardSkeleton() {
return (
<Card>
<div className="flex flex-wrap gap-24 items-start">
<div className="shrink-0 w-64 md:w-80 aspect-square">
<Skeleton circle width={"100%"} height={'100%'} />
</div>
<div className="flex flex-col gap-4 flex-1">
<p className="text-body2 text-gray-900 font-bold"><Skeleton width={"15ch"} /> </p>
<p className="text-body4 text-gray-600 font-medium"><Skeleton width={"25ch"} /> </p>
<ul className="hidden md:flex flex-wrap gap-8 mt-4">
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)}
</ul>
</div>
</div>
<hr className="hidden md:block bg-gray-200 mt-24"></hr>
<div className="md:hidden mt-24">
<p className="text-body5 text-gray-900 font-medium"><Skeleton width={"7ch"} /></p>
<ul className="flex flex-wrap gap-8 mt-4">
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)}
</ul>
</div>
<div className="mt-24">
<p className="text-body5 text-gray-900 font-medium"><Skeleton width={"7ch"} /></p>
<ul className="flex flex-wrap gap-8 mt-12">
{Array(3).fill(0).map((_, idx) => <li key={idx}><Badge size='sm' className='!text-body5'> <span className="opacity-0">Loading role</span> </Badge> </li>)} </ul>
</div>
</Card>
)
}

View File

@@ -11,7 +11,7 @@ import { openModal } from "src/redux/features/modals.slice";
import Card from 'src/Components/Card/Card';
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
import Badge from 'src/Components/Badge/Badge';
import { PAGES_ROUTES } from 'src/utils/routing';
import { createRoute, PAGES_ROUTES } from 'src/utils/routing';
type MakerType = GetMakersInTournamentQuery['getMakersInTournament'][number]
@@ -20,7 +20,7 @@ interface Props {
isMe?: boolean;
}
export default function MakerCard({ maker }: Props) {
export default function MakerCard({ maker, isMe }: Props) {
const dispatch = useAppDispatch()
@@ -35,27 +35,26 @@ export default function MakerCard({ maker }: Props) {
<p className="text-body2 text-gray-900 font-bold">{maker.name}</p>
<p className="text-body4 text-gray-600 font-medium">{maker.jobTitle}</p>
<ul className="hidden md:flex flex-wrap gap-8 mt-4">
{maker.roles.map(role => <li><Badge className='!text-body5'>{role.icon} {role.title}</Badge> </li>)}
{maker.roles.map(role => <li><Badge size='sm' className='!text-body5'>{role.icon} {role.title}</Badge> </li>)}
</ul>
</div>
<span className="ml-auto hidden md:inline-block"><Button color='white' href={PAGES_ROUTES.profile.editProfile} size='sm' className='ml-auto'>Edit Profile</Button></span>
{isMe && <span className="ml-auto hidden md:inline-block"><Button color='white' href={createRoute({ type: 'edit-profile' })} size='sm' className='ml-auto'>Edit Profile</Button></span>}
</div>
<hr className="hidden md:block bg-gray-200 mt-24"></hr>
<div className="md:hidden mt-24">
<p className="text-body5 text-gray-900 font-medium">🌈 Roles</p>
<ul className="flex flex-wrap gap-8 mt-4">
{maker.roles.map(role => <li><Badge className='!text-body5'>{role.icon} </Badge> </li>)}
<ul className="flex flex-wrap gap-8 mt-12">
{maker.roles.map(role => <li><Badge size='sm' className='!text-body5'>{role.icon} {role.title}</Badge> </li>)}
</ul>
</div>
<div className="mt-24">
<p className="text-body5 text-gray-900 font-medium">🛠 Skills</p>
<ul className="flex flex-wrap gap-8 mt-12">
{maker.skills.map(skill => <li><Badge className='!text-body5'>{skill.title}</Badge> </li>)}
{maker.skills.map(skill => <li><Badge size='sm' className='!text-body5'>{skill.title}</Badge> </li>)}
</ul>
</div>
<Button fullWidth color='white' href={PAGES_ROUTES.profile.editProfile} size='sm' className='mt-32 md:hidden'>Edit Profile</Button>
</Card>
{isMe && <Button fullWidth color='white' href={createRoute({ type: 'edit-profile' })} size='sm' className='mt-32 md:hidden'>Edit Profile</Button>} </Card>
)
}

View File

@@ -39,7 +39,15 @@ export default function MakersFilters(props: Props) {
value={props.roleValue}
onChange={props.onRoleChange}
options={options ?? []}
renderOption={option => <div className={`flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 ${option.isSelected ? "bg-gray-100 text-gray-800" : "hover:bg-gray-50"} cursor-pointer`}>
renderOption={option => <div
className={`
flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 cursor-pointer
${!(option.isSelected || option.isFocused) ?
"hover:bg-gray-50"
:
option.isSelected ? "bg-gray-100 text-gray-800" : "bg-gray-50"
}
`}>
{option.data.icon} {option.data.title}
</div>}

View File

@@ -1,56 +1,128 @@
import React, { useState } from 'react'
import { GenericMakerRole, Tournament, TournamentEventTypeEnum, useGetMakersInTournamentQuery, User } from 'src/graphql'
import { NetworkStatus } from '@apollo/client';
import { useDebouncedCallback, useDebouncedState } from '@react-hookz/web';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import IconButton from 'src/Components/IconButton/IconButton';
import LoadingPage from 'src/Components/LoadingPage/LoadingPage';
import { GenericMakerRole, GetMakersInTournamentQueryVariables, Tournament, useGetMakersInTournamentQuery, User } from 'src/graphql'
import ScrollToTop from 'src/utils/routing/scrollToTop';
import MakerCard from './MakerCard/MakerCard';
import EventCard from './MakerCard/MakerCard';
import MakerCardSkeleton from './MakerCard/MakerCard.Skeleton';
import MakersFilters from './MakersFilters/MakersFilters';
import EventsFilters from './MakersFilters/MakersFilters';
interface Props {
data: Pick<Tournament,
| 'id'>
}
const ITEMS_PER_PAGE = 15;
export default function MakersPage({ data: { id } }: Props) {
const [page, setPage] = useState(0)
const [searchFilter, setSearchFilter] = useState("");
const [debouncedsearchFilter, setDebouncedSearchFilter] = useDebouncedState("", 500);
const [roleFilter, setRoleFilter] = useState<GenericMakerRole | null>(null);
const loadingContainerCbRef = useCallback((e: HTMLDivElement) => {
if (e)
e.scrollIntoView({
behavior: 'smooth',
block: "center"
})
}, [])
const [queryFilter, setQueryFilter] = useState<GetMakersInTournamentQueryVariables>({
tournamentId: id,
roleId: roleFilter?.id ?? null,
search: debouncedsearchFilter,
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
});
const query = useGetMakersInTournamentQuery({
variables: {
tournamentId: id,
roleId: null,
search: null,
skip: null,
take: null,
},
})
variables: queryFilter,
notifyOnNetworkStatusChange: true,
});
const [searchFilter, setSearchFilter] = useState("")
const [roleFilter, setRoleFilter] = useState<GenericMakerRole | null>(null)
useEffect(() => {
setPage(0);
setQueryFilter(f => ({ ...f, search: debouncedsearchFilter, roleId: roleFilter?.id ?? null, skip: 0 }))
}, [debouncedsearchFilter, roleFilter]);
if (query.networkStatus === NetworkStatus.loading) return <LoadingPage />
const changeSearchFilter = (new_value: string) => {
setSearchFilter(new_value);
setDebouncedSearchFilter(new_value);
}
const nextPage = () => {
setPage(p => p + 1)
setQueryFilter(f => ({ ...f, skip: (f.skip ?? 0) + ITEMS_PER_PAGE }))
}
const prevPage = () => {
if (page === 0) return
setPage(p => p - 1)
setQueryFilter(f => ({ ...f, skip: (f.skip ?? 0) - ITEMS_PER_PAGE }))
}
const isFetchingMore = query.networkStatus === NetworkStatus.setVariables;
const itemsCount = query.data?.getMakersInTournament && query.data.getMakersInTournament.length;
if (query.loading) return <></>
return (
<div className='pb-42'>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-16 lg:gap-24">
<div className="md:col-span-2 lg:col-span-3"><MakerCard maker={query.data?.me as User} /></div>
<MakersFilters
searchValue={searchFilter}
onSearchChange={setSearchFilter}
roleValue={roleFilter}
onRoleChange={setRoleFilter}
/>
{/* {
events
.filter(event => {
if (!searchFilter) return true;
return event.title.search(new RegExp(searchFilter, 'i')) !== -1 || event.description.search(new RegExp(searchFilter, 'i')) !== -1
})
.filter(event => {
if (!eventFilter) return true;
return event.type === eventFilter;
})
.map(event => <EventCard key={event.id} event={event} />)
} */}
<div className="flex flex-col gap-16 lg:gap-24">
<MakerCard isMe maker={(query.data?.me ?? query.previousData?.me) as User} />
<div className="flex flex-col gap-16">
<h3 className="text-body1 text-gray-900 font-bold mt-24">Makers 👾</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-16 lg:gap-24">
<MakersFilters
searchValue={searchFilter}
onSearchChange={changeSearchFilter}
roleValue={roleFilter}
onRoleChange={setRoleFilter}
/>
</div>
</div>
{isFetchingMore ?
<>
<div ref={loadingContainerCbRef} >
<MakerCardSkeleton />
</div>
<MakerCardSkeleton />
<MakerCardSkeleton />
</>
:
(itemsCount !== 0 ?
query.data?.getMakersInTournament.map(maker => <MakerCard key={maker.id} maker={maker} />) :
<div className="py-80 text-center text-body2">
<p className="text-gray-400">No makers found here...</p>
</div>)
}
<div className='flex justify-center gap-36 text-gray-400'>
<IconButton isDisabled={!itemsCount || page === 0} onClick={prevPage}>
<FaChevronLeft />
</IconButton>
<IconButton isDisabled={!itemsCount || itemsCount < ITEMS_PER_PAGE} onClick={nextPage} >
<FaChevronRight />
</IconButton>
</div>
</div>
</div>
)
}

View File

@@ -68,7 +68,7 @@ export const events: Tournament['events'] = [
image: getCoverImage(),
links: [],
location: "Online",
type: TournamentEventTypeEnum.Workshop,
type: TournamentEventTypeEnum.OnlineMeetup,
website: "https://event.name"
},
]

View File

@@ -141,7 +141,7 @@ const tournaments = [
},
] as Tournament[];
export const users: (User | MyProfile)[] = [{
export const users: (User & MyProfile)[] = [{
id: 123,
email: "mtg0987654321@gmail.com",
avatar: "https://avatars.dicebear.com/api/bottts/Mtgmtg.svg",

View File

@@ -1,6 +1,6 @@
import { graphql } from 'msw'
import { allCategories, getAllHackathons, getAllMakersRoles, getAllMakersSkills, getCategory, getFeed, getMyDrafts, getPostById, getProject, getTournamentById, getTrendingPosts, hottestProjects, me, newProjects, popularTags, profile, projectsByCategory, searchProjects } from './resolvers'
import { allCategories, getAllHackathons, getAllMakersRoles, getAllMakersSkills, getCategory, getFeed, getMakersInTournament, getMyDrafts, getPostById, getProject, getTournamentById, getTrendingPosts, hottestProjects, me, newProjects, popularTags, profile, projectsByCategory, searchProjects } from './resolvers'
import {
NavCategoriesQuery,
ExploreProjectsQuery,
@@ -35,6 +35,7 @@ import {
MyProfileRolesSkillsQuery,
GetAllRolesQuery,
GetMakersInTournamentQuery,
GetMakersInTournamentQueryVariables,
} from 'src/graphql'
const delay = (ms = 1000) => new Promise((res) => setTimeout(res, ms + Math.random() * 1000))
@@ -285,13 +286,13 @@ export const handlers = [
)
}),
graphql.query<GetMakersInTournamentQuery>('GetMakersInTournament', async (req, res, ctx) => {
graphql.query<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>('GetMakersInTournament', async (req, res, ctx) => {
await delay()
return res(
ctx.data({
me: { ...me() },
getMakersInTournament: []
getMakersInTournament: getMakersInTournament(req.variables)
})
)
}),

View File

@@ -1,5 +1,5 @@
import { MOCK_DATA } from "./data";
import { MyProfile, Query, QueryGetFeedArgs, QueryGetPostByIdArgs, User } from 'src/graphql'
import { GetMakersInTournamentQueryVariables, MyProfile, Query, QueryGetFeedArgs, QueryGetPostByIdArgs, User } from 'src/graphql'
import { Chance } from "chance";
import { tags } from "./data/tags";
import { hackathons } from "./data/hackathon";
@@ -78,8 +78,6 @@ export function me() {
} as MyProfile
}
export function profile() {
return { ...MOCK_DATA['user'], __typename: 'User' } as User
}
@@ -97,4 +95,21 @@ export function getMyDrafts(): Query['getMyDrafts'] {
export function getTournamentById(id: number) {
return MOCK_DATA['tournaments'][0]
}
}
export function getMakersInTournament(vars: GetMakersInTournamentQueryVariables) {
const offsetStart = vars.skip ?? 0;
const offsetEnd = offsetStart + (vars.take ?? 15)
return MOCK_DATA.users.slice(1)
.filter(u => {
if (!vars.search) return true;
return [u.name, u.jobTitle].some(attr => attr?.search(new RegExp(vars.search!, 'i')) !== -1)
})
.filter(u => {
if (!vars.roleId) return true;
return u.roles.some(r => r.id === vars.roleId)
})
.slice(offsetStart, offsetEnd) as User[];
}

View File

@@ -31,6 +31,9 @@ type RouteOptions =
id: string | number,
username?: string,
}
| {
type: "edit-profile",
}
export function createRoute(options: RouteOptions) {
@@ -57,6 +60,9 @@ export function createRoute(options: RouteOptions) {
return `/profile/${options.id}`
+ (options.username ? `/${toSlug(options.username)}` : "");
if (options.type === 'edit-profile')
return '/edit-profile'
return ""
}