├── .env.sample ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── (home) │ ├── (default) │ │ ├── loading.tsx │ │ └── page.tsx │ ├── blog │ │ └── [id] │ │ │ ├── components │ │ │ ├── Content.tsx │ │ │ └── Skeleton.tsx │ │ │ └── page.tsx │ ├── error.tsx │ └── layout.tsx ├── api │ ├── blog │ │ └── route.ts │ └── stripe │ │ └── webhook │ │ └── route.ts ├── auth │ ├── callback │ │ └── route.ts │ └── error │ │ └── page.tsx ├── dashboard │ ├── blog │ │ ├── components │ │ │ ├── BlogForm.tsx │ │ │ ├── BlogNav.tsx │ │ │ ├── BlogTable.tsx │ │ │ ├── DeleteAlert.tsx │ │ │ └── SwitchForm.tsx │ │ ├── create │ │ │ └── page.tsx │ │ ├── edit │ │ │ └── [id] │ │ │ │ ├── components │ │ │ │ └── EditForm.tsx │ │ │ │ └── page.tsx │ │ └── schema │ │ │ └── index.ts │ ├── components │ │ └── NavLinks.tsx │ ├── layout.tsx │ ├── loading.tsx │ ├── page.tsx │ └── user │ │ └── page.tsx ├── favicon.ico ├── globals.css └── layout.tsx ├── components.json ├── components ├── Footer.tsx ├── SessisonProvider.tsx ├── markdown │ ├── CopyButton.tsx │ └── MarkdownPreview.tsx ├── nav │ ├── HoverUnderLine.tsx │ ├── LoginForm.tsx │ ├── Navbar.tsx │ └── Profile.tsx ├── stripe │ ├── Checkout.tsx │ └── ManageBill.tsx ├── theme-provider.tsx └── ui │ ├── alert-dialog.tsx │ ├── button.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── switch.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ └── use-toast.ts ├── lib ├── actions │ ├── blog.ts │ ├── stripe.ts │ └── user.ts ├── data.ts ├── icon │ └── index.ts ├── store │ └── user.ts ├── supabase │ └── index.ts ├── types │ ├── index.ts │ └── supabase.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg ├── og-dashboard.png ├── og.png ├── profile.png └── vercel.svg ├── tailwind.config.js └── tsconfig.json /.env.sample: -------------------------------------------------------------------------------- 1 | # supabase 2 | NEXT_PUBLIC_SUPABASE_URL= 3 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 4 | SERVICE_ROLE= 5 | 6 | 7 | # stripe 8 | NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_ 9 | STRIPE_SK_KEY=sk_test_ 10 | STRIPE_ENDPOINT_SECRET=whsec 11 | PRO_PRCIE_ID=price_ 12 | 13 | 14 | 15 | SITE_URL="http://localhost:3000" 16 | 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Full stack blog sass 👋

