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
|
# local env files
|
||||||
.env*.local
|
.env*.local
|
||||||
|
.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
"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,81 +5,75 @@ import { NextRequest } from "next/server";
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(req: NextRequest, options: { params: Promise<{ id: string[] }> }) {
|
||||||
req: NextRequest,
|
const userId = (await options.params).id.join("/");
|
||||||
options: { params: Promise<{ id: string[] }> },
|
|
||||||
) {
|
|
||||||
const userId = (await options.params).id.join("/");
|
|
||||||
|
|
||||||
if (!userId)
|
if (!userId)
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
error: "No ID provided.",
|
error: "No ID provided.",
|
||||||
},
|
},
|
||||||
success: false,
|
success: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
},
|
},
|
||||||
);
|
|
||||||
|
|
||||||
if (!isSnowflake(userId))
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
error: "The ID you provide is not a valid snowflake.",
|
|
||||||
},
|
|
||||||
success: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 400,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let getUser: any = {};
|
|
||||||
|
|
||||||
getUser.data = await fetch(`https://api.lanyard.rest/v1/users/${userId}`, {
|
|
||||||
cache: "no-store",
|
|
||||||
}).then(async (res) => {
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (!data.success) {
|
|
||||||
getUser.error = data.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (getUser.error) {
|
|
||||||
return Response.json(
|
|
||||||
{
|
|
||||||
data: getUser.error,
|
|
||||||
success: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 400,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: Parameters = Object.fromEntries(
|
|
||||||
req.nextUrl.searchParams.entries(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
if (!isSnowflake(userId))
|
||||||
let user = await redis.hget("users", userId);
|
return Response.json(
|
||||||
if (!user) await redis.hset("users", userId, "true");
|
{
|
||||||
} catch {
|
data: {
|
||||||
null;
|
error: "The ID you provide is not a valid snowflake.",
|
||||||
|
},
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let getUser: any = {};
|
||||||
|
|
||||||
|
getUser.data = await fetch(`https://api.lanyard.rest/v1/users/${userId}`, {
|
||||||
|
cache: "no-store",
|
||||||
|
}).then(async res => {
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
getUser.error = data.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(await renderCard(getUser.data, params), {
|
return data;
|
||||||
headers: {
|
});
|
||||||
"Content-Type": "image/svg+xml; charset=utf-8",
|
|
||||||
"content-security-policy":
|
if (getUser.error) {
|
||||||
"default-src 'none'; img-src * data:; style-src 'unsafe-inline'",
|
return Response.json(
|
||||||
},
|
{
|
||||||
status: 200,
|
data: getUser.error,
|
||||||
});
|
success: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: Parameters = Object.fromEntries(req.nextUrl.searchParams.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
let user = await redis.hget("users", userId);
|
||||||
|
if (!user) await redis.hset("users", userId, "true");
|
||||||
|
} catch {
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'",
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,103 +13,6 @@ body {
|
|||||||
font-family: "Poppins", sans-serif;
|
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 {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|||||||
@@ -4,35 +4,28 @@ import "./globals.css";
|
|||||||
import { Poppins } from "next/font/google";
|
import { Poppins } from "next/font/google";
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
weight: ["400", "500", "600", "700"],
|
weight: ["400", "500", "600", "700"],
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
display: "swap",
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
title: "Lanyard for GitHub Profile",
|
||||||
|
description: "Utilize Lanyard to display your Discord Presence in your GitHub Profile",
|
||||||
|
openGraph: {
|
||||||
title: "Lanyard for GitHub Profile",
|
title: "Lanyard for GitHub Profile",
|
||||||
description:
|
description: "Utilize Lanyard to display your Discord Presence in your GitHub Profile",
|
||||||
"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",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en">
|
||||||
<body
|
<body className={`${poppins.className} antialiased`}>{children}</body>
|
||||||
className={`${poppins.className} antialiased`}
|
</html>
|
||||||
suppressHydrationWarning
|
);
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
835
src/app/page.tsx
835
src/app/page.tsx
@@ -1,523 +1,352 @@
|
|||||||
"use client";
|
"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 { motion } from "framer-motion";
|
||||||
|
|
||||||
import { useSmoothCount } from "use-smooth-count";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { getUserCount } from "@/utils/actions";
|
||||||
import { getUserCount, isUserMonitored } from "@/utils/actions";
|
|
||||||
import { isSnowflake } from "@/utils/snowflake";
|
import { isSnowflake } from "@/utils/snowflake";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { parameterInfo } from "@/utils/parameter";
|
import { PARAMETERS } from "@/utils/parameters";
|
||||||
|
|
||||||
import * as Icon from "lucide-react";
|
import * as Icon from "lucide-react";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import {
|
import { cn, filterLetters } from "@/lib/utils";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const originUrl = useMemo(
|
const originUrl = process.env.NODE_ENV === "development" ? "http://localhost:3001" : "https://lanyard.cnrad.dev";
|
||||||
() =>
|
|
||||||
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 [option, setOption] = useState<
|
const [userId, setUserId] = useState("");
|
||||||
Array<{ name: string; value: string }>
|
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 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("&")}` : ""}`;
|
async function onLoadDiscordId(userId: string) {
|
||||||
|
setUserId(userId);
|
||||||
|
setIsLoaded(false);
|
||||||
|
setUserError(undefined);
|
||||||
|
|
||||||
function outputText() {
|
if (userId.length < 1) return;
|
||||||
if (outputType === "html") {
|
if (userId.length > 0 && !isSnowflake(userId)) return setUserError("Invalid Discord ID");
|
||||||
return `<a href="https://discord.com/users/${userData?.userId}"><img src="${url}" /></a>`;
|
}
|
||||||
} else if (outputType === "url") {
|
|
||||||
return `${url}`;
|
const url = `${originUrl}/api/${userId}${
|
||||||
} else {
|
Object.keys(options).length > 0
|
||||||
return `[](https://discord.com/users/${userData?.userId})`;
|
? `?${Object.keys(options)
|
||||||
}
|
.map(option => `${option}=${options[option]}`)
|
||||||
|
.join("&")}`
|
||||||
|
: ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
isOptionsOpen &&
|
||||||
|
optionsContentRef.current &&
|
||||||
|
!optionsContentRef.current.contains(event.target as Node) &&
|
||||||
|
!optionsTriggerRef.current?.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOptionsOpen(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy() {
|
document.addEventListener("mousedown", handleOptionsClickOutside);
|
||||||
navigator.clipboard.writeText(outputText());
|
return () => {
|
||||||
setCopyState("Copied!");
|
document.removeEventListener("mousedown", handleOptionsClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOptionsOpen]);
|
||||||
|
|
||||||
setTimeout(() => setCopyState("Copy"), 1500);
|
return (
|
||||||
}
|
<>
|
||||||
|
<main className="flex min-h-screen max-w-[100vw] flex-col items-center">
|
||||||
|
<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>
|
||||||
|
|
||||||
async function submitDiscordId() {
|
<div className="flex h-[2.25rem] w-full flex-row gap-2">
|
||||||
setIsLoading(true);
|
<input
|
||||||
setOnImageLoaded(false);
|
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"
|
||||||
setUserData(null);
|
onChange={e => onLoadDiscordId(e.target.value)}
|
||||||
setUserError(undefined);
|
value={userId || ""}
|
||||||
|
placeholder="Enter your Discord ID"
|
||||||
|
/>
|
||||||
|
|
||||||
if (!userId) return setUserError("Please enter a Discord ID");
|
<button
|
||||||
|
ref={optionsTriggerRef}
|
||||||
if (!isSnowflake(userId)) return setUserError("Invalid Discord ID");
|
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"
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
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 };
|
|
||||||
|
|
||||||
if (
|
|
||||||
data.data ===
|
|
||||||
(options?.defaultBool! || false)
|
|
||||||
) {
|
|
||||||
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() }];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
submitDiscordId();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
onChange={(e) => setUserId(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"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<p className="text-sm sm:text-base">
|
|
||||||
{item.title}
|
|
||||||
</p>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<Icon.InfoIcon
|
|
||||||
size={24}
|
|
||||||
className="rounded-md p-1 text-gray-400 transition hover:bg-stone-950"
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<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>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={() => setOption([])}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<motion.img
|
|
||||||
className={`${onImageLoaded ? "" : "animate-pulse rounded-md bg-[#3d3d43]"}`}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: onImageLoaded ? 1 : 0,
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
src={url}
|
|
||||||
height={280}
|
|
||||||
width={500}
|
|
||||||
alt="Your Lanyard Banner"
|
|
||||||
onLoad={() => setOnImageLoaded(true)}
|
|
||||||
suppressHydrationWarning
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<motion.footer
|
|
||||||
variants={{
|
|
||||||
open: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
closed: {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
animate={isLoading ? "open" : "closed"}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="stat"
|
|
||||||
>
|
>
|
||||||
Lanyard Profile Readme has{" "}
|
<Icon.Settings size={18} className="text-white/40 group-hover:text-white/60" />
|
||||||
<div
|
</button>
|
||||||
style={{ fontWeight: "bold", width: "3.2rem" }}
|
</div>
|
||||||
ref={countRef}
|
|
||||||
/>{" "}
|
<motion.div
|
||||||
total users!
|
initial={{
|
||||||
</motion.footer>
|
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={16}
|
||||||
|
className="rounded-md text-zinc-700 transition hover:text-gray-400"
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="top" className="text-sm">
|
||||||
|
{item.description}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.type === "string" && (
|
||||||
|
<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) || "")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</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,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
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={() => 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>
|
||||||
|
|
||||||
|
{userCount.data && (
|
||||||
|
<motion.div
|
||||||
|
initial={{
|
||||||
|
scale: 0.99,
|
||||||
|
opacity: 0,
|
||||||
|
transform: "translateY(10px) translateX(-50%)",
|
||||||
|
}}
|
||||||
|
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",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
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,27 +6,24 @@ import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root;
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
const PopoverAnchor = PopoverPrimitive.Anchor;
|
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
));
|
));
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -2,16 +2,8 @@
|
|||||||
import redis from "@/utils/redis";
|
import redis from "@/utils/redis";
|
||||||
|
|
||||||
export async function getUserCount() {
|
export async function getUserCount() {
|
||||||
let users = await redis.hgetall("users");
|
let users = await redis.hgetall("users");
|
||||||
let count = Object.keys(users);
|
let count = Object.keys(users);
|
||||||
|
|
||||||
return count.length;
|
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));
|
||||||
@@ -8,17 +8,12 @@ const config: Config = {
|
|||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: 'var(--background)',
|
background: "var(--background)",
|
||||||
foreground: 'var(--foreground)'
|
foreground: "var(--foreground)",
|
||||||
},
|
},
|
||||||
borderRadius: {
|
},
|
||||||
lg: 'var(--radius)',
|
|
||||||
md: 'calc(var(--radius) - 2px)',
|
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require("tailwindcss-animate")],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user