diff --git a/app/(app)/articles/[slug]/page.tsx b/app/(app)/articles/[slug]/page.tsx index d8203a33..508fa7a8 100644 --- a/app/(app)/articles/[slug]/page.tsx +++ b/app/(app)/articles/[slug]/page.tsx @@ -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 } }; @@ -21,6 +22,7 @@ export async function generateMetadata({ params }: Props): Promise { 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 {}; @@ -84,11 +86,12 @@ const ArticlePage = async ({ params }: Props) => {
{post.tags.map(({ tag }) => ( - {tag.title} + {getCamelCaseFromLower(tag.title)} ))}
diff --git a/app/(app)/articles/_client.tsx b/app/(app)/articles/_client.tsx index 5c3299bf..6a5d25f1 100644 --- a/app/(app)/articles/_client.tsx +++ b/app/(app)/articles/_client.tsx @@ -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(); @@ -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"]; @@ -61,6 +50,10 @@ const ArticlesPage = () => { }, ); + const { status: tagsStatus, data: tagsData } = api.tag.get.useQuery({ + take: 10, + }); + const { ref, inView } = useInView(); useEffect(() => { @@ -68,11 +61,6 @@ const ArticlesPage = () => { 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 ( <>
@@ -81,7 +69,7 @@ const ArticlesPage = () => { {typeof tag === "string" ? (
- {capitalize(tag)} + {getCamelCaseFromLower(tag)}
) : ( "Articles" @@ -106,7 +94,7 @@ const ArticlesPage = () => { > {filters.map((filter) => ( ))} @@ -192,18 +180,21 @@ const ArticlesPage = () => {

- Recommended topics + Popular topics

- {tagsToShow.map((tag) => ( - - {tag} - - ))} + {tagsStatus === "loading" && } + {tagsStatus === "success" && + tagsData.data.map((tag) => ( + + {getCamelCaseFromLower(tag.title)} + + ))}
{session && (
diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index ff459a21..01b7641a 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -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 ( @@ -83,18 +73,12 @@ const Home = async () => {

- Recommended topics + Popular topics

- {tagsToShow.map((tag) => ( - - {tag} - - ))} + }> + +
{session && (
diff --git a/components/PopularTags/PopularTags.tsx b/components/PopularTags/PopularTags.tsx new file mode 100644 index 00000000..14a6764e --- /dev/null +++ b/components/PopularTags/PopularTags.tsx @@ -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 ( +
+ Something went wrong loading topics... Please refresh the page. +
+ ); + + return ( + <> + {tags.map((tag) => ( + + {getCamelCaseFromLower(tag.title)} + + ))} + + ); +} diff --git a/components/PopularTags/PopularTagsLoading.tsx b/components/PopularTags/PopularTagsLoading.tsx new file mode 100644 index 00000000..9520e4bd --- /dev/null +++ b/components/PopularTags/PopularTagsLoading.tsx @@ -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 ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +export default PopularTagsLoading; diff --git a/schema/tag.ts b/schema/tag.ts new file mode 100644 index 00000000..fe5e2142 --- /dev/null +++ b/schema/tag.ts @@ -0,0 +1,5 @@ +import z from "zod"; + +export const GetTagsSchema = z.object({ + take: z.number(), +}); diff --git a/server/api/router/index.ts b/server/api/router/index.ts index e039d32b..275a49bf 100644 --- a/server/api/router/index.ts +++ b/server/api/router/index.ts @@ -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, @@ -17,6 +18,7 @@ export const appRouter = createTRPCRouter({ community: communityRouter, event: eventRouter, report: reportRouter, + tag: tagRouter, }); // export type definition of API diff --git a/server/api/router/tag.ts b/server/api/router/tag.ts new file mode 100644 index 00000000..214c8bd3 --- /dev/null +++ b/server/api/router/tag.ts @@ -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", + }); + } + }), +}); diff --git a/server/lib/tags.ts b/server/lib/tags.ts new file mode 100644 index 00000000..8eeb588a --- /dev/null +++ b/server/lib/tags.ts @@ -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; + +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"); + } +} diff --git a/utils/utils.ts b/utils/utils.ts index ee6dc04e..e7eb7532 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -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; +};