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-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.3.30", "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> <BlurFade className="my-7" delay={1.25} inView>
<Navigation /> <Navigation />
</BlurFade> </BlurFade>
<DiscordStatus /> <BlurFade className="my-7" delay={1.85} inView>
<DiscordStatus />
</BlurFade>
</div> </div>
</main> </main>
); );

View File

@ -1,89 +1,152 @@
"use client"; "use client";
import { ReactElement } from "react"; import { ReactElement, useEffect, useState } from "react";
import { Activity, Data, DiscordUser, useLanyardWS } from "use-lanyard"; import { Data, DiscordUser, useLanyardWS } from "use-lanyard";
import BlurFade from "@/components/ui/blur-fade"; import DCDNUser from "@/types/dcdn";
import axios from "axios";
import Image from "next/image"; import Image from "next/image";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PlayIcon } from "@heroicons/react/24/outline";
const statusColors = { const statusColors = {
online: "bg-green-500", online: "bg-green-500",
idle: "bg-yellow-500", idle: "bg-yellow-500",
dnd: "bg-red-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 discordData: Data | undefined = useLanyardWS("504147739131641857");
const discordUser: DiscordUser | undefined = discordData?.discord_user; 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 ( return (
<div className="absolute left-4 bottom-4"> <div className="flex justify-center">
{discordData && discordUser && ( <div className="flex flex-col bg-zinc-900 rounded-xl">
<BlurFade <BannerAvatar
className="flex select-none pointer-events-none" discordData={discordData}
delay={1.65} discordUser={discordUser}
inView dcdnUser={dcdnUser}
> />
{/* 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>
{/* Username & Basic Activity */} <div className="px-3.5 pt-1.5 py-2.5 flex flex-col">
<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="ml-[5.65rem] flex items-center">
<div className="flex gap-1 items-center"> {dcdnUser.bio && <Bio dcdnUser={dcdnUser} />}
<h1 className="text-lg font-bold"> <Badges dcdnUser={dcdnUser} />
{discordUser.display_name}
</h1>
<h2 className="opacity-65">
{discordUser.username}
</h2>
</div>
{discordData.activities.length > 0 && (
<BasicActivityDisplay
activity={discordData.activities[0]}
/>
)}
</div> </div>
</BlurFade> <div className="mt-3">
)} <Username discordUser={discordUser} />
</div>
</div>
</div>
</div> </div>
); );
}; };
const BasicActivityDisplay = ({ const BannerAvatar = ({
activity, discordData,
discordUser,
dcdnUser,
}: { }: {
activity: Activity; discordData: Data;
}): ReactElement => { discordUser: DiscordUser;
const prefix = dcdnUser: DCDNUser;
activity.type === 0 }): ReactElement => (
? "Playing" <div className="relative select-none">
: activity.type === 1 <Image
? "Streaming" className="rounded-t-xl"
: "Listening to"; src={`https://cdn.discordapp.com/banners/${discordUser.id}/${dcdnUser.banner}.webp?size=1024`}
alt={`${discordUser.username}'s Banner`}
return ( width={300}
<div className="flex gap-1.5 items-center text-sm"> height={300}
<span className="opacity-80">{prefix}</span> {activity.name}{" "} />
<PlayIcon className="text-blue-500" width={16} height={16} /> <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>
); </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; export default DiscordStatus;

View File

@ -74,7 +74,7 @@ const MyWork = (): ReactElement => {
target="_blank" target="_blank"
> >
<MagicCard <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={ gradientColor={
theme === "dark" ? "#262626" : "#D9D9D9" theme === "dark" ? "#262626" : "#D9D9D9"
} }

View File

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

View File

@ -33,7 +33,7 @@ const Navbar = (): ReactElement => (
const Branding = (): ReactElement => ( const Branding = (): ReactElement => (
<Link <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="/" href="/"
> >
<Image <Image
@ -92,7 +92,7 @@ const UsefulLinksContent = (): ReactElement => (
<div className="w-[15.5rem] p-3 flex flex-wrap gap-5"> <div className="w-[15.5rem] p-3 flex flex-wrap gap-5">
{/* Git */} {/* Git */}
<Link href="https://git.rainnny.club" target="_blank"> <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} /> <CodeBracketIcon width={24} height={24} />
<span>Gitea</span> <span>Gitea</span>
</Button> </Button>
@ -100,7 +100,7 @@ const UsefulLinksContent = (): ReactElement => (
{/* Wiki */} {/* Wiki */}
<Link href="https://docs.rainnny.club" target="_blank"> <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} /> <BookOpenIcon width={24} height={24} />
<span>Wiki</span> <span>Wiki</span>
</Button> </Button>
@ -108,7 +108,7 @@ const UsefulLinksContent = (): ReactElement => (
{/* Status Page */} {/* Status Page */}
<Link href="https://status.rainnny.club" target="_blank"> <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} /> <SignalIcon width={24} height={24} />
<span>Status</span> <span>Status</span>
</Button> </Button>

View File

@ -46,7 +46,7 @@ const Navigation = (): ReactElement => {
<BlurFade key={index} delay={0.9 + 0.3 * index} inView> <BlurFade key={index} delay={0.9 + 0.3 * index} inView>
<Button <Button
className={cn( 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" active && "opacity-70"
)} )}
variant="ghost" variant="ghost"

View File

@ -12,7 +12,7 @@ const ThemeSwitcher = (): ReactElement => {
const isLight = theme === "light"; const isLight = theme === "light";
return ( return (
<Button <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" variant="ghost"
onClick={() => setTheme(isLight ? "dark" : "light")} onClick={() => setTheme(isLight ? "dark" : "light")}
> >

View File

@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item; const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva( 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< 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;