Skip to content

Commit

Permalink
feat: Added single blog post page
Browse files Browse the repository at this point in the history
  • Loading branch information
Andry925 committed May 29, 2024
1 parent 6917094 commit c0aaed3
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 1,287 deletions.
1,274 changes: 0 additions & 1,274 deletions 6-create-blog-posts-page.patch

This file was deleted.

23 changes: 23 additions & 0 deletions src/app/posts/[postId]/_components/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "next/link";

interface MenuItemProps {
offset: number;
title: string;
href: string;
highlighted?: boolean;
}

export function MenuItem({ title, href, highlighted, offset }: MenuItemProps) {
console.log("highlighted", highlighted);

return (
<Link
href={href}
scroll={true}
className={`text-${highlighted ? "violet-600" : "black"} text-lg`}
style={{ paddingLeft: `${offset * 8}px` }}
>
{title}
</Link>
);
}
107 changes: 107 additions & 0 deletions src/app/posts/[postId]/_components/PreviewMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";
import { kebabCase } from "@/lib/textIntoKebabNotation";
import { useState, useEffect } from "react";
import { MenuItem } from "./MenuItem";

type TMenuItem = {
name: string;
id: string;
// 0 || 1 || 2
offset: number;
startY: number;
endY: number;
};

interface PostSearchProps {
mdContent: string;
}

export const PreviewMenu = ({ mdContent }: PostSearchProps) => {
const menuItems = useCreateMenuItemsFromMdContent(mdContent);
const scrollY = useGetCurrentScrollY();

if (menuItems.length === 0) return null;

return (
<aside className="sticky top-28 flex h-min w-96 flex-col gap-2 bg-gray-50 px-4 py-3 shadow-md">
{menuItems?.map(({ id, startY, endY, offset, name }) => (
<MenuItem
offset={offset}
key={id}
title={name}
href={`#${id}`}
highlighted={isHighlighted(startY, endY, scrollY)}
/>
))}
</aside>
);
};

function useCreateMenuItemsFromMdContent(mdContent: string): TMenuItem[] {
const [menuItems, setMenuItems] = useState<TMenuItem[]>([]);

const regex = /^(#{1,3}) (.*)$/gm;

let match;

useEffect(() => {
const _menuItems: TMenuItem[] = [];

while ((match = regex.exec(mdContent)) !== null) {
const offset = match[1]!.length - 1;
const name = match[2]!;
const id = kebabCase(name);

const headerItem = document.querySelector(`#${id}`)!;

const clientRect = headerItem?.getBoundingClientRect();
_menuItems.push({
startY: clientRect.top,
endY: 0,
offset,
name,
id: id!,
});
}

if (_menuItems.length === 0) return;

// @ts-expect-error: types suck
_menuItems[0].startY = 0;

// @ts-expect-error: types suck
_menuItems[_menuItems.length - 1].endY = Infinity;

for (let i = 0; i < _menuItems.length - 1; i++) {
// @ts-expect-error: types suck
_menuItems[i].endY = _menuItems[i + 1].startY;
}

setMenuItems(_menuItems);
}, [mdContent]);

return menuItems;
}

function useGetCurrentScrollY() {
const scrollOffset = 0;
const [currentPosition, setCurrentPosition] = useState<number>(0);

useEffect(() => {
const handleScroll = () => {
setCurrentPosition(window.scrollY + scrollOffset);
};

window.addEventListener("scroll", handleScroll);

return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);

return currentPosition;
}

function isHighlighted(startY: number, endY: number, scrollY: number) {
return scrollY > startY && scrollY < endY;
}
29 changes: 29 additions & 0 deletions src/app/posts/[postId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { api } from "@/trpc/server";
import { notFound } from "next/navigation";
import { MdPreview } from "@/components/mdPreview";
import { PreviewMenu } from "@/app/posts/[postId]/_components/PreviewMenu";

export default async function Page(props: { params: { postId: string } }) {
const postId = props.params.postId;

const { post } = await api.post.getPostById.query({ postId: Number(postId) });

if (!post) return notFound();

return (
<main className="flex flex-col items-center py-20">
<div className="max-lg:w-[95%] lg:w-[1200px]">
<section className="relative flex w-full gap-10">
<MdPreview
className="w-[70%]"
content={post.content!}
title={post.title!}
/>
<PreviewMenu mdContent={post.content!} />
</section>

<section>Comments gonna be here</section>
</div>
</main>
);
}
5 changes: 4 additions & 1 deletion src/app/posts/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export default async function Page({
<CreatePostForm defaultValues={defaultFormValues} />
</TabsContent>
<TabsContent value="preview">
<MdPreview {...defaultFormValues} />
<MdPreview
content={defaultFormValues.content}
title={defaultFormValues.title}
/>
</TabsContent>
</Tabs>
</main>
Expand Down
11 changes: 2 additions & 9 deletions src/components/mdPreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,11 @@ import { MDX } from "./components/MDX";
import { type TPostSchema } from "@/schemas/post";
import { cn } from "@/lib/utils";

interface MdPreviewProps extends TPostSchema {
includeComments?: boolean;
interface MdPreviewProps extends Pick<TPostSchema, "content" | "title"> {
className?: string;
}

function MdPreview({
tags,
content,
title,
className,
includeComments = false,
}: MdPreviewProps) {
function MdPreview({ content, title, className }: MdPreviewProps) {
return (
<article className={cn("flex flex-col gap-[32px]", className)}>
<h1 className="text-3xl font-bold">{title}</h1>
Expand Down
13 changes: 13 additions & 0 deletions src/server/api/routers/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,17 @@ export const postRouter = createTRPCRouter({
const result = await Promise.all(insertStatements);
return postId;
}),

getPostById: publicProcedure
.input(
z.object({
postId: z.number(),
}),
)
.query(async ({ input }) => {
const post = await PostService.getPostById(input.postId);
return {
post,
};
}),
});
18 changes: 15 additions & 3 deletions src/server/api/services/Post.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { db } from "../../db";
import { ilike, or, and, inArray, gt } from "drizzle-orm";
import { ilike, or, and, inArray, gt, eq } from "drizzle-orm";
import { blogPosts, tagsToBlogPosts } from "../../db/schema";

class PostService {
Expand Down Expand Up @@ -46,8 +46,20 @@ class PostService {
};
}

private countHowManyMatches(a: number[], b: number[]) {
return a.filter((v) => b.includes(v)).length;
public async getPostById(postId: number) {
const post = await db.query.blogPosts.findFirst({
where: eq(blogPosts.id, postId),
with: {
tagsToBlogPosts: {
with: {
blogPostTags: true,
},
},
// comments: true,
},
});

return post;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/server/api/services/Telegram.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class TelegramService {
constructor() {
this.bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
}

public async sendMessage(contactInfo: TContactSchema) {
await this.bot.telegram.sendMessage(
process.env.MY_TELEGRAM_CHAT_ID!,
Expand Down
2 changes: 2 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

@layer base {
:root {
scroll-behavior: smooth;

--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;

Expand Down

0 comments on commit c0aaed3

Please sign in to comment.