├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── companyaccount.svg ├── congratz.svg ├── cover_bg.png ├── next.svg ├── privateaccount.svg ├── publicaccount.svg └── vercel.svg ├── src ├── app │ ├── (auth) │ │ └── signin │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── link │ │ │ └── route.ts │ │ ├── search │ │ │ └── route.ts │ │ └── user │ │ │ ├── aboutme │ │ │ └── route.ts │ │ │ ├── avatar │ │ │ └── route.ts │ │ │ ├── follow │ │ │ └── route.ts │ │ │ ├── post │ │ │ ├── comment │ │ │ │ ├── commentlike │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── create │ │ │ │ └── route.ts │ │ │ ├── newlike │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ └── username │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── loading.tsx │ ├── newuser │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── page.tsx │ ├── post │ │ └── [postId] │ │ │ └── page.tsx │ ├── profile │ │ └── [userId] │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ └── settings │ │ ├── loading.tsx │ │ └── page.tsx ├── components │ ├── BasicInfoWidget.tsx │ ├── CommentInput.tsx │ ├── CommentLike.tsx │ ├── CommentOuterBox.tsx │ ├── Comments.tsx │ ├── CreatePostOuterBox.tsx │ ├── CreatePostValidator.tsx │ ├── CustomComponents │ │ ├── CommentInputBox.tsx │ │ └── hooks.ts │ ├── Editor.tsx │ ├── EditorOutput.tsx │ ├── QuerryProvider.tsx │ ├── SearchUserInput.tsx │ ├── TopPageNavbar.tsx │ ├── button │ │ ├── CommentButton.tsx │ │ ├── CreatePostExitPost.tsx │ │ ├── ExitProfileBtn.tsx │ │ ├── FollowButton.tsx │ │ ├── LogOutButton.tsx │ │ ├── LoginGoogleBtn.tsx │ │ ├── NewUserButton.tsx │ │ ├── PostLikeBtn.tsx │ │ ├── PostLikeServerside.tsx │ │ ├── SettingBtn.tsx │ │ └── SlideBarResponsiveExitButton.tsx │ ├── feed │ │ ├── CreatePostActivator.tsx │ │ ├── FeedColumn.tsx │ │ ├── FeedPage.tsx │ │ ├── FeedPostsBox.tsx │ │ ├── MyPost.tsx │ │ ├── ProfilePostsColumn.tsx │ │ ├── SuggestUsers.tsx │ │ └── UsersSuggested.tsx │ ├── loaders │ │ └── PostLoader.tsx │ ├── newuserpage │ │ ├── NewUserImageUpload.tsx │ │ ├── ProgressBar.tsx │ │ ├── Progresstitle.tsx │ │ ├── Signup1.tsx │ │ ├── Signup2.tsx │ │ ├── Signup3.tsx │ │ ├── Signup4.tsx │ │ ├── Signup5.tsx │ │ └── cards │ │ │ └── Signupcard.tsx │ ├── renderers │ │ ├── CustomCodeRender.tsx │ │ ├── CustomImageRender.tsx │ │ └── CustomListRenderer.tsx │ ├── slidebar │ │ ├── SearchDropdown.tsx │ │ ├── SearchedUsers.tsx │ │ ├── Slidebar.tsx │ │ └── SlidebarLink.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ └── textarea.tsx ├── config.ts ├── lib │ ├── Apis.ts │ ├── Firebase.tsx │ ├── Functions.tsx │ ├── NewUserFormValidator.tsx │ ├── Prisma.db.ts │ ├── auth.ts │ ├── commentValidator.ts │ ├── redis.ts │ └── utils.ts ├── middleware.ts └── types │ ├── PostLikeValidator.tsx │ ├── db.d.ts │ ├── next-auth.d.ts │ ├── redis.d.ts │ └── types.ts ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project with Shadcn ui [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | ## Best Project To Begineer to learn => 3 | ## Nextsjs 4 | ## Typescript 5 | ## Shadcn ui [https://ui.shadcn.com/] 6 | ## Editorjs [https://editorjs.io/] 7 | ## @mantine/hooks [https://www.npmjs.com/package/@mantine/hooks] 8 | ## useform-hooks 9 | 10 | 11 | 12 | ## Getting Started 13 | 14 | First, run the development server: 15 | 16 | ```bash 17 | npm run dev 18 | # or 19 | yarn dev 20 | # or 21 | pnpm dev 22 | ``` 23 | 24 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 25 | 26 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 27 | 28 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 29 | 30 | ## Learn More 31 | 32 | To learn more about Next.js, take a look at the following resources: 33 | 34 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 35 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 36 | 37 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 38 | 39 | -------------------------------------------------------------------------------- /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": "src/app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images:{ 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'firebasestorage.googleapis.com', 8 | 9 | }, 10 | { 11 | protocol: 'https', 12 | hostname: 'lh3.googleusercontent.com', 13 | 14 | } 15 | ], 16 | }, 17 | 18 | } 19 | 20 | module.exports = nextConfig 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate" 11 | }, 12 | "dependencies": { 13 | "@auth/prisma-adapter": "^1.0.1", 14 | "@editorjs/code": "^2.8.0", 15 | "@editorjs/editorjs": "^2.27.2", 16 | "@editorjs/embed": "^2.5.3", 17 | "@editorjs/header": "^2.7.0", 18 | "@editorjs/image": "^2.8.1", 19 | "@editorjs/inline-code": "^1.4.0", 20 | "@editorjs/link": "^2.5.0", 21 | "@editorjs/list": "^1.8.0", 22 | "@editorjs/table": "^2.2.2", 23 | "@hookform/resolvers": "^3.2.0", 24 | "@mantine/hooks": "^6.0.19", 25 | "@prisma/client": "^5.1.1", 26 | "@radix-ui/react-dialog": "^1.0.4", 27 | "@radix-ui/react-label": "^2.0.2", 28 | "@radix-ui/react-popover": "^1.0.6", 29 | "@radix-ui/react-slot": "^1.0.2", 30 | "@tanstack/react-query": "^4.36.1", 31 | "@types/node": "20.4.7", 32 | "@types/react": "18.2.18", 33 | "@types/react-dom": "18.2.7", 34 | "@upstash/redis": "^1.22.0", 35 | "autoprefixer": "10.4.14", 36 | "axios": "^1.4.0", 37 | "class-variance-authority": "^0.7.0", 38 | "clsx": "^2.0.0", 39 | "editorjs-react-renderer": "^3.5.1", 40 | "encoding": "^0.1.13", 41 | "eslint": "8.46.0", 42 | "eslint-config-next": "^13.4.16", 43 | "firebase": "^10.1.0", 44 | "lodash": "^4.17.21", 45 | "lucide-react": "^0.263.1", 46 | "next": "^13.4.19", 47 | "next-auth": "^4.23.1", 48 | "postcss": "8.4.27", 49 | "prisma": "^5.1.1", 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-icons": "^4.10.1", 55 | "react-textarea-autosize": "^8.5.2", 56 | "server-only": "^0.0.1", 57 | "sonner": "^2.0.1", 58 | "tailwind-merge": "^1.14.0", 59 | "tailwindcss": "3.3.3", 60 | "tailwindcss-animate": "^1.0.6", 61 | "timeago.js": "^4.0.2", 62 | "typescript": "5.1.6", 63 | "zod": "^3.22.1" 64 | }, 65 | "devDependencies": { 66 | "@types/lodash": "^4.17.15" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mongodb" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Account { 11 | id String @id @default(auto()) @map("_id") @db.ObjectId 12 | userId String @db.ObjectId 13 | type String 14 | provider String 15 | providerAccountId String 16 | refresh_token String? 17 | access_token String? 18 | expires_at Int? 19 | token_type String? 20 | scope String? 21 | id_token String? 22 | session_state String? 23 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 24 | 25 | @@unique([provider, providerAccountId]) 26 | } 27 | 28 | model Session { 29 | id String @id @default(auto()) @map("_id") @db.ObjectId 30 | sessionToken String @unique 31 | userId String @db.ObjectId 32 | expires DateTime 33 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 34 | } 35 | 36 | model User { 37 | id String @id @default(auto()) @map("_id") @db.ObjectId 38 | name String? 39 | email String? @unique 40 | emailVerified DateTime? 41 | username String? @unique 42 | image String? 43 | onboardingCompleted Boolean? @default(false) 44 | location String? 45 | Bio String? 46 | accounts Account[] 47 | sessions Session[] 48 | Post Post[] 49 | like Like[] 50 | Comment Comment[] 51 | CommentVote CommentVote[] 52 | followers Follows[] @relation("following") 53 | following Follows[] @relation("follower") 54 | } 55 | 56 | model Post { 57 | id String @id @default(auto()) @map("_id") @db.ObjectId 58 | title String 59 | content Json? 60 | createdAt DateTime @default(now()) 61 | updatedAt DateTime @updatedAt 62 | author User @relation(fields: [authorId], references: [id], onDelete: Cascade) 63 | authorId String @db.ObjectId 64 | comments Comment[] 65 | like Like[] 66 | } 67 | 68 | model Like { 69 | id String @id @default(auto()) @map("_id") @db.ObjectId 70 | user User @relation(fields: [userId], references: [id]) 71 | userId String @db.ObjectId 72 | post Post @relation(fields: [postId], references: [id], onDelete: Cascade) 73 | postId String @db.ObjectId 74 | } 75 | 76 | model Comment { 77 | id String @id @default(auto()) @map("_id") @db.ObjectId 78 | text String 79 | createdAt DateTime @default(now()) 80 | author User @relation(fields: [authorId], references: [id]) 81 | authorId String @db.ObjectId 82 | post Post @relation(fields: [postId], references: [id], onDelete: Cascade) 83 | postId String @db.ObjectId 84 | 85 | replyToId String? @db.ObjectId 86 | replyTo Comment? @relation("ReplyTo", fields: [replyToId], references: [id], onDelete: NoAction, onUpdate: NoAction) 87 | replies Comment[] @relation("ReplyTo") 88 | 89 | like CommentVote[] 90 | commentId String? @db.ObjectId 91 | } 92 | 93 | model CommentVote { 94 | id String @id @default(auto()) @map("_id") @db.ObjectId 95 | 96 | user User @relation(fields: [userId], references: [id]) 97 | userId String @db.ObjectId 98 | comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) 99 | commentId String @db.ObjectId 100 | } 101 | 102 | model Follows { 103 | id String @id @default(auto()) @map("_id") @db.ObjectId 104 | follower User @relation("follower", fields: [followerId], references: [id]) 105 | followerId String @db.ObjectId 106 | following User @relation("following", fields: [followingId], references: [id]) 107 | followingId String @db.ObjectId 108 | 109 | } 110 | 111 | // COMMANDS PRISMA 112 | // 1 . npx prisma db push 113 | // 2. npx prisma generate 114 | -------------------------------------------------------------------------------- /public/companyaccount.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/cover_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taqui-786/project-friendz/fa1e3b9613ed12fcd9b795ea48d0ff217fb6b09b/public/cover_bg.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/publicaccount.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(auth)/signin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | 3 | export const metadata: Metadata = { 4 | title: 'Signin | Signup', 5 | description: 'secure signin with next-auth', 6 | } 7 | export default function SigninLayout({ 8 | children, // will be a page or nested layout 9 | }: { 10 | children: React.ReactNode 11 | }) { 12 | return ( 13 |
14 | 15 | 16 | {children} 17 |
18 | ) 19 | } -------------------------------------------------------------------------------- /src/app/(auth)/signin/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return (<> 3 |
4 | 5 |
6 | ) 7 | } -------------------------------------------------------------------------------- /src/app/(auth)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import LoginGoogleBtn from "@/components/button/LoginGoogleBtn"; 2 | import LoginButton from "@/components/button/LoginGoogleBtn"; 3 | import { FaUserFriends } from "react-icons/fa"; 4 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 5 | import { getAuthSession } from "@/lib/auth"; 6 | 7 | async function page() { 8 | const session = await getAuthSession(); 9 | return ( 10 | <> 11 |
12 | {/* Hero Section */} 13 |
14 |
15 |

16 | Join an
Exciting Social
Experience. 17 |

18 |
19 |
20 | {/* middle section */} 21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 29 | {/* Auth Section */} 30 |
31 | 32 | 33 | 34 | Sign in to your account 35 | 36 | 37 | 38 | 39 |

40 | More sign-in options will be available soon 41 |

42 |
43 |
44 |
45 |
46 | 47 | ); 48 | } 49 | 50 | export default page; 51 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/lib/auth" 2 | import NextAuth from "next-auth" 3 | 4 | const handler = NextAuth(authOptions) 5 | 6 | export { handler as GET, handler as POST } -------------------------------------------------------------------------------- /src/app/api/link/route.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export async function GET(req: Request) { 4 | const url = new URL(req.url) 5 | const href = url.searchParams.get('url') 6 | 7 | if (!href) { 8 | return new Response('Invalid href', { status: 400 }) 9 | } 10 | 11 | const res = await axios.get(href) 12 | 13 | // Parse the HTML using regular expressions 14 | const titleMatch = res.data.match(/(.*?)<\/title>/) 15 | const title = titleMatch ? titleMatch[1] : '' 16 | 17 | const descriptionMatch = res.data.match( 18 | /<meta name="description" content="(.*?)"/ 19 | ) 20 | const description = descriptionMatch ? descriptionMatch[1] : '' 21 | 22 | const imageMatch = res.data.match(/<meta property="og:image" content="(.*?)"/) 23 | const imageUrl = imageMatch ? imageMatch[1] : '' 24 | 25 | // Return the data in the format required by the editor tool 26 | return new Response( 27 | JSON.stringify({ 28 | success: 1, 29 | meta: { 30 | title, 31 | description, 32 | image: { 33 | url: imageUrl, 34 | }, 35 | }, 36 | }) 37 | ) 38 | } -------------------------------------------------------------------------------- /src/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/Prisma.db'; 2 | import {NextResponse} from 'next/server' 3 | 4 | 5 | export const GET = async (req: Request) =>{ 6 | const {searchParams} = new URL(req.url); 7 | var data = searchParams.get('value'); 8 | if(data === '') return NextResponse.json({message:"error",status:201}) 9 | try { 10 | const users = await db.user.findMany({ 11 | where: { 12 | OR: [ 13 | { 14 | username: { 15 | contains: data as string, // Search for users with names containing the searchTerm 16 | mode: 'insensitive' 17 | }, 18 | }, 19 | ], 20 | }, 21 | take: 5 22 | }) 23 | return NextResponse.json(users,{status:201}) 24 | } catch (error) { 25 | return NextResponse.json({ message: 'Internal server error',error }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/api/user/aboutme/route.ts: -------------------------------------------------------------------------------- 1 | import { LocationAndBioValidator } from '@/lib/NewUserFormValidator' 2 | import { db } from '@/lib/Prisma.db' 3 | import { getAuthSession } from '@/lib/auth' 4 | import { z } from 'zod' 5 | 6 | export async function PATCH(req: Request) { 7 | try { 8 | const session = await getAuthSession() 9 | 10 | if (!session?.user) { 11 | return new Response('Unauthorized', { status: 401 }) 12 | } 13 | 14 | const body = await req.json() 15 | const { location , Bio } = LocationAndBioValidator.parse(body) 16 | 17 | // check if username is taken 18 | const usernameExist = await db.user.findFirst({ 19 | where: { 20 | id: session.user.id, 21 | }, 22 | }) 23 | 24 | if (!usernameExist) { 25 | return new Response('User does not exist', { status: 401 }) 26 | } 27 | 28 | // update username 29 | await db.user.update({ 30 | where: { 31 | id: session.user.id, 32 | }, 33 | data: { 34 | location: location, 35 | Bio: Bio, 36 | onboardingCompleted:true 37 | }, 38 | }) 39 | 40 | return new Response('OK') 41 | } catch (error) { 42 | (error) 43 | 44 | if (error instanceof z.ZodError) { 45 | return new Response(error.message, { status: 400 }) 46 | } 47 | 48 | return new Response( 49 | 'Could not update username at this time. Please try later', 50 | { status: 500 } 51 | ) 52 | } 53 | } -------------------------------------------------------------------------------- /src/app/api/user/avatar/route.ts: -------------------------------------------------------------------------------- 1 | import { LocationAndBioValidator } from '@/lib/NewUserFormValidator' 2 | import { db } from '@/lib/Prisma.db' 3 | import { getAuthSession } from '@/lib/auth' 4 | import { z } from 'zod' 5 | 6 | export async function PATCH(req: Request) { 7 | try { 8 | const session = await getAuthSession() 9 | 10 | if (!session?.user) { 11 | return new Response('Unauthorized', { status: 401 }) 12 | } 13 | 14 | const body = await req.json() 15 | // const { location , Bio } = LocationAndBioValidator.parse(body) 16 | 17 | // check if username is taken 18 | const usernameExist = await db.user.findFirst({ 19 | where: { 20 | id: session.user.id, 21 | }, 22 | }) 23 | 24 | if (!usernameExist) { 25 | return new Response('User does not exist', { status: 401 }) 26 | } 27 | 28 | // update username 29 | await db.user.update({ 30 | where: { 31 | id: session.user.id, 32 | }, 33 | data: { 34 | image: body.uploading.file.url 35 | }, 36 | }) 37 | 38 | return new Response('OK') 39 | } catch (error) { 40 | (error) 41 | 42 | if (error instanceof z.ZodError) { 43 | return new Response(error.message, { status: 400 }) 44 | } 45 | 46 | return new Response( 47 | 'Could not update username at this time. Please try later', 48 | { status: 500 } 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /src/app/api/user/follow/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/Prisma.db"; 2 | import { getAuthSession } from "@/lib/auth"; 3 | import { 4 | FollowUserValidator, 5 | } from "@/types/PostLikeValidator"; 6 | import { z } from "zod"; 7 | 8 | export async function POST(req: Request) { 9 | try { 10 | const body = await req.json(); 11 | const { toFollowId } = FollowUserValidator.parse(body); 12 | const session = await getAuthSession(); 13 | 14 | if (!session?.user) { 15 | return new Response("Unauthorized", { status: 401 }); 16 | } 17 | // IF THE FOLLOWING USER IS THE USER ONLY 18 | if (session.user.id === toFollowId) { 19 | return new Response("Action Prohibited !", { status: 40 }); 20 | } 21 | // check if user has already voted on this post 22 | const existingUser = await db.follows.findFirst({ 23 | where: { 24 | followingId: toFollowId, 25 | followerId: session.user.id, 26 | }, 27 | }); 28 | 29 | if (existingUser) { 30 | await db.follows.delete({ 31 | where: { 32 | id: existingUser.id, 33 | }, 34 | }); 35 | 36 | return new Response("unfollowed"); 37 | } else { 38 | await db.follows.create({ 39 | data: { 40 | followerId: session.user.id, 41 | followingId: toFollowId, 42 | }, 43 | }); 44 | 45 | return new Response("followed"); 46 | } 47 | 48 | return new Response("ok"); 49 | } catch (error) { 50 | if (error instanceof z.ZodError) { 51 | return new Response(error.message, { status: 400 }); 52 | } 53 | 54 | return new Response("Could not like.. Please try later" + error, { 55 | status: 500, 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/api/user/post/comment/commentlike/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/Prisma.db"; 2 | import { getAuthSession } from "@/lib/auth"; 3 | import { CommentVoteValidator } from "@/lib/commentValidator"; 4 | 5 | import { z } from "zod"; 6 | 7 | export async function POST(req: Request) { 8 | try { 9 | const body = await req.json(); 10 | const { commentId } = CommentVoteValidator.parse(body); 11 | const session = await getAuthSession(); 12 | 13 | if (!session?.user) { 14 | return new Response("Unauthorized", { status: 401 }); 15 | } 16 | // check if user has already voted on this post 17 | const existingLike = await db.commentVote.findFirst({ 18 | where: { 19 | userId: session.user.id, 20 | commentId, 21 | }, 22 | }); 23 | 24 | if (existingLike) { 25 | await db.commentVote.delete({ 26 | where: { 27 | id: existingLike.id, 28 | }, 29 | }); 30 | 31 | return new Response("Deleted Comment like"); 32 | } else { 33 | await db.commentVote.create({ 34 | data: { 35 | userId: session.user.id, 36 | commentId, 37 | }, 38 | }); 39 | return new Response("Liked Comment"); 40 | } 41 | 42 | return new Response("error"); 43 | } catch (error) { 44 | if (error instanceof z.ZodError) { 45 | return new Response(error.message, { status: 400 }); 46 | } 47 | 48 | return new Response("Could not like.. Please try later" + error, { 49 | status: 500, 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/api/user/post/comment/route.ts: -------------------------------------------------------------------------------- 1 | 2 | import { db } from '@/lib/Prisma.db' 3 | import { getAuthSession } from '@/lib/auth' 4 | import { CommentValidator } from '@/lib/commentValidator' 5 | import { z } from 'zod' 6 | 7 | export async function PATCH(req: Request) { 8 | try { 9 | const body = await req.json() 10 | 11 | const { postId, text, replyToId } = CommentValidator.parse(body) 12 | 13 | const session = await getAuthSession() 14 | 15 | if (!session?.user) { 16 | return new Response('Unauthorized', { status: 401 }) 17 | } 18 | 19 | // if no existing vote, create a new vote 20 | await db.comment.create({ 21 | data: { 22 | text, 23 | postId, 24 | authorId: session.user.id, 25 | replyToId, 26 | }, 27 | }) 28 | 29 | return new Response('OK') 30 | } catch (error) { 31 | if (error instanceof z.ZodError) { 32 | return new Response(error.message, { status: 400 }) 33 | } 34 | 35 | return new Response( 36 | 'Could not post to subreddit at this time. Please try later', 37 | { status: 500 } 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/api/user/post/create/route.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | import { postValidator } from '@/components/CreatePostValidator' 8 | import { db } from '@/lib/Prisma.db' 9 | import { getAuthSession } from '@/lib/auth' 10 | import { z } from 'zod' 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const body = await req.json() 15 | 16 | const { title, content } = postValidator.parse(body) 17 | 18 | const session = await getAuthSession() 19 | 20 | if (!session?.user) { 21 | return new Response('Unauthorized', { status: 401 }) 22 | } 23 | 24 | 25 | 26 | await db.post.create({ 27 | data: { 28 | title, 29 | content, 30 | //@ts-ignore 31 | authorId: session.user.id, 32 | }, 33 | }) 34 | 35 | return new Response('OK') 36 | } catch (error) { 37 | if (error instanceof z.ZodError) { 38 | return new Response(error.message, { status: 400 }) 39 | } 40 | 41 | return new Response( 42 | 'Could not post Error :'+error, 43 | { status: 500 } 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /src/app/api/user/post/newlike/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/Prisma.db"; 2 | import { getAuthSession } from "@/lib/auth"; 3 | import { PostVoteValidator } from "@/types/PostLikeValidator"; 4 | import { z } from "zod"; 5 | 6 | export async function POST(req: Request) { 7 | try { 8 | // Parse request and check auth in parallel 9 | const [body, session] = await Promise.all([ 10 | req.json(), 11 | getAuthSession() 12 | ]); 13 | 14 | const { postId } = PostVoteValidator.parse(body); 15 | 16 | if (!session?.user) { 17 | return new Response("Unauthorized", { status: 401 }); 18 | } 19 | 20 | // Use upsert with delete for atomic operation 21 | const result = await db.like.deleteMany({ 22 | where: { 23 | userId: session.user.id, 24 | postId, 25 | }, 26 | }); 27 | 28 | if (result.count === 0) { 29 | // No like existed, so create one 30 | await db.like.create({ 31 | data: { 32 | userId: session.user.id, 33 | postId, 34 | }, 35 | }); 36 | return new Response('Liked successfully', { status: 200 }); 37 | } 38 | 39 | return new Response('Unliked successfully', { status: 200 }); 40 | 41 | } catch (error) { 42 | if (error instanceof z.ZodError) { 43 | return new Response("Invalid request data", { status: 400 }); 44 | } 45 | 46 | console.error('Like operation failed:', error); 47 | return new Response( 48 | "Operation failed. Please try again later", 49 | { status: 500 } 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/api/user/post/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/Prisma.db' 2 | import { getAuthSession } from '@/lib/auth' 3 | import { z } from 'zod' 4 | 5 | export async function GET(req: Request) { 6 | const url = new URL(req.url) 7 | 8 | const session = await getAuthSession() 9 | if(!session) return new Response('Login first to see poat', { status: 401 }) 10 | 11 | 12 | try { 13 | const { limit, page } = z 14 | .object({ 15 | limit: z.string(), 16 | page: z.string(), 17 | }) 18 | .parse({ 19 | limit: url.searchParams.get('limit'), 20 | page: url.searchParams.get('page'), 21 | }) 22 | 23 | 24 | 25 | 26 | const posts = await db.post.findMany({ 27 | take: parseInt(limit), 28 | skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 29 | orderBy: { 30 | createdAt: 'desc', 31 | }, 32 | include: { 33 | like: true, 34 | author: true, 35 | comments:true 36 | }, 37 | }) 38 | 39 | return new Response(JSON.stringify(posts)) 40 | } catch (error) { 41 | return new Response('Could not fetch posts', { status: 500 }) 42 | } 43 | } -------------------------------------------------------------------------------- /src/app/api/user/username/route.ts: -------------------------------------------------------------------------------- 1 | import { UsernameValidator } from '@/lib/NewUserFormValidator' 2 | import { db } from '@/lib/Prisma.db' 3 | import { getAuthSession } from '@/lib/auth' 4 | import { z } from 'zod' 5 | 6 | export async function PATCH(req: Request) { 7 | try { 8 | const session = await getAuthSession() 9 | 10 | if (!session?.user) { 11 | return new Response('Unauthorized', { status: 401 }) 12 | } 13 | 14 | const body = await req.json() 15 | const { username } = UsernameValidator.parse(body) 16 | 17 | // check if username is taken 18 | const usernameExist = await db.user.findFirst({ 19 | where: { 20 | username: username, 21 | }, 22 | }) 23 | 24 | if (usernameExist) { 25 | return new Response('Username is taken', { status: 409 }) 26 | } 27 | 28 | // update username 29 | await db.user.update({ 30 | where: { 31 | id: session.user.id, 32 | }, 33 | data: { 34 | username: username, 35 | }, 36 | }) 37 | 38 | return new Response('OK') 39 | } catch (error) { 40 | (error) 41 | 42 | if (error instanceof z.ZodError) { 43 | return new Response(error.message, { status: 400 }) 44 | } 45 | 46 | return new Response( 47 | 'Could not update username at this time. Please try later', 48 | { status: 500 } 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taqui-786/project-friendz/fa1e3b9613ed12fcd9b795ea48d0ff217fb6b09b/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 20 14.3% 4.1%; 9 | 10 | --muted: 60 4.8% 95.9%; 11 | --muted-foreground: 25 5.3% 44.7%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 20 14.3% 4.1%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 20 14.3% 4.1%; 18 | 19 | --border: 20 5.9% 90%; 20 | --input: 20 5.9% 90%; 21 | 22 | --primary: 24 9.8% 10%; 23 | --primary-foreground: 60 9.1% 97.8%; 24 | 25 | --secondary: 60 4.8% 95.9%; 26 | --secondary-foreground: 24 9.8% 10%; 27 | 28 | --accent: 60 4.8% 95.9%; 29 | --accent-foreground: 24 9.8% 10%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 60 9.1% 97.8%; 33 | 34 | --ring: 24 5.4% 63.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 20 14.3% 4.1%; 41 | --foreground: 60 9.1% 97.8%; 42 | 43 | --muted: 12 6.5% 15.1%; 44 | --muted-foreground: 24 5.4% 63.9%; 45 | 46 | --popover: 20 14.3% 4.1%; 47 | --popover-foreground: 60 9.1% 97.8%; 48 | 49 | --card: 20 14.3% 4.1%; 50 | --card-foreground: 60 9.1% 97.8%; 51 | 52 | --border: 12 6.5% 15.1%; 53 | --input: 12 6.5% 15.1%; 54 | 55 | --primary: 60 9.1% 97.8%; 56 | --primary-foreground: 24 9.8% 10%; 57 | 58 | --secondary: 12 6.5% 15.1%; 59 | --secondary-foreground: 60 9.1% 97.8%; 60 | 61 | --accent: 12 6.5% 15.1%; 62 | --accent-foreground: 60 9.1% 97.8%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 85.7% 97.3%; 66 | 67 | --ring: 12 6.5% 15.1%; 68 | } 69 | } 70 | body { 71 | background: #f4f4f4; 72 | height: 100%; 73 | width: 100%; 74 | min-height: 100vh; 75 | color: #344258; 76 | overflow: hidden; 77 | } 78 | 79 | /* /// CUSTOM STYLES // */ 80 | 81 | /* SIGNIN */ 82 | .signin_animate{ 83 | animation: gradientShift 12s ease infinite; 84 | 85 | } 86 | .signin_text_shadow{ 87 | text-shadow: 4px 4px #3180e1, 8px 8px #3180e1; 88 | } 89 | 90 | .slide_link_active{ 91 | border-color: #3d70b2; 92 | font-weight: 500; 93 | color: #3d70b2; 94 | } 95 | .slide_link_active svg{ 96 | color: #3d70b2; 97 | } 98 | .popupbox::before{ 99 | border-bottom-color: #dcdcdc; 100 | border-width: 9px; 101 | left: 42px; 102 | margin-left: -9px; 103 | } 104 | .popupbox::before,.popupbox::after{ 105 | position: absolute; 106 | pointer-events: none; 107 | border: solid transparent; 108 | bottom: 100%; 109 | content: ""; 110 | height: 0; 111 | width: 0; 112 | } 113 | .popupbox::after { 114 | border-bottom-color: #fff; 115 | border-width: 8px; 116 | left: 42px; 117 | margin-left: -8px; 118 | } 119 | /* CUSTOM STYLES FOR NEWUSER PAGE ->> */ 120 | .w-calc-100-min-24{ 121 | width: calc(100% - 24px); 122 | } 123 | .h-calc-100-min-113{ 124 | min-height: calc(100vh - 133px); 125 | } 126 | .custom_transition_width{ 127 | transition: width 0.4s; 128 | } 129 | .activeDot{ 130 | color: #039be5; 131 | border-color: #039be5; 132 | } 133 | .firstDot { 134 | left: -19px; 135 | color: #039be5; 136 | border-color: #039be5; 137 | } 138 | .secondDot { 139 | left: calc(25% - 19px); 140 | } 141 | .thirdDot { 142 | left: calc(50% - 19px); 143 | } 144 | .fourthDot { 145 | left: calc(75% - 19px); 146 | } 147 | .fifthDot { 148 | right: -19px; 149 | } 150 | /* HIDING SLIDE BAR CLASS AND MAKING HOME WIDTH FULL--> */ 151 | .slidebar_hide{ 152 | transform: translate(-100%); 153 | } 154 | .homepage_full{ 155 | width: 100%; 156 | margin-left: 0; 157 | } 158 | .home_width{ 159 | width: calc(100% - 280px); 160 | } 161 | /* SCROLLBAR HIDE */ 162 | .hidescrollbar::-webkit-scrollbar{ 163 | display: none; 164 | } 165 | /* LOADING ANIMATION */ 166 | .loads{ 167 | animation-duration: 1s; 168 | animation-fill-mode: forwards; 169 | animation-iteration-count: infinite; 170 | animation-name: placeload; 171 | animation-timing-function: linear; 172 | background: linear-gradient(to right,#eeeeee 8%,#dddddd 18%,#eeeeee 33%); 173 | background-size: 1200px 104px; 174 | position: relative; 175 | } 176 | /* CUSTOM PAGE LOADER FOR PROFILE AND POST */ 177 | .loader { 178 | width: 48px; 179 | height: 48px; 180 | position: relative; 181 | } 182 | .loader::before , .loader::after{ 183 | content: ''; 184 | position: absolute; 185 | left: 50%; 186 | top: 50%; 187 | transform: translate(-50% , -50%); 188 | width: 48em; 189 | height: 48em; 190 | background-image: 191 | radial-gradient(circle 10px, #3180e1 100%, transparent 0), 192 | radial-gradient(circle 10px, #3180e1 100%, transparent 0), 193 | radial-gradient(circle 10px, #3180e1 100%, transparent 0), 194 | radial-gradient(circle 10px, #3180e1 100%, transparent 0), 195 | radial-gradient(circle 10px, #3180e1 100%, transparent 0), 196 | radial-gradient(circle 10px, #3180e1 100%, transparent 0), 197 | radial-gradient(circle 10px, #3180e1 100%, transparent 0), 198 | radial-gradient(circle 10px, #3180e1 100%, transparent 0); 199 | background-position: 0em -18em, 0em 18em, 18em 0em, -18em 0em, 200 | 13em -13em, -13em -13em, 13em 13em, -13em 13em; 201 | background-repeat: no-repeat; 202 | font-size: 0.5px; 203 | border-radius: 50%; 204 | animation: blast 1s ease-in infinite; 205 | } 206 | .loader::after { 207 | font-size: 1px; 208 | background: #3180e1; 209 | animation: bounce 1s ease-in infinite; 210 | } 211 | 212 | @keyframes bounce { 213 | 0% , 100%{ font-size: 0.75px } 214 | 50% { font-size: 1.5px } 215 | } 216 | @keyframes blast { 217 | 0% , 40% { 218 | font-size: 0.5px; 219 | } 220 | 70% { 221 | opacity: 1; 222 | font-size: 4px; 223 | } 224 | 100% { 225 | font-size: 6px; 226 | opacity: 0; 227 | } 228 | } 229 | @keyframes placeload { 230 | 0% { 231 | background-position: -468px 0; 232 | } 233 | 234 | 100% { 235 | background-position: 468px 0; 236 | } 237 | } 238 | @media (max-width:768px){ 239 | .home_width{ 240 | width: 100%; 241 | margin-left: 0; 242 | } 243 | .myslidebar{ 244 | transform: translate(-100%) 245 | } 246 | } 247 | /* ---------------------------- */ 248 | /* ------- */ 249 | @layer base { 250 | * { 251 | @apply border-border; 252 | } 253 | body { 254 | @apply bg-background text-foreground; 255 | } 256 | } 257 | 258 | @layer base { 259 | * { 260 | @apply border-border outline-ring/50; 261 | } 262 | body { 263 | @apply bg-background text-foreground; 264 | } 265 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Slidebar from '@/components/slidebar/Slidebar' 2 | import './globals.css' 3 | import type { Metadata } from 'next' 4 | import { Inter } from 'next/font/google' 5 | import QuerryProvider from '@/components/QuerryProvider' 6 | import { Toaster } from 'sonner'; 7 | const inter = Inter({ subsets: ['latin'] }) 8 | 9 | export const metadata: Metadata = { 10 | title: 'Friendz', 11 | description: 'Generated by create next app', 12 | } 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode 18 | }) { 19 | return ( 20 | <html lang="en"> 21 | <body className={inter.className}> 22 | <Toaster richColors position="top-center" /> 23 | <QuerryProvider> 24 | 25 | <Slidebar /> 26 | {children} 27 | </QuerryProvider> 28 | </body> 29 | </html> 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return ( 3 | <> 4 | <div 5 | id="homePage" 6 | className=" z-[+999999] relative ml-[280px] pt-6 py-[60px] px-[12px] home_width " 7 | > 8 | <div className="max-w-[1040px] m-auto relative grow w-auto"> 9 | {/* TOP NAV TOOLBAR */} 10 | <div className=" bg-white my-0 p-4 mx-auto relative flex items-center w-full max-w-[1040px] z-10 "> 11 | <div className="h-10 ml-3 w-24 loads"></div> 12 | <div className="flex ml-auto items-center"> 13 | <div className="px-[6px] flex items-center py-2 grow-0 shrink-0 text-[#4a4a4a] relative "> 14 | <div className="relative flex justify-center items-center w-[38px] h-[38px] rounded-sm loads"></div> 15 | </div> 16 | <div className="px-[6px] flex items-center py-2 grow-0 shrink-0 text-[#4a4a4a] relative "> 17 | <div className="relative flex justify-center items-center w-[38px] h-[38px] rounded-sm loads"></div> 18 | </div> 19 | <div className="px-[6px] flex items-center py-2 grow-0 shrink-0 text-[#4a4a4a] relative "> 20 | <div className="relative flex justify-center items-center w-[38px] h-[38px] rounded-sm loads"></div> 21 | </div> 22 | <div className="flex justify-start items-center cursor-pointer grow-0 shrink-0 text-[#4a4a4a] px-3 py-2 relative"> 23 | <div className="relative w-10 h-[40px] rounded-full loads"></div> 24 | </div> 25 | </div> 26 | </div> 27 | {/* POST FEED */} 28 | <div className="py-5 px-0"> 29 | <div className="-mx-3 -mt-3 last:-mb-3 md:flex "> 30 | {/* MIDDLE COLUMN --> */} 31 | <div className="block basis-0 grow shrink p-3 max-h-[642px] md:flex-none md:w-[66.66666674%] md:max-h-[555px] overflow-y-auto hidescrollbar "> 32 | {/* CREATE POST LOADER */} 33 | <div className="relative mb-6 border bg-white border-[#e8e8e8] rounded-xl shadow-none max-w-full "> 34 | <div className="rounded-xl"> 35 | <div className="p-8 border border-[#e8e8e8]"> 36 | <div className="flex justify-center items-center"> 37 | <div className="h-11 w-11 loads rounded-full"></div> 38 | <div className="ml-12 w-full clear-both relative"> 39 | <div className="h-6 w-32 loads rounded-md"></div> 40 | </div> 41 | </div> 42 | </div> 43 | </div> 44 | </div> 45 | {/* FEED LOADER */} 46 | <ul className="flex flex-col col-span-2 space-y-6"> 47 | <div 48 | className="bg-white w-full p-5 mb-10 rounded-md relative " 49 | id="PostLoader" 50 | > 51 | {/* HEADER */} 52 | <div className="flex justify-start items-center"> 53 | <div className="rounded-full w-[50px] h-[50px] min-h-[50px] loads"></div> 54 | <div className="ml-5 w-full "> 55 | <div className="h-[10px] mb-[10px] w-[60%] rounded-smc loads"></div> 56 | <div className="h-[10px] mb-[10px] w-[40%] rounded-smc loads"></div> 57 | </div> 58 | </div> 59 | {/* BODY */} 60 | <div className="w-full mt-5 h-[300px] loads"></div> 61 | {/* FOOTER */} 62 | <div className="relative mt-5 w-full flex justify-between items-center"> 63 | <div className="flex justify-start items-center w-full h-full"> 64 | <div className="w-10 min-w-[40px] h-10 rounded-full loads"></div> 65 | <div className="ml-[10px] w-full "> 66 | <div className="h-[10px] mb-[10px] w-[32%] rounded-smc loads"></div> 67 | <div className="h-[10px] mb-[10px] w-[24%] rounded-smc loads"></div> 68 | </div> 69 | </div> 70 | </div> 71 | </div> 72 | 73 | </ul> 74 | </div> 75 | {/* RIGHT COLUMN --> */} 76 | <div className="block basis-0 grow shrink p-3 md:flex-none md:w-[33.33333337%] "> 77 | <div className="h-[382px] p-5 mb-5 w-full rounded-lg bg-white border border-[#e8e8e8]"> 78 | <div className="h-12 flex justify-start items-center"> 79 | <div className="w-[55%] h-[10px] mb-[10px] rounded-md loads"></div> 80 | </div> 81 | <div> 82 | <div className="flex justify-start items-center h-[76px]"> 83 | <div className="h-11 w-11 min-w-[44px] rounded-full loads"></div> 84 | <div className="w-full px-[10px]"> 85 | <div className="h-[10px] mb-[10px] rounded-md w-[78%] loads"></div> 86 | <div className="h-[10px] mb-[10px] rounded-md w-[54%] loads"></div> 87 | </div> 88 | </div> 89 | <div className="flex justify-start items-center h-[76px]"> 90 | <div className="h-11 w-11 min-w-[44px] rounded-full loads"></div> 91 | <div className="w-full px-[10px]"> 92 | <div className="h-[10px] mb-[10px] rounded-md w-[78%] loads"></div> 93 | <div className="h-[10px] mb-[10px] rounded-md w-[54%] loads"></div> 94 | </div> 95 | </div> 96 | <div className="flex justify-start items-center h-[76px]"> 97 | <div className="h-11 w-11 min-w-[44px] rounded-full loads"></div> 98 | <div className="w-full px-[10px]"> 99 | <div className="h-[10px] mb-[10px] rounded-md w-[78%] loads"></div> 100 | <div className="h-[10px] mb-[10px] rounded-md w-[54%] loads"></div> 101 | </div> 102 | </div> 103 | <div className="flex justify-start items-center h-[76px]"> 104 | <div className="h-11 w-11 min-w-[44px] rounded-full loads"></div> 105 | <div className="w-full px-[10px]"> 106 | <div className="h-[10px] mb-[10px] rounded-md w-[78%] loads"></div> 107 | <div className="h-[10px] mb-[10px] rounded-md w-[54%] loads"></div> 108 | </div> 109 | </div> 110 | </div> 111 | </div> 112 | </div> 113 | </div> 114 | </div> 115 | </div> 116 | </div> 117 | </> 118 | ) 119 | } -------------------------------------------------------------------------------- /src/app/newuser/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function NewuserLayout({ 2 | children, // will be a page or nested layout 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 | <main className="h-screen w-screen bg- bg-colorF7 relative z-[+9999] "> 8 | 9 | 10 | {children} 11 | </main> 12 | ) 13 | } -------------------------------------------------------------------------------- /src/app/newuser/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return (<> 3 | <div className="flex justify-center h-screen w-screen items-center"> 4 | <span className="loader"></span> 5 | </div> 6 | </>) 7 | } -------------------------------------------------------------------------------- /src/app/newuser/page.tsx: -------------------------------------------------------------------------------- 1 | import ProgressBar from "@/components/newuserpage/ProgressBar"; 2 | import Progresstitle from "@/components/newuserpage/Progresstitle"; 3 | import { getAuthSession } from "@/lib/auth"; 4 | import { sessionUserType } from "@/types/types"; 5 | import dynamic from "next/dynamic"; 6 | const Signup1 = dynamic(() => import("@/components/newuserpage/Signup1")); 7 | const Signup3 = dynamic(() => import("@/components/newuserpage/Signup3"), { 8 | ssr: false, 9 | }); 10 | const Signup2 = dynamic(() => import("@/components/newuserpage/Signup2"), { 11 | ssr: false, 12 | }); 13 | const Signup4 = dynamic(() => import("@/components/newuserpage/Signup4"), { 14 | ssr: false, 15 | }); 16 | const Signup5 = dynamic(() => import("@/components/newuserpage/Signup5"), { 17 | ssr: false, 18 | }); 19 | 20 | const page = async () => { 21 | const session = await getAuthSession(); 22 | 23 | 24 | return ( 25 | <> 26 | <div className="min-h-screen bg-gray-100 relative h-full w-full overflow-hidden text-gray-700"> 27 | {/* FAKE NAVBAR */} 28 | <div className="h-[55px] w-full flex justify-center items-center bg-white z-50"></div> 29 | {/* PROGRESSBAR */} 30 | <ProgressBar /> 31 | <div className="flex items-center h-calc-100-min-113"> 32 | <div className="w-full"> 33 | {/* POST TITLE */} 34 | <div className="max-w-3xl mx-auto text-center pt-4"> 35 | <Progresstitle /> 36 | </div> 37 | {/* SIGNUP STEP 1 _ PUBLIC - PRIVATE - COMPANY */} 38 | <div 39 | id="stepOne" 40 | className=" block animate-[fadeInLeft] duration-500 max-w-[1040px] mx-auto py-5 px-0 max-[640px]:px-3 " 41 | > 42 | <Signup1 /> 43 | </div> 44 | {/* SIGNUP STEP 2 _ NAME AND EMAIL */} 45 | <div 46 | id="stepTwo" 47 | className="hidden max-w-lg animate-[fadeInLeft] duration-500 mx-auto py-5 px-0 max-[640px]:px-3" 48 | > 49 | <Signup2 existingUsername={session?.user.username || ''} /> 50 | </div> 51 | {/* SIGNUP STEP 3 Password - OTP */} 52 | <div 53 | id="stepThree" 54 | className="hidden max-w-lg animate-[fadeInLeft] duration-500 mx-auto py-5 px-0 max-[640px]:px-3" 55 | > 56 | <Signup3 /> 57 | </div> 58 | {/* SIGNUP STEP 4 PHOTO UPLOAD */} 59 | <div 60 | id="stepFour" 61 | className="hidden max-w-lg animate-[fadeInLeft] duration-500 mx-auto py-5 px-0 max-[640px]:px-3" 62 | > 63 | <Signup4 userImage={session?.user.image || ''} /> 64 | </div> 65 | {/* SIGNUP FINAL STEP CONGRATZ */} 66 | <div 67 | id="stepFive" 68 | className="hidden max-w-lg animate-[fadeInLeft] duration-500 mx-auto py-5 px-0 max-[640px]:px-3" 69 | > 70 | <Signup5 /> 71 | </div> 72 | </div> 73 | </div> 74 | </div> 75 | </> 76 | ); 77 | }; 78 | 79 | export default page; 80 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import CreatePostOuterBox from "@/components/CreatePostOuterBox" 2 | import FeedPage from "@/components/feed/FeedPage" 3 | 4 | 5 | export default async function Home() { 6 | 7 | return ( 8 | <> 9 | <main > 10 | <FeedPage/> 11 | {/* CREATE POST POPUP -> */} 12 | <CreatePostOuterBox/> 13 | </main> 14 | </> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/post/[postId]/page.tsx: -------------------------------------------------------------------------------- 1 | import CommentInput from "@/components/CommentInput"; 2 | import CommentOuterBox from "@/components/CommentOuterBox"; 3 | import { CommentInputBox } from "@/components/CustomComponents/CommentInputBox"; 4 | import EditorOutput from "@/components/EditorOutput"; 5 | import ExitProfileBtn from "@/components/button/ExitProfileBtn"; 6 | import FollowButton from "@/components/button/FollowButton"; 7 | import PostLikeServer from "@/components/button/PostLikeServerside"; 8 | import { db } from "@/lib/Prisma.db"; 9 | import { getAuthSession } from "@/lib/auth"; 10 | import { Like, User, Post } from "@prisma/client"; 11 | import { Loader2 } from "lucide-react"; 12 | import Image from "next/image"; 13 | import { notFound } from "next/navigation"; 14 | import React, { Suspense } from "react"; 15 | import { AiOutlineHeart } from "react-icons/ai"; 16 | import { BiComment } from "react-icons/bi"; 17 | import { format } from 'timeago.js' 18 | interface postpagerpops { 19 | params: { 20 | postId: string; 21 | }; 22 | } 23 | export const dynamic = "force-dynamic"; 24 | export const fetchCache = "force-no-store"; 25 | const page = async ({ params }: postpagerpops) => { 26 | const session = await getAuthSession(); 27 | 28 | const post = await db.post.findFirst({ 29 | where: { 30 | id: params.postId, 31 | }, 32 | include: { 33 | like: true, 34 | author: true 35 | }, 36 | }); 37 | 38 | const isFollowed = await db.user.findUnique({ 39 | where:{ 40 | id: post?.author.id 41 | }, 42 | include:{ 43 | followers:true 44 | } 45 | }) 46 | let isUserFollowed = isFollowed?.followers.find( 47 | (val) => val.followerId === session?.user.id 48 | ); 49 | 50 | if (!post) return notFound(); 51 | 52 | return ( 53 | <div className="z-50 h-screen w-screen relative overflow-y-auto md:overflow-hidden grid md:flex justify-center md:items-center "> 54 | {/* pageId: {params.postId} */} 55 | {/* left side */} 56 | <div className="p-4 h-full w-[inherit] md:w-[68%] bg-colorF7 flex justify-center items-center"> 57 | <div className="relative mb-6 border w-fit min-w-[35%] min-h-[30%] border-[#e8e8e8] bg-white rounded-[.85rem] text-[#4a4a4a] max-h-[600px] md:max-h-[520px] "> 58 | <div> 59 | {/* HEAD */} 60 | <div className="flex justify-start items-center pt-4 px-4 pb-0"></div> 61 | {/* BODY */} 62 | <div className="px-4 py-4 "> 63 | <div className="relative mx-h-80 overflow-hidden decoration-transparent"> 64 | <div> 65 | <h2 className="text-[#222] text-lg">{post.title}</h2> 66 | </div> 67 | <EditorOutput content={post.content} /> 68 | </div> 69 | </div> 70 | {/* FOOTER */} 71 | </div> 72 | </div> 73 | </div> 74 | 75 | <div className="h-full w-full md:w-[32%] flex"> 76 | <div className="relative bg-[#f5f6f7] text-[#6c6f73] py-3 h-full w-[89%] flex flex-col"> 77 | <div className="bg-white"> 78 | {/* **** HEAD ---> */} 79 | <div className="w-full flex justify-start items-center p-3 bg-transparent"> 80 | {post.author.image && ( 81 | <Image 82 | src={post.author.image} 83 | alt="user" 84 | height={42} 85 | width={42} 86 | loading="eager" 87 | className="rounded-full w-[42px] h-[42px] max-h-[42px]" 88 | /> 89 | )} 90 | <div className="px-[10px]"> 91 | <span className="block text-sm font-medium text-black">{post.author.name}</span> 92 | <span className="block text-xs text-left text-gray-500"> 93 | {format(post.createdAt)} 94 | </span> 95 | </div> 96 | {/* FOLLOW BUTTON */} 97 | <FollowButton 98 | myId={session?.user.id} 99 | toFollow={isFollowed?.id} 100 | isFollowed={isUserFollowed} 101 | /> 102 | </div> 103 | {/* ----- Inner Content LIKE ANd Comment */} 104 | <div className="p-3"> 105 | <div className="flex justify-between items-center pb-5 border-b border-b-[#e8e8e8]"> 106 | {/* LIKE SERVER SIDE */} 107 | <Suspense fallback={<MyLoader />}> 108 | <PostLikeServer 109 | postId={post.id} 110 | getData={async () => { 111 | return await db.post.findUnique({ 112 | where: { 113 | id: params.postId, 114 | }, 115 | include: { 116 | like: true, 117 | comments: true, 118 | }, 119 | }); 120 | }} 121 | /> 122 | </Suspense> 123 | </div> 124 | <div className="flex justify-between items-center pt-3"> 125 | <div className="flex justify-center items-center cursor-pointer"> 126 | <AiOutlineHeart className="h-[17px] w-[17px] " /> 127 | <span className="block text-sm mx-1">Like</span> 128 | </div> 129 | <div className="flex justify-center items-center cursor-pointer"> 130 | <div className="flex justify-center items-center cursor-pointer"> 131 | <BiComment className="h-[17px] w-[17px] " /> 132 | <span className="block text-sm mx-1">Comment</span> 133 | </div> 134 | </div> 135 | </div> 136 | </div></div> 137 | {/* COMMENT BOX -> */} 138 | {/* <CommentOuterBox postId={params.postId} /> */} 139 | <Suspense 140 | fallback={ 141 | <div className="flex flex-col gap-4 p-4"> 142 | {/* Multiple skeleton comments for better UX */} 143 | {[1, 2, 3].map((i) => ( 144 | <div key={i} className="flex items-start gap-3 animate-pulse"> 145 | {/* Avatar skeleton */} 146 | <div className="w-8 h-8 bg-gray-200 rounded-full" /> 147 | <div className="flex-1"> 148 | {/* Username and time skeleton */} 149 | <div className="flex items-center gap-2 mb-2"> 150 | <div className="h-4 w-24 bg-gray-200 rounded" /> 151 | <div className="h-3 w-16 bg-gray-100 rounded" /> 152 | </div> 153 | {/* Comment text skeleton */} 154 | <div className="space-y-2"> 155 | <div className="h-4 w-3/4 bg-gray-100 rounded" /> 156 | <div className="h-4 w-1/2 bg-gray-100 rounded" /> 157 | </div> 158 | </div> 159 | </div> 160 | ))} 161 | </div> 162 | } 163 | > 164 | {/* @ts-expect-error Server Component */} 165 | <CommentOuterBox postId={post.id} /> 166 | </Suspense> 167 | 168 | 169 | {/* EXIT BUTTON */} 170 | </div> 171 | <div className="relative z-50 bg-[#5596e6] h-full w-[11%] pt-2"> 172 | <ExitProfileBtn /> 173 | </div> 174 | </div> 175 | </div> 176 | ); 177 | }; 178 | 179 | export default page; 180 | 181 | function MyLoader() { 182 | return ( 183 | <> 184 | <Loader2 className="h-3 w-3 p-4 m-auto text-[blue] animate-spin" /> 185 | </> 186 | ); 187 | } 188 | -------------------------------------------------------------------------------- /src/app/profile/[userId]/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return (<> 3 | <div className="flex justify-center h-screen w-screen items-center"> 4 | <span className="loader"></span> 5 | </div> 6 | </>) 7 | } 8 | -------------------------------------------------------------------------------- /src/app/profile/[userId]/page.tsx: -------------------------------------------------------------------------------- 1 | import TopPageNavbar from "@/components/TopPageNavbar"; 2 | import Image from "next/image"; 3 | import banner from "../../../../public/cover_bg.png"; 4 | import { db } from "@/lib/Prisma.db"; 5 | import { AiOutlinePlus } from "react-icons/ai"; 6 | 7 | import { getAuthSession } from "@/lib/auth"; 8 | import FollowButton from "@/components/button/FollowButton"; 9 | import ProfilePostsColumn from "@/components/feed/ProfilePostsColumn"; 10 | import { Suspense } from "react"; 11 | import { Loader2 } from "lucide-react"; 12 | 13 | import BasicInfoWidget from "@/components/BasicInfoWidget"; 14 | import Link from "next/link"; 15 | 16 | 17 | interface profilepageprops { 18 | params: { 19 | userId: string; 20 | }; 21 | } 22 | const Profile = async ({ params }: profilepageprops) => { 23 | const session = await getAuthSession(); 24 | 25 | let user = await db.user.findUnique({ 26 | where: { 27 | id: params.userId, 28 | }, 29 | include: { 30 | followers: true, 31 | following: true, 32 | Post: { 33 | include: { 34 | like: true, 35 | author: true, 36 | comments:true 37 | }, 38 | // take: 2 39 | }, 40 | }, 41 | }); 42 | let isUserFollowed; 43 | if (user) { 44 | isUserFollowed = user?.followers.find( 45 | (val) => val.followerId === session?.user.id 46 | ); 47 | } 48 | 49 | return ( 50 | <> 51 | <div 52 | id="homePage" 53 | className="relative ml-[280px] pt-6 py-[60px] px-[12px] home_width " 54 | > 55 | <div className="max-w-[1040px] m-auto relative grow w-auto"> 56 | {/* TOP NAV TOOLBAR */} 57 | <TopPageNavbar title="Profile" /> 58 | <div className="pt-[10px] pb-5 h-[88vh] overflow-y-scroll hidescrollbar"> 59 | <div className="flex flex-wrap"> 60 | <div className="block basis-0 grow shrink"> 61 | {/* COVER IMAGE */} 62 | <div className="relative"> 63 | <Image 64 | src={banner} 65 | alt="banner" 66 | height={328} 67 | width={656} 68 | loading="eager" 69 | priority 70 | className="block rounded-sm object-top object-cover w-full h-auto max-h-[328px]" 71 | /> 72 | <div className="absolute shadow-lg -bottom-[50px] left-0 right-0 m-auto flex justify-center items-center h-28 w-28 rounded-full z-10"> 73 | {user?.image && ( 74 | <Image 75 | src={user.image} 76 | alt="avatar" 77 | height={112} 78 | width={112} 79 | loading="eager" 80 | priority 81 | className="relative object-cover rounded-full z-10 w-[112px] h-[112px] max-h-[7rem] " 82 | /> 83 | )} 84 | {session?.user.id === user?.id && ( 85 | <div className="absolute bottom-0 right-0 h-9 w-9 rounded-full flex justify-center items-center bg-[#3d70b2] shadow-md cursor-pointer z-20"> 86 | <Link href='/settings' className="h-fit w-fit decoration-transparent" > 87 | <AiOutlinePlus className="h-5 w-5 text-white" /> 88 | </Link> 89 | </div> 90 | )} 91 | </div> 92 | </div> 93 | {/* PROFILE FOLLOW BUTTON */} 94 | <div className="flex justify-between items-center pt-2"> 95 | <div className="flex justify-between items-start py-[10px] w-[99%]"> 96 | <div className="w-1/4 flex justify-between max-[620px]:flex-col max-[620px]:items-center "> 97 | <div className="h-fit w-fit flex flex-col items-center"> 98 | <span className="text-[1.6rem] font-mono font-bold text-[#393a4f] block"> 99 | {user?.followers.length} 100 | </span> 101 | <span className="text-xs font-medium text-[#999] block "> 102 | FOLLOWERS 103 | </span> 104 | </div> 105 | <div className="h-fit w-fit flex flex-col items-center"> 106 | <span className="text-[1.6rem] font-mono font-bold text-[#393a4f] block"> 107 | {user?.following.length} 108 | </span> 109 | <span className="text-xs font-medium text-[#999] block "> 110 | FOLLOWINGS 111 | </span> 112 | </div> 113 | <div className="h-fit w-fit flex flex-col items-center"> 114 | <span className="text-[1.6rem] font-mono font-bold text-[#393a4f] block"> 115 | {user?.Post.length} 116 | </span> 117 | <span className="text-xs font-medium text-[#999] block "> 118 | POSTS 119 | </span> 120 | </div> 121 | </div> 122 | {/* USER NAME */} 123 | <div className="text-center w-2/4 mt-10"> 124 | <h2 className="text-[#393a4f] font-semibold "> 125 | {user?.username} 126 | </h2> 127 | <span className="block text-sm text-[#999]"> 128 | {user?.name} 129 | </span> 130 | </div> 131 | <div className="text-right w-1/4 max-[620px]:mr-4"> 132 | {(session?.user.id !== user?.id) && 133 | <FollowButton 134 | myId={session?.user.id} 135 | toFollow={user?.id} 136 | isFollowed={isUserFollowed} 137 | /> 138 | } 139 | </div> 140 | </div> 141 | </div> 142 | </div> 143 | </div> 144 | {/* POST SIDE */} 145 | <div className=" -mx-3 -mt-3 last:mb-3 md:flex"> 146 | {/* BASIC INFO WIDGET -> */} 147 | <BasicInfoWidget user={{ 148 | email:user?.email || '', 149 | Bio: user?.Bio || '', 150 | location : user?.location || '', 151 | }} follower={user?.followers?.length || 0} /> 152 | 153 | <div className="block p-3 w-full md:w-[66.66666674%] basis-0 grow shrink md:flex-none "> 154 | {/* WIDGET HEADING */} 155 | <div className="w-full p-2 rounded-sm border border-[#e8e8e8] bg-white flex justify-center items-center"> 156 | <h4 className="font-medium px-[6px]">Posts</h4> 157 | </div> 158 | <div className="py-[10px]"> 159 | {/* POSTS */} 160 | <Suspense 161 | fallback={ 162 | <Loader2 className="h-5 w-5 animate-spin text-blue-700" /> 163 | } 164 | > 165 | <ProfilePostsColumn profilePosts={user?.Post} /> 166 | </Suspense> 167 | </div> 168 | </div> 169 | </div> 170 | </div> 171 | </div> 172 | </div> 173 | </> 174 | ); 175 | }; 176 | 177 | export default Profile; 178 | -------------------------------------------------------------------------------- /src/app/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return (<> 3 | <div className="flex justify-center h-screen w-screen items-center"> 4 | <span className="loader"></span> 5 | </div> 6 | </>) 7 | } 8 | -------------------------------------------------------------------------------- /src/app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import Signup2 from "@/components/newuserpage/Signup2"; 2 | import Signup3 from "@/components/newuserpage/Signup3"; 3 | import Signup4 from "@/components/newuserpage/Signup4"; 4 | import { getAuthSession } from "@/lib/auth"; 5 | import { Metadata } from "next"; 6 | export const metadata: Metadata = { 7 | title: 'Settings', 8 | description: 'user setting page ', 9 | } 10 | const page = async () => { 11 | const session = await getAuthSession() 12 | return ( 13 | <> 14 | <div 15 | id="homePage" 16 | className=" bg-white relative ml-[280px] pt-6 py-[60px] px-[12px] home_width" 17 | > 18 | <div className="max-w-[1040px] m-auto relative grow w-auto "> 19 | <div className="pt-[10px] pb-5 h-[88vh] overflow-y-scroll hidescrollbar "> 20 | <div> 21 | <div className="space-y-12"> 22 | <div className="border-b border-gray-900/10 pb-12"> 23 | <h2 className="text-base font-semibold leading-7 text-gray-900 px-[30px]"> 24 | Settings 25 | </h2> 26 | <p className="mt-1 text-sm leading-6 text-gray-600 px-[30px]"> 27 | This information will be displayed publicly so be careful 28 | what you changes. 29 | </p> 30 | 31 | <div className="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> 32 | <div className="sm:col-span-4"> 33 | <Signup2 existingUsername={session?.user?.username as string} /> 34 | </div> 35 | 36 | <div className="col-span-full"> 37 | <Signup3 /> 38 | </div> 39 | 40 | <div className="col-span-full"> 41 | <Signup4 userImage={session?.user?.image as string} /> 42 | </div> 43 | </div> 44 | </div> 45 | </div> 46 | </div> 47 | </div> 48 | </div> 49 | </div> 50 | </> 51 | ); 52 | }; 53 | 54 | export default page; 55 | -------------------------------------------------------------------------------- /src/components/BasicInfoWidget.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client" 2 | import { FC } from "react" 3 | import {SiAboutdotme} from 'react-icons/si' 4 | import { MdNotificationsActive, MdLocationPin, MdAccountCircle, MdEmail } from "react-icons/md" 5 | 6 | type basicinfowidgetprops ={ 7 | user:Pick<User, 'email' | 'Bio' | 'location' > 8 | follower: number 9 | } 10 | 11 | 12 | const BasicInfoWidget:FC<basicinfowidgetprops> = ({user , follower}) =>{ 13 | 14 | 15 | 16 | return( 17 | <div className="block p-3 w-full md:w-[33.33333337%] basis-0 grow shrink md:flex-none "> 18 | {/* WIDGET HEADING */} 19 | <div className="w-full p-2 rounded-sm border border-[#e8e8e8] bg-white flex justify-center items-center"> 20 | <h4 className="font-medium px-[6px]">Basic Infos</h4> 21 | </div> 22 | {/* WIDGET BODY */} 23 | <div className="pt-[10px]"> 24 | <div className="relative mb-6 border border-[#e8e8e8] last:border last:border-[#e8e8e8] bg-white rounded-lg text-[#4a4a4a] max-w-full"> 25 | {/* WIDGET ITEMS */} 26 | <div className="flex justify-between items-center py-[10px] px-4"> 27 | <div> 28 | <span className="text-sm font-medium text-[#393a4f] block"> 29 | Account Type 30 | </span> 31 | <span className="text-[0.9rem] text-[#999] font-normal block"> 32 | Public 33 | </span> 34 | </div> 35 | <MdAccountCircle className="text-xl text-[#cecece] mx-1" /> 36 | </div> 37 | <div className="flex justify-between items-center py-[10px] px-4"> 38 | <div> 39 | <span className="text-sm font-medium text-[#393a4f] block"> 40 | Email 41 | </span> 42 | <span className="text-[0.9rem] text-[#999] font-normal block"> 43 | {user.email} 44 | </span> 45 | </div> 46 | <MdEmail className="text-xl text-[#cecece] mx-1" /> 47 | </div> 48 | <div className="flex justify-between items-center py-[10px] px-4"> 49 | <div> 50 | <span className="text-sm font-medium text-[#393a4f] block"> 51 | Lives In 52 | </span> 53 | <span className="text-[0.9rem] text-[#999] font-normal block"> 54 | {user.location} 55 | </span> 56 | </div> 57 | <MdLocationPin className="text-xl text-[#cecece] mx-1" /> 58 | </div> 59 | <div className="flex justify-between items-center py-[10px] px-4"> 60 | <div> 61 | <span className="text-sm font-medium text-[#393a4f] block"> 62 | Followers 63 | </span> 64 | <span className="text-[0.9rem] text-[#999] font-normal block"> 65 | {follower} Followers 66 | </span> 67 | </div> 68 | <MdNotificationsActive className="text-xl text-[#cecece] mx-1" /> 69 | </div> 70 | <div className="flex justify-between items-center py-[10px] px-4"> 71 | <div> 72 | <span className="text-sm font-medium text-[#393a4f] block"> 73 | About Me 74 | </span> 75 | <span className="text-[0.9rem] text-[#999] font-normal block"> 76 | {user.Bio} 77 | </span> 78 | </div> 79 | <SiAboutdotme className="text-xl text-[#cecece] mx-1" /> 80 | </div> 81 | </div> 82 | </div> 83 | </div> 84 | ) 85 | } 86 | 87 | export default BasicInfoWidget -------------------------------------------------------------------------------- /src/components/CommentInput.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { FC, useState } from "react" 4 | import { Textarea } from "./ui/textarea" 5 | import { Button } from "./ui/button" 6 | import { useMutation } from "@tanstack/react-query" 7 | import axios, { AxiosError } from "axios" 8 | import { CommentRequest } from "@/lib/commentValidator" 9 | import { useRouter } from "next/navigation" 10 | interface CreateCommentProps { 11 | postId: string 12 | replyToId?: string 13 | } 14 | const CommentInput: FC<CreateCommentProps> = ({ postId, replyToId }) =>{ 15 | const router = useRouter() 16 | const [input, setInput] = useState<string>('') 17 | const { mutate: comment, isLoading } = useMutation({ 18 | mutationFn: async ({ postId, text, replyToId }: CommentRequest) => { 19 | const payload: CommentRequest = { postId, text, replyToId } 20 | 21 | const { data } = await axios.patch( 22 | `/api/user/post/comment`, 23 | payload 24 | ) 25 | return data 26 | }, 27 | 28 | onError: (err) => { 29 | if (err instanceof AxiosError) { 30 | if (err.response?.status === 401) { 31 | // return loginToast() 32 | } 33 | } 34 | return window.alert('Something went wrong') 35 | 36 | }, 37 | onSuccess: () => { 38 | router.refresh() 39 | setInput('') 40 | }, 41 | }) 42 | 43 | return( 44 | 45 | <> 46 | <div className="absolute bottom-0 left-0 bg-[#fbfbfc] h-[85px] w-full border-l border-l-[#dee2e5]"> 47 | <div className="relative flex items-center w-full h-full px-4"> 48 | {/* INPUT box */} 49 | <div className='mt-0 w-[80%]'> 50 | <Textarea 51 | id='comment' 52 | value={input} 53 | className="min-h-[64px] max-h-[65px]" 54 | onChange={(e) => setInput(e.target.value)} 55 | rows={1} 56 | placeholder='What are your thoughts?' 57 | /> 58 | 59 | </div> 60 | <div className='mt-2 w-[20%] flex justify-end'> 61 | <Button 62 | size="xs" 63 | isLoading={isLoading} 64 | disabled={input.length === 0} 65 | onClick={() => comment({ postId, text: input, replyToId })}> 66 | Send 67 | </Button> 68 | </div> 69 | </div> 70 | </div> 71 | 72 | </> 73 | ) 74 | } 75 | export default CommentInput -------------------------------------------------------------------------------- /src/components/CommentLike.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC, useEffect, useState } from "react"; 3 | import { AiFillHeart } from "react-icons/ai"; 4 | import { Button } from "./ui/button"; 5 | import { useMutation, useQueryClient } from "@tanstack/react-query"; 6 | import { CommentVoteRequest } from "@/lib/commentValidator"; 7 | import axios, { AxiosError } from "axios"; 8 | import { toast } from "sonner"; 9 | 10 | interface CommmentLikeProps { 11 | commentId: string; 12 | votesAmt: number; 13 | currentVote?: any; 14 | } 15 | 16 | const CommmentLike: FC<CommmentLikeProps> = ({ 17 | commentId, 18 | votesAmt: initialVotesAmt, 19 | currentVote: initialVote, 20 | }) => { 21 | const [optimisticVotes, setOptimisticVotes] = useState<number>(initialVotesAmt); 22 | const [optimisticLiked, setOptimisticLiked] = useState<boolean>(!!initialVote); 23 | const queryClient = useQueryClient(); 24 | 25 | const { mutate: like, isLoading } = useMutation({ 26 | mutationFn: async () => { 27 | const payload: CommentVoteRequest = { commentId }; 28 | return await axios.post('/api/user/post/comment/commentlike', payload); 29 | }, 30 | mutationKey: ['comment-like', commentId], 31 | onError: (err) => { 32 | // Revert optimistic update 33 | setOptimisticVotes(initialVotesAmt); 34 | setOptimisticLiked(!!initialVote); 35 | 36 | if (err instanceof AxiosError) { 37 | if (err.response?.status === 401) { 38 | return toast.error('Please login to like comments'); 39 | } 40 | } 41 | return toast.error('Failed to update like'); 42 | }, 43 | onMutate: () => { 44 | // Optimistic update 45 | setOptimisticLiked((current) => !current); 46 | setOptimisticVotes((prev) => prev + (optimisticLiked ? -1 : 1)); 47 | }, 48 | onSettled: () => { 49 | // Invalidate and refetch 50 | queryClient.invalidateQueries(['comments', commentId]); 51 | }, 52 | }); 53 | 54 | return ( 55 | <> 56 | <div className="pl-3 text-[#999] flex items-center gap-2 text-xs"> 57 | <Button 58 | variant="ghost" 59 | size="sm" 60 | disabled={isLoading} 61 | onClick={() => like()} 62 | className="p-0 h-auto hover:bg-transparent" 63 | > 64 | <AiFillHeart 65 | className={`text-lg transition-colors ${ 66 | optimisticLiked ? 'text-red-500' : 'text-[#888da8]' 67 | } hover:scale-110 active:scale-95`} 68 | /> 69 | </Button> 70 | <span>{optimisticVotes}</span> 71 | </div> 72 | </> 73 | ); 74 | }; 75 | 76 | export default CommmentLike; 77 | -------------------------------------------------------------------------------- /src/components/CommentOuterBox.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/Prisma.db"; 2 | import { CommentVote, User } from "@prisma/client"; 3 | import { FC } from "react"; 4 | import Comments from "./Comments"; 5 | import { getAuthSession } from "@/lib/auth"; 6 | import { BsArrowReturnRight } from "react-icons/bs"; 7 | import { CommentInputBox } from "./CustomComponents/CommentInputBox"; 8 | 9 | type ExtendedComment = Comment & { 10 | votes: CommentVote[]; 11 | author: User; 12 | replies: ReplyComment[]; 13 | }; 14 | 15 | type ReplyComment = Comment & { 16 | votes: CommentVote[]; 17 | author: User; 18 | }; 19 | 20 | interface CommentOuterBoxProps { 21 | postId: string; 22 | comments: ExtendedComment[]; 23 | } 24 | const CommentOuterBox = async ({ postId }: CommentOuterBoxProps) => { 25 | const session = await getAuthSession(); 26 | const comments = await db.comment.findMany({ 27 | where: { 28 | postId: postId, 29 | replyToId: undefined, // only fetch top-level comments 30 | }, 31 | include: { 32 | author: true, 33 | like: true, 34 | replies: { 35 | // first level replies 36 | include: { 37 | author: true, 38 | like: true, 39 | }, 40 | }, 41 | }, 42 | }); 43 | 44 | return ( 45 | <> 46 | {/* COMMENT OUTER BOX */} 47 | <div 48 | className="bg-[#f5f6f7] py-5 px-[14px] overflow-y-auto " 49 | style={{ height: "calc(100% - 220px)" }} 50 | > 51 | {/* COMMENTS */} 52 | {comments 53 | .filter((comment) => !comment.replyToId) 54 | .map((topLevelComment, index, array) => { 55 | // CHECKING THAT COMMENT IS LIKED OR NOT 56 | const isLiked = topLevelComment.like.find( 57 | (vote) => vote.userId === session?.user.id 58 | ); 59 | // LIKES 60 | const topLevelCommentVotesAmt = topLevelComment?.like.length; 61 | return ( 62 | <> 63 | <Comments 64 | currentVote={isLiked} 65 | votesAmt={topLevelCommentVotesAmt} 66 | postId={postId} 67 | comment={topLevelComment} 68 | key={topLevelComment.id} 69 | islastComment={index === array.length - 1} 70 | /> 71 | {topLevelComment.replies 72 | .sort((a, b) => b.like.length - a.like.length) // Sort replies by most liked 73 | .map((reply) => { 74 | const replyVote = reply.like.find( 75 | (vote) => vote.userId === session?.user.id 76 | ); 77 | 78 | const replyVoteAmt = reply?.like.length; 79 | 80 | return ( 81 | <div className="relative ml-10 h-fit " key={reply.id}> 82 | <h3 className="absolute -top-[20px] left-0 p-2"> 83 | <BsArrowReturnRight className="text-2xl text-[#999]" />{" "} 84 | </h3> 85 | <Comments 86 | currentVote={replyVote} 87 | votesAmt={replyVoteAmt} 88 | postId={postId} 89 | comment={reply } 90 | /> 91 | </div> 92 | ); 93 | })} 94 | </> 95 | ); 96 | })} 97 | </div> 98 | {/* COMMENT SENT INPUT BOX */} 99 | <CommentInputBox postId={postId} /> 100 | </> 101 | ); 102 | }; 103 | 104 | export default CommentOuterBox; 105 | -------------------------------------------------------------------------------- /src/components/Comments.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Comment, CommentVote, User } from "@prisma/client"; 3 | import { useSession } from "next-auth/react"; 4 | import Image from "next/image"; 5 | import { FC, useRef, useState } from "react"; 6 | import { Button } from "./ui/button"; 7 | import CommmentLike from "./CommentLike"; 8 | import { Textarea } from "./ui/textarea"; 9 | import axios, { AxiosError } from "axios"; 10 | import { CommentRequest } from "@/lib/commentValidator"; 11 | import { useMutation } from "@tanstack/react-query"; 12 | import { useRouter } from "next/navigation"; 13 | import { format } from "timeago.js"; 14 | import { toast } from "sonner"; 15 | import { MessageCircleIcon } from "lucide-react"; 16 | type ExtendedComment = Comment & { 17 | like: CommentVote[]; 18 | author: User; 19 | replies?: Comment[]; 20 | }; 21 | 22 | interface PostCommentProps { 23 | comment: ExtendedComment; 24 | votesAmt: number; 25 | currentVote: CommentVote | undefined; 26 | postId: string; 27 | islastComment?: boolean; 28 | } 29 | 30 | const Comments: FC<PostCommentProps> = ({ 31 | comment, 32 | postId, 33 | currentVote, 34 | votesAmt, 35 | islastComment, 36 | }) => { 37 | // const { data: session } = useSession(); 38 | const [isReplying, setIsReplying] = useState<boolean>(false); 39 | const commentRef = useRef<HTMLDivElement>(null); 40 | const [input, setInput] = useState<string>(`@${comment.author.name} `); 41 | 42 | // COMMENT REPLY FINCTION 43 | const router = useRouter(); 44 | const { mutate: replyComment, isLoading } = useMutation({ 45 | mutationFn: async ({ postId, text, replyToId }: CommentRequest) => { 46 | const payload: CommentRequest = { postId, text, replyToId }; 47 | 48 | const { data } = await axios.patch(`/api/user/post/comment`, payload); 49 | return data; 50 | }, 51 | 52 | onError: (err) => { 53 | if (err instanceof AxiosError) { 54 | if (err.response?.status === 401) { 55 | // return loginToast() 56 | } 57 | } 58 | return toast.error("Something went wrong"); 59 | }, 60 | onSuccess: () => { 61 | setIsReplying(false); 62 | router.refresh(); 63 | setInput(""); 64 | }, 65 | }); 66 | 67 | return ( 68 | <div className="flex items-center mb-4 flex-col " ref={commentRef}> 69 | <div className="h-full w-full flex items-center relative "> 70 | <figure className="mr-[10px] mb-auto mt-3 basis-0 grow-0 shrink-0 block "> 71 | <p className="h-8 w-8 block relative "> 72 | {comment.author.image && ( 73 | <Image 74 | src={comment.author.image} 75 | alt="img" 76 | height={32} 77 | width={32} 78 | loading="eager" 79 | priority 80 | className="block h-8 w-8 rounded-full border border-gray-500" 81 | /> 82 | )} 83 | </p> 84 | </figure> 85 | {/* {!islastComment && ( 86 | <div className="absolute left-4 top-11 w-px h-full bg-border" /> 87 | )} */} 88 | <div className="p-3 rounded-md bg-white basis-auto grow shrink"> 89 | <div className=" flex flex-col"> 90 | <div className="text-sm font-bold text-primary "> 91 | {comment.author.name} 92 | </div> 93 | <span className="text-xs text-gray-500 "> 94 | {format(comment.createdAt)} 95 | </span> 96 | </div> 97 | <p className="text-sm text-gray-800 whitespace-pre-wrap break-words py-1"> 98 | {comment.text} 99 | </p> 100 | <div className="flex items-center gap-2 mt-2"> 101 | <CommmentLike 102 | commentId={comment.id} 103 | votesAmt={votesAmt} 104 | currentVote={currentVote} 105 | /> 106 | <Button 107 | variant="ghost" 108 | size="xs" 109 | className="text-gray-500 h-6 px-2" 110 | onClick={() => setIsReplying(true)} 111 | > 112 | <MessageCircleIcon className="w-4 h-4 mr-1" /> 113 | Reply 114 | </Button> 115 | </div> 116 | </div> 117 | </div> 118 | {isReplying ? ( 119 | <div className="relative bottom-0 left-0 bg-transparent h-[85px] w-full "> 120 | <div className="relative flex items-center w-full h-full px-4"> 121 | {/* INPUT box */} 122 | <div className="mt-0 w-[80%]"> 123 | <Textarea 124 | onFocus={(e) => 125 | e.currentTarget.setSelectionRange( 126 | e.currentTarget.value.length, 127 | e.currentTarget.value.length 128 | ) 129 | } 130 | autoFocus 131 | id="comment" 132 | value={input} 133 | onChange={(e) => setInput(e.target.value)} 134 | rows={1} 135 | placeholder="What are your thoughts?" 136 | /> 137 | </div> 138 | <div className="mt-2 w-[20%] h-full flex justify-evenly flex-col items-center"> 139 | <Button 140 | size={"xs"} 141 | isLoading={isLoading} 142 | onClick={() => { 143 | if (!input) return; 144 | replyComment({ 145 | postId, 146 | text: input, 147 | replyToId: comment.replyToId ?? comment.id, // default to top-level comment 148 | }); 149 | }} 150 | > 151 | Reply 152 | </Button> 153 | <Button 154 | variant="destructive" 155 | size="xs" 156 | onClick={() => setIsReplying(false)} 157 | > 158 | Cancel 159 | </Button> 160 | </div> 161 | </div> 162 | </div> 163 | ) : null} 164 | </div> 165 | ); 166 | }; 167 | export default Comments; 168 | -------------------------------------------------------------------------------- /src/components/CreatePostOuterBox.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthSession } from '@/lib/auth' 2 | import Image from 'next/image' 3 | import React from 'react' 4 | import CreatePostExitPost from './button/CreatePostExitPost' 5 | import Editor from './Editor' 6 | import { Button } from './ui/button' 7 | async function CreatePostOuterBox() { 8 | const session = await getAuthSession() 9 | return ( 10 | <div id='createPost' className=' overflow-auto hidden justify-center items-center opacity-100 fixed h-full w-full top-0 right-0 left-0 bottom-0 m-auto z-50' style={{background:"rgba(0,0,0,.6)"}}> 11 | <div className="p-4 rounded-md bg-white relative m-auto max-w-[95%] w-auto md:w-[60%] min-h-[75%] "> 12 | <div className="flex w-full p-3 relative border-b border-b-borderE3"> 13 | {session?.user?.image && <Image src={session.user.image} alt='img' height={45} width={45} loading='eager' className='rounded-full' />} 14 | <div className='flex flex-col ml-2 font-serif' > 15 | <h3 className='text-[#4a4a4a] text-base font-medium '>{session?.user.username}</h3> 16 | <span className='text-[#666] text-sm'>{session?.user.name}</span> 17 | </div> 18 | <Button form='create_post' className='ml-3 bg-[#3d70b2]'>Post</Button> 19 | <CreatePostExitPost/> 20 | </div> 21 | <div className='relative'> 22 | <Editor userId={session?.user?.id} /> 23 | </div> 24 | </div> 25 | </div> 26 | ) 27 | } 28 | 29 | export default CreatePostOuterBox -------------------------------------------------------------------------------- /src/components/CreatePostValidator.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | import {z} from 'zod' 7 | 8 | 9 | export const postValidator = z.object({ 10 | title: z 11 | .string() 12 | .min(3,{message:'Title must be longer than 3 characters'}) 13 | .max(60,{message:'Title not more than 60 characters'}), 14 | userId: z.string(), 15 | content: z.any(), 16 | }) 17 | export type PostCreationRequest = z.infer<typeof postValidator> -------------------------------------------------------------------------------- /src/components/CustomComponents/CommentInputBox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Textarea } from "@/components/ui/textarea"; 5 | import { cn } from "@/lib/utils"; 6 | import { useTextareaResize } from "./hooks"; 7 | import { ArrowUpIcon, Send } from "lucide-react"; 8 | import type React from "react"; 9 | import { createContext, useContext, useState } from "react"; 10 | import { RiLoader2Line } from "react-icons/ri"; 11 | import { useMutation } from "@tanstack/react-query"; 12 | import { CommentRequest } from "@/lib/commentValidator"; 13 | import axios, { AxiosError } from "axios"; 14 | import { useRouter } from "next/navigation"; 15 | import { toast } from "sonner"; 16 | 17 | interface ChatInputContextValue { 18 | value?: string; 19 | onChange?: React.ChangeEventHandler<HTMLTextAreaElement>; 20 | onSubmit?: () => void; 21 | loading?: boolean; 22 | onStop?: () => void; 23 | variant?: "default" | "unstyled"; 24 | rows?: number; 25 | } 26 | 27 | const ChatInputContext = createContext<ChatInputContextValue>({}); 28 | 29 | interface ChatInputProps extends Omit<ChatInputContextValue, "variant"> { 30 | children: React.ReactNode; 31 | className?: string; 32 | variant?: "default" | "unstyled"; 33 | rows?: number; 34 | } 35 | 36 | function ChatInput({ 37 | children, 38 | className, 39 | variant = "default", 40 | value, 41 | onChange, 42 | onSubmit, 43 | loading, 44 | onStop, 45 | rows = 1, 46 | }: ChatInputProps) { 47 | const contextValue: ChatInputContextValue = { 48 | value, 49 | onChange, 50 | onSubmit, 51 | loading, 52 | onStop, 53 | variant, 54 | rows, 55 | }; 56 | 57 | return ( 58 | <ChatInputContext.Provider value={contextValue}> 59 | <div 60 | className={cn( 61 | variant === "default" && 62 | "flex flex-col items-end w-full p-2 rounded-2xl border border-input bg-transparent focus-within:ring-1 focus-within:ring-ring focus-within:outline-none", 63 | variant === "unstyled" && "flex items-start gap-2 w-full", 64 | className 65 | )} 66 | > 67 | {children} 68 | </div> 69 | </ChatInputContext.Provider> 70 | ); 71 | } 72 | 73 | ChatInput.displayName = "ChatInput"; 74 | 75 | interface ChatInputTextAreaProps extends React.ComponentProps<typeof Textarea> { 76 | value?: string; 77 | onChange?: React.ChangeEventHandler<HTMLTextAreaElement>; 78 | onSubmit?: () => void; 79 | variant?: "default" | "unstyled"; 80 | } 81 | 82 | function ChatInputTextArea({ 83 | onSubmit: onSubmitProp, 84 | value: valueProp, 85 | onChange: onChangeProp, 86 | className, 87 | variant: variantProp, 88 | ...props 89 | }: ChatInputTextAreaProps) { 90 | const context = useContext(ChatInputContext); 91 | const value = valueProp ?? context.value ?? ""; 92 | const onChange = onChangeProp ?? context.onChange; 93 | const onSubmit = onSubmitProp ?? context.onSubmit; 94 | const rows = context.rows ?? 1; 95 | 96 | // Convert parent variant to textarea variant unless explicitly overridden 97 | const variant = 98 | variantProp ?? (context.variant === "default" ? "unstyled" : "default"); 99 | 100 | const textareaRef = useTextareaResize(value, rows); 101 | const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { 102 | if (!onSubmit) { 103 | return; 104 | } 105 | if (e.key === "Enter" && !e.shiftKey) { 106 | if (typeof value !== "string" || value.trim().length === 0) { 107 | return; 108 | } 109 | e.preventDefault(); 110 | onSubmit(); 111 | } 112 | }; 113 | 114 | return ( 115 | <Textarea 116 | ref={textareaRef} 117 | {...props} 118 | value={value} 119 | onChange={onChange} 120 | onKeyDown={handleKeyDown} 121 | className={cn( 122 | "max-h-[400px] min-h-0 resize-none overflow-x-hidden", 123 | variant === "unstyled" && 124 | "border-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none", 125 | className 126 | )} 127 | rows={rows} 128 | /> 129 | ); 130 | } 131 | 132 | ChatInputTextArea.displayName = "ChatInputTextArea"; 133 | 134 | interface ChatInputSubmitProps extends React.ComponentProps<typeof Button> { 135 | onSubmit?: () => void; 136 | loading?: boolean; 137 | onStop?: () => void; 138 | } 139 | 140 | function ChatInputSubmit({ 141 | onSubmit: onSubmitProp, 142 | loading: loadingProp, 143 | onStop: onStopProp, 144 | className, 145 | ...props 146 | }: ChatInputSubmitProps) { 147 | const context = useContext(ChatInputContext); 148 | const loading = loadingProp ?? context.loading; 149 | const onStop = onStopProp ?? context.onStop; 150 | const onSubmit = onSubmitProp ?? context.onSubmit; 151 | 152 | if (loading ) { 153 | return ( 154 | <Button 155 | onClick={onStop} 156 | className={cn( 157 | "shrink-0 rounded-full p-1.5 h-fit border dark:border-zinc-600", 158 | className 159 | )} 160 | {...props} 161 | > 162 | <RiLoader2Line className="h-5 w-5 animate-spin text-zinc-500" /> 163 | </Button> 164 | ); 165 | } 166 | 167 | const isDisabled = 168 | typeof context.value !== "string" || context.value.trim().length === 0; 169 | 170 | return ( 171 | <Button 172 | className={cn( 173 | "shrink-0 rounded-full p-1.5 h-fit border dark:border-zinc-600", 174 | // loading && "hidden", 175 | className 176 | )} 177 | disabled={isDisabled} 178 | onClick={(event) => { 179 | event.preventDefault(); 180 | if (!isDisabled) { 181 | onSubmit?.(); 182 | } 183 | }} 184 | {...props} 185 | > 186 | <Send className="h-5 w-5" /> 187 | </Button> 188 | ); 189 | } 190 | 191 | ChatInputSubmit.displayName = "ChatInputSubmit"; 192 | 193 | const CommentInputBox = ({ postId, replyToId }: { postId: string, replyToId?: string }) => { 194 | const router = useRouter() 195 | const [input, setInput] = useState<string>('') 196 | const { mutate: comment, isLoading } = useMutation({ 197 | mutationFn: async ({ postId, text, replyToId }: CommentRequest) => { 198 | const payload: CommentRequest = { postId, text, replyToId } 199 | 200 | const { data } = await axios.patch( 201 | `/api/user/post/comment`, 202 | payload 203 | ) 204 | return data 205 | }, 206 | mutationKey: ['comment'], 207 | onError: (err) => { 208 | if (err instanceof AxiosError) { 209 | if (err.response?.status === 401) { 210 | // return loginToast() 211 | } 212 | } 213 | return toast.error('Something went wrong') 214 | 215 | }, 216 | onSuccess: () => { 217 | router.refresh() 218 | setInput('') 219 | }, 220 | }) 221 | return ( 222 | <ChatInput 223 | value={input} 224 | onChange={(e) => setInput(e.target.value)} 225 | onSubmit={() => comment({ postId, text: input, replyToId })} 226 | loading={isLoading} 227 | className="bg-white" 228 | > 229 | <ChatInputTextArea placeholder="Type a message..." /> 230 | <ChatInputSubmit /> 231 | </ChatInput> 232 | ); 233 | }; 234 | export { ChatInput, ChatInputTextArea, ChatInputSubmit, CommentInputBox }; 235 | -------------------------------------------------------------------------------- /src/components/CustomComponents/hooks.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useLayoutEffect, useRef } from "react"; 4 | import type { ComponentProps } from "react"; 5 | 6 | export function useTextareaResize( 7 | value: ComponentProps<"textarea">["value"], 8 | rows = 1, 9 | ) { 10 | const textareaRef = useRef<HTMLTextAreaElement>(null); 11 | 12 | // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 13 | useLayoutEffect(() => { 14 | const textArea = textareaRef.current; 15 | 16 | if (textArea) { 17 | // Get the line height to calculate minimum height based on rows 18 | const computedStyle = window.getComputedStyle(textArea); 19 | const lineHeight = Number.parseInt(computedStyle.lineHeight, 10) || 20; 20 | const padding = 21 | Number.parseInt(computedStyle.paddingTop, 10) + 22 | Number.parseInt(computedStyle.paddingBottom, 10); 23 | 24 | // Calculate minimum height based on rows 25 | const minHeight = lineHeight * rows + padding; 26 | 27 | // Reset height to auto first to get the correct scrollHeight 28 | textArea.style.height = "0px"; 29 | const scrollHeight = Math.max(textArea.scrollHeight, minHeight); 30 | 31 | // Set the final height 32 | textArea.style.height = `${scrollHeight + 2}px`; 33 | } 34 | }, [textareaRef, value, rows]); 35 | 36 | return textareaRef; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { FC, useCallback, useEffect, useRef, useState } from "react"; 3 | import { useForm } from "react-hook-form"; 4 | import ReactTextareaAutosize from "react-textarea-autosize"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import EditorJS from "@editorjs/editorjs"; 7 | import { useMutation } from "@tanstack/react-query"; 8 | import axios from "axios"; 9 | import { handleImageUpload } from "@/lib/Functions"; 10 | import { PostCreationRequest, postValidator } from "./CreatePostValidator"; 11 | import { Loader2 } from "lucide-react"; 12 | import { exitPopup } from "./button/CreatePostExitPost"; 13 | import { useRouter } from "next/navigation"; 14 | import toast from "react-hot-toast"; 15 | 16 | interface editorProp { 17 | userId: any; 18 | } 19 | 20 | const Editor: FC<editorProp> = ({ userId }) => { 21 | 22 | const { 23 | register, 24 | handleSubmit, 25 | formState: { errors }, 26 | } = useForm<PostCreationRequest>({ 27 | resolver: zodResolver(postValidator), 28 | defaultValues: { 29 | userId, 30 | title: "", 31 | content: null, 32 | }, 33 | }); 34 | const router = useRouter() 35 | const ref = useRef<EditorJS>(); 36 | const [isMounted, setIsMounted] = useState<boolean>(false); 37 | const _titleRef = useRef<HTMLTextAreaElement>(null); 38 | useEffect(() => { 39 | if (typeof window !== "undefined") { 40 | setIsMounted(true); 41 | } 42 | 43 | }, []); 44 | 45 | // EDITOR INITALISATION FUNCTION --- > 46 | const initializeEditor = useCallback(async () => { 47 | const EditorJS = (await import("@editorjs/editorjs")).default; 48 | // @ts-ignore 49 | const Header = (await import("@editorjs/header")).default; 50 | // @ts-ignore 51 | const Embed = (await import("@editorjs/embed")).default; 52 | // @ts-ignore 53 | const Table = (await import("@editorjs/table")).default; 54 | // @ts-ignore 55 | const List = (await import("@editorjs/list")).default; 56 | // @ts-ignore 57 | const Code = (await import("@editorjs/code")).default; 58 | // @ts-ignore 59 | const LinkTool = (await import("@editorjs/link")).default; 60 | // @ts-ignore 61 | const InlineCode = (await import("@editorjs/inline-code")).default; 62 | // @ts-ignore 63 | const ImageTool = (await import("@editorjs/image")).default; 64 | 65 | if (!ref.current) { 66 | const editor = new EditorJS({ 67 | holder: "editor", 68 | onReady() { 69 | ref.current = editor; 70 | }, 71 | placeholder: "Type here to write your post...", 72 | inlineToolbar: true, 73 | data: { blocks: [] }, 74 | tools: { 75 | header: Header, 76 | linkTool: { 77 | class: LinkTool, 78 | config: { 79 | endpoint: "/api/link", 80 | }, 81 | }, 82 | image: { 83 | class: ImageTool, 84 | config: { 85 | uploader: { 86 | uploadByFile: handleImageUpload, 87 | }, 88 | }, 89 | }, 90 | list: List, 91 | code: Code, 92 | inlineCode: InlineCode, 93 | embed: Embed, 94 | table: Table, 95 | }, 96 | }); 97 | } 98 | }, []); 99 | 100 | // INITIALIZING 101 | useEffect(() => { 102 | const init = async () => { 103 | await initializeEditor(); 104 | 105 | setTimeout(() => { 106 | _titleRef?.current?.focus(); 107 | }, 0); 108 | }; 109 | 110 | if (isMounted) { 111 | init(); 112 | 113 | return () => { 114 | ref.current?.destroy(); 115 | ref.current = undefined; 116 | }; 117 | } 118 | }, [isMounted, initializeEditor]); 119 | 120 | // ERROR HANDLING OF EDITOR 121 | useEffect(() => { 122 | if (Object.keys(errors).length) { 123 | for (const [_key, value] of Object.entries(errors)) { 124 | toast.error((value as { message: string }).message); 125 | } 126 | } 127 | }, [errors]); 128 | 129 | const { mutate: createPost, isLoading } = useMutation({ 130 | mutationFn: async ({ title, content, userId }: PostCreationRequest) => { 131 | const payload: PostCreationRequest = { title, content, userId }; 132 | const { data } = await axios.post("/api/user/post/create", payload); 133 | return data; 134 | }, 135 | onError: () => { 136 | toast.error("Post submit Error"); 137 | }, 138 | onSuccess: () => { 139 | // A WAY TO HANDLE CREATE POST AND REDIRECT IT 140 | router.refresh() 141 | ref.current?.blocks.clear(); 142 | toast.success("Successfull Posted") 143 | exitPopup(); 144 | }, 145 | }); 146 | 147 | // EDITOR ON SUBMIT 148 | async function onSubmit(data: PostCreationRequest) { 149 | const blocks = await ref.current?.save(); 150 | 151 | const payload: PostCreationRequest = { 152 | title: data.title, 153 | content: blocks, 154 | userId, 155 | }; 156 | 157 | createPost(payload); 158 | } 159 | 160 | if (!isMounted) { 161 | return null; 162 | } 163 | // CUSTOM REF SETTING 164 | const { ref: titleRef, ...rest } = register("title"); 165 | 166 | return ( 167 | <div className="w-full p-4 bg-zinc-50 rounded-lg border border-zinc-200 "> 168 | {/* On SUBMIT LOADING */} 169 | {isLoading && ( 170 | <div className="absolute h-full w-full m-auto top-0 bottom-0 left-0 right-0 z-[+9999] flex justify-center items-center bg-zinc-50"> 171 | <Loader2 className="h-14 w-14 text-[#3d70b2] animate-spin" /> 172 | </div> 173 | )} 174 | <form 175 | id="create_post" 176 | className="w-fit" 177 | onSubmit={handleSubmit(onSubmit)} 178 | > 179 | <div className="max-w-full"> 180 | {/* TEXTAREA FOR POST TITLE */} 181 | <ReactTextareaAutosize 182 | placeholder="Title" 183 | disabled={isLoading} 184 | ref={(e) => { 185 | titleRef(e); 186 | // @ts-ignore 187 | _titleRef.current = e; 188 | }} 189 | {...rest} 190 | className="w-full resize-none overflow-hidden bg-transparent appearance-none text-5xl font-bold focus:outline-none" 191 | /> 192 | <div id="editor" className="min-h-[50%]" /> 193 | </div> 194 | </form> 195 | </div> 196 | ); 197 | }; 198 | 199 | export default Editor; 200 | -------------------------------------------------------------------------------- /src/components/EditorOutput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import dynamic from "next/dynamic"; 5 | import CustomCodeRenderer from "./renderers/CustomCodeRender"; 6 | import CustomImageRenderer from "./renderers/CustomImageRender"; 7 | import PostLoader from "./loaders/PostLoader"; 8 | import CustomListRenderer from "./renderers/CustomListRenderer"; 9 | 10 | const Output = dynamic( 11 | async () => (await import("editorjs-react-renderer")).default, 12 | { ssr: false, loading: () => <PostLoader /> } 13 | ); 14 | 15 | interface EditorOutputProps { 16 | content: any; 17 | } 18 | 19 | const renderers = { 20 | image: CustomImageRenderer, 21 | code: CustomCodeRenderer, 22 | list: CustomListRenderer, 23 | }; 24 | 25 | const style = { 26 | paragraph: { 27 | fontSize: "0.875rem", 28 | lineHeight: "1.25rem", 29 | }, 30 | }; 31 | 32 | const EditorOutput: FC<EditorOutputProps> = ({ content }) => { 33 | return ( 34 | <Output 35 | style={style} 36 | className="text-sm " 37 | renderers={renderers} 38 | data={content} 39 | /> 40 | ); 41 | }; 42 | 43 | export default EditorOutput; 44 | -------------------------------------------------------------------------------- /src/components/QuerryProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 4 | import { SessionProvider } from "next-auth/react"; 5 | import { FC, ReactNode, useState } from "react"; 6 | import { Toaster } from "react-hot-toast"; 7 | interface LayoutProps { 8 | children: ReactNode; 9 | } 10 | 11 | const QuerryProvider: FC<LayoutProps> = ({ children }) => { 12 | const [queryClient] = useState(() => new QueryClient()); 13 | return ( 14 | <QueryClientProvider client={queryClient}> 15 | <Toaster position="top-center" reverseOrder={false} /> 16 | <SessionProvider>{children}</SessionProvider> 17 | </QueryClientProvider> 18 | ); 19 | }; 20 | 21 | export default QuerryProvider; 22 | -------------------------------------------------------------------------------- /src/components/SearchUserInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useCallback } from "react"; 3 | import { FiSearch } from "react-icons/fi"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "@/components/ui/dialog"; 10 | import { Input } from "@/components/ui/input"; 11 | import axios from "axios"; 12 | import { Loader2 } from "lucide-react"; 13 | import debounce from "lodash/debounce"; 14 | import { useSearch } from "@/lib/Apis"; 15 | 16 | function SearchUserInput() { 17 | const [open, setOpen] = useState(false); 18 | const [searchQuery, setSearchQuery] = useState(""); 19 | const [isTyping, setIsTyping] = useState(false); 20 | 21 | const { data: users = [], error, isFetching } = useSearch(searchQuery); 22 | 23 | const handleSearch = (value: string) => { 24 | setSearchQuery(value); 25 | }; 26 | 27 | return ( 28 | <> 29 | <div className="flex justify-center mb-0"> 30 | <div className="relative w-full"> 31 | <div className="absolute p-2.5 h-full grid items-center text-muted-foreground"> 32 | <FiSearch className="h-5 w-5" /> 33 | </div> 34 | <input 35 | type="search" 36 | readOnly 37 | placeholder="Search users..." 38 | onClick={() => setOpen(true)} 39 | className="w-full h-11 px-10 text-base rounded-lg outline-none border bg-colorF7 focus:bg-white focus:border-[#3180e1] cursor-pointer placeholder:text-muted-foreground/70" 40 | /> 41 | </div> 42 | </div> 43 | 44 | <Dialog open={open} onOpenChange={setOpen}> 45 | <DialogContent className="sm:max-w-[600px]"> 46 | <DialogHeader> 47 | <DialogTitle className="text-xl font-semibold"> 48 | Search Users 49 | </DialogTitle> 50 | </DialogHeader> 51 | <div className="space-y-6"> 52 | <Input 53 | placeholder="Type here user name or email..." 54 | value={searchQuery} 55 | onChange={(e) => handleSearch(e.target.value)} 56 | className="w-full h-11 text-base" 57 | /> 58 | 59 | <div className="max-h-[400px] overflow-y-auto"> 60 | {error ? ( 61 | <div className="flex flex-col items-center justify-center py-8"> 62 | <p className="text-muted-foreground mt-2"> 63 | Error fetching users 64 | </p> 65 | </div> 66 | ) : isFetching ? ( 67 | <div className="flex flex-col items-center justify-center py-8"> 68 | <Loader2 className="h-8 w-8 animate-spin text-primary" /> 69 | <p className="text-muted-foreground mt-2">Searching...</p> 70 | </div> 71 | ) : users.length > 0 ? ( 72 | <div className="space-y-2"> 73 | {users.map((user: any) => ( 74 | <div 75 | key={user.id} 76 | className="flex items-center gap-4 p-3 rounded-lg hover:bg-muted cursor-pointer transition-colors duration-200" 77 | onClick={() => { 78 | // Handle user selection here 79 | setOpen(false); 80 | }} 81 | > 82 | <img 83 | src={user.image || "/default-avatar.png"} 84 | alt={user.name} 85 | loading="eager" 86 | className="w-10 h-10 rounded-full object-cover" 87 | /> 88 | <span className="text-base font-medium">{user.name}</span> 89 | </div> 90 | ))} 91 | </div> 92 | ) : searchQuery ? ( 93 | <div className="flex flex-col items-center justify-center py-12 text-center"> 94 | <FiSearch className="h-12 w-12 text-muted-foreground mb-3" /> 95 | <p className="text-lg font-medium text-foreground"> 96 | No users found 97 | </p> 98 | <p className="text-muted-foreground mt-1"> 99 | Try searching with a different term 100 | </p> 101 | </div> 102 | ) : ( 103 | <div className="flex flex-col items-center justify-center py-12 text-center"> 104 | <FiSearch className="h-12 w-12 text-muted-foreground mb-3" /> 105 | <p className="text-lg font-medium text-foreground"> 106 | Start typing to search users 107 | </p> 108 | <p className="text-muted-foreground mt-1"> 109 | Search results will appear here 110 | </p> 111 | </div> 112 | )} 113 | </div> 114 | </div> 115 | </DialogContent> 116 | </Dialog> 117 | </> 118 | ); 119 | } 120 | 121 | export default SearchUserInput; 122 | -------------------------------------------------------------------------------- /src/components/TopPageNavbar.tsx: -------------------------------------------------------------------------------- 1 | import { AiFillGithub } from "react-icons/ai" 2 | import { SlideBarOuterButton } from "./button/SlideBarResponsiveExitButton" 3 | import { TiSocialLinkedin, TiSocialTwitter } from "react-icons/ti" 4 | import Image from "next/image" 5 | import { getAuthSession } from "@/lib/auth" 6 | import { FC } from "react" 7 | import Link from "next/link" 8 | 9 | interface TopPageNavbarprop{ 10 | title: string 11 | } 12 | 13 | const TopPageNavbar:FC<TopPageNavbarprop> = async ({title}) =>{ 14 | 15 | const session = await getAuthSession() 16 | 17 | 18 | 19 | return( 20 | <div className="my-0 mx-auto relative flex items-center w-full max-w-[1040px] z-10"> 21 | {/* ARROW ICON */} 22 | <SlideBarOuterButton /> 23 | <h1 className='text-[#393a4f] font-bold text-2xl hidden sm:block '>{title} Page</h1> 24 | 25 | <div className="flex ml-auto items-center "> 26 | <div className="px-[6px] flex items-center py-2 grow-0 shrink-0 text-[#4a4a4a] relative "> 27 | <Link href={'https://www.linkedin.com/in/taqui-imam-88a7b325a/'} className="relative flex justify-center items-center w-[38px] h-[38px] rounded-sm decoration-transparent "> 28 | <TiSocialTwitter className="h-5 w-5 text-[#999] hover:text-[#3180e1] transition-all" /> 29 | </Link> 30 | </div> 31 | 32 | <div className="px-[6px] flex items-center py-2 grow-0 shrink-0 text-[#4a4a4a] relative "> 33 | 34 | <Link href={"https://www.linkedin.com/in/taqui-imam-88a7b325a/"} className="relative flex justify-center items-center w-[38px] h-[38px] rounded-sm decoration-transparent"> 35 | <TiSocialLinkedin className="h-5 w-5 text-[#999] hover:text-[#3180e1] transition-all" /> 36 | </Link> 37 | </div> 38 | <div className="px-[6px] flex items-center py-2 grow-0 shrink-0 text-[#4a4a4a] relative "> 39 | <Link href={"https://github.com/taqui-786"} className="relative flex justify-center items-center w-[38px] h-[38px] rounded-sm decoration-transparent "> 40 | 41 | <AiFillGithub className="h-5 w-5 text-[#999] hover:text-[#3180e1] transition-all" /> 42 | </Link> 43 | </div> 44 | {/* profile opition --> */} 45 | <div className="flex justify-start items-center cursor-pointer grow-0 shrink-0 text-[#4a4a4a] px-3 py-2 relative"> 46 | <div className="relative max-h-[38px]"> 47 | 48 | {session?.user?.image && <Image src={session.user.image} alt='user' height={38} width={38} className='rounded-full max-h-auto w-auto' />} 49 | </div> 50 | </div> 51 | </div> 52 | 53 | </div> 54 | ) 55 | } 56 | 57 | export default TopPageNavbar -------------------------------------------------------------------------------- /src/components/button/CommentButton.tsx: -------------------------------------------------------------------------------- 1 | import { BiSolidComment } from "react-icons/bi"; 2 | import { FC } from "react"; 3 | import Link from "next/link"; 4 | interface commentButtonProps{ 5 | postId:string 6 | } 7 | const CommentButton:FC<commentButtonProps> = ({postId}) => { 8 | return ( 9 | <> 10 | <Link 11 | href={`/post/${postId}`} 12 | className=" -top-[58px] right-0 absolute h-[50px] w-[50px] text-white border border-[#6aa2e6] bg-[#6aa2e6] flex justify-center items-center rounded-full shadow-lg outline-none transition-colors " 13 | > 14 | <BiSolidComment className="h-5 w-5 " /> 15 | </Link> 16 | </> 17 | ); 18 | }; 19 | 20 | export default CommentButton; 21 | -------------------------------------------------------------------------------- /src/components/button/CreatePostExitPost.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { RxCross1 } from 'react-icons/rx' 3 | export const exitPopup = () =>{ 4 | const createPost = document.getElementById('createPost') 5 | createPost? createPost.style.display = "none" : "" 6 | 7 | } 8 | function CreatePostExitPost() { 9 | 10 | return ( 11 | <div 12 | onClick={()=>exitPopup()} className='absolute px-3 right-0'><RxCross1 className="text-[#4a4a4a] h-5 w-5 " /></div> 13 | ) 14 | } 15 | 16 | export default CreatePostExitPost -------------------------------------------------------------------------------- /src/components/button/ExitProfileBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { ImCross } from 'react-icons/im' 3 | import { Button } from '../ui/button' 4 | import { useRouter } from 'next/navigation' 5 | 6 | function ExitProfileBtn() { 7 | const router = useRouter() 8 | 9 | return ( 10 | <Button onClick={() => router.back()} variant="ghost" className='outline-none hover:bg-blue-700' ><ImCross className="text-white text-lg" /> </Button> 11 | ) 12 | } 13 | 14 | export default ExitProfileBtn -------------------------------------------------------------------------------- /src/components/button/FollowButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC, useEffect, useState } from "react"; 3 | import { Button } from "../ui/button"; 4 | import { AiOutlinePlus } from "react-icons/ai"; 5 | import axios, { AxiosError } from "axios"; 6 | import { FollowUserRequest } from "@/types/PostLikeValidator"; 7 | import { useMutation } from "@tanstack/react-query"; 8 | import toast from "react-hot-toast"; 9 | 10 | 11 | interface FollowButtonprops { 12 | myId?: string; 13 | toFollow?: any; 14 | isFollowed?: any; 15 | } 16 | 17 | const FollowButton: FC<FollowButtonprops> = ({ 18 | myId, 19 | toFollow, 20 | isFollowed, 21 | }) => { 22 | const [isFollow, setIsFollow] = useState<boolean>(false); 23 | 24 | useEffect(() => { 25 | if (isFollowed) { 26 | setIsFollow(true); 27 | } else { 28 | setIsFollow(false); 29 | } 30 | }, [isFollowed]); 31 | 32 | const { mutate: follow, isLoading } = useMutation({ 33 | mutationFn: async () => { 34 | const payload: FollowUserRequest = { 35 | toFollowId: toFollow, 36 | }; 37 | await axios.post("/api/user/follow", payload); 38 | }, 39 | mutationKey: ['follow',toFollow], 40 | onError: (err) => { 41 | setIsFollow(false); 42 | if (err instanceof AxiosError) { 43 | if (err.response?.status === 401) { 44 | return toast.error(" Login First. "); 45 | } 46 | } 47 | 48 | return toast.error(" Unable to Follow "); 49 | }, 50 | onMutate: () => { 51 | if (isFollow) { 52 | setIsFollow(false); 53 | } else { 54 | setIsFollow(true); 55 | } 56 | }, 57 | }); 58 | 59 | return ( 60 | <Button 61 | onClick={() => follow()} 62 | className={`ml-auto py-[14px] px-[18px] `} 63 | variant="subtle" 64 | isLoading={isLoading} 65 | disabled={myId === toFollow} 66 | > 67 | {!isFollow ? ( !isLoading ? <AiOutlinePlus className="h-3 w-3 mr-2" /> : '') :''} 68 | {/* {followed} */} 69 | {isFollow ? "Following" : "Follow"} 70 | </Button> 71 | ); 72 | }; 73 | 74 | export default FollowButton; 75 | -------------------------------------------------------------------------------- /src/components/button/LogOutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { signOut } from "next-auth/react" 3 | import { FiLogOut } from "react-icons/fi" 4 | 5 | 6 | const LogOutButton = () =>{ 7 | 8 | return( 9 | <div 10 | onClick={()=> signOut({callbackUrl:`${window.location.origin}/signin`})} 11 | className='flex text-[#393a4f] items-center py-3 px-8 border-l-[5px] border-l-transparent cursor-pointer ' > 12 | <FiLogOut className="h-5 w-5 mr-4 text-[#a2a5b9]" /> 13 | <span className=' text-base'>Logout</span> 14 | </div> 15 | ) 16 | } 17 | 18 | export default LogOutButton -------------------------------------------------------------------------------- /src/components/button/LoginGoogleBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC, useState } from "react"; 3 | import { Button } from "../ui/button"; 4 | import { FcGoogle } from "react-icons/fc"; 5 | import { signIn } from "next-auth/react"; 6 | import toast from "react-hot-toast"; 7 | 8 | const LoginGoogleBtn: FC = () => { 9 | const [loading, setLoading] = useState<boolean>(false); 10 | const loginwithGoogle = async () => { 11 | setLoading(true); 12 | try { 13 | await signIn("google"); 14 | } catch (error) { 15 | console.log(error); 16 | } finally { 17 | // LOADING OR ANY OTHER STUF 18 | setLoading(false); 19 | toast.success(" Good to go. "); 20 | } 21 | }; 22 | 23 | return ( 24 | <> 25 | 26 | 27 | <Button 28 | variant="outline" 29 | className="w-full" 30 | isLoading={loading} 31 | onClick={loginwithGoogle} 32 | > 33 | <svg 34 | className="mr-2 h-4 w-4" 35 | aria-hidden="true" 36 | focusable="false" 37 | data-prefix="fab" 38 | data-icon="google" 39 | role="img" 40 | xmlns="http://www.w3.org/2000/svg" 41 | viewBox="0 0 488 512" 42 | > 43 | <path 44 | fill="currentColor" 45 | d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z" 46 | ></path> 47 | </svg> 48 | {!loading ? ( 49 | "Continue with Google" 50 | ) : ( 51 | "please wait..." 52 | )} 53 | </Button> 54 | </> 55 | ); 56 | }; 57 | 58 | export default LoginGoogleBtn; 59 | -------------------------------------------------------------------------------- /src/components/button/NewUserButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { HandleNext } from "@/lib/Functions"; 3 | import { Button } from "../ui/button"; 4 | import { BsArrowLeft, BsArrowRight } from "react-icons/bs"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | export const AccountTypeSubmitBtn = (props: { value: number }) => { 8 | return ( 9 | <Button 10 | onClick={() => HandleNext(props.value)} 11 | variant={"custom"} 12 | size={"lg"} 13 | className={`mt-2 px-16 max-[640px]:absolute max-[640px]:top-0 max-[640px]:left-0 max-[640px]:m-0 max-[640px]:h-full max-[640px]:w-full max-[640px]:opacity-0 `} 14 | > 15 | Continue 16 | </Button> 17 | ); 18 | }; 19 | 20 | export const NewUserNextStepBtn = (props: {next: number , prev:number , disable:boolean}) =>{ 21 | 22 | const params = usePathname() 23 | 24 | 25 | return( 26 | <div className={`p-5 items-center flex-wrap justify-end gap-1 ${params === '/newuser' ? 'flex' : 'hidden'}`}> 27 | <Button onClick={() => HandleNext(props.prev)} size={'sm'} variant={'outline'} > 28 | <BsArrowLeft className='mx-2' /> 29 | Back 30 | </Button> 31 | <Button disabled={props.disable} onClick={() => HandleNext(props.next)} size={'sm'} variant={'outline'} className="bg-green-100 text-primary hover:bg-green-200" > 32 | Next 33 | <BsArrowRight className="mx-2" /> 34 | </Button> 35 | </div> 36 | ) 37 | } -------------------------------------------------------------------------------- /src/components/button/PostLikeBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { usePrevious } from '@mantine/hooks' 3 | import { useMutation } from '@tanstack/react-query' 4 | import React,{FC, useEffect, useState} from 'react' 5 | import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai' 6 | import {PostVoteRequest} from '@/types/PostLikeValidator' 7 | import axios, { AxiosError } from 'axios' 8 | import { Button } from '../ui/button' 9 | import toast from 'react-hot-toast' 10 | import { Loader } from 'lucide-react' 11 | 12 | interface postlikebtnprops{ 13 | postData: any 14 | isLiked?: any 15 | initialLikeAmt:any 16 | } 17 | 18 | const PostLikeBtn:FC<postlikebtnprops> = ({postData,isLiked,initialLikeAmt}) => { 19 | const postId = postData?.id 20 | 21 | const prevLike = usePrevious(initialLikeAmt) 22 | const [votesAmt, setVotesAmt] = useState<number>(initialLikeAmt) 23 | const [liked,setLiked] = useState<boolean>(false) 24 | // CHECKING USER HAD LIKED POST OR NOT 25 | useEffect(()=>{ 26 | if(!isLiked){ 27 | setLiked(false) 28 | }else{ 29 | setLiked(true) 30 | } 31 | },[isLiked]) 32 | 33 | useEffect(()=>{ 34 | setVotesAmt(initialLikeAmt) 35 | },[initialLikeAmt]) 36 | 37 | const {mutate: like , isLoading} = useMutation({ 38 | 39 | mutationFn: async () => { 40 | const payload: PostVoteRequest ={ 41 | postId: postId 42 | } 43 | await axios.post('/api/user/post/newlike',payload) 44 | 45 | }, 46 | mutationKey: ['like',postId], 47 | onError: (err) => { 48 | setVotesAmt(prevLike) 49 | if (err instanceof AxiosError) { 50 | if (err.response?.status === 401) { 51 | return toast.error(" Login First. "); 52 | 53 | } 54 | } 55 | 56 | return toast.error(" Unable to Like "); 57 | }, 58 | onMutate: () =>{ 59 | if( !liked){ 60 | setLiked(true) 61 | setVotesAmt((prev) => prev + 1) 62 | }else{ 63 | setLiked(false) 64 | setVotesAmt((prev) => prev - 1) 65 | } 66 | }, 67 | 68 | 69 | }) 70 | 71 | 72 | return ( 73 | <> 74 | 75 | {/* Like count */} 76 | <div className="relative flex justify-start items-center mx-1 "> 77 | <Button disabled={isLoading} onClick={()=> like()} variant="ghost" className=" -top-[58px] -left-4 absolute h-[50px] w-[50px] border border-[crimson] bg-white flex justify-center items-center rounded-full shadow-lg outline-none transition-colors " style={{color:!liked?"crimson":"#fff",backgroundColor:!liked?"#fff":"crimson"}}> 78 | 79 | <AiFillHeart className="h-[30px] w-[30px] " /> 80 | </Button> 81 | 82 | <Button className='h-fit w-fit p-0 outline-none' variant="ghost" >{isLoading ? <Loader className="h-[18px] w-[18px] text-gray-500 animate-spin " /> : <AiFillHeart className="h-[18px] w-[18px] " style={{color:!liked?"#888da8":"crimson"}} />}</Button> 83 | 84 | <span className="block text-sm mx-[6px]">{votesAmt}</span> 85 | </div> 86 | 87 | </> 88 | ) 89 | } 90 | 91 | export default PostLikeBtn -------------------------------------------------------------------------------- /src/components/button/PostLikeServerside.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthSession } from "@/lib/auth"; 2 | import { Comment, Like, Post } from "@prisma/client"; 3 | import { notFound } from "next/navigation"; 4 | import { AiOutlineHeart } from "react-icons/ai"; 5 | import { BiComment } from "react-icons/bi"; 6 | 7 | interface PostVoteServerProps { 8 | postId: string; 9 | initialVotesAmt?: number; 10 | initialVote?: any; 11 | getData?: () => Promise<(Post & { like: Like[] } & { comments: Comment[] }) | null>; 12 | } 13 | 14 | const PostLikeServer = async ({ 15 | postId, 16 | initialVotesAmt, 17 | initialVote, 18 | getData, 19 | }: PostVoteServerProps) => { 20 | const session = await getAuthSession(); 21 | 22 | let _votesAmt: number = 0; 23 | let _commentAmt: number = 0; 24 | let _currentVote: Like[]; 25 | 26 | if (getData) { 27 | // fetch data in component 28 | const post = await getData(); 29 | if (!post) return notFound(); 30 | 31 | 32 | _votesAmt = post.like.length; 33 | _commentAmt = post.comments.length; 34 | 35 | // @ts-ignore 36 | _currentVote = post.like.find((vote) => vote.userId === session?.user?.id); 37 | } else { 38 | // passed as props 39 | _votesAmt = initialVotesAmt!; 40 | _currentVote = initialVote; 41 | } 42 | 43 | return ( 44 | <> 45 | <div className="flex items-stretch "> 46 | <div className="flex justify-start items-center text-[#888da8] mx-[3px]"> 47 | <AiOutlineHeart className="h-[15px] w-[15px] " /> 48 | <span className="block text-sm mx-1 ">{_votesAmt}</span> 49 | </div> 50 | 51 | <div className="flex justify-start items-center text-[#888da8] mx-[3px]"> 52 | <BiComment className="h-[15px] w-[15px] " /> 53 | <span className="block text-sm mx-1 ">{_commentAmt}</span> 54 | </div> 55 | </div> 56 | <div className="flex items-stretch ml-auto"> 57 | <div className="flex justify-start items-center mx-1 text-[#888da8]"> 58 | <span className="block text-sm mx-1 ">{_commentAmt}</span> 59 | <span className="block text-sm mx-[2px]"> 60 | <small>Comments</small> 61 | </span> 62 | </div> 63 | </div> 64 | </> 65 | ); 66 | }; 67 | 68 | export default PostLikeServer; 69 | -------------------------------------------------------------------------------- /src/components/button/SettingBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Link from "next/link"; 3 | import { usePathname } from "next/navigation"; 4 | import { AiOutlineSetting } from "react-icons/ai"; 5 | 6 | const SettingBtn = () => { 7 | const router = usePathname() 8 | return ( 9 | <Link 10 | href="/settings" 11 | 12 | className={`flex text-[#393a4f] items-center py-3 px-8 border-l-[5px] border-l-transparent ${router === '/settings' ? 'slide_link_active' :'' } `} 13 | > 14 | <AiOutlineSetting className="h-5 w-5 mr-4 text-[#a2a5b9]" /> 15 | <span className=" text-base">Settings</span> 16 | </Link> 17 | ); 18 | }; 19 | 20 | export default SettingBtn; 21 | -------------------------------------------------------------------------------- /src/components/button/SlideBarResponsiveExitButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useState } from 'react' 3 | import { AiOutlineArrowLeft } from 'react-icons/ai' 4 | import { IoIosArrowBack } from 'react-icons/io' 5 | import { RiMenu2Fill } from 'react-icons/ri' 6 | 7 | export const SlideBarInnerButton = () => { 8 | const handleResponsiveButton =() =>{ 9 | const slidebar = document.getElementById('slidebar') 10 | slidebar ? slidebar.style.transform = 'translate(-100%)' : "" 11 | } 12 | 13 | return ( 14 | <button onClick={handleResponsiveButton}> <AiOutlineArrowLeft className="absolute top-[10px] right-3 block items-center justify-center h-8 w-8 outline-none border-none md:hidden " /></button> 15 | 16 | ) 17 | } 18 | export const SlideBarOuterButton = () => { 19 | const [open,setOpen] = useState<boolean>(false) 20 | const hideSlidebar = () =>{ 21 | const slidebar = document.getElementById('slidebar') 22 | const homePage = document.getElementById('homePage') 23 | if(!open){ 24 | slidebar?.classList.add('slidebar_hide') 25 | homePage?.classList.replace('home_width','homepage_full') 26 | setOpen(true) 27 | }else{ 28 | slidebar?.classList.remove('slidebar_hide') 29 | homePage?.classList.replace('homepage_full','home_width') 30 | setOpen(false) 31 | } 32 | } 33 | const handleResponsiveButton =() =>{ 34 | const slidebar = document.getElementById('slidebar') 35 | slidebar ? slidebar.style.transform = 'translate(0)' : "" 36 | } 37 | 38 | return ( 39 | <div className="mr-4 cursor-pointer block" onClick={hideSlidebar} > 40 | {!open && <span className="h-fit w-fit hidden md:block" onClick={hideSlidebar} ><IoIosArrowBack className="h-7 w-7 text-[#3180e1] " /></span>} 41 | {open && <span className="h-fit w-fit hidden md:block" onClick={hideSlidebar} ><RiMenu2Fill className="h-7 w-7 text-[#3180e1] " /> </span>} 42 | <span className="h-fit w-fit block md:hidden" onClick={handleResponsiveButton} ><RiMenu2Fill className="h-7 w-7 text-[#3180e1] " /> </span> 43 | 44 | </div> 45 | ) 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/feed/CreatePostActivator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from 'next/image' 4 | import React,{FC} from 'react' 5 | interface CreatePostActivatorprops{ 6 | image:string 7 | } 8 | 9 | const CreatePostActivator:FC<CreatePostActivatorprops> = ({image}) => { 10 | 11 | const popupCreatePost = () =>{ 12 | const createPost = document.getElementById('createPost') 13 | createPost? createPost.style.display = "flex" : "" 14 | } 15 | 16 | 17 | return ( 18 | <div className="relative mb-4 sm:mb-6 border border-[#e8e8e8] bg-white cursor-text rounded-lg sm:rounded-xl shadow-none max-w-full text-[#4a4a4a]" 19 | onClick={popupCreatePost} > 20 | <div className="rounded-lg sm:rounded-xl"> 21 | <div className="p-2 sm:p-4 border border-[#e8e8e8]"> 22 | <div className="flex justify-start items-start"> 23 | { image && <Image src={image} alt='user' height={44} width={44} className='rounded-full w-8 h-8 sm:w-11 sm:h-11 border' loading='eager' priority />} 24 | <div className="ml-3 sm:ml-4 bg-[#f7f7f7] rounded-md border border-[#e8e8e8] w-full clear-both relative"> 25 | <h3 className='text-[0.8rem] sm:text-[0.9rem] mt-2 sm:mt-[13px] mb-6 sm:mb-[51px] ml-3 sm:ml-[18px] mr-2 sm:mr-[9px]'>Write Something here to create your post...</h3> 26 | </div> 27 | </div> 28 | </div> 29 | </div> 30 | </div> 31 | ) 32 | } 33 | 34 | export default CreatePostActivator -------------------------------------------------------------------------------- /src/components/feed/FeedColumn.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useInfiniteQuery } from "@tanstack/react-query" 3 | import axios from "axios" 4 | import { useSession } from "next-auth/react" 5 | import { FC, useEffect, useRef } from "react" 6 | import MyPost from "./MyPost" 7 | import { useIntersection } from '@mantine/hooks' 8 | import { INFINITE_SCROLL_PAGINATION_RESULTS } from "@/config" 9 | import { ExtendedPost } from "@/types/db" 10 | import { Loader2 } from "lucide-react" 11 | 12 | interface postfeedprops { 13 | initialPosts: ExtendedPost[] 14 | } 15 | 16 | 17 | const FeedColumn: FC<postfeedprops> = ({ initialPosts }) => { 18 | 19 | const lastPostRef = useRef<HTMLElement>(null) 20 | const { ref, entry } = useIntersection({ 21 | root: lastPostRef.current, 22 | threshold: 0.5, 23 | }) 24 | const { data: session } = useSession() 25 | 26 | const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery( 27 | ['infinite-query'], 28 | async ({ pageParam = 1 }) => { 29 | const query = 30 | `/api/user/post?limit=${INFINITE_SCROLL_PAGINATION_RESULTS}&page=${pageParam}` 31 | 32 | const { data } = await axios.get(query) 33 | return data as ExtendedPost[] 34 | }, 35 | 36 | { 37 | getNextPageParam: (_, pages) => { 38 | return pages.length + 1 39 | }, 40 | initialData: { pages: [initialPosts], pageParams: [1] }, 41 | } 42 | ) 43 | 44 | useEffect(() => { 45 | if (entry?.isIntersecting) { 46 | fetchNextPage() // Load more posts when the last post comes into view 47 | } 48 | }, [entry, fetchNextPage]) 49 | 50 | const posts = data?.pages.flatMap((page) => page) ?? initialPosts 51 | 52 | // console.log(isFetchingNextPage); 53 | 54 | return ( 55 | 56 | <> 57 | <ul className='flex flex-col col-span-2 space-y-6'> 58 | 59 | { 60 | posts.map((post, index) => { 61 | 62 | // CHECKING USER HAD LIKED THIS POST OR NOT 63 | const isLiked = post?.like?.find( 64 | (vote) => vote.userId === session?.user.id 65 | ) 66 | // LIKES AMOUNT 67 | const LikeAmount = post?.like.length 68 | // COMMENTS COUNT 69 | const commentLength = post?.comments.length 70 | 71 | if (index === posts.length - 1) { 72 | // Add a ref to the last post in the list 73 | return ( 74 | <li key={post.id} 75 | ref={ref} 76 | > 77 | <MyPost 78 | post={post} 79 | key={index} 80 | isPostLiked={isLiked} 81 | commentLength={commentLength} 82 | LikeAmt={LikeAmount} 83 | /> 84 | </li> 85 | ) 86 | } else { 87 | return <MyPost 88 | post={post} 89 | key={index} 90 | isPostLiked={isLiked} 91 | commentLength={commentLength} 92 | LikeAmt={LikeAmount} 93 | /> 94 | } 95 | }) 96 | } 97 | {isFetchingNextPage && ( 98 | <li className='flex justify-center mb-3 p-4'> 99 | <Loader2 className='w-6 h-6 text-2xl text-[#3180e1] animate-spin' /> 100 | </li> 101 | )} 102 | </ul> 103 | </> 104 | 105 | 106 | 107 | 108 | 109 | ) 110 | } 111 | 112 | 113 | 114 | 115 | 116 | export default FeedColumn -------------------------------------------------------------------------------- /src/components/feed/FeedPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FeedPostsBox from './FeedPostsBox' 3 | import TopPageNavbar from '../TopPageNavbar' 4 | 5 | async function FeedPage() { 6 | return ( 7 | <> 8 | <div id='homePage' className='relative ml-[280px] pt-6 py-[60px] px-[12px] home_width ' > 9 | <div className="max-w-[1040px] m-auto relative grow w-auto"> 10 | {/* TOP NAV TOOLBAR */} 11 | <TopPageNavbar title='Home' /> 12 | {/* POST FEED */} 13 | <FeedPostsBox/> 14 | </div> 15 | {/* ACTIVITY FEED -> */} 16 | </div> 17 | 18 | </> 19 | 20 | ) 21 | } 22 | 23 | export default FeedPage -------------------------------------------------------------------------------- /src/components/feed/FeedPostsBox.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthSession } from '@/lib/auth' 2 | import React from 'react' 3 | import CreatePostActivator from './CreatePostActivator' 4 | import FeedColumn from './FeedColumn' 5 | import { db } from '@/lib/Prisma.db' 6 | import { INFINITE_SCROLL_PAGINATION_RESULTS } from '@/config' 7 | import { notFound } from 'next/navigation' 8 | import SuggestUsers from './SuggestUsers' 9 | 10 | async function FeedPostsBox() { 11 | const session = await getAuthSession() 12 | const myPost = await db.post.findMany({ 13 | include:{ 14 | like: true, 15 | author: true, 16 | comments: true, 17 | }, 18 | orderBy: { 19 | createdAt: 'desc' 20 | }, 21 | take: INFINITE_SCROLL_PAGINATION_RESULTS, 22 | }) 23 | 24 | if (!myPost) return notFound() 25 | return ( 26 | <div className='py-5 px-0'> 27 | <div className="-mx-3 -mt-3 last:-mb-3 md:flex "> 28 | {/* MIDDLE COLUMN --> */} 29 | <div className="block basis-0 grow shrink p-3 max-h-[calc(100dvh-100px)] w-full md:flex-none md:w-[66.66666674%] overflow-y-auto hidescrollbar "> 30 | { session?.user?.image && <CreatePostActivator image={session?.user?.image} />} 31 | {/* @ts-ignore */} 32 | {myPost && <FeedColumn initialPosts={myPost} />} 33 | </div> 34 | {/* RIGHT COLUMN --> */} 35 | <div className="hidden basis-0 grow shrink p-3 md:block md:flex-none md:w-[33.33333337%] "> 36 | <SuggestUsers/> 37 | </div> 38 | </div> 39 | </div> 40 | ) 41 | } 42 | 43 | export default FeedPostsBox -------------------------------------------------------------------------------- /src/components/feed/MyPost.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import React, { FC, useState } from "react"; 4 | import EditorOutput from "../EditorOutput"; 5 | import { BiSolidComment } from "react-icons/bi"; 6 | import PostLikeBtn from "../button/PostLikeBtn"; 7 | import { Post, User } from "@prisma/client"; 8 | import CommentButton from "../button/CommentButton"; 9 | import { format } from 'timeago.js' 10 | interface mypostProps { 11 | post: Post & { 12 | author: User; 13 | 14 | }; 15 | commentLength?:number 16 | isPostLiked?: any; 17 | LikeAmt?:any 18 | } 19 | const MyPost: FC<mypostProps> = ({ post, isPostLiked ,commentLength,LikeAmt}) => { 20 | return ( 21 | <> 22 | <div className="relative mb-6 border border-[#e8e8e8] bg-white rounded-[.85rem] text-[#4a4a4a] max-w-full max-h-fit "> 23 | <div> 24 | {/* HEAD */} 25 | <div className="flex justify-start items-center pt-4 px-4 pb-0"> 26 | <div className="flex justify-start items-center"> 27 | <div className="relative block rounded-full bg-gray-500"> 28 | {post?.author.image && ( 29 | <Image 30 | src={post.author.image} 31 | alt="post" 32 | height={42} 33 | width={42} 34 | loading="eager" 35 | priority 36 | className="block rounded-full w-[42px] h-[42px] max-h-[42px]" 37 | /> 38 | )} 39 | </div> 40 | <div className="py-0 px-[10px] flex flex-col"> 41 | <Link 42 | href={`/profile/${post.authorId}`} 43 | className="text-[0.9rem] text-[#393a4f] font-medium" 44 | > 45 | {post.author.username} 46 | </Link> 47 | <span className="text-[#999] text-[0.8rem]">{format(post.createdAt)}</span> 48 | </div> 49 | </div> 50 | <div className="ml-auto inline-flex relative align-top"> 51 | {/* THREE DOT ICON AND DROPDOWN */} 52 | </div> 53 | </div> 54 | {/* BODY */} 55 | <div className="px-4 pt-4 pb-0"> 56 | <Link 57 | href={`/post/${post.id}`} 58 | className="relative mx-h-80 overflow-hidden decoration-transparent" 59 | > 60 | <div> 61 | <h2 className="text-[#222] text-lg font-semibold "> 62 | {post?.title} 63 | </h2> 64 | </div> 65 | <EditorOutput content={post?.content} /> 66 | </Link> 67 | </div> 68 | {/* FOOTER */} 69 | <div className="flex justify-center items-center p-4 m-0 "> 70 | <div className="ml-3 "> 71 | <div> 72 | {/* <Link 73 | className="text-[.8rem] text-[#393a4f] font-medium" 74 | href={"/"} 75 | > 76 | Milly 77 | </Link> 78 | , 79 | <Link 80 | className="text-[.8rem] text-[#393a4f] font-medium" 81 | href={"/"} 82 | > 83 | 84 | </Link> */} 85 | <p className="text-[.7rem] text-[#888da8]"> 86 | © FRIENDZ 87 | </p> 88 | </div> 89 | </div> 90 | <div className="ml-auto relative flex items-stretch"> 91 | {/* Like count */} 92 | <PostLikeBtn postData={post} isLiked={isPostLiked} initialLikeAmt={LikeAmt} /> 93 | <CommentButton postId={post.id} /> 94 | {/* comment count */} 95 | <div className="flex justify-start text-[#888da8] items-center mx-1"> 96 | <BiSolidComment className="h-[18px] w-[18px]" /> 97 | <span className="block text-sm mx-[6px]">{commentLength}</span> 98 | </div> 99 | </div> 100 | </div> 101 | </div> 102 | {/* COMMENT WILL BE HERE --> */} 103 | </div> 104 | </> 105 | ); 106 | }; 107 | 108 | export default MyPost; 109 | -------------------------------------------------------------------------------- /src/components/feed/ProfilePostsColumn.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import MyPost from "./MyPost" 3 | import { getAuthSession } from "@/lib/auth" 4 | 5 | 6 | interface ProfilePostsColumnprops{ 7 | profilePosts: any 8 | } 9 | const ProfilePostsColumn:FC<ProfilePostsColumnprops> = async({profilePosts}) =>{ 10 | const session = await getAuthSession() 11 | 12 | 13 | return( 14 | <> 15 | { 16 | profilePosts.map((posts:any,index:number)=>{ 17 | 18 | const isLiked = posts?.like?.find( 19 | (vote:any) => vote.userId === session?.user.id 20 | ) 21 | const LikeAmt = posts?.like.length 22 | const CommentAmt = posts?.comments.length 23 | return( 24 | 25 | <MyPost 26 | post={posts} 27 | key={index} 28 | isPostLiked={isLiked} 29 | LikeAmt={LikeAmt} 30 | commentLength={CommentAmt} 31 | 32 | /> 33 | 34 | 35 | ) 36 | }) 37 | } 38 | 39 | 40 | 41 | </> 42 | ) 43 | } 44 | 45 | 46 | export default ProfilePostsColumn -------------------------------------------------------------------------------- /src/components/feed/SuggestUsers.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthSession } from "@/lib/auth"; 2 | import UsersSuggested from "./UsersSuggested"; 3 | import { db } from "@/lib/Prisma.db"; 4 | 5 | const SuggestUsers = async () => { 6 | const session = await getAuthSession(); 7 | const randomIndex = Math.floor(Math.random() * 5); 8 | const users = await db.user.findMany({ 9 | // skip: randomIndex, 10 | where:{ 11 | id:{ 12 | not:session?.user.id 13 | }, 14 | NOT:{ 15 | followers:{ 16 | some:{ 17 | followerId:session?.user.id 18 | } 19 | } 20 | } 21 | }, 22 | include: { 23 | followers: true, 24 | }, 25 | 26 | 27 | }); 28 | const randomUsers = users.sort(() => 0.5 - Math.random()).slice(0, 5); 29 | 30 | return ( 31 | <> 32 | <div className="relative mb-6 border border-[#e8e8e8] bg-white rounded-lg text-[#4a4a4a] max-w-full"> 33 | <div className="border-b border-b-[#e8e8e8] flex justify-start items-center p-4"> 34 | <h4 className="text-sm text-[#757a91] font-normal"> 35 | Suggested Friends 36 | </h4> 37 | </div> 38 | <div className=""> 39 | {/* USERS */} 40 | {randomUsers.map((val) => { 41 | const isFollowed = val.followers.find((is) => is.followerId === session?.user.id) 42 | 43 | if(!isFollowed){ 44 | return ( 45 | <UsersSuggested 46 | key={val.id} 47 | user={{ 48 | id: val.id, 49 | name: val.name, 50 | username: val.username, 51 | image: val.image, 52 | }} 53 | followers={val.followers} 54 | sessionid={session?.user?.id || "12"} 55 | /> 56 | ); 57 | } 58 | 59 | })} 60 | </div> 61 | </div> 62 | </> 63 | ); 64 | }; 65 | 66 | export default SuggestUsers; 67 | -------------------------------------------------------------------------------- /src/components/feed/UsersSuggested.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client" 2 | import Image from "next/image" 3 | import { FC } from "react" 4 | import FollowButton from "../button/FollowButton" 5 | import Link from "next/link" 6 | 7 | type usersuggestedprops ={ 8 | user: Pick<User, 'image'| 'name' | 'username' | 'id'> 9 | followers:any 10 | sessionid:string 11 | } 12 | 13 | const UsersSuggested:FC<usersuggestedprops> = ({user,followers,sessionid}) =>{ 14 | 15 | let isUserFollowed = followers.find( 16 | (val:any) => val.followerId === sessionid 17 | ); 18 | 19 | return( 20 | <div className="flex justify-start items-center p-4 hover:bg-[#f2f2f2]"> 21 | <Link href={`/profile/${user.id}`} className="decoration-transparent h-fit w-fit flex" > 22 | <Image src={user.image || ''} alt="user" height={40} width={40} loading="eager" priority className="max-h-[40px] rounded-full h-10 w-10 " /> 23 | <div className="px-[10px]"> 24 | <span className="text-sm text-[#393a4f] font-medium block ">{user.username}</span> 25 | <span className="text-xs text[#757a91] block">{user.name}</span> 26 | </div> 27 | </Link> 28 | <div className="flex justify-end items-center w-9 h-9 ml-auto rounded-md transition-all"> 29 | <FollowButton isFollowed={isUserFollowed} myId={sessionid} toFollow={user.id} /> 30 | </div> 31 | </div> 32 | ) 33 | } 34 | 35 | 36 | export default UsersSuggested -------------------------------------------------------------------------------- /src/components/loaders/PostLoader.tsx: -------------------------------------------------------------------------------- 1 | const PostLoader = () => { 2 | return ( 3 | <div className="bg-white w-full p-5 rounded-md relative " id="PostLoader" > 4 | {/* HEADER */} 5 | <div className="flex justify-start items-center"> 6 | <div className="ml-5 w-full "> 7 | <div className="h-[10px] mb-[10px] w-[60%] rounded-smc loads"></div> 8 | <div className="h-[10px] mb-[10px] w-[40%] rounded-smc loads"></div> 9 | </div> 10 | </div> 11 | {/* BODY */} 12 | <div className="w-full mt-5 h-[300px] loads"></div> 13 | 14 | </div> 15 | ); 16 | }; 17 | 18 | export default PostLoader; 19 | -------------------------------------------------------------------------------- /src/components/newuserpage/NewUserImageUpload.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { handleImageUpload } from '@/lib/Functions' 3 | import axios from 'axios' 4 | import Image from 'next/image' 5 | import React,{useState} from 'react' 6 | import {IoAdd} from 'react-icons/io5' 7 | import { NewUserNextStepBtn } from '../button/NewUserButton' 8 | import toast from 'react-hot-toast' 9 | 10 | type NewUserImagetype = { 11 | Myimage: string 12 | } 13 | const NewUserImageUpload = ({Myimage}:NewUserImagetype) =>{ 14 | const [img,setImg] = useState<any>(Myimage) 15 | const [loading ,setLoading] = useState<boolean>(false) 16 | // FUNCTION IMAGE UPLOAD 17 | const onImageChange = async (e:React.ChangeEvent<HTMLInputElement>) => { 18 | setLoading(true) 19 | try { 20 | const [file]:any = e.target.files; 21 | if(!file) return 22 | 23 | const uploading = await handleImageUpload(file) 24 | setImg(uploading.file.url) 25 | const { data }= await axios.patch('/api/user/avatar',{uploading}) 26 | toast.success('Avatar changed.') 27 | 28 | } catch (error) { 29 | console.log(error); 30 | 31 | } finally{ 32 | setLoading(false) 33 | } 34 | }; 35 | return( 36 | <> 37 | <div className='text-center'> 38 | <div className='relative h-[120px] w-[120px] flex justify-center items-center rounded-full border-2 border-solid border-[#cecece] mx-auto '> 39 | <i className='cursor-pointer absolute top-0 right-0 flex justify-center items-center h-9 w-9 rounded-full border-2 border-solid border-[#fff] bg-[#cecece] transition-all duration-100 hover:bg-[#039be5]'> 40 | <IoAdd className='h-[18px] w-[18px] text-white stroke-2 ' /> 41 | <input type="file" accept='image/*' onChange={onImageChange} 42 | className='h-full w-full opacity-0 absolute '/> 43 | </i> 44 | <Image 45 | src={img} 46 | id="UploadImg" 47 | 48 | width={100} 49 | height={100} 50 | alt="UploadImg" 51 | loading='eager' 52 | priority 53 | className='block rounded-full h-auto w-auto max-h-[120px] ' 54 | /> 55 | </div> 56 | {loading && <span className=" z-50 my-2 relative font-medium text-base p-3 text-[#039be5]" >Uploading...</span> } 57 | <div className='mt-5 text-center'> 58 | <small className='text-sm text-[#999] '> 59 | Only images with a size lower than 3MB are allowed. 60 | </small> 61 | </div> 62 | </div> 63 | 64 | 65 | </> 66 | ) 67 | } 68 | 69 | export default NewUserImageUpload -------------------------------------------------------------------------------- /src/components/newuserpage/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { BsEmojiSmile } from "react-icons/bs"; 2 | import { AiOutlineUser } from "react-icons/ai"; 3 | import { BiLockAlt, BiFlag } from "react-icons/bi"; 4 | import { MdOutlineInsertPhoto } from "react-icons/md"; 5 | 6 | 7 | function ProgressBar() { 8 | return ( 9 | <div className="pt-[30px] max-[640px]:mx-auto max-[639px]:max-w-xs"> 10 | <div className="h-[18px] bg-white rounded-full max-w-[520px] mx-auto flex items-center"> 11 | <div className='relative mx-3 w-calc-100-min-24 h-[6px] rounded-full '> 12 | {/*track*/} <div className='track bg-[#eaeaea] w-full absolute top-0 left-0 h-full rounded-[100px]'></div> 13 | <div id="bar" style={{ width: `0%` }} className='bar bg-[#5596e6] transition-[width] duration-400 absolute top-0 left-0 h-full rounded-[100px]'> </div> 14 | {/* DOT */} 15 | <div id="first" className=' activeDot left-[-19px] absolute top-1/2 transform -translate-y-1/2 h-10 w-10 bg-white rounded-full border border-gray-300 shadow flex justify-center items-center pointer-events-none text-gray-400'> 16 | <BsEmojiSmile className='h-4 w-4 transition duration-300 stroke-current' /> 17 | </div> 18 | <div id="second" className=' secondDot absolute top-1/2 transform -translate-y-1/2 h-10 w-10 bg-white rounded-full border border-gray-300 shadow flex justify-center items-center pointer-events-none text-gray-400'> 19 | <AiOutlineUser className='h-4 w-4 transition duration-300 stroke-current' /> 20 | </div> 21 | <div id="third" className=' thirdDot absolute top-1/2 transform -translate-y-1/2 h-10 w-10 bg-white rounded-full border border-gray-300 shadow flex justify-center items-center pointer-events-none text-gray-400'> 22 | <BiLockAlt className='h-4 w-4 transition duration-300 stroke-current' /> 23 | </div> 24 | <div id="fourth" className=' fourthDot absolute top-1/2 transform -translate-y-1/2 h-10 w-10 bg-white rounded-full border border-gray-300 shadow flex justify-center items-center pointer-events-none text-gray-400'> 25 | <MdOutlineInsertPhoto className='h-4 w-4 transition duration-300 stroke-current' /> 26 | </div> 27 | <div id="fifth" className=' fifthDot absolute top-1/2 transform -translate-y-1/2 h-10 w-10 bg-white rounded-full border border-gray-300 shadow flex justify-center items-center pointer-events-none text-gray-400'> 28 | <BiFlag className='h-4 w-4 transition duration-300 stroke-current' /> 29 | </div> 30 | </div> 31 | </div> 32 | 33 | </div> 34 | ) 35 | } 36 | 37 | export default ProgressBar -------------------------------------------------------------------------------- /src/components/newuserpage/Progresstitle.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | function Progresstitle() { 4 | return ( 5 | <h2 id='signupTitle' className='font-medium text-xl animate-[fadeInUp] duration-300 block'>Welcome, select an account type.</h2> 6 | ) 7 | } 8 | 9 | export default Progresstitle -------------------------------------------------------------------------------- /src/components/newuserpage/Signup1.tsx: -------------------------------------------------------------------------------- 1 | 2 | import {SignupAsCompany, SignupAsPrivate, SignupAsPublic} from './cards/Signupcard' 3 | 4 | function Signup1() { 5 | return ( 6 | <div className='columns mt-4 min-[640px]:flex'> 7 | <SignupAsCompany/> 8 | <SignupAsPublic/> 9 | <SignupAsPrivate/> 10 | </div> 11 | ) 12 | } 13 | 14 | export default Signup1 -------------------------------------------------------------------------------- /src/components/newuserpage/Signup2.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Form, 6 | FormControl, 7 | FormDescription, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { useForm } from "react-hook-form"; 15 | import { z } from "zod"; 16 | import { UsernameValidator } from "@/lib/NewUserFormValidator"; 17 | import { zodResolver } from "@hookform/resolvers/zod"; 18 | import { NewUserNextStepBtn } from "../button/NewUserButton"; 19 | import { useMutation } from "@tanstack/react-query"; 20 | import axios, { AxiosError } from "axios"; 21 | import { useRouter } from "next/navigation"; 22 | import {toast} from "sonner"; 23 | type FormData = z.infer<typeof UsernameValidator>; 24 | function Signup2({existingUsername}:{existingUsername:string}) { 25 | const router = useRouter() 26 | const [btnDisable , setBtnDisable] = useState(!existingUsername ? true : false) 27 | const form = useForm<FormData>({ 28 | resolver: zodResolver(UsernameValidator), 29 | defaultValues: { 30 | username: existingUsername, 31 | }, 32 | 33 | }); 34 | 35 | 36 | // 2. Defining a submit handler here --. 37 | const {mutate:updateUsername , isLoading} = useMutation({ 38 | mutationKey:['updateUsername'], 39 | mutationFn: async ({ username }: FormData) => { 40 | const payload: FormData = { username } 41 | 42 | const { data } = await axios.patch(`/api/user/username/`, payload) 43 | return data 44 | }, 45 | 46 | onError: (err) => { 47 | if (err instanceof AxiosError) { 48 | if (err.response?.status === 409) { 49 | form.setError('username', { 50 | type: 'manual', 51 | message: 'Username already taken.' 52 | }) 53 | return 54 | } 55 | } 56 | form.setError('username', { 57 | type: 'manual', 58 | message: 'Something went wrong.' 59 | }) 60 | }, 61 | onSuccess: () => { 62 | setBtnDisable(false) 63 | router.refresh() 64 | toast.success('Username Changed successfully.') 65 | }, 66 | }) 67 | return ( 68 | <> 69 | <div className="w-full bg-white common_border_e8 rounded-lg p-[30px] font-sans "> 70 | <Form {...form}> 71 | <form onSubmit={form.handleSubmit((e) => updateUsername(e))} className="space-y-8"> 72 | <FormField 73 | control={form.control} 74 | name="username" 75 | render={({ field }) => ( 76 | <FormItem> 77 | <FormLabel>Create your Username</FormLabel> 78 | <FormControl> 79 | <Input placeholder="example_123" {...field} /> 80 | </FormControl> 81 | <FormDescription> 82 | This is your public display username. 83 | 84 | </FormDescription> 85 | <FormMessage className="text-red-500" /> 86 | </FormItem> 87 | )} 88 | /> 89 | <Button type="submit" variant="custom" isLoading={isLoading} >Submit</Button> 90 | </form> 91 | </Form> 92 | </div> 93 | <NewUserNextStepBtn next={2} prev={0} disable={btnDisable} /> 94 | </> 95 | ); 96 | } 97 | 98 | export default Signup2; 99 | -------------------------------------------------------------------------------- /src/components/newuserpage/Signup3.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import { NewUserNextStepBtn } from "../button/NewUserButton"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Form, 7 | FormControl, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { Textarea } from "../ui/textarea"; 15 | import { LocationAndBioValidator } from "@/lib/NewUserFormValidator"; 16 | import { useForm } from "react-hook-form"; 17 | import { zodResolver } from "@hookform/resolvers/zod"; 18 | import { z } from "zod"; 19 | import { useMutation } from "@tanstack/react-query"; 20 | import axios, { AxiosError } from "axios"; 21 | import { useRouter } from "next/navigation"; 22 | import {toast} from "sonner"; 23 | 24 | type FormData = z.infer<typeof LocationAndBioValidator>; 25 | 26 | function Signup3() { 27 | const [isDisabled, setIsDisabled] = useState(true); 28 | const router = useRouter(); 29 | const form = useForm<FormData>({ 30 | resolver: zodResolver(LocationAndBioValidator), 31 | defaultValues: { 32 | location: "", 33 | Bio: "", 34 | }, 35 | 36 | }); 37 | 38 | // 2. Define a submit handler. 39 | const { mutate: AddLocationAndBio, isLoading } = useMutation({ 40 | mutationFn: async ({ location, Bio }: FormData) => { 41 | const payload: FormData = { location, Bio }; 42 | 43 | const { data } = await axios.patch(`/api/user/aboutme/`, payload); 44 | return data; 45 | }, 46 | onError: (err) => { 47 | if (err instanceof AxiosError) { 48 | if (err.response?.status === 409) { 49 | return toast.error(' Please enter properly') 50 | } 51 | } 52 | 53 | return toast.error('Something went wrong.') 54 | 55 | }, 56 | onSuccess: () => { 57 | setIsDisabled(false); 58 | router.refresh(); 59 | toast.success('Location and Bio Added.') 60 | }, 61 | }); 62 | 63 | return ( 64 | <> 65 | <div className="w-full bg-white common_border_e8 rounded-lg p-[30px] font-sans "> 66 | <Form {...form}> 67 | <form 68 | onSubmit={form.handleSubmit((e) => AddLocationAndBio(e))} 69 | className="space-y-8" 70 | > 71 | <FormField 72 | control={form.control} 73 | name="location" 74 | render={({ field }) => ( 75 | <FormItem> 76 | <FormLabel> Location</FormLabel> 77 | <FormControl> 78 | <Input placeholder="Enter your city,state..." {...field} /> 79 | </FormControl> 80 | 81 | <FormMessage className="text-red-500" /> 82 | </FormItem> 83 | )} 84 | /> 85 | <FormField 86 | control={form.control} 87 | name="Bio" 88 | render={({ field }) => ( 89 | <FormItem> 90 | <FormLabel> About me</FormLabel> 91 | <FormControl> 92 | <Textarea 93 | placeholder="Enter your short biography" 94 | className="max-h-[27px] resize-none" 95 | {...field} 96 | /> 97 | </FormControl> 98 | 99 | <FormMessage className="text-red-500" /> 100 | </FormItem> 101 | )} 102 | /> 103 | <Button 104 | type="submit" 105 | variant="custom" 106 | isLoading={isLoading} 107 | className="mt-1" 108 | > 109 | Submit 110 | </Button> 111 | </form> 112 | </Form> 113 | </div> 114 | <NewUserNextStepBtn next={3} prev={1} disable={isDisabled} /> 115 | </> 116 | ); 117 | } 118 | 119 | export default Signup3; 120 | -------------------------------------------------------------------------------- /src/components/newuserpage/Signup4.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import { NewUserNextStepBtn } from "../button/NewUserButton" 5 | import NewUserImageUpload from "./NewUserImageUpload" 6 | 7 | async function Signup4({userImage}:{userImage:string}) { 8 | 9 | return ( 10 | <> 11 | <div className="w-full bg-white common_border_e8 rounded-lg p-[30px] font-sans "> 12 | <NewUserImageUpload Myimage={userImage} /> 13 | </div> 14 | <NewUserNextStepBtn next={4} prev={2} disable={false}/> 15 | </> 16 | ) 17 | } 18 | 19 | export default Signup4 -------------------------------------------------------------------------------- /src/components/newuserpage/Signup5.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import congratz from '../../../public/congratz.svg' 4 | function Signup5() { 5 | return ( 6 | <div className='w-full bg-white common_border_e8 rounded-lg p-[30px] font-sans '> 7 | <Image src={congratz} alt='emage' className='block max-w-[120px] mx-auto h-auto' loading='eager' priority /> 8 | <div className='text-center my-3 mx-auto max-w-[370px] '> 9 | <h3 className='text-[100%] font-semibold text-[#393a4f]'>Congratz, you successfully created your account.</h3> 10 | <p className='text-sm text-[#999]'>Thankyou for registering, Your data will be secure in our data base</p> 11 | <Link href={"/"} 12 | className='bg-white text-base no-underline py-4 px-5 rounded-lg transition-all duration-300 cursor-pointer justify-center my-5 mx-auto max-w-[280px] border-[1.4px] border-solid border-[#039be5] text-[#039be5] text-center whitespace-nowrap flex w-full hover:bg-[#039be5] hover:text-white'> Let me in</Link> 13 | </div> 14 | </div> 15 | ) 16 | } 17 | 18 | export default Signup5 -------------------------------------------------------------------------------- /src/components/newuserpage/cards/Signupcard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image" 3 | import publicImg from "../../../../public/publicaccount.svg"; 4 | import companyImg from "../../../../public/companyaccount.svg"; 5 | import privateImg from "../../../../public/privateaccount.svg"; 6 | import { AccountTypeSubmitBtn } from "@/components/button/NewUserButton"; 7 | export const SignupAsCompany = () => { 8 | return ( 9 | <div className=' block shrink grow basis-0 p-3 mt-4 '> 10 | <div className="w-full bg-white border border-gray-300 rounded-lg p-6 text-center relative "> 11 | <div className="relative max-[640px]:hidden"> 12 | <Image priority loading='eager' src={companyImg} className='max-h-[11rem]' alt='signup' sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> 13 | </div> 14 | <h3 className="font-semibold text-base py-2 font-montserrat">Company</h3> 15 | <p className="text-gray-500 text-sm"> 16 | Create a company account to be able to do some awesome things. 17 | </p> 18 | <AccountTypeSubmitBtn value={1} /> 19 | </div> 20 | </div> 21 | ) 22 | } 23 | export const SignupAsPublic = () => { 24 | return ( 25 | <div className=' block shrink grow basis-0 p-3 mt-4 '> 26 | <div className="w-full bg-white border border-gray-300 rounded-lg p-6 text-center relative"> 27 | <div className="relative max-[640px]:hidden"> 28 | <Image priority loading='eager' src={publicImg} alt='signup' className='max-h-[11rem]' sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> 29 | </div> 30 | <h3 className="font-semibold text-base py-2 font-montserrat">Public</h3> 31 | <p className="text-gray-500 text-sm"> 32 | Create a company account to be able to do some awesome things. 33 | </p> 34 | <AccountTypeSubmitBtn value={1}/> 35 | 36 | </div> 37 | </div> 38 | ) 39 | } 40 | export const SignupAsPrivate = () => { 41 | return ( 42 | <div className=' block shrink grow basis-0 p-3 mt-4 '> 43 | <div className="w-full bg-white border border-gray-300 rounded-lg p-6 text-center relative"> 44 | <div className="relative max-[640px]:hidden"> 45 | <Image priority loading='eager' src={privateImg} alt='signup' className='max-h-[11rem]' sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> 46 | </div> 47 | <h3 className="font-semibold text-base py-2 font-montserrat">Private</h3> 48 | <p className="text-gray-500 text-sm"> 49 | Create a company account to be able to do some awesome things. 50 | </p> 51 | <AccountTypeSubmitBtn value={1}/> 52 | 53 | </div> 54 | </div> 55 | ) 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/components/renderers/CustomCodeRender.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | function CustomCodeRenderer({ data }: any) { 4 | (data) 5 | 6 | return ( 7 | <pre className='bg-gray-800 rounded-md mt-4 p-4'> 8 | <code className='text-gray-100 text-sm'>{data.code}</code> 9 | </pre> 10 | ) 11 | } 12 | 13 | export default CustomCodeRenderer -------------------------------------------------------------------------------- /src/components/renderers/CustomImageRender.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FaClosedCaptioning } from "react-icons/fa"; 3 | import Image from "next/image"; 4 | import { Button } from "../ui/button"; 5 | import { 6 | Popover, 7 | PopoverContent, 8 | PopoverTrigger, 9 | } from "@/components/ui/popover"; 10 | 11 | function CustomImageRenderer({ data }: any) { 12 | const src = data.file.url; 13 | const caption = data.caption; 14 | 15 | return ( 16 | <> 17 | <div className="relative mt-3 w-full min-h-[21rem] group"> 18 | <Image alt="image" className="object-contain" fill src={src} /> 19 | </div> 20 | {caption.length >= 1 && ( 21 | <div className="h-fit w-full flex items-center justify-start ml-2 mt-1"> 22 | <Popover> 23 | <PopoverTrigger asChild> 24 | <Button variant={'ghost'} size={'lg'} > <FaClosedCaptioning className="text-2xl text-[#3d70b2]" /> </Button> 25 | 26 | </PopoverTrigger> 27 | <PopoverContent>{caption}</PopoverContent> 28 | </Popover> 29 | </div> 30 | 31 | )} 32 | </> 33 | ); 34 | } 35 | 36 | export default CustomImageRenderer; 37 | -------------------------------------------------------------------------------- /src/components/renderers/CustomListRenderer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import React from "react"; 4 | 5 | function CustomListRenderer({ data }: any) { 6 | return <div>{data.items.map((item: string, idx: number) => ( 7 | <li key={idx} className={cn({ 8 | 'list-decimal pl-4 ml-6': data.style === 'ordered', 9 | 'list-disc pl-4 ml-6': data.style === 'unordered', 10 | })} 11 | > 12 | {item} 13 | </li> 14 | ))}</div>; 15 | } 16 | 17 | export default CustomListRenderer; 18 | -------------------------------------------------------------------------------- /src/components/slidebar/SearchDropdown.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { FC } from 'react' 3 | import SearchedUsers from './SearchedUsers' 4 | import { Loader2 } from 'lucide-react' 5 | 6 | 7 | interface searchdropdownprops { 8 | users: any 9 | loading: boolean 10 | } 11 | const SearchDropdown: FC<searchdropdownprops> = ({ users, loading }) => { 12 | return ( 13 | <div className='flex absolute w-[300px] my-5 mx-0 z-50 '> 14 | <div className="relative bg-white border border-[#dcdcdc] rounded-sm shadow-md popupbox"> 15 | <div className="m-1"> 16 | { 17 | users.length > 0 ? ( 18 | users.map((val: any) => <SearchedUsers key={val.id} id={val.id} name={val.name} image={val.image} username={val.username} />) 19 | ) : ( 20 | !loading ? <div className='py-12 px-[3.65rem] w-full flex'> No User Found</div> : <div className="py-8 px-[3.65rem] w-full flex"> 21 | <Loader2 className='text-[#3180e1] animate-spin mr-1' /> searching... 22 | </div> 23 | ) 24 | } 25 | 26 | </div> 27 | </div> 28 | </div> 29 | ) 30 | } 31 | 32 | export default SearchDropdown -------------------------------------------------------------------------------- /src/components/slidebar/SearchedUsers.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import { FC } from 'react' 4 | 5 | 6 | interface searcheduserprops{ 7 | name:string, 8 | image:string, 9 | username:string 10 | id: string 11 | } 12 | 13 | const SearchedUsers:FC<searcheduserprops> = ({name,image,id,username}) => { 14 | return ( 15 | <Link href={`/profile/${id}`} className='decoration-transparent hover:bg-slate-400'> 16 | <div className="bg-white"> 17 | <div className="p-2 table-cell"> 18 | <Image src={image} alt='user' height={50} width={50} loading='eager' priority className='relative m-0 p-1 rounded-full w-[50px] h-[50px] max-h-[50px] border border-[#cecece]' /> 19 | </div> 20 | <div className="table-cell align-middle text-sm font-medium w-[170px] text-[#515365] py-0 px-[7px]"> 21 | {username} 22 | <div><small className='text-[#999] font-normal'>{name}</small></div> 23 | </div> 24 | </div> 25 | </Link> 26 | ) 27 | } 28 | 29 | export default SearchedUsers -------------------------------------------------------------------------------- /src/components/slidebar/Slidebar.tsx: -------------------------------------------------------------------------------- 1 | import { FiUser, FiUsers } from 'react-icons/fi' 2 | import React from 'react' 3 | import { AiOutlineSetting } from 'react-icons/ai' 4 | import { getAuthSession } from '@/lib/auth' 5 | import Image from 'next/image' 6 | import Link from 'next/link' 7 | import { SlideBarInnerButton } from '../button/SlideBarResponsiveExitButton' 8 | import SearchUserInput from '../SearchUserInput' 9 | import LogOutButton from '../button/LogOutButton' 10 | import SlidebarLink from './SlidebarLink' 11 | import SettingBtn from '../button/SettingBtn' 12 | async function Slidebar() { 13 | const session = await getAuthSession() 14 | 15 | return ( 16 | <div id='slidebar' className='absolute top-0 left-0 h-full w-[280px] bg-white border-r border-r-borderE3 shadow-md z-30 transition-transform myslidebar'> 17 | {/* Top section */} 18 | <div className='flex flex-col gap-6 p-8'> 19 | <SlideBarInnerButton /> 20 | <SearchUserInput/> 21 | {/* USER BLOCK */} 22 | {session?.user && 23 | <div> 24 | <Image src={session.user?.image || ''} alt="user" height={70} width={70} loading='eager' priority className='block h-[70px] w-[70px] max-w-full mb-4 rounded-full' /> 25 | <div className=""> 26 | <span className='block font-bold text-xl text-[#393a4f] font-sans '>{session.user.name}</span> 27 | <span className='block text-sm text-[#a2a5b9] font-serif '>Public</span> 28 | </div> 29 | </div> 30 | } 31 | </div> 32 | {/* Bottom section */} 33 | <div className='flex flex-col justify-between h-[calc(100%-280px)] py-8'> 34 | <ul className='list-none'> 35 | <SlidebarLink session={session} /> 36 | 37 | <li className='hover:bg-[#f2f2f2]'> 38 | <SettingBtn/> 39 | </li> 40 | <li className='hover:bg-[#f2f2f2]'> 41 | <LogOutButton/> 42 | </li> 43 | </ul> 44 | </div> 45 | </div> 46 | 47 | ) 48 | } 49 | 50 | export default Slidebar 51 | 52 | -------------------------------------------------------------------------------- /src/components/slidebar/SlidebarLink.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { usePathname } from "next/navigation" 5 | import { AiOutlineHome } from "react-icons/ai" 6 | import { CgProfile } from "react-icons/cg" 7 | import { FiUser, FiUsers } from "react-icons/fi" 8 | 9 | const SlidebarLink = ({session}:any) =>{ 10 | 11 | const router = usePathname() 12 | 13 | 14 | 15 | return( 16 | <> 17 | <li className=' hover:bg-[#f2f2f2]'> 18 | <Link href="/" className={` flex text-[#393a4f] items-center py-3 px-8 border-l-[5px] border-l-transparent ${router === '/' ? 'slide_link_active' :'' } `} > 19 | <AiOutlineHome className="h-5 w-5 mr-4 text-[#a2a5b9]" /> 20 | <span className=' text-base'>Home</span> 21 | </Link> 22 | </li> 23 | 24 | <li className=' hover:bg-[#f2f2f2]'> 25 | <Link href={`/profile/${session?.user.id}`} className={`flex text-[#393a4f] items-center py-3 px-8 border-l-[5px] border-l-transparent active:slide_link_active ${router === `/profile/${session?.user.id}` ? 'slide_link_active' :'' }`} > 26 | <CgProfile className="h-5 w-5 mr-4 text-[#a2a5b9]" /> 27 | <span className=' text-base'>Profile</span> 28 | </Link> 29 | </li> 30 | </> 31 | 32 | ) 33 | } 34 | 35 | export default SlidebarLink -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import { cva, VariantProps } from 'class-variance-authority' 3 | import { Loader2 } from 'lucide-react' 4 | import * as React from 'react' 5 | 6 | const buttonVariants = cva( 7 | 'active:scale-95 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900', 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 12 | destructive: 13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: 15 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 16 | custom: 17 | ' rounded-lg border border-input bg-background hover:bg-[#5596e6] hover:text-white hover:shadow-lg', 18 | subtle: 19 | 'border border-input bg-background hover:bg-[#6aa2e6] hover:text-white', 20 | ghost: 21 | 'bg-transparent hover:bg-zinc-100 text-zinc-800 data-[state=open]:bg-transparent data-[state=open]:bg-transparent', 22 | link: 'bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent', 23 | }, 24 | size: { 25 | default: 'h-10 py-2 px-4', 26 | sm: 'h-9 px-2 rounded-md', 27 | xs: 'h-8 px-1.5 rounded-sm', 28 | lg: 'h-11 px-8 rounded-md', 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: 'default', 33 | size: 'default', 34 | }, 35 | } 36 | ) 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes<HTMLButtonElement>, 40 | VariantProps<typeof buttonVariants> { 41 | isLoading?: boolean 42 | } 43 | 44 | const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 45 | ({ className, children, variant, isLoading, size, ...props }, ref) => { 46 | return ( 47 | <button 48 | className={cn(buttonVariants({ variant, size, className }))} 49 | ref={ref} 50 | disabled={isLoading} 51 | {...props}> 52 | {isLoading ? <Loader2 className='mr-2 h-4 w-4 animate-spin' /> : null} 53 | {children} 54 | </button> 55 | ) 56 | } 57 | ) 58 | Button.displayName = 'Button' 59 | 60 | export { Button, buttonVariants } -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes<HTMLDivElement> 8 | >(({ className, ...props }, ref) => ( 9 | <div 10 | ref={ref} 11 | className={cn( 12 | "rounded-lg border bg-card text-card-foreground shadow-sm", 13 | className 14 | )} 15 | {...props} 16 | /> 17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes<HTMLDivElement> 23 | >(({ className, ...props }, ref) => ( 24 | <div 25 | ref={ref} 26 | className={cn("flex flex-col space-y-1.5 p-6", className)} 27 | {...props} 28 | /> 29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes<HTMLDivElement> 35 | >(({ className, ...props }, ref) => ( 36 | <div 37 | ref={ref} 38 | className={cn( 39 | "text-2xl font-semibold leading-none tracking-tight", 40 | className 41 | )} 42 | {...props} 43 | /> 44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes<HTMLDivElement> 50 | >(({ className, ...props }, ref) => ( 51 | <div 52 | ref={ref} 53 | className={cn("text-sm text-muted-foreground", className)} 54 | {...props} 55 | /> 56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes<HTMLDivElement> 62 | >(({ className, ...props }, ref) => ( 63 | <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> 64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes<HTMLDivElement> 70 | >(({ className, ...props }, ref) => ( 71 | <div 72 | ref={ref} 73 | className={cn("flex items-center p-6 pt-0", className)} 74 | {...props} 75 | /> 76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = ({ 14 | className, 15 | ...props 16 | }: DialogPrimitive.DialogPortalProps) => ( 17 | <DialogPrimitive.Portal className={cn(className)} {...props} /> 18 | ) 19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName 20 | 21 | const DialogOverlay = React.forwardRef< 22 | React.ElementRef<typeof DialogPrimitive.Overlay>, 23 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> 24 | >(({ className, ...props }, ref) => ( 25 | <DialogPrimitive.Overlay 26 | ref={ref} 27 | className={cn( 28 | "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", 29 | className 30 | )} 31 | {...props} 32 | /> 33 | )) 34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 35 | 36 | const DialogContent = React.forwardRef< 37 | React.ElementRef<typeof DialogPrimitive.Content>, 38 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> 39 | >(({ className, children, ...props }, ref) => ( 40 | <DialogPortal> 41 | <DialogOverlay /> 42 | <DialogPrimitive.Content 43 | ref={ref} 44 | className={cn( 45 | "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full", 46 | className 47 | )} 48 | {...props} 49 | > 50 | {children} 51 | <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> 52 | <X className="h-4 w-4" /> 53 | <span className="sr-only">Close</span> 54 | </DialogPrimitive.Close> 55 | </DialogPrimitive.Content> 56 | </DialogPortal> 57 | )) 58 | DialogContent.displayName = DialogPrimitive.Content.displayName 59 | 60 | const DialogHeader = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes<HTMLDivElement>) => ( 64 | <div 65 | className={cn( 66 | "flex flex-col space-y-1.5 text-center sm:text-left", 67 | className 68 | )} 69 | {...props} 70 | /> 71 | ) 72 | DialogHeader.displayName = "DialogHeader" 73 | 74 | const DialogFooter = ({ 75 | className, 76 | ...props 77 | }: React.HTMLAttributes<HTMLDivElement>) => ( 78 | <div 79 | className={cn( 80 | "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 81 | className 82 | )} 83 | {...props} 84 | /> 85 | ) 86 | DialogFooter.displayName = "DialogFooter" 87 | 88 | const DialogTitle = React.forwardRef< 89 | React.ElementRef<typeof DialogPrimitive.Title>, 90 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> 91 | >(({ className, ...props }, ref) => ( 92 | <DialogPrimitive.Title 93 | ref={ref} 94 | className={cn( 95 | "text-lg font-semibold leading-none tracking-tight", 96 | className 97 | )} 98 | {...props} 99 | /> 100 | )) 101 | DialogTitle.displayName = DialogPrimitive.Title.displayName 102 | 103 | const DialogDescription = React.forwardRef< 104 | React.ElementRef<typeof DialogPrimitive.Description>, 105 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> 106 | >(({ className, ...props }, ref) => ( 107 | <DialogPrimitive.Description 108 | ref={ref} 109 | className={cn("text-sm text-muted-foreground", className)} 110 | {...props} 111 | /> 112 | )) 113 | DialogDescription.displayName = DialogPrimitive.Description.displayName 114 | 115 | export { 116 | Dialog, 117 | DialogTrigger, 118 | DialogContent, 119 | DialogHeader, 120 | DialogFooter, 121 | DialogTitle, 122 | DialogDescription, 123 | } 124 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext<FormFieldContextValue>( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> 32 | >({ 33 | ...props 34 | }: ControllerProps<TFieldValues, TName>) => { 35 | return ( 36 | <FormFieldContext.Provider value={{ name: props.name }}> 37 | <Controller {...props} /> 38 | </FormFieldContext.Provider> 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within <FormField>") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext<FormItemContextValue>( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes<HTMLDivElement> 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | <FormItemContext.Provider value={{ id }}> 81 | <div ref={ref} className={cn("space-y-2", className)} {...props} /> 82 | </FormItemContext.Provider> 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef<typeof LabelPrimitive.Root>, 89 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 | <Label 95 | ref={ref} 96 | className={cn(error && "text-destructive", className)} 97 | htmlFor={formItemId} 98 | {...props} 99 | /> 100 | ) 101 | }) 102 | FormLabel.displayName = "FormLabel" 103 | 104 | const FormControl = React.forwardRef< 105 | React.ElementRef<typeof Slot>, 106 | React.ComponentPropsWithoutRef<typeof Slot> 107 | >(({ ...props }, ref) => { 108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField() 109 | 110 | return ( 111 | <Slot 112 | ref={ref} 113 | id={formItemId} 114 | aria-describedby={ 115 | !error 116 | ? `${formDescriptionId}` 117 | : `${formDescriptionId} ${formMessageId}` 118 | } 119 | aria-invalid={!!error} 120 | {...props} 121 | /> 122 | ) 123 | }) 124 | FormControl.displayName = "FormControl" 125 | 126 | const FormDescription = React.forwardRef< 127 | HTMLParagraphElement, 128 | React.HTMLAttributes<HTMLParagraphElement> 129 | >(({ className, ...props }, ref) => { 130 | const { formDescriptionId } = useFormField() 131 | 132 | return ( 133 | <p 134 | ref={ref} 135 | id={formDescriptionId} 136 | className={cn("text-sm text-muted-foreground", className)} 137 | {...props} 138 | /> 139 | ) 140 | }) 141 | FormDescription.displayName = "FormDescription" 142 | 143 | const FormMessage = React.forwardRef< 144 | HTMLParagraphElement, 145 | React.HTMLAttributes<HTMLParagraphElement> 146 | >(({ className, children, ...props }, ref) => { 147 | const { error, formMessageId } = useFormField() 148 | const body = error ? String(error?.message) : children 149 | 150 | if (!body) { 151 | return null 152 | } 153 | 154 | return ( 155 | <p 156 | ref={ref} 157 | id={formMessageId} 158 | className={cn("text-sm font-medium text-destructive", className)} 159 | {...props} 160 | > 161 | {body} 162 | </p> 163 | ) 164 | }) 165 | FormMessage.displayName = "FormMessage" 166 | 167 | export { 168 | useFormField, 169 | Form, 170 | FormItem, 171 | FormLabel, 172 | FormControl, 173 | FormDescription, 174 | FormMessage, 175 | FormField, 176 | } 177 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes<HTMLInputElement> {} 7 | 8 | const Input = React.forwardRef<HTMLInputElement, InputProps>( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | <input 12 | type={type} 13 | className={cn( 14 | "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 15 | className 16 | )} 17 | ref={ref} 18 | {...props} 19 | /> 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/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( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef<typeof LabelPrimitive.Root>, 15 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & 16 | VariantProps<typeof labelVariants> 17 | >(({ className, ...props }, ref) => ( 18 | <LabelPrimitive.Root 19 | ref={ref} 20 | className={cn(labelVariants(), className)} 21 | {...props} 22 | /> 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef<typeof PopoverPrimitive.Content>, 14 | React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | <PopoverPrimitive.Portal> 17 | <PopoverPrimitive.Content 18 | ref={ref} 19 | align={align} 20 | sideOffset={sideOffset} 21 | className={cn( 22 | "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 23 | className 24 | )} 25 | {...props} 26 | /> 27 | </PopoverPrimitive.Portal> 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} 7 | 8 | const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 | <textarea 12 | className={cn( 13 | "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 14 | className 15 | )} 16 | ref={ref} 17 | {...props} 18 | /> 19 | ) 20 | } 21 | ) 22 | Textarea.displayName = "Textarea" 23 | 24 | export { Textarea } 25 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | // 2 to demonstrate infinite scroll, should be higher in production 2 | export const INFINITE_SCROLL_PAGINATION_RESULTS = 4 -------------------------------------------------------------------------------- /src/lib/Apis.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | 4 | // Function to fetch search results 5 | const fetchSearchResults = async (value: string) => { 6 | const { data } = await axios.get(`/api/search?value=${value}`); 7 | return data; 8 | }; 9 | 10 | // Hook to use the search query 11 | export const useSearch = (searchValue: string) => { 12 | return useQuery({ 13 | queryKey: [ searchValue], 14 | queryFn: () => fetchSearchResults(searchValue), 15 | enabled: searchValue.length > 0, // More specific condition 16 | // Don't run query if searchValue is empty or too short 17 | initialData: [], // Provide initial empty array 18 | refetchOnWindowFocus: false, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/Firebase.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { initializeApp } from 'firebase/app'; 3 | import { getStorage } from 'firebase/storage'; 4 | const firebaseConfig = { 5 | apiKey: process.env.APIKEY, 6 | authDomain:"projectfriendz-45b49.firebaseapp.com", 7 | projectId: "projectfriendz-45b49", 8 | storageBucket: "projectfriendz-45b49.appspot.com", 9 | messagingSenderId: "186662584426", 10 | appId: "1:186662584426:web:b37a6002f57c2af7578a13", 11 | measurementId:"G-ZCMDL02FYD" 12 | }; 13 | const app = initializeApp(firebaseConfig); 14 | const storage = getStorage(app); 15 | export{ storage }; -------------------------------------------------------------------------------- /src/lib/Functions.tsx: -------------------------------------------------------------------------------- 1 | import { storage } from "./Firebase"; 2 | import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; 3 | 4 | 5 | 6 | // ****************** SIGN UP PROGRESS BAR ANIMATE AND STEPS FUNCTION *********************** 7 | export const HandleNext = (index: number) => { 8 | const ids = ["first", "second", "third", "fourth", "fifth"]; 9 | const steps = ["stepOne", "stepTwo", "stepThree", "stepFour", "stepFive"]; 10 | const title = [ 11 | "Welcome, select an account type.", 12 | "Let's create a unique username", 13 | "Tell us more about you.", 14 | "Change a profile picture. Or go Next ", 15 | "You're all set. Ready?", 16 | ]; 17 | const value = [0, 25, 50, 75, 100]; 18 | // Set the clicked step as active and mark previous steps as completed 19 | const signupTitle = document.getElementById('signupTitle'); 20 | var progressBar = document.getElementById('bar') 21 | if (signupTitle !== null) signupTitle.innerHTML = title[index] 22 | for (let i = 0; i <= index; i++) { 23 | document.getElementById(`${ids[i]}`)?.classList.add("activeDot"); 24 | const pannelShow = document.getElementById(`${steps[i]}`); 25 | const pannelHide = document.getElementById(`${steps[i - 1]}`); 26 | 27 | if (pannelShow !== null) pannelShow.style.display = "block"; 28 | if (pannelHide !== null) pannelHide.style.display = "none"; 29 | if (progressBar !== null) progressBar.style.width = `${value[i]}%` 30 | } 31 | 32 | // Mark remaining steps as inactive and incomplete 33 | for (let i = index + 1; i < ids.length; i++) { 34 | const hidePannel = document.getElementById(`${steps[i]}`); 35 | if (hidePannel !== null) hidePannel.style.display = "none"; 36 | document.getElementById(`${ids[i]}`)?.classList.remove("activeDot"); 37 | } 38 | }; 39 | // *********************** HANDLE Image Upload For Editor.js in firebase ************************* 40 | 41 | export async function handleImageUpload (file:File) { 42 | 43 | const storageRef = ref(storage, `images/${file.name}`); 44 | await uploadBytes(storageRef, file); 45 | const res = await getDownloadURL(storageRef); 46 | return { 47 | success: 1, 48 | file: { 49 | url: res, 50 | }, 51 | } 52 | }; -------------------------------------------------------------------------------- /src/lib/NewUserFormValidator.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const UsernameValidator = z.object({ 4 | username: z.string() 5 | .min(3,{message: "Username must be at least 4 characters."}) 6 | .max(20,{message: "Username not more than 20 characters.",}) 7 | .regex(/^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9_]+$/,{message: "Username must include any number and character ",}), 8 | }) 9 | 10 | export const LocationAndBioValidator = z.object({ 11 | location: z.string().min(5,{message:"Location must at least 5 characters"}) 12 | .max(45,{message:"Location not more than 15 characters."}), 13 | Bio: z.string().min(5,{message:"Bio must at least 5 characters"}) 14 | .max(65,{message:"Bio not more than 25 characters."}) 15 | }) 16 | export const NewUserImageValidator = z.object({ 17 | image: z.string() 18 | }) -------------------------------------------------------------------------------- /src/lib/Prisma.db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import "server-only" 3 | 4 | declare global { 5 | // eslint-disable-next-line no-var, no-unused-vars 6 | var cachedPrisma: PrismaClient 7 | } 8 | 9 | let prisma: PrismaClient 10 | if (process.env.NODE_ENV === 'production') { 11 | prisma = new PrismaClient() 12 | } else { 13 | if (!global.cachedPrisma) { 14 | global.cachedPrisma = new PrismaClient() 15 | } 16 | prisma = global.cachedPrisma 17 | } 18 | 19 | export const db = prisma 20 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./Prisma.db"; 2 | 3 | import { PrismaAdapter } from "@auth/prisma-adapter"; 4 | import { nanoid } from "nanoid"; 5 | import { NextAuthOptions, getServerSession } from "next-auth"; 6 | import GoogleProvider from "next-auth/providers/google"; 7 | export const authOptions: NextAuthOptions = { 8 | // @ts-ignore 9 | adapter: PrismaAdapter(db), 10 | session: { 11 | strategy: "jwt", 12 | }, 13 | pages: { 14 | signIn: "/signin", 15 | newUser:'/newuser' 16 | }, 17 | providers: [ 18 | GoogleProvider({ 19 | clientId: process.env.GOOGLE_CLIENT_ID!, 20 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!, 21 | }), 22 | ], 23 | callbacks: { 24 | async session({ token, session }) { 25 | if (token) { 26 | // @ts-ignore 27 | session.user.id = token.id; 28 | // @ts-ignore 29 | session.user.name = token.name; 30 | // @ts-ignore 31 | session.user.email = token.email; 32 | // @ts-ignore 33 | session.user.image = token.picture; 34 | // @ts-ignore 35 | session.user.username = token.username; 36 | } 37 | 38 | return session; 39 | }, 40 | 41 | async jwt({ token, user }) { 42 | const dbUser = await db.user.findFirst({ 43 | where: { 44 | email: token.email, 45 | }, 46 | }); 47 | 48 | if (!dbUser) { 49 | token.id = user!.id; 50 | return token; 51 | } 52 | 53 | if (!dbUser.username) { 54 | await db.user.update({ 55 | where: { 56 | id: dbUser.id, 57 | }, 58 | data: { 59 | username: nanoid(10), 60 | }, 61 | }); 62 | } 63 | 64 | return { 65 | id: dbUser.id, 66 | name: dbUser.name, 67 | email: dbUser.email, 68 | picture: dbUser.image, 69 | username: dbUser.username, 70 | onboardingCompleted: dbUser.onboardingCompleted, 71 | location: dbUser.location, 72 | Bio: dbUser.Bio, 73 | }; 74 | }, 75 | redirect() { 76 | 77 | return "/"; 78 | 79 | }, 80 | }, 81 | }; 82 | 83 | export const getAuthSession = () => getServerSession(authOptions); 84 | -------------------------------------------------------------------------------- /src/lib/commentValidator.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const CommentValidator = z.object({ 4 | postId: z.string(), 5 | text: z.string(), 6 | replyToId: z.string().optional() 7 | }) 8 | 9 | export type CommentRequest = z.infer<typeof CommentValidator> 10 | 11 | 12 | 13 | export const CommentVoteValidator = z.object({ 14 | commentId: z.string(), 15 | }) 16 | 17 | export type CommentVoteRequest = z.infer<typeof CommentVoteValidator> -------------------------------------------------------------------------------- /src/lib/redis.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from '@upstash/redis' 2 | 3 | export const redis = new Redis({ 4 | url: process.env.REDIS_URL!, 5 | token: process.env.REDIS_SECRET!, 6 | }) -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | 2 | import { getToken } from 'next-auth/jwt' 3 | import { NextResponse } from 'next/server' 4 | import type { NextRequest } from 'next/server' 5 | 6 | export async function middleware(req: NextRequest) { 7 | const token = await getToken({ req }) 8 | 9 | if (!token) { 10 | return NextResponse.redirect(new URL('/signin', req.nextUrl)) 11 | } 12 | } 13 | 14 | 15 | 16 | 17 | // See "Matching Paths" below to learn more 18 | export const config = { 19 | matcher: '/', 20 | } -------------------------------------------------------------------------------- /src/types/PostLikeValidator.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const PostVoteValidator = z.object({ 4 | postId: z.string(), 5 | }) 6 | 7 | export type PostVoteRequest = z.infer<typeof PostVoteValidator> 8 | 9 | 10 | export const FollowUserValidator = z.object({ 11 | toFollowId: z.string(), 12 | }) 13 | 14 | export type FollowUserRequest = z.infer<typeof FollowUserValidator> -------------------------------------------------------------------------------- /src/types/db.d.ts: -------------------------------------------------------------------------------- 1 | import { Post, User, Like, Comment } from "@prisma/client"; 2 | 3 | 4 | 5 | 6 | export type ExtendedPost = Post & { 7 | author:User, 8 | like: Like[], 9 | comments: Comment[], 10 | 11 | 12 | } -------------------------------------------------------------------------------- /src/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import type { Session, User } from 'next-auth' 2 | import type { JWT } from 'next-auth/jwt' 3 | 4 | type UserId = string 5 | 6 | declare module 'next-auth/jwt' { 7 | interface JWT { 8 | id: UserId 9 | username?: string | null 10 | } 11 | } 12 | 13 | declare module 'next-auth' { 14 | interface Session { 15 | user: User & { 16 | id: UserId 17 | username?: string | null 18 | onboardingCompleted?: boolean 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/types/redis.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export type CachedPost = { 3 | id: string 4 | title: string 5 | authorUsername: string 6 | content: string 7 | // authorName : string 8 | // authorImage: string 9 | createdAt: Date 10 | } -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | export type user = { 2 | id:string; 3 | accountType:string; 4 | fullName:string; 5 | userName:string; 6 | email:string; 7 | password:any; 8 | Repassword:any; 9 | avatar:string; 10 | 11 | 12 | } 13 | export type sessionUserType = { 14 | user: { 15 | name: string; 16 | email: string; 17 | image: string; 18 | id: string; 19 | username: string; 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | borderE3:"#e3e3e3", 22 | colorF7:"#f7f7f7", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: 0 }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: 0 }, 69 | }, 70 | shake: { 71 | '0%, 100%': { transform: 'rotate(0deg)' }, 72 | '25%': { transform: 'rotate(-10deg)' }, 73 | '75%': { transform: 'rotate(10deg)' } 74 | } 75 | }, 76 | animation: { 77 | "accordion-down": "accordion-down 0.2s ease-out", 78 | "accordion-up": "accordion-up 0.2s ease-out", 79 | shake: 'shake 1s ease-in-out infinite' 80 | }, 81 | }, 82 | }, 83 | plugins: [require("tailwindcss-animate")], 84 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------