feat(ui): rough out boards UI

This commit is contained in:
psychedelicious 2023-06-15 16:43:50 +10:00
parent 5865ecd530
commit d306a84447
5 changed files with 172 additions and 60 deletions

View File

@ -0,0 +1,36 @@
import { Flex, Icon, Text } from '@chakra-ui/react';
import { FaPlus } from 'react-icons/fa';
const AddBoardButton = () => {
return (
<Flex
sx={{
flexDir: 'column',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
w: 'full',
h: 'full',
gap: 1,
}}
>
<Flex
sx={{
justifyContent: 'center',
alignItems: 'center',
borderWidth: '1px',
borderRadius: 'base',
borderColor: 'base.800',
w: 'full',
h: 'full',
aspectRatio: '1/1',
}}
>
<Icon boxSize={8} color="base.700" as={FaPlus} />
</Flex>
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>New Board</Text>
</Flex>
);
};
export default AddBoardButton;

View File

@ -0,0 +1,53 @@
import { Grid } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { selectBoardsAll } from 'features/gallery/store/boardSlice';
import { memo } from 'react';
import HoverableBoard from './HoverableBoard';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import AddBoardButton from './AddBoardButton';
const selector = createSelector(
selectBoardsAll,
(boards) => {
return { boards };
},
defaultSelectorOptions
);
const BoardsList = () => {
const { boards } = useAppSelector(selector);
return (
<OverlayScrollbarsComponent
defer
style={{ height: '100%', width: '100%' }}
options={{
scrollbars: {
visibility: 'auto',
autoHide: 'move',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
}}
>
<Grid
className="list-container"
sx={{
gap: 2,
gridTemplateRows: '5rem 5rem',
gridAutoFlow: 'column dense',
gridAutoColumns: '4rem',
}}
>
<AddBoardButton />
{boards.map((board) => (
<HoverableBoard key={board.board_id} board={board} />
))}
</Grid>
</OverlayScrollbarsComponent>
);
};
export default memo(BoardsList);

View File

@ -1,7 +1,15 @@
import { Box, Image, MenuItem, MenuList, Text } from '@chakra-ui/react'; import {
Box,
Flex,
Icon,
Image,
MenuItem,
MenuList,
Text,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { memo, useCallback, useState } from 'react'; import { PropsWithChildren, memo, useCallback, useState } from 'react';
import { FaImage } from 'react-icons/fa'; import { FaFolder, FaImage } from 'react-icons/fa';
import { ContextMenu } from 'chakra-ui-contextmenu'; import { ContextMenu } from 'chakra-ui-contextmenu';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ExternalLinkIcon } from '@chakra-ui/icons'; import { ExternalLinkIcon } from '@chakra-ui/icons';
@ -11,10 +19,12 @@ import { EntityId, createSelector } from '@reduxjs/toolkit';
import { import {
selectFilteredImagesIds, selectFilteredImagesIds,
selectImagesById, selectImagesById,
} from '../store/imagesSlice'; } from '../../store/imagesSlice';
import { RootState } from '../../../app/store/store'; import { RootState } from '../../../../app/store/store';
import { defaultSelectorOptions } from '../../../app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { boardIdSelected } from 'features/gallery/store/boardSlice';
interface HoverableBoardProps { interface HoverableBoardProps {
board: BoardDTO; board: BoardDTO;
@ -26,20 +36,16 @@ interface HoverableBoardProps {
const HoverableBoard = memo(({ board }: HoverableBoardProps) => { const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { board_name, board_id, cover_image_name } = board; const { board_name, board_id, cover_image_url } = board;
const coverImage = useAppSelector((state) =>
selectImagesById(state, cover_image_name as EntityId)
);
const { t } = useTranslation(); const { t } = useTranslation();
const handleSelectBoard = useCallback(() => { const handleSelectBoard = useCallback(() => {
// dispatch(imageSelected(board_id)); dispatch(boardIdSelected(board_id));
}, []); }, [board_id, dispatch]);
return ( return (
<Box sx={{ w: 'full', h: 'full', touchAction: 'none' }}> <Box sx={{ touchAction: 'none' }}>
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }} menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => ( renderMenu={() => (
@ -54,42 +60,50 @@ const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
)} )}
> >
{(ref) => ( {(ref) => (
<Box <Flex
position="relative" position="relative"
key={board_id} key={board_id}
userSelect="none" userSelect="none"
onClick={handleSelectBoard} onClick={handleSelectBoard}
ref={ref} ref={ref}
sx={{ sx={{
display: 'flex',
flexDir: 'column', flexDir: 'column',
justifyContent: 'center', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
cursor: 'pointer',
w: 'full', w: 'full',
h: 'full', h: 'full',
transition: 'transform 0.2s ease-out', gap: 1,
aspectRatio: '1/1',
cursor: 'pointer',
}} }}
> >
<Image <Flex
loading="lazy"
// objectFit={
// shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
// }
draggable={false}
rounded="md"
src={coverImage ? coverImage.thumbnail_url : undefined}
fallback={<FaImage />}
sx={{ sx={{
width: '100%', justifyContent: 'center',
height: '100%', alignItems: 'center',
maxWidth: '100%', borderWidth: '1px',
maxHeight: '100%', borderRadius: 'base',
borderColor: 'base.800',
w: 'full',
h: 'full',
aspectRatio: '1/1',
}} }}
/> >
<Text textAlign="center">{board_name}</Text> {cover_image_url ? (
</Box> <Image
loading="lazy"
objectFit="cover"
draggable={false}
rounded="md"
src={cover_image_url}
fallback={<IAIImageFallback />}
sx={{}}
/>
) : (
<Icon boxSize={8} color="base.700" as={FaFolder} />
)}
</Flex>
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>{board_name}</Text>
</Flex>
)} )}
</ContextMenu> </ContextMenu>
</Box> </Box>

View File

@ -57,9 +57,11 @@ import { receivedPageOfImages } from 'services/thunks/image';
import { boardSelector } from '../store/boardSelectors'; import { boardSelector } from '../store/boardSelectors';
import { BoardDTO, ImageDTO } from '../../../services/api'; import { BoardDTO, ImageDTO } from '../../../services/api';
import { isBoardDTO, isImageDTO } from '../../../services/types/guards'; import { isBoardDTO, isImageDTO } from '../../../services/types/guards';
import HoverableBoard from './HoverableBoard'; import HoverableBoard from './Boards/HoverableBoard';
import IAIInput from '../../../common/components/IAIInput'; import IAIInput from '../../../common/components/IAIInput';
import { boardCreated } from '../../../services/thunks/board'; import { boardCreated } from '../../../services/thunks/board';
import BoardsList from './Boards/BoardsList';
import { selectBoardsById } from '../store/boardSlice';
const itemSelector = createSelector( const itemSelector = createSelector(
[(state: RootState) => state], [(state: RootState) => state],
@ -70,24 +72,23 @@ const itemSelector = createSelector(
let areMoreAvailable = false; let areMoreAvailable = false;
let isLoading = true; let isLoading = true;
if (gallery.galleryView === 'images' || gallery.galleryView === 'assets') { const { categories } = images;
const { categories } = images;
const allImages = selectImagesAll(state); const allImages = selectImagesAll(state);
items = allImages.filter((i) => categories.includes(i.image_category)); items = allImages.filter((i) => categories.includes(i.image_category));
areMoreAvailable = items.length < images.total; areMoreAvailable = items.length < images.total;
isLoading = images.isLoading; isLoading = images.isLoading;
} else if (gallery.galleryView === 'boards') {
items = Object.values(boards.entities) as BoardDTO[]; const selectedBoard = boards.selectedBoardId
areMoreAvailable = items.length < boards.total; ? selectBoardsById(state, boards.selectedBoardId)
isLoading = boards.isLoading; : undefined;
}
return { return {
items, items,
isLoading, isLoading,
areMoreAvailable, areMoreAvailable,
categories: images.categories, categories: images.categories,
selectedBoard,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -153,7 +154,7 @@ const ImageGalleryContent = () => {
boards, boards,
} = useAppSelector(mainSelector); } = useAppSelector(mainSelector);
const { items, areMoreAvailable, isLoading, categories } = const { items, areMoreAvailable, isLoading, categories, selectedBoard } =
useAppSelector(itemSelector); useAppSelector(itemSelector);
const handleLoadMoreImages = useCallback(() => { const handleLoadMoreImages = useCallback(() => {
@ -247,17 +248,14 @@ const ImageGalleryContent = () => {
size="sm" size="sm"
icon={<FaServer />} icon={<FaServer />}
/> />
<IAIIconButton
tooltip={t('gallery.boards')}
aria-label={t('gallery.boards')}
onClick={handleClickBoardsView}
isChecked={galleryView === 'boards'}
size="sm"
icon={<FaFolder />}
/>
</ButtonGroup> </ButtonGroup>
{selectedBoard && (
<Flex>
<Text>{selectedBoard.board_name}</Text>
</Flex>
)}
<Flex gap={2}> <Flex gap={2}>
<IAIPopover {/* <IAIPopover
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
tooltip="Add Board" tooltip="Add Board"
@ -283,7 +281,7 @@ const ImageGalleryContent = () => {
Create Create
</IAIButton> </IAIButton>
</Flex> </Flex>
</IAIPopover> </IAIPopover> */}
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
@ -342,6 +340,9 @@ const ImageGalleryContent = () => {
/> />
</Flex> </Flex>
</Flex> </Flex>
<Box>
<BoardsList />
</Box>
<Flex direction="column" gap={2} h="full"> <Flex direction="column" gap={2} h="full">
{items.length || areMoreAvailable ? ( {items.length || areMoreAvailable ? (
<> <>

View File

@ -1,4 +1,5 @@
import { import {
EntityId,
PayloadAction, PayloadAction,
Update, Update,
createEntityAdapter, createEntityAdapter,
@ -19,6 +20,7 @@ type AdditionalBoardsState = {
limit: number; limit: number;
total: number; total: number;
isLoading: boolean; isLoading: boolean;
selectedBoardId: EntityId | null;
}; };
export const initialBoardsState = export const initialBoardsState =
@ -27,6 +29,7 @@ export const initialBoardsState =
limit: 0, limit: 0,
total: 0, total: 0,
isLoading: false, isLoading: false,
selectedBoardId: null,
}); });
export type BoardsState = typeof initialBoardsState; export type BoardsState = typeof initialBoardsState;
@ -44,6 +47,9 @@ const boardsSlice = createSlice({
boardRemoved: (state, action: PayloadAction<string>) => { boardRemoved: (state, action: PayloadAction<string>) => {
boardsAdapter.removeOne(state, action.payload); boardsAdapter.removeOne(state, action.payload);
}, },
boardIdSelected: (state, action: PayloadAction<string>) => {
state.selectedBoardId = action.payload;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(receivedBoards.pending, (state) => { builder.addCase(receivedBoards.pending, (state) => {
@ -71,7 +77,9 @@ export const {
selectTotal: selectBoardsTotal, selectTotal: selectBoardsTotal,
} = boardsAdapter.getSelectors<RootState>((state) => state.boards); } = boardsAdapter.getSelectors<RootState>((state) => state.boards);
export const { boardUpserted, boardUpdatedOne, boardRemoved } = export const { boardUpserted, boardUpdatedOne, boardRemoved, boardIdSelected } =
boardsSlice.actions; boardsSlice.actions;
export const boardsSelector = (state: RootState) => state.boards;
export default boardsSlice.reducer; export default boardsSlice.reducer;