├── .eslintrc.json ├── public ├── 12.jpg ├── laptop.jpg ├── rayi-christian-wicaksono-6PF6DaiWz48-unsplash.jpg ├── vercel.svg └── next.svg ├── src ├── app │ ├── favicon.ico │ ├── (pages) │ │ ├── [userId] │ │ │ ├── [postId] │ │ │ │ ├── layout.tsx │ │ │ │ ├── error.tsx │ │ │ │ ├── edit │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── setting │ │ │ ├── [settingParams] │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── new │ │ │ ├── page.tsx │ │ │ └── layout.tsx │ │ ├── layout.tsx │ │ ├── dashboard │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── [filter] │ │ │ │ └── page.tsx │ │ ├── search │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── tags │ │ │ └── page.tsx │ │ └── page.tsx │ ├── global-error.tsx │ ├── api │ │ ├── tags │ │ │ ├── route.ts │ │ │ └── addTagInPost │ │ │ │ └── route.ts │ │ ├── auth │ │ │ ├── logout │ │ │ │ └── route.ts │ │ │ ├── login │ │ │ │ └── route.ts │ │ │ └── signup │ │ │ │ └── route.ts │ │ ├── users │ │ │ ├── [username] │ │ │ │ └── route.ts │ │ │ ├── follow │ │ │ │ └── route.ts │ │ │ └── me │ │ │ │ └── route.ts │ │ ├── dashboard │ │ │ ├── [filter] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── comment │ │ │ ├── reply │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── posts │ │ │ ├── saved │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── [postId] │ │ │ │ └── route.ts │ │ └── search │ │ │ └── route.ts │ ├── layout.tsx │ ├── not-found.tsx │ ├── globals.css │ └── (auth) │ │ ├── signin │ │ └── page.tsx │ │ └── signup │ │ └── page.tsx ├── utils │ ├── getPublicIdCloudinary.ts │ ├── constants.ts │ ├── uploadImageToCloudinary.ts │ ├── deleteFileFromCloudinary.ts │ ├── convertImageTobase64.ts │ ├── getDataFromToken.ts │ └── utils.ts ├── components │ ├── Loading.tsx │ ├── search │ │ ├── SearchNotMatch.tsx │ │ ├── SearchPostLoop.tsx │ │ └── SearchInput.tsx │ ├── Icon.tsx │ ├── Analytics.tsx │ ├── GetCurrentUser.tsx │ ├── Providers.tsx │ ├── comments │ │ ├── CommentCard.tsx │ │ ├── AddReply.tsx │ │ ├── CommentOption.tsx │ │ ├── AddComment.tsx │ │ └── Comments.tsx │ ├── Footer.tsx │ ├── AuthModal.tsx │ ├── dashboard │ │ ├── Count.tsx │ │ ├── Filter.tsx │ │ └── Posts.tsx │ ├── posts │ │ ├── DeletePostModal.tsx │ │ ├── PostArticle.tsx │ │ ├── UserProfileCard.tsx │ │ └── PostCard.tsx │ ├── navbar │ │ ├── SideNav.tsx │ │ ├── Navbar.tsx │ │ └── NavbarProfile.tsx │ ├── RightAside.tsx │ └── profile │ │ └── ProfileDetails.tsx ├── lib │ ├── config │ │ ├── cloudinary.ts │ │ └── site.ts │ ├── validation │ │ ├── signInSchema.ts │ │ ├── editProfileSchema.ts │ │ └── signUpSchema.ts │ ├── db.ts │ └── types.ts ├── hooks │ ├── reduxHooks.ts │ └── useQueryString.tsx ├── redux │ ├── userSlice.ts │ ├── commonSlice.ts │ ├── editorSlice.ts │ ├── store.ts │ ├── authSlice.ts │ └── dashboardSlice.ts └── middleware.ts ├── postcss.config.js ├── next.config.js ├── editorjs.d.ts ├── .gitignore ├── tailwind.config.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── prisma └── schema.prisma └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehseen01/nextjs-blog/HEAD/public/12.jpg -------------------------------------------------------------------------------- /public/laptop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehseen01/nextjs-blog/HEAD/public/laptop.jpg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehseen01/nextjs-blog/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/rayi-christian-wicaksono-6PF6DaiWz48-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehseen01/nextjs-blog/HEAD/public/rayi-christian-wicaksono-6PF6DaiWz48-unsplash.jpg -------------------------------------------------------------------------------- /src/app/(pages)/[userId]/[postId]/layout.tsx: -------------------------------------------------------------------------------- 1 | const PostLayout = ({ children }: { children: React.ReactNode }) => { 2 | return <>{children}; 3 | }; 4 | 5 | export default PostLayout; 6 | -------------------------------------------------------------------------------- /src/utils/getPublicIdCloudinary.ts: -------------------------------------------------------------------------------- 1 | export const getPublicIdCloudinary = (imageURL: string) => { 2 | const publicID = imageURL.split("/").pop()?.split(".")[0]; 3 | return publicID; 4 | }; 5 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 | Loading... 7 |
8 | ); 9 | }; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // Constants related to search functionality 2 | export const FILTER_POSTS = "Posts"; 3 | export const FILTER_PEOPLE = "People"; 4 | export const FILTER_COMMENTS = "Comments"; 5 | export const FILTER_TAGS = "Tags"; 6 | export const FILTER_MY_POSTS_ONLY = "My_posts"; 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "res.cloudinary.com", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | module.exports = nextConfig; 14 | -------------------------------------------------------------------------------- /src/app/(pages)/setting/[settingParams]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const settingParamsPage = ({ 4 | params, 5 | }: { 6 | params: { settingParams: string }; 7 | }) => { 8 | return
{params.settingParams}: settingParamsPage
; 9 | }; 10 | 11 | export default settingParamsPage; 12 | -------------------------------------------------------------------------------- /src/components/search/SearchNotMatch.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SearchNotMatch = () => { 4 | return ( 5 |
6 | No results match that query 7 |
8 | ); 9 | }; 10 | 11 | export default SearchNotMatch; 12 | -------------------------------------------------------------------------------- /src/lib/config/cloudinary.ts: -------------------------------------------------------------------------------- 1 | // cloudinaryConfig.js 2 | const cloudinary = require("cloudinary").v2; 3 | 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUDINARY_NAME, 6 | api_key: process.env.CLOUDINARY_API_KEY, 7 | api_secret: process.env.CLOUDINARY_API_SECRET, 8 | }); 9 | 10 | export default cloudinary; 11 | -------------------------------------------------------------------------------- /src/app/(pages)/new/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Editor from "@/components/posts/Editor"; 3 | 4 | const Page = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default Page; 13 | -------------------------------------------------------------------------------- /src/app/(pages)/new/layout.tsx: -------------------------------------------------------------------------------- 1 | import Loading from "@/components/Loading"; 2 | import React, { Suspense } from "react"; 3 | 4 | const NewLayout = ({ children }: { children: React.ReactNode }) => { 5 | return ( 6 | <> 7 | }>{children} 8 | 9 | ); 10 | }; 11 | 12 | export default NewLayout; 13 | -------------------------------------------------------------------------------- /src/hooks/reduxHooks.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import type { RootState, AppDispatch } from "@/redux/store"; 3 | import type { TypedUseSelectorHook } from "react-redux/es/types"; 4 | 5 | export const useAppDispatch: () => AppDispatch = useDispatch; 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /src/utils/uploadImageToCloudinary.ts: -------------------------------------------------------------------------------- 1 | import cloudinary from "../lib/config/cloudinary"; 2 | 3 | export const uploadImageToCloudinary = async (file: string, folder: string) => { 4 | try { 5 | return await cloudinary.uploader.upload(file, { 6 | folder: folder, 7 | }); 8 | } catch (error: any) { 9 | return new Error(error.message); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/deleteFileFromCloudinary.ts: -------------------------------------------------------------------------------- 1 | import cloudinary from "../lib/config/cloudinary"; 2 | 3 | export const deleteFileFromCloudinary = async ( 4 | publicId: string, 5 | folder: string 6 | ) => { 7 | try { 8 | return await cloudinary.uploader.destroy(`blog/${folder}/${publicId}`); 9 | } catch (error: any) { 10 | return new Error(error.message); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default function GlobalError({ 4 | error, 5 | reset, 6 | }: { 7 | error: Error; 8 | reset: () => void; 9 | }) { 10 | return ( 11 | 12 | 13 |

