Skip to content

Commit ab4954c

Browse files
committed
Users and individual users profile
1 parent ca074ea commit ab4954c

File tree

14 files changed

+284
-4
lines changed

14 files changed

+284
-4
lines changed

components/Avatar.tsx

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import useUser from '@/hooks/useUser';
2+
import Image from 'next/image';
3+
import { useRouter } from 'next/router';
4+
import React, { useCallback } from 'react';
5+
6+
type Props = {
7+
userId: string;
8+
isLarge?: boolean;
9+
hasBorder?: boolean;
10+
};
11+
12+
function Avatar({ userId, isLarge, hasBorder }: Props) {
13+
const { data: fetchedUser } = useUser(userId);
14+
const router = useRouter();
15+
const onClick = useCallback(
16+
(event: any) => {
17+
event.stopPropagation();
18+
const url = `/users/${userId}`;
19+
router.push(url);
20+
},
21+
[router, userId]
22+
);
23+
return (
24+
<div
25+
className={`
26+
${hasBorder ? 'border-4 border-black' : ''}
27+
${isLarge ? 'h-32' : 'h-12'}
28+
${isLarge ? 'w-32' : 'w-12'}
29+
rounded-full
30+
hover:opacity-90
31+
transition
32+
cursor-pointer
33+
relative
34+
`}>
35+
<Image
36+
fill
37+
style={{ objectFit: 'cover', borderRadius: '100%' }}
38+
onClick={onClick}
39+
alt='Avatar'
40+
src={fetchedUser?.profileImage || '/images/placeholder.png'}
41+
/>
42+
</div>
43+
);
44+
}
45+
46+
export default Avatar;

components/layout/FollowBar.tsx

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
1+
import useUsers from '@/hooks/useUsers';
12
import React from 'react';
3+
import Avatar from '../Avatar';
24

35
type Props = {};
46

