refactor explore ui

This commit is contained in:
Ajay Bura 2024-03-16 19:28:24 +05:30
parent a6204f8fb4
commit 958f14712c
6 changed files with 396 additions and 268 deletions

View file

@ -1 +0,0 @@
export * from './Content';

View file

@ -1,34 +1,45 @@
import React, { ComponentProps, ReactNode } from 'react';
import { Box, Scroll, Text, as } from 'folds';
import { Box, Header, Text, as } from 'folds';
import classNames from 'classnames';
import { ContainerColor } from '../../styles/ContainerColor.css';
import * as css from './style.css';
export const Content = as<'div'>(({ className, children, ...props }, ref) => (
export const Page = as<'div'>(({ className, ...props }, ref) => (
<Box
grow="Yes"
direction="Column"
className={classNames(ContainerColor({ variant: 'Surface' }), className)}
{...props}
ref={ref}
>
<Scroll hideTrack>
<div className={css.Content}>{children}</div>
</Scroll>
</Box>
/>
));
export const ContentHeroSection = as<'div', ComponentProps<typeof Box>>(
export const PageHeader = as<'div'>(({ className, ...props }, ref) => (
<Header
as="header"
size="600"
className={classNames(css.PageHeader, className)}
{...props}
ref={ref}
/>
));
export const PageContent = as<'div'>(({ className, ...props }, ref) => (
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
));
export const PageHeroSection = as<'div', ComponentProps<typeof Box>>(
({ className, ...props }, ref) => (
<Box
direction="Column"
className={classNames(css.ContentHeroSection, className)}
className={classNames(css.PageHeroSection, className)}
{...props}
ref={ref}
/>
)
);
export function ContentHero({
export function PageHero({
icon,
title,
subTitle,
@ -54,6 +65,6 @@ export function ContentHero({
);
}
export const ContentBody = as<'div'>(({ className, ...props }, ref) => (
<div className={classNames(css.ContentBody, className)} {...props} ref={ref} />
export const PageContentCenter = as<'div'>(({ className, ...props }, ref) => (
<div className={classNames(css.PageContentCenter, className)} {...props} ref={ref} />
));

View file

@ -0,0 +1 @@
export * from './Page';

View file

@ -1,16 +1,23 @@
import { style } from '@vanilla-extract/css';
import { DefaultReset, config, toRem } from 'folds';
export const Content = style([
export const PageHeader = style({
paddingLeft: config.space.S400,
paddingRight: config.space.S200,
borderBottomWidth: config.borderWidth.B300,
});
export const PageContent = style([
DefaultReset,
{
paddingLeft: config.space.S500,
paddingRight: config.space.S100,
paddingTop: config.space.S400,
paddingLeft: config.space.S400,
paddingRight: 0,
paddingBottom: toRem(100),
},
]);
export const ContentHeroSection = style([
export const PageHeroSection = style([
DefaultReset,
{
padding: '40px 0',
@ -20,7 +27,7 @@ export const ContentHeroSection = style([
},
]);
export const ContentBody = style([
export const PageContentCenter = style([
DefaultReset,
{
maxWidth: toRem(964),

View file

@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { Box, Icon, Icons, Text } from 'folds';
import { Box, Icon, Icons, Scroll, Text } from 'folds';
import { useNavigate } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { useClientConfig } from '../../../hooks/useClientConfig';
@ -12,7 +12,13 @@ import {
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { RoomSummaryLoader } from '../../../components/RoomSummaryLoader';
import { Content, ContentBody, ContentHero, ContentHeroSection } from '../../../components/content';
import {
Page,
PageContent,
PageContentCenter,
PageHero,
PageHeroSection,
} from '../../../components/page';
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import { getHomeRoomPath, getSpacePath, getSpaceRoomPath } from '../../pathUtils';
import { getOrphanParents } from '../../../utils/room';
@ -63,78 +69,84 @@ export function FeaturedRooms() {
);
return (
<Content>
<Box direction="Column" gap="200">
<ContentHeroSection>
<ContentHero
icon={<Icon size="600" src={Icons.Bulb} />}
title="Featured by Client"
subTitle="Find and explore public rooms and spaces featured by client provider."
/>
</ContentHeroSection>
<ContentBody>
<Box direction="Column" gap="700">
{spaces && spaces.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Spaces</Text>
<RoomCardGrid>
{spaces.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => (
<RoomCard
roomIdOrAlias={roomIdOrAlias}
joinedRoomId={joinedRoomId(roomIdOrAlias)}
avatarUrl={roomSummary?.avatar_url}
name={roomSummary?.name}
topic={roomSummary?.topic}
memberCount={roomSummary?.num_joined_members}
onView={navigateSpace}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer
name={name}
topic={topic}
requestClose={requestClose}
/>
)}
/>
)}
</RoomSummaryLoader>
))}
</RoomCardGrid>
<Page>
<Box grow="Yes">
<Scroll hideTrack visibility="Hover">
<PageContent>
<PageContentCenter>
<Box direction="Column" gap="200">
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Bulb} />}
title="Featured by Client"
subTitle="Find and explore public rooms and spaces featured by client provider."
/>
</PageHeroSection>
<Box direction="Column" gap="700">
{spaces && spaces.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Spaces</Text>
<RoomCardGrid>
{spaces.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => (
<RoomCard
roomIdOrAlias={roomIdOrAlias}
joinedRoomId={joinedRoomId(roomIdOrAlias)}
avatarUrl={roomSummary?.avatar_url}
name={roomSummary?.name}
topic={roomSummary?.topic}
memberCount={roomSummary?.num_joined_members}
onView={navigateSpace}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer
name={name}
topic={topic}
requestClose={requestClose}
/>
)}
/>
)}
</RoomSummaryLoader>
))}
</RoomCardGrid>
</Box>
)}
{rooms && rooms.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Rooms</Text>
<RoomCardGrid>
{rooms.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => (
<RoomCard
roomIdOrAlias={roomIdOrAlias}
joinedRoomId={joinedRoomId(roomIdOrAlias)}
avatarUrl={roomSummary?.avatar_url}
name={roomSummary?.name}
topic={roomSummary?.topic}
memberCount={roomSummary?.num_joined_members}
onView={navigateRoom}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer
name={name}
topic={topic}
requestClose={requestClose}
/>
)}
/>
)}
</RoomSummaryLoader>
))}
</RoomCardGrid>
</Box>
)}
</Box>
</Box>
)}
{rooms && rooms.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Rooms</Text>
<RoomCardGrid>
{rooms.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => (
<RoomCard
roomIdOrAlias={roomIdOrAlias}
joinedRoomId={joinedRoomId(roomIdOrAlias)}
avatarUrl={roomSummary?.avatar_url}
name={roomSummary?.name}
topic={roomSummary?.topic}
memberCount={roomSummary?.num_joined_members}
onView={navigateRoom}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer
name={name}
topic={topic}
requestClose={requestClose}
/>
)}
/>
)}
</RoomSummaryLoader>
))}
</RoomCardGrid>
</Box>
)}
</Box>
</ContentBody>
</PageContentCenter>
</PageContent>
</Scroll>
</Box>
</Content>
</Page>
);
}

