├── .prettierignore ├── .eslintrc.json ├── vercel.json ├── .vscode └── settings.json ├── components ├── protected │ ├── main │ │ ├── index.ts │ │ ├── sidebars │ │ │ └── index.ts │ │ ├── header │ │ │ ├── index.ts │ │ │ ├── protected-mobile-menu-button.tsx │ │ │ └── protected-top-bar.tsx │ │ └── protected-main.tsx │ ├── editor │ │ ├── wysiwyg │ │ │ ├── extensions │ │ │ │ ├── updated-image.tsx │ │ │ │ ├── custom-keymap.ts │ │ │ │ └── image-resizer.tsx │ │ │ ├── default-content.tsx │ │ │ ├── props.ts │ │ │ └── bubble-menu │ │ │ │ └── image-selector.tsx │ │ └── upload │ │ │ ├── gallery-image │ │ │ ├── editor-upload-gallery-image-table-empty.tsx │ │ │ ├── editor-upload-gallery-image-placeholder.tsx │ │ │ └── editor-upload-gallery-image-table.tsx │ │ │ ├── index.ts │ │ │ └── cover-image │ │ │ └── editor-upload-cover-image-placeholder.tsx │ ├── post │ │ ├── table │ │ │ ├── data-table-row-actions.tsx │ │ │ ├── data │ │ │ │ └── data.ts │ │ │ ├── data-table-view-options.tsx │ │ │ ├── data-table-toolbar.tsx │ │ │ └── data-table-column-header.tsx │ │ ├── post-refresh-once.tsx │ │ ├── post-table-title.tsx │ │ └── post-emtpy-table.tsx │ └── bookmark │ │ ├── index.ts │ │ ├── protected-bookmark-view-button.tsx │ │ ├── protected-bookmark-table-title.tsx │ │ ├── protected-bookmark-table-row-actions.tsx │ │ └── protected-bookmark-table-columns.tsx ├── login │ ├── index.ts │ ├── login-header.tsx │ ├── login-button.tsx │ └── login-menu.tsx ├── main │ ├── header │ │ ├── navigations │ │ │ ├── index.ts │ │ │ ├── menu │ │ │ │ ├── index.ts │ │ │ │ ├── main-mobile-menu-button.tsx │ │ │ │ ├── main-desktop-navigation-menu.tsx │ │ │ │ └── main-mobile-navigation-menu.tsx │ │ │ ├── main-mobile-navigation.tsx │ │ │ └── main-desktop-navigation.tsx │ │ └── main-header.tsx │ ├── post │ │ ├── loading │ │ │ └── index.ts │ │ └── main-post-item-loading.tsx │ ├── index.ts │ ├── tailwind-indicator │ │ └── tailwind-indicator.tsx │ ├── pages │ │ ├── main-terms-page.tsx │ │ ├── main-policy-page.tsx │ │ ├── email.tsx │ │ └── main-about-page.tsx │ ├── grid │ │ └── main-grid.tsx │ └── banner │ │ └── main-banner.tsx ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── input.tsx │ ├── checkbox.tsx │ ├── badge.tsx │ ├── switch.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── radio-group.tsx │ ├── card.tsx │ └── button.tsx ├── detail │ └── post │ │ ├── buttons │ │ ├── index.ts │ │ ├── detail-post-scroll-up-button.tsx │ │ └── detail-post-comment-button.tsx │ │ ├── index.ts │ │ ├── comment │ │ ├── index.ts │ │ ├── detail-post-comment-wrapper.tsx │ │ ├── detail-post-sign-in-to-comment.tsx │ │ └── detail-post-comment-item.tsx │ │ ├── detail-post-floating-bar.tsx │ │ ├── detail-post-header.tsx │ │ └── detail-post-comment.tsx └── shared │ ├── index.ts │ ├── shared-og-image-wrapper.tsx │ ├── shared-empty.tsx │ ├── shared-error.tsx │ ├── shared-back-button.tsx │ ├── shared-table-empty.tsx │ └── shared-pager.tsx ├── public ├── images │ ├── logo.png │ ├── not-found.jpg │ ├── twitter-image.png │ ├── opengraph-image.png │ └── user-placeholder.png ├── favicons │ ├── favicon.ico │ ├── apple-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-180x180.png │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── apple-icon-precomposed.png │ ├── browserconfig.xml │ └── manifest.json └── fonts │ ├── Inter-Bold.ttf │ ├── Inter-Medium.ttf │ └── Inter-Regular.ttf ├── config ├── main │ ├── main-post-config.ts │ ├── main-banner-config.ts │ ├── pages │ │ ├── main-page-contact-config.ts │ │ ├── index.ts │ │ ├── main-page-policy-config.ts │ │ ├── main-page-about-config.ts │ │ └── main-page-terms-config.ts │ ├── index.ts │ ├── main-newsletter-config.ts │ ├── main-category-config.ts │ └── main-footer-config.ts ├── detail │ ├── detail-share-config.ts │ ├── index.ts │ ├── detail-comment-config.ts │ └── detail-bookmark-config.ts ├── shared │ ├── shared-paging-config.ts │ ├── shared-not-found-config.ts │ ├── shared-empty-config.ts │ ├── dashboard │ │ ├── dashboard-logout.ts │ │ ├── dashboard-profile.ts │ │ ├── dashboard-post.ts │ │ ├── dashboard-settings.ts │ │ ├── dashboard-bookmark.ts │ │ ├── dashboard-menu.ts │ │ └── index.ts │ ├── index.ts │ └── shared-login-config.ts ├── protected │ ├── index.ts │ ├── protected-post-config.ts │ ├── protected-profile-config.ts │ └── protected-editor-config.ts └── root │ └── seo.tsx ├── .prettierrc ├── lib ├── validation │ ├── bookmark.ts │ ├── image.ts │ ├── og.ts │ ├── comment.ts │ ├── contact.ts │ ├── profile.ts │ └── post.ts └── nodemailer.ts ├── app ├── (main) │ ├── not-found.tsx │ ├── category │ │ └── [...slug] │ │ │ ├── not-found.tsx │ │ │ ├── error.tsx │ │ │ └── loading.tsx │ ├── about │ │ └── page.tsx │ ├── terms │ │ └── page.tsx │ ├── policy │ │ └── page.tsx │ ├── contact │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── not-found.tsx ├── (protected) │ ├── not-found.tsx │ ├── bookmarks │ │ └── loading.tsx │ ├── editor │ │ └── posts │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── layout.tsx │ └── settings │ │ └── page.tsx ├── (detail) │ └── posts │ │ └── [...slug] │ │ ├── not-found.tsx │ │ ├── error.tsx │ │ ├── loading.tsx │ │ └── layout.tsx ├── login │ └── page.tsx ├── api │ ├── contact │ │ └── route.tsx │ ├── subscribe │ │ └── route.ts │ └── og │ │ └── route.tsx └── auth │ └── callback │ └── route.ts ├── postcss.config.js ├── utils └── supabase │ ├── client.ts │ ├── server.ts │ └── middleware.ts ├── icons ├── socials │ ├── index.ts │ ├── facebook-icon.tsx │ ├── twitter-icon.tsx │ ├── youtube-icon.tsx │ ├── github-icon.tsx │ └── instagram-icon.tsx ├── categories │ ├── index.ts │ ├── category-technology-icon.tsx │ ├── category-health-icon.tsx │ ├── category-science-icon.tsx │ ├── category-marketing-icon.tsx │ └── category-home-icon.tsx ├── loading-dots.tsx ├── icon-wrapper-rounded.tsx ├── bookmark-solid-icon.tsx ├── index.ts ├── loading-dots.module.css ├── bookmark-outline-icon.tsx ├── message-solid-icon.tsx ├── share-solid-icon.tsx ├── message-outline-icon.tsx ├── login-icon.tsx ├── github-icon.tsx ├── share-outline-icon.tsx ├── google-icon.tsx └── logo-icon.tsx ├── components.json ├── .gitignore ├── hooks └── use-reading-progress.ts ├── tsconfig.json ├── styles └── editor.css ├── actions ├── post │ ├── create-post.ts │ ├── delete-post.ts │ └── update-post.ts ├── bookmark │ ├── add-bookmark.ts │ ├── get-bookmark.ts │ └── delete-bookmark.ts ├── comment │ ├── post-comment.ts │ └── delete-comment.ts ├── settings │ └── update-settings.ts └── images │ ├── delete-cover-image.tsx │ └── delete-gallery-image.tsx ├── middleware.ts ├── types └── collection.ts ├── next.config.js ├── .env.example └── tailwind.config.js /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "installCommand": "npm install --legacy-peer-deps" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode" 3 | } -------------------------------------------------------------------------------- /components/protected/main/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ProtectedMain } from "./protected-main"; 2 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /public/images/not-found.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/images/not-found.jpg -------------------------------------------------------------------------------- /public/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /config/main/main-post-config.ts: -------------------------------------------------------------------------------- 1 | const mainPostConfig = { 2 | author: "Author", 3 | }; 4 | 5 | export default mainPostConfig; 6 | -------------------------------------------------------------------------------- /public/favicons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/apple-icon.png -------------------------------------------------------------------------------- /public/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /public/images/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/images/twitter-image.png -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /public/images/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/images/opengraph-image.png -------------------------------------------------------------------------------- /public/images/user-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/images/user-placeholder.png -------------------------------------------------------------------------------- /config/detail/detail-share-config.ts: -------------------------------------------------------------------------------- 1 | const detailShareConfig = { 2 | title: "Share", 3 | }; 4 | 5 | export default detailShareConfig; 6 | -------------------------------------------------------------------------------- /public/favicons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/favicons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/favicons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/favicons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/favicons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/favicons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terabyte-sourcer/next.js-blog-app/HEAD/public/favicons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@ianvs/prettier-plugin-sort-imports", 4 | "prettier-plugin-tailwindcss" 5 | ], 6 | "pluginSearchDirs": false 7 | } 8 | -------------------------------------------------------------------------------- /lib/validation/bookmark.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const bookmarkSchema = z.object({ 4 | id: z.string(), 5 | user_id: z.string(), 6 | }); 7 | -------------------------------------------------------------------------------- /config/shared/shared-paging-config.ts: -------------------------------------------------------------------------------- 1 | const sharedPagingConfig = { 2 | previous: "Previous", 3 | next: "Next", 4 | }; 5 | 6 | export default sharedPagingConfig; 7 | -------------------------------------------------------------------------------- /app/(main)/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { SharedNotFound } from "@/components/shared"; 2 | 3 | const NotFound = () => { 4 | return ; 5 | }; 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /lib/validation/image.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const imageDeleteSchema = z.object({ 4 | userId: z.string(), 5 | postId: z.string(), 6 | fileName: z.string(), 7 | }); 8 | -------------------------------------------------------------------------------- /components/login/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LoginMenu } from "./login-menu"; 2 | export { default as LoginSection } from "./login-section"; 3 | export { default as LoginHeader } from "./login-header"; 4 | -------------------------------------------------------------------------------- /components/main/header/navigations/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MainDesktopNavigation } from "./main-desktop-navigation"; 2 | export { default as MainMobileNavigation } from "./main-mobile-navigation"; 3 | -------------------------------------------------------------------------------- /components/protected/main/sidebars/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ProtectedDesktopSideBar } from "./protected-desktop-sidebar"; 2 | export { default as ProtectedMobileSideBar } from "./protected-mobile-sidebar"; 3 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { SharedNotFound } from "@/components/shared"; 2 | import React from "react"; 3 | 4 | const NotFound = () => { 5 | return ; 6 | }; 7 | 8 | export default NotFound; 9 | -------------------------------------------------------------------------------- /components/main/post/loading/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MainPostItemDesktopLoading } from "./main-post-item-desktop-loading"; 2 | export { default as MainPostItemMobileLoading } from "./main-post-item-mobile-loading"; 3 | -------------------------------------------------------------------------------- /app/(protected)/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { SharedNotFound } from "@/components/shared"; 2 | import React from "react"; 3 | 4 | const NotFound = () => { 5 | return ; 6 | }; 7 | 8 | export default NotFound; 9 | -------------------------------------------------------------------------------- /lib/validation/og.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const ogImageSchema = z.object({ 4 | title: z.string(), 5 | subTitle: z.string(), 6 | tags: z.string().array(), 7 | slug: z.string(), 8 | }); 9 | -------------------------------------------------------------------------------- /app/(detail)/posts/[...slug]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { SharedNotFound } from "@/components/shared"; 2 | import React from "react"; 3 | 4 | const NotFound = () => { 5 | return ; 6 | }; 7 | 8 | export default NotFound; 9 | -------------------------------------------------------------------------------- /app/(main)/category/[...slug]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { SharedNotFound } from "@/components/shared"; 2 | import React from "react"; 3 | 4 | const NotFound = () => { 5 | return ; 6 | }; 7 | 8 | export default NotFound; 9 | -------------------------------------------------------------------------------- /app/(detail)/posts/[...slug]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SharedError } from "@/components/shared"; 4 | import React from "react"; 5 | 6 | const Error = () => { 7 | return ; 8 | }; 9 | 10 | export default Error; 11 | -------------------------------------------------------------------------------- /app/(main)/category/[...slug]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SharedError } from "@/components/shared"; 4 | import React from "react"; 5 | 6 | const Error = () => { 7 | return ; 8 | }; 9 | 10 | export default Error; 11 | -------------------------------------------------------------------------------- /config/detail/index.ts: -------------------------------------------------------------------------------- 1 | export { default as detailBookMarkConfig } from "./detail-bookmark-config"; 2 | export { default as detailCommentConfig } from "./detail-comment-config"; 3 | export { default as detailShareConfig } from "./detail-share-config"; 4 | -------------------------------------------------------------------------------- /utils/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from "@supabase/ssr"; 2 | 3 | export const createClient = () => 4 | createBrowserClient( 5 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 7 | ); 8 | -------------------------------------------------------------------------------- /app/(main)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import MainAboutPage from "@/components/main/pages/main-about-page"; 2 | import React from "react"; 3 | 4 | export default function About() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/(main)/terms/page.tsx: -------------------------------------------------------------------------------- 1 | import MainTermsPage from "@/components/main/pages/main-terms-page"; 2 | import React from "react"; 3 | 4 | export default function Terms() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /config/shared/shared-not-found-config.ts: -------------------------------------------------------------------------------- 1 | const sharedNotFoundConfig = { 2 | title: "Page not found", 3 | description: "The page you are looking for does not exist.", 4 | back: "Back", 5 | menu: "Menu", 6 | }; 7 | 8 | export default sharedNotFoundConfig; 9 | -------------------------------------------------------------------------------- /app/(main)/policy/page.tsx: -------------------------------------------------------------------------------- 1 | import MainPolicyPage from "@/components/main/pages/main-policy-page"; 2 | import React from "react"; 3 | 4 | export default function Policy() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /config/protected/index.ts: -------------------------------------------------------------------------------- 1 | export { default as protectedEditorConfig } from "./protected-editor-config"; 2 | export { default as protectedPostConfig } from "./protected-post-config"; 3 | export { default as protectedProfileConfig } from "./protected-profile-config"; 4 | -------------------------------------------------------------------------------- /app/(main)/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import MainContactPage from "@/components/main/pages/main-contact-page"; 2 | import React from "react"; 3 | 4 | export default function ContactPage() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /config/shared/shared-empty-config.ts: -------------------------------------------------------------------------------- 1 | const sharedEmptyConfig = { 2 | title: "Empty", 3 | description: "No posts to display", 4 | error: "Something went wrong", 5 | tryAgain: "Try again", 6 | sorry: "Sorry", 7 | }; 8 | 9 | export default sharedEmptyConfig; 10 | -------------------------------------------------------------------------------- /components/protected/main/header/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ProtectedMobileMenuButton } from "./protected-mobile-menu-button"; 2 | export { default as ProtectedProfileDropDown } from "./protected-profile-dropdown"; 3 | export { default as ProtectedTopBar } from "./protected-top-bar"; 4 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
; 5 | } 6 | 7 | export { Skeleton }; 8 | -------------------------------------------------------------------------------- /config/main/main-banner-config.ts: -------------------------------------------------------------------------------- 1 | const mainBannerConfig = { 2 | title: "Fullstack Blogging App", 3 | description: "built with Next.js and Supabase.", 4 | link: "https://github.com/timtbdev/Next.js-Blog-App", 5 | button: "Github", 6 | }; 7 | 8 | export default mainBannerConfig; 9 | -------------------------------------------------------------------------------- /app/(detail)/posts/[...slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { DetailPostLoading } from "@/components/detail/post"; 2 | import React from "react"; 3 | 4 | const Loading = () => { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Loading; 13 | -------------------------------------------------------------------------------- /config/shared/dashboard/dashboard-logout.ts: -------------------------------------------------------------------------------- 1 | import { DashBoardType } from "@/types"; 2 | import { LogOut } from "lucide-react"; 3 | 4 | const dashBoardLogout: DashBoardType = { 5 | title: "Sign Out", 6 | slug: "/logout", 7 | icon: LogOut, 8 | }; 9 | 10 | export default dashBoardLogout; 11 | -------------------------------------------------------------------------------- /components/main/header/navigations/menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MainDesktopNavigationMenu } from "./main-desktop-navigation-menu"; 2 | export { default as MainMobileNavigationMenu } from "./main-mobile-navigation-menu"; 3 | export { default as MainMobileMenuButton } from "./main-mobile-menu-button"; 4 | -------------------------------------------------------------------------------- /config/main/pages/main-page-contact-config.ts: -------------------------------------------------------------------------------- 1 | const mainPageContactConfig = { 2 | error: "Error sending message", 3 | emailSent: "Message sent successfully", 4 | email: "Email", 5 | message: "Message", 6 | send: "Send", 7 | name: "Name", 8 | }; 9 | 10 | export default mainPageContactConfig; 11 | -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /config/shared/dashboard/dashboard-profile.ts: -------------------------------------------------------------------------------- 1 | import { DashBoardType } from "@/types"; 2 | import { UserCircle } from "lucide-react"; 3 | 4 | const dashBoardProfile: DashBoardType = { 5 | title: "Profile", 6 | slug: "/settings", 7 | icon: UserCircle, 8 | }; 9 | 10 | export default dashBoardProfile; 11 | -------------------------------------------------------------------------------- /config/shared/dashboard/dashboard-post.ts: -------------------------------------------------------------------------------- 1 | import { DashBoardType } from "@/types"; 2 | import { FileTextIcon as PostIcon } from "lucide-react"; 3 | 4 | const dashBoardPost: DashBoardType = { 5 | title: "Posts", 6 | slug: "/editor/posts", 7 | icon: PostIcon, 8 | }; 9 | 10 | export default dashBoardPost; 11 | -------------------------------------------------------------------------------- /config/shared/dashboard/dashboard-settings.ts: -------------------------------------------------------------------------------- 1 | import { DashBoardType } from "@/types"; 2 | import { SettingsIcon } from "lucide-react"; 3 | 4 | const dashBoardSettings: DashBoardType = { 5 | title: "Settings", 6 | slug: "/settings", 7 | icon: SettingsIcon, 8 | }; 9 | 10 | export default dashBoardSettings; 11 | -------------------------------------------------------------------------------- /config/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { default as sharedLoginConfig } from "./shared-login-config"; 2 | export { default as sharedPagingConfig } from "./shared-paging-config"; 3 | export { default as sharedNotFoundConfig } from "./shared-not-found-config"; 4 | export { default as sharedEmptyConfig } from "./shared-empty-config"; 5 | -------------------------------------------------------------------------------- /icons/socials/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FacebookIcon } from "./facebook-icon"; 2 | export { default as GithubIcon } from "./github-icon"; 3 | export { default as InstagramIcon } from "./instagram-icon"; 4 | export { default as TwitterIcon } from "./twitter-icon"; 5 | export { default as YoutubeIcon } from "./youtube-icon"; 6 | -------------------------------------------------------------------------------- /config/main/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as mainPageAboutConfig } from "./main-page-about-config"; 2 | export { default as mainPageContactConfig } from "./main-page-contact-config"; 3 | export { default as mainPagePolicyConfig } from "./main-page-policy-config"; 4 | export { default as mainPageTermsConfig } from "./main-page-terms-config"; 5 | -------------------------------------------------------------------------------- /config/shared/dashboard/dashboard-bookmark.ts: -------------------------------------------------------------------------------- 1 | import { DashBoardType } from "@/types"; 2 | import { BookMarkedIcon, FileTextIcon as PostIcon } from "lucide-react"; 3 | 4 | const dashBoardBookMark: DashBoardType = { 5 | title: "Bookmarks", 6 | slug: "/bookmarks", 7 | icon: BookMarkedIcon, 8 | }; 9 | 10 | export default dashBoardBookMark; 11 | -------------------------------------------------------------------------------- /app/(protected)/bookmarks/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SharedTableLoading } from "@/components/shared"; 2 | import React from "react"; 3 | 4 | export default function Loading() { 5 | return ( 6 | <> 7 |
8 | 9 |
10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/(protected)/editor/posts/loading.tsx: -------------------------------------------------------------------------------- 1 | import { SharedTableLoading } from "@/components/shared"; 2 | import React from "react"; 3 | 4 | export default function Loading() { 5 | return ( 6 | <> 7 |
8 | 9 |
10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/detail/post/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DetailPostBookMarkButton } from "./detail-post-bookmark-button"; 2 | export { default as DetailPostCommentButton } from "./detail-post-comment-button"; 3 | export { default as DetailPostScrollUpButton } from "./detail-post-scroll-up-button"; 4 | export { default as DetailPostShareButton } from "./detail-post-share-button"; 5 | -------------------------------------------------------------------------------- /config/shared/dashboard/dashboard-menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dashBoardBookMark, 3 | dashBoardPost, 4 | dashBoardSettings, 5 | } from "@/config/shared/dashboard"; 6 | import { DashBoardType } from "@/types"; 7 | 8 | const dashBoardMenu: DashBoardType[] = [ 9 | dashBoardPost, 10 | dashBoardBookMark, 11 | dashBoardSettings, 12 | ]; 13 | 14 | export default dashBoardMenu; 15 | -------------------------------------------------------------------------------- /components/detail/post/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DetailPostComment } from "./detail-post-comment"; 2 | export { default as DetailPostHeading } from "./detail-post-heading"; 3 | export { default as DetailPostHeader } from "./detail-post-header"; 4 | export { default as DetailPostLoading } from "./detail-post-loading"; 5 | export { default as DetailPostFloatingBar } from "./detail-post-floating-bar"; 6 | -------------------------------------------------------------------------------- /icons/categories/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CategoryHomeIcon } from "./category-home-icon"; 2 | export { default as CategoryHealthIcon } from "./category-health-icon"; 3 | export { default as CategoryMarketingIcon } from "./category-marketing-icon"; 4 | export { default as CategoryScienceIcon } from "./category-science-icon"; 5 | export { default as CategoryTechnologyIcon } from "./category-technology-icon"; 6 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/styles/tailwind.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /components/main/header/main-header.tsx: -------------------------------------------------------------------------------- 1 | import { MainDesktopNavigation, MainMobileNavigation } from "./navigations"; 2 | 3 | export default function MainHeader() { 4 | return ( 5 |
6 | 7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /components/main/post/main-post-item-loading.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MainPostItemDesktopLoading, 3 | MainPostItemMobileLoading, 4 | } from "./loading"; 5 | 6 | const MainPostItemLoading = () => { 7 | return ( 8 | <> 9 | {/* LoadingItems */} 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default MainPostItemLoading; 17 | -------------------------------------------------------------------------------- /components/protected/editor/wysiwyg/extensions/updated-image.tsx: -------------------------------------------------------------------------------- 1 | import Image from "@tiptap/extension-image"; 2 | 3 | const UpdatedImage = Image.extend({ 4 | addAttributes() { 5 | return { 6 | ...this.parent?.(), 7 | width: { 8 | default: null, 9 | }, 10 | height: { 11 | default: null, 12 | }, 13 | }; 14 | }, 15 | }); 16 | 17 | export default UpdatedImage; 18 | -------------------------------------------------------------------------------- /config/shared/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as dashBoardBookMark } from "./dashboard-bookmark"; 2 | export { default as dashBoardLogout } from "./dashboard-logout"; 3 | export { default as dashBoardPost } from "./dashboard-post"; 4 | export { default as dashBoardProfile } from "./dashboard-profile"; 5 | export { default as dashBoardSettings } from "./dashboard-settings"; 6 | export { default as dashBoardMenu } from "./dashboard-menu"; 7 | -------------------------------------------------------------------------------- /config/main/index.ts: -------------------------------------------------------------------------------- 1 | export { default as mainBannerConfig } from "./main-banner-config"; 2 | export { default as sharedEmptyConfig } from "../shared/shared-empty-config"; 3 | export { default as mainFooterConfig } from "./main-footer-config"; 4 | export { default as mainCategoryConfig } from "./main-category-config"; 5 | export { default as mainNewsLetterConfig } from "./main-newsletter-config"; 6 | export { default as mainPostConfig } from "./main-post-config"; 7 | -------------------------------------------------------------------------------- /components/detail/post/comment/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DetailPostCommentDeleteButton } from "./detail-post-comment-delete-button"; 2 | export { default as DetailPostCommentForm } from "./detail-post-comment-form"; 3 | export { default as DetailPostCommentItem } from "./detail-post-comment-item"; 4 | export { default as DetailPostCommentWrapper } from "./detail-post-comment-wrapper"; 5 | export { default as DetailPostSignInToComment } from "./detail-post-sign-in-to-comment"; 6 | -------------------------------------------------------------------------------- /components/protected/post/table/data-table-row-actions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import PostEditButton from "@/components/protected/post/buttons/post-edit-button"; 4 | import { Row } from "@tanstack/react-table"; 5 | 6 | interface DataTableRowActionsProps { 7 | row: Row; 8 | } 9 | 10 | export function DataTableRowActions({ 11 | row, 12 | }: DataTableRowActionsProps) { 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /lib/nodemailer.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import SMPTransport from 'nodemailer-smtp-transport'; 3 | 4 | export const smtpEmail = process.env.GOOGLE_EMAIL; 5 | export const smtpPassword = process.env.GOOGLE_PASSWORD; 6 | 7 | export const transporter = nodemailer.createTransport( 8 | SMPTransport({ 9 | service: 'gmail', 10 | auth: { 11 | user: smtpEmail, 12 | pass: smtpPassword, 13 | }, 14 | }) 15 | ); 16 | -------------------------------------------------------------------------------- /components/main/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MainHeader } from "./header/main-header"; 2 | export { default as MainGrid } from "./grid/main-grid"; 3 | export { default as MainBanner } from "./banner/main-banner"; 4 | export { default as MainFooter } from "./footer/main-footer"; 5 | export { default as MainPostItem } from "./post/main-post-item"; 6 | export { default as MainPostItemLoading } from "./post/main-post-item-loading"; 7 | export { default as TailwindIndicator } from "./tailwind-indicator/tailwind-indicator"; 8 | -------------------------------------------------------------------------------- /components/protected/bookmark/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ProtectedBookMarkDeleteButton } from "./protected-bookmark-delete-button"; 2 | export { default as ProtectedBookMarkViewButton } from "./protected-bookmark-view-button"; 3 | export { default as ProtectedBookMarkTableColumns } from "./protected-bookmark-table-columns"; 4 | export { default as ProtectedBookMarkTableTitle } from "./protected-bookmark-table-title"; 5 | export { default as ProtectedBookMarkTableRowActions } from "./protected-bookmark-table-row-actions"; 6 | -------------------------------------------------------------------------------- /app/(main)/category/[...slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import MainPostItemDesktopLoading from "@/components/main/post/loading/main-post-item-desktop-loading"; 2 | import MainPostItemMobileLoading from "@/components/main/post/loading/main-post-item-mobile-loading"; 3 | import React from "react"; 4 | 5 | const Loading = () => { 6 | return ( 7 | <> 8 | {/* LoadingItems */} 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default Loading; 16 | -------------------------------------------------------------------------------- /config/main/main-newsletter-config.ts: -------------------------------------------------------------------------------- 1 | const mainNewsLetterConfig = { 2 | title: "Subscribe to our newsletter", 3 | description: 4 | "The latest news, articles, and resources, sent to your inbox weekly.", 5 | email: "Email address", 6 | subscribe: "Subscribe", 7 | warning: "You need to confirm your email address.", 8 | success: "Thank you for subscribing.", 9 | error: "Something went wrong.", 10 | emailRequiredError: "Please provide your email address.", 11 | }; 12 | 13 | export default mainNewsLetterConfig; 14 | -------------------------------------------------------------------------------- /icons/loading-dots.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import styles from "./loading-dots.module.css"; 3 | 4 | interface LoadingDotsProps { 5 | color?: string; 6 | } 7 | 8 | const LoadingDots: FC = ({ color = "#000" }) => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default LoadingDots; 19 | -------------------------------------------------------------------------------- /components/protected/post/post-refresh-once.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter, useSearchParams } from "next/navigation"; 4 | import React, { useEffect } from "react"; 5 | 6 | const PostRefreshOnce = () => { 7 | const router = useRouter(); 8 | const searchParams = useSearchParams(); 9 | const search = searchParams.get("search"); 10 | 11 | useEffect(() => { 12 | if (search === "refresh") { 13 | router.refresh(); 14 | } 15 | }, [search, router]); 16 | return null; 17 | }; 18 | 19 | export default PostRefreshOnce; 20 | -------------------------------------------------------------------------------- /components/shared/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SharedBackButton } from "./shared-back-button"; 2 | export { default as SharedPagination } from "./shared-pagination"; 3 | export { default as SharedOgImage } from "./shared-og-image"; 4 | export { default as SharedNotFound } from "./shared-not-found"; 5 | export { default as SharedError } from "./shared-error"; 6 | export { default as SharedEmpty } from "./shared-empty"; 7 | export { default as SharedTableLoading } from "./shared-table-loading"; 8 | export { default as SharedTableEmpty } from "./shared-table-empty"; 9 | -------------------------------------------------------------------------------- /lib/validation/comment.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const commentSchema = z.object({ 4 | postId: z.string(), 5 | userId: z.string(), 6 | comment: z.string(), 7 | }); 8 | 9 | export const commentDeleteSchema = z.object({ 10 | id: z.string(), 11 | userId: z.string(), 12 | }); 13 | 14 | export const commentFormSchema = z.object({ 15 | comment: z 16 | .string() 17 | .min(3, { message: 'Comment must be at least 3 characters long.' }) 18 | .max(500, { message: 'Comment must be at most 500 characters long.' }), 19 | }); 20 | -------------------------------------------------------------------------------- /components/detail/post/comment/detail-post-comment-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | interface DetailPostCommentWrapperProps { 4 | children?: React.ReactNode; 5 | } 6 | 7 | const DetailPostCommentWrapper: FC = ({ 8 | children, 9 | }) => { 10 | return ( 11 |
15 |
{children}
16 |
17 | ); 18 | }; 19 | 20 | export default DetailPostCommentWrapper; 21 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | #supabase 38 | ubdotcafe.code-workspace 39 | /supabase 40 | -------------------------------------------------------------------------------- /config/detail/detail-comment-config.ts: -------------------------------------------------------------------------------- 1 | const detailCommentConfig = { 2 | title: "Comment", 3 | comments: "Comments", 4 | delete: "Delete", 5 | submit: "Submit", 6 | edit: "Edit", 7 | questionDelete: "Are you sure you want to delete this comment?", 8 | warning: "This action cannot be undone.", 9 | cancel: "Cancel", 10 | confirm: "Confirm", 11 | successDeleted: "Comment deleted", 12 | successAdd: "Comment added", 13 | errorAdd: "Error adding comment", 14 | errorDeleted: "Error deleting comment", 15 | leaveComment: "Leave a comment", 16 | }; 17 | 18 | export default detailCommentConfig; 19 | -------------------------------------------------------------------------------- /config/shared/shared-login-config.ts: -------------------------------------------------------------------------------- 1 | const sharedLoginConfig = { 2 | // Login form 3 | title: "Login", 4 | description: "Please sign in to continue.", 5 | or: "or", 6 | close: "Close", 7 | // Social login form 8 | google: "Sign in with Google", 9 | facebook: "Sign in with Facebook", 10 | github: "Sign in with Github", 11 | 12 | // Email login form 13 | email: "Sign in with magic link", 14 | sendButton: "Send magic link", 15 | emailRequiredError: "Email is required.", 16 | emailSent: "Email sent.", 17 | error: "Error occured.", 18 | }; 19 | 20 | export default sharedLoginConfig; 21 | -------------------------------------------------------------------------------- /app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MainBanner, 3 | MainFooter, 4 | MainGrid, 5 | MainHeader, 6 | } from "@/components/main"; 7 | import { ReactNode } from "react"; 8 | 9 | export default function MainLayout({ children }: { children: ReactNode }) { 10 | return ( 11 | <> 12 | 13 | 14 | 15 |
16 |
17 |
{children}
18 |
19 |
20 |
21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/shared/shared-og-image-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from "react"; 2 | 3 | /* eslint-disable jsx-a11y/alt-text */ 4 | /* eslint-disable @next/next/no-img-element */ 5 | 6 | interface ShaerdOgImageWrapperProps { 7 | children: ReactNode; 8 | } 9 | 10 | export const SharedOgImageWrapper: FC = ({ 11 | children, 12 | }) => ( 13 |
14 |
18 | {children} 19 |
20 |
21 | ); 22 | -------------------------------------------------------------------------------- /icons/icon-wrapper-rounded.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from "react"; 2 | 3 | interface IconWrapperRoundedProps { 4 | children: ReactNode; 5 | } 6 | 7 | const IconWrapperRounded: FC = ({ children }) => { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | }; 14 | 15 | export default IconWrapperRounded; 16 | -------------------------------------------------------------------------------- /components/protected/bookmark/protected-bookmark-view-button.tsx: -------------------------------------------------------------------------------- 1 | import { EyeIcon } from "lucide-react"; 2 | import Link from "next/link"; 3 | import { FC } from "react"; 4 | 5 | interface ProtectedBookMarkViewButtonProps { 6 | slug?: string; 7 | } 8 | 9 | const ProtectedBookMarkViewButton: FC = ({ 10 | slug, 11 | }) => { 12 | return ( 13 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default ProtectedBookMarkViewButton; 24 | -------------------------------------------------------------------------------- /components/protected/editor/upload/gallery-image/editor-upload-gallery-image-table-empty.tsx: -------------------------------------------------------------------------------- 1 | import EditorUploadGalleryImagePlaceholder from "./editor-upload-gallery-image-placeholder"; 2 | 3 | const EditorUploadGalleryImageTableEmpty = () => { 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default EditorUploadGalleryImageTableEmpty; 16 | -------------------------------------------------------------------------------- /components/shared/shared-empty.tsx: -------------------------------------------------------------------------------- 1 | import { sharedEmptyConfig } from "@/config/shared"; 2 | import { AlertTriangleIcon } from "lucide-react"; 3 | 4 | const SharedEmpty = () => { 5 | return ( 6 |
7 | 8 |

9 | {sharedEmptyConfig.title} 10 |

11 |

12 | {sharedEmptyConfig.description} 13 |

14 |
15 | ); 16 | }; 17 | 18 | export default SharedEmpty; 19 | -------------------------------------------------------------------------------- /components/protected/editor/upload/gallery-image/editor-upload-gallery-image-placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { PhotoIcon } from "@heroicons/react/20/solid"; 2 | import React from "react"; 3 | 4 | const EditorUploadGalleryImagePlaceholder = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default EditorUploadGalleryImagePlaceholder; 18 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginHeader, LoginSection } from "@/components/login"; 2 | import { createClient } from "@/utils/supabase/server"; 3 | import { cookies } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | 6 | const LoginPage = async () => { 7 | const cookieStore = cookies(); 8 | const supabase = createClient(cookieStore); 9 | 10 | const { 11 | data: { user }, 12 | } = await supabase.auth.getUser(); 13 | 14 | user && redirect("/editor/posts"); 15 | 16 | return ( 17 | <> 18 | {" "} 19 |
20 | 21 |
22 | 23 | ); 24 | }; 25 | 26 | export default LoginPage; 27 | -------------------------------------------------------------------------------- /components/protected/editor/upload/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EditorUploadCoverImageItem } from "./cover-image/editor-upload-cover-image-item"; 2 | export { default as EditorUploadCoverImagePlaceHolder } from "./cover-image/editor-upload-cover-image-placeholder"; 3 | export { default as EditorUploadGalleryImageItem } from "./gallery-image/editor-upload-gallery-image-item"; 4 | export { default as EditorUploadGallerImagePlaceHolder } from "./gallery-image/editor-upload-gallery-image-placeholder"; 5 | export { default as EditorUploadGalleryImageTableEmpty } from "./gallery-image/editor-upload-gallery-image-table-empty"; 6 | export { default as EditorUploadGalleryImageTable } from "./gallery-image/editor-upload-gallery-image-table"; 7 | -------------------------------------------------------------------------------- /lib/validation/contact.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | // Email validation schame for newsletter 4 | export const emailSchema = z.object({ 5 | email: z.string().email({ message: 'Email is required.' }), 6 | }); 7 | 8 | // Contact form validation schema 9 | export const contactFormSchema = z.object({ 10 | name: z.string().min(3, { message: 'Name is required' }), 11 | email: z.string().email({ message: 'Email is required.' }), 12 | message: z 13 | .string() 14 | .min(4, { 15 | message: 'Your message must be at least 4 characters long.', 16 | }) 17 | .max(320, { 18 | message: 'Your message cannot be more than 320 characters long.', 19 | }), 20 | }); 21 | -------------------------------------------------------------------------------- /icons/socials/facebook-icon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | interface FacebookIconProps { 4 | className?: string; 5 | } 6 | 7 | const FacebookIcon: FC = ({ className = "" }) => { 8 | return ( 9 | 10 | 15 | 16 | ); 17 | }; 18 | 19 | export default FacebookIcon; 20 | -------------------------------------------------------------------------------- /config/detail/detail-bookmark-config.ts: -------------------------------------------------------------------------------- 1 | const detailBookMarkConfig = { 2 | title: "Bookmarks", 3 | description: "Manage your bookmarks", 4 | delete: "Delete", 5 | view: "View", 6 | deleted: "Deleted", 7 | edit: "Edit", 8 | question: "Are you sure you want to delete this bookmark?", 9 | warning: "This action cannot be undone.", 10 | cancel: "Cancel", 11 | confirm: "Confirm", 12 | tableHeader: ["Post", "Date", "Action"], 13 | errorDelete: "Couldn't delete this bookmark.", 14 | successDelete: "Bookmark deleted.", 15 | successAdd: "Bookmark added.", 16 | errorAdd: "Couldn't add this bookmark.", 17 | bookmark: "Bookmark", 18 | unBookmark: "Unbookmark", 19 | bookmarked: "Bookmarked", 20 | }; 21 | 22 | export default detailBookMarkConfig; 23 | -------------------------------------------------------------------------------- /components/login/login-header.tsx: -------------------------------------------------------------------------------- 1 | import SharedBackButton from "@/components/shared/shared-back-button"; 2 | 3 | const LoginHeader = () => { 4 | return ( 5 |
6 | 16 |
17 | ); 18 | }; 19 | 20 | export default LoginHeader; 21 | -------------------------------------------------------------------------------- /components/protected/bookmark/protected-bookmark-table-title.tsx: -------------------------------------------------------------------------------- 1 | import { protectedPostConfig } from "@/config/protected"; 2 | 3 | const ProtectedBookMarkTableTitle = () => { 4 | return ( 5 | <> 6 |
7 |
8 |

9 | {protectedPostConfig.title} 10 |

11 |

12 | {protectedPostConfig.description} 13 |

14 |
15 |
16 |
17 | 18 | ); 19 | }; 20 | 21 | export default ProtectedBookMarkTableTitle; 22 | -------------------------------------------------------------------------------- /components/protected/bookmark/protected-bookmark-table-row-actions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ProtectedBookMarkDeleteButton, 5 | ProtectedBookMarkViewButton, 6 | } from "@/components/protected/bookmark"; 7 | import { Row } from "@tanstack/react-table"; 8 | 9 | interface ProtectedBookMarkTableRowActionsProps { 10 | row: Row; 11 | } 12 | 13 | export default function ProtectedBookMarkTableRowActions({ 14 | row, 15 | }: ProtectedBookMarkTableRowActionsProps) { 16 | return ( 17 |
18 | {/* @ts-ignore */} 19 | 20 | {/* @ts-ignore */} 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /icons/socials/twitter-icon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | interface TwitterIconProps { 4 | className?: string; 5 | } 6 | 7 | const TwitterIcon: FC = ({ className = "" }) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default TwitterIcon; 16 | -------------------------------------------------------------------------------- /icons/socials/youtube-icon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | interface YoutubeIconProps { 4 | className?: string; 5 | } 6 | 7 | const YoutubeIcon: FC = ({ className = "" }) => { 8 | return ( 9 | 10 | 15 | 16 | ); 17 | }; 18 | 19 | export default YoutubeIcon; 20 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & VariantProps 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )); 17 | Label.displayName = LabelPrimitive.Root.displayName; 18 | 19 | export { Label }; 20 | -------------------------------------------------------------------------------- /icons/bookmark-solid-icon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | interface BookmarkSolidIconProps { 4 | className: string; 5 | } 6 | 7 | const BookmarkSolidIcon: FC = ({ className = "" }) => { 8 | return ( 9 | <> 10 | 17 | 18 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default BookmarkSolidIcon; 29 | -------------------------------------------------------------------------------- /icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BookMarkOutlineIcon } from "./bookmark-outline-icon"; 2 | export { default as BookMarkSolidIcon } from "./bookmark-solid-icon"; 3 | export { default as GithubIcon } from "./github-icon"; 4 | export { default as GoogleIcon } from "./google-icon"; 5 | export { default as IconWrapperRounded } from "./icon-wrapper-rounded"; 6 | export { default as LogoIcon } from "./logo-icon"; 7 | export { default as LoadingDots } from "./loading-dots"; 8 | export { default as LoginIcon } from "./login-icon"; 9 | export { default as MessageOutlineIcon } from "./message-outline-icon"; 10 | export { default as MessageSolidIcon } from "./message-solid-icon"; 11 | export { default as ShareOutlineIcon } from "./share-outline-icon"; 12 | export { default as ShareSolidIcon } from "./share-solid-icon"; 13 | -------------------------------------------------------------------------------- /icons/loading-dots.module.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | 6 | .loading .spacer { 7 | margin-right: 2px; 8 | } 9 | 10 | .loading span { 11 | animation-name: blink; 12 | animation-duration: 1.4s; 13 | animation-iteration-count: infinite; 14 | animation-fill-mode: both; 15 | width: 5px; 16 | height: 5px; 17 | border-radius: 50%; 18 | display: inline-block; 19 | margin: 0 1px; 20 | } 21 | 22 | .loading span:nth-of-type(2) { 23 | animation-delay: 0.2s; 24 | } 25 | 26 | .loading span:nth-of-type(3) { 27 | animation-delay: 0.4s; 28 | } 29 | 30 | @keyframes blink { 31 | 0% { 32 | opacity: 0.2; 33 | } 34 | 20% { 35 | opacity: 1; 36 | } 37 | 100% { 38 | opacity: 0.2; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /hooks/use-reading-progress.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useReadingProgress = () => { 4 | const [completion, setCompletion] = useState(0); 5 | 6 | useEffect(() => { 7 | const updateScrollCompletion = () => { 8 | const currentProgress = window.scrollY; 9 | const scrollHeight = document.body.scrollHeight - window.innerHeight; 10 | if (scrollHeight) { 11 | setCompletion(Number((currentProgress / scrollHeight).toFixed(2)) * 100); 12 | } 13 | }; 14 | 15 | window.addEventListener('scroll', updateScrollCompletion); 16 | 17 | return () => { 18 | window.removeEventListener('scroll', updateScrollCompletion); 19 | }; 20 | }, []); 21 | 22 | return completion; 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noImplicitAny": false, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": ["./*"] 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /components/protected/main/header/protected-mobile-menu-button.tsx: -------------------------------------------------------------------------------- 1 | import { Bars3Icon } from "@heroicons/react/20/solid"; 2 | import React, { Dispatch, FC, SetStateAction } from "react"; 3 | 4 | type Dispatcher = Dispatch>; 5 | interface ProtectedMobileMenuButtonProps { 6 | setSidebarOpen: Dispatcher; 7 | } 8 | 9 | const ProtectedMobileMenuButton: FC = ({ 10 | setSidebarOpen, 11 | }) => { 12 | return ( 13 | 21 | ); 22 | }; 23 | 24 | export default ProtectedMobileMenuButton; 25 | -------------------------------------------------------------------------------- /components/main/tailwind-indicator/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | const TailwindIndicator = () => { 2 | if (process.env.NODE_ENV === "production") return null; 3 | 4 | return ( 5 |
6 |
xs
7 |
8 | sm 9 |
10 |
md
11 |
lg
12 |
xl
13 |
2xl
14 |
15 | ); 16 | }; 17 | 18 | export default TailwindIndicator; 19 | -------------------------------------------------------------------------------- /components/main/pages/main-terms-page.tsx: -------------------------------------------------------------------------------- 1 | import { mainPageTermsConfig } from "@/config/main/pages"; 2 | import React from "react"; 3 | 4 | const MainTermsPage = () => { 5 | return ( 6 |
7 |
8 |
9 |

10 | {mainPageTermsConfig.title} 11 |

12 | 13 | {mainPageTermsConfig.paragraphs.map((item) => ( 14 | <> 15 |

16 | {item.description} 17 |

18 | 19 | ))} 20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default MainTermsPage; 27 | -------------------------------------------------------------------------------- /components/login/login-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; 4 | import { LoginIcon } from "@/icons"; 5 | import { useState } from "react"; 6 | import { default as LoginSection } from "./login-section"; 7 | 8 | const LoginButton = () => { 9 | const [open, setOpen] = useState(false); 10 | return ( 11 | 12 | 13 |
14 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default LoginButton; 27 | -------------------------------------------------------------------------------- /icons/bookmark-outline-icon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | interface BookmarkOutlineIconProps { 4 | className: string; 5 | } 6 | 7 | const BookmarkOutlineIcon: FC = ({ 8 | className = "", 9 | }) => { 10 | return ( 11 | <> 12 | 19 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default BookmarkOutlineIcon; 35 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface TextareaProps extends React.TextareaHTMLAttributes {} 6 | 7 | const Textarea = React.forwardRef(({ className, ...props }, ref) => { 8 | return ( 9 |