Эцсийн төсөл
Та Next.js курсын 39 хичээлийг дуусгалаа — App Router, Server Component, data fetching, Supabase, deploy, security, performance бүгдийг сурлаа. Одоо тэр мэдлэгийг нэг бүрэн апп болгох цаг болсон. Эцсийн төсөлд хувийн блог платформ хийнэ — нийтлэл бичих, уншигчид сэтгэгдэл үлдээх, зохиогч нэвтрэх боломжтой fullstack апп. Энэ бол жинхэнэ ажлын байранд шаарддаг чадварыг дадлагажуулах хамгийн сайн арга юм.
Төслийн тойм
Бүтээх аппын онцлог:
| Feature | Технологи | | ------------------ | ---------------------------------- | | Нийтлэл харах | Server Component, Markdown | | Нийтлэл хайх | URL searchParams, Server Component | | Сэтгэгдэл үлдээх | Server Action, Supabase | | Зохиогч нэвтрэх | Supabase Auth | | Шинэ нийтлэл нэмэх | Server Action, form | | Deploy | Vercel, custom domain |
Файлын бүтэц:
app/
├── page.tsx ← Нийтлэлийн жагсаалт
├── posts/
│ └── [slug]/
│ └── page.tsx ← Нэг нийтлэл + сэтгэгдэл
├── dashboard/
│ ├── page.tsx ← Зохиогчийн хяналтын самбар
│ └── new/
│ └── page.tsx ← Шинэ нийтлэл үүсгэх
├── (auth)/
│ ├── login/page.tsx
│ └── register/page.tsx
└── api/
└── comments/route.ts
Supabase schema
create table posts (
id uuid default gen_random_uuid() primary key,
author_id uuid references profiles(id) on delete cascade,
slug text unique not null,
title text not null,
content text not null,
published boolean default false,
created_at timestamptz default now()
);
create table comments (
id uuid default gen_random_uuid() primary key,
post_id uuid references posts(id) on delete cascade,
user_id uuid references profiles(id) on delete cascade,
body text not null,
created_at timestamptz default now()
);
alter table posts enable row level security;
alter table comments enable row level security;
-- Хэн ч нийтлэгдсэн нийтлэл унших боломжтой
create policy "Нийтлэгдсэн нийтлэл нийтэд харагдана"
on posts for select
using (published = true);
-- Зохиогч өөрийн бүх нийтлэл харна
create policy "Зохиогч өөрийн нийтлэл харна"
on posts for select
using (auth.uid() = author_id);
-- Зохиогч нийтлэл үүсгэнэ
create policy "Зохиогч нийтлэл нэмнэ"
on posts for insert
with check (auth.uid() = author_id);
-- Нэвтэрсэн хэрэглэгч сэтгэгдэл нэмнэ
create policy "Нэвтэрсэн хэрэглэгч сэтгэгдэл нэмнэ"
on comments for insert
with check (auth.uid() = user_id);
create policy "Сэтгэгдэл нийтэд харагдана"
on comments for select
using (true);
Нийтлэлийн жагсаалт хуудас
// app/page.tsx
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import Link from "next/link";
interface Post {
id: string;
slug: string;
title: string;
created_at: string;
profiles: { username: string };
}
export default async function HomePage() {
const cookieStore = await cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookieStore.getAll() } },
);
const { data: posts } = await supabase
.from("posts")
.select("id, slug, title, created_at, profiles(username)")
.eq("published", true)
.order("created_at", { ascending: false })
.returns<Post[]>();
return (
<main className="max-w-2xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-[#f1f5f9] mb-8">Нийтлэлүүд</h1>
<ul className="space-y-6">
{posts?.map((post) => (
<li key={post.id} className="border border-[#1e293b] rounded-xl p-6">
<Link
href={`/posts/${post.slug}`}
className="text-xl font-semibold text-[#f1f5f9] hover:text-[#818cf8]"
>
{post.title}
</Link>
<p className="text-[#94a3b8] text-sm mt-2">
{post.profiles.username} ·{" "}
{new Date(post.created_at).toLocaleDateString("mn-MN")}
</p>
</li>
))}
</ul>
</main>
);
}
Сэтгэгдэл нэмэх Server Action
// app/actions/comments.ts
"use server";
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";
interface AddCommentInput {
postId: string;
postSlug: string;
body: string;
}
interface ActionResult {
success: boolean;
error?: string;
}
export async function addComment(
input: AddCommentInput,
): Promise<ActionResult> {
const { postId, postSlug, body } = input;
if (!body.trim() || body.length > 1000) {
return { success: false, error: "Сэтгэгдэл хоосон эсвэл хэт урт байна" };
}
const cookieStore = await cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookieStore.getAll() } },
);
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) return { success: false, error: "Нэвтрэх шаардлагатай" };
const { error } = await supabase.from("comments").insert({
post_id: postId,
user_id: session.user.id,
body: body.trim(),
});
if (error) return { success: false, error: "Сэтгэгдэл нэмэхэд алдаа гарлаа" };
revalidatePath(`/posts/${postSlug}`);
return { success: true };
}
Эцсийн шатлал ба deploy
Төслөө дуусгахдаа дараах дарааллыг дагана:
# 1. Орон нутагт бүрэн ажиллахыг шалгана
npm run dev
# 2. TypeScript алдаа шалгана
npx tsc --noEmit
# 3. Build амжилттай болохыг шалгана
npm run build
# 4. Git-д commit хийнэ
git add .
git commit -m "feat: final project complete"
git push origin main
# 5. Vercel автоматаар deploy хийнэ
# Dashboard-д build log-г хянана
Амжилттай deploy болсны дараа:
- Custom domain тохируулна
- Environment variable production орчинд зөв байгааг шалгана
- Analytics болон Speed Insights идэвхжүүлнэ
- Нэг найздаа холбоосыг явуулж туршина
Дараагийн хичээлд:
Та Next.js курсыг бүрэн дуусгалаа! App Router, Server Component, Supabase, deploy, security, performance — бүгдийг эзэмшлээ. Дараагийн алхам: React Native курсаар гар утасны апп хөгжүүлэх эсвэл TypeScript курсаар type системийг гүнзгийрүүлэн судлах. Кодлосоор байгаарай — туршлага хуримтлагдах тусам бүх зүйл илүү хялбар болдог.