Skip to content

Commit

Permalink
Created masonry layout v2
Browse files Browse the repository at this point in the history
  • Loading branch information
olehkhalin committed Jul 21, 2022
1 parent 3df112e commit 801e340
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 21 deletions.
42 changes: 23 additions & 19 deletions src/app/components/blocks/OverviewContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useForceUpdate from "use-force-update";
import { useAtom, useAtomValue } from "jotai";
import { RESET } from "jotai/utils";
import { ethers } from "ethers";
import { Masonry } from "masonic";
import Masonry from "lib/masonry-layout/Masonry";

import {
AccountAsset,
Expand Down Expand Up @@ -353,31 +353,35 @@ const AssetsList: FC = () => {
/>
))
) : (
<MasonryContainer />
<Masonry gap="0.25rem">
{NFTS_LIST.map((nft) => (
<NftCard key={nft.name ?? nft.id} nft={nft} />
))}
</Masonry>
)}
</ScrollAreaContainer>
)}
</div>
);
};

const MasonryContainer: FC = () => {
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
const t = setTimeout(() => setIsMounted(true), 150);
return () => clearTimeout(t);
}, []);

return isMounted ? (
<Masonry
columnCount={3}
columnGutter={4}
items={NFTS_LIST}
render={NftCard}
/>
) : null;
};
// const MasonryContainer: FC = () => {
// const [isMounted, setIsMounted] = useState(false);
//
// useEffect(() => {
// const t = setTimeout(() => setIsMounted(true), 150);
// return () => clearTimeout(t);
// }, []);
//
// return isMounted ? (
// <Masonry
// columnCount={3}
// columnGutter={4}
// items={NFTS_LIST}
// render={NftCard}
// />
// ) : null;
// };

const NFTS_LIST = [
{
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/blocks/overview/NftCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { IS_FIREFOX } from "app/defaults";
import Avatar from "app/components/elements/Avatar";

type NftCardProps = {
data: {
nft: {
img: string;
name?: string;
id?: string;
amount: number;
};
};

const NftCard: FC<NftCardProps> = ({ data: { img, name, id, amount } }) => {
const NftCard: FC<NftCardProps> = ({ nft: { img, name, id, amount } }) => {
const [loaded, setLoaded] = useState(false);

const title = getNFTName("", name, id);
Expand Down
145 changes: 145 additions & 0 deletions src/lib/masonry-layout/Masonry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useEffect } from "react";

import { useWindowWidth } from "../react-hooks/useWindowWidth";

export interface PlockProps extends React.ComponentPropsWithoutRef<"div"> {
gap?: string;
debounce?: number;
breakpoints?: Breakpoint[];
}

export interface Breakpoint {
size: number;
columns: number;
}

const first = (breakpoints: Breakpoint[]): Breakpoint => {
return breakpoints[0];
};

const last = (breakpoints: Breakpoint[]): Breakpoint => {
return breakpoints[breakpoints.length - 1];
};

const sorted = (breakpoints: Breakpoint[]): Breakpoint[] => {
return breakpoints.sort((a, b) => a.size - b.size);
};

const contained = (breakpoints: Breakpoint[], width: number): Breakpoint[] => {
return breakpoints.filter((el) => el.size <= width);
};

const calculateColumns = (breakpoints: Breakpoint[], width: number) => {
const sortedBp = sorted(breakpoints);
const containedBp = contained(sortedBp, width);
const { columns } =
containedBp.length < 1 ? first(sortedBp) : last(containedBp);

// ??? OMG THIS IS SO UGLY!
return Array.from({ length: columns }, (e) => []) as unknown as [
React.ReactElement[]
];
};

const DEFAULT_BREAKPOINTS = [{ size: 1280, columns: 3 }];

const Masonry = ({
children,
gap = "10px",
debounce = 200,
breakpoints = DEFAULT_BREAKPOINTS,
...props
}: PlockProps) => {
const width = useWindowWidth({ debounceMs: debounce });
const [columns, setColumns] = React.useState<[React.ReactElement[]?]>([]);

useEffect(() => {
const calculated = calculateColumns(breakpoints, width || 0);

React.Children.forEach(children, (child, index) => {
const key = uniqueId("plock-item-");

if (React.isValidElement(child)) {
const cloned = React.cloneElement(child, {
...child.props,
key: key,
});

calculated[index % calculated.length].push(cloned);
}
});

setColumns(calculated);
}, [children, breakpoints, width]);

return (
<MasonryWrapper columns={columns.length} gap={gap} {...props}>
{columns.map((column, index) => {
return (
<MasonryColumn gap={gap} key={index} data-testid="masonry-column">
{column}
</MasonryColumn>
);
})}
</MasonryWrapper>
);
};

export default Masonry;

const idCounter: { [key: string]: number } = {};

function uniqueId(prefix = "$lodash$") {
if (!idCounter[prefix]) {
idCounter[prefix] = 0;
}

const id = ++idCounter[prefix];
if (prefix === "$lodash$") {
return `${id}`;
}

return `${prefix}${id}`;
}

interface MasonryProps extends React.ComponentPropsWithoutRef<"div"> {
columns: number;
gap: string;
children: React.ReactNode;
}

const MasonryWrapper = ({ children, columns, gap, ...props }: MasonryProps) => {
return (
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
columnGap: gap,
alignItems: "start",
}}
{...props}
>
{children}
</div>
);
};

interface MasonryColumnProps extends React.ComponentPropsWithoutRef<"div"> {
gap: string;
children: React.ReactNode;
}

const MasonryColumn = ({ children, gap, ...props }: MasonryColumnProps) => {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "100%",
rowGap: gap,
}}
{...props}
>
{children}
</div>
);
};
39 changes: 39 additions & 0 deletions src/lib/react-hooks/useWindowWidth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useState } from "react";

interface Props {
debounceMs: number;
}

export function useWindowWidth({ debounceMs }: Props): number | undefined {
const [width, setWidth] = useState<number | undefined>(undefined);

useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};

const debouncedHandleResize = debounce(handleResize, debounceMs);

window.addEventListener("resize", debouncedHandleResize);

handleResize();

return () => window.removeEventListener("resize", debouncedHandleResize);
}, [debounceMs]);

return width;
}

function debounce<T extends unknown[], U>(
callback: (...args: T) => PromiseLike<U> | U,
wait: number
) {
let timer: ReturnType<typeof setTimeout>;

return (...args: T): Promise<U> => {
clearTimeout(timer);
return new Promise((resolve) => {
timer = setTimeout(() => resolve(callback(...args)), wait);
});
};
}

0 comments on commit 801e340

Please sign in to comment.