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:
push:
branches: ["master"]
branches: [ "master" ]
paths-ignore:
- README.md
- LICENSE
@ -12,8 +12,8 @@ jobs:
docker:
strategy:
matrix:
arch: ["ubuntu-latest"]
git-version: ["2.44.0"]
arch: [ "ubuntu-latest" ]
git-version: [ "2.44.0" ]
runs-on: ${{ matrix.arch }}
# Steps to run

View File

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

View File

@ -1,2 +1,3 @@
# RainnnyCLUB
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 = {
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.rainnny.club",
},
{
protocol: "https",
hostname: "cdn.discordapp.com",
},
{
protocol: "https",
hostname: "i.scdn.co",
},
{
protocol: "https",
hostname: "bonfire.wtf",
},
{
protocol: "https",
hostname: "img.icons8.com",
},
],
},
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.rainnny.club",
},
{
protocol: "https",
hostname: "cdn.discordapp.com",
},
{
protocol: "https",
hostname: "i.scdn.co",
},
{
protocol: "https",
hostname: "bonfire.wtf",
},
{
protocol: "https",
hostname: "img.icons8.com",
},
],
},
};
export default nextConfig;

View File

@ -1,42 +1,41 @@
{
"name": "rainnny.club",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.1.5",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@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",
"lucide-react": "^0.439.0",
"moment": "^2.30.1",
"next": "^14.2.7",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"sharp": "^0.33.5",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"use-lanyard": "^1.5.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.9"
}
"name": "rainnny.club",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.1.5",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.3.30",
"lucide-react": "^0.439.0",
"moment": "^2.30.1",
"next": "^14.2.7",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"sharp": "^0.33.5",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"usetether": "^1.2.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"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 = {
plugins: {
tailwindcss: {},
},
plugins: {
tailwindcss: {},
},
};
export default config;

View File

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

View File

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