View file

@ -7,12 +7,25 @@ import React, {
useRef,
useState,
} from 'react';
import { Box, Button, Chip, Icon, Icons, Input, Menu, PopOut, Spinner, Text, config } from 'folds';
import {
Box,
Button,
Chip,
Icon,
Icons,
Input,
Menu,
PopOut,
Scroll,
Spinner,
Text,
config,
} from 'folds';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import FocusTrap from 'focus-trap-react';
import { useQuery } from '@tanstack/react-query';
import { MatrixClient, Method, RoomType } from 'matrix-js-sdk';
import { Content, ContentHeroSection, ContentHero, ContentBody } from '../../../components/content';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import { RoomCard, RoomCardGrid } from '../../../components/room-card';
@ -57,16 +70,14 @@ export function PublicRooms() {
const [searchParams] = useSearchParams();
const serverSearchParams = getServerSearchParams(searchParams);
const isSearch = serverSearchParams.term;
const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
const roomTypeFilters = useRoomTypeFilters();
const [openLimit, setOpenLimit] = useState(false);
const resetScroll = useCallback(() => {
const container = containerRef.current;
if (!container) return;
const scroll = container.children[0];
const scroll = scrollRef.current;
if (scroll) scroll.scrollTop = 0;
}, []);
@ -165,191 +176,278 @@ export function PublicRooms() {
});
};
const setLimit = (limit: string) => {
setOpenLimit(false);
explore({ limit });
};
const handleLimitSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const limitInput = evt.currentTarget.limitInput as HTMLInputElement;
if (!limitInput) return;
const limit = limitInput.value.trim();
if (!limit) return;
explore({ limit });
setLimit(limit);
};
return (
<Content ref={containerRef}>
<Box direction="Column" gap="200">
<ContentHeroSection direction="Column" gap="400">
<ContentHero
icon={<Icon size="600" src={Icons.Category} />}
title={server}
subTitle={`Find and explore public rooms and spaces on ${server} server.`}
/>
</ContentHeroSection>
<ContentBody>
<Box direction="Column" gap="700">
<Box direction="Column" gap="400">
<Box direction="Column" gap="300">
{isSearch ? (
<Text size="H4">{`Public Communities for "${serverSearchParams.term}"`}</Text>
) : (
<Text size="H4">Public Community</Text>
)}
<form onSubmit={handleSearchSubmit}>
<Input
ref={searchInputRef}
name="searchInput"
size="500"
variant="Background"
placeholder="Search"
before={
isSearch && isLoading ? (
<Spinner variant="Secondary" size="200" />
) : (
<Icon size="200" src={Icons.Search} />
)
}
after={
isSearch && (
<Chip
type="button"
variant="Secondary"
size="400"
radii="Pill"
aria-pressed
after={<Icon size="50" src={Icons.Cross} />}
onClick={handleSearchClear}
>
<Text size="B300">Clear</Text>
</Chip>
)
}
/>
</form>
<Box gap="200">
{roomTypeFilters.map((filter) => (
<Chip
key={filter.title}
onClick={handleRoomFilterClick}
data-room-filter={filter.value}
variant={filter.value === serverSearchParams.type ? 'Success' : 'Surface'}
aria-pressed={filter.value === serverSearchParams.type}
<Page>
<PageHeader>
{isSearch ? (
<>
<Box grow="Yes" basis="No">
<Chip
size="500"
variant="Surface"
radii="Pill"
before={<Icon size="100" src={Icons.ArrowLeft} />}
onClick={handleSearchClear}
>
<Text size="T300">{server}</Text>
</Chip>
</Box>
<Box grow="No" justifyContent="Center" alignItems="Center" gap="200">
<Icon size="400" src={Icons.Search} />
<Text size="H3" truncate>
Search
</Text>
</Box>
<Box grow="Yes" />
</>
) : (
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
<Icon size="400" src={Icons.Category} />
<Text size="H3" truncate>
{server}
</Text>
</Box>
)}
</PageHeader>
<Box grow="Yes">
<Scroll ref={scrollRef} hideTrack visibility="Hover">
<PageContent>
<PageContentCenter>
<Box direction="Column" gap="200">
<Box direction="Column" gap="500">
<Box as="form" direction="Column" gap="100" onSubmit={handleSearchSubmit}>
<span data-spacing-node />
<Text size="L400">Search</Text>
<Input
ref={searchInputRef}
style={{ paddingRight: config.space.S300 }}
name="searchInput"
size="500"
variant="Background"
placeholder="Search for keyword"
before={
filter.value === serverSearchParams.type && (
<Icon size="100" src={Icons.Check} />
isSearch && isLoading ? (
<Spinner variant="Secondary" size="200" />
) : (
<Icon size="200" src={Icons.Search} />
)
}
outlined
>
<Text size="T200">{filter.title}</Text>
</Chip>
))}
after={
isSearch ? (
<Chip
type="button"
variant="Secondary"
size="400"
radii="Pill"
outlined
after={<Icon size="50" src={Icons.Cross} />}
onClick={handleSearchClear}
>
<Text size="B300">Clear</Text>
</Chip>
) : (
<Chip type="submit" variant="Primary" size="400" radii="Pill" outlined>
<Text size="B300">Enter</Text>
</Chip>
)
}
/>
</Box>
<Box direction="Column" gap="400">
<Box direction="Column" gap="300">
{isSearch ? (
<Text size="H4">{`Public Communities for "${serverSearchParams.term}"`}</Text>
) : (
<Text size="H4">Public Communities</Text>
)}
<Box gap="200">
{roomTypeFilters.map((filter) => (
<Chip
key={filter.title}
onClick={handleRoomFilterClick}
data-room-filter={filter.value}
variant={
filter.value === serverSearchParams.type ? 'Success' : 'Surface'
}
aria-pressed={filter.value === serverSearchParams.type}
before={
filter.value === serverSearchParams.type && (
<Icon size="100" src={Icons.Check} />
)
}
outlined
>
<Text size="T200">{filter.title}</Text>
</Chip>
))}
<Box grow="Yes" data-spacing-node />
<PopOut
open={openLimit}
align="End"
position="Bottom"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setOpenLimit(false),
clickOutsideDeactivates: true,
}}
>
<Menu variant="Surface">
<Box
direction="Column"
gap="300"
style={{ padding: config.space.S200 }}
>
<Box direction="Column" gap="100">
<Text size="L400">Presets</Text>
<Box gap="100" wrap="Wrap">
<Chip
variant="SurfaceVariant"
onClick={() => setLimit('24')}
radii="Pill"
>
<Text size="T200">24</Text>
</Chip>
<Chip
variant="SurfaceVariant"
onClick={() => setLimit('48')}
radii="Pill"
>
<Text size="T200">48</Text>
</Chip>
<Chip
variant="SurfaceVariant"
onClick={() => setLimit('96')}
radii="Pill"
>
<Text size="T200">96</Text>
</Chip>
</Box>
</Box>
<Box
as="form"
onSubmit={handleLimitSubmit}
direction="Column"
gap="200"
>
<Box direction="Column" gap="100">
<Text size="L400">Custom Limit</Text>
<Input
name="limitInput"
size="300"
variant="Background"
defaultValue={
serverSearchParams.limit ?? FALLBACK_ROOMS_LIMIT
}
min={1}
step={1}
outlined
type="number"
radii="400"
aria-label="Per Page Item Limit"
/>
</Box>
<Button type="submit" size="300" variant="Primary" radii="400">
<Text size="B300">Change Limit</Text>
</Button>
</Box>
</Box>
</Menu>
</FocusTrap>
}
>
{(anchorRef) => (
<Chip
ref={anchorRef}
onClick={() => setOpenLimit(!openLimit)}
aria-pressed={openLimit}
radii="Pill"
size="400"
variant="SurfaceVariant"
after={<Icon size="100" src={Icons.ChevronBottom} />}
>
<Text size="T200" truncate>{`Page Limit: ${
serverSearchParams.limit ?? FALLBACK_ROOMS_LIMIT
}`}</Text>
</Chip>
)}
</PopOut>
</Box>
</Box>
{isLoading && !error && <Text>loading...</Text>}
{error && <Text>{error.message}</Text>}
<RoomCardGrid>
{data?.chunk.map((chunkRoom) => (
<RoomCard
key={chunkRoom.room_id}
roomIdOrAlias={chunkRoom.canonical_alias ?? chunkRoom.room_id}
joinedRoomId={mx.getRoom(chunkRoom.room_id)?.roomId}
avatarUrl={chunkRoom.avatar_url}
name={chunkRoom.name}
topic={chunkRoom.topic}
memberCount={chunkRoom.num_joined_members}
roomType={chunkRoom.room_type}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer
name={name}
topic={topic}
requestClose={requestClose}
/>
)}
/>
))}
</RoomCardGrid>
{data && (
<>
<span data-spacing-node />
<Box justifyContent="Center" gap="200">
<Button
onClick={paginateBack}
size="300"
fill="Soft"
disabled={!data.prev_batch}
>
<Text size="B300" truncate>
Previous Page
</Text>
</Button>
<Box data-spacing-node grow="Yes" />
<Button
onClick={paginateFront}
size="300"
fill="Solid"
disabled={!data.next_batch}
>
<Text size="B300" truncate>
Next Page
</Text>
</Button>
</Box>
</>
)}
</Box>
</Box>
</Box>
{isLoading && !error && <Text>loading...</Text>}
{error && <Text>{error.message}</Text>}
<RoomCardGrid>
{data?.chunk.map((chunkRoom) => (
<RoomCard
key={chunkRoom.room_id}
roomIdOrAlias={chunkRoom.canonical_alias ?? chunkRoom.room_id}
joinedRoomId={mx.getRoom(chunkRoom.room_id)?.roomId}
avatarUrl={chunkRoom.avatar_url}
name={chunkRoom.name}
topic={chunkRoom.topic}
memberCount={chunkRoom.num_joined_members}
roomType={chunkRoom.room_type}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
)}
/>
))}
</RoomCardGrid>
{data && (
<>
<span data-spacing-node />
<Box justifyContent="Center" gap="200">
<PopOut
open={openLimit}
align="Center"
position="Top"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setOpenLimit(false),
clickOutsideDeactivates: true,
}}
>
<Menu variant="Surface">
<Box
as="form"
onSubmit={handleLimitSubmit}
style={{ padding: config.space.S200 }}
direction="Column"
gap="200"
>
<Input
name="limitInput"
size="300"
variant="Background"
defaultValue={serverSearchParams.limit ?? FALLBACK_ROOMS_LIMIT}
min={1}
step={1}
outlined
type="number"
radii="300"
aria-label="Per Page Item Limit"
/>
<Button type="submit" size="300" variant="Primary" radii="300">
<Text size="B300">Change Limit</Text>
</Button>
</Box>
</Menu>
</FocusTrap>
}
>
{(anchorRef) => (
<Chip
ref={anchorRef}
onClick={() => setOpenLimit(!openLimit)}
aria-pressed={openLimit}
radii="Pill"
size="500"
variant="SurfaceVariant"
after={<Icon size="200" src={Icons.ChevronBottom} />}
>
<Text size="B300" truncate>{`Page Limit: ${data.chunk.length}`}</Text>
</Chip>
)}
</PopOut>
<Box data-spacing-node grow="Yes" />
<Button
onClick={paginateBack}
size="300"
fill="Soft"
disabled={!data.prev_batch}
>
<Text size="B300" truncate>
Previous Page
</Text>
</Button>
<Button
onClick={paginateFront}
size="300"
fill="Solid"
disabled={!data.next_batch}
>
<Text size="B300" truncate>
Next Page
</Text>
</Button>
</Box>
</>
)}
</Box>
</Box>
</ContentBody>
</PageContentCenter>
</PageContent>
</Scroll>
</Box>
</Content>
</Page>
);
}