Improve the Discord status component
All checks were successful
Deploy Site / docker (ubuntu-latest, 2.44.0) (push) Successful in 2m10s

This commit is contained in:
Braydon 2024-09-04 00:11:36 -04:00
parent 65f8d0c189
commit f623cf4bfc
12 changed files with 151 additions and 75 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -14,6 +14,7 @@
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.3.30",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -19,7 +19,9 @@ const LandingPage = (): ReactElement => (
<BlurFade className="my-7" delay={1.25} inView>
<Navigation />
</BlurFade>
<DiscordStatus />
<BlurFade className="my-7" delay={1.85} inView>
<DiscordStatus />
</BlurFade>
</div>
</main>
);

View File

@ -1,89 +1,152 @@
"use client";
import { ReactElement } from "react";
import { Activity, Data, DiscordUser, useLanyardWS } from "use-lanyard";
import BlurFade from "@/components/ui/blur-fade";
import { ReactElement, useEffect, useState } from "react";
import { Data, DiscordUser, useLanyardWS } from "use-lanyard";
import DCDNUser from "@/types/dcdn";
import axios from "axios";
import Image from "next/image";
import { cn } from "@/lib/utils";
import { PlayIcon } from "@heroicons/react/24/outline";
const statusColors = {
online: "bg-green-500",
idle: "bg-yellow-500",
dnd: "bg-red-500",
offline: "bg-zinc-500",
};
const DiscordStatus = (): ReactElement => {
const userBadges = {
// Nitro
"https://cdn.discordapp.com/badge-icons/2ba85e8026a8614b640c2837bcdfe21b.png":
(dcdnUser: DCDNUser) => dcdnUser.premiumType,
// Early Supporter
"https://cdn.discordapp.com/badge-icons/7060786766c9c840eb3019e725d2b358.png":
(dcdnUser: DCDNUser) => (dcdnUser.flags & (1 << 9)) === 1 << 9,
// Active Developer
"https://cdn.discordapp.com/badge-icons/6bdc42827a38498929a4920da12695d9.png":
(dcdnUser: DCDNUser) => (dcdnUser.flags & (1 << 22)) === 1 << 22,
};
const DiscordStatus = (): ReactElement | undefined => {
// Data from Lanyard
const discordData: Data | undefined = useLanyardWS("504147739131641857");
const discordUser: DiscordUser | undefined = discordData?.discord_user;
// Some data isn't provided by Lanyard, use DCDN for more
const [dcdnUser, setDCDNUser] = useState<DCDNUser | undefined>(undefined);
// When the data changes, update the DCDN user
useEffect(() => {
if (!discordUser) {
return;
}
axios
.get(`https://dcdn.dstn.to/profile/${discordUser.id}`)
.then((res) =>
setDCDNUser({
premiumType: res.data.premium_type,
...res.data.user,
})
);
}, [discordData]);
// Missing required data
if (!discordData || !discordUser || !dcdnUser) {
return undefined;
}
return (
<div className="absolute left-4 bottom-4">
{discordData && discordUser && (
<BlurFade
className="flex select-none pointer-events-none"
delay={1.65}
inView
>
{/* Avatar & Status */}
<div className="relative z-50">
<Image
className="scale-110 rounded-full border"
src={`https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.webp`}
alt={`${discordUser.username}'s Discord Avatar`}
width={60}
height={60}
/>
{discordData.discord_status !== "offline" && (
<div
className={cn(
"absolute bottom-1.5 right-1 w-2.5 h-2.5 bg-red-500 rounded-full",
statusColors[discordData.discord_status]
)}
/>
)}
</div>
<div className="flex justify-center">
<div className="flex flex-col bg-zinc-900 rounded-xl">
<BannerAvatar
discordData={discordData}
discordUser={discordUser}
dcdnUser={dcdnUser}
/>
{/* Username & Basic Activity */}
<div className="-translate-x-7 p-2 pl-10 pr-6 flex flex-col justify-center bg-white/55 dark:bg-zinc-800/45 border rounded-r-3xl">
<div className="flex gap-1 items-center">
<h1 className="text-lg font-bold">
{discordUser.display_name}
</h1>
<h2 className="opacity-65">
{discordUser.username}
</h2>
</div>
{discordData.activities.length > 0 && (
<BasicActivityDisplay
activity={discordData.activities[0]}
/>
)}
<div className="px-3.5 pt-1.5 py-2.5 flex flex-col">
<div className="ml-[5.65rem] flex items-center">
{dcdnUser.bio && <Bio dcdnUser={dcdnUser} />}
<Badges dcdnUser={dcdnUser} />
</div>
</BlurFade>
)}
<div className="mt-3">
<Username discordUser={discordUser} />
</div>
</div>
</div>
</div>
);
};
const BasicActivityDisplay = ({
activity,
const BannerAvatar = ({
discordData,
discordUser,
dcdnUser,
}: {
activity: Activity;
}): ReactElement => {
const prefix =
activity.type === 0
? "Playing"
: activity.type === 1
? "Streaming"
: "Listening to";
return (
<div className="flex gap-1.5 items-center text-sm">
<span className="opacity-80">{prefix}</span> {activity.name}{" "}
<PlayIcon className="text-blue-500" width={16} height={16} />
discordData: Data;
discordUser: DiscordUser;
dcdnUser: DCDNUser;
}): ReactElement => (
<div className="relative select-none">
<Image
className="rounded-t-xl"
src={`https://cdn.discordapp.com/banners/${discordUser.id}/${dcdnUser.banner}.webp?size=1024`}
alt={`${discordUser.username}'s Banner`}
width={300}
height={300}
/>
<div className="relative">
<Image
className="absolute left-2 -bottom-12 border-[5px] border-zinc-900 rounded-full"
src={`https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.webp?size=1024`}
alt={`${discordUser.username}'s Avatar`}
width={96}
height={96}
/>
<div
className={cn(
"absolute left-[4.5rem] -bottom-12 w-7 h-7 border-[5px] border-zinc-900 rounded-full",
statusColors[discordData.discord_status]
)}
/>
</div>
);
};
</div>
);
const Bio = ({ dcdnUser }: { dcdnUser: DCDNUser }): ReactElement => (
<div className="p-2 bg-zinc-950/75 text-sm rounded-xl select-none">
{dcdnUser.bio}
</div>
);
const Badges = ({ dcdnUser }: { dcdnUser: DCDNUser }): ReactElement => (
<div className="ml-auto flex gap-1">
{Object.entries(userBadges)
.filter(([_, predicate]) => predicate(dcdnUser))
.map(([badge], index) => (
<Image
key={index}
src={badge}
alt="Discord Profile Badge"
width={22}
height={22}
/>
))}
</div>
);
const Username = ({
discordUser,
}: {
discordUser: DiscordUser;
}): ReactElement => (
<div className="flex flex-col">
<h1 className="text-lg font-bold leading-none">
{discordUser.global_name}
</h1>
<h2 className="font-light opacity-70">{discordUser.username}</h2>
</div>
);
export default DiscordStatus;

View File

@ -74,7 +74,7 @@ const MyWork = (): ReactElement => {
target="_blank"
>
<MagicCard
className="w-[15rem] lg:w-[25rem] p-3.5 cursor-default opacity-95"
className="w-[15rem] lg:w-[25rem] p-3.5 opacity-95"
gradientColor={
theme === "dark" ? "#262626" : "#D9D9D9"
}

View File

@ -121,7 +121,7 @@ const Skills = (): ReactElement => (
{skillset.map((skill, index) => (
<Link
key={index}
className="cursor-default hover:opacity-75 transition-all transform-gpu"
className="hover:opacity-75 transition-all transform-gpu"
href={skill.link}
target="_blank"
>

View File

@ -33,7 +33,7 @@ const Navbar = (): ReactElement => (
const Branding = (): ReactElement => (
<Link
className="flex gap-3 items-center hover:opacity-75 cursor-default select-none transition-all transform-gpu"
className="flex gap-3 items-center hover:opacity-75 select-none transition-all transform-gpu"
href="/"
>
<Image
@ -92,7 +92,7 @@ const UsefulLinksContent = (): ReactElement => (
<div className="w-[15.5rem] p-3 flex flex-wrap gap-5">
{/* Git */}
<Link href="https://git.rainnny.club" target="_blank">
<Button className="gap-3 cursor-default" variant="ghost">
<Button className="gap-3" variant="ghost">
<CodeBracketIcon width={24} height={24} />
<span>Gitea</span>
</Button>
@ -100,7 +100,7 @@ const UsefulLinksContent = (): ReactElement => (
{/* Wiki */}
<Link href="https://docs.rainnny.club" target="_blank">
<Button className="gap-3 cursor-default" variant="ghost">
<Button className="gap-3" variant="ghost">
<BookOpenIcon width={24} height={24} />
<span>Wiki</span>
</Button>
@ -108,7 +108,7 @@ const UsefulLinksContent = (): ReactElement => (
{/* Status Page */}
<Link href="https://status.rainnny.club" target="_blank">
<Button className="gap-3 cursor-default" variant="ghost">
<Button className="gap-3" variant="ghost">
<SignalIcon width={24} height={24} />
<span>Status</span>
</Button>

View File

@ -46,7 +46,7 @@ const Navigation = (): ReactElement => {
<BlurFade key={index} delay={0.9 + 0.3 * index} inView>
<Button
className={cn(
"px-3 sm:px-4 py-6 gap-2 shadow-sm bg-white/75 dark:bg-zinc-800/75 rounded-xl cursor-default hover:opacity-75 transition-all transform-gpu",
"px-3 sm:px-4 py-6 gap-2 shadow-sm bg-white/75 dark:bg-zinc-800/75 rounded-xl hover:opacity-75 transition-all transform-gpu",
active && "opacity-70"
)}
variant="ghost"

View File

@ -12,7 +12,7 @@ const ThemeSwitcher = (): ReactElement => {
const isLight = theme === "light";
return (
<Button
className="relative px-5 py-1.5 flex items-center cursor-default hover:opacity-85"
className="relative px-5 py-1.5 flex items-center hover:opacity-85"
variant="ghost"
onClick={() => setTheme(isLight ? "dark" : "light")}
>

View File

@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium cursor-default transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
);
const NavigationMenuTrigger = React.forwardRef<

10
src/types/dcdn.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* The user type from <a href="https://dcdn.dstn.to/profile/504147739131641857">DCDN</a>
*/
type DCDNUser = {
flags: number;
banner: string | undefined;
bio: string | undefined;
premiumType: number | undefined; // 1 = Nitro Classic, 2 = Nitro
};
export default DCDNUser;