feat(front): use framer-motion and optimize a thing.

This commit is contained in:
Hexagonn
2024-10-21 09:12:32 +00:00
committed by GitHub
parent 4c0dc41aa8
commit 5bb65e575c
3 changed files with 128 additions and 60 deletions

View File

@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@types/escape-html": "^1.0.4", "@types/escape-html": "^1.0.4",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"framer-motion": "^11.11.9",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"next": "14.2.15", "next": "14.2.15",
"react": "^18", "react": "^18",

24
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
escape-html: escape-html:
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3 version: 1.0.3
framer-motion:
specifier: ^11.11.9
version: 11.11.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ioredis: ioredis:
specifier: ^5.4.1 specifier: ^5.4.1
version: 5.4.1 version: 5.4.1
@@ -856,6 +859,20 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'} engines: {node: '>=14'}
framer-motion@11.11.9:
resolution: {integrity: sha512-XpdZseuCrZehdHGuW22zZt3SF5g6AHJHJi7JwQIigOznW4Jg1n0oGPMJQheMaKLC+0rp5gxUKMRYI6ytd3q4RQ==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fs.realpath@1.0.0: fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -2756,6 +2773,13 @@ snapshots:
cross-spawn: 7.0.3 cross-spawn: 7.0.3
signal-exit: 4.1.0 signal-exit: 4.1.0
framer-motion@11.11.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
tslib: 2.8.0
optionalDependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
fs.realpath@1.0.0: {} fs.realpath@1.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import Image from "next/image"; import React, { useState, useRef } from "react";
import React, { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { useSmoothCount } from "use-smooth-count"; import { useSmoothCount } from "use-smooth-count";
import useSWR from "swr"; import useSWR from "swr";
@@ -12,14 +13,17 @@ import Link from "next/link";
export default function Home() { export default function Home() {
const [userId, setUserId] = useState<null | string>(null); const [userId, setUserId] = useState<null | string>(null);
const [userError, setUserError] = useState<string | JSX.Element>(); const [userError, setUserError] = useState<string | JSX.Element>();
const [userData, setUserData] = useState<{ userId: string } | null>(null);
const [copyState, setCopyState] = useState("Copy"); const [copyState, setCopyState] = useState("Copy");
const [outputType, setOutputType] = useState<"markdown" | "html">( const [outputType, setOutputType] = useState<"markdown" | "html">(
"markdown", "markdown",
); );
const [isLoading, setIsLoading] = useState(true);
const [onImageLoaded, setOnImageLoaded] = useState(false);
const userCount = useSWR("getUserCount", getUserCount); const userCount = useSWR("getUserCount", getUserCount);
const countRef = useRef<HTMLDivElement | null>(null); const countRef = useRef<HTMLDivElement | null>(null);
const counter = useSmoothCount({ useSmoothCount({
ref: countRef, ref: countRef,
target: userCount.data || 0, target: userCount.data || 0,
duration: 3, duration: 3,
@@ -28,9 +32,9 @@ export default function Home() {
const outputText = () => { const outputText = () => {
if (outputType === "html") { if (outputType === "html") {
return `<a href="https://discord.com/users/${userId}"><img src="https://lanyard-profile-readme.vercel.app/api/${userId}" /></a>`; return `<a href="https://discord.com/users/${userData?.userId}"><img src="${window.location.origin}/api/${userData?.userId}" /></a>`;
} else { } else {
return `[![Discord Presence](https://lanyard-profile-readme.vercel.app/api/${userId})](https://discord.com/users/${userId})`; return `[![Discord Presence](${window.location.origin}/api/${userData?.userId})](https://discord.com/users/${userData?.userId})`;
} }
}; };
const copy = () => { const copy = () => {
@@ -40,17 +44,17 @@ export default function Home() {
setTimeout(() => setCopyState("Copy"), 1500); setTimeout(() => setCopyState("Copy"), 1500);
}; };
const changeDiscordId = async (e: React.ChangeEvent<HTMLInputElement>) => { const submitDiscordId = async () => {
e.preventDefault(); setIsLoading(true);
setOnImageLoaded(false);
setUserData(null);
setUserError(undefined); setUserError(undefined);
setUserId(e.target.value);
if (e.target.value === "") return;
if (!isSnowflake(e.target.value)) if (!userId) return setUserError("Please enter a Discord ID");
return setUserError("Invalid Discord ID");
if ((await isUserMonitored(e.target.value)) === false) if (!isSnowflake(userId)) return setUserError("Invalid Discord ID");
if ((await isUserMonitored(userId)) === false)
return setUserError( return setUserError(
<> <>
User is not being monitored by Lanyard, please join{" "} User is not being monitored by Lanyard, please join{" "}
@@ -64,6 +68,9 @@ export default function Home() {
and try again. and try again.
</>, </>,
); );
setUserData({ userId });
setIsLoading(false);
}; };
return ( return (
@@ -78,53 +85,89 @@ export default function Home() {
GitHub Profile GitHub Profile
</p> </p>
<br /> <br />
<input <form
className="input" className="flex w-full gap-2"
onChange={changeDiscordId} onSubmit={(e) => {
placeholder="Enter your Discord ID" e.preventDefault();
/>
{userError && ( submitDiscordId();
<p className="mt-1 text-sm text-red-500"> }}
* {userError} >
</p> <input
)} className="input"
{userId && ( onChange={(e) => setUserId(e.target.value)}
<> value={userId || ""}
<div className="mb-1 mt-4 flex gap-1"> placeholder="Enter your Discord ID"
<button />
className={`action ${outputType === "markdown" ? "active" : ""}`} <button className="action" type="submit">
onClick={() => setOutputType("markdown")} {">>"}
> </button>
Markdown </form>
</button> <motion.p
<button variants={{
className={`action ${outputType === "html" ? "active" : ""}`} open: { opacity: 1 },
onClick={() => setOutputType("html")} closed: { opacity: 0 },
> }}
HTML initial="closed"
</button> animate={userError ? "open" : "closed"}
</div> className="mt-1 text-sm text-red-500"
<div className="output bg-black"> >
{outputText()} * {userError}
</div> </motion.p>
<div className="mt-4 flex gap-2"> <motion.div
<button className="action" onClick={copy}> variants={{
{copyState} open: {
</button> opacity: 1,
<button className="action">Option</button> },
</div> closed: {
{!userError && ( opacity: 0,
<div className="mt-2"> },
<img }}
src={`/api/${userId}`} initial="closed"
height={350} animate={!isLoading ? "open" : "closed"}
width={500} transition={{ duration: 0.5 }}
alt="Your Lanyard banner" >
/> <div className="mb-1 mt-4 flex gap-1">
</div> <button
)} className={`action ${outputType === "markdown" ? "active" : ""}`}
</> onClick={() => setOutputType("markdown")}
)} >
Markdown
</button>
<button
className={`action ${outputType === "html" ? "active" : ""}`}
onClick={() => setOutputType("html")}
>
HTML
</button>
</div>
<div className="output bg-black">{outputText()}</div>
<div className="mt-4 flex gap-2">
<button className="action" onClick={copy}>
{copyState}
</button>
<button className="action">Option</button>
</div>
<div className="mt-2">
<motion.img
className={`${onImageLoaded ? "" : "animate-pulse rounded-md bg-[#3d3d43]"}`}
initial={{
opacity: 0,
"aria-hidden": true,
}}
animate={{
opacity: onImageLoaded ? 1 : 0,
"aria-hidden": onImageLoaded ? false : true,
}}
transition={{ duration: 0.5 }}
src={`/api/${userData?.userId}`}
height={280}
width={500}
alt="Your Lanyard banners"
onLoad={() => setOnImageLoaded(true)}
/>
</div>
</motion.div>
</div> </div>
</main> </main>
<footer className="stat"> <footer className="stat">