Move to Tether

This commit is contained in:
Braydon 2024-09-09 21:25:27 -04:00
parent e31841a923
commit 195ff7f7bd
8 changed files with 191 additions and 221 deletions

View File

@ -2,7 +2,7 @@ name: Deploy Site
on: on:
push: push:
branches: ["master"] branches: [ "master" ]
paths-ignore: paths-ignore:
- README.md - README.md
- LICENSE - LICENSE
@ -12,8 +12,8 @@ jobs:
docker: docker:
strategy: strategy:
matrix: matrix:
arch: ["ubuntu-latest"] arch: [ "ubuntu-latest" ]
git-version: ["2.44.0"] git-version: [ "2.44.0" ]
runs-on: ${{ matrix.arch }} runs-on: ${{ matrix.arch }}
# Steps to run # Steps to run

View File

@ -1,4 +1,4 @@
{ {
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 4 "tabWidth": 4
} }

View File

@ -1,2 +1,3 @@
# RainnnyCLUB # RainnnyCLUB
My personal portfolio website hosted [here](https://rainnny.club) My personal portfolio website hosted [here](https://rainnny.club)

View File

@ -1,29 +1,29 @@
/** @type {import('next').NextConfig} */ /** @type {import("next").NextConfig} */
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
images: { images: {
remotePatterns: [ remotePatterns: [
{ {
protocol: "https", protocol: "https",
hostname: "cdn.rainnny.club", hostname: "cdn.rainnny.club",
}, },
{ {
protocol: "https", protocol: "https",
hostname: "cdn.discordapp.com", hostname: "cdn.discordapp.com",
}, },
{ {
protocol: "https", protocol: "https",
hostname: "i.scdn.co", hostname: "i.scdn.co",
}, },
{ {
protocol: "https", protocol: "https",
hostname: "bonfire.wtf", hostname: "bonfire.wtf",
}, },
{ {
protocol: "https", protocol: "https",
hostname: "img.icons8.com", hostname: "img.icons8.com",
}, },
], ],
}, },
}; };
export default nextConfig; export default nextConfig;

View File

@ -1,42 +1,41 @@
{ {
"name": "rainnny.club", "name": "rainnny.club",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbo", "dev": "next dev --turbo",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.1.5", "@heroicons/react": "^2.1.5",
"@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-navigation-menu": "^1.2.0",
"@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", "lucide-react": "^0.439.0",
"lucide-react": "^0.439.0", "moment": "^2.30.1",
"moment": "^2.30.1", "next": "^14.2.7",
"next": "^14.2.7", "next-themes": "^0.3.0",
"next-themes": "^0.3.0", "react": "^18",
"react": "^18", "react-dom": "^18",
"react-dom": "^18", "sharp": "^0.33.5",
"sharp": "^0.33.5", "tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7",
"tailwindcss-animate": "^1.0.7", "usetether": "^1.2.2"
"use-lanyard": "^1.5.2" },
}, "devDependencies": {
"devDependencies": { "typescript": "^5",
"typescript": "^5", "@types/node": "^20",
"@types/node": "^20", "@types/react": "^18",
"@types/react": "^18", "@types/react-dom": "^18",
"@types/react-dom": "^18", "postcss": "^8",
"postcss": "^8", "tailwindcss": "^3.4.1",
"tailwindcss": "^3.4.1", "eslint": "^8",
"eslint": "^8", "eslint-config-next": "14.2.9"
"eslint-config-next": "14.2.9" }
}
} }

View File

@ -1,8 +1,8 @@
/** @type {import('postcss-load-config').Config} */ /** @type {import("postcss-load-config").Config} */
const config = { const config = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
}, },
}; };
export default config; export default config;

View File

