mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-03 22:44:24 +01:00
feat: makers page base filters, maker card skeleton, update mocks api
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>)
|
||||
|
||||
|
||||
@@ -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🔩FUN’s 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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const events: Tournament['events'] = [
|
||||
image: getCoverImage(),
|
||||
links: [],
|
||||
location: "Online",
|
||||
type: TournamentEventTypeEnum.Workshop,
|
||||
type: TournamentEventTypeEnum.OnlineMeetup,
|
||||
website: "https://event.name"
|
||||
},
|
||||
]
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user