feat: update layout

This commit is contained in:
Khairul Hidayat 2024-01-13 23:06:04 +00:00
parent 550df8fb36
commit 032285e2e7
5 changed files with 140 additions and 91 deletions

View File

@ -16,7 +16,10 @@ const router = createBrowserRouter([
{ {
path: "/treasures", path: "/treasures",
Component: ArtworksPage, Component: ArtworksPage,
children: [{ index: true }, { path: ":id" }], },
{
path: "/treasures/:id",
Component: ArtworksPage,
}, },
], ],
ErrorBoundary: () => ( ErrorBoundary: () => (

View File

@ -4,32 +4,40 @@ import React, { useState } from "react";
type Props = React.ComponentProps<"img"> & { type Props = React.ComponentProps<"img"> & {
lazySrc: string; lazySrc: string;
containerClassName?: string; containerClassName?: string;
placeholderClassName?: string;
placeholder?: React.ReactNode;
}; };
const LazyImage = ({ const LazyImage = ({
lazySrc, lazySrc,
src, src,
containerClassName, containerClassName,
placeholderClassName,
className, className,
placeholder,
...props ...props
}: Props) => { }: Props) => {
const [isLoaded, setLoaded] = useState(false); const [isLoaded, setLoaded] = useState(false);
return ( return (
<div <div className={cn("relative", containerClassName)}>
className={cn( <div
"bg-no-repeat bg-cover", className={cn(
!isLoaded ? "blur-md" : "", "absolute inset-0 bg-no-repeat bg-cover blur-md z-0 transition-all duration-500",
containerClassName placeholderClassName,
)} isLoaded ? "brightness-75" : ""
style={{ backgroundImage: `url('${lazySrc}')` }} )}
> style={{ backgroundImage: `url('${lazySrc}')` }}
></div>
{!isLoaded && placeholder ? placeholder : null}
<img <img
src={src} src={src}
loading="lazy" loading="lazy"
onLoad={() => setLoaded(true)} onLoad={() => setTimeout(() => setLoaded(true), 50)}
className={cn( className={cn(
"transition-all", "transition-all duration-500 relative z-[1]",
isLoaded ? "opacity-100" : "opacity-0", isLoaded ? "opacity-100" : "opacity-0",
className className
)} )}

View File

@ -0,0 +1,83 @@
import pb from "@/utility/api";
import { useInfiniteQuery } from "react-query";
import { Link } from "react-router-dom";
import LazyImage from "@/components/ui/LazyImage";
import React, { useMemo } from "react";
import Button from "@/components/ui/Button";
import { useBottomScrollListener } from "react-bottom-scroll-listener";
import { Skeleton } from "@/components/ui/Skeleton";
const ArtworkListing = () => {
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["artworks"],
queryFn: ({ pageParam = 1 }) => {
return pb
.collection("artworks")
.getList(pageParam, 12, { sort: "-created" });
},
getNextPageParam: (lastPage) =>
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
});
useBottomScrollListener<HTMLDivElement>(
() => {
if (!isFetchingNextPage) {
fetchNextPage();
}
},
{ offset: 100 }
);
const items = useMemo(
() => data?.pages.flatMap((i) => i.items) || [],
[data]
);
return (
<div>
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mt-8">
{items.map((item) => (
<Link
key={item.id}
to={`/treasures/${item.id}`}
className="bg-white rounded-lg shadow border border-gray-300 overflow-hidden hover:scale-105 transition-all relative"
>
<LazyImage
lazySrc={pb.files.getUrl(item, item.image, { thumb: "32x48" })}
src={pb.files.getUrl(item, item.image, { thumb: "256x384" })}
className="w-full aspect-[0.8] object-cover"
/>
<div className="absolute bottom-2 left-2 px-3 py-1 rounded-md bg-black/20 backdrop-blur-sm z-[2]">
<p className="text-white">{item.artistName}</p>
</div>
</Link>
))}
{isLoading || isFetchingNextPage
? [...Array(12)].map((_, idx) => (
<Skeleton key={idx} className="aspect-[0.8]" />
))
: null}
</div>
{hasNextPage && !isFetchingNextPage ? (
<div className="flex justify-center mt-8">
<Button
onClick={() => {
if (!isFetchingNextPage) {
fetchNextPage();
}
}}
variant="solid"
className="min-w-[200px]"
>
Load More
</Button>
</div>
) : null}
</div>
);
};
export default React.memo(ArtworkListing);

View File

@ -1,16 +1,11 @@
import pb from "@/utility/api";
import { Howl } from "howler"; import { Howl } from "howler";
import { useInfiniteQuery } from "react-query"; import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Link, useNavigate, useParams } from "react-router-dom";
import playIcon from "@/assets/icons/play-outline.svg"; import playIcon from "@/assets/icons/play-outline.svg";
import openingSfx from "@/assets/audio/VO_JA_Furina_Opening_Treasure_Chest_02.ogg"; import openingSfx from "@/assets/audio/VO_JA_Furina_Opening_Treasure_Chest_02.ogg";
import ViewSheet from "./viewSheet"; import ViewSheet from "./viewSheet";
import LazyImage from "@/components/ui/LazyImage";
import PageMetadata from "@/components/containers/PageMetadata"; import PageMetadata from "@/components/containers/PageMetadata";
import { useMemo } from "react"; import ArtworkListing from "./ArtworkListing";
import Button from "@/components/ui/Button"; import { useCallback } from "react";
import { useBottomScrollListener } from "react-bottom-scroll-listener";
import { Skeleton } from "@/components/ui/Skeleton";
const openingChestSfx = new Howl({ const openingChestSfx = new Howl({
src: openingSfx, src: openingSfx,
@ -20,31 +15,15 @@ const openingChestSfx = new Howl({
const ArtworksPage = () => { const ArtworksPage = () => {
const { id: viewArtId } = useParams(); const { id: viewArtId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = const goBack = useCallback(() => {
useInfiniteQuery({ if (location.key !== "default") {
queryKey: ["artworks"], navigate(-1);
queryFn: ({ pageParam = 1 }) => { } else {
return pb navigate("/treasures");
.collection("artworks") }
.getList(pageParam, 12, { sort: "-created" }); }, [location, navigate]);
},
getNextPageParam: (lastPage) =>
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
});
useBottomScrollListener<HTMLDivElement>(
() => {
if (!isFetchingNextPage) {
fetchNextPage();
}
},
{ offset: 100 }
);
const items = useMemo(
() => data?.pages.flatMap((i) => i.items) || [],
[data]
);
return ( return (
<div className="container py-16"> <div className="container py-16">
@ -66,52 +45,9 @@ const ArtworksPage = () => {
</button> </button>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mt-8"> <ArtworkListing />
{items.map((item) => (
<Link
key={item.id}
to={`/treasures/${item.id}`}
className="bg-white rounded-lg shadow border border-gray-300 overflow-hidden hover:scale-105 transition-all relative"
>
<LazyImage
lazySrc={pb.files.getUrl(item, item.image, { thumb: "32x48" })}
src={pb.files.getUrl(item, item.image, { thumb: "256x384" })}
className="w-full aspect-[0.8] object-cover"
/>
<div className="absolute bottom-2 left-2 px-3 py-1 rounded-md bg-black/20 backdrop-blur-sm">
<p className="text-white">{item.artistName}</p>
</div>
</Link>
))}
{isLoading || isFetchingNextPage <ViewSheet isOpen={viewArtId != null} id={viewArtId} onClose={goBack} />
? [...Array(12)].map((_, idx) => (
<Skeleton key={idx} className="aspect-[0.8]" />
))
: null}
</div>
{hasNextPage && !isFetchingNextPage ? (
<div className="flex justify-center mt-8">
<Button
onClick={() => {
if (!isFetchingNextPage) {
fetchNextPage();
}
}}
variant="solid"
className="min-w-[200px]"
>
Load More
</Button>
</div>
) : null}
<ViewSheet
isOpen={viewArtId != null}
id={viewArtId}
onClose={() => navigate("/treasures")}
/>
</div> </div>
); );
}; };

View File

@ -5,6 +5,8 @@ import loadingIllust from "@/assets/images/l9fsdoa2j7vb1.gif";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import { ChevronLeft } from "lucide-react"; import { ChevronLeft } from "lucide-react";
import LazyImage from "@/components/ui/LazyImage";
import { useEffect, useState } from "react";
type Props = { type Props = {
id?: string | null; id?: string | null;
@ -12,13 +14,20 @@ type Props = {
onClose: () => void; onClose: () => void;
}; };
const ViewSheet = ({ id, isOpen, onClose }: Props) => { const ViewSheet = ({ id: viewId, isOpen, onClose }: Props) => {
const [id, setId] = useState(viewId);
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ["artwork", id], queryKey: ["artwork", id],
queryFn: () => pb.collection("artworks").getOne(id || ""), queryFn: () => pb.collection("artworks").getOne(id || ""),
enabled: !!id, enabled: !!id,
}); });
useEffect(() => {
if (viewId) {
setId(viewId);
}
}, [viewId, setId]);
return ( return (
<Sheet <Sheet
isOpen={isOpen} isOpen={isOpen}
@ -45,14 +54,24 @@ const ViewSheet = ({ id, isOpen, onClose }: Props) => {
<div className="flex flex-col md:flex-row md:h-full"> <div className="flex flex-col md:flex-row md:h-full">
<div className="flex-1 bg-gray-50 flex items-center justify-center"> <div className="flex-1 bg-gray-50 flex items-center justify-center">
<a href={data.srcUrl} target="_blank" className="w-full h-full"> <a href={data.srcUrl} target="_blank" className="w-full h-full">
<img <LazyImage
lazySrc={pb.files.getUrl(data, data.image, { thumb: "32x48" })}
src={pb.files.getUrl(data, data.image)} src={pb.files.getUrl(data, data.image)}
className="w-full h-full object-contain" className="w-full h-full object-contain"
containerClassName="w-full h-full"
placeholderClassName="scale-x-110 -translate-x-10"
placeholder={
<div className="absolute z-10 inset-0 flex items-center justify-center">
<p className="text-center bg-white py-1 px-2 rounded animate-bounce">
Loading...
</p>
</div>
}
/> />
</a> </a>
</div> </div>
<div className="md:w-1/3 border-t md:border-l md:border-t-0 py-4 md:pt-0 px-4 lg:px-8 overflow-y-auto"> <div className="md:w-1/3 border-t md:border-t-0 py-4 md:pt-0 px-4 lg:px-8 overflow-y-auto">
<Button className="flex pl-2 mb-6" onClick={onClose}> <Button className="flex pl-2 mb-6" onClick={onClose}>
<ChevronLeft /> Back <ChevronLeft /> Back
</Button> </Button>