Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/app/(home)/_components/ArticleFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"use client";

import * as articleActions from "@/backend/services/article.actions";
import * as seriesActions from "@/backend/services/series.action";
import ArticleCard from "@/components/ArticleCard";
import SeriesCard from "@/components/SeriesCard";
import VisibilitySensor from "@/components/VisibilitySensor";
import { readingTime } from "@/lib/utils";
import getFileUrl from "@/utils/getFileUrl";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Loader } from "lucide-react";
import { useState } from "react";

const ArticleFeed = () => {
Expand Down Expand Up @@ -53,10 +50,22 @@ const ArticleFeed = () => {
<div className="flex flex-col gap-10 mt-2">
{articleFeedQuery.isPending && (
<>
<div className="h-56 bg-muted animate-pulse mx-4" />
<div className="h-56 bg-muted animate-pulse mx-4" />
<div className="h-56 bg-muted animate-pulse mx-4" />
<div className="h-56 bg-muted animate-pulse mx-4" />
<div
className="h-56 bg-muted animate-pulse mx-4"
suppressHydrationWarning={true}
/>
<div
className="h-56 bg-muted animate-pulse mx-4"
suppressHydrationWarning={true}
/>
<div
className="h-56 bg-muted animate-pulse mx-4"
suppressHydrationWarning={true}
/>
<div
className="h-56 bg-muted animate-pulse mx-4"
suppressHydrationWarning={true}
/>
</>
)}

Expand Down
2 changes: 1 addition & 1 deletion src/app/(home)/_components/HomeLeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const tags = [
{
icon: "https://res.cloudinary.com/techdiary-dev/image/upload/v1620782239/static-assets/tag-icons/erfbu54l2mquphszheck.svg",
label: "react",
link: "/tags/186e052a-9c5b-4ffe-b753-ea172ac2e663",
link: "/tags/reactjs",
},
{
icon: "https://res.cloudinary.com/techdiary-dev/image/upload/v1620782240/static-assets/tag-icons/rh7xfiz28bxklfzymftd.svg",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
"use client";

import { Tag } from "@/backend/models/domain-models";
import * as articleActions from "@/backend/services/article.actions";
import ArticleCard from "@/components/ArticleCard";
import VisibilitySensor from "@/components/VisibilitySensor";
import { useTranslation } from "@/i18n/use-translation";
import { readingTime } from "@/lib/utils";
import getFileUrl from "@/utils/getFileUrl";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import React, { useMemo } from "react";

interface TagArticleFeedProps {
tagId: string;
tag: Tag;
}

const TagArticleFeed: React.FC<TagArticleFeedProps> = ({ tagId }) => {
const TagArticleFeed: React.FC<TagArticleFeedProps> = ({ tag }) => {
const { _t } = useTranslation();
const tagFeedQuery = useInfiniteQuery({
queryKey: ["tag-articles", tagId],
queryKey: ["tag-articles", tag.id],
queryFn: ({ pageParam }) =>
articleActions.articlesByTag({
tag_id: tagId,
tag_id: tag.id,
limit: 5,
page: pageParam,
}),
Expand All @@ -37,18 +40,14 @@ const TagArticleFeed: React.FC<TagArticleFeedProps> = ({ tagId }) => {
return tagFeedQuery.data?.pages?.[0]?.meta?.total ?? 0;
}, [tagFeedQuery.data]);

const tagName = useMemo(() => {
return tagFeedQuery.data?.pages?.[0]?.tagName ?? "Unknown Tag";
}, [tagFeedQuery.data]);

// Show loading skeletons
if (tagFeedQuery.isPending) {
return (
<div className="flex flex-col gap-10 mt-2">
<div className="h-56 bg-muted animate-pulse mx-4" />
<div className="h-56 bg-muted animate-pulse mx-4" />
<div className="h-56 bg-muted animate-pulse mx-4" />
<div className="h-56 bg-muted animate-pulse mx-4" />
<div className="h-56 bg-muted animate-pulse mx-4" suppressHydrationWarning={true} />
<div className="h-56 bg-muted animate-pulse mx-4" suppressHydrationWarning={true} />
<div className="h-56 bg-muted animate-pulse mx-4" suppressHydrationWarning={true} />
<div className="h-56 bg-muted animate-pulse mx-4" suppressHydrationWarning={true} />
</div>
);
}
Expand Down Expand Up @@ -78,10 +77,10 @@ const TagArticleFeed: React.FC<TagArticleFeedProps> = ({ tagId }) => {
return (
<div className="flex flex-col items-center justify-center py-12">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
No articles found
{_t("No articles found")}
</h2>
<p className="text-gray-600 dark:text-gray-400">
No articles have been tagged with &ldquo;{tagName}&rdquo; yet.
{_t(`No articles have been tagged with "$" yet.`, [tag.name])}
</p>
</div>
);
Expand All @@ -91,10 +90,10 @@ const TagArticleFeed: React.FC<TagArticleFeedProps> = ({ tagId }) => {
<>
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Articles tagged with &ldquo;{tagName}&rdquo;
{_t(`Articles tagged with "$"`, [tag.name])}
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
Found {totalArticles} articles
{_t(`Found $ articles`, [totalArticles])}
</p>
</div>

Expand All @@ -106,7 +105,9 @@ const TagArticleFeed: React.FC<TagArticleFeedProps> = ({ tagId }) => {
handle={article?.handle ?? ""}
title={article?.title ?? ""}
excerpt={article?.excerpt ?? ""}
coverImage={article?.cover_image ? getFileUrl(article.cover_image) : ""}
coverImage={
article?.cover_image ? getFileUrl(article.cover_image) : ""
}
author={{
id: article?.user?.id ?? "",
name: article?.user?.name ?? "",
Expand All @@ -133,4 +134,4 @@ const TagArticleFeed: React.FC<TagArticleFeedProps> = ({ tagId }) => {
);
};

export default TagArticleFeed;
export default TagArticleFeed;
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ import HomeRightSidebar from "@/app/(home)/_components/HomeRightSidebar";
import SidebarToggleButton from "@/app/(home)/_components/SidebarToggleButton";
import HomepageLayout from "@/components/layout/HomepageLayout";
import TagArticleFeed from "./_components/TagArticleFeed";
import { getTag, getTags } from "@/backend/services/tag.action";
import { notFound } from "next/navigation";

interface TagPageProps {
params: Promise<{
tag_id: string;
tag_name: string;
}>;
}

export default async function TagPage({ params }: TagPageProps) {
const { tag_id } = await params;
const { tag_name } = await params;
const tag = await getTag({ name: tag_name });

if (!tag?.success) {
throw notFound();
}

return (
<HomepageLayout
Expand All @@ -20,21 +27,21 @@ export default async function TagPage({ params }: TagPageProps) {
NavbarTrailing={<SidebarToggleButton />}
>
<div className="px-4 py-6">
<TagArticleFeed tagId={tag_id} />
<TagArticleFeed tag={tag.data} />
</div>
</HomepageLayout>
);
}

export async function generateMetadata({ params }: TagPageProps) {
const { tag_id } = await params;
const { tag_name } = await params;

// For now, use tag_id in the title. Later we can fetch the tag name if needed
return {
title: `Tag ${tag_id} - Tech Diary`,
title: `Tag ${tag_name} - Tech Diary`,
description: `Browse all articles with this tag on Tech Diary`,
openGraph: {
title: `Tag ${tag_id} - Tech Diary`,
title: `Tag ${tag_name} - Tech Diary`,
description: `Browse all articles with this tag on Tech Diary`,
},
};
Expand Down
3 changes: 3 additions & 0 deletions src/backend/services/inputs/tag.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export const TagRepositoryInput = {
color: z.string().optional().nullable(),
description: z.string().optional().nullable(),
}),
getTag: z.object({
name: z.string(),
}),
updateInput: z.object({
tag_id: z.string(),
name: z.string().optional(),
Expand Down
18 changes: 18 additions & 0 deletions src/backend/services/tag.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ export const getTags = async (
}
};

export const getTag = async (
_input: z.infer<typeof TagRepositoryInput.getTag>
) => {
try {
const input = await TagRepositoryInput.createInput.parseAsync(_input);
const response = await persistenceRepository.tags.find({
where: eq("name", input.name),
});

return {
data: response[0],
success: true as const,
};
} catch (error) {
handleActionException(error);
}
};

export const createTag = async (
_input: z.infer<typeof TagRepositoryInput.createInput>
) => {
Expand Down