Skip to content

Commit

Permalink
feat: pull most used tags from DB instead of using hardcoded list (#795)
Browse files Browse the repository at this point in the history
* feat: pull most used tags from DB instead of using hardcoded list

* Update server/api/router/tag.ts

* chore: die prettier die
  • Loading branch information
JohnAllenTech authored Mar 6, 2024
1 parent 6924ace commit 01cdafe
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 54 deletions.
5 changes: 4 additions & 1 deletion app/(app)/articles/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getServerAuthSession } from "@/server/auth";
import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel";
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";

type Props = { params: { slug: string } };

Expand All @@ -21,6 +22,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost({ slug });

// @TODO revisit to give more defaults
// @TODO can we parse article and give recommended tags?
const tags = post?.tags.map((tag) => tag.tag.title);

if (!post) return {};
Expand Down Expand Up @@ -84,11 +86,12 @@ const ArticlePage = async ({ params }: Props) => {
<section className="flex flex-wrap gap-3">
{post.tags.map(({ tag }) => (
<Link
// only reason this is toLowerCase is to make url look nicer. Not needed for functionality
href={`/articles?tag=${tag.title.toLowerCase()}`}
key={tag.title}
className="rounded-full bg-gradient-to-r from-orange-400 to-pink-600 px-3 py-1 text-xs font-bold text-white hover:bg-pink-700"
>
{tag.title}
{getCamelCaseFromLower(tag.title)}
</Link>
))}
</section>
Expand Down
53 changes: 22 additions & 31 deletions app/(app)/articles/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,8 @@ import challenge from "@/public/images/announcements/challenge.png";
import { api } from "@/server/trpc/react";
import SideBarSavedPosts from "@/components/SideBar/SideBarSavedPosts";
import { useSession } from "next-auth/react";

// Needs to be added to DB but testing with hardcoding
const tagsToShow = [
"JavaScript",
"Web Development",
"Tutorial",
"Productivity",
"CSS",
"Terminal",
"Django",
"Python",
"Tips",
];
import { getCamelCaseFromLower } from "@/utils/utils";
import PopularTagsLoading from "@/components/PopularTags/PopularTagsLoading";

const ArticlesPage = () => {
const searchParams = useSearchParams();
Expand All @@ -33,7 +22,7 @@ const ArticlesPage = () => {
const filter = searchParams?.get("filter");
const dirtyTag = searchParams?.get("tag");

const tag = typeof dirtyTag === "string" ? dirtyTag.toLowerCase() : null;
const tag = typeof dirtyTag === "string" ? dirtyTag : null;
type Filter = "newest" | "oldest" | "top";
const filters: Filter[] = ["newest", "oldest", "top"];

Expand Down Expand Up @@ -61,18 +50,17 @@ const ArticlesPage = () => {
},
);

const { status: tagsStatus, data: tagsData } = api.tag.get.useQuery({
take: 10,
});

const { ref, inView } = useInView();

useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView]);

