Skip to content

Commit

Permalink
feat(website): show blog posts on /blog route (#76)
Browse files Browse the repository at this point in the history
finally..
  • Loading branch information
Khenziii authored Oct 26, 2024
1 parent 70af364 commit 5ada7a7
Show file tree
Hide file tree
Showing 25 changed files with 594 additions and 84 deletions.
139 changes: 139 additions & 0 deletions src/app/(website)/blog/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client";

import {
useContext,
useEffect,
type FC,
} from "react";
import { api, IsNotFoundContext } from "@khenzii-dev/providers";
import {
Loading,
Paragraph,
Button,
Icon,
Anchor,
Header,
Flex,
} from "@khenzii-dev/ui/atoms";
import { MarkdownRenderer } from "@khenzii-dev/ui/molecules";
import { Tags } from "@khenzii-dev/ui/organisms";
import { blogTagToUiTag } from "@khenzii-dev/utils";
import { useMobile } from "@khenzii-dev/hooks";
import style from "@khenzii-dev/styles/blog_post.module.scss";

const formatDate = (date: Date): string => {
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");

return `${day}/${month}/${year} - ${hour}:${minute}`;
};

type BlogPostProps = {
params: { id: string };
};

const BlogPost: FC<BlogPostProps> = ({ params }) => {
const { setIsNotFound } = useContext(IsNotFoundContext);
const mobile = useMobile();

const {
data: postData,
isLoading: postIsLoading,
isError: postError,
} = api.blog.blogPost.getPost.useQuery({
id: params.id,
});
const { data: tagsData } = api.blog.blogTag.getTags.useQuery();

useEffect(() => {
if (postIsLoading) return;

if (postError || postData === null || postData === undefined) {
setIsNotFound(true);
return;
}

return () => setIsNotFound(false);
}, [
postIsLoading,
postError,
postData,
setIsNotFound,
]);

if (postIsLoading) return (
<Loading size={200} />
);

if (postError) return (
<Flex
direction={"column"}
align={"center"}
styles={{ paddingInline: "10px" }}
>
<Header>Error!</Header>
<Paragraph
fontSize={1.5}
styles={{ textAlign: "center" }}
>
{"Something went wrong while trying to fetch the post!"}
</Paragraph>

<Anchor href={"/"} prefetch>
<Button style={{ width: "fit-content", padding: "10px" }}>
<Icon iconName={"house"} size={2.5} />
</Button>
</Anchor>
</Flex>
);

if (postData === null || postData === undefined) return (
<Paragraph fontSize={1.5} styles={{ textAlign: "center" }}>
{"This route doesn't exist."}
</Paragraph>
);

return (
<Flex
direction={"column"}
className={style.post_container}
>
<Anchor href={"/blog"} styles={{ width: "fit-content" }} prefetch>
<Button style={{ width: "fit-content", padding: "10px" }}>
<Icon iconName={"arrow-left-short"} size={mobile ? 1.75 : 2.5} />
</Button>
</Anchor>

<Flex direction={mobile ? "column" : "row"}>
<Paragraph
fontSize={mobile ? 2 : 3}
styles={{ lineBreak: "auto", hyphens: "auto" }}
>
{postData.title}
</Paragraph>

<hr className={style.post_line} />

<Flex align={"center"}>
<Icon iconName={"clock"} size={mobile ? 1.5 : 2} />
<Paragraph fontSize={mobile ? 1.5 : 2}>{formatDate(postData.created_at)}</Paragraph>
</Flex>
</Flex>

<Tags
tags={blogTagToUiTag(postData.tagIDs, tagsData)}
size={mobile ? 1.5 : 2}
clickable={false}
showOnlyActive
/>

<MarkdownRenderer sizeMultiplier={mobile ? 1 : 1.25}>{postData.content}</MarkdownRenderer>
</Flex>
);
};

export default BlogPost;

6 changes: 2 additions & 4 deletions src/app/(website)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { Paragraph } from "@khenzii-dev/ui/atoms";
import type { Metadata } from "next";
import { Posts } from "@khenzii-dev/ui/organisms";

export const metadata: Metadata = {
title: "Blog",
};

const Blog = () => (
<Paragraph fontSize={1.5} styles={{ textAlign: "center" }}>
This page is not yet available!
</Paragraph>
<Posts />
);

export default Blog;
4 changes: 2 additions & 2 deletions src/app/(website)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const Home = () => {
</Paragraph>

<Flex direction={"column"} align={"flex-start"} gap={0}>
<Header fontsize={mobile ? 1.75 : 2}>About Me</Header>
<Header fontSize={mobile ? 1.75 : 2}>About Me</Header>

<ul className={style.aboutList}>
<li>
Expand All @@ -78,7 +78,7 @@ const Home = () => {
</Flex>

<Flex direction={"column"} align={"flex-start"}>
<Header fontsize={mobile ? 1.75 : 2}>Current Project</Header>
<Header fontSize={mobile ? 1.75 : 2}>Current Project</Header>

{(currentProjectIsLoading || currentProjectData === undefined)
? (
Expand Down
16 changes: 9 additions & 7 deletions src/app/admin/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ const AdminBlog = () => {
}])}
id={"dialog"}
>
<Flex direction={"column"} gap={10} styles={{ height: "100%" }}>
<Flex direction={"column"} gap={10} styles={{ height: "100%", overflowY: "scroll" }}>
<Flex
direction={"row"}
gap={10}
Expand Down Expand Up @@ -402,12 +402,14 @@ const AdminBlog = () => {
</div>

<div style={(dialogVariant === dialogVariantEnum.TAG || !showMarkdown) ? { display: "none" } : {}}>
<Paragraph fontSize={2}>Tags: {tags
.filter((tag) => tag.active)
.map((tag) => tag.name)
.join(", ")
}</Paragraph>

<Paragraph fontSize={2}>Tags:</Paragraph>
<Tags
size={1.5}
tags={tags}
clickable={false}
showOnlyActive
/>

<Paragraph fontSize={2}>{blogPostTitle}</Paragraph>

<MarkdownRenderer>{blogPostContent}</MarkdownRenderer>
Expand Down
13 changes: 8 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import clsx from "clsx";
import {
TRPCProvider,
SessionProviderWrapper,
IsNotFoundProvider,
} from "@khenzii-dev/providers";
import style from "@khenzii-dev/styles/layout.module.scss";

Expand All @@ -26,11 +27,13 @@ export const metadata: Metadata = {
const RootLayout: FC<{ children: ReactNode }> = ({ children }) => (
<TRPCProvider>
<SessionProviderWrapper>
<html lang="en">
<body className={clsx(style.layout, montserrat.className)}>
{children}
</body>
</html>
<IsNotFoundProvider>
<html lang="en">
<body className={clsx(style.layout, montserrat.className)}>
{children}
</body>
</html>
</IsNotFoundProvider>
</SessionProviderWrapper>
</TRPCProvider>
);
Expand Down
44 changes: 29 additions & 15 deletions src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
"use client";

import { useContext, useEffect } from "react";
import { IsNotFoundContext } from "@khenzii-dev/providers";
import { Footer, Nav } from "@khenzii-dev/ui/organisms";
import { Paragraph } from "@khenzii-dev/ui/atoms";
import style from "@khenzii-dev/styles/layout_website.module.scss";

const NotFound = () => (
<>
<nav className={style.nav}>
<Nav/>
</nav>
const NotFound = () => {
const { setIsNotFound } = useContext(IsNotFoundContext);

useEffect(() => {
setIsNotFound(true);

<main className={style.content}>
<Paragraph fontSize={1.5} styles={{ textAlign: "center" }}>
{"This route doesn't exist."}
</Paragraph>
</main>
return () => setIsNotFound(false);
}, [setIsNotFound]);

<footer className={style.footer}>
<Footer/>
</footer>
</>
);
return (
<>
<nav className={style.nav}>
<Nav/>
</nav>

<main className={style.content}>
<Paragraph fontSize={1.5} styles={{ textAlign: "center" }}>
{"This route doesn't exist."}
</Paragraph>
</main>

<footer className={style.footer}>
<Footer/>
</footer>
</>
);
};

export default NotFound;

1 change: 1 addition & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./trpc_provider";
export * from "./session_provider_wrapper";
export * from "./is_not_found_provider";
36 changes: 36 additions & 0 deletions src/providers/is_not_found_provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import {
createContext,
useState,
type Dispatch,
type SetStateAction,
type ReactNode,
type FC,
} from "react";

export type IsNotFoundContextProps = {
isNotFound: boolean;
setIsNotFound: Dispatch<SetStateAction<boolean>>;
};

export const IsNotFoundContext = createContext<IsNotFoundContextProps>({
isNotFound: false,
setIsNotFound: () => {
console.log("IsNotFoundContext needs to be initialized before usage!");
},
});

export type IsNotFoundProviderProps = {
children: ReactNode;
};

export const IsNotFoundProvider: FC<IsNotFoundProviderProps> = ({ children }) => {
const [isNotFound, setIsNotFound] = useState(false);

return (
<IsNotFoundContext.Provider value={{ isNotFound, setIsNotFound }}>
{children}
</IsNotFoundContext.Provider>
);
};
7 changes: 7 additions & 0 deletions src/server/api/routers/blog/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "@khenzii-dev/server/api/trpc";
import {
BlogPostService,
getPostInput,
getPostsInput,
createPostInput,
updatePostInput,
Expand All @@ -13,6 +14,12 @@ import {


export const blogPostRouter = createTRPCRouter({
getPost: publicProcedure
.input(getPostInput)
.query(async ({ ctx, input }) => {
const handler = new BlogPostService(ctx);
return await handler.getPost(input);
}),
getPosts: publicProcedure
.input(getPostsInput)
.query(async ({ ctx, input }) => {
Expand Down
21 changes: 21 additions & 0 deletions src/server/backend/blog/post/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { BaseService, Event } from "@khenzii-dev/server/backend";
import { z } from "zod";

export const getPostInput = z.object({
id: z.string(),
});

export const getPostsInput = z.object({
offset: z.number().optional(),
tags: z.object({
id: z.string(),
}).array().optional(),
}).optional();

export const createPostInput = z.object({
Expand Down Expand Up @@ -31,6 +38,7 @@ export const deletePostInput = z.object({
id: z.string(),
});

type getPostInputType = z.infer<typeof getPostInput>;
type getPostsInputType = z.infer<typeof getPostsInput>;
type createPostInputType = z.infer<typeof createPostInput>;
type updatePostInputType = z.infer<typeof updatePostInput>;
Expand All @@ -45,11 +53,24 @@ export type BlogPost = {
};

export class BlogPostService extends BaseService {
async getPost(input: getPostInputType): Promise<BlogPost | null> {
try {
return await this.ctx.db.post.findUnique({
where: { id: input.id },
});
} catch {
return null;
}
}

async getPosts(input: getPostsInputType): Promise<BlogPost[]> {
return await this.ctx.db.post.findMany({
orderBy: { created_at: "desc" },
skip: (input?.offset ?? 0) * 10,
take: 10,
where: input?.tags === undefined
? undefined
: { tagIDs: { hasSome: input.tags.map((tag) => tag.id) } },
});
}

Expand Down
Loading

0 comments on commit 5ada7a7

Please sign in to comment.