Skip to content

Commit 7fcb47e

Browse files
committed
feat(components, hooks): add empty-list component for empty state handling and implement animation on view hook
1 parent d7c6596 commit 7fcb47e

File tree

5 files changed

+86
-6
lines changed

5 files changed

+86
-6
lines changed

src/app/(public)/explore/page.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
22

33
import { getResources } from "@/actions/resources";
44
import { ResourceList } from "@/components/resources";
5+
import { EmptyList } from "@/components/layout";
56
import { Heading } from "@/components/ui";
67
import { PROJECT_NAME } from "@/config/constants";
78

@@ -12,12 +13,18 @@ export const metadata: Metadata = {
1213

1314
export default async function ExplorePage() {
1415
const resources = await getResources();
15-
if (resources.length === 0) return <div>No resources found</div>;
16+
17+
const renderResources = () => {
18+
if (resources.length === 0) {
19+
return <EmptyList type="resources" />;
20+
}
21+
return <ResourceList resources={resources} />;
22+
};
1623

1724
return (
18-
<main className="max-w-screen-xl mx-auto p-4">
25+
<main className="max-w-screen-xl mx-auto p-4 flex flex-col gap-4">
1926
<Heading title="Explore" subtitle="Discover new resources" />
20-
<ResourceList resources={resources} />
27+
{renderResources()}
2128
</main>
2229
);
2330
}

src/app/(public)/resources/page.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
22

33
import { getResources } from "@/actions/resources";
44
import { ResourceList } from "@/components/resources";
5+
import { EmptyList } from "@/components/layout";
56
import { Heading } from "@/components/ui";
67
import { PROJECT_NAME } from "@/config/constants";
78

@@ -12,12 +13,18 @@ export const metadata: Metadata = {
1213

1314
export default async function ResourcesPage() {
1415
const resources = await getResources();
15-
if (resources.length === 0) return <div>No resources found</div>;
16+
17+
const renderResources = () => {
18+
if (resources.length === 0) {
19+
return <EmptyList type="resources" />;
20+
}
21+
return <ResourceList resources={resources} />;
22+
};
1623

1724
return (
18-
<main className="max-w-screen-xl mx-auto p-4">
25+
<main className="max-w-screen-xl mx-auto p-4 flex flex-col gap-4">
1926
<Heading title="Resources" subtitle="Explore all the resources" />
20-
<ResourceList resources={resources} />
27+
{renderResources()}
2128
</main>
2229
);
2330
}

src/components/layout/empty-list.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { motion } from "framer-motion";
4+
import { FolderOpen } from "lucide-react";
5+
6+
type Props = {
7+
type: string;
8+
};
9+
10+
export default function EmptyList({ type }: Props) {
11+
return (
12+
<section className="relative flex flex-col items-center justify-center gap-4 text-center border-2 border-neutral-200 dark:border-neutral-800 p-10 rounded-xl h-[calc(100vh-200px)]">
13+
<motion.header
14+
className="flex flex-col gap-4 justify-center items-center z-10"
15+
initial={{ opacity: 0, scale: 0.8 }}
16+
animate={{ opacity: 1, scale: 1 }}
17+
transition={{ duration: 0.3, ease: "easeOut" }}
18+
>
19+
<FolderOpen
20+
size={40}
21+
className="text-neutral-950 dark:text-white animate-pulse"
22+
/>
23+
<h2 className="text-2xl font-bold z-10">No {type} found.</h2>
24+
</motion.header>
25+
<motion.div
26+
className="text-sm text-center"
27+
initial={{ opacity: 0, y: 20 }}
28+
animate={{ opacity: 1, y: 0 }}
29+
transition={{ duration: 0.3, ease: "easeOut" }}
30+
>
31+
<p className="text-sm text-neutral-500">
32+
Try searching for a different keyword or category.
33+
</p>
34+
</motion.div>
35+
</section>
36+
);
37+
}

src/components/layout/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { default as EmptyList } from "./empty-list";
12
export { default as Footer } from "./footer";
23
export { default as Navbar } from "./navbar";
34
export { default as Sidebar } from "./sidebar";

src/hooks/use-animate-on-view.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
import { useInView } from "react-intersection-observer";
5+
import { useAnimation } from "framer-motion";
6+
7+
export const useAnimateOnView = (threshold = 0.1, triggerOnce = false) => {
8+
const controls = useAnimation();
9+
const { ref, inView } = useInView({
10+
triggerOnce,
11+
threshold,
12+
});
13+
14+
useEffect(() => {
15+
if (inView) {
16+
controls.start("visible");
17+
} else {
18+
controls.start("hidden");
19+
}
20+
}, [inView, controls]);
21+
22+
const itemVariants = {
23+
hidden: { opacity: 0, x: 0 },
24+
visible: { opacity: 1, x: 0 },
25+
};
26+
27+
return { ref, controls, itemVariants };
28+
};

0 commit comments

Comments
 (0)