// @TODO make a list of words like "JavaScript" that we can map the words to if they exist
const capitalize = (str: string) =>
str.replace(/(?:^|\s|["'([{])+\S/g, (match) => match.toUpperCase());

return (
<>
<div className="mx-2">
Expand All @@ -81,7 +69,7 @@ const ArticlesPage = () => {
{typeof tag === "string" ? (
<div className="flex items-center justify-center">
<TagIcon className="mr-3 h-6 w-6 text-neutral-800 dark:text-neutral-200" />
{capitalize(tag)}
{getCamelCaseFromLower(tag)}
</div>
) : (
"Articles"
Expand All @@ -106,7 +94,7 @@ const ArticlesPage = () => {
>
{filters.map((filter) => (
<option key={filter} value={filter}>
{capitalize(filter)}
{getCamelCaseFromLower(filter)}
</option>
))}
</select>
Expand Down Expand Up @@ -192,18 +180,21 @@ const ArticlesPage = () => {
</div>
</div>
<h3 className="mb-4 mt-4 text-2xl font-semibold leading-6 tracking-wide">
Recommended topics
Popular topics
</h3>
<div className="flex flex-wrap gap-2">
{tagsToShow.map((tag) => (
<Link
key={tag}
href={`/articles?tag=${tag.toLowerCase()}`}
className="border border-neutral-300 bg-white px-6 py-2 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-50"
>
{tag}
</Link>
))}
{tagsStatus === "loading" && <PopularTagsLoading />}
{tagsStatus === "success" &&
tagsData.data.map((tag) => (
<Link
key={tag.id}
// only reason this is toLowerCase is to make url look nicer. Not needed for functionality
href={`/articles?tag=${tag.title.toLowerCase()}`}
className="border border-neutral-300 bg-white px-6 py-2 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-50"
>
{getCamelCaseFromLower(tag.title)}
</Link>
))}
</div>
{session && (
<div className="flex flex-wrap gap-2">
Expand Down
28 changes: 6 additions & 22 deletions app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,10 @@ import TrendingPosts from "@/components/TrendingPosts/TrendingPosts";
import TrendingLoading from "@/components/TrendingPosts/TrendingPostsLoading";
import SideBarSavedPosts from "@/components/SideBar/SideBarSavedPosts";
import { getServerAuthSession } from "@/server/auth";
import PopularTags from "@/components/PopularTags/PopularTags";
import PopularTagsLoading from "@/components/PopularTags/PopularTagsLoading";

const Home = async () => {
const tagsToShow = [
"JavaScript",
"Web Development",
"Tutorial",
"Productivity",
"CSS",
"Terminal",
"Django",
"Python",
"Tips",
];

const session = await getServerAuthSession();

return (
Expand Down Expand Up @@ -83,18 +73,12 @@ const Home = async () => {
</div>
</div>
<h4 className="mb-4 mt-4 text-2xl font-semibold leading-6 tracking-wide">
Recommended topics
Popular topics
</h4>
<div className="flex flex-wrap gap-2">
{tagsToShow.map((tag) => (
<Link
key={tag}
href={`/articles?tag=${tag.toLowerCase()}`}
className="border border-neutral-300 bg-white px-6 py-2 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-50"
>
{tag}
</Link>
))}
<Suspense fallback={<PopularTagsLoading />}>
<PopularTags />
</Suspense>
</div>
{session && (
<div className="flex flex-wrap gap-2">
Expand Down
31 changes: 31 additions & 0 deletions components/PopularTags/PopularTags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use server";

import Link from "next/link";
import { GetTags } from "@/server/lib/tags";
import { getCamelCaseFromLower } from "@/utils/utils";

export default async function PopularTags() {
const tags = await GetTags({ take: 10 });
// Refactor with option to refresh
if (!tags)
return (
<div className="relative mt-4 text-lg font-semibold md:col-span-7">
Something went wrong loading topics... Please refresh the page.
</div>
);

return (
<>
{tags.map((tag) => (
<Link
// only reason this is toLowerCase is to make url look nicer. Not needed for functionality
href={`/articles?tag=${tag.title.toLowerCase()}`}
key={tag.title}
className="border border-neutral-300 bg-white px-6 py-2 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-50"
>
{getCamelCaseFromLower(tag.title)}
</Link>
))}
</>
);
}
34 changes: 34 additions & 0 deletions components/PopularTags/PopularTagsLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//className="border border-neutral-300 bg-white px-6 py-2 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-50"

function PopularTagsLoading() {
return (
<div className=" w-full shadow dark:bg-black">
<div>
<div className="my-2 flex h-10">
<div className="h-10 w-1/2 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
<div className="ml-2 h-10 w-2/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
</div>
<div className="my-2 flex h-10">
<div className="h-10 w-1/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
<div className="ml-2 h-10 w-1/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
<div className="ml-2 h-10 w-1/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
</div>
<div className="my-2x flex h-10">
<div className="h-10 w-1/2 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
<div className="ml-2 h-10 w-2/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
</div>
<div className="my-2 flex h-10">
<div className="h-10 w-1/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
<div className="ml-2 h-10 w-1/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
<div className="ml-2 h-10 w-1/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
</div>
<div className="my-2x flex h-10">
<div className="h-10 w-1/2 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
<div className="ml-2 h-10 w-2/3 animate-pulse rounded-md bg-neutral-300 dark:bg-neutral-800" />
</div>
</div>
</div>
);
}

export default PopularTagsLoading;
5 changes: 5 additions & 0 deletions schema/tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import z from "zod";

export const GetTagsSchema = z.object({
take: z.number(),
});
2 changes: 2 additions & 0 deletions server/api/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { eventRouter } from "./event";
import { notificationRouter } from "./notification";
import { adminRouter } from "./admin";
import { reportRouter } from "./report";
import { tagRouter } from "./tag";

export const appRouter = createTRPCRouter({
post: postRouter,
Expand All @@ -17,6 +18,7 @@ export const appRouter = createTRPCRouter({
community: communityRouter,
event: eventRouter,
report: reportRouter,
tag: tagRouter,
});

// export type definition of API
Expand Down
26 changes: 26 additions & 0 deletions server/api/router/tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createTRPCRouter, publicProcedure } from "../trpc";
import { GetTagsSchema } from "../../../schema/tag";
import { TRPCError } from "@trpc/server";

export const tagRouter = createTRPCRouter({
get: publicProcedure.input(GetTagsSchema).query(async ({ ctx, input }) => {
try {
const count = await ctx.db.tag.count({});
const response = await ctx.db.tag.findMany({
orderBy: {
PostTag: {
_count: "desc",
},
},
take: input.take,
});

return { data: response, count };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to fetch tags",
});
}
}),
});
34 changes: 34 additions & 0 deletions server/lib/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import db from "@/server/db/client";
import * as Sentry from "@sentry/nextjs";
import "server-only";
import { z } from "zod";

export const GetTagsSchema = z.object({
take: z.number(),
});

type GetTags = z.infer<typeof GetTagsSchema>;

export async function GetTags({ take }: GetTags) {
try {
GetTagsSchema.parse({ take });

const response = await db.tag.findMany({
orderBy: {
PostTag: {
_count: "desc",
},
},
take: take,
});

if (!response) {
return null;
}

return response;
} catch (error) {
Sentry.captureException(error);
throw new Error("Error fetching tags");
}
}
17 changes: 17 additions & 0 deletions utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,20 @@ export function getUrlFromString(str: string) {
return null;
}
}

// @TODO move this somewhere nicer
const commonCamelCaseCSWords = new Map([
["javascript", "JavaScript"],
["css", "CSS"],
]);

// @TODO make a list of words like "JavaScript" that we can map the words to if they exist
export const getCamelCaseFromLower = (str: string) => {
let formatedString = commonCamelCaseCSWords.get(str.toLowerCase());
if (!formatedString) {
formatedString = str
.toLowerCase()
.replace(/(?:^|\s|["'([{])+\S/g, (match) => match.toUpperCase());
}
return formatedString;
};

0 comments on commit 01cdafe

Please sign in to comment.