57
function FollowBar({}: Props) {
8+
const { data: users = [] } = useUsers();
9+
10+
if (users.length === 0) return null;
11+
612
return (
713
<div className='px-6 py-4 hidden lg:block'>
814
<div className='bg-neutral-800 rounded-xl p-4'>
915
<h2 className='text-white text-xl font-semibold'>Who to follow</h2>
10-
<div className='flex flex-col gap-6 mt-4'>{/* USER LIST */}</div>
16+
<div className='flex flex-col gap-6 mt-4'>
17+
{users.map((user: Record<string, any>) => (
18+
<div key={user.id} className='flex flex-row gap-4'>
19+
<Avatar userId={user.id} />
20+
<div className='flex flex-col'>
21+
<p className='text-white font-semibold text-sm'>{user.name}</p>
22+
<p className='text-neutral-400 text-sm'>@{user.username}</p>
23+
</div>
24+
</div>
25+
))}
26+
</div>
1127
</div>
1228
</div>
1329
);

components/modals/RegisterModal.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function RegisterModal({}: Props) {
2020
const onSubmit = useCallback(async () => {
2121
try {
2222
setIsLoading(true);
23-
await axios.post('/api/register', { email, password, username });
23+
await axios.post('/api/register', { email, password, username, name });
2424
toast.success('Account created');
2525
signIn('credentials', { email, password });
2626
registerModal.onClose();
@@ -31,7 +31,7 @@ function RegisterModal({}: Props) {
3131
} finally {
3232
setIsLoading(false);
3333
}
34-
}, [email, password, registerModal, username]);
34+
}, [email, name, password, registerModal, username]);
3535

3636
const onToggle = useCallback(() => {
3737
if (isLoading) return;

components/users/UserBio.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import useCurrentUser from '@/hooks/useCurrentUser';
2+
import useUser from '@/hooks/useUser';
3+
import React, { useMemo } from 'react';
4+
import { format } from 'date-fns';
5+
import { BiCalendar } from 'react-icons/bi';
6+
import Button from '../Button';
7+
8+
type Props = {
9+
userId: string;
10+
};
11+
12+
function UserBio({ userId }: Props) {
13+
const { data: currentUser } = useCurrentUser();
14+
const { data: fetchedUser } = useUser(userId);
15+
const createdAt = useMemo(() => {
16+
if (!fetchedUser) return null;
17+
18+
return format(new Date(fetchedUser.createdAt), 'MMMM yyyy');
19+
}, [fetchedUser?.createdAt]);
20+
21+
return (
22+
<div className='border-b-[1px] border-neutral-800 pb-4'>
23+
<div className='flex justify-end p-2'>
24+
{currentUser?.id === userId ? (
25+
<Button secondary label='Edit' onClick={() => {}} />
26+
) : (
27+
<Button onClick={() => {}} label='Follow' secondary />
28+
)}
29+
</div>
30+
<div className='mt-8 px-4'>
31+
<div className='flex flex-col'>
32+
<p className='text-white text-2xl font-semibold'>{fetchedUser?.name}</p>
33+
<p className='text-md text-neutral-500'>@{fetchedUser?.username}</p>
34+
</div>
35+
<div className='flex flex-col mt-4'>
36+
<p className='text-white'>{fetchedUser?.bio}</p>
37+
<div className='flex flex-row items-center gap-2 mt-4 text-neutral-500'>
38+
<BiCalendar />
39+
<p>Joined {createdAt}</p>
40+
</div>
41+
</div>
42+
<div className='flex flex-row items-center mt-4 gap-6'>
43+
<div className='flex flex-row items-center gap-1'>
44+
<p className='text-white'>{fetchedUser?.followingIds?.length}</p>
45+
<p className='text-neutral-500'>Following</p>
46+
</div>
47+
<div className='flex flex-row items-center gap-1'>
48+
<p className='text-white'>{fetchedUser?.followersCount || 0}</p>
49+
<p className='text-neutral-500'>Followers</p>
50+
</div>
51+
</div>
52+
</div>
53+
</div>
54+
);
55+
}
56+
57+
export default UserBio;

components/users/UserHero.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
import Image from 'next/image';
3+
import useUser from '@/hooks/useUser';
4+
import Avatar from '../Avatar';
5+
6+
type Props = {
7+
userId: string;
8+
};
9+
10+
function UserHero({ userId }: Props) {
11+
const { data: fetchedUser } = useUser(userId);
12+
13+
return (
14+
<div className='bg-neutral-700 h-44 relative'>
15+
{fetchedUser?.coverImage && (
16+
<Image fill src={fetchedUser.coverImage} alt='cover image' style={{ objectFit: 'cover' }} />
17+
)}
18+
<div className='absolute -bottom-16 left-4'>
19+
<Avatar userId={userId} isLarge hasBorder />
20+
</div>
21+
</div>
22+
);
23+
}
24+
25+
export default UserHero;

hooks/useUser.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import useSWR from 'swr';
2+
import fetcher from '@/libs/fetcher';
3+
4+
const useUser = (userId: string) => {
5+
const { data, error, isLoading, mutate } = useSWR(
6+
userId ? `/api/users/${userId}` : null,
7+
fetcher
8+
);
9+
return { data, error, isLoading, mutate };
10+
};
11+
12+
export default useUser;

hooks/useUsers.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import useSWR from 'swr';
2+
import fetcher from '@/libs/fetcher';
3+
4+
const useUsers = () => {
5+
const { data, error, isLoading, mutate } = useSWR('/api/users', fetcher);
6+
return { data, error, isLoading, mutate };
7+
};
8+
9+
export default useUsers;

package-lock.json

+34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"autoprefixer": "10.4.14",
1818
"axios": "^1.3.5",
1919
"bcrypt": "^5.1.0",
20+
"date-fns": "^2.29.3",
2021
"eslint": "8.38.0",
2122
"eslint-config-next": "13.3.0",
2223
"next": "13.3.0",
@@ -26,6 +27,7 @@
2627
"react-dom": "18.2.0",
2728
"react-hot-toast": "^2.4.0",
2829
"react-icons": "^4.8.0",
30+
"react-spinners": "^0.13.8",
2931
"swr": "^2.1.3",
3032
"tailwindcss": "3.3.1",
3133
"typescript": "5.0.4",

pages/api/register.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
88
}
99

1010
try {
11-
const { email, username, password } = req.body;
11+
const { email, username, password, name } = req.body;
1212
const hashedPassword = await bcrypt.hash(password, 12);
1313
const user = await prisma.user.create({
1414
data: {
1515
email,
1616
username,
17+
name,
1718
hashedPassword,
1819
},
1920
});

pages/api/users/[userId].ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
import prisma from '@/libs/prismadb';
3+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
4+
if (req.method !== 'GET') {
5+
return res.status(405).end();
6+
}
7+
8+
try {
9+
const { userId } = req.query;
10+
11+
if (!userId || typeof userId !== 'string') {
12+
throw new Error('Invalid ID');
13+
}
14+
15+
const existingUser = await prisma.user.findUnique({
16+
where: { id: userId },
17+
});
18+
19+
const followersCount = await prisma.user.count({ where: { followingIds: { has: userId } } });
20+
21+
return res.status(200).json({ ...existingUser, followersCount });
22+
} catch (error) {
23+
console.log(error);
24+
return res.status(400).end();
25+
}
26+
}

pages/api/users/index.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
import prisma from '@/libs/prismadb';
3+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
4+
if (req.method !== 'GET') {
5+
return res.status(405).end();
6+
}
7+
8+
try {
9+
const user = await prisma.user.findMany({
10+
orderBy: { createdAt: 'desc' },
11+
});
12+
13+
return res.status(200).json(user);
14+
} catch (error) {
15+
console.log(error);
16+
return res.status(400).end();
17+
}
18+
}

pages/users/[userId].tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Header from '@/components/Header';
2+
import UserBio from '@/components/users/UserBio';
3+
import UserHero from '@/components/users/UserHero';
4+
import useUser from '@/hooks/useUser';
5+
import { useRouter } from 'next/router';
6+
import React from 'react';
7+
import { ClipLoader } from 'react-spinners';
8+
9+
type Props = {};
10+
11+
function UserView({}: Props) {
12+
const router = useRouter();
13+
const { userId } = router.query;
14+
15+
const { data: fetchedUser, isLoading } = useUser(userId as string);
16+
17+
if (isLoading || !fetchedUser) {
18+
return (
19+
<div className='flex justify-center items-center h-full'>
20+
<ClipLoader color='lightblue' size={80} />
21+
</div>
22+
);
23+
}
24+
25+
return (
26+
<>
27+
<Header label={fetchedUser?.name} showBackArrow />
28+
<UserHero userId={fetchedUser?.id} />
29+
<UserBio userId={fetchedUser?.id} />
30+
</>
31+
);
32+
}
33+
34+
export default UserView;

public/images/placeholder.png

211 KB
Loading

0 commit comments

Comments
 (0)