Spaces:
Running
Running
update: add trending category + pagination to load fast the page
Browse files- components/pagination.tsx +40 -0
- components/sort.tsx +15 -2
- components/spaces/index.tsx +42 -16
- utils/index.ts +1 -1
components/pagination.tsx
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { SpaceProps } from "@/utils/type";
|
2 |
+
import { MAX_ITEMS_PER_PAGE } from "./spaces";
|
3 |
+
|
4 |
+
export const Pagination = ({
|
5 |
+
spaces,
|
6 |
+
page,
|
7 |
+
handleNextPage,
|
8 |
+
handlePrevPage,
|
9 |
+
}: {
|
10 |
+
spaces: SpaceProps[];
|
11 |
+
page: number;
|
12 |
+
handleNextPage: () => void;
|
13 |
+
handlePrevPage: () => void;
|
14 |
+
}) => {
|
15 |
+
const max_pages = Math.ceil(spaces.length / MAX_ITEMS_PER_PAGE);
|
16 |
+
|
17 |
+
return (
|
18 |
+
<footer className="flex justify-between max-w-2xl mx-auto my-auto w-full p-3 sticky items-center bottom-3 bg-white dark:bg-slate-950 dark:border-slate-800 shadow-lg shadow-black/5 border border-zinc-200 rounded-full z-[9999]">
|
19 |
+
<button
|
20 |
+
disabled={page === 0}
|
21 |
+
className="flex items-center gap-2.5 text-gray-500 dark:text-gray-400 hover:bg-gray-100 rounded-full px-3 py-1.5 dark:hover:bg-slate-900 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:!bg-transparent"
|
22 |
+
onClick={handlePrevPage}
|
23 |
+
>
|
24 |
+
<span className="max-lg:hidden">←</span>
|
25 |
+
<span>Previous</span>
|
26 |
+
</button>
|
27 |
+
<p className="text-gray-500/70">
|
28 |
+
Page {page + 1} of {max_pages}
|
29 |
+
</p>
|
30 |
+
<button
|
31 |
+
disabled={page === max_pages - 1}
|
32 |
+
className="flex items-center gap-2.5 text-gray-500 dark:text-gray-400 hover:bg-gray-100 rounded-full px-3 py-1.5 dark:hover:bg-slate-900 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:!bg-transparent"
|
33 |
+
onClick={handleNextPage}
|
34 |
+
>
|
35 |
+
<span>Next</span>
|
36 |
+
<span className="max-lg:hidden">→</span>
|
37 |
+
</button>
|
38 |
+
</footer>
|
39 |
+
);
|
40 |
+
};
|
components/sort.tsx
CHANGED
@@ -3,12 +3,25 @@ import { TiHeartFullOutline } from "react-icons/ti";
|
|
3 |
import { MdCalendarToday } from "react-icons/md";
|
4 |
|
5 |
interface Props {
|
6 |
-
value: "likes" | "createdAt";
|
7 |
-
onChange: (s: "createdAt" | "likes") => void;
|
8 |
}
|
9 |
export const Sort = ({ value, onChange }: Props) => {
|
10 |
return (
|
11 |
<div className="flex items-center justify-center border-[3px] rounded-full border-gray-50 drop-shadow-sm bg-gray-100 dark:bg-slate-900 dark:border-slate-800 ring-[1px] ring-gray-200 dark:ring-slate-700 gap-1">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
<button
|
13 |
className={classNames(
|
14 |
"rounded-full pl-3 pr-4 py-2.5 transition-all duration-200 font-semibold text-xs hover:bg-gray-200/70 dark:hover:bg-gray-700/70 flex items-center justify-center gap-2",
|
|
|
3 |
import { MdCalendarToday } from "react-icons/md";
|
4 |
|
5 |
interface Props {
|
6 |
+
value: "likes" | "createdAt" | "trending";
|
7 |
+
onChange: (s: "createdAt" | "likes" | "trending") => void;
|
8 |
}
|
9 |
export const Sort = ({ value, onChange }: Props) => {
|
10 |
return (
|
11 |
<div className="flex items-center justify-center border-[3px] rounded-full border-gray-50 drop-shadow-sm bg-gray-100 dark:bg-slate-900 dark:border-slate-800 ring-[1px] ring-gray-200 dark:ring-slate-700 gap-1">
|
12 |
+
<button
|
13 |
+
className={classNames(
|
14 |
+
"rounded-full pl-3 pr-4 py-2.5 transition-all duration-200 font-semibold text-xs hover:bg-gray-200/70 dark:hover:bg-gray-700/70 flex items-center justify-center gap-2",
|
15 |
+
{
|
16 |
+
"bg-black hover:!bg-black dark:bg-slate-950 dark:hover:!bg-slate-950 text-white":
|
17 |
+
value === "trending",
|
18 |
+
}
|
19 |
+
)}
|
20 |
+
onClick={() => onChange("trending")}
|
21 |
+
>
|
22 |
+
<TiHeartFullOutline className="w-3.5 h-3.5" />
|
23 |
+
Trending
|
24 |
+
</button>
|
25 |
<button
|
26 |
className={classNames(
|
27 |
"rounded-full pl-3 pr-4 py-2.5 transition-all duration-200 font-semibold text-xs hover:bg-gray-200/70 dark:hover:bg-gray-700/70 flex items-center justify-center gap-2",
|
components/spaces/index.tsx
CHANGED
@@ -8,15 +8,22 @@ import { SpaceProps } from "@/utils/type";
|
|
8 |
import { SpaceIcon } from "@/components/space_icon";
|
9 |
import { Space } from "@/components/spaces/space";
|
10 |
import { Sort } from "@/components/sort";
|
|
|
|
|
|
|
|
|
11 |
|
12 |
export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
13 |
-
const {
|
14 |
-
const currentTheme = theme === "system" ? systemTheme : theme;
|
15 |
|
16 |
const [selectedGpu, setSelectedGpu] = useState<string | undefined>(undefined);
|
17 |
const router = useRouter();
|
18 |
-
const [sort, setSort] = useState<"likes" | "createdAt">(
|
|
|
|
|
19 |
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
|
|
|
20 |
|
21 |
useEffect(() => {
|
22 |
router.push("/?sort=" + sort);
|
@@ -30,21 +37,33 @@ export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
|
30 |
setSelectedGpu(gpu);
|
31 |
};
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
.
|
|
|
|
|
|
|
41 |
);
|
42 |
}, [spaces, searchQuery]);
|
43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
return (
|
45 |
-
<section className="flex h-screen overflow-auto">
|
46 |
<div className="w-full container px-6 py-10 lg:py-20 mx-auto space-y-10 lg:space-y-14">
|
47 |
-
<header className="max-w-5xl mx-auto w-full
|
48 |
<div>
|
49 |
<div className="mb-6 flex items-center justify-start gap-2.5">
|
50 |
<button
|
@@ -61,7 +80,7 @@ export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
|
61 |
</button>
|
62 |
<div className="font-regular text-xs text-center max-w-max rounded-full border-gray-200 bg-gray-50 border text-gray-700 dark:text-slate-400 dark:bg-slate-900 dark:border-slate-800 px-3 py-2 transition-all duration-300">
|
63 |
<SpaceIcon className="inline-block w-4 h-4 mr-2 drop-shadow-lg" />
|
64 |
-
Browse {
|
65 |
</div>
|
66 |
</div>
|
67 |
<h1 className="font-extrabold text-3xl text-black dark:text-slate-100">
|
@@ -71,7 +90,7 @@ export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
|
71 |
Discover spaces with zero GPU usage on 🤗 Hugging Face Spaces.
|
72 |
</p>
|
73 |
</div>
|
74 |
-
<div className="flex items-center justify-
|
75 |
<div className="relative">
|
76 |
<MdSearch className="absolute left-2.5 top-2.5 w-5 h-5 text-gray-600 dark:text-slate-400 z-[1]" />
|
77 |
<input
|
@@ -85,7 +104,7 @@ export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
|
85 |
<Sort value={sort} onChange={setSort} />
|
86 |
</div>
|
87 |
</header>
|
88 |
-
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4
|
89 |
{filteredSpaces.length === 0 && (
|
90 |
<div className="col-span-full flex items-center flex-col justify-center gap-2">
|
91 |
<p className="text-neutral-500 font-regular text-base dark:text-slate-300">
|
@@ -96,6 +115,7 @@ export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
|
96 |
onClick={() => {
|
97 |
setSearchQuery("");
|
98 |
setSelectedGpu(undefined);
|
|
|
99 |
}}
|
100 |
>
|
101 |
Reset filters
|
@@ -111,6 +131,12 @@ export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
|
111 |
/>
|
112 |
))}
|
113 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
</div>
|
115 |
</section>
|
116 |
);
|
|
|
8 |
import { SpaceIcon } from "@/components/space_icon";
|
9 |
import { Space } from "@/components/spaces/space";
|
10 |
import { Sort } from "@/components/sort";
|
11 |
+
import { Pagination } from "@/components/pagination";
|
12 |
+
import { useUpdateEffect } from "react-use";
|
13 |
+
|
14 |
+
export const MAX_ITEMS_PER_PAGE = 60;
|
15 |
|
16 |
export const Spaces: React.FC<{ spaces: SpaceProps[] }> = ({ spaces }) => {
|
17 |
+
const { theme, setTheme } = useTheme();
|
|
|
18 |
|
19 |
const [selectedGpu, setSelectedGpu] = useState<string | undefined>(undefined);
|
20 |
const router = useRouter();
|
21 |
+
const [sort, setSort] = useState<"likes" | "createdAt" | "trending">(
|
22 |
+
"trending"
|
23 |
+
);
|
24 |
const [searchQuery, setSearchQuery] = useState<string>("");
|
25 |
+
const [page, setPage] = useState<number>(0);
|
26 |
+
const [filteredSpaces, setFilteredSpaces] = useState<SpaceProps[]>([]);
|
27 |
|
28 |
useEffect(() => {
|
29 |
router.push("/?sort=" + sort);
|
|
|
37 |
setSelectedGpu(gpu);
|
38 |
};
|
39 |
|
40 |
+
useUpdateEffect(() => {
|
41 |
+
return setFilteredSpaces(
|
42 |
+
spaces.filter(
|
43 |
+
(space) =>
|
44 |
+
space.title
|
45 |
+
?.toLowerCase()
|
46 |
+
.includes(searchQuery.trim().toLowerCase()) ||
|
47 |
+
space.authorData?.name
|
48 |
+
?.toLowerCase()
|
49 |
+
.includes(searchQuery.trim().toLowerCase())
|
50 |
+
)
|
51 |
);
|
52 |
}, [spaces, searchQuery]);
|
53 |
|
54 |
+
useUpdateEffect(() => {
|
55 |
+
const app = document.getElementById("app");
|
56 |
+
app?.scrollTo({ top: 0, behavior: "smooth" });
|
57 |
+
|
58 |
+
const start = page * MAX_ITEMS_PER_PAGE;
|
59 |
+
const end = start + MAX_ITEMS_PER_PAGE;
|
60 |
+
setFilteredSpaces(spaces.slice(start, end));
|
61 |
+
}, [page, spaces]);
|
62 |
+
|
63 |
return (
|
64 |
+
<section id="app" className="flex h-screen overflow-auto">
|
65 |
<div className="w-full container px-6 py-10 lg:py-20 mx-auto space-y-10 lg:space-y-14">
|
66 |
+
<header className="max-w-5xl mx-auto w-full flex-col items-start flex gap-5 lg:gap-10">
|
67 |
<div>
|
68 |
<div className="mb-6 flex items-center justify-start gap-2.5">
|
69 |
<button
|
|
|
80 |
</button>
|
81 |
<div className="font-regular text-xs text-center max-w-max rounded-full border-gray-200 bg-gray-50 border text-gray-700 dark:text-slate-400 dark:bg-slate-900 dark:border-slate-800 px-3 py-2 transition-all duration-300">
|
82 |
<SpaceIcon className="inline-block w-4 h-4 mr-2 drop-shadow-lg" />
|
83 |
+
Browse {spaces.length} spaces
|
84 |
</div>
|
85 |
</div>
|
86 |
<h1 className="font-extrabold text-3xl text-black dark:text-slate-100">
|
|
|
90 |
Discover spaces with zero GPU usage on 🤗 Hugging Face Spaces.
|
91 |
</p>
|
92 |
</div>
|
93 |
+
<div className="flex items-center justify-between gap-2 max-lg:flex-col max-lg:items-start max-lg:gap-3 w-full">
|
94 |
<div className="relative">
|
95 |
<MdSearch className="absolute left-2.5 top-2.5 w-5 h-5 text-gray-600 dark:text-slate-400 z-[1]" />
|
96 |
<input
|
|
|
104 |
<Sort value={sort} onChange={setSort} />
|
105 |
</div>
|
106 |
</header>
|
107 |
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
108 |
{filteredSpaces.length === 0 && (
|
109 |
<div className="col-span-full flex items-center flex-col justify-center gap-2">
|
110 |
<p className="text-neutral-500 font-regular text-base dark:text-slate-300">
|
|
|
115 |
onClick={() => {
|
116 |
setSearchQuery("");
|
117 |
setSelectedGpu(undefined);
|
118 |
+
setPage(0);
|
119 |
}}
|
120 |
>
|
121 |
Reset filters
|
|
|
131 |
/>
|
132 |
))}
|
133 |
</div>
|
134 |
+
<Pagination
|
135 |
+
spaces={spaces}
|
136 |
+
page={page}
|
137 |
+
handleNextPage={() => setPage(page + 1)}
|
138 |
+
handlePrevPage={() => setPage(page - 1)}
|
139 |
+
/>
|
140 |
</div>
|
141 |
</section>
|
142 |
);
|
utils/index.ts
CHANGED
@@ -13,7 +13,7 @@ export const fetchAllPages = async (sort = "trending") => {
|
|
13 |
|
14 |
const jsonResponses = await Promise.all(urls);
|
15 |
const spaces = jsonResponses.flat();
|
16 |
-
return spaces
|
17 |
};
|
18 |
|
19 |
export const sortByCreatedAt = (spaces: SpaceProps[]) => {
|
|
|
13 |
|
14 |
const jsonResponses = await Promise.all(urls);
|
15 |
const spaces = jsonResponses.flat();
|
16 |
+
return spaces
|
17 |
};
|
18 |
|
19 |
export const sortByCreatedAt = (spaces: SpaceProps[]) => {
|