mirror of
https://github.com/NohamR/lanyard-profile-readme.git
synced 2026-05-24 20:00:37 +00:00
chore(landing): general cleanup + ui touchup
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"tabWidth": 4
|
||||
"quoteProps": "consistent",
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true,
|
||||
"singleQuote": false,
|
||||
"useTabs": false,
|
||||
"arrowParens": "avoid",
|
||||
"tabWidth": 2
|
||||
}
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"editor.tabSize": 4,
|
||||
"prettier.tabWidth": 4
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
739
src/app/page.tsx
739
src/app/page.tsx
@@ -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 `[](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: `[](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
|
||||
<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>
|
||||
total users!
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
191
src/utils/parameters.ts
Normal 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));
|
||||
@@ -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")],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user