Skip to content

Commit

Permalink
feat: implement profile page (#4)
Browse files Browse the repository at this point in the history
Includes:
* Activities
* Daily Streak
* Profile Page
* Following
  • Loading branch information
dinkelspiel authored May 9, 2024
1 parent 3d22ad0 commit 2c510f3
Show file tree
Hide file tree
Showing 15 changed files with 1,180 additions and 10 deletions.
5 changes: 4 additions & 1 deletion app/(app)/dashboard/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ const Dashboard = ({
}
return b.watchedAt.getTime() - a.watchedAt.getTime();
case 'updated':
return b.updatedAt.getTime() - a.updatedAt.getTime();
return (
new Date(b.updatedAt).getTime() -
new Date(a.updatedAt).getTime()
);
}
})
.map(userEntry => {
Expand Down
48 changes: 48 additions & 0 deletions app/(app)/users/[username]/diary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import prisma from '@/server/db';

export type Diary = Record<
string,
{
title: string;
day: number;
}[]
>;

export const getUserDiary = async (userId: number): Promise<Diary> => {
const userEntries = await prisma.userEntry.findMany({
where: {
userId,
status: 'completed',
},
orderBy: {
id: 'desc',
},
take: 10,
include: {
entry: true,
},
});

const diary: Diary = {};

userEntries.forEach(userEntry => {
const month = userEntry
.watchedAt!.toLocaleString('default', { month: 'short' })
.toUpperCase()
.slice(0, 3);
if (!diary[month]) {
diary[month] = [];
}
diary[month]!.push({
title: userEntry.entry.originalTitle,
day: userEntry.watchedAt!.getDate(),
});
});

const sortedDiary: Diary = {};
for (const key in diary) {
sortedDiary[key] = diary[key]!.sort((a, b) => b.day - a.day);
}

return sortedDiary;
};
65 changes: 65 additions & 0 deletions app/(app)/users/[username]/followButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import { Button } from '@/components/ui/button';
import { User, UserFollow } from '@prisma/client';
import React, { useEffect, useState } from 'react';
import { toast } from 'sonner';

const FollowButton = ({
authUser,
user,
}: {
authUser: User;
user: User & { followers: UserFollow[] };
}) => {
const [following, setFollowing] = useState(false);

const toggleFollow = async () => {
if (!authUser) {
return;
}

const oldFollowingState = Boolean(following);

setFollowing(!following);

let response = await fetch(`/api/user/follows/${user.id}`, {
method: following ? 'DELETE' : 'POST',
headers: {
'Content-Type': 'application/json',
},
});

if (response.status !== 200) {
toast.error(
`Failed ${following ? 'unfollowing' : 'following'} user with error "${(await response.json()).error}"`
);
setFollowing(oldFollowingState);
} else {
toast.success(
`Successfully ${following ? 'unfollowed' : 'followed'} user`
);
}
};

useEffect(() => {
setFollowing(
authUser
? user.followers.find(
e => e.userId === authUser.id && e.isFollowing
) !== undefined
: false
);
}, [authUser, user]);

return (
<Button
variant={following ? 'destructive' : 'outline'}
onClick={() => toggleFollow()}
>
{following ? 'Unfollow' : 'Follow'}
</Button>
);
};

export default FollowButton;
224 changes: 224 additions & 0 deletions app/(app)/users/[username]/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
'use client';

import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import {
Header,
HeaderContent,
HeaderDescription,
HeaderHeader,
HeaderTitle,
} from '@/components/header';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import Link from 'next/link';
import { validateSessionToken } from '@/server/auth/validateSession';
import { User, UserEntry, UserFollow } from '@prisma/client';
import FollowButton from './followButton';

export const ProfileHeader = ({
user,
profileUser,
}: {
user: User | null;
profileUser: User & {
userEntries: UserEntry[];
followers: (UserFollow & {
user: User & { following: UserFollow[]; followers: UserFollow[] };
})[];
following: (UserFollow & {
follow: User & { following: UserFollow[]; followers: UserFollow[] };
})[];
};
}) => {
const [page, setPage] = useState<'Following' | 'Followers'>('Followers');

const FollowEditProfile = () => {
return (
user &&
user.id !== profileUser.id && (
<FollowButton user={profileUser} authUser={user} />
)
);
};

return (
<Header className="col-span-2 h-max flex-col items-center justify-center gap-6 lg:flex-row">
<div className="flex w-full flex-row items-center justify-between lg:w-max lg:justify-start">
<HeaderHeader className="pt-4 text-center lg:pt-0 lg:text-left">
<HeaderTitle>{profileUser.username}</HeaderTitle>
<HeaderDescription>
{profileUser.username}'s profile
</HeaderDescription>
</HeaderHeader>
<div className="block lg:hidden">
<FollowEditProfile />
</div>
</div>
<div className="flex w-full flex-1 flex-row items-center justify-center lg:justify-between">
<div className="hidden lg:block">
<FollowEditProfile />
</div>
<div className="grid w-full grid-cols-[1fr,1fr,2fr] justify-between px-6 sm:flex sm:w-max sm:flex-row sm:justify-start sm:gap-6 sm:px-0">
<div className="flex flex-col">
<div className="text-center text-2xl font-semibold">
{profileUser.userEntries.length}
</div>
<div className="text-center text-sm text-slate-500">Watched</div>
</div>
<div className="flex flex-col">
<div className="text-center text-2xl font-semibold">
{
profileUser.userEntries.filter(e =>
e.watchedAt
? e.watchedAt?.getTime() >
new Date(new Date().getFullYear().toString()).getTime()
: false
).length
}
</div>
<div className="text-center text-sm text-slate-500">This Year</div>
</div>
<Sheet>
<SheetTrigger asChild>
<div className="grid cursor-pointer grid-cols-2 sm:flex sm:flex-row sm:gap-6">
<div
className="flex flex-col"
onMouseOver={() => setPage('Following')}
>
<div className="text-center text-2xl font-semibold">
{profileUser.following.filter(e => e.isFollowing).length}
</div>
<div className="text-center text-sm text-slate-500">
Following
</div>
</div>
<div
className="flex flex-col"
onMouseOver={() => setPage('Followers')}
>
<div className="text-center text-2xl font-semibold">
{profileUser.followers.filter(e => e.isFollowing).length}
</div>
<div className="text-center text-sm text-slate-500">
Followers
</div>
</div>
</div>
</SheetTrigger>
<SheetContent className="!max-w-[450px] min-[450px]:max-w-[100dvw]">
<SheetHeader>
<SheetTitle className="flex flex-row gap-6">
<div
className={cn(
`cursor-pointer px-2 pb-1 text-lg font-semibold text-slate-500`,
{
'border-b border-b-black text-black':
page === 'Following',
}
)}
onClick={() => setPage('Following')}
>
Following
</div>
<div
className={cn(
`cursor-pointer px-2 pb-1 text-lg font-semibold text-slate-500`,
{
'border-b border-b-black text-black':
page === 'Followers',
}
)}
onClick={() => setPage('Followers')}
>
Followers
</div>
</SheetTitle>
</SheetHeader>
<UserList
users={
(page === 'Followers'
? profileUser.followers
: profileUser.following) as (UserFollow & {
follow: User & {
following: UserFollow[];
followers: UserFollow[];
};
} & {
user: User & {
following: UserFollow[];
followers: UserFollow[];
};
})[]
}
authUser={user}
/>
</SheetContent>
</Sheet>
</div>
</div>
</Header>
);
};

const UserList = ({
users,
authUser,
}: {
users: (UserFollow & {
follow: User & { following: UserFollow[]; followers: UserFollow[] };
} & { user: User & { following: UserFollow[]; followers: UserFollow[] } })[];
authUser: User | null;
}) => {
return (
<div className="py-6">
<div className="grid grid-cols-[1fr,96px] items-center gap-3">
{users
.filter(e => e.isFollowing)
.map(user => {
if (user.follow) {
return <UserCard user={user.follow} authUser={authUser} />;
}

if (user.user) {
return <UserCard user={user.user} authUser={authUser} />;
}
})}
</div>
</div>
);
};

const UserCard = ({
user,
authUser,
}: {
user: User & { following: UserFollow[]; followers: UserFollow[] };
authUser: User | null;
}) => {
return (
<React.Fragment key={user.username}>
<Link href={`/@${user.username}`}>
<div className="flex flex-col">
<div className="font-semibold">{user.username}</div>
<div className="text-sm text-slate-500">
{user.followers.filter(e => e.isFollowing).length} followers,
following {user.following.filter(e => e.isFollowing).length}
</div>
</div>
</Link>
<div className="flex flex-row justify-end">
{authUser && authUser.username !== user.username && (
<FollowButton user={user} authUser={authUser} />
)}
</div>
</React.Fragment>
);
};
Loading

0 comments on commit 2c510f3

Please sign in to comment.