Skip to content

Commit 7b4c305

Browse files
lwinmoepaingclaude
andcommitted
✨ feat(blog): add Firebase Auth + Firestore blog system
Add Google authentication, Firestore blog CRUD with Lexical editor, paginated blog listing, daily publish limits, and i18n support (en/mm). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1e35134 commit 7b4c305

31 files changed

Lines changed: 2206 additions & 9 deletions

.github/workflows/build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ jobs:
5151
run: bun install
5252
- name: Build with Next.js
5353
run: bun run build
54+
env:
55+
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
56+
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
57+
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
58+
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
59+
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
60+
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
61+
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
5462
- name: Upload artifact
5563
uses: actions/upload-pages-artifact@v3
5664
with:

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ screen-shot
5353
# playwright mcp
5454
.playwright-mcp/
5555

56-
/docs
56+
/docs
57+
/plans

bun.lockb

40.3 KB
Binary file not shown.

messages/en.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,27 @@
185185
"articles": "Articles",
186186
"readArticle": "Read article",
187187
"noPostsTitle": "No posts yet",
188-
"noPostsBody": "Exciting articles are on the horizon. Our community of engineers is crafting stories worth reading."
188+
"noPostsBody": "Exciting articles are on the horizon. Our community of engineers is crafting stories worth reading.",
189+
"write": "Write",
190+
"writeBlog": "Write Blog",
191+
"newBlog": "New Blog",
192+
"myBlogs": "My Blogs",
193+
"saveDraft": "Save Draft",
194+
"publish": "Publish",
195+
"unpublish": "Unpublish",
196+
"editBlog": "Edit Blog",
197+
"deleteBlog": "Delete Blog",
198+
"communityPost": "Community"
199+
},
200+
"auth": {
201+
"signIn": "Sign In",
202+
"signOut": "Sign Out",
203+
"signInRequired": "Sign in required",
204+
"signInDesc": "Sign in with your Google account to access this feature.",
205+
"signInWithGoogle": "Sign in with Google",
206+
"accessDenied": "Access denied",
207+
"accessDeniedDesc": "You don't have permission to view this page.",
208+
"backToBlog": "Back to Blog"
189209
},
190210
"profileHero": {
191211
"label": "Developer Directory",

messages/mm.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,27 @@
185185
"articles": "ဆောင်းပါးများ",
186186
"readArticle": "ဆောင်းပါး ဖတ်ရန်",
187187
"noPostsTitle": "ဆောင်းပါး မရှိသေးပါ",
188-
"noPostsBody": "စိတ်လှုပ်ရှားဖွယ် ဆောင်းပါးများ မကြာမီ ရောက်လာပါမည်။ ကျွန်ုပ်တို့၏ အင်ဂျင်နီယာ အသိုင်းအဝိုင်းသည် ဖတ်ရှုရကျိုးနပ်သော ပုံပြင်များ ရေးသားနေပါသည်။"
188+
"noPostsBody": "စိတ်လှုပ်ရှားဖွယ် ဆောင်းပါးများ မကြာမီ ရောက်လာပါမည်။ ကျွန်ုပ်တို့၏ အင်ဂျင်နီယာ အသိုင်းအဝိုင်းသည် ဖတ်ရှုရကျိုးနပ်သော ပုံပြင်များ ရေးသားနေပါသည်။",
189+
"write": "ရေးရန်",
190+
"writeBlog": "ဘလော့ဂ် ရေးရန်",
191+
"newBlog": "ဘလော့ဂ် အသစ်",
192+
"myBlogs": "ကျွန်ုပ်၏ ဘလော့ဂ်များ",
193+
"saveDraft": "မူကြမ်း သိမ်းရန်",
194+
"publish": "ထုတ်ဝေရန်",
195+
"unpublish": "ပြန်ရုပ်ရန်",
196+
"editBlog": "ဘလော့ဂ် ပြင်ရန်",
197+
"deleteBlog": "ဘလော့ဂ် ဖျက်ရန်",
198+
"communityPost": "အသိုင်းအဝိုင်း"
199+
},
200+
"auth": {
201+
"signIn": "ဝင်ရောက်ရန်",
202+
"signOut": "ထွက်ရန်",
203+
"signInRequired": "ဝင်ရောက်ရန် လိုအပ်ပါသည်",
204+
"signInDesc": "ဤအင်္ဂါရပ်ကို အသုံးပြုရန် သင့် Google အကောင့်ဖြင့် ဝင်ရောက်ပါ။",
205+
"signInWithGoogle": "Google ဖြင့် ဝင်ရောက်ရန်",
206+
"accessDenied": "ဝင်ခွင့် မရှိပါ",
207+
"accessDeniedDesc": "ဤစာမျက်နှာကို ကြည့်ရှုခွင့် မရှိပါ။",
208+
"backToBlog": "ဘလော့ဂ် သို့ ပြန်သွားရန်"
189209
},
190210
"profileHero": {
191211
"label": "Developer လမ်းညွှန်",

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
},
2929
"dependencies": {
3030
"@formkit/auto-animate": "^0.8.0",
31+
"@lexical/clipboard": "^0.41.0",
32+
"@lexical/code": "^0.41.0",
33+
"@lexical/html": "^0.41.0",
34+
"@lexical/link": "^0.41.0",
35+
"@lexical/list": "^0.41.0",
36+
"@lexical/markdown": "^0.41.0",
37+
"@lexical/react": "^0.41.0",
38+
"@lexical/rich-text": "^0.41.0",
39+
"@lexical/selection": "^0.41.0",
40+
"@lexical/utils": "^0.41.0",
3141
"@react-three/drei": "^10.7.7",
3242
"@react-three/fiber": "^9.5.0",
3343
"@react-three/postprocessing": "^3.0.4",
@@ -41,8 +51,10 @@
4151
"cz-customizable": "^7.0.0",
4252
"date-fns": "^2.30.0",
4353
"fast-shuffle": "^6.0.1",
54+
"firebase": "^12.10.0",
4455
"framer-motion": "^12.36.0",
4556
"husky": "^8.0.3",
57+
"lexical": "^0.41.0",
4658
"lint-staged": "^15.0.2",
4759
"lucide-react": "^0.577.0",
4860
"next": "16.1.6",

src/app/blog/BlogPageClient.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ import { useTranslations } from "next-intl";
2222
import { useLanguage } from "@/hooks/useLanguage";
2323
import { khitHaungg } from "@/fonts/fonts";
2424

25+
import { useBlogList } from "@/hooks/blog/useBlogList";
26+
2527
/* ── Types ── */
2628
type BlogItem = {
2729
_id: string;
2830
title: string;
2931
description?: string;
3032
date: string;
3133
slug: string;
34+
source?: "static" | "firestore";
3235
};
3336

3437
/* ── Floating ambient orbs ── */
@@ -421,7 +424,7 @@ const EmptyState = ({ isInView, mmFont, t }: { isInView: boolean; mmFont: string
421424
);
422425

423426
/* ── Main Blog Page Client ── */
424-
const BlogPageClient = ({ blogs }: { blogs: BlogItem[] }) => {
427+
const BlogPageClient = ({ blogs: staticBlogs }: { blogs: BlogItem[] }) => {
425428
const heroRef = useRef(null);
426429
const gridRef = useRef(null);
427430
const heroInView = useInView(heroRef, { amount: 0.3, once: true });
@@ -430,6 +433,19 @@ const BlogPageClient = ({ blogs }: { blogs: BlogItem[] }) => {
430433
const { isMyanmar } = useLanguage();
431434
const mmFont = isMyanmar ? khitHaungg.className : "";
432435

436+
// Merge static + Firestore blogs with pagination
437+
const { firestoreBlogs, loading: firestoreLoading, hasMore, loadMore, loadingMore } = useBlogList(10);
438+
const firestoreItems: BlogItem[] = firestoreBlogs.map((p) => ({
439+
_id: p.id,
440+
title: p.title,
441+
description: p.description,
442+
date: (p.publishedAt ?? p.createdAt).toISOString(),
443+
slug: `/blog/post?slug=${p.slug}`,
444+
source: "firestore" as const,
445+
}));
446+
const blogs = [...staticBlogs.map((b) => ({ ...b, source: "static" as const })), ...firestoreItems]
447+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
448+
433449
const hasPosts = blogs.length > 0;
434450
const [featured, ...rest] = blogs;
435451

@@ -595,7 +611,20 @@ const BlogPageClient = ({ blogs }: { blogs: BlogItem[] }) => {
595611
{/* Blog grid */}
596612
<div ref={gridRef} className="relative z-10 pb-16">
597613
<Container withPadding>
598-
{!hasPosts ? (
614+
{firestoreLoading && staticBlogs.length === 0 ? (
615+
<motion.div
616+
className="flex items-center justify-center py-20"
617+
initial={{ opacity: 0 }}
618+
animate={{ opacity: 1 }}
619+
>
620+
<div className="flex flex-col items-center gap-3">
621+
<div className="w-6 h-6 border-2 border-prism-violet/30 border-t-prism-violet rounded-full animate-spin" />
622+
<span className={cn("text-[11px] font-mono text-zinc-600", mmFont)}>
623+
Loading blogs...
624+
</span>
625+
</div>
626+
</motion.div>
627+
) : !hasPosts ? (
599628
<EmptyState isInView={gridInView} mmFont={mmFont} t={t} />
600629
) : (
601630
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
@@ -618,6 +647,38 @@ const BlogPageClient = ({ blogs }: { blogs: BlogItem[] }) => {
618647
))}
619648
</div>
620649
)}
650+
651+
{/* Load More */}
652+
{hasMore && (
653+
<motion.div
654+
className="flex justify-center mt-10"
655+
initial={{ opacity: 0 }}
656+
animate={gridInView ? { opacity: 1 } : { opacity: 0 }}
657+
transition={{ delay: 0.5 }}
658+
>
659+
<button
660+
type="button"
661+
onClick={loadMore}
662+
disabled={loadingMore}
663+
className={cn(
664+
"inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-medium",
665+
"bg-white/[0.04] border border-white/[0.08]",
666+
"text-zinc-400 hover:text-white hover:bg-white/[0.08] hover:border-white/[0.12]",
667+
"transition-all duration-300",
668+
"disabled:opacity-40 disabled:cursor-not-allowed"
669+
)}
670+
>
671+
{loadingMore ? (
672+
<>
673+
<div className="w-4 h-4 border-2 border-zinc-600 border-t-zinc-300 rounded-full animate-spin" />
674+
Loading...
675+
</>
676+
) : (
677+
"Load more posts"
678+
)}
679+
</button>
680+
</motion.div>
681+
)}
621682
</Container>
622683
</div>
623684
</div>

0 commit comments

Comments
 (0)