-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
04aa509
commit b13805b
Showing
8 changed files
with
466 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { getCachedSolvedChallenges } from "@/cache/user"; | ||
import { | ||
Table, | ||
TableBody, | ||
TableCaption, | ||
TableCell, | ||
TableHead, | ||
TableHeader, | ||
TableRow, | ||
} from "@/components/ui/table"; | ||
import { fromNow } from "@/utils/time"; | ||
|
||
import { Metadata } from "next"; | ||
import Link from "next/link"; | ||
|
||
export const metadata: Metadata = { | ||
title: "Solved Challenges", | ||
}; | ||
|
||
async function Challenges({ params }: { params: { username: string } }) { | ||
const username = decodeURIComponent(params.username).replace("@", ""); | ||
const challenges = await getCachedSolvedChallenges(username); | ||
return ( | ||
<> | ||
<div className="flex flex-col p-6 pb-0"> | ||
<h3 className="text-lg font-semibold leading-none tracking-tight"> | ||
Solved Challenges | ||
</h3> | ||
</div> | ||
<div className="p-6"> | ||
{challenges.length > 0 ? ( | ||
<Table> | ||
<TableHeader> | ||
<TableRow> | ||
<TableHead>Name</TableHead> | ||
<TableHead>Latest Submission</TableHead> | ||
</TableRow> | ||
</TableHeader> | ||
<TableBody> | ||
{challenges.map((challenge) => ( | ||
<TableRow key={challenge.id}> | ||
<TableCell className="font-medium"> | ||
<Link | ||
href={`/track/${challenge.track!.slug}/challenge/${ | ||
challenge.slug | ||
}`} | ||
className="hover:underline" | ||
> | ||
{challenge.label} | ||
</Link> | ||
</TableCell> | ||
<TableCell> | ||
{fromNow(challenge.solves[0].createdAt)} | ||
</TableCell> | ||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
) : null} | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default Challenges; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
"use client"; | ||
|
||
import { getCacheSolvesCount, getCacheUser } from "@/cache/user"; | ||
import { Button, buttonVariants } from "@/components/ui/button"; | ||
import { AudioWaveform, BadgeCheck, CheckCheck } from "lucide-react"; | ||
import { useSession } from "next-auth/react"; | ||
import Link from "next/link"; | ||
import { usePathname } from "next/navigation"; | ||
import { IoCog } from "react-icons/io5"; | ||
|
||
function LayoutClientComponents({ username }: { username: string }) { | ||
const { data: session } = useSession(); | ||
return ( | ||
<> | ||
{session?.user?.username?.toLowerCase() === username.toLowerCase() ? ( | ||
<Link | ||
className={buttonVariants({ | ||
variant: "outline", | ||
className: | ||
"rounded-xl text-left flex flex-row items-center !justify-start gap-2", | ||
})} | ||
href="/settings" | ||
> | ||
<IoCog size={24} /> | ||
Settings | ||
</Link> | ||
) : null} | ||
</> | ||
); | ||
} | ||
|
||
export default LayoutClientComponents; | ||
|
||
export function ProfileButtons({ | ||
user, | ||
username, | ||
solves, | ||
}: { | ||
username: string; | ||
user: NonNullable<Awaited<ReturnType<typeof getCacheUser>>>; | ||
solves: NonNullable<Awaited<ReturnType<typeof getCacheSolvesCount>>>; | ||
}) { | ||
const pathname = usePathname() | ||
.replaceAll(`/@${username}/`, "") | ||
.replace("/", ""); | ||
return ( | ||
<div className="md:flex grid grid-cols-1 grid-rows-4 sm:grid-cols-2 sm:grid-rows-2 gap-4 md:w-full md:flex-col"> | ||
<Link | ||
href={`/@${username}/challenges`} | ||
className={buttonVariants({ | ||
variant: pathname === "challenges" ? "secondary" : "outline", | ||
className: | ||
"rounded-xl text-left flex flex-row items-center !justify-start gap-2 ", | ||
})} | ||
> | ||
<BadgeCheck className="text-green-500" /> | ||
{solves?.[0]?.solves_count || 0}{" "} | ||
{solves?.[0]?.solves_count <= 1 ? "Challenge" : "Challenges"} Conquered | ||
</Link> | ||
<Button | ||
variant={"outline"} | ||
className="rounded-xl text-left flex flex-row items-center justify-start gap-2" | ||
> | ||
<AudioWaveform className="text-blue-500" /> | ||
Enrolled in {user._count.tracks}{" "} | ||
{user._count.tracks <= 1 ? "Track" : "Tracks"} | ||
</Button> | ||
<Button | ||
variant={"outline"} | ||
className="rounded-xl text-left flex flex-row items-center justify-start gap-2" | ||
> | ||
<CheckCheck className="text-purple-500" /> | ||
{user._count.solutions}{" "} | ||
{user._count.solutions <= 1 ? "Solution" : "Solutions"} Posted | ||
</Button> | ||
<LayoutClientComponents username={username} /> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { PropsWithChildren } from "react"; | ||
import { notFound } from "next/navigation"; | ||
import { getCacheSolvesCount, getCacheUser } from "@/cache/user"; | ||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | ||
import { fromNow } from "@/utils/time"; | ||
import { ProfileButtons } from "./layout.client"; | ||
import { Metadata } from "next"; | ||
import { env } from "@/env.mjs"; | ||
import { siteMetadataConfig } from "@repo/config"; | ||
|
||
export async function generateMetadata({ | ||
params, | ||
}: { | ||
params: { username: string }; | ||
}): Promise<Metadata> { | ||
const username = decodeURIComponent(params.username).replace("@", ""); | ||
const user = await getCacheUser(username); | ||
|
||
if (!user) | ||
return { | ||
title: `${username} profile`, | ||
description: `View profile of ${username} on FrameGround`, | ||
}; | ||
const searchParams = new URLSearchParams(); | ||
searchParams.set("name", user.name!); | ||
searchParams.set("username", user.username!); | ||
return { | ||
metadataBase: new URL(env.HOST), | ||
title: { | ||
template: `${user.username}'s | %s`, | ||
default: `${user.username}'s profile`, | ||
}, | ||
description: `View profile of ${user.username} on FrameGround`, | ||
twitter: siteMetadataConfig.twitter, | ||
openGraph: { | ||
type: "website", | ||
title: `${user.username}'s profile`, | ||
description: `View profile of ${user.username} on FrameGround`, | ||
url: `${env.HOST}/@${user.username}`, | ||
images: [ | ||
{ | ||
url: `${env.HOST}/api/og/user?${searchParams.toString()}`, | ||
width: 1200, | ||
height: 600, | ||
alt: `${user.username}'s profile`, | ||
}, | ||
], | ||
}, | ||
}; | ||
} | ||
|
||
async function ProfileLayout({ | ||
params, | ||
children, | ||
}: PropsWithChildren<{ | ||
params: { username: string }; | ||
}>) { | ||
const username = decodeURIComponent(params.username).replace("@", ""); | ||
|
||
const user = await getCacheUser(username); | ||
if (!user) notFound(); | ||
const solves = await getCacheSolvesCount(user.id); | ||
return ( | ||
<> | ||
<div className="container"> | ||
<div className="flex flex-col gap-8 py-8 md:flex-row"> | ||
<div className="flex flex-col gap-4"> | ||
<Avatar className="h-32 w-32 rounded-3xl bg-cover bg-center bg-no-repeat md:h-64 md:w-64 mx-auto md:mx-0"> | ||
<AvatarImage src={`/github-avatar/${username}`} /> | ||
<AvatarFallback>{username.toUpperCase()}</AvatarFallback> | ||
</Avatar> | ||
<div className="flex flex-col items-center gap-2 md:w-full md:items-start "> | ||
<div className="flex gap-0 flex-col"> | ||
<div className="text-[2rem] font-bold [&>small]:text-[0.7em] [&>small]:dark:text-slate-400 [&>small]:text-slate-600 font-default h-fit"> | ||
{user?.name} | ||
</div> | ||
<span className="text-lg text-muted-foreground"> | ||
@{user?.username} | ||
</span> | ||
</div> | ||
<span className="text-muted-foreground tracking-tight mt-4"> | ||
Joined {fromNow(new Date(user!.createdAt!))} | ||
</span> | ||
<ProfileButtons solves={solves} user={user} username={username} /> | ||
</div> | ||
</div> | ||
<div className="bg-card text-card-foreground rounded-3xl border shadow-sm col-span-4 md:min-h-[calc(100vh_-_56px_-_6rem)] w-full"> | ||
{children} | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default ProfileLayout; |
Oops, something went wrong.