@ -1,27 +1,18 @@
"use client"; "use client";
import { ReactElement, useEffect, useState } from "react"; import { ReactElement, useEffect, useState } from "react";
import { import { DiscordUser, SpotifyActivity, useTetherWS } from "usetether";
Activity,
Data,
DiscordUser,
Spotify,
useLanyardWS,
} from "use-lanyard";
import DCDNUser from "@/types/dcdn";
import axios from "axios";
import Image from "next/image"; import Image from "next/image";
import { cn, truncateText } from "@/lib/utils"; import { cn, truncateText } from "@/lib/utils";
import moment from "moment";
import { PuzzlePieceIcon } from "@heroicons/react/24/outline";
import SimpleTooltip from "@/components/ui/simple-tooltip"; import SimpleTooltip from "@/components/ui/simple-tooltip";
import Link from "next/link"; import Link from "next/link";
import moment from "moment";
const statusColors = { const statusColors = {
online: "bg-green-500", ONLINE: "bg-green-500",
idle: "bg-yellow-500", IDLE: "bg-yellow-500",
dnd: "bg-red-500", DO_NOT_DISTURB: "bg-red-500",
offline: "bg-zinc-500", OFFLINE: "bg-zinc-500",
}; };
const userBadges = { const userBadges = {
@ -29,86 +20,51 @@ const userBadges = {
"https://cdn.discordapp.com/badge-icons/2ba85e8026a8614b640c2837bcdfe21b.png": "https://cdn.discordapp.com/badge-icons/2ba85e8026a8614b640c2837bcdfe21b.png":
{ {
name: "Nitro Subscriber", name: "Nitro Subscriber",
predicate: (dcdnUser: DCDNUser) => dcdnUser.premiumType, predicate: (discordUser: DiscordUser) => true, // TODO: Add Nitro predicate
}, },
// Early Supporter // Early Supporter
"https://cdn.discordapp.com/badge-icons/7060786766c9c840eb3019e725d2b358.png": "https://cdn.discordapp.com/badge-icons/7060786766c9c840eb3019e725d2b358.png":
{ {
name: "Early Supporter", name: "Early Supporter",
predicate: (dcdnUser: DCDNUser) => predicate: (discordUser: DiscordUser) =>
(dcdnUser.flags & (1 << 9)) === 1 << 9, discordUser.flags.list.includes("EARLY_SUPPORTER"),
}, },
// Active Developer // Active Developer
"https://cdn.discordapp.com/badge-icons/6bdc42827a38498929a4920da12695d9.png": "https://cdn.discordapp.com/badge-icons/6bdc42827a38498929a4920da12695d9.png":
{ {
name: "Active Developer", name: "Active Developer",
predicate: (dcdnUser: DCDNUser) => predicate: (discordUser: DiscordUser) =>
(dcdnUser.flags & (1 << 22)) === 1 << 22, discordUser.flags.list.includes("ACTIVE_DEVELOPER"),
}, },
}; };
const DiscordStatus = (): ReactElement | undefined => { const DiscordStatus = (): ReactElement | undefined => {
// Data from Lanyard const discordUser: DiscordUser | undefined =
const discordData: Data | undefined = useLanyardWS("504147739131641857"); useTetherWS("504147739131641857");
const discordUser: DiscordUser | undefined = discordData?.discord_user; if (!discordUser) {
// 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 undefined;
} }
return ( return (
<div className="flex justify-center select-none"> <div className="flex justify-center select-none">
<div className="flex flex-col bg-zinc-300 dark:bg-zinc-900 rounded-xl"> <div className="flex flex-col bg-zinc-300 dark:bg-zinc-900 rounded-xl">
<BannerAvatar <BannerAvatar user={discordUser} />
discordData={discordData}
discordUser={discordUser}
dcdnUser={dcdnUser}
/>
<div className="px-3 pt-1.5 py-2.5 flex flex-col"> <div className="px-3 pt-1.5 py-2.5 flex flex-col">
<div className="ml-[5.65rem] flex items-start"> <div className="ml-[5.65rem] flex items-start">
{dcdnUser.bio && <Bio bio={dcdnUser.bio} />} <Bio bio="7th ward" /> {/* TODO: Add bio */}
<Badges dcdnUser={dcdnUser} /> <Badges discordUser={discordUser} />
</div> </div>
<div className="mt-3"> <div className="mt-3">
<Username discordUser={discordUser} /> <Username discordUser={discordUser} />
{/* Activity */} {/* Activity */}
{discordData.activities.length > 0 && ( {discordUser.spotify && (
<div className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-950/65 rounded-lg"> <div className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-950/65 rounded-lg">
{discordData.activities[0].name !== <SpotifyActivityContent
"Spotify" ? ( spotify={discordUser.spotify}
<GameActivity />
activity={discordData.activities[0]}
/>
) : (
discordData.spotify && (
<SpotifyActivity
spotify={discordData.spotify}
/>
)
)}
</div> </div>
)} )}
</div> </div>
@ -118,35 +74,29 @@ const DiscordStatus = (): ReactElement | undefined => {
); );
}; };
const BannerAvatar = ({ const BannerAvatar = ({ user }: { user: DiscordUser }): ReactElement => (
discordData,
discordUser,
dcdnUser,
}: {
discordData: Data;
discordUser: DiscordUser;
dcdnUser: DCDNUser;
}): ReactElement => (
<div className="relative pointer-events-none"> <div className="relative pointer-events-none">
<Image {user.banner && (
className="border-2 border-zinc-300 dark:border-zinc-900 rounded-t-xl" <Image
src={`https://cdn.discordapp.com/banners/${discordUser.id}/${dcdnUser.banner}.webp?size=1024`} className="border-2 border-zinc-300 dark:border-zinc-900 rounded-t-xl"
alt={`${discordUser.username}'s Banner`} src={user.banner.url}
width={300} alt={`${user.username}'s Banner`}
height={300} width={300}
/> height={300}
/>
)}
<div className="relative"> <div className="relative">
<Image <Image
className="absolute left-2 -bottom-12 border-[5px] border-zinc-300 dark:border-zinc-900 rounded-full" className="absolute left-2 -bottom-12 border-[5px] border-zinc-300 dark:border-zinc-900 rounded-full"
src={`https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.webp?size=1024`} src={user.avatar.url}
alt={`${discordUser.username}'s Avatar`} alt={`${user.username}'s Avatar`}
width={96} width={96}
height={96} height={96}
/> />
<div <div
className={cn( className={cn(
"absolute left-[4.5rem] -bottom-12 w-7 h-7 border-[5px] border-zinc-300 dark:border-zinc-900 rounded-full", "absolute left-[4.5rem] -bottom-12 w-7 h-7 border-[5px] border-zinc-300 dark:border-zinc-900 rounded-full",
statusColors[discordData.discord_status] statusColors[user.onlineStatus]
)} )}
/> />
</div> </div>
@ -161,10 +111,14 @@ const Bio = ({ bio }: { bio: string }): ReactElement => (
</SimpleTooltip> </SimpleTooltip>
); );
const Badges = ({ dcdnUser }: { dcdnUser: DCDNUser }): ReactElement => ( const Badges = ({
discordUser,
}: {
discordUser: DiscordUser;
}): ReactElement => (
<div className="ml-auto flex gap-1"> <div className="ml-auto flex gap-1">
{Object.entries(userBadges) {Object.entries(userBadges)
.filter(([_, badge]) => badge.predicate(dcdnUser)) .filter(([_, badge]) => badge.predicate(discordUser))
.map(([badgeIcon, badge], index) => ( .map(([badgeIcon, badge], index) => (
<SimpleTooltip key={index} content={badge.name}> <SimpleTooltip key={index} content={badge.name}>
<Image <Image
@ -186,17 +140,20 @@ const Username = ({
}): ReactElement => ( }): ReactElement => (
<div className="flex flex-col"> <div className="flex flex-col">
<h1 className="text-lg font-bold leading-none"> <h1 className="text-lg font-bold leading-none">
{discordUser.global_name} {discordUser.displayName}
</h1> </h1>
<h2 className="font-light opacity-70">{discordUser.username}</h2> <h2 className="font-light opacity-70">{discordUser.username}</h2>
</div> </div>
); );
const SpotifyActivity = ({ spotify }: { spotify: Spotify }): ReactElement => { const SpotifyActivityContent = ({
const trackUrl: string = `https://open.spotify.com/track/${spotify.track_id}`; spotify,
}: {
spotify: SpotifyActivity;
}): ReactElement => {
const trackArtist: string = spotify.artist.replace(";", ","); const trackArtist: string = spotify.artist.replace(";", ",");
const startTimestamp: number = spotify.timestamps.start; const startTimestamp: number = spotify.started;
const endTimestamp: number = spotify.timestamps.end; const endTimestamp: number = spotify.ends;
const [trackProgress, setTrackProgress] = useState<string | undefined>(); const [trackProgress, setTrackProgress] = useState<string | undefined>();
const trackDuration: number = endTimestamp - startTimestamp; const trackDuration: number = endTimestamp - startTimestamp;
@ -216,10 +173,10 @@ const SpotifyActivity = ({ spotify }: { spotify: Spotify }): ReactElement => {
<div className="flex items-start"> <div className="flex items-start">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{/* Artwork */} {/* Artwork */}
<Link href={trackUrl} target="_blank"> <Link href={spotify.trackUrl} target="_blank">
<Image <Image
className="rounded-lg" className="rounded-lg"
src={spotify.album_art_url as string} src={spotify.albumArtUrl}
alt={`Track artwork of ${spotify.song} by ${spotify.artist}`} alt={`Track artwork of ${spotify.song} by ${spotify.artist}`}
width={54} width={54}
height={54} height={54}
@ -228,7 +185,7 @@ const SpotifyActivity = ({ spotify }: { spotify: Spotify }): ReactElement => {
{/* Track Info */} {/* Track Info */}
<div className="flex flex-col text-sm"> <div className="flex flex-col text-sm">
<Link href={trackUrl} target="_blank"> <Link href={spotify.trackUrl} target="_blank">
<SimpleTooltip content={spotify.song}> <SimpleTooltip content={spotify.song}>
<h1 className="font-bold leading-none"> <h1 className="font-bold leading-none">
{truncateText(spotify.song, 22)} {truncateText(spotify.song, 22)}
@ -258,53 +215,53 @@ const SpotifyActivity = ({ spotify }: { spotify: Spotify }): ReactElement => {
); );
}; };
const GameActivity = ({ activity }: { activity: Activity }): ReactElement => { // const GameActivity = ({ activity }: { activity: Activity }): ReactElement => {
const startTimestamp: number = activity.timestamps?.start || Date.now(); // const startTimestamp: number = activity.timestamps?.start || Date.now();
const [activityProgress, setActivityProgress] = useState< // const [activityProgress, setActivityProgress] = useState<
string | undefined // string | undefined
>(); // >();
//
// Update the activity progress every second // // Update the activity progress every second
useEffect(() => { // useEffect(() => {
const interval = setInterval(() => { // const interval = setInterval(() => {
setActivityProgress( // setActivityProgress(
moment(Date.now() - startTimestamp).format("h:m:ss") // moment(Date.now() - startTimestamp).format("h:m:ss")
); // );
}, 1000); // }, 1000);
return () => clearInterval(interval); // return () => clearInterval(interval);
}, [startTimestamp]); // }, [startTimestamp]);
//
return ( // return (
<div className="relative flex items-start"> // <div className="relative flex items-start">
<div className="flex gap-2 items-center"> // <div className="flex gap-2 items-center">
{/* Artwork */} // {/* Artwork */}
<Image // <Image
className="rounded-lg pointer-events-none" // className="rounded-lg pointer-events-none"
src={`https://cdn.discordapp.com/app-assets/${activity.application_id}/${activity.assets?.large_image || activity.assets?.small_image}.png?size=1024`} // src={`https://cdn.discordapp.com/app-assets/${activity.application_id}/${activity.assets?.large_image || activity.assets?.small_image}.png?size=1024`}
alt={`Game artwork of ${activity.name}`} // alt={`Game artwork of ${activity.name}`}
width={54} // width={54}
height={54} // height={54}
/> // />
//
{/* Activity Info */} // {/* Activity Info */}
<div className="flex flex-col text-sm"> // <div className="flex flex-col text-sm">
<h1 className="font-bold leading-none">{activity.name}</h1> // <h1 className="font-bold leading-none">{activity.name}</h1>
<h2 className="font-light opacity-70"> // <h2 className="font-light opacity-70">
{activity.details} // {activity.details}
</h2> // </h2>
<p className="text-xs font-light opacity-70"> // <p className="text-xs font-light opacity-70">
{activity.state} // {activity.state}
</p> // </p>
</div> // </div>
</div> // </div>
//
{/* Activity Progress & Logo */} // {/* Activity Progress & Logo */}
<div className="absolute top-0 right-0 flex gap-1 text-green-500/80"> // <div className="absolute top-0 right-0 flex gap-1 text-green-500/80">
<p className="text-xs font-light">{activityProgress}</p> // <p className="text-xs font-light">{activityProgress}</p>
<PuzzlePieceIcon width={18} height={18} /> // <PuzzlePieceIcon width={18} height={18} />
</div> // </div>
</div> // </div>
); // );
}; // };
export default DiscordStatus; export default DiscordStatus;

View File

@ -1,6 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -18,9 +22,18 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }