diff --git a/components/dashboard/NavBar.tsx b/components/dashboard/NavBar.tsx index b940fd6..af25a5f 100644 --- a/components/dashboard/NavBar.tsx +++ b/components/dashboard/NavBar.tsx @@ -6,7 +6,7 @@ function NavBar() { return ( <div className="m-3"> <m.div - className="flex min-h-[5rem] w-full flex-row items-center justify-between rounded-2xl bg-zinc-800 p-1 lg:h-full lg:w-24 lg:flex-col" + className="flex min-h-[5rem] w-full flex-row items-center justify-between rounded-2xl bg-zinc-800 bg-opacity-70 p-1 lg:h-full lg:w-24 lg:flex-col" variants={navContainerVariants} initial="initial" animate="animate" diff --git a/layouts/DashLayout.tsx b/layouts/DashLayout.tsx index d212598..3d6536f 100644 --- a/layouts/DashLayout.tsx +++ b/layouts/DashLayout.tsx @@ -18,7 +18,13 @@ function DashLayout(props: DashLayoutProps) { // get the current route for animation purposes const router = useRouter(); return ( - <> + <m.div + className="bg-gradient-to-t from-zinc-900 to-[#202737b6]" + initial="initial" + animate="animate" + exit="exit" + variants={containerVariants} + > <Head> <title>Dashboard - InvestBot</title> <meta name="description" content="Dashboard statistics for InvestBot" /> @@ -57,7 +63,7 @@ function DashLayout(props: DashLayoutProps) { </AnimatePresence> </LazyMotion> </div> - </> + </m.div> ); } diff --git a/layouts/HomeLayout.tsx b/layouts/HomeLayout.tsx index 8273c0f..da130f1 100644 --- a/layouts/HomeLayout.tsx +++ b/layouts/HomeLayout.tsx @@ -23,7 +23,12 @@ function HomeLayout(props: HomeLayoutProps) { // get the current route for animation purposes const router = useRouter(); return ( - <> + <m.div + initial="initial" + animate="animate" + exit="exit" + variants={containerVariants} + > <Head> <title>InvestBot</title> <meta name="description" content="Serving anny's community est. 2022" /> @@ -59,7 +64,7 @@ function HomeLayout(props: HomeLayoutProps) { </m.div> </AnimatePresence> </LazyMotion> - </> + </m.div> ); } diff --git a/pages/_app.tsx b/pages/_app.tsx index c6d8054..d9d8111 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,6 +2,7 @@ import "../styles/globals.css"; import type { ReactElement, ReactNode } from "react"; import type { NextPage } from "next"; import type { AppProps } from "next/app"; +import { AnimatePresence, domAnimation, LazyMotion } from "framer-motion"; export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & { getLayout?: (page: ReactElement) => ReactNode; @@ -15,5 +16,11 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { // Use the layout defined at the page level, if available const getLayout = Component.getLayout ?? ((page) => page); - return getLayout(<Component {...pageProps} />); + return ( + <LazyMotion features={domAnimation}> + <AnimatePresence mode="wait" initial={false}> + {getLayout(<Component {...pageProps} />)} + </AnimatePresence> + </LazyMotion> + ); } diff --git a/pages/api/fakeRanking.ts b/pages/api/fakeRanking.ts index 0b5731d..eb93f4d 100644 --- a/pages/api/fakeRanking.ts +++ b/pages/api/fakeRanking.ts @@ -9,6 +9,7 @@ export default async function handler( res: NextApiResponse<Data> ) { const sortBy = req.query.s ? (req.query.s as string) : undefined; + const sortAsc = req.query.a ? (req.query.a as string) : undefined; let data = fakeData; if (sortBy) { @@ -25,6 +26,10 @@ export default async function handler( } else if (sortBy === "name") { data = data.sort((a, b) => a.name.localeCompare(b.name)); } + if (sortAsc === "true") { + // slow but only needed for temporary fake data anyway + data = data.reverse(); + } } res.status(200).json({ data }); @@ -48,7 +53,7 @@ const fakeData: fakeDataEntry[] = [ points: 70, /// uninvested points shares: 20, dailyChange: -500, - dailyChangePercent: -0.523, + dailyChangePercent: -0.0498504486540378863409770687936, }, { id: 1, @@ -57,7 +62,7 @@ const fakeData: fakeDataEntry[] = [ points: 10020, shares: 200, dailyChange: 5420, - dailyChangePercent: 0.14, + dailyChangePercent: 0.0379259673920649359736897347981, }, { id: 2, @@ -70,138 +75,138 @@ const fakeData: fakeDataEntry[] = [ }, { id: 3, - name: "SecondSock", + name: "SecondSockSan", netWorth: 153495, points: 15020, shares: 20, - dailyChange: 5432, - dailyChangePercent: 0.104, + dailyChange: -10432, + dailyChangePercent: -0.06796312583471774324896576435715, }, { id: 0, - name: "Ente", + name: "e__n__t__e", netWorth: 429481824, points: 1002022, shares: 94214, - dailyChange: 3294444224, - dailyChangePercent: 0.94, + dailyChange: 329444422, + dailyChangePercent: 4.2932124926634939999741296760186, }, { id: 5, - name: "ObnoxiouslyLongNameWICKED", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "luckytohavefoundyou14252", + netWorth: 8024, + points: 423, + shares: 4, + dailyChange: 9, + dailyChangePercent: 0.00112163509471585244267198404786, }, { id: 6, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "ZeroxZerich", + netWorth: 842190, + points: 88542, + shares: 532, + dailyChange: -10219, + dailyChangePercent: -0.01213384153219582279533121979601, }, { id: 7, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "joeeyo", + netWorth: 10000000, + points: 9999979, + shares: 1, + dailyChange: 1, + dailyChangePercent: 0.0000001, }, { id: 8, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "dd_maru", + netWorth: 10328421, + points: 328421, + shares: 252, + dailyChange: 85192, + dailyChangePercent: 0.00824830823607984221402284047097, }, { id: 9, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "Goldeneye128", + netWorth: 58292, + points: 6521, + shares: 63, + dailyChange: -1942, + dailyChangePercent: -0.03331503465312564331297605160228, }, { id: 10, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "lilpastatv", + netWorth: 7328919, + points: 40, + shares: 93, + dailyChange: 921821, + dailyChangePercent: 0.12577857662228222197571019682439, }, { id: 11, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "domiswitch", + netWorth: 43290, + points: 5002, + shares: 15, + dailyChange: 2429, + dailyChangePercent: 0.05610995610995610995610995610996, }, { id: 12, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "minosura", + netWorth: 904328, + points: 32901, + shares: 83, + dailyChange: 94821, + dailyChangePercent: 0.10485244291894091524314186887943, }, { id: 13, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "scienceteam_member", + netWorth: 34894, + points: 958, + shares: 5, + dailyChange: -7964, + dailyChangePercent: -0.22823408035765461110792686421734, }, { id: 14, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "witchdev", + netWorth: 94382912, + points: 8532, + shares: 329, + dailyChange: -421, + dailyChangePercent: -0.0000044605531984433792422085896, }, { id: 15, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "justone123879", + netWorth: 8889123, + points: 86333, + shares: 153, + dailyChange: 53289, + dailyChangePercent: 0.00599485461051669551653183334284, }, { id: 16, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "marcelr_", + netWorth: 400329, + points: 39291, + shares: 52, + dailyChange: 1329, + dailyChangePercent: 0.00331976948959480827019776234047, }, { id: 17, - name: "User", - netWorth: 0, - points: 100, - shares: 0, - dailyChange: 0, - dailyChangePercent: 0, + name: "fossabot", + netWorth: 20005, + points: 0, + shares: 1, + dailyChange: -31042, + dailyChangePercent: -1.5517120719820044988752811797051, }, ]; diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 6595e0a..8ad0a5a 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -17,31 +17,31 @@ function Dashboard() { animate="animate" > <m.div - className="col-span-1 m-2 rounded-2xl bg-zinc-800 p-3" + className="col-span-1 m-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3" variants={gridItemVariants} > 1 </m.div> <m.div - className="col-span-1 row-span-3 m-2 rounded-2xl bg-zinc-800 p-3 lg:col-span-3 lg:row-span-1" + className="col-span-1 row-span-3 m-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3 lg:col-span-3 lg:row-span-1" variants={gridItemVariants} > 2 </m.div> <m.div - className="col-span-1 m-2 rounded-2xl bg-zinc-800 p-3" + className="col-span-1 m-2 rounded-2xl bg-zinc-800 bg-opacity-80 p-3" variants={gridItemVariants} > 3 </m.div> <m.div - className="col-span-1 row-span-4 m-2 rounded-2xl bg-zinc-800 p-3 lg:col-span-4 lg:row-span-1" + className="col-span-1 row-span-4 m-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3 lg:col-span-4 lg:row-span-1" variants={gridItemVariants} > 4 </m.div> <m.div - className="col-span-1 m-2 rounded-2xl bg-zinc-800 p-3" + className="col-span-1 m-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3" variants={gridItemVariants} > 5 diff --git a/pages/dashboard/ranking.tsx b/pages/dashboard/ranking.tsx index 54ec832..13b8956 100644 --- a/pages/dashboard/ranking.tsx +++ b/pages/dashboard/ranking.tsx @@ -6,15 +6,75 @@ import { fakeDataEntry } from "../api/fakeRanking"; function Ranking() { const [sortBy, setSortBy] = useState("netWorth"); + const [sortAsc, setSortAsc] = useState(false); const [fakeData, setFakeData] = useState([]); useEffect(() => { - fetch(`/api/fakeRanking?s=${sortBy}`) + // fetch data from api on change to sort method + fetch(`/api/fakeRanking?s=${sortBy}&a=${sortAsc}`) .then((res) => res.json()) .then((data) => { setFakeData(data.data); }); - }, [sortBy]); + }, [sortBy, sortAsc]); + + const SortSVG = (props: { sortType: string; children?: ReactElement }) => { + // if not current sort, return a line, otherwise return an arrow corresponding to sortAsc + if (sortBy != props.sortType) { + return ( + <m.svg + className="ml-2" + origin="center" + width="15" + height="15" + viewBox="0 0 6 6" + x={0} + y={0} + > + <m.line + x1="1" + y1="3" + x2="5" + y2="3" + stroke="white" + stroke-linecap="round" + /> + </m.svg> + ); + } + return ( + <m.svg + className="ml-2" + origin="center" + width="15" + height="15" + viewBox="0 0 330 330" + x={0} + y={0} + // if asc, rotate 180 + animate={{ rotate: sortAsc ? 180 : 0 }} + > + <m.path + d="M325.607,79.393c-5.857-5.857-15.355-5.858-21.213,0.001l-139.39,139.393L25.607,79.393 c-5.857-5.857-15.355-5.858-21.213,0.001c-5.858,5.858-5.858,15.355,0,21.213l150.004,150c2.813,2.813,6.628,4.393,10.606,4.393 s7.794-1.581,10.606-4.394l149.996-150C331.465,94.749,331.465,85.251,325.607,79.393z" + fill="white" + stroke="white" + strokeWidth="15" + strokeLinecap="round" + /> + </m.svg> + ); + }; + + const setSortMethod = (sortType: string) => { + // if same sort, toggle asc + // a change in sort means asc should be false for intuitive behavior + if (sortBy == sortType) { + setSortAsc(!sortAsc); + } else { + setSortAsc(false); + setSortBy(sortType); + } + }; return ( <> @@ -37,37 +97,64 @@ function Ranking() { </m.h1> {/* TODO: responsive for extremely skinny displays (i.e. galaxy fold), or really for mobile entirely so info is not lost */} <m.div - className="text-md inline-grid w-full rounded-t-2xl bg-zinc-800 bg-opacity-70 p-3 pt-4 backdrop-blur sm:text-xl" + className="text-md inline-grid w-full rounded-t-2xl bg-zinc-800 bg-opacity-70 p-3 pt-4 sm:text-xl" variants={rankingCardVariants} initial="initial" animate="animate" > - <m.div className="inline-grid w-full grid-flow-col grid-cols-[0.75fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-400 px-5 pb-3 text-right text-gray-300 md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"> - <m.h1 className="text-left md:text-center">#</m.h1> - <m.h1 - className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left" - onClick={() => setSortBy("name")} + {/* Column names and arrows */} + <div className="inline-grid w-full grid-flow-col grid-cols-[0.75fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-400 px-5 pb-3 text-right text-gray-300 md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"> + <h1 className="text-left md:text-center">#</h1> + <div + className="pointer-events-auto flex cursor-pointer flex-row items-center justify-start" + onClick={() => setSortMethod("name")} > - Name - </m.h1> - <m.h1 onClick={() => setSortBy("netWorth")}>Assets</m.h1> - <m.h1 - className="hidden md:block" - onClick={() => setSortBy("points")} + <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left"> + Name + </h1> + <SortSVG sortType="name" /> + </div> + <div + className="pointer-events-auto flex cursor-pointer flex-row items-center justify-end" + onClick={() => setSortMethod("netWorth")} > - Points - </m.h1> - <m.h1 - className="hidden md:block" - onClick={() => setSortBy("shares")} + <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left"> + Assets + </h1> + <SortSVG sortType="netWorth" /> + </div> + <div + className="pointer-events-auto hidden cursor-pointer flex-row items-center justify-end md:flex" + onClick={() => setSortMethod("points")} > - Shares - </m.h1> - <m.h1 onClick={() => setSortBy("dailyChange")}>Daily</m.h1> - </m.div> + <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left"> + Points + </h1> + <SortSVG sortType="points" /> + </div> + <div + className="pointer-events-auto hidden cursor-pointer flex-row items-center justify-end md:flex" + onClick={() => setSortMethod("shares")} + > + <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left"> + Shares + </h1> + <SortSVG sortType="shares" /> + </div> + <div + className="pointer-events-auto flex cursor-pointer flex-row items-center justify-end" + onClick={() => setSortMethod("dailyChange")} + > + <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left"> + Daily + </h1> + <SortSVG sortType="dailyChange" /> + </div> + </div> { - // TODO: add arrow to show which column is being sorted by and which direction + // generate table rows fakeData.map((entry: fakeDataEntry, index) => { + // if daily change is negative, make it red let changeClass = " text-lime-500"; if (entry.dailyChangePercent < 0) { changeClass = " text-red-500"; @@ -105,6 +192,7 @@ function Ranking() { ); } +// entire container page animation const containerVariants: Variants = { initial: { opacity: 1, @@ -122,6 +210,7 @@ const containerVariants: Variants = { }, }; +// header animation if needed const headerVariants: Variants = { initial: { opacity: 0, @@ -140,6 +229,7 @@ const headerVariants: Variants = { }, }; +// table container animation const rankingCardVariants: Variants = { initial: { opacity: 0,