commit
44b1405979
40 changed files with 5170 additions and 192 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -39,3 +39,6 @@ next-env.d.ts
|
|||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
# wiki files from prebuild
|
||||
/public/img/wiki/
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "InvestWiki"]
|
||||
path = InvestWiki
|
||||
url = git@github.com:Invest-Bot/InvestWiki.git
|
1
InvestWiki
Submodule
1
InvestWiki
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit db6f7fefae70e63e577a88627fd2b4e0505749d9
|
|
@ -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,
|
||||
|
|
83
components/userpage/RankChart.tsx
Normal file
83
components/userpage/RankChart.tsx
Normal 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;
|
20
components/wiki/PageBody.tsx
Normal file
20
components/wiki/PageBody.tsx
Normal 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>
|
||||
);
|
||||
}
|
63
components/wiki/RenderMarkdown.tsx
Normal file
63
components/wiki/RenderMarkdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
944
components/wiki/markdown.module.css
Normal file
944
components/wiki/markdown.module.css
Normal 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
6
interfaces/APIError.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default interface APIError {
|
||||
error: {
|
||||
message: string;
|
||||
code: number;
|
||||
};
|
||||
}
|
3
interfaces/ChartRankHistoryJSON.ts
Normal file
3
interfaces/ChartRankHistoryJSON.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default interface RankHistoryJson {
|
||||
rank: number[];
|
||||
}
|
3
interfaces/IScriptParams.ts
Normal file
3
interfaces/IScriptParams.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default interface IScriptParams {
|
||||
env: any;
|
||||
}
|
5
interfaces/UserAsset.ts
Normal file
5
interfaces/UserAsset.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default interface UserAsset {
|
||||
name: string;
|
||||
count: number;
|
||||
provider: "7tv" | "bttv" | "ffz" | "twitch";
|
||||
}
|
5
interfaces/UserBadge.ts
Normal file
5
interfaces/UserBadge.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default interface UserBadge {
|
||||
name: string;
|
||||
color: string;
|
||||
priority: number;
|
||||
}
|
12
interfaces/UserFakeDataEntry.ts
Normal file
12
interfaces/UserFakeDataEntry.ts
Normal 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[];
|
||||
}
|
5
interfaces/UserFakeDataJSON.ts
Normal file
5
interfaces/UserFakeDataJSON.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import UserJSONEntry from "./UserJSONEntry";
|
||||
|
||||
export default interface UserFakeDataJSON {
|
||||
data: UserJSONEntry[];
|
||||
}
|
8
interfaces/UserJSONEntry.ts
Normal file
8
interfaces/UserJSONEntry.ts
Normal 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
10
interfaces/WikiPage.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default interface WikiPage {
|
||||
slug: string;
|
||||
layout?: string;
|
||||
content: string;
|
||||
language: string;
|
||||
path: string;
|
||||
data: {
|
||||
layout?: string;
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
70
lib/wiki/api.ts
Normal 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
26
middleware.ts
Normal 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;
|
||||
}
|
|
@ -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
3125
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
287
pages/wiki/[...slug]/index.tsx
Normal file
287
pages/wiki/[...slug]/index.tsx
Normal 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;
|
BIN
public/img/emotes/peepoTalk.webp
Normal file
BIN
public/img/emotes/peepoTalk.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
50
scripts/pre-build/WikiImages.ts
Normal file
50
scripts/pre-build/WikiImages.ts
Normal 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
43
scripts/runner.ts
Normal 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
|
||||
}
|
|
@ -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 */
|
||||
}
|
||||
|
|
|
@ -15,6 +15,11 @@
|
|||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS"
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue