feat: update layout
This commit is contained in:
parent
550df8fb36
commit
032285e2e7
@ -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: () => (
|
||||||
|
@ -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
|
||||||
)}
|
)}
|
||||||
|
83
src/pages/artworks/ArtworkListing.tsx
Normal file
83
src/pages/artworks/ArtworkListing.tsx
Normal 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);
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user