chore(landing): general cleanup + ui touchup

This commit is contained in:
cnrad
2024-11-11 22:16:26 -05:00
parent 8ba5d6544c
commit 4a9c696be4
17 changed files with 635 additions and 1344 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel

View File

@@ -1,4 +1,10 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"tabWidth": 4
"quoteProps": "consistent",
"printWidth": 120,
"bracketSpacing": true,
"singleQuote": false,
"useTabs": false,
"arrowParens": "avoid",
"tabWidth": 2
}

View File

@@ -1,4 +0,0 @@
{
"editor.tabSize": 4,
"prettier.tabWidth": 4
}

View File

@@ -5,10 +5,7 @@ import { NextRequest } from "next/server";
export const dynamic = "force-dynamic";
export async function GET(
req: NextRequest,
options: { params: Promise<{ id: string[] }> },
) {
export async function GET(req: NextRequest, options: { params: Promise<{ id: string[] }> }) {
const userId = (await options.params).id.join("/");
if (!userId)
@@ -41,7 +38,7 @@ export async function GET(
getUser.data = await fetch(`https://api.lanyard.rest/v1/users/${userId}`, {
cache: "no-store",
}).then(async (res) => {
}).then(async res => {
const data = await res.json();
if (!data.success) {
@@ -63,9 +60,7 @@ export async function GET(
);
}
const params: Parameters = Object.fromEntries(
req.nextUrl.searchParams.entries(),
);
const params: Parameters = Object.fromEntries(req.nextUrl.searchParams.entries());
try {
let user = await redis.hget("users", userId);
@@ -77,8 +72,7 @@ export async function GET(
return new Response(await renderCard(getUser.data, params), {
headers: {
"Content-Type": "image/svg+xml; charset=utf-8",
"content-security-policy":
"default-src 'none'; img-src * data:; style-src 'unsafe-inline'",
"content-security-policy": "default-src 'none'; img-src * data:; style-src 'unsafe-inline'",
},
status: 200,
});

View File

@@ -13,103 +13,6 @@ body {
font-family: "Poppins", sans-serif;
}
.input {
text-align: left;
border-radius: 8px;
border: none;
width: 100%;
font-size: 0.9rem;
padding: 0.45rem 0.75rem;
color: #aaabaf;
border: solid 1px rgba(255, 255, 255, 0.2);
background: #000;
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
transition: all ease-in-out 0.1s;
&:focus {
outline: 0;
border-color: rgba(255, 255, 255, 0.5);
}
}
.output {
color: #aaabaf;
word-break: break-word;
border-radius: 8px;
border: solid 1px #333;
padding: 8px;
background: #000;
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
font-family: Monospace, sans-serif;
}
.action {
font-size: 0.9rem;
padding: 5px 25px;
border-radius: 6px;
cursor: pointer;
color: #888;
border: solid 1px #333;
background: transparent;
transition: all ease-in-out 0.1s;
&:hover {
color: #e6e6e6;
border-color: #e6e6e6;
}
&:active {
color: #fff;
border-color: #fff;
}
}
.active {
color: #fff;
border-color: #fff;
}
.stat {
position: fixed;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
line-height: 1rem;
bottom: 1rem;
left: 50%;
transform: translate(-50%, 0);
background: #000;
padding: 1rem 1.25rem;
color: #fff;
border-radius: 0.55rem;
text-align: center;
box-shadow: 0 2px 15px -10px #a21caf;
min-width: 400px;
@media (max-width: 400px) {
font-size: 14px;
min-width: 365px;
padding: 0.75rem 1rem;
}
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 0.55rem;
border: 2px solid transparent;
background: linear-gradient(45deg, #be123c, #6b21a8, #3730a3) border-box;
-webkit-mask:
linear-gradient(#fff 0 0) padding-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
@layer base {
:root {
--radius: 0.5rem;

View File

@@ -11,12 +11,10 @@ const poppins = Poppins({
export const metadata: Metadata = {
title: "Lanyard for GitHub Profile",
description:
"Utilize Lanyard to display your Discord Presence in your GitHub Profile",
description: "Utilize Lanyard to display your Discord Presence in your GitHub Profile",
openGraph: {
title: "Lanyard for GitHub Profile",
description:
"Utilize Lanyard to display your Discord Presence in your GitHub Profile",
description: "Utilize Lanyard to display your Discord Presence in your GitHub Profile",
},
};
@@ -26,13 +24,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${poppins.className} antialiased`}
suppressHydrationWarning
>
{children}
</body>
<html lang="en">
<body className={`${poppins.className} antialiased`}>{children}</body>
</html>
);
}

View File

@@ -1,523 +1,352 @@
"use client";
import React, { useState, useRef, useMemo, JSX } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import React, { useState, useMemo, JSX, useRef, useEffect } from "react";
import { motion } from "framer-motion";
import { useSmoothCount } from "use-smooth-count";
import useSWR from "swr";
import { getUserCount, isUserMonitored } from "@/utils/actions";
import { getUserCount } from "@/utils/actions";
import { isSnowflake } from "@/utils/snowflake";
import Link from "next/link";
import { parameterInfo } from "@/utils/parameter";
import { PARAMETERS } from "@/utils/parameters";
import * as Icon from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { filterLetters } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn, filterLetters } from "@/lib/utils";
export default function Home() {
const originUrl = useMemo(
() =>
typeof window !== "undefined"
? window.location.origin
: "https://lanyard.cnrad.dev",
[],
);
const [userId, setUserId] = useState<null | string>(null);
const [userError, setUserError] = useState<string | JSX.Element>();
const [userData, setUserData] = useState<{ userId: string } | null>(null);
const [copyState, setCopyState] = useState("Copy");
const [outputType, setOutputType] = useState<"markdown" | "html" | "url">(
"markdown",
);
const [isLoading, setIsLoading] = useState(true);
const [onImageLoaded, setOnImageLoaded] = useState(false);
const originUrl = process.env.NODE_ENV === "development" ? "http://localhost:3001" : "https://lanyard.cnrad.dev";
const [option, setOption] = useState<
Array<{ name: string; value: string }>
>([]);
const [userId, setUserId] = useState("");
const [userError, setUserError] = useState<string | JSX.Element>();
const [copyState, setCopyState] = useState("Copy");
const [outputType, setOutputType] = useState<"markdown" | "html" | "url">("markdown");
const [isLoaded, setIsLoaded] = useState(false);
const [options, setOptions] = useState<Record<string, string | boolean>>({});
const userCount = useSWR("getUserCount", getUserCount);
const countRef = useRef<HTMLDivElement | null>(null);
useSmoothCount({
ref: countRef,
target: userCount.data || 0,
duration: 3,
curve: [0, 1, 0, 1],
});
const url = `${originUrl}/api/${userData?.userId}${option.length > 0 ? `?${option.map((o) => `${o.name}=${o.value}`).join("&")}` : ""}`;
function outputText() {
if (outputType === "html") {
return `<a href="https://discord.com/users/${userData?.userId}"><img src="${url}" /></a>`;
} else if (outputType === "url") {
return `${url}`;
} else {
return `[![Discord Presence](${url})](https://discord.com/users/${userData?.userId})`;
}
}
function copy() {
navigator.clipboard.writeText(outputText());
setCopyState("Copied!");
setTimeout(() => setCopyState("Copy"), 1500);
}
async function submitDiscordId() {
setIsLoading(true);
setOnImageLoaded(false);
setUserData(null);
async function onLoadDiscordId(userId: string) {
setUserId(userId);
setIsLoaded(false);
setUserError(undefined);
if (!userId) return setUserError("Please enter a Discord ID");
if (!isSnowflake(userId)) return setUserError("Invalid Discord ID");
if ((await isUserMonitored(userId)) === false)
return setUserError(
<>
User is not being monitored by Lanyard, please join{" "}
<Link
href="https://discord.gg/lanyard"
target="_blank"
className="underline"
>
this server
</Link>{" "}
and try again.
</>,
);
setUserData({ userId });
setIsLoading(false);
if (userId.length < 1) return;
if (userId.length > 0 && !isSnowflake(userId)) return setUserError("Invalid Discord ID");
}
function modifyOption(
data:
| {
type: "string";
name: string;
data: string;
event: React.ChangeEvent<HTMLInputElement>;
}
| {
type: "list";
name: string;
data: string;
}
| {
type: "boolean";
name: string;
data: string | boolean;
},
) {
if (data.type === "string") {
const filteredValue = encodeURIComponent(
filterLetters(
data.data,
(
parameterInfo.find(
(p) =>
p.type === "string" &&
p.parameter === data.name,
) as any
).options.omit,
),
);
const url = `${originUrl}/api/${userId}${
Object.keys(options).length > 0
? `?${Object.keys(options)
.map(option => `${option}=${options[option]}`)
.join("&")}`
: ""
}`;
setOption((prev) => {
if (data.data === "") {
return prev?.filter((o) => o.name !== data.name) || [];
} else {
if (prev?.find((o) => o.name === data.name)) {
return prev.map((o) => {
if (o.name === data.name) {
o.value = filteredValue;
}
return o;
});
} else {
return prev
? [
...prev,
{
name: data.name,
value: filteredValue,
},
]
: [
{
name: data.name,
value: filteredValue,
},
];
}
}
});
} else if (data.type === "list") {
setOption((prev) => {
if (prev?.find((o) => o.name === data.name)) {
return prev.map((o) => {
if (o.name === data.name) {
o.value = data.data;
}
return o;
});
} else {
return prev
? [...prev, { name: data.name, value: data.data }]
: [{ name: data.name, value: data.data }];
}
});
} else if (data.type === "boolean") {
setOption((prev) => {
if (prev?.find((o) => o.name === data.name)) {
return prev
.map((opt) => {
if (opt.name === data.name) {
const options = parameterInfo.find(
(p) => p.parameter === data.name,
)?.options as { defaultBool?: boolean };
const copyContent = {
markdown: `[![Discord Presence](${url})](https://discord.com/users/${userId})`,
html: `<a href="https://discord.com/users/${userId}"><img src="${url}" /></a>`,
url: `${url}`,
};
const [isOptionsOpen, setIsOptionsOpen] = useState(false);
const optionsTriggerRef = useRef<HTMLButtonElement | null>(null);
const optionsContentRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
function handleOptionsClickOutside(event: MouseEvent) {
if (
data.data ===
(options?.defaultBool! || false)
isOptionsOpen &&
optionsContentRef.current &&
!optionsContentRef.current.contains(event.target as Node) &&
!optionsTriggerRef.current?.contains(event.target as Node)
) {
return null;
} else {
if (opt.name === data.name) {
opt.value = data.data.toString();
}
return opt;
}
} else {
return opt;
}
})
.filter((opt) => opt !== null);
} else {
return prev
? [
...prev,
{ name: data.name, value: data.data.toString() },
]
: [{ name: data.name, value: data.data.toString() }];
}
});
setIsOptionsOpen(false);
}
}
document.addEventListener("mousedown", handleOptionsClickOutside);
return () => {
document.removeEventListener("mousedown", handleOptionsClickOutside);
};
}, [isOptionsOpen]);
return (
<>
<main className="flex min-h-screen max-w-[100vw] flex-col items-center">
<div className="mt-16 w-[80%] max-w-[28rem] rounded-md">
<p className="my-2 text-left text-3xl font-semibold text-[#cecece]">
lanyard profile readme 🏷
</p>
<p className="text-base text-[#aaabaf]">
Utilize Lanyard to display your Discord Presence in your
GitHub Profile
</p>
<br />
<form
className="flex w-full gap-2"
onSubmit={(e) => {
e.preventDefault();
<div className="relative mt-16 flex w-[80%] max-w-[28rem] flex-col gap-2 rounded-md">
<p className="text-left text-3xl font-semibold text-[#cecece]">🏷 lanyard-profile-readme </p>
<p className="mb-2 text-sm text-[#aaabaf]">Uses Lanyard to display your Discord Presence anywhere.</p>
submitDiscordId();
}}
>
<div className="flex h-[2.25rem] w-full flex-row gap-2">
<input
className="input"
onChange={(e) => setUserId(e.target.value)}
className="w-full rounded-lg border border-white/10 bg-transparent px-2.5 py-1.5 font-mono text-sm text-gray-200 transition-colors duration-150 ease-out focus:border-white/50 focus:outline-none"
onChange={e => onLoadDiscordId(e.target.value)}
value={userId || ""}
placeholder="Enter your Discord ID"
/>
<button className="action" type="submit">
{">>"}
</button>
</form>
<motion.p
variants={{
open: { opacity: 1 },
closed: { opacity: 0 },
}}
initial="closed"
animate={userError ? "open" : "closed"}
className="mt-1 text-sm text-red-500"
>
* {userError}
</motion.p>
<motion.div
variants={{
open: {
opacity: 1,
},
closed: {
opacity: 0,
},
}}
initial="closed"
animate={!isLoading ? "open" : "closed"}
transition={{ duration: 0.5 }}
>
<div className="mb-1 mt-4 flex gap-1">
<button
className={`action ${outputType === "markdown" ? "active" : ""}`}
onClick={() => setOutputType("markdown")}
>
Markdown
</button>
<button
className={`action ${outputType === "html" ? "active" : ""}`}
onClick={() => setOutputType("html")}
>
HTML
</button>
<button
className={`action ${outputType === "url" ? "active" : ""}`}
onClick={() => setOutputType("url")}
>
URL
</button>
</div>
<div
className="output bg-black"
suppressHydrationWarning
>
{outputText()}
</div>
<div className="mt-4 flex gap-2">
<button className="action" onClick={copy}>
{copyState}
</button>
<Dialog>
<DialogTrigger className="action">
Option
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Option</DialogTitle>
<DialogDescription>
Select an option to enable/disable
features to your Lanyard Profile
</DialogDescription>
</DialogHeader>
<div className="flex max-h-[75dvh] flex-col gap-4 overflow-x-hidden overflow-y-scroll rounded-xl bg-black/50 p-4 px-6 text-[#cecece]">
{parameterInfo.map((item, idx) => {
return (
<div
key={item.parameter}
className="flex flex-col gap-1"
<button
ref={optionsTriggerRef}
onClick={() => setIsOptionsOpen(p => !p)}
className="group flex min-h-[2.25rem] min-w-[2.25rem] items-center justify-center rounded-lg border border-white/10 bg-stone-900/50 transition-colors duration-150 ease-out hover:border-white/40"
>
<div className="flex items-center gap-1">
<p className="text-sm sm:text-base">
{item.title}
</p>
<Icon.Settings size={18} className="text-white/40 group-hover:text-white/60" />
</button>
</div>
<motion.div
initial={{
scale: 0.98,
opacity: isOptionsOpen ? 1 : 0,
display: isOptionsOpen ? "block" : "none",
}}
animate={{
scale: isOptionsOpen ? 1 : 0.98,
opacity: isOptionsOpen ? 1 : 0,
display: isOptionsOpen ? "block" : "none",
}}
ref={optionsContentRef}
transition={{ duration: 0.2, ease: [0, 0.6, 0.4, 1] }}
className={cn(
"absolute top-32 z-[2] flex h-auto flex-col overflow-hidden rounded-lg border border-white/5 bg-black/75 p-4 text-white shadow-[0_6px_50px_-25px_rgba(180,177,255,0.2)] backdrop-blur-xl max-sm:h-[30rem] max-sm:w-full max-sm:overflow-y-scroll sm:-left-[1rem] sm:w-[30rem] sm:max-w-[30rem]",
)}
>
<div className="grid-rows-auto mb-4 flex w-full flex-col gap-2.5 sm:grid sm:grid-cols-2">
{PARAMETERS.filter(item => item.type !== "boolean").map(item => {
return (
<div key={item.parameter} className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<p className="text-sm text-gray-300">{item.title}</p>
<Popover>
<PopoverTrigger>
<Icon.InfoIcon
size={24}
className="rounded-md p-1 text-gray-400 transition hover:bg-stone-950"
size={16}
className="rounded-md text-zinc-700 transition hover:text-gray-400"
/>
</PopoverTrigger>
<PopoverContent
side="top"
className="text-sm"
>
{
item.description
}
<PopoverContent side="top" className="text-sm">
{item.description}
</PopoverContent>
</Popover>
</div>
{item.type === "string" && (
<Input
placeholder={
item.options
?.placeholder ||
"..."
}
onChange={(e) =>
modifyOption({
type: "string",
name: item.parameter,
data: e
.target
.value,
event: e,
})
}
value={decodeURIComponent(
option?.find(
(o) =>
o.name ===
item.parameter,
)?.value || "",
)}
className="text-sm sm:text-base"
/>
)}
{item.type ===
"boolean" && (
<Checkbox
onCheckedChange={(
bool,
) =>
modifyOption({
type: "boolean",
name: item.parameter,
data: bool,
})
}
checked={
option?.find(
(o) =>
o.name ===
item.parameter,
)?.value ===
"true"
? true
: option?.find(
(
o,
) =>
o.name ===
item.parameter,
)
?.value ===
"false"
? false
: item
.options
?.defaultBool ||
false
}
/>
)}
{item.type === "list" && (
<Select
onValueChange={(
val,
) =>
modifyOption({
type: "list",
name: item.parameter,
data: val,
})
}
value={
option?.find(
(o) =>
o.name ===
item.parameter,
)?.value || ""
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent className="text-sm sm:text-base">
{item.options.list.map(
(
option,
) => (
<SelectItem
value={
option.value
}
key={
option.value
}
>
{
option.name
}
</SelectItem>
<input
className="relative h-8 w-full appearance-none rounded-md border border-white/10 bg-transparent px-2 py-0.5 text-sm outline-none transition-all duration-150 ease-out placeholder:text-white/30 focus:border-white/50 disabled:cursor-not-allowed disabled:opacity-50"
placeholder={item.options?.placeholder || "..."}
onChange={e => {
const filteredValue = encodeURIComponent(
filterLetters(
e.target.value,
(PARAMETERS.find(p => p.parameter === item.parameter) as any).options.omit,
),
);
setOptions(prev => ({
...prev,
[item.parameter]: filteredValue,
}));
}}
value={decodeURIComponent((options[item.parameter] as string) || "")}
/>
)}
</SelectContent>
</Select>
{item.type === "list" && (
<div className="relative">
<select
value={(options[item.parameter] as string) || ""}
onChange={e =>
setOptions(prev => ({
...prev,
[item.parameter]: e.target.value,
}))
}
className={cn(
"relative h-8 w-full appearance-none rounded-md border border-white/10 bg-transparent px-2 py-0.5 text-sm outline-none transition-all duration-150 ease-out placeholder:text-white/30 focus:border-white/50 disabled:cursor-not-allowed disabled:opacity-50",
{
"text-white/30": !options[item.parameter] || options[item.parameter] === "",
},
)}
>
<option value="">None</option>
{item.options.list.map(option => (
<option value={option.value} key={option.value}>
{option.name}
</option>
))}
</select>
<Icon.ChevronDown
size={14}
className="absolute right-2 top-0 my-auto flex h-full text-white/50"
/>
</div>
)}
</div>
);
})}
</div>
<DialogFooter>
<Button onClick={() => setOption([])}>
Reset
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Separated for easier styling/readability */}
<div className="sm:grid-rows-auto flex flex-col gap-2 sm:grid sm:grid-cols-2">
{PARAMETERS.filter(item => item.type === "boolean").map(item => {
return (
<div key={item.parameter} className="flex flex-row items-start gap-2.5 text-sm">
<input
type="checkbox"
className={cn(
"mt-0.5 max-h-4 min-h-4 min-w-4 max-w-4 cursor-pointer appearance-none before:overflow-clip before:rounded-[0.25rem] after:absolute after:h-4 after:w-4 after:rounded-[0.25rem] after:border after:border-white/10 after:transition-all after:duration-150 after:ease-out",
{
"after:border-gray-200/50 after:bg-gray-500/40": options[item.parameter] === "true",
"after:bg-zinc-700/10 after:hover:bg-zinc-700/25": options[item.parameter] !== "true",
},
)}
checked={options[item.parameter] === "true"}
onChange={e =>
setOptions(prev => ({
...prev,
[item.parameter]: e.target.checked.toString(),
}))
}
/>
<p
className="text-gray-300"
style={{
textDecoration: PARAMETERS.find(p => p.parameter === item.parameter)?.deprecated
? "line-through"
: "none",
}}
>
{item.title}
</p>
<Popover>
<PopoverTrigger>
<Icon.InfoIcon
size={16}
className="mt-0.5 rounded-md text-zinc-700 transition hover:text-gray-400"
/>
</PopoverTrigger>
<PopoverContent side="top" className="text-sm">
{item.description}
</PopoverContent>
</Popover>
</div>
<div className="mt-2">
<motion.img
className={`${onImageLoaded ? "" : "animate-pulse rounded-md bg-[#3d3d43]"}`}
initial={{
);
})}
</div>
</motion.div>
{!isLoaded ? (
<motion.p
variants={{
open: { opacity: 1, display: "block" },
closed: { opacity: 0, display: "none" },
}}
initial="closed"
animate={userError ? "open" : "closed"}
className="mt-1 text-sm text-red-500"
transition={{ duration: 0.15 }}
>
{userError}
</motion.p>
) : null}
<motion.div
variants={{
loaded: {
opacity: 1,
},
waiting: {
opacity: 0,
},
}}
animate={{
opacity: onImageLoaded ? 1 : 0,
}}
transition={{ duration: 0.5 }}
initial="waiting"
animate={isLoaded ? "loaded" : "waiting"}
transition={{ duration: 0.15 }}
className="mt-2 flex flex-col gap-2"
>
<img
src={url}
height={280}
width={500}
alt="Your Lanyard Banner"
onLoad={() => setOnImageLoaded(true)}
suppressHydrationWarning
onLoad={() => setIsLoaded(true)}
onError={() =>
userId.length > 0 && isSnowflake(userId)
? setUserError(
<>
User is not monitored by Lanyard, please join{" "}
<Link href="https://discord.gg/lanyard" target="_blank" className="inline underline">
the server
</Link>{" "}
and try again.
</>,
)
: null
}
/>
<div className="mt-4 grid grid-cols-3 gap-1">
{(["markdown", "html", "url"] as const).map(type => (
<button
key={type}
className={cn(
"rounded-md border border-white/10 px-1.5 py-1 font-mono text-sm font-medium uppercase tracking-wide text-white/50 transition-colors duration-100 ease-out",
{
"border-white/20 bg-white/10 font-semibold text-white/75": outputType === type,
"hover:border-white/15 hover:bg-white/5": outputType !== type,
},
)}
onClick={() => setOutputType(type)}
>
{type}
</button>
))}
</div>
<div className="break-all rounded-lg border border-white/10 bg-zinc-950 px-3 py-2 font-mono text-sm text-blue-400">
{copyContent[outputType]}
</div>
<div className="flex gap-2">
<button
className="rounded-md border border-white/10 px-3 py-1 font-mono text-sm font-medium text-white/50 transition-colors duration-75 ease-out hover:border-white/20 hover:text-white/75"
onClick={() => {
navigator.clipboard.writeText(copyContent[outputType]);
setCopyState("Copied!");
setTimeout(() => setCopyState("Copy"), 1500);
}}
>
{copyState}
</button>
</div>
</motion.div>
</div>
</main>
<motion.footer
variants={{
open: {
opacity: 1,
},
closed: {
{userCount.data && (
<motion.div
initial={{
scale: 0.99,
opacity: 0,
},
transform: "translateY(10px) translateX(-50%)",
}}
animate={isLoading ? "open" : "closed"}
transition={{ duration: 0.5 }}
className="stat"
animate={{
scale: 1,
opacity: 1,
transform: "translateY(0) translateX(-50%)",
}}
transition={{ duration: 1.25, ease: [0, 0.4, 0.2, 1] }}
className={cn(
"fixed bottom-0 left-1/2 mb-8 flex h-min w-min min-w-[10rem] flex-row items-center justify-center whitespace-nowrap rounded-full border border-white/5 bg-[#2A2A2A]/15 px-4 py-2.5 text-center text-sm leading-[1rem] text-white/50 shadow-[0_4px_45px_-20px_#b390ff] max-sm:hidden",
)}
>
Lanyard Profile Readme has{" "}
<div
style={{ fontWeight: "bold", width: "3.2rem" }}
ref={countRef}
/>{" "}
total users!
</motion.footer>
Currently at&nbsp;
<span className="bg-gradient-to-tr from-red-500 to-purple-700 bg-clip-text font-semibold text-transparent drop-shadow-[0_0_8px_#a931ff]">
{userCount.data?.toLocaleString()}
</span>
&nbsp;total users!
</motion.div>
)}
</>
);
}

View File

@@ -1,57 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:ring-stone-300",
{
variants: {
variant: {
default:
"shadow bg-stone-50 text-stone-900 hover:bg-stone-50/90",
destructive:
"shadow-sm bg-red-900 text-stone-50 hover:bg-red-900/90",
outline:
"border shadow-sm text-stone-900 border-stone-800 bg-stone-950 hover:bg-stone-800 hover:text-stone-50",
secondary:
"shadow-sm bg-stone-800 text-stone-50 hover:bg-stone-800/80",
ghost: "hover:bg-stone-800 hover:text-stone-50",
link: "underline-offset-4 hover:underline text-stone-50",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -1,30 +0,0 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-stone-50 border-stone-800 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-stone-300 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-stone-50 data-[state=checked]:text-stone-900",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@@ -1,125 +0,0 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-stone-800 bg-stone-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
tabIndex={1}
className="absolute right-4 top-4 rounded-sm text-stone-300 opacity-70 ring-offset-stone-950 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-stone-300 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-stone-800 data-[state=open]:text-stone-400"
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight text-stone-300",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-stone-400", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -1,25 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-stone-800 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-stone-50 placeholder:text-stone-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-stone-300 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@@ -6,11 +6,8 @@ import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
@@ -21,7 +18,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-stone-800 bg-stone-950 p-4 text-stone-50 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 w-72 rounded-md border border-white/10 bg-black/50 p-4 text-stone-50 shadow-md outline-none backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}

View File

@@ -1,164 +0,0 @@
"use client";
import * as React from "react";
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-stone-800 bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-stone-950 placeholder:text-stone-400 focus:outline-none focus:ring-1 focus:ring-stone-300 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-stone-800 bg-stone-950 text-stone-50 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-stone-800 focus:text-stone-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-stone-800", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -7,11 +7,3 @@ export async function getUserCount() {
return count.length;
}
export async function isUserMonitored(userId: string) {
const user = await fetch(`https://api.lanyard.rest/v1/users/${userId}`, {
cache: "no-store",
}).then((res) => res.json());
return user.success === true;
}

View File

@@ -1,205 +0,0 @@
export type Parameters = {
theme?: string;
bg?: string;
clanbg?: string;
animated?: string;
animatedDecoration?: string;
hideDiscrim?: string;
hideStatus?: string;
hideTimestamp?: string;
hideBadges?: string;
hideProfile?: string;
hideActivity?: string;
hideSpotify?: string;
hideClan?: string;
hideDecoration?: string;
ignoreAppId?: string;
showDisplayName?: string;
borderRadius?: string;
idleMessage?: string;
};
export const parameterInfo: Array<
| {
parameter: string;
type: "boolean";
title: string;
description?: string;
options?: {
defaultBool?: boolean;
};
}
| {
parameter: string;
type: "string";
title: string;
description?: string;
options?: {
placeholder?: string;
omit?: string[];
};
}
| {
parameter: string;
type: "list";
title: string;
description?: string;
options: {
list: Array<{
name: string;
value: string;
}>;
};
}
> = [
{
parameter: "theme",
type: "list",
title: "Theme",
description:
'This will change the background and the font colors, but the background can be overridden with the "Background Color" parameter.',
options: {
list: [
{
name: "Light",
value: "light",
},
{
name: "Dark",
value: "dark",
},
],
},
},
{
parameter: "bg",
type: "string",
title: "Background Color",
description:
"This will change the background color. Must be in hex format. Omit the # symbol.",
options: {
placeholder: "1A1C1F",
omit: ["#"],
},
},
{
parameter: "borderRadius",
type: "string",
title: "Border Radius",
description:
"This will change the border radius of the card. Must have a size unit (px, rem, em, and more).",
options: {
placeholder: "10px",
},
},
{
parameter: "animated",
type: "boolean",
title: "Toggle Animated Avatar",
description:
"If you have an animated avatar, but don't want it animated, this is the right option.",
options: {
defaultBool: true,
},
},
{
parameter: "idleMessage",
type: "string",
title: "Idle Message",
description:
"If you don't want the default \"I'm not currently doing anything!\" as your idle message, this is the right option.",
options: {
placeholder: "I'm not currently doing anything!",
},
},
{
parameter: "showDisplayName",
type: "boolean",
title: "Show Display Name",
description:
"If you'd like to show your global display name as well as your username, this is the right option.",
},
{
parameter: "animatedDecoration",
type: "boolean",
title: "Toggle Animated Avatar Decoration",
description:
"If you have an Animated Avatar Decoration, but don't want it animated, this is the right option.",
options: {
defaultBool: true,
},
},
{
parameter: "hideDecoration",
type: "boolean",
title: "Hide Avatar Decoration",
description:
"If you don't want people seeing your Avatar Decoration, this is the right option.",
},
{
parameter: "hideStatus",
type: "boolean",
title: "Hide Status",
description:
"If you don't want people seeing your status, this is the right option.",
},
{
parameter: "hideTimestamp",
type: "boolean",
title: "Hide Elapsed Time",
description:
"If you don't want people seeing the elapsed time on an activity, this is the right option.",
},
{
parameter: "hideClan",
type: "boolean",
title: "Hide Clan Tag",
description:
"If you don't want people seeing your Guild Tag (formerly known as Clans), this is the right option.",
},
{
parameter: "hideBadges",
type: "boolean",
title: "Hide Badges",
description:
"If you don't want people seeing your Badges, this is the right option.",
},
{
parameter: "hideProfile",
type: "boolean",
title: "Hide Profile",
description:
"If you don't want people seeing your Profile, this is the right option.",
},
{
parameter: "hideActivity",
type: "boolean",
title: "Hide Activity",
description:
"If you don't want people seeing your activity, this is the right option.",
},
{
parameter: "hideSpotify",
type: "boolean",
title: "Hide Spotify",
description:
"If you don't want people seeing your Spotify activity, this is the right option.",
},
{
parameter: "ignoreAppId",
type: "string",
title: "Hide App by ID",
description:
"If you don't want to display a specific application, this is the right option. IDs separate by ','",
options: {
placeholder: "1302143410907648071, 1302132259368861759",
},
},
{
parameter: "hideDiscrim",
type: "boolean",
title: "Hide Discriminator (DEPRECATED)",
description:
"If you don't want people seeing your Discriminator, this is the right option. (DEPRECATED)",
},
];

191
src/utils/parameters.ts Normal file
View File

@@ -0,0 +1,191 @@
export type Parameters = {
theme?: string;
bg?: string;
clanbg?: string;
animated?: string;
animatedDecoration?: string;
hideDiscrim?: string;
hideStatus?: string;
hideTimestamp?: string;
hideBadges?: string;
hideProfile?: string;
hideActivity?: string;
hideSpotify?: string;
hideClan?: string;
hideDecoration?: string;
ignoreAppId?: string;
showDisplayName?: string;
borderRadius?: string;
idleMessage?: string;
};
export const PARAMETERS: Array<
{ deprecated?: boolean } & (
| {
parameter: string;
type: "boolean";
title: string;
description?: string;
options?: {
defaultBool?: boolean;
};
}
| {
parameter: string;
type: "string";
title: string;
description?: string;
options?: {
placeholder?: string;
omit?: string[];
};
}
| {
parameter: string;
type: "list";
title: string;
description?: string;
options: {
list: Array<{
name: string;
value: string;
}>;
};
}
)
> = [
{
parameter: "theme",
type: "list",
title: "Theme",
description: "Changes the background and text colors. Can be overridden with the `bg` parameter.",
options: {
list: [
{
name: "Light",
value: "light",
},
{
name: "Dark",
value: "dark",
},
],
},
},
{
parameter: "bg",
type: "string",
title: "Background Color",
description: "Changes the background color to a hex color (no octothorpe).",
options: {
placeholder: "1A1C1F",
omit: ["#"],
},
},
{
parameter: "borderRadius",
type: "string",
title: "Border Radius",
description: "Changes the border radius of the card. Follows the CSS <length> spec (px, rem, etc.).",
options: {
placeholder: "10px",
},
},
{
parameter: "animated",
type: "boolean",
title: "Disable Animated Avatar",
description: "Disables an animated avatar.",
options: {
defaultBool: true,
},
},
{
parameter: "idleMessage",
type: "string",
title: "Idle Message",
description: 'Changes the idle message. Defaults to "I\'m not currently doing anything!".',
options: {
placeholder: "I'm not currently doing anything!",
},
},
{
parameter: "showDisplayName",
type: "boolean",
title: "Show Display Name",
description: "Shows your global display name alongside your username.",
},
{
parameter: "animatedDecoration",
type: "boolean",
title: "Disable Animated Avatar Decoration",
description: "Disables animated avatar decorations.",
options: {
defaultBool: true,
},
},
{
parameter: "hideDecoration",
type: "boolean",
title: "Hide Avatar Decoration",
description: "Hides any avatar decorations.",
},
{
parameter: "hideStatus",
type: "boolean",
title: "Hide Status",
description: "Hides your custom Discord status.",
},
{
parameter: "hideTimestamp",
type: "boolean",
title: "Hide Activity Time",
description: "Hides the time spent on an activity.",
},
{
parameter: "hideClan",
type: "boolean",
title: "Hide Clan Tag",
description: "Hides your Guild Tag (formerly Clan Tag)",
},
{
parameter: "hideBadges",
type: "boolean",
title: "Hide Badges",
description: "Hides your profile badges.",
},
{
parameter: "hideProfile",
type: "boolean",
title: "Hide Profile",
description: "Hides your profile, keeps your activity.",
},
{
parameter: "hideActivity",
type: "boolean",
title: "Hide Activity",
description: "Hides your activity, keeps your profile.",
},
{
parameter: "hideSpotify",
type: "boolean",
title: "Hide Spotify",
description: "Hides your Spotify activity only.",
},
{
parameter: "ignoreAppId",
type: "string",
title: "Hide App by ID",
description: "Hide apps by their respective ID, as a comma-separated list.",
options: {
placeholder: "1302143410907648071, 1302132259368861759",
},
},
{
parameter: "hideDiscrim",
type: "boolean",
title: "Hide Discriminator",
description: "Hides your discriminator. (DEPRECATED, RIP)",
deprecated: true,
},
].sort((a, b) => b.type.localeCompare(a.type));

View File

@@ -10,15 +10,10 @@ const config: Config = {
theme: {
extend: {
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)'
background: "var(--background)",
foreground: "var(--foreground)",
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate")],
};