Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

Commit

Permalink
feat: infinite scroll on assets (#130)
Browse files Browse the repository at this point in the history
* update Loading component to be more useful

* wip: infinite scroll

* wip: infinite scroll

* move assets per page number to a variable

* fix: projects not showing

* Add sort param to asset query

* fix assetContainer style, move scroll code

* wip: get assets working in dashboard and settings page

* pass hasNextPage to dashboards project creation

* dashboard assets working, fix AssetsModal organism, wip: settings

* refactor using AssetsModal in ProfileSection

* make naming consistent

* use AssetModal in Project's public and profile sections, keep naming consistent

* infinite scroll in earth editor

* wip: update based on backend API changes

* Small cards only in modal, med cards only elsewhere

* update asset fetchMore w pagination object

* wip: move sort and search to backend

* wip: search, light refactor

* move sort functionality out of frontend

* check if more assets from hasNext/hasPrev

* move search func to query

* use assetHooks, clean up

* update assetsData type, update AssetModal storybook

* update assetmodal storybook

* update gql config, fix pagination reverse

* update naming and prop order in AssetModal

* make onClose a callback

* update based on review

* update asset settings

* update earth editor

* update closing modal

* getMoreAssets -> onGetMoreAssets

* update names, fix ci errors

* add toGQLEnum utility and use with assetSort

* fix initial asset fetch variables

* use toGQLEnum with Theme as well

* update assetmodal's story

* fix text getting clipped

* remove gqlenum convert from value

* create assetcontainer organism and use in modal

* wip: refactor

* wip: refactor - cache on unmount, fix modal toggle

* fix image deselection in dashboard proj creation

* set selectedAssets to initialAsset when available

* update settings project asset modal props

* Move asset hooks from modal to container

* reorder declarations

* move some asset modal molecule func into hooks

* update asset settings

* update earth editor right panel w AssetModal

* Remove unneeded story, update project settings

* selectAsseturl -> onAssetUrlSelect

* sortOptions -> useMemo

* move handleScroll outside hook

* Changes based on pr review

* fix ci check error

* make EditableItem generic

* fix errors on onSubmit

* fix type errors

Co-authored-by: rot1024 <aayhrot@gmail.com>
  • Loading branch information
KaWaite and rot1024 committed Mar 16, 2022
1 parent e19fab3 commit 11f2f27
Show file tree
Hide file tree
Showing 62 changed files with 1,122 additions and 949 deletions.
10 changes: 6 additions & 4 deletions src/components/atoms/Loading/index.tsx
Expand Up @@ -6,29 +6,31 @@ import { styled, useTheme } from "@reearth/theme";
import Portal from "../Portal";

export type Props = {
className?: string;
portal?: boolean;
fixed?: boolean;
relative?: boolean;
overlay?: boolean;
};

const Loading: React.FC<Props> = ({ portal, fixed, overlay }) => {
const Loading: React.FC<Props> = ({ className, portal, fixed, relative, overlay }) => {
const theme = useTheme();
const loading = (
<LoadingWrapper fixed={fixed} overlay={overlay}>
<LoadingWrapper className={className} fixed={fixed} overlay={overlay} relative={relative}>
<RingLoader size={33} color={theme.main.highlighted} />
</LoadingWrapper>
);
return portal ? <Portal>{loading}</Portal> : loading;
};

const LoadingWrapper = styled.div<{ fixed?: boolean; overlay?: boolean }>`
const LoadingWrapper = styled.div<{ fixed?: boolean; overlay?: boolean; relative?: boolean }>`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
position: ${({ fixed, relative }) => (fixed ? "fixed" : relative ? "relative" : "absolute")};
top: 0;
left: 0;
background: ${props => (props.overlay ? props.theme.main.deepBg : null)};
Expand Down
22 changes: 10 additions & 12 deletions src/components/atoms/TextBox/index.tsx
Expand Up @@ -5,10 +5,10 @@ import { styled, metrics } from "@reearth/theme";
import fonts from "@reearth/theme/fonts";
import { metricsSizes } from "@reearth/theme/metrics";

export type Props = {
export type Props<T extends string = string> = {
className?: string;
value?: string;
onChange?: (value: string) => void;
value?: T;
onChange?: (value: T | undefined) => void;
disabled?: boolean;
type?: "text" | "password";
multiline?: boolean;
Expand All @@ -25,7 +25,7 @@ export type Props = {
autofocus?: boolean;
};

const TextBox: React.FC<Props> = ({
export default function TextBox<T extends string = string>({
className,
value,
onChange,
Expand All @@ -43,15 +43,15 @@ const TextBox: React.FC<Props> = ({
floatedTextColor,
doesChangeEveryTime = false,
autofocus = false,
}) => {
}: Props<T>): JSX.Element | null {
const isDirty = useRef(false);
const [innerValue, setInnerValue] = useState(value);
const [rows, setRows] = useState(5);
const textAreaRef = useRef<HTMLTextAreaElement>(null);

const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = e.currentTarget.value;
const newValue = e.currentTarget.value as T;
isDirty.current = value !== newValue;
setInnerValue(newValue);
doesChangeEveryTime && onChange?.(newValue);
Expand All @@ -74,7 +74,7 @@ const TextBox: React.FC<Props> = ({
const handleKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (onChange && e.key === "Enter" && isDirty.current) {
onChange(e.currentTarget.value);
onChange(e.currentTarget.value as T);
}
},
[onChange],
Expand All @@ -83,7 +83,7 @@ const TextBox: React.FC<Props> = ({
const handleBlur = useCallback(
(e: React.SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (onChange && isDirty.current) {
onChange(e.currentTarget.value);
onChange(e.currentTarget.value as T);
}
},
[onChange],
Expand All @@ -97,7 +97,7 @@ const TextBox: React.FC<Props> = ({
useEffect(() => {
if (throttle && onChange && isDirty.current) {
const timeout = setTimeout(() => {
onChange(innerValue ?? "");
onChange(innerValue ?? undefined);
}, throttleTimeout);
return () => clearTimeout(timeout);
}
Expand Down Expand Up @@ -148,7 +148,7 @@ const TextBox: React.FC<Props> = ({
</FormWrapper>
</div>
);
};
}

type InputProps = Pick<Props, "color" | "backgroundColor" | "borderColor" | "floatedTextColor">;

Expand Down Expand Up @@ -199,5 +199,3 @@ const FloatedText = styled.span<InputProps>`
font-size: ${fonts.sizes.s}px;
user-select: none;
`;

export default TextBox;
24 changes: 13 additions & 11 deletions src/components/molecules/Common/AssetModal/AssetCard/index.tsx
Expand Up @@ -45,16 +45,18 @@ const AssetCard: React.FC<Props> = ({
<Icon icon={icon} size={iconSize} />
)}
</ImgWrapper>
<FileName size={cardSize === "large" ? "m" : "xs"} cardSize={cardSize} customColor>
{name}
</FileName>
{checked && (
<StyledIcon
icon="checkCircle"
alt="checked"
size={cardSize === "small" ? "18px" : "24px"}
/>
)}
<Flex>
<FileName size={cardSize === "large" ? "m" : "2xs"} cardSize={cardSize} customColor>
{name}
</FileName>
{checked && (
<StyledIcon
icon="checkCircle"
alt="checked"
size={cardSize === "small" ? "18px" : "24px"}
/>
)}
</Flex>
</Wrapper>
);
};
Expand Down Expand Up @@ -102,7 +104,7 @@ const FileName = styled(Text)<{ cardSize?: CardSize }>`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: ${({ cardSize }) => (cardSize === "large" ? "16px" : "12px")};
margin-top: ${({ cardSize }) => (cardSize === "large" ? "12px" : "8px")};
color: inherit;
`;

Expand Down
145 changes: 59 additions & 86 deletions src/components/molecules/Common/AssetModal/AssetContainer/hooks.ts
@@ -1,7 +1,8 @@
import { useState, useCallback, useEffect } from "react";
import { useState, useCallback, useMemo } from "react";
import { useIntl } from "react-intl";
import useFileInput from "use-file-input";

export type FilterTypes = "time" | "size" | "name";
export type SortType = "date" | "name" | "size";

export type LayoutTypes = "medium" | "small" | "list";

Expand All @@ -14,94 +15,65 @@ export type Asset = {
contentType: string;
};

function handleScroll(
{ currentTarget }: React.UIEvent<HTMLDivElement, UIEvent>,
onLoadMore?: () => void,
) {
if (currentTarget.scrollTop + currentTarget.clientHeight >= currentTarget.scrollHeight) {
onLoadMore?.();
}
}

export default ({
assets,
isMultipleSelectable,
accept,
onCreateAsset,
initialAsset,
selectAsset,
selectedAssets,
sort,
smallCardOnly,
onCreateAssets,
onAssetUrlSelect,
onRemove,
onSortChange,
onSearch,
}: {
assets?: Asset[];
isMultipleSelectable?: boolean;
accept?: string;
onCreateAsset?: (files: FileList) => void;
initialAsset?: Asset;
selectAsset?: (assets: Asset[]) => void;
selectedAssets?: Asset[];
sort?: { type?: SortType | null; reverse?: boolean };
smallCardOnly?: boolean;
onCreateAssets?: (files: FileList) => void;
onAssetUrlSelect?: (asset?: string) => void;
onRemove?: (assetIds: string[]) => void;
onSortChange?: (type?: string, reverse?: boolean) => void;
onSearch?: (term?: string | undefined) => void;
}) => {
const [layoutType, setLayoutType] = useState<LayoutTypes>("medium");
const [currentSaved, setCurrentSaved] = useState(initialAsset);
const [reverse, setReverse] = useState(false);

const [searchResults, setSearchResults] = useState<Asset[]>();
const [filterSelected, selectFilter] = useState<FilterTypes>("time");

const [filteredAssets, setAssets] = useState(assets);

const intl = useIntl();
const [layoutType, setLayoutType] = useState<LayoutTypes>(smallCardOnly ? "small" : "medium");
const [deleteModalVisible, setDeleteModalVisible] = useState(false);

const handleRemove = useCallback(() => {
if (selectedAssets?.length) {
onRemove?.(selectedAssets.map(a => a.id));
selectAsset?.([]);
setDeleteModalVisible(false);
}
}, [onRemove, selectAsset, selectedAssets]);
const sortOptions: { key: SortType; label: string }[] = useMemo(
() => [
{ key: "date", label: intl.formatMessage({ defaultMessage: "Date" }) },
{ key: "size", label: intl.formatMessage({ defaultMessage: "File size" }) },
{ key: "name", label: intl.formatMessage({ defaultMessage: "Alphabetical" }) },
],
[intl],
);

const iconChoice =
filterSelected === "name"
? reverse
sort?.type === "name"
? sort?.reverse
? "filterNameReverse"
: "filterName"
: filterSelected === "size"
? reverse
: sort?.type === "size"
? sort?.reverse
? "filterSizeReverse"
: "filterSize"
: reverse
: sort?.reverse
? "filterTimeReverse"
: "filterTime";

const handleFilterChange = useCallback(
(f: FilterTypes) => {
selectFilter(f);
setReverse(false);
setCurrentSaved(initialAsset);
if (!assets) return;
const newArray =
f === "time"
? [...assets]
: [...assets].sort((a: Asset, a2: Asset) => {
return f === "name"
? a.name.localeCompare(a2.name)
: a[f] < a2[f]
? -1
: a[f] > a2[f]
? 1
: 0;
});
setAssets(newArray);
},
[assets, initialAsset],
);

useEffect(() => {
if (!assets) return;
handleFilterChange(filterSelected);
}, [handleFilterChange, filterSelected, assets]);

const handleAssetsSelect = (asset: Asset) => {
selectedAssets?.includes(asset)
? selectAsset?.(selectedAssets?.filter(a => a !== asset))
: selectAsset?.(
isMultipleSelectable && selectedAssets ? [...selectedAssets, asset] : [asset],
);
};

const handleFileSelect = useFileInput(files => onCreateAsset?.(files), {
const handleFileSelect = useFileInput(files => onCreateAssets?.(files), {
accept,
multiple: isMultipleSelectable,
});
Expand All @@ -110,38 +82,39 @@ export default ({
handleFileSelect();
}, [handleFileSelect]);

const handleRemove = useCallback(() => {
if (selectedAssets?.length) {
onRemove?.(selectedAssets.map(a => a.id));
onAssetUrlSelect?.();
setDeleteModalVisible(false);
}
}, [onRemove, onAssetUrlSelect, selectedAssets]);

const handleReverse = useCallback(() => {
setReverse(!reverse);
if (!filteredAssets) return;
setAssets(filteredAssets.reverse());
}, [filteredAssets, reverse]);
onSortChange?.(undefined, !sort?.reverse);
}, [onSortChange, sort?.reverse]);

const handleSearch = useCallback(
(value: string) => {
if (!value) {
setSearchResults(undefined);
(term?: string) => {
if (!term || term.length < 1) {
onSearch?.(undefined);
} else {
if (!filteredAssets) return;
setSearchResults(filteredAssets.filter(a => a.name.toLowerCase().includes(value)));
onSearch?.(term);
}
},
[filteredAssets],
[onSearch],
);

return {
layoutType,
setLayoutType,
filteredAssets,
handleFilterChange,
filterSelected,
currentSaved,
searchResults,
iconChoice,
handleAssetsSelect,
deleteModalVisible,
sortOptions,
handleScroll,
setLayoutType,
handleUploadToAsset,
handleReverse,
handleSearch,
deleteModalVisible,
setDeleteModalVisible,
handleRemove,
};
Expand Down

0 comments on commit 11f2f27

Please sign in to comment.