Skip to content

Commit

Permalink
Refactor: codes (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
moonlitgrace authored Aug 28, 2024
2 parents a4251ab + d291ddb commit 91f7fc0
Show file tree
Hide file tree
Showing 46 changed files with 2,662 additions and 71 deletions.
3 changes: 0 additions & 3 deletions .eslintrc.json

This file was deleted.

1 change: 1 addition & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx --no-install commitlint --edit "$1"
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
4 changes: 4 additions & 0 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"**/*.{js,jsx,ts,tsx}": ["eslint --max-warnings=0", "prettier --write"],
"**/*.{html,json,css,scss,md,mdx}": ["prettier -w"]
}
38 changes: 38 additions & 0 deletions actions/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AdminBlogFormState, AdminBlogSchema } from '@/zod_schemas/admin';

export default async function adminBlogSubmit(state: AdminBlogFormState, formData: FormData) {
const validatedFields = AdminBlogSchema.safeParse({
id: formData.get('id') ? Number(formData.get('id')) : undefined,
title: formData.get('title'),
tag: formData.get('tag'),
content: formData.get('content'),
});

if (!validatedFields.success) {
console.log(validatedFields.error);
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}

try {
const res = await fetch('/api/blog', {
method: 'PATCH',
body: JSON.stringify(validatedFields.data),
headers: {
'Content-Type': 'application/json',
},
});

if (!res.ok) {
const err = await res.json();
return { message: err };
}

const data = await res.json();
return { message: data };
} catch (err) {
console.error(err);
return { message: 'API call failed' };
}
}
20 changes: 20 additions & 0 deletions app/(admin)/admin/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import AdminBlogForm from '@/components/admin/forms/BlogForm';
import { db } from '@/db';
import { posts, PostSelect } from '@/db/schema';
import { eq } from 'drizzle-orm';

export default async function Page({ params }: { params: { slug: string } }) {
const postData: PostSelect = (
await db.select().from(posts).where(eq(posts.slug, params.slug))
)[0];

return (
<AdminBlogForm
id={postData.id}
title={postData.title}
tag={postData.tag}
content={postData.content}
cover={postData.cover}
/>
);
}
5 changes: 5 additions & 0 deletions app/(admin)/admin/blog/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import AdminBlogForm from "@/components/admin/forms/BlogForm";

export default function AdminNewBlogPage() {
return <AdminBlogForm />;
}
72 changes: 72 additions & 0 deletions app/(admin)/admin/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { db } from '@/db';
import { posts, PostSelect } from '@/db/schema';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
import AdminBlogDeleteButton from '@/app/_components/_admin/BlogDeleteButton';

export default async function AdminBlogPage() {
const postsData: Omit<PostSelect, 'content' | 'cover' | 'createdAt'>[] = await db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
tag: posts.tag,
})
.from(posts);

