Skip to content

Commit ff9a65f

Browse files
committed
feat(resources): enhance resource retrieval and detail components, add empty state handling
1 parent 7fcb47e commit ff9a65f

File tree

7 files changed

+242
-51
lines changed

7 files changed

+242
-51
lines changed

src/actions/resources.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,47 @@ import type {
44
CreateResourceInput,
55
UpdateResourceInput,
66
Resource,
7+
ResourceList,
78
} from "@/types";
89

910
import { createClient } from "@/lib/supabase/server";
1011

11-
export const getResources = async () => {
12+
export const getResources = async (): Promise<ResourceList[]> => {
1213
const supabase = await createClient();
1314
const { data, error } = await supabase.from("resources").select(`
1415
id,
1516
name,
1617
slug,
1718
image,
1819
tags,
19-
category:categories (id, name)
20+
category:categories (id, name, slug, created_at)
2021
`);
2122

22-
if (error) throw error;
23+
if (error) throw new Error(error.message);
24+
// @ts-ignore - supabase returns an array, but we want to return a single object
25+
return data;
26+
};
27+
28+
export const getResourcesByCategory = async (
29+
category: string,
30+
): Promise<ResourceList[]> => {
31+
const supabase = await createClient();
32+
const { data, error } = await supabase
33+
.from("resources")
34+
.select(
35+
`
36+
id,
37+
name,
38+
slug,
39+
image,
40+
tags,
41+
category:categories!inner (id, name, slug)
42+
`,
43+
)
44+
.eq("categories.slug", category);
45+
46+
if (error) throw new Error(error.message);
47+
// @ts-ignore - supabase returns an array, but we want to return a single object
2348
return data;
2449
};
2550

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { notFound } from "next/navigation";
22
import { getResourceBySlug } from "@/actions/resources";
3+
import { ResourceDetail } from "@/components/resources";
34

45
type Props = {
56
params: { slug: string };
@@ -10,37 +11,5 @@ export default async function ResourceDetailPage({ params }: Props) {
1011
const resource = await getResourceBySlug(slug);
1112
if (!resource) return notFound();
1213

13-
return (
14-
<main className="max-w-4xl mx-auto px-4 py-8">
15-
<article className="space-y-6">
16-
<header className="space-y-4">
17-
<h1 className="text-3xl font-bold text-white">{resource.name}</h1>
18-
<p className="text-gray-600 text-lg">
19-
{resource.description || "No description available."}
20-
</p>
21-
<div className="flex flex-wrap gap-2">
22-
{resource.tags?.map((tag, idx) => (
23-
<span
24-
key={idx}
25-
className="bg-blue-100 text-blue-800 text-sm font-medium px-2.5 py-0.5 rounded"
26-
>
27-
{tag}
28-
</span>
29-
))}
30-
</div>
31-
<div className="text-sm text-gray-500">
32-
<span>{new Date(resource.created_at).toLocaleDateString()}</span>
33-
</div>
34-
</header>
35-
36-
{resource.image && (
37-
<img
38-
src={resource.image}
39-
alt={resource.name}
40-
className="w-full h-auto rounded-xl shadow-md"
41-
/>
42-
)}
43-
</article>
44-
</main>
45-
);
14+
return <ResourceDetail resource={resource} />;
4615
}
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
1+
import { getResourcesByCategory } from "@/actions/resources";
2+
import { ResourceList } from "@/components/resources";
3+
import { EmptyList } from "@/components/layout";
14
import { Heading } from "@/components/ui";
25

6+
export const dynamic = "force-dynamic";
7+
38
type Props = {
49
params: { category: string };
510
};
611

712
export default async function ResourcesByCategoryPage({ params }: Props) {
813
const { category } = await params;
14+
const resources = await getResourcesByCategory(category);
15+
16+
const renderResources = () => {
17+
if (resources.length === 0) {
18+
return <EmptyList type="resources" />;
19+
}
20+
return <ResourceList resources={resources} />;
21+
};
922

1023
return (
11-
<main className="max-w-screen-xl mx-auto p-4">
24+
<main className="max-w-screen-xl mx-auto p-4 flex flex-col gap-4">
1225
<Heading
1326
title={category}
1427
subtitle="Explore all the resources by category"
1528
/>
16-
{JSON.stringify(category)}
29+
{renderResources()}
1730
</main>
1831
);
1932
}

src/components/resources/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as ResourceCard } from "./resource-card";
2+
export { default as ResourceDetail } from "./resource-detail";
23
export { default as ResourceFilters } from "./resource-filters";
34
export { default as ResourceList } from "./resource-list";

src/components/resources/resource-card.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"use client";
22

3-
import type { Resource } from "@/types";
3+
import type { ResourceList } from "@/types";
44

55
import Link from "next/link";
66
import { Card, CardFooter, Chip, Image } from "@heroui/react";
77

88
type Props = {
9-
resource: Resource;
9+
resource: ResourceList;
1010
};
1111

1212
export default function ResourceCard({ resource }: Props) {
@@ -20,7 +20,7 @@ export default function ResourceCard({ resource }: Props) {
2020
>
2121
<figure className="relative border-2 border-neutral-200 dark:border-neutral-800 rounded-md overflow-hidden">
2222
<span className="hidden group-hover:block absolute top-2 right-2 bg-neutral-950 dark:bg-white text-neutral-950 px-2 py-1 rounded-lg text-xs z-50">
23-
{resource.category.name}
23+
{resource.category?.name ?? "Uncategorized"}
2424
</span>
2525
<Image
2626
isBlurred
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"use client";
2+
3+
import type { Resource } from "@/types";
4+
5+
import Link from "next/link";
6+
import Image from "next/image";
7+
import { Avatar, Button, Card, Chip } from "@heroui/react";
8+
import { formatDistanceToNow } from "date-fns";
9+
import { ArrowUpRight, Clock, Hash } from "lucide-react";
10+
11+
type Props = {
12+
resource: Resource;
13+
};
14+
15+
const comments = [
16+
{
17+
id: "1",
18+
author: {
19+
name: "Sarah Miller",
20+
avatar: "/placeholder.svg?height=40&width=40",
21+
},
22+
content:
23+
"This was incredibly helpful for my current project. Thank you for sharing!",
24+
date: "April 16, 2023",
25+
},
26+
{
27+
id: "2",
28+
author: {
29+
name: "David Chen",
30+
avatar: "/placeholder.svg?height=40&width=40",
31+
},
32+
content:
33+
"Great resource. I especially appreciated the section on keyboard navigation patterns.",
34+
date: "April 17, 2023",
35+
},
36+
{
37+
id: "3",
38+
author: {
39+
name: "Emily Rodriguez",
40+
avatar: "/placeholder.svg?height=40&width=40",
41+
},
42+
content:
43+
"I've been looking for something like this for weeks. The examples are clear and the explanations are easy to follow.",
44+
date: "April 18, 2023",
45+
},
46+
];
47+
48+
export default function ResourceDetail({ resource }: Props) {
49+
return (
50+
<div className="max-screen-xl mx-auto flex flex-col gap-4">
51+
<div className="lg:grid lg:grid-cols-3">
52+
{/* Main Content */}
53+
<main className="lg:col-span-2 flex flex-col gap-4 p-4">
54+
<header className="relative flex flex-col gap-2 border-2 border-neutral-200 dark:border-neutral-800 rounded-lg p-4">
55+
<h1 className="text-2xl font-bold tracking-tight text-neutral-950 dark:text-white mb-2">
56+
{resource.name}
57+
</h1>
58+
59+
<div className="flex gap-2">
60+
{resource.tags.map((tag) => (
61+
<Chip
62+
key={tag}
63+
size="sm"
64+
variant="flat"
65+
radius="sm"
66+
startContent={<Hash className="h-4 w-4" />}
67+
className="flex items-center gap-1"
68+
>
69+
{tag}
70+
</Chip>
71+
))}
72+
</div>
73+
74+
<div className="max-w-xl line-clamp-2">
75+
{resource.description && (
76+
<p className="text-sm text-neutral-500">
77+
{resource.description}
78+
</p>
79+
)}
80+
</div>
81+
82+
<div className="absolute top-4 right-4 flex items-center gap-1 text-sm text-neutral-500">
83+
<Clock className="h-4 w-4" />
84+
<span>
85+
{formatDistanceToNow(new Date(resource.created_at), {
86+
addSuffix: true,
87+
})}
88+
</span>
89+
</div>
90+
91+
<footer className="flex justify-between">
92+
<div className="flex gap-4">
93+
<div className="flex items-center gap-2 text-sm text-neutral-500">
94+
by{" "}
95+
<span className="text-neutral-950 dark:text-white font-medium">
96+
{comments[0].author.name}
97+
</span>
98+
</div>
99+
</div>
100+
<div className="flex gap-2">
101+
<Button
102+
as={Link}
103+
size="sm"
104+
href="#"
105+
variant="light"
106+
aria-label="Add"
107+
>
108+
Report
109+
</Button>
110+
<Button
111+
as={Link}
112+
size="sm"
113+
target="_blank"
114+
rel="noopener noreferrer"
115+
aria-label="Visit resource"
116+
href={resource.url}
117+
endContent={<ArrowUpRight className="h-4 w-4" />}
118+
className="bg-neutral-950 dark:bg-white text-white dark:text-neutral-950 font-medium"
119+
>
120+
Visit resource
121+
</Button>
122+
</div>
123+
</footer>
124+
</header>
125+
126+
{/* Image */}
127+
{resource.image && (
128+
<figure className="overflow-hidden rounded-xl border-2 border-neutral-200 dark:border-neutral-800">
129+
<Image
130+
src={resource.image || "/placeholder.svg"}
131+
alt={`Image for ${resource.name}`}
132+
width={600}
133+
height={400}
134+
className="h-auto w-full object-cover transition-all hover:scale-105"
135+
/>
136+
</figure>
137+
)}
138+
</main>
139+
140+
{/* Comments */}
141+
<aside className="lg:border-l-2 border-neutral-200 dark:border-neutral-800 flex flex-col gap-4 p-4">
142+
<h2 className="text-2xl font-bold tracking-tight text-neutral-950 dark:text-white mb-2">
143+
Comments
144+
</h2>
145+
<section className="flex flex-col gap-4">
146+
{comments.map((comment) => (
147+
<Card
148+
key={comment.id}
149+
className="space-y-3 rounded-lg p-4"
150+
classNames={{
151+
base: "bg-white dark:bg-neutral-950 border-2 border-neutral-200 dark:border-neutral-800 shadow-none",
152+
}}
153+
>
154+
<div className="flex items-center justify-between">
155+
<div className="flex items-center gap-2">
156+
<Avatar className="h-8 w-8">
157+
<Image
158+
src={comment.author.avatar || "/placeholder.svg"}
159+
alt={comment.author.name}
160+
/>
161+
<div>{comment.author.name.charAt(0)}</div>
162+
</Avatar>
163+
<span className="font-medium">{comment.author.name}</span>
164+
</div>
165+
<span className="text-xs text-muted-foreground">
166+
{comment.date}
167+
</span>
168+
</div>
169+
<p className="text-sm text-neutral-500">{comment.content}</p>
170+
</Card>
171+
))}
172+
</section>
173+
<footer>
174+
<Button
175+
size="sm"
176+
className="w-full"
177+
variant="bordered"
178+
startContent={<ArrowUpRight className="h-4 w-4" />}
179+
>
180+
View all comments
181+
</Button>
182+
</footer>
183+
</aside>
184+
</div>
185+
</div>
186+
);
187+
}
Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
"use client";
22

3-
import type { Resource } from "@/types";
3+
import type { ResourceList } from "@/types";
44

55
import { ResourceCard } from "@/components/resources";
66

77
type Props = {
8-
resources: Resource[];
8+
resources: ResourceList[];
99
};
1010

1111
export default function ResourceList({ resources }: Props) {
1212
return (
13-
<section className="py-8">
14-
<div className="container mx-auto px-4">
15-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
16-
{resources.map((resource) => (
17-
<ResourceCard key={resource.id} resource={resource} />
18-
))}
19-
</div>
20-
</div>
13+
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
14+
{resources.map((resource) => (
15+
<ResourceCard key={resource.id} resource={resource} />
16+
))}
2117
</section>
2218
);
2319
}

0 commit comments

Comments
 (0)