2 | Screenshot 2023-11-19 at 3 44 24 PM 3 | 4 | ## Support 5 | 6 | please give like to my video and subscript to my channel 🙏 7 | link here 👉 https://youtube.com/@DailyWebCoding?si=7vd8IWx53e6azYPB 8 | 9 | ## Getting Started 10 | 11 | First, run the development server: 12 | 13 | ```bash 14 | npm i 15 | ``` 16 | 17 | ```bash 18 | npm run dev 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 22 | -------------------------------------------------------------------------------- /app/(home)/(default)/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function loading() { 4 | return ( 5 |
6 | {[1, 2, 3, 4, 5]?.map((_, index) => { 7 | return ( 8 |
12 |
13 |
14 |

15 |

16 |
17 |
18 | ); 19 | })} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/(home)/(default)/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import Image from "next/image"; 4 | import { readBlog } from "@/lib/actions/blog"; 5 | 6 | export default async function Home() { 7 | let { data: blogs } = await readBlog(); 8 | 9 | if (!blogs?.length) { 10 | blogs = []; 11 | } 12 | 13 | return ( 14 |
15 | {blogs.map((blog, index) => { 16 | return ( 17 | 22 |
23 | cover 31 |
32 |
33 |

34 | {new Date(blog.created_at).toDateString()} 35 |

36 | 37 |

38 | {blog.title} 39 |

40 |
41 | 42 | ); 43 | })} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/(home)/blog/[id]/components/Content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import MarkdownPreview from "@/components/markdown/MarkdownPreview"; 3 | import { Database } from "@/lib/types/supabase"; 4 | import { createBrowserClient } from "@supabase/ssr"; 5 | import React, { useEffect, useState, useTransition } from "react"; 6 | import { BlogContentLoading } from "./Skeleton"; 7 | import Checkout from "@/components/stripe/Checkout"; 8 | 9 | export default function Content({ blogId }: { blogId: string }) { 10 | const [loading, setLoading] = useState(true); 11 | 12 | const [blog, setBlog] = useState<{ 13 | blog_id: string; 14 | content: string; 15 | created_at: string; 16 | } | null>(); 17 | 18 | const supabase = createBrowserClient( 19 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 20 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 21 | ); 22 | 23 | const readBlogContent = async () => { 24 | const { data } = await supabase 25 | .from("blog_content") 26 | .select("*") 27 | .eq("blog_id", blogId) 28 | .single(); 29 | setBlog(data); 30 | setLoading(false); 31 | }; 32 | 33 | useEffect(() => { 34 | readBlogContent(); 35 | 36 | // eslint-disable-next-line 37 | }, []); 38 | 39 | if (loading) { 40 | return ; 41 | } 42 | 43 | if (!blog?.content) { 44 | return ; 45 | } 46 | 47 | return ; 48 | } 49 | -------------------------------------------------------------------------------- /app/(home)/blog/[id]/components/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function BlogContentLoading() { 4 | return ( 5 |
6 |

7 |

8 |

9 |

10 |

11 |

12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/(home)/blog/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IBlog } from "@/lib/types"; 3 | import Image from "next/image"; 4 | import Content from "./components/Content"; 5 | 6 | export async function generateStaticParams() { 7 | const { data: blogs } = await fetch( 8 | process.env.SITE_URL + "/api/blog?id=*" 9 | ).then((res) => res.json()); 10 | 11 | return blogs; 12 | } 13 | 14 | export async function generateMetadata({ params }: { params: { id: string } }) { 15 | const { data: blog } = (await fetch( 16 | process.env.SITE_URL + "/api/blog?id=" + params.id 17 | ).then((res) => res.json())) as { data: IBlog }; 18 | 19 | return { 20 | title: blog?.title, 21 | authors: { 22 | name: "chensokheng", 23 | }, 24 | openGraph: { 25 | title: blog?.title, 26 | url: "https://dailyblog-demo.vercel.app/blog" + params.id, 27 | siteName: "Daily Blog", 28 | images: blog?.image_url, 29 | type: "website", 30 | }, 31 | keywords: ["daily web coding", "chensokheng", "dailywebcoding"], 32 | }; 33 | } 34 | 35 | export default async function page({ params }: { params: { id: string } }) { 36 | const { data: blog } = (await fetch( 37 | process.env.SITE_URL + "/api/blog?id=" + params.id 38 | ).then((res) => res.json())) as { data: IBlog }; 39 | 40 | if (!blog?.id) { 41 | return

Not found

; 42 | } 43 | 44 | return ( 45 |
46 |
47 |

48 | {blog?.title} 49 |

50 |

51 | {new Date(blog?.created_at!).toDateString()} 52 |

53 |
54 | 55 |
56 | cover 64 |
65 | 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /app/(home)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error components must be Client Components 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string }; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error); 15 | }, [error]); 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import Footer from "../../components/Footer"; 3 | 4 | export default function Layout({ children }: { children: ReactNode }) { 5 | return ( 6 | <> 7 | {children} 8 |