Merge pull request #12 from Invest-Bot/dev

Merge Dev: Wiki Implemented
This commit is contained in:
zach 2023-02-09 01:27:58 -08:00 committed by GitHub
commit 44b1405979
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 5170 additions and 192 deletions

3
.gitignore vendored
View file

@ -39,3 +39,6 @@ next-env.d.ts
# vscode
.vscode
# wiki files from prebuild
/public/img/wiki/

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "InvestWiki"]
path = InvestWiki
url = git@github.com:Invest-Bot/InvestWiki.git

1
InvestWiki Submodule

@ -0,0 +1 @@
Subproject commit db6f7fefae70e63e577a88627fd2b4e0505749d9

View file

@ -1,6 +1,25 @@
import { m, Variants } from "framer-motion";
import { useRouter } from "next/router";
import Link from "next/link";
import { useState } from "react";
const ActiveLink = (props: {
href: string;
pageName: string;
children: React.ReactNode;
}) => {
const router = useRouter();
let styling = "text-white";
// if first part of path equals the pageName
if (router.pathname.split("/")[1] === props.pageName) {
styling = "text-[#a855f7]";
}
return (
<Link href={props.href} className={styling}>
{props.children}
</Link>
);
};
function NavBar() {
return (
@ -16,7 +35,7 @@ function NavBar() {
variants={navStripVariants}
>
<m.div variants={navIconVariants} className="pr-5 lg:pr-0 lg:pb-3">
<ActiveLink href="/dashboard">
<ActiveLink href="/dashboard" pageName="dashboard">
<DashIcon />
</ActiveLink>
</m.div>
@ -24,7 +43,7 @@ function NavBar() {
variants={navIconVariants}
className="pr-5 lg:pr-0 lg:pt-3 lg:pb-3"
>
<ActiveLink href="/ranking">
<ActiveLink href="/ranking" pageName="ranking">
<RankingIcon />
</ActiveLink>
</m.div>
@ -33,8 +52,13 @@ function NavBar() {
className="flex flex-row items-center justify-center pr-5 lg:w-full lg:flex-col lg:pr-0 lg:pb-5"
variants={navStripVariants}
>
<m.div variants={navIconVariants} className="pr-5 lg:pr-0 lg:pb-3">
<ActiveLink href="/wiki" pageName="wiki">
<WikiIcon />
</ActiveLink>
</m.div>
<m.div
className="fill-white stroke-white"
className="fill-white stroke-white lg:pt-3"
whileHover={{
color: "#fca311",
}}
@ -113,16 +137,16 @@ const RankingIcon = () => {
);
};
const ActiveLink = (props: { href: string; children: React.ReactNode }) => {
const router = useRouter();
let styling = "text-white";
if (router.pathname === props.href) {
styling = "text-[#a855f7]";
}
const WikiIcon = () => {
return (
<Link href={props.href} className={styling}>
{props.children}
</Link>
<NavSvgWrap>
<m.path
d="M5 13.18v4L12 21l7-3.82v-4L12 17l-7-3.82zM12 3 1 9l11 6 9-4.91V17h2V9L12 3z"
strokeWidth="1"
stroke="currentColor"
fill="currentColor"
/>
</NavSvgWrap>
);
};
@ -149,7 +173,7 @@ const navContainerVariants: Variants = {
const navStripVariants: Variants = {
initial: {
opacity: 0,
y: 100,
y: 40,
},
animate: {
opacity: 1,

View file

@ -0,0 +1,83 @@
import RankHistoryJson from "../../interfaces/ChartRankHistoryJSON";
import { Line } from "react-chartjs-2";
import {
Chart,
ChartData,
ChartOptions,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Tooltip,
} from "chart.js";
interface RankChartProps {
rankHistory: RankHistoryJson;
}
Chart.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip);
function RankChart(props: RankChartProps) {
let delayed: boolean;
const options: ChartOptions<"line"> = {
animation: {
onComplete: () => {
delayed = true;
},
delay: (context) => {
let delay = 0;
if (context.type === "data" && context.mode === "default" && !delayed) {
delay = context.dataIndex * 9;
}
return delay;
},
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
displayColors: false,
callbacks: {
label: (context) => {
const daysAgo = context.dataset.data.length - context.dataIndex - 1;
if (daysAgo === 0) {
return `Today`;
}
return `${daysAgo} days ago`;
},
},
},
},
scales: {
x: {
display: false,
},
y: {
display: false,
reverse: true,
},
},
responsive: true,
maintainAspectRatio: false,
};
const data: ChartData<"line"> = {
// make labels size dynamic
labels: props.rankHistory.rank.map((rank, i) => {
return "Rank " + rank;
}),
datasets: [
{
label: "Rank",
data: props.rankHistory.rank,
fill: false,
borderColor: "rgb(244, 114, 182)",
pointBackgroundColor: "rgba(0, 0, 0, 0)",
pointBorderColor: "rgba(0, 0, 0, 0)",
tension: 0,
},
],
};
return <Line options={options} data={data} />;
}
export default RankChart;

View file

@ -0,0 +1,20 @@
import WikiPage from "../../interfaces/WikiPage";
import mdStyles from "./markdown.module.css";
import RenderMarkdown from "./RenderMarkdown";
interface PageBodyProps {
children: string;
page: WikiPage;
}
export default function PageBody(props: PageBodyProps) {
return (
<div className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl">
<div className={mdStyles["markdown-body"]}>
<div className="text-left">
<RenderMarkdown page={props.page}>{props.children}</RenderMarkdown>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,63 @@
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import rehypeHighlight from "rehype-highlight";
import rehypeRaw from "rehype-raw";
import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
import WikiPage from "../../interfaces/WikiPage";
interface RenderMarkdownProps {
children: string;
page: WikiPage;
}
export default function RenderMarkdown({
children,
page,
}: RenderMarkdownProps) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeHighlight, rehypeSlug]}
components={{
a: ({ node, ...props }) => {
// if the link is internal, reformat it; if it ends with a slash, do not apply this
let href = props.href as string;
if (!href.endsWith("/") && !href.startsWith("http")) {
if (href.startsWith("/wiki/")) {
href = `/wiki/${page.language}${href.slice(5)}`;
} else {
// if single relative
href = `/wiki/${page.language}/${page.path}/${href}`;
}
}
return (
<Link legacyBehavior href={href as string}>
<a>{props.children ? props.children[0] : href}</a>
</Link>
);
},
img: ({ node, ...props }) => {
// if image is internal (relative), prefix it with the current page's path
let src = props.src as string;
if (!src.startsWith("http") && !src.startsWith("/")) {
src = `/img/wiki/${page.path}/${src}`;
}
return (
<div className="flex w-full flex-col items-center justify-center">
<img
className="mb-2"
src={src}
alt={props.alt as string}
title={props.title as string}
/>
<p> {props.title as string} </p>
</div>
);
},
}}
>
{children}
</ReactMarkdown>
);
}

View file

@ -0,0 +1,944 @@
/**
https://github.com/sindresorhus/github-markdown-css/blob/main/github-markdown-dark.css
**/
.markdown-body {
color-scheme: dark;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.markdown-body .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: " ";
display: inline-block;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
}
.markdown-body details,
.markdown-body figcaption,
.markdown-body figure {
display: block;
}
.markdown-body summary {
display: list-item;
}
.markdown-body [hidden] {
display: none !important;
}
.markdown-body a {
background-color: transparent;
color: #58a6ff;
text-decoration: none;
}
.markdown-body a:active,
.markdown-body a:hover {
outline-width: 0;
}
.markdown-body abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.markdown-body b,
.markdown-body strong {
font-weight: 600;
}
.markdown-body dfn {
font-style: italic;
}
.markdown-body h1 {
margin: 0.67em 0;
font-weight: 600;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid #414141;
}
.markdown-body mark {
background-color: rgba(187, 128, 9, 0.15);
color: #c9d1d9;
}
.markdown-body small {
font-size: 90%;
}
.markdown-body sub,
.markdown-body sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.markdown-body sub {
bottom: -0.25em;
}
.markdown-body sup {
top: -0.5em;
}
.markdown-body img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: #0d1117;
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre,
.markdown-body samp {
font-family: monospace, monospace;
font-size: 1em;
}
.markdown-body figure {
margin: 1em 40px;
}
.markdown-body hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid #414141;
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #30363d;
border: 0;
}
.markdown-body input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body [type="button"],
.markdown-body [type="reset"],
.markdown-body [type="submit"] {
-webkit-appearance: button;
}
.markdown-body [type="button"]::-moz-focus-inner,
.markdown-body [type="reset"]::-moz-focus-inner,
.markdown-body [type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
.markdown-body [type="button"]:-moz-focusring,
.markdown-body [type="reset"]:-moz-focusring,
.markdown-body [type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
.markdown-body [type="checkbox"],
.markdown-body [type="radio"] {
box-sizing: border-box;
padding: 0;
}
.markdown-body [type="number"]::-webkit-inner-spin-button,
.markdown-body [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.markdown-body [type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
.markdown-body [type="search"]::-webkit-search-cancel-button,
.markdown-body [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
.markdown-body ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.markdown-body ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body hr::before {
display: table;
content: "";
}
.markdown-body hr::after {
display: table;
clear: both;
content: "";
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body details summary {
cursor: pointer;
}
.markdown-body details:not([open]) > *:not(summary) {
display: none !important;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
line-height: 10px;
color: #c9d1d9;
vertical-align: middle;
background-color: #161b22;
border: solid 1px rgba(110, 118, 129, 0.4);
border-bottom-color: rgba(110, 118, 129, 0.4);
border-radius: 6px;
box-shadow: inset 0 -1px 0 rgba(110, 118, 129, 0.4);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h2 {
font-weight: 600;
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid #414141;
}
.markdown-body h3 {
font-weight: 600;
font-size: 1.25em;
}
.markdown-body h4 {
font-weight: 600;
font-size: 1em;
}
.markdown-body h5 {
font-weight: 600;
font-size: 0.875em;
}
.markdown-body h6 {
font-weight: 600;
font-size: 0.85em;
color: #8b949e;
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 0;
padding: 0 1em;
color: #8b949e;
border-left: 0.25em solid #30363d;
}
.markdown-body ul,
.markdown-body ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body tt,
.markdown-body code {
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
font-size: 12px;
word-wrap: normal;
}
.markdown-body .octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
}
.markdown-body ::placeholder {
color: #484f58;
opacity: 1;
}
.markdown-body input::-webkit-outer-spin-button,
.markdown-body input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.markdown-body .pl-c {
color: #8b949e;
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: #79c0ff;
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: #d2a8ff;
}
.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
color: #c9d1d9;
}
.markdown-body .pl-ent {
color: #7ee787;
}
.markdown-body .pl-k {
color: #ff7b72;
}
.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
color: #a5d6ff;
}
.markdown-body .pl-v,
.markdown-body .pl-smw {
color: #ffa657;
}
.markdown-body .pl-bu {
color: #f85149;
}
.markdown-body .pl-ii {
color: #f0f6fc;
background-color: #8e1519;
}
.markdown-body .pl-c2 {
color: #f0f6fc;
background-color: #b62324;
}
.markdown-body .pl-sr .pl-cce {
font-weight: bold;
color: #7ee787;
}
.markdown-body .pl-ml {
color: #f2cc60;
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: bold;
color: #1f6feb;
}
.markdown-body .pl-mi {
font-style: italic;
color: #c9d1d9;
}
.markdown-body .pl-mb {
font-weight: bold;
color: #c9d1d9;
}
.markdown-body .pl-md {
color: #ffdcd7;
background-color: #67060c;
}
.markdown-body .pl-mi1 {
color: #aff5b4;
background-color: #033a16;
}
.markdown-body .pl-mc {
color: #ffdfb6;
background-color: #5a1e02;
}
.markdown-body .pl-mi2 {
color: #c9d1d9;
background-color: #1158c7;
}
.markdown-body .pl-mdr {
font-weight: bold;
color: #d2a8ff;
}
.markdown-body .pl-ba {
color: #8b949e;
}
.markdown-body .pl-sg {
color: #484f58;
}
.markdown-body .pl-corl {
text-decoration: underline;
color: #a5d6ff;
}
.markdown-body [data-catalyst] {
display: block;
}
.markdown-body g-emoji {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: 400;
line-height: 1;
vertical-align: -0.075em;
}
.markdown-body g-emoji img {
width: 1em;
height: 1em;
}
.markdown-body::before {
display: table;
content: "";
}
.markdown-body::after {
display: table;
clear: both;
content: "";
}
.markdown-body > *:first-child {
margin-top: 0 !important;
}
.markdown-body > *:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .absent {
color: #f85149;
}
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre,
.markdown-body details {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body blockquote > :first-child {
margin-top: 0;
}
.markdown-body blockquote > :last-child {
margin-bottom: 0;
}
.markdown-body sup > a::before {
content: "[";
}
.markdown-body sup > a::after {
content: "]";
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: #c9d1d9;
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 tt,
.markdown-body h1 code,
.markdown-body h2 tt,
.markdown-body h2 code,
.markdown-body h3 tt,
.markdown-body h3 code,
.markdown-body h4 tt,
.markdown-body h4 code,
.markdown-body h5 tt,
.markdown-body h5 code,
.markdown-body h6 tt,
.markdown-body h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.markdown-body ul.no-list,
.markdown-body ol.no-list {
padding: 0;
list-style-type: none;
}
.markdown-body ol[type="1"] {
list-style-type: decimal;
}
.markdown-body ol[type="a"] {
list-style-type: lower-alpha;
}
.markdown-body ol[type="i"] {
list-style-type: lower-roman;
}
.markdown-body div > ol:not([type]) {
list-style-type: decimal;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li > p {
margin-top: 16px;
}
.markdown-body li + li {
margin-top: 0.25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table th {
font-weight: 600;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid #30363d;
}
.markdown-body table tr {
background-color: #00000060;
border-top: 1px solid #414141;
}
.markdown-body table tr:nth-child(2n) {
background-color: #0000003d;
}
.markdown-body table img {
background-color: transparent;
}
.markdown-body img[align="right"] {
padding-left: 20px;
}
.markdown-body img[align="left"] {
padding-right: 20px;
}
.markdown-body .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
.markdown-body span.frame {
display: block;
overflow: hidden;
}
.markdown-body span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid #30363d;
}
.markdown-body span.frame span img {
display: block;
float: left;
}
.markdown-body span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: #c9d1d9;
}
.markdown-body span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.markdown-body span.align-center span img {
margin: 0 auto;
text-align: center;
}
.markdown-body span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.markdown-body span.align-right span img {
margin: 0;
text-align: right;
}
.markdown-body span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.markdown-body span.float-left span {
margin: 13px 0 0;
}
.markdown-body span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.markdown-body span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.markdown-body code,
.markdown-body tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(110, 118, 129, 0.4);
border-radius: 6px;
}
.markdown-body code br,
.markdown-body tt br {
display: none;
}
.markdown-body del code {
text-decoration: inherit;
}
.markdown-body pre code {
font-size: 100%;
}
.markdown-body pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #00000060;
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body .csv-data td,
.markdown-body .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.markdown-body .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: #0d1117;
border: 0;
}
.markdown-body .csv-data tr {
border-top: 0;
}
.markdown-body .csv-data th {
font-weight: 600;
background: #161b22;
border-top: 0;
}
.markdown-body .footnotes {
font-size: 12px;
color: #8b949e;
border-top: 1px solid #30363d;
}
.markdown-body .footnotes ol {
padding-left: 16px;
}
.markdown-body .footnotes li {
position: relative;
}
.markdown-body .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid #1f6feb;
border-radius: 6px;
}
.markdown-body .footnotes li:target {
color: #c9d1d9;
}
.markdown-body .footnotes .data-footnote-backref g-emoji {
font-family: monospace;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item label {
font-weight: 400;
}
.markdown-body .task-list-item.enabled label {
cursor: pointer;
}
.markdown-body .task-list-item + .task-list-item {
margin-top: 3px;
}
.markdown-body .task-list-item .handle {
display: none;
}
.markdown-body .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}

6
interfaces/APIError.ts Normal file
View file

@ -0,0 +1,6 @@
export default interface APIError {
error: {
message: string;
code: number;
};
}

View file

@ -0,0 +1,3 @@
export default interface RankHistoryJson {
rank: number[];
}

View file

@ -0,0 +1,3 @@
export default interface IScriptParams {
env: any;
}

5
interfaces/UserAsset.ts Normal file
View file

@ -0,0 +1,5 @@
export default interface UserAsset {
name: string;
count: number;
provider: "7tv" | "bttv" | "ffz" | "twitch";
}

5
interfaces/UserBadge.ts Normal file
View file

@ -0,0 +1,5 @@
export default interface UserBadge {
name: string;
color: string;
priority: number;
}

View file

@ -0,0 +1,12 @@
import UserAsset from "./UserAsset";
import UserBadge from "./UserBadge";
export default interface UserFakeDataEntry {
id: number;
name: string;
points: number;
daily_change: number;
daily_change_percent: number;
assets: UserAsset[];
badges: UserBadge[];
}

View file

@ -0,0 +1,5 @@
import UserJSONEntry from "./UserJSONEntry";
export default interface UserFakeDataJSON {
data: UserJSONEntry[];
}

View file

@ -0,0 +1,8 @@
import UserFakeDataEntry from "./UserFakeDataEntry";
export default interface UserJSONEntry extends UserFakeDataEntry {
net_worth: number;
shares: number;
avatar_url: string;
rank: number;
}

10
interfaces/WikiPage.ts Normal file
View file

@ -0,0 +1,10 @@
export default interface WikiPage {
slug: string;
layout?: string;
content: string;
language: string;
path: string;
data: {
layout?: string;
};
}

View file

@ -31,7 +31,7 @@ function DashLayout(props: DashLayoutProps) {
const title = props.metaTags.title ?? "Dashboard - toffee";
return (
<m.div
className="bg-gradient-to-t from-zinc-900 to-[#3015457b]"
className="bg-zinc-900"
initial="initial"
animate="animate"
exit="exit"

View file

@ -52,6 +52,7 @@ const homeMain: NavTemplate[] = [
// { content: <DefaultNavOption label="About" href="/about" /> },
{ content: <DefaultNavOption label="Dashboard" href="/dashboard" /> },
{ content: <DefaultNavOption label="Team" href="/team" /> },
{ content: <DefaultNavOption label="Wiki" href="/wiki" /> },
// { content: <DefaultNavOption label="Contact" href="/contact" /> },
];

70
lib/wiki/api.ts Normal file
View file

@ -0,0 +1,70 @@
import { default as pathlib } from "path";
import fg from "fast-glob";
import fs from "fs";
import matter from "gray-matter";
const wikiDir = pathlib.join(process.cwd(), "InvestWiki/wiki");
const getAllFiles = (path: string) => {
const files = fg.sync("**/*.md", {
cwd: path,
onlyFiles: true,
absolute: false,
});
return files;
};
function getAllWikiPaths() {
const files = getAllFiles(wikiDir);
// manipulate array entries, remove the .md extension and move it to the front of the string
const paths = files.map((file: string) => {
const path = file.replace(".md", "");
// move the last part of the path to the front
const pathParts = path.split("/");
const lastPart = pathParts.pop();
if (lastPart) {
pathParts.unshift(lastPart);
}
return pathParts.join("/");
});
return paths;
}
function getWikiPath(lang: string, path: string) {
const files = getAllFiles(wikiDir);
// filter the files to only include the ones in the path
const pageFiles = files.filter(
(file) => file.split("/").slice(0, -1).join("/") === path
);
let pagePath = "";
// do https://github.com/vercel/next.js/blob/canary/examples/blog-starter/lib/api.ts
if (pageFiles.length !== 0) {
// check if there is a file with the language code
const langFile = pageFiles.find((file) => file.includes(`${lang}.md`));
if (langFile) {
pagePath = langFile;
} else {
// otherwise, use the english file if it exists, otherwise return the first file
pagePath =
pageFiles.find((file) => file.includes("en.md")) ?? pageFiles[0];
}
}
return pagePath;
}
function getWikiContent(lang: string, path: string) {
// get the path to the file
const relativePath = getWikiPath(lang, path);
if (!relativePath) {
return null;
}
const filePath = pathlib.join(wikiDir, relativePath);
// read the file
const fileContents = fs.readFileSync(filePath, "utf8");
const { data, content } = matter(fileContents);
return { data, content };
}
export { getWikiPath, getAllWikiPaths, getWikiContent };

26
middleware.ts Normal file
View file

@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
const PUBLIC_FILE = /\.(.*)$/;
export async function middleware(req: NextRequest) {
if (
req.nextUrl.pathname.startsWith("/_next") ||
req.nextUrl.pathname.includes("/api/") ||
PUBLIC_FILE.test(req.nextUrl.pathname)
) {
return;
}
// if path is /wiki/, redirect to /wiki/:locale
if (req.nextUrl.pathname === "/wiki") {
const language =
req.headers
.get("accept-language")
?.split(",")?.[0]
.split("-")?.[0]
.toLowerCase() || "en";
const redirUrl = req.nextUrl.clone();
redirUrl.pathname = redirUrl.pathname + `/${language}`;
return NextResponse.rewrite(redirUrl);
}
return;
}

View file

@ -11,6 +11,11 @@ const nextConfig = {
"cdn.frankerfacez.com",
],
},
i18n: {
// append/clean as needed
locales: ["en", "de", "fr", "es", "it", "pt", "ru", "zh", "ja", "ko"],
defaultLocale: "en",
},
};
module.exports = nextConfig;

3125
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,18 +5,30 @@
"scripts": {
"dev": "next dev -p 3010",
"build": "next build",
"prebuild": "ts-node ./scripts/runner.ts",
"start": "next start -p 3010",
"lint": "next lint",
"prepare": "husky install"
},
"dependencies": {
"@types/fs-extra": "^11.0.1",
"chart.js": "^4.2.0",
"eslint": "8.28.0",
"eslint-config-next": "13.0.4",
"fast-glob": "^3.2.12",
"framer-motion": "^7.6.19",
"fs-extra": "^11.1.0",
"gray-matter": "^4.0.3",
"ioredis": "^5.2.5",
"next": "13.0.4",
"react": "18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.5",
"rehype-highlight": "^6.0.0",
"rehype-raw": "^6.1.1",
"rehype-slug": "^5.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.31.2",
"typescript": "4.9.3"
},
@ -31,7 +43,8 @@
"postcss": "^8.4.19",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13",
"tailwindcss": "^3.2.4"
"tailwindcss": "^3.2.4",
"ts-node": "^10.9.1"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"

View file

@ -3,6 +3,7 @@ import type { ReactElement, ReactNode } from "react";
import type { NextPage } from "next";
import type { AppProps } from "next/app";
import { AnimatePresence, domAnimation, LazyMotion } from "framer-motion";
import { Router } from "next/router";
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
@ -12,6 +13,24 @@ type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
const routeChange = () => {
// Temporary fix to avoid flash of unstyled content
// during route transitions. Keep an eye on this
// issue and remove this code when resolved:
// https://github.com/vercel/next.js/issues/17464
const tempFix = () => {
const allStyleElems = document.querySelectorAll('style[media="x"]');
allStyleElems.forEach((elem) => {
elem.removeAttribute("media");
});
};
tempFix();
};
Router.events.on("routeChangeComplete", routeChange);
Router.events.on("routeChangeStart", routeChange);
export default function App({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);

View file

@ -1,21 +1,21 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createRedisInstance } from "../../misc/redis";
import { createRedisInstance } from "../../lib/redis";
import {
getGlobalEmotes as get7TVGlobalEmotes,
getChannelEmotes as get7TVChannelEmotes,
} from "../../misc/7TVAPI";
} from "../../lib/7TVAPI";
import {
getGlobalEmotes as getBTTVGlobalEmotes,
getUserByID as getBTTVUser,
} from "../../misc/BTTVAPI";
} from "../../lib/BTTVAPI";
import {
getGlobalEmotes as getFFZGlobalEmotes,
getEmoteSet as getFFZEmoteSet,
} from "../../misc/FFZAPI";
} from "../../lib/FFZAPI";
import {
getGlobalEmotes as getTwitchGlobalEmotes,
getChannelEmotes as getTwitchChannelEmotes,
} from "../../misc/TwitchAPI";
} from "../../lib/TwitchAPI";
type Data = {
[key: string]: any;

View file

@ -1,6 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createRedisInstance } from "../../misc/redis";
import { getUserByName } from "../../misc/TwitchAPI";
import UserBadge from "../../interfaces/UserBadge";
import UserFakeDataEntry from "../../interfaces/UserFakeDataEntry";
import UserJSONEntry from "../../interfaces/UserJSONEntry";
import { createRedisInstance } from "../../lib/redis";
import { getUserByName } from "../../lib/TwitchAPI";
import { fakePrices } from "./fakePrices";
type Data = {
@ -22,12 +25,24 @@ export default async function handler(
});
return;
}
let data = fakeData;
// calculate all net worths
data = data.map((user) => {
let userJSON: UserJSONEntry[];
let userList: UserFakeDataEntry[] = fakeData;
userJSON = userList.map((user) => {
return {
...user,
// calculate total assets held (shares)
shares: user.assets.reduce((a, b) => a + b.count, 0),
// sort users badges by priority
badges: (user.badges ?? []).sort((a, b) => b.priority - a.priority ?? 0),
avatar_url: "/img/logo.webp",
rank: 0,
// sort users assets by total value
assets: user.assets.sort(
(a, b) =>
(fakePrices[b.name] ?? 0) * b.count -
(fakePrices[a.name] ?? 0) * a.count
),
// calculate net worth
net_worth:
user.points +
user.assets.reduce(
@ -37,29 +52,19 @@ export default async function handler(
};
});
// calculate ranking based on net worth
data = data.sort((a, b) => (b.net_worth ?? 0) - (a.net_worth ?? 0));
data = data.map((user, i) => {
userJSON = userJSON.sort((a, b) => (b.net_worth ?? 0) - (a.net_worth ?? 0));
userJSON = userJSON.map((u, i) => {
return {
...user,
...u,
rank: i + 1,
// calculate total assets held (shares)
shares: user.assets.reduce((a, b) => a + b.count, 0),
// sort users badges by priority
badges: (user.badges ?? []).sort((a, b) => b.priority - a.priority ?? 0),
// sort users assets by total value
assets: user.assets.sort(
(a, b) =>
(fakePrices[b.name] ?? 0) * b.count -
(fakePrices[a.name] ?? 0) * a.count
),
};
});
// if username is specified, only return that user
if (username) {
// if user does not exist, return error
data = data.filter((u) => u.name === username);
if (data.length === 0) {
userJSON = userJSON.filter((u) => u.name === username);
if (userJSON.length === 0) {
res
.status(404)
.json({ error: { message: "User not found", code: 20000 } });
@ -82,90 +87,60 @@ export default async function handler(
twitchData.data[0].profile_image_url = "/img/logo.webp";
}
// add users profile picture url
data = data.map((u) => {
userJSON = userJSON.map((u) => {
return {
...u,
avatar_url: twitchData.data[0].profile_image_url ?? "",
};
});
res.status(200).json({ data: data[0] });
return;
}
if (sortBy) {
if (sortBy === "daily_change") {
data = data.sort((a, b) => b.daily_change - a.daily_change);
userJSON = userJSON.sort((a, b) => b.daily_change - a.daily_change);
} else if (sortBy === "daily_change_percent") {
data = data.sort(
userJSON = userJSON.sort(
(a, b) => b.daily_change_percent - a.daily_change_percent
);
} else if (sortBy === "shares") {
data = data.sort((a, b) => (b.shares ?? 0) - (a.shares ?? 0));
userJSON = userJSON.sort((a, b) => (b.shares ?? 0) - (a.shares ?? 0));
} else if (sortBy === "points") {
data = data.sort((a, b) => b.points - a.points);
userJSON = userJSON.sort((a, b) => b.points - a.points);
} else if (sortBy === "name") {
data = data.sort((a, b) => a.name.localeCompare(b.name));
userJSON = userJSON.sort((a, b) => a.name.localeCompare(b.name));
}
if (sortAsc === "true") {
// slow but only needed for temporary fake data anyway
data = data.reverse();
userJSON = userJSON.reverse();
}
}
// fake loading time
await new Promise((resolve) =>
setTimeout(resolve, 250 + Math.random() * 1000)
);
res.status(200).json({ data: data });
res.status(200).json({ data: userJSON });
}
interface asset {
name: string;
count: number;
provider: "7tv" | "bttv" | "ffz" | "twitch";
}
interface fakeDataEntry {
id: number;
name: string;
points: number;
daily_change: number;
daily_change_percent: number;
assets: asset[];
net_worth?: number;
shares?: number;
avatar_url?: string;
badges?: badge[];
}
interface badge {
name: string;
color: string;
priority: number;
}
const adminBadge: badge = {
const adminBadge: UserBadge = {
name: "Admin",
color: "#CC3333",
priority: 99999,
};
const CEOBadge: badge = {
const CEOBadge: UserBadge = {
name: "CEO",
color: "#F97316",
priority: 100000,
};
const webDevBadge: badge = {
const webDevBadge: UserBadge = {
name: "Web Dev",
color: "#a855f7",
priority: 50000,
};
const botDevBadge: badge = {
const botDevBadge: UserBadge = {
name: "Bot Dev",
color: "#48b2f1",
priority: 50001,
};
const fakeData: fakeDataEntry[] = [
const fakeData: UserFakeDataEntry[] = [
{
id: 4,
name: "3zachm",
@ -384,6 +359,7 @@ const fakeData: fakeDataEntry[] = [
provider: "7tv",
},
],
badges: [],
},
{
id: 6,
@ -423,6 +399,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 7,
@ -462,6 +439,7 @@ const fakeData: fakeDataEntry[] = [
provider: "ffz",
},
],
badges: [],
},
{
id: 8,
@ -496,6 +474,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 9,
@ -535,6 +514,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 10,
@ -574,6 +554,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 11,
@ -613,6 +594,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 12,
@ -657,6 +639,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 13,
@ -686,6 +669,7 @@ const fakeData: fakeDataEntry[] = [
provider: "7tv",
},
],
badges: [],
},
{
id: 14,
@ -720,6 +704,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 15,
@ -754,6 +739,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 16,
@ -788,6 +774,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 17,
@ -822,6 +809,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
{
id: 18,
@ -856,7 +844,6 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch",
},
],
badges: [],
},
];
export type { fakeDataEntry };

View file

@ -2,6 +2,7 @@ import { m, Variants } from "framer-motion";
import Link from "next/link";
import { ReactElement, useEffect, useState } from "react";
import Loading from "../../components/common/Loading";
import UserJSONEntry from "../../interfaces/UserJSONEntry";
import DashLayout from "../../layouts/DashLayout";
function Ranking() {
@ -158,47 +159,44 @@ function Ranking() {
>
{
// generate table rows
fakeData.map(
(entry: { [key: string]: any }, index: number) => {
// if daily change is negative, make it red
let changeClass = " text-lime-500";
if (entry.daily_change_percent < 0) {
changeClass = " text-red-500";
}
return (
<m.div
className="inline-grid w-full grid-flow-col grid-cols-[1fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-700 px-5 py-2 text-right md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"
key={entry.id}
variants={rankingDataLineVariants}
>
<h1 className="text-left md:text-center">
{index + 1}
</h1>
<Link
href={`/user/${entry.name}`}
className="overflow-hidden"
>
<h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left">
{entry.name}
</h1>
</Link>
<h1>{entry.net_worth.toLocaleString("en-US")}</h1>
<h1 className="hidden md:block">
{entry.points.toLocaleString("en-US")}
</h1>
<h1 className="hidden md:block">
{entry.shares.toLocaleString("en-US")}
</h1>
<h1 className={changeClass}>
{(
Math.round(entry.daily_change_percent * 1000) /
10
).toFixed(1) + "%"}
</h1>
</m.div>
);
fakeData.map((entry: UserJSONEntry, index: number) => {
// if daily change is negative, make it red
let changeClass = " text-lime-500";
if (entry.daily_change_percent < 0) {
changeClass = " text-red-500";
}
)
return (
<m.div
className="inline-grid w-full grid-flow-col grid-cols-[1fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-700 px-5 py-2 text-right md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"
key={entry.id}
variants={rankingDataLineVariants}
>
<h1 className="text-left md:text-center">
{index + 1}
</h1>
<Link
href={`/user/${entry.name}`}
className="overflow-hidden"
>
<h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left">
{entry.name}
</h1>
</Link>
<h1>{entry.net_worth.toLocaleString("en-US")}</h1>
<h1 className="hidden md:block">
{entry.points.toLocaleString("en-US")}
</h1>
<h1 className="hidden md:block">
{entry.shares.toLocaleString("en-US")}
</h1>
<h1 className={changeClass}>
{(
Math.round(entry.daily_change_percent * 1000) / 10
).toFixed(1) + "%"}
</h1>
</m.div>
);
})
}
</m.div>
)

View file

@ -5,6 +5,10 @@ import DashLayout from "../../../layouts/DashLayout";
import Image from "next/image";
import Loading from "../../../components/common/Loading";
import { GetServerSideProps } from "next";
import UserJSONEntry from "../../../interfaces/UserJSONEntry";
import APIError from "../../../interfaces/APIError";
import RankChart from "../../../components/userpage/RankChart";
import RankHistoryJSON from "../../../interfaces/ChartRankHistoryJSON";
interface EmoteURLs {
"7tv": { [key: string]: string };
@ -15,7 +19,8 @@ interface EmoteURLs {
}
interface UserPageProps {
userData: { [key: string]: any };
userData: UserJSONEntry;
serverError: APIError | null;
}
function UserPage(props: UserPageProps) {
@ -25,11 +30,15 @@ function UserPage(props: UserPageProps) {
const [errorCode, setErrorCode] = useState<number | null>(null);
const router = useRouter();
const { username } = router.query;
const [rankHistory, setRankHistory] = useState<RankHistoryJSON>(
randomRankHistory(props.userData.rank)
);
useEffect(() => {
if (!router.isReady) return;
if (props.userData.error) {
setErrorCode(props.userData.error.code);
// if it is of
if (props.serverError) {
setErrorCode(props.serverError.error.code);
}
fetch("/api/emotes")
.then((res) => res.json())
@ -122,9 +131,9 @@ function UserPage(props: UserPageProps) {
return (
<>
<div className="flex justify-center">
<div className="flex justify-center overflow-hidden">
<m.div
className="mt-7 inline-grid w-[calc(100%-40px)] max-w-5xl grid-cols-10 gap-3 pl-2 font-plusJakarta lg:mt-12 lg:pl-0 lg:pr-2"
className="mt-7 inline-grid w-[calc(100%-40px)] max-w-5xl grid-cols-10 gap-8 pl-2 font-plusJakarta sm:gap-3 lg:mt-12 lg:pl-0 lg:pr-2"
variants={containerVariants}
>
{/* User "banner" */}
@ -185,6 +194,7 @@ function UserPage(props: UserPageProps) {
</div>
</div>
</div>
{/* User's net worth (Desktop) */}
<div className="hidden md:block">
<h1>
<span className="text-4xl font-semibold text-zinc-400">
@ -204,38 +214,85 @@ function UserPage(props: UserPageProps) {
>
{/* User's Rank/Graph */}
<div className="col-span-7 rounded-2xl bg-zinc-800 bg-opacity-70">
<div className="flex flex-row items-center justify-between p-5">
<div className="flex-col px-2">
<h1 className="mb-1 whitespace-nowrap text-center text-xl font-medium text-white underline">
Global Rank
</h1>
<div className="flex items-center text-3xl font-bold">
<span className="text-zinc-400">#</span>
<span className="text-white">
{props.userData.rank.toLocaleString("en-US")}
</span>
<div className="inline-grid w-full grid-cols-5 p-5">
<div className="col-span-1 flex items-center justify-start">
<div className="flex-col px-2">
<h1 className="mb-1 whitespace-nowrap text-center text-xl font-medium text-white underline">
Global Rank
</h1>
<div className="flex items-center text-3xl font-bold">
<span className="text-zinc-400">#</span>
<span className="text-white">
{props.userData.rank.toLocaleString("en-US")}
</span>
</div>
</div>
</div>
<div className="hidden md:block">
<Image
src="/img/well_drawn_rank_chart.webp"
alt="Rank chart"
width={497}
height={100}
/>
{/* User's Rank Graph (Desktop) */}
<div className="col-span-4 hidden w-full items-center justify-center pr-4 md:flex lg:justify-end">
<div className="relative h-20 w-[90%] max-w-lg">
<RankChart rankHistory={rankHistory} />
</div>
<div className="fixed">
<m.div
className="relative top-10 rounded-3xl bg-zinc-900 bg-opacity-70 p-1 px-2 hover:cursor-pointer lg:left-7"
onClick={() =>
setRankHistory(randomRankHistory(props.userData.rank))
}
initial={{
color: "rgb(244, 114, 182)",
}}
whileHover={{
scale: 1.05,
backgroundColor: "rgb(244, 114, 182)",
color: "white",
}}
>
<p className="text-[8px]">randomize</p>
</m.div>
</div>
</div>
<div className="md:hidden">
<h1>
<span className="text-3xl font-semibold text-zinc-400 sm:text-4xl">
$
</span>
<span className="text-3xl text-white sm:text-4xl">
{props.userData.net_worth.toLocaleString("en-US")}
</span>
</h1>
{/* User's net worth (Mobile) */}
<div className="col-span-4 md:hidden">
<div className="flex h-full w-full items-center justify-end">
<h1>
<span className="text-3xl font-semibold text-zinc-400 sm:text-4xl">
$
</span>
<span className="text-3xl text-white sm:text-4xl">
{props.userData.net_worth.toLocaleString("en-US")}
</span>
</h1>
</div>
</div>
</div>
</div>
{/* User's Graph (Mobile) */}
<div className="col-span-7 rounded-2xl bg-zinc-800 bg-opacity-70 p-5 md:hidden">
<div className="flex items-center justify-center">
<div className="relative h-20 w-full">
<RankChart rankHistory={rankHistory} />
</div>
</div>
<div className="flex items-center justify-center">
<m.div
className="rounded-3xl bg-zinc-900 bg-opacity-70 p-1 px-2 hover:cursor-pointer"
onClick={() =>
setRankHistory(randomRankHistory(props.userData.rank))
}
initial={{
color: "rgb(244, 114, 182)",
}}
whileHover={{
scale: 1.05,
backgroundColor: "rgb(244, 114, 182)",
color: "white",
}}
>
<p className="text-[8px]">randomize</p>
</m.div>
</div>
</div>
{/* User's Assets */}
<div className="col-span-7 flex flex-col rounded-2xl bg-zinc-800 bg-opacity-70">
{/* User's Assets Header */}
@ -342,18 +399,15 @@ function UserPage(props: UserPageProps) {
<h1>Shares</h1>
<h1>{props.userData.shares.toLocaleString("en-US")}</h1>
<h1>Trades</h1>
<h1>{(props.userData.trades ?? 0).toLocaleString("en-US")}</h1>
<h1>{(0).toLocaleString("en-US")}</h1>
<h1>Peak rank</h1>
<h1>{(props.userData.peak_rank ?? 0).toLocaleString("en-US")}</h1>
<h1>{(0).toLocaleString("en-US")}</h1>
<h1>Joined</h1>
<h1>
{new Date(props.userData.joined ?? 0).toLocaleDateString(
"en-US",
{
year: "numeric",
month: "short",
}
)}
{new Date(0).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
})}
</h1>
</m.div>
{/* User's Favorite Emote */}
@ -465,6 +519,21 @@ const TwitchLogo = () => {
);
};
const randomRankHistory = (currentRank: number): RankHistoryJSON => {
// make a random rank array of size 31 ranging 1 - 18, with a 50% chance to remain the previous index's rank, end with current rank
let prevRank = Math.floor(Math.random() * 18) + 1;
const history: number[] = Array.from({ length: 31 }, (_, i) => {
if (i === 30) return currentRank;
let chance = i === 0 ? 0 : Math.random();
prevRank = chance <= 0.5 ? prevRank : Math.floor(Math.random() * 18) + 1;
return prevRank;
});
return {
rank: history,
};
};
const containerVariants: Variants = {
initial: {
opacity: 0,
@ -565,25 +634,30 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (
`/api/fakeUsers?u=${context.query.username}`,
process.env.NEXT_PUBLIC_URL
);
// TODO: add error handling
const res = await fetch(url);
let user = await res.json();
// return error in user.data if user not found
if (user.error) {
user = { data: user };
return {
props: {
userData: user,
serverError: user,
},
};
}
return { props: { userData: user.data } };
return { props: { userData: user.data[0], serverError: null } };
};
UserPage.getLayout = function getLayout(page: ReactElement) {
const { userData } = page.props;
const { userData, serverError } = page.props;
const metaTags = {
title: !userData.error
title: !serverError
? `${userData.name ?? "User 404"} - toffee`
: "User 404 - toffee",
description: !userData.error
description: !serverError
? `${userData.name}'s portfolio on toffee`
: "Couldn't find that user on toffee... :(",
imageUrl: !userData.error ? userData.avatar_url : undefined,
imageUrl: !serverError ? userData.avatar_url : undefined,
misc: {
"twitter:card": "summary",
},

View file

@ -0,0 +1,287 @@
import { getAllWikiPaths, getWikiContent } from "../../../lib/wiki/api";
import WikiPage from "../../../interfaces/WikiPage";
import DashLayout from "../../../layouts/DashLayout";
import Link from "next/link";
import { m } from "framer-motion";
import PageBody from "../../../components/wiki/PageBody";
import { ReactElement, useEffect, useState } from "react";
import Image from "next/image";
interface WikiLandingPageProps {
children: React.ReactNode;
page: WikiPage;
}
interface TableOfContentsItem {
id: string;
type: string;
text: string;
}
function WikiLandingPage(props: WikiLandingPageProps) {
const [wikiContent, setWikiContent] = useState<ReactElement>(<></>);
const [indexContent, setIndexContent] = useState<TableOfContentsItem[]>([]);
const [showMobileIndex, setShowMobileIndex] = useState<boolean>(false);
// needed for proper hydration due to replacing some elements
useEffect(() => {
setWikiContent(<PageBody page={props.page}>{props.page.content}</PageBody>);
}, [props.page]);
useEffect(() => {
const toc: TableOfContentsItem[] = [];
const headings = document.querySelectorAll("h2, h3");
// store the heading text, id, and type, keep order
headings.forEach((heading) => {
const id = heading.getAttribute("id");
const type = heading.tagName.toLowerCase();
const text = heading.textContent;
if (id && type && text) {
toc.push({ id, type, text });
}
});
setIndexContent(toc);
}, [wikiContent]);
const dirPath = props.page.path.split("/").map((path, index) => {
// if home page, don't show
if (path === "home") return <></>;
return (
<div key={path} className="flex flex-row">
<span className="mx-2 flex items-center text-white">
<m.svg
viewBox={"0 0 24 24"}
width={20}
height={20}
origin="center"
color="currentColor"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<m.path d="M6.23 20.23 8 22l10-10L8 2 6.23 3.77 14.46 12z" />
</m.svg>
</span>
<Link
href={`/wiki/${props.page.language}/${props.page.path
.split("/")
.slice(0, index + 1)
.join("/")}`}
>
<p className="hover:text-orange-400">{path}</p>
</Link>
</div>
);
});
return (
<div className="flex w-full flex-col items-center justify-center p-3 font-plusJakarta ">
<div className="inline-grid h-full w-full max-w-screen-2xl grid-cols-10">
{/* Desktop Header */}
<div className="col-span-10 mb-2 hidden grid-cols-10 lg:inline-grid">
{/* Title */}
<div className="col-span-2 flex items-center justify-center">
<div className="text-2xl text-white">
<p>toffee wiki</p>
</div>
<Image
className="h-auto w-auto"
src="/img/logo.webp"
alt="toffee logo"
width={64}
height={64}
/>
</div>
{/* Dir Path */}
<div className="col-span-8 ml-3 mb-3 mt-3 flex text-left text-xl">
<div className="flex w-auto flex-row items-center justify-start rounded-full bg-black p-2 px-4">
<Link href="/wiki">
<p className="hover:text-orange-400">home</p>
</Link>
{dirPath}
</div>
</div>
</div>
{/* Sidebar */}
<div className="col-span-2 hidden w-full flex-col items-center justify-center lg:block">
<div className="w-full rounded-tl-2xl rounded-bl-2xl border-r-2 border-orange-400 border-opacity-60 bg-zinc-800 bg-opacity-70 p-6 text-left text-6xl text-white">
<div className="text-2xl">Contents</div>
<div className="mt-4 text-left text-orange-400">
{indexContent.map((item) => {
return (
// increase indent based on heading level
<div
style={{
paddingLeft: `${(parseInt(item.type[1]) - 2) * 1.25}rem`,
}}
className="text-xl"
key={item.id}
>
<Link href={`#${item.id}`}>
<p className="mt-2 overflow-hidden overflow-ellipsis whitespace-nowrap hover:text-white">
{item.text}
</p>
</Link>
</div>
);
})}
</div>
</div>
</div>
<div className="col-span-10 mb-6 lg:hidden">
{/* Dir Path Mobile */}
<div className="col-span-8 flex text-left text-xl">
<div className="flex w-auto flex-row items-center justify-start rounded-full bg-black p-2 px-4">
<Link href="/wiki">
<p className="hover:text-orange-400">home</p>
</Link>
{dirPath}
</div>
</div>
</div>
{/* Mobile "Side"-bar */}
<div className="col-span-10 mb-6 lg:hidden">
<div className="w-full rounded-2xl rounded-tl-2xl bg-zinc-800 bg-opacity-70 p-6 text-left text-6xl text-white">
<div
className="flex cursor-pointer flex-row justify-between text-2xl"
onClick={() => setShowMobileIndex(!showMobileIndex)}
>
<div>Contents</div>
<m.svg
className="pointer-events-auto mt-2 ml-3 cursor-pointer lg:hidden"
origin="center"
width="20"
height="21"
viewBox="0 0 330 330"
x={0}
y={0}
animate={{ rotate: showMobileIndex ? 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>
</div>
<m.div
className="overflow-hidden text-left text-orange-400"
animate={{
height: showMobileIndex ? "auto" : 0,
marginTop: showMobileIndex ? "0.5rem" : 0,
}}
>
{indexContent.map((item) => {
return (
// increase indent based on heading level
<div
style={{
paddingLeft: `${(parseInt(item.type[1]) - 2) * 2}rem`,
}}
className="text-xl"
key={item.id}
>
<Link href={`#${item.id}`}>
<p className="mt-2 overflow-hidden overflow-ellipsis whitespace-nowrap hover:text-white">
{item.text}
</p>
</Link>
</div>
);
})}
</m.div>
</div>
</div>
{/* Main content */}
<div className="col-span-10 rounded-2xl bg-zinc-800 bg-opacity-70 px-6 pb-5 text-center text-6xl text-white lg:col-span-8 lg:rounded-tl-none">
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
key={props.page.slug}
>
<div className="w-full px-3">{wikiContent}</div>
</m.div>
</div>
</div>
</div>
);
}
type Params = {
params: {
slug: string[];
};
};
export async function getStaticProps({ params }: Params) {
// only language
if (params.slug.length < 2) {
// if langauge is two letters, redirect to its home
if (params.slug[0].length === 2 && params.slug[0].match(/^[a-z]+$/i)) {
return {
redirect: {
destination: `/wiki/${params.slug[0]}/home`,
},
};
}
// else, 404 to prevemt building pointless pages (e.g. /wiki/this-is-not-a-language x 580258538 times)
return {
notFound: true,
};
}
// slug[0] = language
// slug[1...n] = page path
const lang = params.slug[0];
const path = params.slug.slice(1).join("/");
const pageData = getWikiContent(lang, path);
// if no content, 404
if (!pageData) {
return {
notFound: true,
};
}
return {
props: {
page: {
slug: params.slug,
content: pageData.content,
language: lang,
path: path,
data: pageData.data,
},
},
};
}
export async function getStaticPaths() {
const paths = getAllWikiPaths();
return {
paths: paths.map((path) => {
return {
params: {
slug: path.split("/"),
},
};
}),
fallback: "blocking",
};
}
WikiLandingPage.getLayout = function getLayout(page: React.ReactNode) {
const metaTags = {
title: "Wiki - toffee",
description: "Wiki for toffee",
};
return <DashLayout metaTags={metaTags}>{page}</DashLayout>;
};
export default WikiLandingPage;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,50 @@
import path from "path";
import fs from "fs-extra";
import fg from "fast-glob";
import IScriptParams from "../../interfaces/IScriptParams";
export default async function execute(params: IScriptParams) {
// delete all files in /public/img/wiki/
const publicImgWikiPath = path.join(process.cwd(), "public/img/wiki");
try {
if (fs.existsSync(publicImgWikiPath)) {
const files = fs.readdirSync(publicImgWikiPath);
for (const file of files) {
fs.unlinkSync(path.join(publicImgWikiPath, file));
}
} else {
fs.mkdirSync(publicImgWikiPath, { recursive: true });
}
} catch (e) {
throw new Error("Please delete all files in /public/img/wiki/ manually.");
}
// recursively retrieve all /img folder paths in working directory/InvestWiki/wiki/
const wikiImgPaths = await fg("**/img", {
cwd: path.join(process.cwd(), "InvestWiki/wiki"),
onlyDirectories: true,
absolute: false,
});
// copy all image directories to /public/img/wiki/
for (const wikiImgPath of wikiImgPaths) {
const srcPath = path.join(process.cwd(), "InvestWiki/wiki", wikiImgPath);
const destPath = path.join(process.cwd(), "public/img/wiki", wikiImgPath);
fs.mkdirSync(destPath, { recursive: true });
const files = fs.readdirSync(srcPath);
for (const file of files) {
console.log(
"copying",
path.join(srcPath, file),
"to",
path.join(destPath, file)
);
try {
fs.copySync(path.join(srcPath, file), path.join(destPath, file), {
overwrite: false,
});
} catch (e) {
console.error(e);
}
}
}
}

43
scripts/runner.ts Normal file
View file

@ -0,0 +1,43 @@
// https://kontent.ai/blog/how-to-run-scripts-before-every-build-on-next-js/
import path from "path";
import fs from "fs";
import IScriptParams from "../interfaces/IScriptParams";
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd());
const runAsync = async () => {
// find all scripts in subfolder
const files = fs
.readdirSync(path.join(__dirname, "pre-build"))
.filter((file) => file.endsWith(".ts"))
.sort();
for (const file of files) {
const {
default: defaultFunc,
}: { default: (params: IScriptParams) => void } = await import(
`./pre-build/${file}`
);
try {
console.log(`Running pre-build script '${file}'`);
await defaultFunc({ env: process.env });
} catch (e) {
console.error(
`SCRIPT RUNNER: failed to execute pre-build script '${file}'`
);
console.error(e);
}
}
};
// Self-invocation async function
(async () => {
await runAsync();
})().catch((err) => {
console.error(err);
throw err;
});
export default function execute() {
// do nothing
}

View file

@ -51,3 +51,118 @@ body::-webkit-scrollbar-corner,
div::body::-webkit-scrollbar-corner {
background-color: transparent;
}
.hljs {
color: #c9d1d9;
background: #0d1117;
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #ff7b72;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #d2a8ff;
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #79c0ff;
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #a5d6ff;
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #ffa657;
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #8b949e;
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #7ee787;
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #c9d1d9;
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #1f6feb;
font-weight: bold;
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #f2cc60;
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #c9d1d9;
font-style: italic;
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #c9d1d9;
font-weight: bold;
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #aff5b4;
background-color: #033a16;
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #ffdcd7;
background-color: #67060c;
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}

View file

@ -15,6 +15,11 @@
"jsx": "preserve",
"incremental": true
},
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}