Something went wrong!

14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /editorjs.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@editorjs/header"; 2 | declare module "@editorjs/table"; 3 | declare module "@editorjs/embed"; 4 | declare module "@editorjs/list"; 5 | declare module "@editorjs/code"; 6 | declare module "@editorjs/link"; 7 | declare module "@editorjs/inline-code"; 8 | declare module "@editorjs/quote"; 9 | declare module "@editorjs/raw"; 10 | declare module "@editorjs/checklist"; 11 | -------------------------------------------------------------------------------- /src/lib/validation/signInSchema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const signInSchema = z.object({ 4 | email: z 5 | .string() 6 | .nonempty("Email is required!") 7 | .email("Invalid email address!"), 8 | password: z 9 | .string() 10 | .nonempty("Password is required!") 11 | .min(6, "Password must be at least 6 characters"), 12 | }); 13 | 14 | export type SignInSchemaType = z.infer; 15 | -------------------------------------------------------------------------------- /src/utils/convertImageTobase64.ts: -------------------------------------------------------------------------------- 1 | export const convertImageToBase64 = (file: any) => { 2 | if (!file) return; 3 | 4 | return new Promise((resolve, reject) => { 5 | const fileReader = new FileReader(); 6 | fileReader.readAsDataURL(file); 7 | fileReader.onload = () => { 8 | resolve(fileReader.result as string); 9 | }; 10 | fileReader.onerror = (error) => { 11 | reject(error); 12 | }; 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/redux/userSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | moreInfo: false, 5 | }; 6 | 7 | const userSlice = createSlice({ 8 | name: "user", 9 | initialState, 10 | reducers: { 11 | setMoreInfo: (state, action) => { 12 | state.moreInfo = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setMoreInfo } = userSlice.actions; 18 | export default userSlice.reducer; 19 | -------------------------------------------------------------------------------- /src/utils/getDataFromToken.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { NextRequest } from "next/server"; 3 | 4 | export const getDataFromToken = (req: NextRequest) => { 5 | try { 6 | const token = req.cookies.get("token")?.value || ""; 7 | const decodedToken: any = jwt.verify(token, process.env.JWT_SECRET!); 8 | 9 | return decodedToken.id; 10 | } catch (error: any) { 11 | return new Error(error.message); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/redux/commonSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | progress: 0, 5 | }; 6 | 7 | const commonSlice = createSlice({ 8 | name: "common", 9 | initialState, 10 | reducers: { 11 | setProgress: (state, action) => { 12 | state.progress = action.payload; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { setProgress } = commonSlice.actions; 18 | export default commonSlice.reducer; 19 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-var 5 | var cachedPrisma: PrismaClient; 6 | } 7 | 8 | let prisma: PrismaClient; 9 | if (process.env.NODE_ENV === "production") { 10 | prisma = new PrismaClient(); 11 | } else { 12 | if (!global.cachedPrisma) { 13 | global.cachedPrisma = new PrismaClient(); 14 | } 15 | prisma = global.cachedPrisma; 16 | } 17 | 18 | export default prisma; 19 | -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { LucideProps } from "lucide-react"; 3 | import dynamicIconImports from "lucide-react/dynamicIconImports"; 4 | 5 | interface IconProps extends LucideProps { 6 | name: keyof typeof dynamicIconImports; 7 | } 8 | 9 | const Icon = ({ name, ...props }: IconProps) => { 10 | const LucideIcon = dynamic(dynamicIconImports[name]); 11 | 12 | return ; 13 | }; 14 | 15 | export default Icon; 16 | -------------------------------------------------------------------------------- /src/app/api/tags/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET(req: NextRequest) { 5 | try { 6 | const tags = await prisma.tag.findMany({ 7 | include: { Post: { select: { _count: true } } }, 8 | }); 9 | 10 | return NextResponse.json(tags, { status: 200 }); 11 | } catch (error: any) { 12 | return NextResponse.json({ message: error.message }, { status: 500 }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.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 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .vercel 39 | -------------------------------------------------------------------------------- /src/app/api/auth/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export async function GET(req: NextRequest) { 4 | try { 5 | const response = NextResponse.json({ 6 | success: true, 7 | message: "Log out successful", 8 | }); 9 | response.cookies.set("token", "", { 10 | httpOnly: true, 11 | secure: true, 12 | expires: new Date(0), 13 | }); 14 | 15 | return response; 16 | } catch (error: any) { 17 | return NextResponse.json({ error: error.message }, { status: 500 }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useQueryString.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from "next/navigation"; 2 | import { useCallback } from "react"; 3 | 4 | const useQueryString = () => { 5 | const searchParams = useSearchParams()!; 6 | 7 | const createQueryString = useCallback( 8 | (name: string, value: string) => { 9 | const params = new URLSearchParams(searchParams as any); 10 | params.set(name, value); 11 | return params.toString(); 12 | }, 13 | [searchParams] 14 | ); 15 | 16 | return { createQueryString }; 17 | }; 18 | 19 | export default useQueryString; 20 | -------------------------------------------------------------------------------- /src/app/(pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | import GetCurrentUser from "@/components/GetCurrentUser"; 2 | import Loading from "@/components/Loading"; 3 | import { cookies } from "next/headers"; 4 | import React, { Suspense } from "react"; 5 | 6 | const HomeLayout = ({ children }: { children: React.ReactNode }) => { 7 | const cookiesList = cookies(); 8 | const token = cookiesList.has("token"); 9 | 10 | return ( 11 | <> 12 | {token && } 13 | }>{children} 14 | 15 | ); 16 | }; 17 | 18 | export default HomeLayout; 19 | -------------------------------------------------------------------------------- /src/redux/editorSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | postContent: "", 5 | postTitle: "", 6 | }; 7 | 8 | const editorSlice = createSlice({ 9 | name: "editor", 10 | initialState, 11 | reducers: { 12 | setPostContent: (state, action) => { 13 | state.postContent = action.payload; 14 | }, 15 | 16 | setPostTitle: (state, action) => { 17 | state.postTitle = action.payload; 18 | }, 19 | }, 20 | }); 21 | 22 | export const { setPostContent, setPostTitle } = editorSlice.actions; 23 | export default editorSlice.reducer; 24 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import authSlice from "./authSlice"; 3 | import editorSlice from "./editorSlice"; 4 | import userSlice from "./userSlice"; 5 | import dashboardSlice from "./dashboardSlice"; 6 | import commonSlice from "./commonSlice"; 7 | 8 | export const store = configureStore({ 9 | reducer: { 10 | auth: authSlice, 11 | user: userSlice, 12 | editor: editorSlice, 13 | dashboard: dashboardSlice, 14 | common: commonSlice, 15 | }, 16 | }); 17 | 18 | export type RootState = ReturnType; 19 | export type AppDispatch = typeof store.dispatch; 20 | -------------------------------------------------------------------------------- /src/components/search/SearchPostLoop.tsx: -------------------------------------------------------------------------------- 1 | import { TPost } from "@/lib/types"; 2 | import React from "react"; 3 | import SearchNotMatch from "./SearchNotMatch"; 4 | import PostCard from "../posts/PostCard"; 5 | 6 | type TSearchPostLoopProp = { 7 | searchPost: TPost[]; 8 | }; 9 | 10 | const SearchPostLoop = ({ searchPost }: TSearchPostLoopProp) => { 11 | return ( 12 | <> 13 | {searchPost && searchPost.length > 0 ? ( 14 | searchPost.map((post: TPost) => ) 15 | ) : ( 16 | 17 | )} 18 | 19 | ); 20 | }; 21 | 22 | export default SearchPostLoop; 23 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export function middleware(request: NextRequest) { 4 | const path = request.nextUrl.pathname; 5 | 6 | const token = request.cookies.get("token")?.value || ""; 7 | 8 | if ( 9 | !token && 10 | (path === "/new" || 11 | path.startsWith("/dashboard") || 12 | path.startsWith("/setting")) 13 | ) { 14 | return NextResponse.redirect(new URL("/signin", request.nextUrl)); 15 | } 16 | } 17 | 18 | export const config = { 19 | matcher: [ 20 | "/", 21 | "/signin", 22 | "/signup", 23 | "/new", 24 | "/dashboard/:path*", 25 | "/setting/:path*", 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Analytics.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | import React from "react"; 3 | 4 | const Analytics = () => { 5 | return ( 6 | <> 7 | 20 | 21 | ); 22 | }; 23 | 24 | export default Analytics; 25 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { nextui } from "@nextui-org/react"; 2 | import type { Config } from "tailwindcss"; 3 | 4 | const config: Config = { 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", 10 | ], 11 | theme: { 12 | extend: { 13 | gridTemplateColumns: { 14 | lg: "240px 2fr 1fr", 15 | md: "240px 1fr", 16 | sm: "1fr", 17 | }, 18 | }, 19 | }, 20 | darkMode: "class", 21 | plugins: [nextui(), require("@tailwindcss/typography")], 22 | }; 23 | export default config; 24 | -------------------------------------------------------------------------------- /src/lib/validation/editProfileSchema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const editProfileData = z.object({ 4 | name: z 5 | .string() 6 | .nonempty("Name can't be empty") 7 | .min(2, "Name is too sort (minimum is 2 character)"), 8 | email: z 9 | .string() 10 | .nonempty("Email is required!") 11 | .email("Invalid email address!"), 12 | username: z 13 | .string() 14 | .nonempty("Username can't be empty") 15 | .min(2, "Username is too sort (minimum is 2 character)"), 16 | file: z.any(), 17 | bio: z.union([z.string(), z.undefined()]), 18 | site: z.union([z.string(), z.undefined()]), 19 | }); 20 | 21 | export type TEditProfileType = z.infer; 22 | -------------------------------------------------------------------------------- /src/redux/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { TUser } from "@/lib/types"; 2 | import { createSlice } from "@reduxjs/toolkit"; 3 | 4 | type TInitialState = { 5 | user: TUser | null; 6 | authStatus: boolean; 7 | }; 8 | 9 | const initialState: TInitialState = { 10 | authStatus: false, 11 | user: null, 12 | }; 13 | 14 | const authSlice = createSlice({ 15 | name: "auth", 16 | initialState, 17 | reducers: { 18 | setAuthStatus: (state, action) => { 19 | state.authStatus = action.payload; 20 | }, 21 | 22 | setUser: (state, action) => { 23 | state.user = action.payload; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { setAuthStatus, setUser } = authSlice.actions; 29 | export default authSlice.reducer; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/app/api/tags/addTagInPost/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET(req: NextRequest) { 5 | try { 6 | const query = req.nextUrl.searchParams.get("q"); 7 | 8 | if (query) { 9 | const queryTags = await prisma.tag.findMany({ 10 | where: { value: { contains: query, mode: "insensitive" } }, 11 | take: 20, 12 | }); 13 | 14 | return NextResponse.json(queryTags, { status: 200 }); 15 | } else { 16 | const tags = await prisma.tag.findMany({ take: 20 }); 17 | 18 | return NextResponse.json(tags, { status: 200 }); 19 | } 20 | } catch (error: any) { 21 | return NextResponse.json({ message: error.message }, { status: 500 }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/redux/dashboardSlice.ts: -------------------------------------------------------------------------------- 1 | import { TDashboard } from "@/lib/types"; 2 | import { createSlice } from "@reduxjs/toolkit"; 3 | 4 | type TDashboardSliceProp = { 5 | dashboardData: TDashboard | null; 6 | filterPost: string; 7 | }; 8 | 9 | const initialState: TDashboardSliceProp = { 10 | dashboardData: null, 11 | filterPost: "", 12 | }; 13 | 14 | const dashboardSlice = createSlice({ 15 | name: "dashboard", 16 | initialState, 17 | reducers: { 18 | setDashBoardData: (state, action) => { 19 | state.dashboardData = action.payload; 20 | }, 21 | 22 | setFilterPost: (state, action) => { 23 | state.filterPost = action.payload; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { setDashBoardData, setFilterPost } = dashboardSlice.actions; 29 | export default dashboardSlice.reducer; 30 | -------------------------------------------------------------------------------- /src/lib/validation/signUpSchema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const signUpSchema = z.object({ 4 | name: z 5 | .string() 6 | .nonempty("Name is required") 7 | .min(2, "Name must me at least 2 characters"), 8 | username: z 9 | .string() 10 | .nonempty("Username is required") 11 | .min(2, "username must me at least 2 characters") 12 | .regex( 13 | /^[a-zA-Z0-9_]+$/, 14 | "Username can only contain alphanumeric and underscores" 15 | ), 16 | email: z 17 | .string() 18 | .nonempty("Email is required") 19 | .email("Invalid email address"), 20 | password: z 21 | .string() 22 | .nonempty("Password is required") 23 | .min(6, "Password must be at least 6 characters"), 24 | }); 25 | 26 | export type signUpSchemaType = z.infer; 27 | -------------------------------------------------------------------------------- /src/components/GetCurrentUser.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAppDispatch } from "@/hooks/reduxHooks"; 4 | import { setAuthStatus, setUser } from "@/redux/authSlice"; 5 | import { useQuery } from "@tanstack/react-query"; 6 | import axios from "axios"; 7 | 8 | const GetCurrentUser = () => { 9 | const dispatch = useAppDispatch(); 10 | 11 | useQuery(["me"], { 12 | queryFn: async () => { 13 | try { 14 | const { data } = await axios.get("/api/users/me"); 15 | return data; 16 | } catch (error) { 17 | console.log(error); 18 | } 19 | }, 20 | retry: 2, 21 | refetchOnWindowFocus: false, 22 | onSuccess: (data) => { 23 | dispatch(setUser(data)); 24 | dispatch(setAuthStatus(true)); 25 | }, 26 | }); 27 | 28 | return null; 29 | }; 30 | 31 | export default GetCurrentUser; 32 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Providers } from "@/components/Providers"; 2 | import "./globals.css"; 3 | import type { Metadata } from "next"; 4 | import Analytics from "@/components/Analytics"; 5 | import { Inter } from "next/font/google"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Next blog | Home", 9 | description: "A Next.js blog app where user can read and write blog posts", 10 | }; 11 | 12 | // If loading a variable font, you don't need to specify the font weight 13 | const inter = Inter({ 14 | subsets: ["latin"], 15 | display: "swap", 16 | }); 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: { 21 | children: React.ReactNode; 22 | }) { 23 | return ( 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/(pages)/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Loading from "@/components/Loading"; 4 | import Count from "@/components/dashboard/Count"; 5 | import DashboardFilter from "@/components/dashboard/Filter"; 6 | 7 | import { useParams } from "next/navigation"; 8 | import React, { Suspense } from "react"; 9 | 10 | const DashboardLayout = ({ children }: { children: React.ReactNode }) => { 11 | const params = useParams(); 12 | 13 | return ( 14 |
15 |

16 | Dashboard {params.filter ? ">>" + params.filter : null} 17 |

18 | 19 |
20 | 21 | }>{children} 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default DashboardLayout; 28 | -------------------------------------------------------------------------------- /src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { NextUIProvider } from "@nextui-org/react"; 4 | 5 | import Navbar from "./navbar/Navbar"; 6 | 7 | import { Provider } from "react-redux"; 8 | import { store } from "@/redux/store"; 9 | 10 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 11 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 12 | 13 | import { Toaster } from "react-hot-toast"; 14 | import { useState } from "react"; 15 | 16 | export function Providers({ children }: { children: React.ReactNode }) { 17 | const [queryClient] = useState(new QueryClient()); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Muhammad tehseen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/lib/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "next blog", 3 | description: "A Next.js blog app where users can read and write blog posts", 4 | url: "https://next-blog-tehseen01.vercel.app", 5 | ogImage: "https://tx.shadcn.com/og.jpg", 6 | links: { 7 | twitter: "https://twitter.com/tehseen01", 8 | github: "https://github.com/tehseen01/nextjs-blog", 9 | }, 10 | }; 11 | 12 | // export const metadata = { 13 | // title: { 14 | // default: siteConfig.name, 15 | // template: `%s | ${siteConfig.name}`, 16 | // }, 17 | // description: siteConfig.description, 18 | // keywords: [ 19 | // "Next.js", 20 | // "React", 21 | // "Tailwind CSS", 22 | // "Server Components", 23 | // "Radix UI", 24 | // ], 25 | // authors: [ 26 | // { 27 | // name: "tehseen", 28 | // url: "https://tehseen-site.vercel.app", 29 | // }, 30 | // ], 31 | // creator: "tehseen", 32 | // openGraph: { 33 | // type: "website", 34 | // locale: "en_US", 35 | // url: siteConfig.url, 36 | // title: siteConfig.name, 37 | // description: siteConfig.description, 38 | // siteName: siteConfig.name, 39 | // }, 40 | // }; 41 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function formatTime(timeDifference: Date) { 2 | const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); 3 | 4 | const milliseconds = new Date(timeDifference).getTime(); 5 | 6 | const relativeTime = Date.now() - milliseconds; 7 | 8 | const seconds = Math.round(relativeTime / 1000); 9 | const minutes = Math.round(seconds / 60); 10 | const hours = Math.round(minutes / 60); 11 | const days = Math.round(hours / 24); 12 | const weeks = Math.round(days / 7); 13 | const months = Math.round(days / 30); 14 | const years = Math.round(days / 365); 15 | 16 | if (seconds < 60) { 17 | return rtf.format(-seconds, "second"); 18 | } else if (minutes < 60) { 19 | return rtf.format(-minutes, "minute"); 20 | } else if (hours < 24) { 21 | return rtf.format(-hours, "hour"); 22 | } else if (days < 7) { 23 | return rtf.format(-days, "day"); 24 | } else if (weeks < 4) { 25 | return rtf.format(-weeks, "week"); 26 | } else if (months < 12) { 27 | return rtf.format(-months, "month"); 28 | } else { 29 | return rtf.format(-years, "year"); 30 | } 31 | } 32 | 33 | export function formatDate(date: any) { 34 | const newDate = new Date(date).toLocaleDateString(); 35 | return newDate; 36 | } 37 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/search/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import useQueryString from "@/hooks/useQueryString"; 2 | import { useRouter } from "next/navigation"; 3 | import React, { useState } from "react"; 4 | import Icon from "../Icon"; 5 | import { Input } from "@nextui-org/react"; 6 | 7 | const SearchInput = () => { 8 | const router = useRouter(); 9 | const { createQueryString } = useQueryString(); 10 | 11 | const [searchInput, setSearchInput] = useState(""); 12 | 13 | return ( 14 | <> 15 |
{ 18 | e.preventDefault(); 19 | router.push(`/search?${createQueryString("q", searchInput)}`); 20 | setSearchInput(""); 21 | }} 22 | className="relative" 23 | > 24 | setSearchInput(e.target.value)} 35 | size="sm" 36 | startContent={ 37 | 40 | } 41 | type="search" 42 | /> 43 |
44 | 45 | ); 46 | }; 47 | 48 | export default SearchInput; 49 | -------------------------------------------------------------------------------- /src/app/api/users/[username]/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET( 5 | req: NextRequest, 6 | { params }: { params: { username: string } } 7 | ) { 8 | try { 9 | const user = await prisma.user.findFirst({ 10 | where: { username: params.username }, 11 | select: { 12 | id: true, 13 | name: true, 14 | username: true, 15 | bio: true, 16 | avatar: true, 17 | email: true, 18 | createdAt: true, 19 | updatedAt: true, 20 | followerIDs: true, 21 | followingIDs: true, 22 | comment: true, 23 | followingTags: true, 24 | posts: { 25 | orderBy: { 26 | createdAt: "desc", 27 | }, 28 | where: { NOT: { type: "DRAFT" } }, 29 | include: { 30 | _count: { select: { comments: true } }, 31 | saved: true, 32 | author: { 33 | select: { 34 | id: true, 35 | name: true, 36 | username: true, 37 | avatar: true, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }); 44 | 45 | if (!user) { 46 | return NextResponse.json({ message: "User not found" }, { status: 404 }); 47 | } 48 | 49 | return NextResponse.json(user, { status: 200 }); 50 | } catch (error: any) { 51 | return NextResponse.json({ message: error.message }, { status: 500 }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/comments/CommentCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, CardHeader } from "@nextui-org/react"; 2 | 3 | import React from "react"; 4 | 5 | import clsx from "clsx"; 6 | import moment from "moment"; 7 | 8 | import { TCommentReplyOption } from "@/lib/types"; 9 | 10 | import Icon from "../Icon"; 11 | import CommentOption from "./CommentOption"; 12 | 13 | import { useAppSelector } from "@/hooks/reduxHooks"; 14 | 15 | const CommentCard = ({ data, type, postPath }: TCommentReplyOption) => { 16 | const { user } = useAppSelector((state) => state.auth); 17 | 18 | return ( 19 | 27 | 28 |
29 |
{data.author.username}
{" "} 30 | 31 | 32 | {moment(data.updatedAt, moment.ISO_8601).format("Do MMM")} 33 | 34 |
35 | {/* ===DROPDOWN FOR MORE OPTION IN COMMENT=== */} 36 | {type === "comment" ? ( 37 | 38 | ) : ( 39 | 40 | )} 41 |
42 | {data.content} 43 |
44 | ); 45 | }; 46 | 47 | export default CommentCard; 48 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@nextui-org/react"; 4 | import Link from "next/link"; 5 | 6 | export default function NotFound() { 7 | return ( 8 |
9 |
10 |
11 |
12 | 18 | 24 | 25 |
26 |
27 |

28 | 404 - Page not found 29 |

30 |

31 | The page you are looking for doesn't exist or
32 | has been removed. 33 |

34 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/api/auth/login/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import bcrypt from "bcryptjs"; 4 | import jwt from "jsonwebtoken"; 5 | 6 | import prisma from "@/lib/db"; 7 | 8 | export async function POST(req: NextRequest) { 9 | try { 10 | const reqBody = await req.json(); 11 | const { email, password } = reqBody; 12 | 13 | const user = await prisma.user.findUnique({ where: { email: email } }); 14 | 15 | if (!user) { 16 | return NextResponse.json( 17 | { success: false, message: "User not found!" }, 18 | { status: 401 } 19 | ); 20 | } 21 | 22 | const validPassword = await bcrypt.compare(password, user.password); 23 | if (!validPassword) { 24 | return NextResponse.json( 25 | { success: false, message: "Invalid password" }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | const tokenData = { 31 | id: user.id, 32 | username: user.username, 33 | name: user.name, 34 | email: user.email, 35 | }; 36 | 37 | const token = await jwt.sign(tokenData, process.env.JWT_SECRET!, { 38 | expiresIn: "30 days", 39 | }); 40 | 41 | const options = { 42 | httpOnly: true, 43 | secure: true, 44 | expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), 45 | }; 46 | 47 | const response = NextResponse.json( 48 | { success: true, message: "Login successful" }, 49 | { status: 200 } 50 | ); 51 | response.cookies.set("token", token, options); 52 | 53 | return response; 54 | } catch (error: any) { 55 | return NextResponse.json({ error: error.message }, { status: 500 }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import Icon from "./Icon"; 5 | 6 | const Footer = () => { 7 | return ( 8 |
9 |
10 | 11 | © 2023{" "} 12 | 16 | Tehseen 17 | 18 | . All Rights Reserved. 19 | 20 |
    21 |
  • 22 | 23 | 24 | 25 |
  • 26 |
  • 27 | 28 | 29 | 30 |
  • 31 |
  • 32 | 36 | 37 | 38 |
  • 39 |
  • 40 | 41 | 42 | 43 |
  • 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default Footer; 51 | -------------------------------------------------------------------------------- /src/app/api/auth/signup/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import jwt from "jsonwebtoken"; 4 | import bcryptjs from "bcryptjs"; 5 | import prisma from "@/lib/db"; 6 | 7 | export async function POST(req: NextRequest, res: NextResponse) { 8 | try { 9 | const bodyData = await req.json(); 10 | const { name, username, email, password } = bodyData; 11 | 12 | const existingUser = await prisma.user.findUnique({ 13 | where: { 14 | email: email, 15 | }, 16 | }); 17 | 18 | if (existingUser) { 19 | return NextResponse.json( 20 | { success: false, message: "User already exist!" }, 21 | { status: 400 } 22 | ); 23 | } 24 | 25 | const hashedPassword = await bcryptjs.hash(password, 10); 26 | 27 | const newUser = await prisma.user.create({ 28 | data: { 29 | name, 30 | username, 31 | email, 32 | password: hashedPassword, 33 | }, 34 | }); 35 | 36 | const tokenData = { 37 | id: newUser.id, 38 | name: newUser.name, 39 | username: newUser.username, 40 | email: newUser.email, 41 | }; 42 | 43 | const token = await jwt.sign(tokenData, process.env.JWT_SECRET!, { 44 | expiresIn: "30 days", 45 | }); 46 | 47 | const options = { 48 | httpOnly: true, 49 | secure: true, 50 | expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), 51 | }; 52 | 53 | const response = NextResponse.json( 54 | { 55 | success: true, 56 | message: "Account created successfully", 57 | }, 58 | { status: 200 } 59 | ); 60 | response.cookies.set("token", token, options); 61 | 62 | return response; 63 | } catch (error: any) { 64 | return NextResponse.json({ error: error.message }, { status: 500 }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/api/users/follow/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { getDataFromToken } from "@/utils/getDataFromToken"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | 5 | export async function POST(req: NextRequest, res: NextResponse) { 6 | try { 7 | const { userId } = await req.json(); 8 | const currentUserId = await getDataFromToken(req); 9 | 10 | const currentUser = await prisma.user.findUnique({ 11 | where: { id: currentUserId }, 12 | }); 13 | 14 | if (!currentUser) { 15 | return NextResponse.json( 16 | { message: "Current user not found" }, 17 | { status: 404 } 18 | ); 19 | } 20 | 21 | if (currentUser.followingIDs.includes(userId)) { 22 | // UnFollow 23 | await prisma.user.update({ 24 | where: { id: currentUserId }, 25 | data: { following: { disconnect: { id: userId } } }, 26 | }); 27 | 28 | await prisma.user.update({ 29 | where: { id: userId }, 30 | data: { follower: { disconnect: { id: currentUserId } } }, 31 | }); 32 | 33 | return NextResponse.json( 34 | { success: true, message: "User unFollowed successfully" }, 35 | { status: 200 } 36 | ); 37 | } else { 38 | // Follow 39 | await prisma.user.update({ 40 | where: { id: currentUserId }, 41 | data: { following: { connect: { id: userId } } }, 42 | }); 43 | 44 | await prisma.user.update({ 45 | where: { id: userId }, 46 | data: { follower: { connect: { id: currentUserId } } }, 47 | }); 48 | } 49 | 50 | return NextResponse.json( 51 | { success: true, message: "User followed successfully" }, 52 | { status: 200 } 53 | ); 54 | } catch (error: any) { 55 | return NextResponse.json({ message: error.message }, { status: 500 }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/AuthModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | Button, 5 | Modal, 6 | ModalBody, 7 | ModalContent, 8 | ModalFooter, 9 | ModalHeader, 10 | } from "@nextui-org/react"; 11 | 12 | import Link from "next/link"; 13 | 14 | const AuthModal = ({ 15 | isOpen, 16 | onOpenChange, 17 | }: { 18 | isOpen: boolean; 19 | onOpenChange: () => void; 20 | }) => { 21 | return ( 22 | <> 23 | 24 | 25 | {(onClose) => ( 26 | <> 27 | 28 | Log in to continue 29 | 30 | 31 |

32 | We're a place where coders share, stay up-to-date and 33 | grow their careers. 34 |

35 |
36 | 37 | 48 | 58 | 59 | 60 | )} 61 |
62 |
63 | 64 | ); 65 | }; 66 | 67 | export default AuthModal; 68 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --editor-content-h: 56px; 7 | --editor-title-h: 80px; 8 | } 9 | 10 | @layer components { 11 | .mdx-h1 { 12 | @apply mt-2 scroll-m-20 lg:text-5xl md:text-4xl text-3xl sm:font-extrabold font-semibold tracking-tight; 13 | } 14 | .prose-h2 { 15 | @apply text-3xl font-semibold tracking-tight my-3; 16 | } 17 | .mdx-h3 { 18 | @apply scroll-m-20 text-2xl font-semibold tracking-tight; 19 | } 20 | .mdx-h4 { 21 | @apply scroll-m-20 text-xl font-semibold tracking-tight; 22 | } 23 | .mdx-h5 { 24 | @apply scroll-m-20 text-lg font-semibold tracking-tight; 25 | } 26 | .mdx-h6 { 27 | @apply scroll-m-20 text-base font-semibold tracking-tight; 28 | } 29 | .mdx-a { 30 | @apply font-medium underline text-primary hover:text-primary-300; 31 | } 32 | .mdx-p { 33 | @apply md:text-xl text-lg text-black mb-3; 34 | } 35 | .mdx-ul { 36 | @apply my-6 ml-6 list-disc; 37 | } 38 | .mdx-ol { 39 | @apply my-6 ml-6 list-decimal; 40 | } 41 | .mdx-li { 42 | @apply mt-2; 43 | } 44 | .mdx-blockquote { 45 | @apply mt-6 border-l-2 pl-6 italic; 46 | } 47 | 48 | .mdx-img { 49 | @apply w-full; 50 | } 51 | .mdx-hr { 52 | @apply my-4 md:my-8; 53 | } 54 | 55 | .mdx-div { 56 | @apply my-6 w-full overflow-y-auto; 57 | } 58 | .mdx-table { 59 | @apply w-full; 60 | } 61 | .mdx-tr { 62 | @apply m-0 border-t p-0; 63 | } 64 | .mdx-th { 65 | @apply border px-4 py-2 text-left font-bold; 66 | } 67 | .mdx-td { 68 | @apply border px-4 py-2 text-left; 69 | } 70 | .mdx-pre { 71 | @apply mb-4 mt-6 overflow-x-auto rounded-lg border bg-black py-4; 72 | } 73 | .mdx-code { 74 | @apply relative rounded border px-[0.3rem] py-[0.2rem] font-mono text-sm; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/(pages)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import DashboardPosts from "@/components/dashboard/Posts"; 4 | import { TDashboard } from "@/lib/types"; 5 | 6 | import { useAppDispatch, useAppSelector } from "@/hooks/reduxHooks"; 7 | import { setDashBoardData } from "@/redux/dashboardSlice"; 8 | 9 | import { useQuery } from "@tanstack/react-query"; 10 | import axios from "axios"; 11 | import { useState } from "react"; 12 | 13 | const Dashboard = () => { 14 | const [data, setData] = useState(null); 15 | 16 | const dispatch = useAppDispatch(); 17 | const { user } = useAppSelector((state) => state.auth); 18 | const { filterPost } = useAppSelector((state) => state.dashboard); 19 | 20 | const postData = useQuery(["dashboard", user?.username], { 21 | queryFn: async (): Promise => { 22 | const { data } = await axios.get(`/api/dashboard`); 23 | return data; 24 | }, 25 | onSuccess: (fetchedData) => { 26 | setData(fetchedData); 27 | dispatch(setDashBoardData(fetchedData)); 28 | }, 29 | }); 30 | 31 | const postDataWithFilter = useQuery(["dashboard", filterPost], { 32 | queryFn: async (): Promise => { 33 | const { data } = await axios.get(`/api/dashboard?filter=${filterPost}`); 34 | return data; 35 | }, 36 | enabled: !!filterPost, 37 | onSuccess: (fetchedData) => { 38 | setData(fetchedData); 39 | dispatch(setDashBoardData(fetchedData)); 40 | }, 41 | }); 42 | 43 | if (postData.isLoading) { 44 | return ( 45 |
46 | Loading... 47 |
48 | ); 49 | } 50 | 51 | return ( 52 |
53 | {data !== null && Object.entries(data).length > 0 ? ( 54 | 55 | ) : null} 56 |
57 | ); 58 | }; 59 | 60 | export default Dashboard; 61 | -------------------------------------------------------------------------------- /src/components/dashboard/Count.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAppSelector } from "@/hooks/reduxHooks"; 4 | 5 | import { useParams } from "next/navigation"; 6 | import React from "react"; 7 | 8 | const Count = () => { 9 | const params = useParams(); 10 | const { dashboardData } = useAppSelector((state) => state.dashboard); 11 | 12 | return ( 13 | <> 14 | {dashboardData !== null ? ( 15 | <> 16 | {!params.filter ? ( 17 |
18 |
19 | 0 20 |

Total post reactions

21 |
22 | 23 |
24 | 25 | {dashboardData.posts.reduce( 26 | (acu, count) => acu + count.views, 27 | 0 28 | )} 29 | 30 |

Total post views

31 |
32 |
33 | 0 34 |

Total credits

35 |
36 |
37 | 38 | {dashboardData._count.comment + dashboardData._count.replies} 39 | 40 |

Total post comments

41 |
42 |
43 | ) : null} 44 | 45 | ) : null} 46 | 47 | ); 48 | }; 49 | 50 | export default Count; 51 | -------------------------------------------------------------------------------- /src/app/(pages)/[userId]/[postId]/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; 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 |
20 |
21 |
22 | 28 | 34 | 40 | 41 |
42 |
43 |

44 | 500 - Server error 45 |

46 |

47 | Oops something went wrong. Try to{" "} 48 | {" "} 54 | this page or
feel free to contact us if the problem presists. 55 |

56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/app/api/dashboard/[filter]/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { getDataFromToken } from "@/utils/getDataFromToken"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | 5 | export async function GET( 6 | req: NextRequest, 7 | { params }: { params: { filter: string } } 8 | ) { 9 | try { 10 | const filter = params.filter; 11 | 12 | const userID = await getDataFromToken(req); 13 | if (!userID) { 14 | return NextResponse.json( 15 | { success: false, message: "You are not authorize" }, 16 | { status: 401 } 17 | ); 18 | } 19 | 20 | let results; 21 | 22 | if (filter === "followers") { 23 | results = await prisma.user.findUnique({ 24 | where: { id: userID }, 25 | select: { 26 | id: true, 27 | username: true, 28 | name: true, 29 | avatar: true, 30 | follower: { 31 | select: { 32 | id: true, 33 | username: true, 34 | name: true, 35 | avatar: true, 36 | bio: true, 37 | }, 38 | }, 39 | }, 40 | }); 41 | } else if (filter === "following_users") { 42 | results = await prisma.user.findUnique({ 43 | where: { id: userID }, 44 | select: { 45 | id: true, 46 | username: true, 47 | name: true, 48 | avatar: true, 49 | following: { 50 | select: { 51 | id: true, 52 | username: true, 53 | name: true, 54 | avatar: true, 55 | bio: true, 56 | }, 57 | }, 58 | }, 59 | }); 60 | } else if (filter === "following_tags") { 61 | results = await prisma.user.findUnique({ 62 | where: { id: userID }, 63 | select: { 64 | id: true, 65 | username: true, 66 | name: true, 67 | avatar: true, 68 | followingTags: true, 69 | }, 70 | }); 71 | } 72 | 73 | return NextResponse.json(results, { status: 200 }); 74 | } catch (error: any) { 75 | return NextResponse.json({ message: error.message }, { status: 500 }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "postinstall": "prisma generate", 8 | "build": "prisma generate && next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@editorjs/checklist": "^1.5.0", 14 | "@editorjs/code": "^2.8.0", 15 | "@editorjs/editorjs": "^2.28.0", 16 | "@editorjs/embed": "^2.5.3", 17 | "@editorjs/header": "^2.7.0", 18 | "@editorjs/inline-code": "^1.4.0", 19 | "@editorjs/link": "^2.5.0", 20 | "@editorjs/list": "^1.8.0", 21 | "@editorjs/quote": "^2.5.0", 22 | "@editorjs/raw": "^2.4.0", 23 | "@editorjs/table": "^2.2.2", 24 | "@hookform/resolvers": "^3.2.0", 25 | "@nextui-org/react": "^2.1.13", 26 | "@prisma/client": "^5.2.0", 27 | "@reduxjs/toolkit": "^1.9.5", 28 | "@tanstack/react-query": "^4.33.0", 29 | "@tanstack/react-query-devtools": "^4.33.0", 30 | "@types/jsonwebtoken": "^9.0.2", 31 | "@types/node": "20.4.10", 32 | "@types/react": "18.2.20", 33 | "@types/react-dom": "18.2.7", 34 | "autoprefixer": "10.4.14", 35 | "axios": "^1.4.0", 36 | "bcryptjs": "^2.4.3", 37 | "cloudinary": "^1.40.0", 38 | "clsx": "^2.0.0", 39 | "date-fns": "^2.30.0", 40 | "editorjs-blocks-react-renderer": "^1.3.0", 41 | "eslint": "8.47.0", 42 | "eslint-config-next": "13.4.13", 43 | "framer-motion": "^10.15.1", 44 | "jsonwebtoken": "^9.0.1", 45 | "lucide-react": "^0.265.0", 46 | "moment": "^2.29.4", 47 | "next": "13.4.13", 48 | "nodemailer": "^6.9.4", 49 | "postcss": "8.4.27", 50 | "react": "18.2.0", 51 | "react-dom": "18.2.0", 52 | "react-hook-form": "^7.45.4", 53 | "react-hot-toast": "^2.4.1", 54 | "react-intersection-observer": "^9.5.2", 55 | "react-redux": "^8.1.2", 56 | "react-syntax-highlighter": "^15.5.0", 57 | "react-textarea-autosize": "^8.5.3", 58 | "react-top-loading-bar": "^2.3.1", 59 | "tailwindcss": "3.3.3", 60 | "typescript": "5.1.6", 61 | "zod": "^3.21.4" 62 | }, 63 | "devDependencies": { 64 | "@tailwindcss/typography": "^0.5.9", 65 | "@types/bcryptjs": "^2.4.2", 66 | "@types/react-syntax-highlighter": "^15.5.7", 67 | "prisma": "^5.2.0", 68 | "ts-node": "^10.9.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/api/comment/reply/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { getDataFromToken } from "@/utils/getDataFromToken"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | 5 | export async function POST(req: NextRequest) { 6 | try { 7 | const { commentId, replyText } = await req.json(); 8 | 9 | if (!commentId || !replyText) { 10 | return NextResponse.json( 11 | { success: false, message: "Invalid data sent." }, 12 | { status: 400 } 13 | ); 14 | } 15 | 16 | const userID = await getDataFromToken(req); 17 | if (!userID) { 18 | return NextResponse.json( 19 | { success: false, message: "Please log in first!" }, 20 | { status: 401 } 21 | ); 22 | } 23 | 24 | const reply = await prisma.reply.create({ 25 | data: { content: replyText, commentId: commentId, authorId: userID }, 26 | }); 27 | 28 | return NextResponse.json( 29 | { success: true, message: "Reply added successfully", reply }, 30 | { status: 201 } 31 | ); 32 | } catch (error: any) { 33 | return NextResponse.json({ message: error.message }, { status: 500 }); 34 | } 35 | } 36 | 37 | export async function DELETE(req: NextRequest) { 38 | try { 39 | const replyId = req.nextUrl.searchParams.get("id"); 40 | if (!replyId) { 41 | return NextResponse.json( 42 | { success: false, message: "Invalid data sent." }, 43 | { status: 400 } 44 | ); 45 | } 46 | 47 | const userID = await getDataFromToken(req); 48 | if (!userID) { 49 | return NextResponse.json( 50 | { success: false, message: "Please log in first!" }, 51 | { status: 401 } 52 | ); 53 | } 54 | 55 | const existReply = await prisma.reply.findUnique({ 56 | where: { id: replyId, authorId: userID }, 57 | }); 58 | if (!existReply) { 59 | return NextResponse.json( 60 | { success: false, message: "Reply not found!" }, 61 | { status: 404 } 62 | ); 63 | } 64 | 65 | const replyToDelete = await prisma.reply.delete({ 66 | where: { id: replyId, authorId: userID }, 67 | }); 68 | 69 | return NextResponse.json( 70 | { success: true, message: "Reply deleted successfully", replyToDelete }, 71 | { status: 200 } 72 | ); 73 | } catch (error: any) { 74 | return NextResponse.json({ message: error.message }, { status: 500 }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/comments/AddReply.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { TUser } from "@/lib/types"; 4 | 5 | import { Avatar, Button, Textarea } from "@nextui-org/react"; 6 | import { useQueryClient } from "@tanstack/react-query"; 7 | 8 | import axios from "axios"; 9 | import Link from "next/link"; 10 | import React from "react"; 11 | 12 | import { useForm, SubmitHandler } from "react-hook-form"; 13 | import toast from "react-hot-toast"; 14 | 15 | type TReplyFormState = { 16 | reply: string; 17 | }; 18 | 19 | type TAddReplyProp = { 20 | user: TUser | null; 21 | commentId: string; 22 | postPath: string; 23 | }; 24 | 25 | const AddReply = ({ user, commentId, postPath }: TAddReplyProp) => { 26 | const { 27 | register, 28 | reset, 29 | formState: { isDirty, isSubmitting, isValid }, 30 | handleSubmit, 31 | } = useForm(); 32 | 33 | const queryClient = useQueryClient(); 34 | 35 | const onSubmitReply: SubmitHandler = async (data) => { 36 | try { 37 | const res = await axios.post("/api/comment/reply", { 38 | replyText: data.reply, 39 | commentId: commentId, 40 | }); 41 | queryClient.invalidateQueries(["comments", postPath]); 42 | toast.success(res.data.message); 43 | console.log(res.data); 44 | reset(); 45 | } catch (error: any) { 46 | if (error.response.data.message) { 47 | toast.error(error.response.data.message); 48 | } else { 49 | toast.error(error.response.statusText); 50 | } 51 | console.log(error); 52 | } 53 | }; 54 | 55 | return ( 56 |
57 | 64 |
65 |