Install Tailwind && Daisy UI (check out the docs)
Update Tailwind config file for daisy UI
import daisyui from "daisyui" ;
import daisyUIThemes from "daisyui/src/theming/themes" ;
/** @type {import('tailwindcss').Config} */
export default {
content : [ "./index.html" , "./src/**/*.{js,ts,jsx,tsx}" ] ,
theme : {
extend : { } ,
} ,
plugins : [ daisyui ] ,
daisyui : {
themes : [
"light" ,
{
black : {
...daisyUIThemes [ "black" ] ,
primary : "rgb(29, 155, 240)" ,
secondary : "rgb(24, 24, 24)" ,
} ,
} ,
] ,
} ,
} ;
Add data-theme="black" to the HTML TAG
<!DOCTYPE html>
< html lang ="en " data-theme ="black ">
< head >
< meta charset ="UTF-8 " />
< link rel ="icon " type ="image/svg+xml " href ="/vite.svg " />
< meta name ="viewport " content ="width=device-width, initial-scale=1.0 " />
< title > Vite + React</ title >
</ head >
< body >
< div id ="root "> </ div >
< script type ="module " src ="/src/main.jsx "> </ script >
</ body >
</ html >
Install React Router and React Icons
npm install react-router-dom react-icons
Wrap the App component with BrowserRouter in main.js
function App ( ) {
return (
< div className = 'flex max-w-6xl mx-auto' >
< Routes >
< Route path = '/' element = { < HomePage /> } />
< Route path = '/signup' element = { < SignUpPage /> } />
< Route path = '/login' element = { < LoginPage /> } />
</ Routes >
</ div >
) ;
}
SIGN UP PAGE => /src/pages/auth/signup/SignUpPage.jsx
import { Link } from "react-router-dom" ;
import { useState } from "react" ;
import XSvg from "../../../components/svgs/X" ;
import { MdOutlineMail } from "react-icons/md" ;
import { FaUser } from "react-icons/fa" ;
import { MdPassword } from "react-icons/md" ;
import { MdDriveFileRenameOutline } from "react-icons/md" ;
const SignUpPage = ( ) => {
const [ formData , setFormData ] = useState ( {
email : "" ,
username : "" ,
fullName : "" ,
password : "" ,
} ) ;
const handleSubmit = ( e ) => {
e . preventDefault ( ) ;
console . log ( formData ) ;
} ;
const handleInputChange = ( e ) => {
setFormData ( { ...formData , [ e . target . name ] : e . target . value } ) ;
} ;
const isError = false ;
return (
< div className = 'max-w-screen-xl mx-auto flex h-screen px-10' >
< div className = 'flex-1 hidden lg:flex items-center justify-center' >
< XSvg className = ' lg:w-2/3 fill-white' />
</ div >
< div className = 'flex-1 flex flex-col justify-center items-center' >
< form className = 'lg:w-2/3 mx-auto md:mx-20 flex gap-4 flex-col' onSubmit = { handleSubmit } >
< XSvg className = 'w-24 lg:hidden fill-white' />
< h1 className = 'text-4xl font-extrabold text-white' > Join today.</ h1 >
< label className = 'input input-bordered rounded flex items-center gap-2' >
< MdOutlineMail />
< input
type = 'email'
className = 'grow'
placeholder = 'Email'
name = 'email'
onChange = { handleInputChange }
value = { formData . email }
/>
</ label >
< div className = 'flex gap-4 flex-wrap' >
< label className = 'input input-bordered rounded flex items-center gap-2 flex-1' >
< FaUser />
< input
type = 'text'
className = 'grow '
placeholder = 'Username'
name = 'username'
onChange = { handleInputChange }
value = { formData . username }
/>
</ label >
< label className = 'input input-bordered rounded flex items-center gap-2 flex-1' >
< MdDriveFileRenameOutline />
< input
type = 'text'
className = 'grow'
placeholder = 'Full Name'
name = 'fullName'
onChange = { handleInputChange }
value = { formData . fullName }
/>
</ label >
</ div >
< label className = 'input input-bordered rounded flex items-center gap-2' >
< MdPassword />
< input
type = 'password'
className = 'grow'
placeholder = 'Password'
name = 'password'
onChange = { handleInputChange }
value = { formData . password }
/>
</ label >
< button className = 'btn rounded-full btn-primary text-white' > Sign up</ button >
{ isError && < p className = 'text-red-500' > Something went wrong</ p > }
</ form >
< div className = 'flex flex-col lg:w-2/3 gap-2 mt-4' >
< p className = 'text-white text-lg' > Already have an account?</ p >
< Link to = '/login' >
< button className = 'btn rounded-full btn-primary text-white btn-outline w-full' > Sign in</ button >
</ Link >
</ div >
</ div >
</ div >
) ;
} ;
export default SignUpPage ;
XSvg Component => /src/components/svgs/X.jsx
const XSvg = ( props ) => (
< svg aria-hidden = 'true' viewBox = '0 0 24 24' { ...props } >
< path d = 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />
</ svg >
) ;
export default XSvg ;
LOGIN PAGE => /src/pages/auth/login/LoginPage.jsx
import { useState } from "react" ;
import { Link } from "react-router-dom" ;
import XSvg from "../../../components/svgs/X" ;
import { MdOutlineMail } from "react-icons/md" ;
import { MdPassword } from "react-icons/md" ;
const LoginPage = ( ) => {
const [ formData , setFormData ] = useState ( {
username : "" ,
password : "" ,
} ) ;
const handleSubmit = ( e ) => {
e . preventDefault ( ) ;
console . log ( formData ) ;
} ;
const handleInputChange = ( e ) => {
setFormData ( { ...formData , [ e . target . name ] : e . target . value } ) ;
} ;
const isError = false ;
return (
< div className = 'max-w-screen-xl mx-auto flex h-screen' >
< div className = 'flex-1 hidden lg:flex items-center justify-center' >
< XSvg className = 'lg:w-2/3 fill-white' />
</ div >
< div className = 'flex-1 flex flex-col justify-center items-center' >
< form className = 'flex gap-4 flex-col' onSubmit = { handleSubmit } >
< XSvg className = 'w-24 lg:hidden fill-white' />
< h1 className = 'text-4xl font-extrabold text-white' > { "Let's" } go.</ h1 >
< label className = 'input input-bordered rounded flex items-center gap-2' >
< MdOutlineMail />
< input
type = 'text'
className = 'grow'
placeholder = 'username'
name = 'username'
onChange = { handleInputChange }
value = { formData . username }
/>
</ label >
< label className = 'input input-bordered rounded flex items-center gap-2' >
< MdPassword />
< input
type = 'password'
className = 'grow'
placeholder = 'Password'
name = 'password'
onChange = { handleInputChange }
value = { formData . password }
/>
</ label >
< button className = 'btn rounded-full btn-primary text-white' > Login</ button >
{ isError && < p className = 'text-red-500' > Something went wrong</ p > }
</ form >
< div className = 'flex flex-col gap-2 mt-4' >
< p className = 'text-white text-lg' > { "Don't" } have an account?</ p >
< Link to = '/signup' >
< button className = 'btn rounded-full btn-primary text-white btn-outline w-full' > Sign up</ button >
</ Link >
</ div >
</ div >
</ div >
) ;
} ;
export default LoginPage ;
HOME PAGE => /src/pages/home/HomePage.jsx
import { useState } from "react" ;
import Posts from "../../components/common/Posts" ;
import CreatePost from "./CreatePost" ;
const HomePage = ( ) => {
const [ feedType , setFeedType ] = useState ( "forYou" ) ;
return (
< >
< div className = 'flex-[4_4_0] mr-auto border-r border-gray-700 min-h-screen' >
{ /* Header */ }
< div className = 'flex w-full border-b border-gray-700' >
< div
className = {
"flex justify-center flex-1 p-3 hover:bg-secondary transition duration-300 cursor-pointer relative"
}
onClick = { ( ) => setFeedType ( "forYou" ) }
>
For you
{ feedType === "forYou" && (
< div className = 'absolute bottom-0 w-10 h-1 rounded-full bg-primary' > </ div >
) }
</ div >
< div
className = 'flex justify-center flex-1 p-3 hover:bg-secondary transition duration-300 cursor-pointer relative'
onClick = { ( ) => setFeedType ( "following" ) }
>
Following
{ feedType === "following" && (
< div className = 'absolute bottom-0 w-10 h-1 rounded-full bg-primary' > </ div >
) }
</ div >
</ div >
{ /* CREATE POST INPUT */ }
< CreatePost />
{ /* POSTS */ }
< Posts />
</ div >
</ >
) ;
} ;
export default HomePage ;
Temporarily add CreatePost and Posts components with boilerplate code for now, we will update them in a second
/src/pages/home/CreatePost.jsx
/src/components/common/Posts.jsx
SIDEBAR COMPONENT => /src/components/common/Sidebar.jsx
import XSvg from "../svgs/X" ;
import { MdHomeFilled } from "react-icons/md" ;
import { IoNotifications } from "react-icons/io5" ;
import { FaUser } from "react-icons/fa" ;
import { Link } from "react-router-dom" ;
import { BiLogOut } from "react-icons/bi" ;
const Sidebar = ( ) => {
const data = {
fullName : "John Doe" ,
username : "johndoe" ,
profileImg : "/avatars/boy1.png" ,
} ;
return (
< div className = 'md:flex-[2_2_0] w-18 max-w-52' >
< div className = 'sticky top-0 left-0 h-screen flex flex-col border-r border-gray-700 w-20 md:w-full' >
< Link to = '/' className = 'flex justify-center md:justify-start' >
< XSvg className = 'px-2 w-12 h-12 rounded-full fill-white hover:bg-stone-900' />
</ Link >
< ul className = 'flex flex-col gap-3 mt-4' >
< li className = 'flex justify-center md:justify-start' >
< Link
to = '/'
className = 'flex gap-3 items-center hover:bg-stone-900 transition-all rounded-full duration-300 py-2 pl-2 pr-4 max-w-fit cursor-pointer'
>
< MdHomeFilled className = 'w-8 h-8' />
< span className = 'text-lg hidden md:block' > Home</ span >
</ Link >
</ li >
< li className = 'flex justify-center md:justify-start' >
< Link
to = '/notifications'
className = 'flex gap-3 items-center hover:bg-stone-900 transition-all rounded-full duration-300 py-2 pl-2 pr-4 max-w-fit cursor-pointer'
>
< IoNotifications className = 'w-6 h-6' />
< span className = 'text-lg hidden md:block' > Notifications</ span >
</ Link >
</ li >
< li className = 'flex justify-center md:justify-start' >
< Link
to = { `/profile/${ data ?. username } ` }
className = 'flex gap-3 items-center hover:bg-stone-900 transition-all rounded-full duration-300 py-2 pl-2 pr-4 max-w-fit cursor-pointer'
>
< FaUser className = 'w-6 h-6' />
< span className = 'text-lg hidden md:block' > Profile</ span >
</ Link >
</ li >
</ ul >
{ data && (
< Link
to = { `/profile/${ data . username } ` }
className = 'mt-auto mb-10 flex gap-2 items-start transition-all duration-300 hover:bg-[#181818] py-2 px-4 rounded-full'
>
< div className = 'avatar hidden md:inline-flex' >
< div className = 'w-8 rounded-full' >
< img src = { data ?. profileImg || "/avatar-placeholder.png" } />
</ div >
</ div >
< div className = 'flex justify-between flex-1' >
< div className = 'hidden md:block' >
< p className = 'text-white font-bold text-sm w-20 truncate' > { data ?. fullName } </ p >
< p className = 'text-slate-500 text-sm' > @{ data ?. username } </ p >
</ div >
< BiLogOut className = 'w-5 h-5 cursor-pointer' />
</ div >
</ Link >
) }
</ div >
</ div >
) ;
} ;
export default Sidebar ;
ADD SOME DUMMY DATA => /src/utils/db/dummy.js
export const POSTS = [
{
_id : "1" ,
text : "Let's build a fullstack WhatsApp clone with NEXT.JS 14 😍" ,
img : "/posts/post1.png" ,
user : {
username : "johndoe" ,
profileImg : "/avatars/boy1.png" ,
fullName : "John Doe" ,
} ,
comments : [
{
_id : "1" ,
text : "Nice Tutorial" ,
user : {
username : "janedoe" ,
profileImg : "/avatars/girl1.png" ,
fullName : "Jane Doe" ,
} ,
} ,
] ,
likes : [ "6658s891" , "6658s892" , "6658s893" , "6658s894" ] ,
} ,
{
_id : "2" ,
text : "How you guys doing? 😊" ,
user : {
username : "johndoe" ,
profileImg : "/avatars/boy2.png" ,
fullName : "John Doe" ,
} ,
comments : [
{
_id : "1" ,
text : "Nice Tutorial" ,
user : {
username : "janedoe" ,
profileImg : "/avatars/girl2.png" ,
fullName : "Jane Doe" ,
} ,
} ,
] ,
likes : [ "6658s891" , "6658s892" , "6658s893" , "6658s894" ] ,
} ,
{
_id : "3" ,
text : "Astronaut in a room of drawers, generated by Midjourney. 🚀" ,
img : "/posts/post2.png" ,
user : {
username : "johndoe" ,
profileImg : "/avatars/boy3.png" ,
fullName : "John Doe" ,
} ,
comments : [ ] ,
likes : [ "6658s891" , "6658s892" , "6658s893" , "6658s894" , "6658s895" , "6658s896" ] ,
} ,
{
_id : "4" ,
text : "I'm learning GO this week. Any tips? 🤔" ,
img : "/posts/post3.png" ,
user : {
username : "johndoe" ,
profileImg : "/avatars/boy3.png" ,
fullName : "John Doe" ,
} ,
comments : [
{
_id : "1" ,
text : "Nice Tutorial" ,
user : {
username : "janedoe" ,
profileImg : "/avatars/girl3.png" ,
fullName : "Jane Doe" ,
} ,
} ,
] ,
likes : [
"6658s891" ,
"6658s892" ,
"6658s893" ,
"6658s894" ,
"6658s895" ,
"6658s896" ,
"6658s897" ,
"6658s898" ,
"6658s899" ,
] ,
} ,
] ;
export const USERS_FOR_RIGHT_PANEL = [
{
_id : "1" ,
fullName : "John Doe" ,
username : "johndoe" ,
profileImg : "/avatars/boy2.png" ,
} ,
{
_id : "2" ,
fullName : "Jane Doe" ,
username : "janedoe" ,
profileImg : "/avatars/girl1.png" ,
} ,
{
_id : "3" ,
fullName : "Bob Doe" ,
username : "bobdoe" ,
profileImg : "/avatars/boy3.png" ,
} ,
{
_id : "4" ,
fullName : "Daisy Doe" ,
username : "daisydoe" ,
profileImg : "/avatars/girl2.png" ,
} ,
] ;
RIGHT PANEL COMPONENT => /src/components/common/RightPanel.jsx
import { Link } from "react-router-dom" ;
import RightPanelSkeleton from "../skeletons/RightPanelSkeleton" ;
import { USERS_FOR_RIGHT_PANEL } from "../../utils/db/dummy" ;
const RightPanel = ( ) => {
const isLoading = false ;
return (
< div className = 'hidden lg:block my-4 mx-2' >
< div className = 'bg-[#16181C] p-4 rounded-md sticky top-2' >
< p className = 'font-bold' > Who to follow</ p >
< div className = 'flex flex-col gap-4' >
{ /* item */ }
{ isLoading && (
< >
< RightPanelSkeleton />
< RightPanelSkeleton />
< RightPanelSkeleton />
< RightPanelSkeleton />
</ >
) }
{ ! isLoading &&
USERS_FOR_RIGHT_PANEL ?. map ( ( user ) => (
< Link
to = { `/profile/${ user . username } ` }
className = 'flex items-center justify-between gap-4'
key = { user . _id }
>
< div className = 'flex gap-2 items-center' >
< div className = 'avatar' >
< div className = 'w-8 rounded-full' >
< img src = { user . profileImg || "/avatar-placeholder.png" } />
</ div >
</ div >
< div className = 'flex flex-col' >
< span className = 'font-semibold tracking-tight truncate w-28' >
{ user . fullName }
</ span >
< span className = 'text-sm text-slate-500' > @{ user . username } </ span >
</ div >
</ div >
< div >
< button
className = 'btn bg-white text-black hover:bg-white hover:opacity-90 rounded-full btn-sm'
onClick = { ( e ) => e . preventDefault ( ) }
>
Follow
</ button >
</ div >
</ Link >
) ) }
</ div >
</ div >
</ div >
) ;
} ;
export default RightPanel ;
RIGHT PANEL SKELETON => /src/components/skeletons/RightPanelSkeleton.jsx
const RightPanelSkeleton = ( ) => {
return (
< div className = 'flex flex-col gap-2 w-52 my-2' >
< div className = 'flex gap-2 items-center' >
< div className = 'skeleton w-8 h-8 rounded-full shrink-0' > </ div >
< div className = 'flex flex-1 justify-between' >
< div className = 'flex flex-col gap-1' >
< div className = 'skeleton h-2 w-12 rounded-full' > </ div >
< div className = 'skeleton h-2 w-16 rounded-full' > </ div >
</ div >
< div className = 'skeleton h-6 w-14 rounded-full' > </ div >
</ div >
</ div >
</ div >
) ;
} ;
export default RightPanelSkeleton ;
CREATE POST COMPONENT => /src/pages/home/CreatePost.jsx
import { CiImageOn } from "react-icons/ci" ;
import { BsEmojiSmileFill } from "react-icons/bs" ;
import { useRef , useState } from "react" ;
import { IoCloseSharp } from "react-icons/io5" ;
const CreatePost = ( ) => {
const [ text , setText ] = useState ( "" ) ;
const [ img , setImg ] = useState ( null ) ;
const imgRef = useRef ( null ) ;
const isPending = false ;
const isError = false ;
const data = {
profileImg : "/avatars/boy1.png" ,
} ;
const handleSubmit = ( e ) => {
e . preventDefault ( ) ;
alert ( "Post created successfully" ) ;
} ;
const handleImgChange = ( e ) => {
const file = e . target . files [ 0 ] ;
if ( file ) {
const reader = new FileReader ( ) ;
reader . onload = ( ) => {
setImg ( reader . result ) ;
} ;
reader . readAsDataURL ( file ) ;
}
} ;
return (
< div className = 'flex p-4 items-start gap-4 border-b border-gray-700' >
< div className = 'avatar' >
< div className = 'w-8 rounded-full' >
< img src = { data . profileImg || "/avatar-placeholder.png" } />
</ div >
</ div >
< form className = 'flex flex-col gap-2 w-full' onSubmit = { handleSubmit } >
< textarea
className = 'textarea w-full p-0 text-lg resize-none border-none focus:outline-none border-gray-800'
placeholder = 'What is happening?!'
value = { text }
onChange = { ( e ) => setText ( e . target . value ) }
/>
{ img && (
< div className = 'relative w-72 mx-auto' >
< IoCloseSharp
className = 'absolute top-0 right-0 text-white bg-gray-800 rounded-full w-5 h-5 cursor-pointer'
onClick = { ( ) => {
setImg ( null ) ;
imgRef . current . value = null ;
} }
/>
< img src = { img } className = 'w-full mx-auto h-72 object-contain rounded' />
</ div >
) }
< div className = 'flex justify-between border-t py-2 border-t-gray-700' >
< div className = 'flex gap-1 items-center' >
< CiImageOn
className = 'fill-primary w-6 h-6 cursor-pointer'
onClick = { ( ) => imgRef . current . click ( ) }
/>
< BsEmojiSmileFill className = 'fill-primary w-5 h-5 cursor-pointer' />
</ div >
< input type = 'file' hidden ref = { imgRef } onChange = { handleImgChange } />
< button className = 'btn btn-primary rounded-full btn-sm text-white px-4' >
{ isPending ? "Posting..." : "Post" }
</ button >
</ div >
{ isError && < div className = 'text-red-500' > Something went wrong</ div > }
</ form >
</ div >
) ;
} ;
export default CreatePost ;
POSTS COMPONENT => /src/components/common/Posts.jsx
import Post from "./Post" ;
import PostSkeleton from "../skeletons/PostSkeleton" ;
import { POSTS } from "../../utils/db/dummy" ;
const Posts = ( ) => {
const isLoading = false ;
return (
< >
{ isLoading && (
< div className = 'flex flex-col justify-center' >
< PostSkeleton />
< PostSkeleton />
< PostSkeleton />
</ div >
) }
{ ! isLoading && POSTS ?. length === 0 && < p className = 'text-center my-4' > No posts in this tab. Switch 👻</ p > }
{ ! isLoading && POSTS && (
< div >
{ POSTS . map ( ( post ) => (
< Post key = { post . _id } post = { post } />
) ) }
</ div >
) }
</ >
) ;
} ;
export default Posts ;
POST SKELETON COMPONENT => /src/components/skeletons/PostSkeleton.jsx
const PostSkeleton = ( ) => {
return (
< div className = 'flex flex-col gap-4 w-full p-4' >
< div className = 'flex gap-4 items-center' >
< div className = 'skeleton w-10 h-10 rounded-full shrink-0' > </ div >
< div className = 'flex flex-col gap-2' >
< div className = 'skeleton h-2 w-12 rounded-full' > </ div >
< div className = 'skeleton h-2 w-24 rounded-full' > </ div >
</ div >
</ div >
< div className = 'skeleton h-40 w-full' > </ div >
</ div >
) ;
} ;
export default PostSkeleton ;
POST COMPONENT => /src/components/common/Post.jsx
import { FaRegComment } from "react-icons/fa" ;
import { BiRepost } from "react-icons/bi" ;
import { FaRegHeart } from "react-icons/fa" ;
import { FaRegBookmark } from "react-icons/fa6" ;
import { FaTrash } from "react-icons/fa" ;
import { useState } from "react" ;
import { Link } from "react-router-dom" ;
const Post = ( { post } ) => {
const [ comment , setComment ] = useState ( "" ) ;
const postOwner = post . user ;
const isLiked = false ;
const isMyPost = true ;
const formattedDate = "1h" ;
const isCommenting = false ;
const handleDeletePost = ( ) => { } ;
const handlePostComment = ( e ) => {
e . preventDefault ( ) ;
} ;
const handleLikePost = ( ) => { } ;
return (
< >
< div className = 'flex gap-2 items-start p-4 border-b border-gray-700' >
< div className = 'avatar' >
< Link to = { `/profile/${ postOwner . username } ` } className = 'w-8 rounded-full overflow-hidden' >
< img src = { postOwner . profileImg || "/avatar-placeholder.png" } />
</ Link >
</ div >
< div className = 'flex flex-col flex-1' >
< div className = 'flex gap-2 items-center' >
< Link to = { `/profile/${ postOwner . username } ` } className = 'font-bold' >
{ postOwner . fullName }
</ Link >
< span className = 'text-gray-700 flex gap-1 text-sm' >
< Link to = { `/profile/${ postOwner . username } ` } > @{ postOwner . username } </ Link >
< span > ·</ span >
< span > { formattedDate } </ span >
</ span >
{ isMyPost && (
< span className = 'flex justify-end flex-1' >
< FaTrash className = 'cursor-pointer hover:text-red-500' onClick = { handleDeletePost } />
</ span >
) }
</ div >
< div className = 'flex flex-col gap-3 overflow-hidden' >
< span > { post . text } </ span >
{ post . img && (
< img
src = { post . img }
className = 'h-80 object-contain rounded-lg border border-gray-700'
alt = ''
/>
) }
</ div >
< div className = 'flex justify-between mt-3' >
< div className = 'flex gap-4 items-center w-2/3 justify-between' >
< div
className = 'flex gap-1 items-center cursor-pointer group'
onClick = { ( ) => document . getElementById ( "comments_modal" + post . _id ) . showModal ( ) }
>
< FaRegComment className = 'w-4 h-4 text-slate-500 group-hover:text-sky-400' />
< span className = 'text-sm text-slate-500 group-hover:text-sky-400' >
{ post . comments . length }
</ span >
</ div >
{ /* We're using Modal Component from DaisyUI */ }
< dialog id = { `comments_modal${ post . _id } ` } className = 'modal border-none outline-none' >
< div className = 'modal-box rounded border border-gray-600' >
< h3 className = 'font-bold text-lg mb-4' > COMMENTS</ h3 >
< div className = 'flex flex-col gap-3 max-h-60 overflow-auto' >
{ post . comments . length === 0 && (
< p className = 'text-sm text-slate-500' >
No comments yet 🤔 Be the first one 😉
</ p >
) }
{ post . comments . map ( ( comment ) => (
< div key = { comment . _id } className = 'flex gap-2 items-start' >
< div className = 'avatar' >
< div className = 'w-8 rounded-full' >
< img
src = { comment . user . profileImg || "/avatar-placeholder.png" }
/>
</ div >
</ div >
< div className = 'flex flex-col' >
< div className = 'flex items-center gap-1' >
< span className = 'font-bold' > { comment . user . fullName } </ span >
< span className = 'text-gray-700 text-sm' >
@{ comment . user . username }
</ span >
</ div >
< div className = 'text-sm' > { comment . text } </ div >
</ div >
</ div >
) ) }
</ div >
< form
className = 'flex gap-2 items-center mt-4 border-t border-gray-600 pt-2'
onSubmit = { handlePostComment }
>
< textarea
className = 'textarea w-full p-1 rounded text-md resize-none border focus:outline-none border-gray-800'
placeholder = 'Add a comment...'
value = { comment }
onChange = { ( e ) => setComment ( e . target . value ) }
/>
< button className = 'btn btn-primary rounded-full btn-sm text-white px-4' >
{ isCommenting ? (
< span className = 'loading loading-spinner loading-md' > </ span >
) : (
"Post"
) }
</ button >
</ form >
</ div >
< form method = 'dialog' className = 'modal-backdrop' >
< button className = 'outline-none' > close</ button >
</ form >
</ dialog >
< div className = 'flex gap-1 items-center group cursor-pointer' >
< BiRepost className = 'w-6 h-6 text-slate-500 group-hover:text-green-500' />
< span className = 'text-sm text-slate-500 group-hover:text-green-500' > 0</ span >
</ div >
< div className = 'flex gap-1 items-center group cursor-pointer' onClick = { handleLikePost } >
{ ! isLiked && (
< FaRegHeart className = 'w-4 h-4 cursor-pointer text-slate-500 group-hover:text-pink-500' />
) }
{ isLiked && < FaRegHeart className = 'w-4 h-4 cursor-pointer text-pink-500 ' /> }
< span
className = { `text-sm text-slate-500 group-hover:text-pink-500 ${
isLiked ? "text-pink-500" : ""
} `}
>
{ post . likes . length }
</ span >
</ div >
</ div >
< div className = 'flex w-1/3 justify-end gap-2 items-center' >
< FaRegBookmark className = 'w-4 h-4 text-slate-500 cursor-pointer' />
</ div >
</ div >
</ div >
</ div >
</ >
) ;
} ;
export default Post ;
NOTIFICATIONS PAGE => /src/pages/notification/NotificationPage.jsx
import { Link } from "react-router-dom" ;
import LoadingSpinner from "../../components/common/LoadingSpinner" ;
import { IoSettingsOutline } from "react-icons/io5" ;
import { FaUser } from "react-icons/fa" ;
import { FaHeart } from "react-icons/fa6" ;
const NotificationPage = ( ) => {
const isLoading = false ;
const notifications = [
{
_id : "1" ,
from : {
_id : "1" ,
username : "johndoe" ,
profileImg : "/avatars/boy2.png" ,
} ,
type : "follow" ,
} ,
{
_id : "2" ,
from : {
_id : "2" ,
username : "janedoe" ,
profileImg : "/avatars/girl1.png" ,
} ,
type : "like" ,
} ,
] ;
const deleteNotifications = ( ) => {
alert ( "All notifications deleted" ) ;
} ;
return (
< >
< div className = 'flex-[4_4_0] border-l border-r border-gray-700 min-h-screen' >
< div className = 'flex justify-between items-center p-4 border-b border-gray-700' >
< p className = 'font-bold' > Notifications</ p >
< div className = 'dropdown ' >
< div tabIndex = { 0 } role = 'button' className = 'm-1' >
< IoSettingsOutline className = 'w-4' />
</ div >
< ul
tabIndex = { 0 }
className = 'dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52'
>
< li >
< a onClick = { deleteNotifications } > Delete all notifications</ a >
</ li >
</ ul >
</ div >
</ div >
{ isLoading && (
< div className = 'flex justify-center h-full items-center' >
< LoadingSpinner size = 'lg' />
</ div >
) }
{ notifications ?. length === 0 && < div className = 'text-center p-4 font-bold' > No notifications 🤔</ div > }
{ notifications ?. map ( ( notification ) => (
< div className = 'border-b border-gray-700' key = { notification . _id } >
< div className = 'flex gap-2 p-4' >
{ notification . type === "follow" && < FaUser className = 'w-7 h-7 text-primary' /> }
{ notification . type === "like" && < FaHeart className = 'w-7 h-7 text-red-500' /> }
< Link to = { `/profile/${ notification . from . username } ` } >
< div className = 'avatar' >
< div className = 'w-8 rounded-full' >
< img src = { notification . from . profileImg || "/avatar-placeholder.png" } />
</ div >
</ div >
< div className = 'flex gap-1' >
< span className = 'font-bold' > @{ notification . from . username } </ span > { " " }
{ notification . type === "follow" ? "followed you" : "liked your post" }
</ div >
</ Link >
</ div >
</ div >
) ) }
</ div >
</ >
) ;
} ;
export default NotificationPage ;
LOADING SPINNER COMPONENT => /src/components/common/LoadingSpinner.jsx
const LoadingSpinner = ( { size = "md" } ) => {
const sizeClass = `loading-${ size } ` ;
return < span className = { `loading loading-spinner ${ sizeClass } ` } /> ;
} ;
export default LoadingSpinner ;
PROFILE PAGE => /src/pages/profile/ProfilePage.jsx
import { useRef , useState } from "react" ;
import { Link } from "react-router-dom" ;
import Posts from "../../components/common/Posts" ;
import ProfileHeaderSkeleton from "../../components/skeletons/ProfileHeaderSkeleton" ;
import EditProfileModal from "./EditProfileModal" ;
import { POSTS } from "../../utils/db/dummy" ;
import { FaArrowLeft } from "react-icons/fa6" ;
import { IoCalendarOutline } from "react-icons/io5" ;
import { FaLink } from "react-icons/fa" ;
import { MdEdit } from "react-icons/md" ;
const ProfilePage = ( ) => {
const [ coverImg , setCoverImg ] = useState ( null ) ;
const [ profileImg , setProfileImg ] = useState ( null ) ;
const [ feedType , setFeedType ] = useState ( "posts" ) ;
const coverImgRef = useRef ( null ) ;
const profileImgRef = useRef ( null ) ;
const isLoading = false ;
const isMyProfile = true ;
const user = {
_id : "1" ,
fullName : "John Doe" ,
username : "johndoe" ,
profileImg : "/avatars/boy2.png" ,
coverImg : "/cover.png" ,
bio : "Lorem ipsum dolor sit amet, consectetur adipiscing elit." ,
link : "https://youtube.com/@asaprogrammer_" ,
following : [ "1" , "2" , "3" ] ,
followers : [ "1" , "2" , "3" ] ,
} ;
const handleImgChange = ( e , state ) => {
const file = e . target . files [ 0 ] ;
if ( file ) {
const reader = new FileReader ( ) ;
reader . onload = ( ) => {
state === "coverImg" && setCoverImg ( reader . result ) ;
state === "profileImg" && setProfileImg ( reader . result ) ;
} ;
reader . readAsDataURL ( file ) ;
}
} ;
return (
< >
< div className = 'flex-[4_4_0] border-r border-gray-700 min-h-screen ' >
{ /* HEADER */ }
{ isLoading && < ProfileHeaderSkeleton /> }
{ ! isLoading && ! user && < p className = 'text-center text-lg mt-4' > User not found</ p > }
< div className = 'flex flex-col' >
{ ! isLoading && user && (
< >
< div className = 'flex gap-10 px-4 py-2 items-center' >
< Link to = '/' >
< FaArrowLeft className = 'w-4 h-4' />
</ Link >
< div className = 'flex flex-col' >
< p className = 'font-bold text-lg' > { user ?. fullName } </ p >
< span className = 'text-sm text-slate-500' > { POSTS ?. length } posts</ span >
</ div >
</ div >
{ /* COVER IMG */ }
< div className = 'relative group/cover' >
< img
src = { coverImg || user ?. coverImg || "/cover.png" }
className = 'h-52 w-full object-cover'
alt = 'cover image'
/>
{ isMyProfile && (
< div
className = 'absolute top-2 right-2 rounded-full p-2 bg-gray-800 bg-opacity-75 cursor-pointer opacity-0 group-hover/cover:opacity-100 transition duration-200'
onClick = { ( ) => coverImgRef . current . click ( ) }
>
< MdEdit className = 'w-5 h-5 text-white' />
</ div >
) }
< input
type = 'file'
hidden
ref = { coverImgRef }
onChange = { ( e ) => handleImgChange ( e , "coverImg" ) }
/>
< input
type = 'file'
hidden
ref = { profileImgRef }
onChange = { ( e ) => handleImgChange ( e , "profileImg" ) }
/>
{ /* USER AVATAR */ }
< div className = 'avatar absolute -bottom-16 left-4' >
< div className = 'w-32 rounded-full relative group/avatar' >
< img src = { profileImg || user ?. profileImg || "/avatar-placeholder.png" } />
< div className = 'absolute top-5 right-3 p-1 bg-primary rounded-full group-hover/avatar:opacity-100 opacity-0 cursor-pointer' >
{ isMyProfile && (
< MdEdit
className = 'w-4 h-4 text-white'
onClick = { ( ) => profileImgRef . current . click ( ) }
/>
) }
</ div >
</ div >
</ div >
</ div >
< div className = 'flex justify-end px-4 mt-5' >
{ isMyProfile && < EditProfileModal /> }
{ ! isMyProfile && (
< button
className = 'btn btn-outline rounded-full btn-sm'
onClick = { ( ) => alert ( "Followed successfully" ) }
>
Follow
</ button >
) }
{ ( coverImg || profileImg ) && (
< button
className = 'btn btn-primary rounded-full btn-sm text-white px-4 ml-2'
onClick = { ( ) => alert ( "Profile updated successfully" ) }
>
Update
</ button >
) }
</ div >
< div className = 'flex flex-col gap-4 mt-14 px-4' >
< div className = 'flex flex-col' >
< span className = 'font-bold text-lg' > { user ?. fullName } </ span >
< span className = 'text-sm text-slate-500' > @{ user ?. username } </ span >
< span className = 'text-sm my-1' > { user ?. bio } </ span >
</ div >
< div className = 'flex gap-2 flex-wrap' >
{ user ?. link && (
< div className = 'flex gap-1 items-center ' >
< >
< FaLink className = 'w-3 h-3 text-slate-500' />
< a
href = 'https://youtube.com/@asaprogrammer_'
target = '_blank'
rel = 'noreferrer'
className = 'text-sm text-blue-500 hover:underline'
>
youtube.com/@asaprogrammer_
</ a >
</ >
</ div >
) }
< div className = 'flex gap-2 items-center' >
< IoCalendarOutline className = 'w-4 h-4 text-slate-500' />
< span className = 'text-sm text-slate-500' > Joined July 2021</ span >
</ div >
</ div >
< div className = 'flex gap-2' >
< div className = 'flex gap-1 items-center' >
< span className = 'font-bold text-xs' > { user ?. following . length } </ span >
< span className = 'text-slate-500 text-xs' > Following</ span >
</ div >
< div className = 'flex gap-1 items-center' >
< span className = 'font-bold text-xs' > { user ?. followers . length } </ span >
< span className = 'text-slate-500 text-xs' > Followers</ span >
</ div >
</ div >
</ div >
< div className = 'flex w-full border-b border-gray-700 mt-4' >
< div
className = 'flex justify-center flex-1 p-3 hover:bg-secondary transition duration-300 relative cursor-pointer'
onClick = { ( ) => setFeedType ( "posts" ) }
>
Posts
{ feedType === "posts" && (
< div className = 'absolute bottom-0 w-10 h-1 rounded-full bg-primary' />
) }
</ div >
< div
className = 'flex justify-center flex-1 p-3 text-slate-500 hover:bg-secondary transition duration-300 relative cursor-pointer'
onClick = { ( ) => setFeedType ( "likes" ) }
>
Likes
{ feedType === "likes" && (
< div className = 'absolute bottom-0 w-10 h-1 rounded-full bg-primary' />
) }
</ div >
</ div >
</ >
) }
< Posts />
</ div >
</ div >
</ >
) ;
} ;
export default ProfilePage ;
EDIT PROFILE MODAL => /src/pages/profile/EditProfileModal.jsx
import { useState } from "react" ;
const EditProfileModal = ( ) => {
const [ formData , setFormData ] = useState ( {
fullName : "" ,
username : "" ,
email : "" ,
bio : "" ,
link : "" ,
newPassword : "" ,
currentPassword : "" ,
} ) ;
const handleInputChange = ( e ) => {
setFormData ( { ...formData , [ e . target . name ] : e . target . value } ) ;
} ;
return (
< >
< button
className = 'btn btn-outline rounded-full btn-sm'
onClick = { ( ) => document . getElementById ( "edit_profile_modal" ) . showModal ( ) }
>
Edit profile
</ button >
< dialog id = 'edit_profile_modal' className = 'modal' >
< div className = 'modal-box border rounded-md border-gray-700 shadow-md' >
< h3 className = 'font-bold text-lg my-3' > Update Profile</ h3 >
< form
className = 'flex flex-col gap-4'
onSubmit = { ( e ) => {
e . preventDefault ( ) ;
alert ( "Profile updated successfully" ) ;
} }
>
< div className = 'flex flex-wrap gap-2' >
< input
type = 'text'
placeholder = 'Full Name'
className = 'flex-1 input border border-gray-700 rounded p-2 input-md'
value = { formData . fullName }
name = 'fullName'
onChange = { handleInputChange }
/>
< input
type = 'text'
placeholder = 'Username'
className = 'flex-1 input border border-gray-700 rounded p-2 input-md'
value = { formData . username }
name = 'username'
onChange = { handleInputChange }
/>
</ div >
< div className = 'flex flex-wrap gap-2' >
< input
type = 'email'
placeholder = 'Email'
className = 'flex-1 input border border-gray-700 rounded p-2 input-md'
value = { formData . email }
name = 'email'
onChange = { handleInputChange }
/>
< textarea
placeholder = 'Bio'
className = 'flex-1 input border border-gray-700 rounded p-2 input-md'
value = { formData . bio }
name = 'bio'
onChange = { handleInputChange }
/>
</ div >
< div className = 'flex flex-wrap gap-2' >
< input
type = 'password'
placeholder = 'Current Password'
className = 'flex-1 input border border-gray-700 rounded p-2 input-md'
value = { formData . currentPassword }
name = 'currentPassword'
onChange = { handleInputChange }
/>
< input
type = 'password'
placeholder = 'New Password'
className = 'flex-1 input border border-gray-700 rounded p-2 input-md'
value = { formData . newPassword }
name = 'newPassword'
onChange = { handleInputChange }
/>
</ div >
< input
type = 'text'
placeholder = 'Link'
className = 'flex-1 input border border-gray-700 rounded p-2 input-md'
value = { formData . link }
name = 'link'
onChange = { handleInputChange }
/>
< button className = 'btn btn-primary rounded-full btn-sm text-white' > Update</ button >
</ form >
</ div >
< form method = 'dialog' className = 'modal-backdrop' >
< button className = 'outline-none' > close</ button >
</ form >
</ dialog >
</ >
) ;
} ;
export default EditProfileModal ;
PROFILE HEADER SKELETON => /src/components/skeletons/ProfileHeaderSkeleton.jsx
const ProfileHeaderSkeleton = ( ) => {
return (
< div className = 'flex flex-col gap-2 w-full my-2 p-4' >
< div className = 'flex gap-2 items-center' >
< div className = 'flex flex-1 gap-1' >
< div className = 'flex flex-col gap-1 w-full' >
< div className = 'skeleton h-4 w-12 rounded-full' > </ div >
< div className = 'skeleton h-4 w-16 rounded-full' > </ div >
< div className = 'skeleton h-40 w-full relative' >
< div className = 'skeleton h-20 w-20 rounded-full border absolute -bottom-10 left-3' > </ div >
</ div >
< div className = 'skeleton h-6 mt-4 w-24 ml-auto rounded-full' > </ div >
< div className = 'skeleton h-4 w-14 rounded-full mt-4' > </ div >
< div className = 'skeleton h-4 w-20 rounded-full' > </ div >
< div className = 'skeleton h-4 w-2/3 rounded-full' > </ div >
</ div >
</ div >
</ div >
</ div >
) ;
} ;
export default ProfileHeaderSkeleton ;