return (
<>
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold">
Admin Blog
<span className="text-primary">.</span>
</h2>
<Button asChild>
<Link href="/admin/blog/new">New Post</Link>
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Id</TableHead>
<TableHead>Title</TableHead>
<TableHead>Tag</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{postsData.map((post) => (
<TableRow>
<TableCell>{post.id}</TableCell>
<TableCell className="font-medium">
<Link href={`/blog/${post.slug}`} className="underline">
{post.title}
</Link>
</TableCell>
<TableCell>
<Badge className="capitalize" variant="secondary">
{post.tag}
</Badge>
</TableCell>
<TableCell className="flex gap-4">
<Button size="sm">
<Link href={`/admin/blog/${post.slug}`}>Edit</Link>
</Button>
<AdminBlogDeleteButton postId={post.id} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
}
5 changes: 5 additions & 0 deletions app/(admin)/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export default async function AdminPage() {
redirect('/admin/blog');
}
14 changes: 14 additions & 0 deletions app/(admin)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import AdminBar from "@/components/admin/AdminBar";

export default function ProtectedLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<main className="mb-40 mt-10 flex w-full flex-col gap-10 md:mb-10">
<AdminBar />
{children}
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Markdown from '@/components/markdown';
import TableOfContents from '@/components/toc';
import { db } from '@/db';
import { posts, PostSelect } from '@/db/schema';
import { formatDate } from '@/lib/utils';
Expand All @@ -9,6 +7,8 @@ import { marked, Tokens } from 'marked';
import { Metadata } from 'next';
import Image from 'next/image';
import { Badge } from '@/components/ui/badge';
import Markdown from '@/components/shared/Markdown';
import TableOfContents from '@/components/main/TableOfContents';

export async function generateMetadata({
params,
Expand Down
2 changes: 1 addition & 1 deletion app/writings/layout.tsx → app/(main)/blog/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';

export default function WritingsLayout({ children }: Readonly<{ children: React.ReactNode }>) {
export default function BlogLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return <main className="mb-40 mt-10 flex w-full flex-col gap-10 md:mb-10">{children}</main>;
}
16 changes: 9 additions & 7 deletions app/writings/page.tsx → app/(main)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { db } from '@/db';
import { type PostSelect, posts } from '@/db/schema';
import { posts, PostSelect } from '@/db/schema';
import { formatDate } from '@/lib/utils';
import { Metadata } from 'next';
import Link from 'next/link';
import { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Writings. | moonlitspace',
description: 'my graceful thoughts',
title: 'Blog | Moonlitgrace',
description:
'Dive into the Blog at Moonlitgrace, where a passionate web developer and open-source contributor shares thoughts, tutorials, and insights—all under the alias Moonlitgrace.',
};

export default async function WritingsPage() {
export default async function BlogPage() {
const postsData: Omit<PostSelect, 'content' | 'cover'>[] = await db
.select({
id: posts.id,
Expand All @@ -25,7 +26,8 @@ export default async function WritingsPage() {
return (
<>
<h2 className="text-3xl font-bold">
Writings<span className="text-primary">.</span>
Blog
<span className="text-primary">.</span>
</h2>
<div className="flex flex-col gap-5">
{postsData.map((item) => (
Expand All @@ -34,7 +36,7 @@ export default async function WritingsPage() {
{formatDate(item.createdAt)}
</span>
<div className="flex items-center gap-4">
<Link href={`/writings/${item.slug}`} className="relative text-lg underline">
<Link href={`/blog/${item.slug}`} className="relative text-lg underline">
{item.title}
</Link>
<Separator className="hidden flex-1 md:flex" />
Expand Down
14 changes: 14 additions & 0 deletions app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import AppBar from "@/components/main/AppBar";

export default function MainLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
{children}
<AppBar />
</>
);
}
16 changes: 16 additions & 0 deletions app/(main)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import MoonlitGraceArt from '@/components/main/MoonlitGraceArt';
import { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Moonlitgrace',
description:
'Step into Moonlitgrace, where a passionate web developer and open-source contributor shares insights, projects, and creativity—all under the alias Moonlitgrace.',
};

export default function Home() {
return (
<main className="grid w-full place-items-center">
<MoonlitGraceArt />
</main>
);
}
34 changes: 34 additions & 0 deletions app/_components/_admin/BlogDeleteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { Button } from '@/components/ui/button';
import { useState } from 'react';

export default function AdminBlogDeleteButton({ postId }: { postId: number }) {
const [pending, setPending] = useState(false);

async function handleDeleteBlog() {
setPending(true);
try {
const result = confirm(`Sure to delete blog: ${postId}?`);
if (result) {
await fetch('/api/blog/', {
method: 'DELETE',
body: JSON.stringify({ postId }),
headers: {
'Content-Type': 'application/json',
},
});
}
} catch (err) {
console.error(err);
} finally {
setPending(false);
}
}

return (
<Button disabled={pending} variant="destructive" size="sm" onClick={handleDeleteBlog}>
Delete
</Button>
);
}
19 changes: 19 additions & 0 deletions app/api/admin/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { db } from '@/db';
import { posts } from '@/db/schema';
import { count } from 'drizzle-orm';
import { NextResponse } from 'next/server';

export async function GET() {
try {
const postsCount = (await db.select({ count: count() }).from(posts))[0];

const obj = {
posts: postsCount,
};

return NextResponse.json(obj);
} catch (err) {
console.log(err);
return NextResponse.json({ message: 'Data counting failed!' }, { status: 500 });
}
}
53 changes: 53 additions & 0 deletions app/api/blog/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { db } from '@/db';
import { posts } from '@/db/schema';
import { AdminBlogData } from '@/zod_schemas/admin';
import { eq } from 'drizzle-orm';
import { NextRequest, NextResponse } from 'next/server';
import slugify from 'slugify';

export async function PATCH(request: NextRequest) {
const data: AdminBlogData = await request.json();
const isToUpdate = data.id !== undefined;

try {
if (isToUpdate) {
await db
.update(posts)
.set({
title: data.title,
tag: data.tag,
content: data.content,
slug: slugify(data.title.toLowerCase()),
...(data.cover && { cover: data.cover }),
})
.where(eq(posts.id, data.id as number));

return NextResponse.json({ message: 'Success' });
} else {
await db.insert(posts).values({
title: data.title,
tag: data.tag,
content: data.content,
slug: slugify(data.title.toLowerCase()),
...(data.cover && { cover: data.cover }),
});

return NextResponse.json({ message: 'Success' });
}
} catch (err) {
console.error(err);
return NextResponse.json({ message: 'Data update failed' }, { status: 500 });
}
}

export async function DELETE(request: NextRequest) {
const data: { postId: string } = await request.json();

try {
await db.delete(posts).where(eq(posts.id, Number(data.postId)));
return NextResponse.json({ message: 'Deleted successfully!' });
} catch (err) {
console.error(err);
return NextResponse.json({ message: 'Deletion failed!' }, { status: 500 });
}
}
2 changes: 0 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { cn } from '@/lib/utils';

// Styles
import '@/styles/globals.css';
import Appbar from '@/components/appbar';

const fontSans = FontSans({
subsets: ['latin'],
Expand All @@ -21,7 +20,6 @@ export default function RootLayout({
className={cn('dark container flex min-h-screen font-sans antialiased', fontSans.variable)}
>
{children}
<Appbar />
</body>
</html>
);
Expand Down
Loading

0 comments on commit 91f7fc0

Please sign in to comment.