mirror of
https://github.com/PretendoNetwork/mario_kart_8_website.git
synced 2024-05-18 04:40:53 -04:00
Initial release
This commit is contained in:
parent
e52736b576
commit
9233ddec1c
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -33,3 +33,5 @@ yarn-error.log*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
app.config.ts
|
39
README.md
39
README.md
|
@ -1,34 +1,11 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
# MK8 Pretendo website
|
||||
|
||||
## Getting Started
|
||||
# Instructions
|
||||
|
||||
First, run the development server:
|
||||
- Rename ``app.config.example.ts`` to ``app.config.ts`` and edit the configuration file to match your running env.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
```shell
|
||||
npm install
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
15
app.config.example.ts
Normal file
15
app.config.example.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
interface AppConfig {
|
||||
jwt_secret: string;
|
||||
grpc_host: string;
|
||||
grpc_port: number;
|
||||
grpc_api_key: string;
|
||||
}
|
||||
|
||||
const app_config: AppConfig = {
|
||||
jwt_secret: "qsdqsdqsdqsdqsdqsdqsd",
|
||||
grpc_host: "localhost",
|
||||
grpc_port: 50051,
|
||||
grpc_api_key: "azeazeaz"
|
||||
}
|
||||
|
||||
export default app_config;
|
3
app/admin/page.tsx
Normal file
3
app/admin/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default async function AdminPage() {
|
||||
return <h1>Hello, Admin Page!</h1>
|
||||
}
|
26
app/api/gatherings/route.ts
Normal file
26
app/api/gatherings/route.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import app_config from "@/app.config";
|
||||
import { amkj_grpc_client } from "@/helpers/grpc";
|
||||
import { GetAllGatheringsResponse } from "@/helpers/proto/amkj_service";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Metadata } from "nice-grpc";
|
||||
|
||||
var allGatherings: GetAllGatheringsResponse | null = null;
|
||||
var lastAllGatheringsTime: Date = new Date();
|
||||
|
||||
export async function GET(request: Request) {
|
||||
request.url; // https://nextjs.org/docs/app/building-your-application/routing/router-handlers#dynamic-route-handlers
|
||||
|
||||
try {
|
||||
if (!allGatherings || ((new Date().getTime() - lastAllGatheringsTime.getTime()) > 5000)) {
|
||||
allGatherings = await amkj_grpc_client.getAllGatherings({ offset: 0, limit: -1 }, {
|
||||
metadata: Metadata({
|
||||
"X-API-Key": app_config.grpc_api_key
|
||||
})
|
||||
});
|
||||
lastAllGatheringsTime = new Date();
|
||||
}
|
||||
return NextResponse.json(allGatherings);
|
||||
} catch (err) {
|
||||
return new NextResponse("{}", { status: 500 });
|
||||
}
|
||||
}
|
26
app/api/status/route.ts
Normal file
26
app/api/status/route.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import app_config from "@/app.config";
|
||||
import { amkj_grpc_client } from "@/helpers/grpc";
|
||||
import { GetServerStatusResponse } from "@/helpers/proto/amkj_service";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Metadata } from "nice-grpc";
|
||||
|
||||
var status: GetServerStatusResponse | null = null;
|
||||
var lastStatusTime: Date = new Date();
|
||||
|
||||
export async function GET(request: Request) {
|
||||
request.url; // https://nextjs.org/docs/app/building-your-application/routing/router-handlers#dynamic-route-handlers
|
||||
|
||||
try {
|
||||
if (!status || ((new Date().getTime() - lastStatusTime.getTime()) > 5000)) {
|
||||
status = await amkj_grpc_client.getServerStatus({}, {
|
||||
metadata: Metadata({
|
||||
"X-API-Key": app_config.grpc_api_key
|
||||
})
|
||||
});
|
||||
lastStatusTime = new Date();
|
||||
}
|
||||
return NextResponse.json(status);
|
||||
} catch (err) {
|
||||
return new NextResponse("{}", { status: 500 });
|
||||
}
|
||||
}
|
3
app/dashboard/page.tsx
Normal file
3
app/dashboard/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default async function DashboardPage() {
|
||||
return <h1>Hello, Dashboard Page!</h1>
|
||||
}
|
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 7.2 KiB |
109
app/gatherings/page.tsx
Normal file
109
app/gatherings/page.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
'use client'
|
||||
|
||||
import { GatheringEntry } from '@/components/GatheringEntry';
|
||||
import { Gathering } from '@/helpers/proto/amkj_service';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Spinner } from 'react-bootstrap';
|
||||
|
||||
export default function GatheringsPage() {
|
||||
|
||||
const updateTime = 20;
|
||||
|
||||
const [allGatheringsResponse, setAllGatheringsResponse] = useState<Response | null>(null);
|
||||
const [allGatherings, setAllGatherings] = useState<Gathering[]>([]);
|
||||
const [timer, setTimer] = useState<number>(updateTime - 1);
|
||||
|
||||
const fetchAllGatherings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/gatherings', { cache: "no-store" });
|
||||
setAllGatheringsResponse(response);
|
||||
const data = await response.json();
|
||||
setAllGatherings(data.gatherings);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch all gatherings:', error);
|
||||
setAllGatheringsResponse(null);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllGatherings();
|
||||
const fetchInterval = setInterval(fetchAllGatherings, updateTime * 1000);
|
||||
const timerInterval = setInterval(() => {
|
||||
setTimer((timer) => {
|
||||
if (timer == 0) {
|
||||
return updateTime - 1;
|
||||
} else {
|
||||
return timer - 1;
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(fetchInterval);
|
||||
clearInterval(timerInterval)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const getPageJSX = () => {
|
||||
if (!allGatheringsResponse) {
|
||||
return <Spinner animation="border" role="status" />;
|
||||
} else {
|
||||
if (allGatheringsResponse.status != 200) {
|
||||
return (
|
||||
<Alert variant="danger" onClick={() => { document.location.reload() }}>
|
||||
<Alert.Heading>Error fetching gatherings</Alert.Heading>
|
||||
<p>
|
||||
This error should never happen, try refreshing the page!
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
If this happens again, contact the developers on the <Alert.Link href="https://discord.gg/pretendo">Pretendo Discord</Alert.Link>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
} else {
|
||||
if (allGatherings.length > 0) {
|
||||
return (
|
||||
<div className="d-flex flex-wrap justify-content-center">
|
||||
{
|
||||
allGatherings.map((gat, idx) => {
|
||||
return (
|
||||
<div className="ms-5 me-5 mb-3" key={idx}>
|
||||
<GatheringEntry gathering={gat} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Alert variant="primary" onClick={() => { document.location.reload() }}>
|
||||
<Alert.Heading>No gatherings!</Alert.Heading>
|
||||
<hr />
|
||||
<p>
|
||||
This means no players are currently in a matchmaking session, join the <Alert.Link href="https://discord.gg/pretendo">Pretendo Discord</Alert.Link> to communicate with other players.
|
||||
</p>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container-fluid m-5 h-100 d-flex justify-content-center align-items-center">
|
||||
<form className="form-floating text-center w-100 bg-light p-5 rounded-1 shadow-lg">
|
||||
<h6><small className="text-muted">Refreshing in {timer} seconds ...</small></h6>
|
||||
<h1 className="mb-3">Gatherings</h1>
|
||||
<div>
|
||||
{getPageJSX()}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
107
app/globals.css
107
app/globals.css
|
@ -1,107 +0,0 @@
|
|||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
|
||||
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
|
||||
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
|
||||
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
--primary-glow: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
#16abff33 0deg,
|
||||
#0885ff33 55deg,
|
||||
#54d6ff33 120deg,
|
||||
#0071ff33 160deg,
|
||||
transparent 360deg
|
||||
);
|
||||
--secondary-glow: radial-gradient(
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 239, 245, 249;
|
||||
--tile-end-rgb: 228, 232, 233;
|
||||
--tile-border: conic-gradient(
|
||||
#00000080,
|
||||
#00000040,
|
||||
#00000030,
|
||||
#00000020,
|
||||
#00000010,
|
||||
#00000010,
|
||||
#00000080
|
||||
);
|
||||
|
||||
--callout-rgb: 238, 240, 241;
|
||||
--callout-border-rgb: 172, 175, 176;
|
||||
--card-rgb: 180, 185, 188;
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
|
||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
||||
--secondary-glow: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(
|
||||
#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80
|
||||
);
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,24 @@
|
|||
import './globals.css'
|
||||
import { Inter } from 'next/font/google'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
import { NEXStatus } from '@/components/NEXStatus';
|
||||
import NavigationBar from '@/components/NavigationBar';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
title: 'Pretendo MK8',
|
||||
description: 'Pretendo Mario Kart 8 website',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div>
|
||||
<NavigationBar />
|
||||
<div className="mt-0 container-fluid d-flex justify-content-center">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<NEXStatus />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
width: var(--max-width);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: '';
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
}
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5)
|
||||
);
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
98
app/page.tsx
98
app/page.tsx
|
@ -1,95 +1,5 @@
|
|||
import Image from 'next/image'
|
||||
import styles from './page.module.css'
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.description}>
|
||||
<p>
|
||||
Get started by editing
|
||||
<code className={styles.code}>app/page.tsx</code>
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{' '}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className={styles.vercelLogo}
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.center}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Docs <span>-></span>
|
||||
</h2>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Learn <span>-></span>
|
||||
</h2>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Templates <span>-></span>
|
||||
</h2>
|
||||
<p>Explore the Next.js 13 playground.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2>
|
||||
Deploy <span>-></span>
|
||||
</h2>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
export default function HomePage() {
|
||||
return <h1>Hello, Dashboard Page!</h1>
|
||||
}
|
3
app/rankings/page.tsx
Normal file
3
app/rankings/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default async function RankingsPage() {
|
||||
return <h1>Hello, Rankings Page!</h1>
|
||||
}
|
3
app/tournaments/page.tsx
Normal file
3
app/tournaments/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default async function TournamentsPage() {
|
||||
return <h1>Hello, Tournaments Page!</h1>
|
||||
}
|
6
compile_proto.sh
Normal file
6
compile_proto.sh
Normal file
|
@ -0,0 +1,6 @@
|
|||
./node_modules/.bin/grpc_tools_node_protoc \
|
||||
--plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
|
||||
--ts_proto_out=./helpers/proto \
|
||||
--ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,esModuleInterop=true \
|
||||
--proto_path=./helpers/proto \
|
||||
./helpers/proto/amkj_service.proto
|
56
components/GatheringEntry.tsx
Normal file
56
components/GatheringEntry.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { Gathering } from '@/helpers/proto/amkj_service';
|
||||
import { Card, OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import Toast from 'react-bootstrap/Toast';
|
||||
import ToastContainer from 'react-bootstrap/ToastContainer';
|
||||
|
||||
export interface GatheringEntryProps {
|
||||
gathering: Gathering;
|
||||
}
|
||||
|
||||
interface Player {
|
||||
pid: number;
|
||||
guests: number;
|
||||
}
|
||||
|
||||
export const GatheringEntry: React.FC<GatheringEntryProps> = ({ gathering }) => {
|
||||
|
||||
const playerList: Player[] = [];
|
||||
gathering.players.forEach((player) => {
|
||||
const res = playerList.find(p => p.pid === Math.abs(player));
|
||||
if (res) {
|
||||
res.guests += 1;
|
||||
} else {
|
||||
playerList.push({ pid: player, guests: 0 });
|
||||
}
|
||||
});
|
||||
|
||||
console.log(playerList);
|
||||
|
||||
const popover = (
|
||||
<Popover id="popover-basic" className="text-center">
|
||||
<Popover.Body>
|
||||
<ul>
|
||||
{
|
||||
playerList.map((player, idx) => {
|
||||
return <li key={`${gathering.gid}_${idx}`}>PID {player.pid} {(player.guests > 0) ? ` (+${player.guests} guest)` : ""}</li>;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ width: '18rem' }}>
|
||||
<Card.Header className="text-center">Gathering {gathering.gid}</Card.Header>
|
||||
<Card.Body>
|
||||
|
||||
</Card.Body>
|
||||
<OverlayTrigger trigger={["hover", "focus"]} placement="bottom" overlay={popover}>
|
||||
<Card.Header className="text-center">{gathering.players.length} player{gathering.players.length > 1 && 's'}</Card.Header>
|
||||
</OverlayTrigger>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
136
components/NEXStatus.tsx
Normal file
136
components/NEXStatus.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link';
|
||||
import { GetServerStatusResponse } from '@/helpers/proto/amkj_service';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Spinner from 'react-bootstrap/Spinner';
|
||||
import { StaticPopover } from './StaticPopover';
|
||||
|
||||
export const NEXStatus = () => {
|
||||
|
||||
const [statusResponse, setStatusResponse] = useState<Response | null>(null);
|
||||
const [nexStatus, setNEXStatus] = useState<GetServerStatusResponse | null>(null);
|
||||
|
||||
const fetchNEXStatus = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/status', { cache: "no-store" });
|
||||
setStatusResponse(response);
|
||||
|
||||
const data = await response.json();
|
||||
setNEXStatus(data as GetServerStatusResponse);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch NEX server status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchNEXStatus();
|
||||
const interval = setInterval(fetchNEXStatus, 10000);
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
if (!statusResponse) {
|
||||
return (
|
||||
<StaticPopover
|
||||
title={
|
||||
<strong className="me-auto">Fetching server status</strong>
|
||||
}
|
||||
body={
|
||||
<Spinner animation="border" role="status" />
|
||||
} />
|
||||
);
|
||||
} else {
|
||||
if (statusResponse.status != 200 || !nexStatus) {
|
||||
return (
|
||||
<StaticPopover
|
||||
title={
|
||||
<strong className="me-auto">🔴 Error fetching server status</strong>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<span>The problem is on our side:</span>
|
||||
<ul className="mt-3">
|
||||
<li>The NEX server may be down</li>
|
||||
<li>Server may be misconfigured</li>
|
||||
</ul>
|
||||
</>
|
||||
} />
|
||||
);
|
||||
} else {
|
||||
if (nexStatus.isMaintenance) {
|
||||
const startMaintenanceDate = new Date(nexStatus.startMaintenanceTime as Date);
|
||||
const endMaintenanceDate = new Date(nexStatus.endMaintenanceTime as Date);
|
||||
const currentDate = new Date();
|
||||
|
||||
let numSeconds = Math.floor((endMaintenanceDate.getTime() - currentDate.getTime()) / 1000);
|
||||
let numMinutes = Math.floor(numSeconds / 60);
|
||||
|
||||
if (numSeconds > 0) {
|
||||
if (numMinutes > 60) {
|
||||
let numHours = Math.floor(numMinutes / 60);
|
||||
var text = <span>🟠 {`Maintenance is over in ${numHours}h${numMinutes % 60}min`}</span>;
|
||||
} else {
|
||||
var text = <span>🟠 {`Maintenance is over in ${numMinutes}min`}</span>;
|
||||
}
|
||||
} else {
|
||||
numSeconds = Math.abs(numSeconds);
|
||||
numMinutes = Math.floor(numSeconds / 60);
|
||||
if (numMinutes > 60) {
|
||||
let numHours = Math.floor(numMinutes / 60);
|
||||
var text = <span>🟠 {`Maintenance has been extended by ${numHours}h${numMinutes % 60}min`}</span>;
|
||||
} else {
|
||||
var text = <span>🟠 {`Maintenance has been extended by ${numMinutes}min`}</span>;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<StaticPopover
|
||||
title={
|
||||
<strong className="me-auto">{text}</strong>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<span>The server is aware of these maintenance times:</span>
|
||||
<ul className="mt-3">
|
||||
<li>Start: <strong>{startMaintenanceDate.toLocaleString()}</strong></li>
|
||||
<li>End: <strong>{endMaintenanceDate.toLocaleString()}</strong></li>
|
||||
</ul>
|
||||
</>
|
||||
} />
|
||||
);
|
||||
} else if (nexStatus.isWhitelist) {
|
||||
return (
|
||||
<StaticPopover
|
||||
title={
|
||||
<strong className="me-auto">⚫ Whitelist mode</strong>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<span>Only allowed users can join the server</span>
|
||||
<ul className="mt-3">
|
||||
<li>This mode was activated by an admin</li>
|
||||
<li>This is different from our functionality where only supporters can access the server</li>
|
||||
<li>This is activated for private testing and development only</li>
|
||||
</ul>
|
||||
</>
|
||||
} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<StaticPopover
|
||||
title={
|
||||
<strong className="me-auto">🟢 {`Online with ${nexStatus.numClients} player${(nexStatus.numClients == 0 || nexStatus.numClients > 1) ? 's' : ''}`}</strong>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
The server is online, join the server on the
|
||||
<Link href="https://pretendo.network" style={{ textDecoration: "none" }}> Pretendo Network</Link>
|
||||
</>
|
||||
}
|
||||
align_center={true} />
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
104
components/NavigationBar.tsx
Normal file
104
components/NavigationBar.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Navbar, Nav } from 'react-bootstrap';
|
||||
import { usePathname, } from 'next/navigation';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Popover from 'react-bootstrap/Popover';
|
||||
import { MdLogin } from 'react-icons/md';
|
||||
import { isBrowser } from 'react-device-detect';
|
||||
|
||||
interface NavigationBarEntryProps {
|
||||
name: string;
|
||||
path: string;
|
||||
desc: string | JSX.Element;
|
||||
}
|
||||
|
||||
const NavigationBarEntry: React.FC<NavigationBarEntryProps> = ({ name, path, desc }) => {
|
||||
const pathname = usePathname();
|
||||
const isActive = pathname === path;
|
||||
|
||||
const popover = (
|
||||
<Popover id="popover-basic" className="text-center">
|
||||
<Popover.Header as="h3">{name}</Popover.Header>
|
||||
<Popover.Body>
|
||||
{desc}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayTrigger trigger={["hover", "focus"]} placement="bottom" overlay={popover}>
|
||||
<Link href={path} className="active nav-link me-5" style={{ fontWeight: (isActive ? 'bold' : 'inherit') }}>
|
||||
{name}
|
||||
</Link>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavigationBarProps { }
|
||||
|
||||
const NavigationBar: React.FC<NavigationBarProps> = ({ }) => {
|
||||
const pathname = usePathname();
|
||||
const getUserProfileJSX = () => {
|
||||
/*
|
||||
if (statusResponse) {
|
||||
if (statusResponse.headers.has("x-mk8-pretendo-imageurl")) {
|
||||
return (
|
||||
<Link href="#" className="nav-link me-5" prefetch={false} onClick={() => {
|
||||
fetch('/logout').then(() => window.location.reload());
|
||||
}}>
|
||||
<Image src={statusResponse.headers.get("x-mk8-pretendo-imageurl") as string} width={48} height={48} alt="Mii profile picture" className="mb-2" />
|
||||
<strong>{statusResponse.headers.get("x-mk8-pretendo-username")}</strong>
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link href={`https://pretendo.network/account/login?redirect=${window.location.origin + pathname}`} className="active nav-link me-5" prefetch={false}>
|
||||
<strong className="me-2">Login</strong>
|
||||
<MdLogin size={25} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}*/
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navbar expand="lg" style={{ borderBottom: '1px solid #e2edff', backgroundColor: '#e2edff' }}>
|
||||
<Navbar.Brand>
|
||||
<Link href='/' style={{ textDecoration: 'none' }}>
|
||||
<div className="d-flex gap-2 align-items-center ms-3">
|
||||
<div className="p-1">
|
||||
<Image src="/assets/pretendo-logo.png" alt="Pretendo logo" width={35} height={35} />
|
||||
</div>
|
||||
<div className="d-flex flex-column">
|
||||
<span className="text-muted fs-6">Mario Kart 8 Pretendo website</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Navbar.Brand>
|
||||
|
||||
{isBrowser && <div className="ms-2 vr" />}
|
||||
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
<Navbar.Collapse id="basic-navbar-nav" className="justify-content-between me-3 ms-2">
|
||||
<Nav className="ms-3">
|
||||
<NavigationBarEntry path="/" name="Home" desc="Main page of the website" />
|
||||
<NavigationBarEntry path="/gatherings" name="Gatherings" desc="List of matchmaking sessions (worldwide, regional, friend rooms, tournaments)" />
|
||||
<NavigationBarEntry path="/tournaments" name="Tournaments" desc="List of tournaments" />
|
||||
<NavigationBarEntry path="/rankings" name="Rankings" desc="Time trial rankings" />
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
|
||||
<Navbar.Collapse className="justify-content-end">
|
||||
{getUserProfileJSX()}
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavigationBar;
|
23
components/StaticPopover.tsx
Normal file
23
components/StaticPopover.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import Toast from 'react-bootstrap/Toast';
|
||||
import ToastContainer from 'react-bootstrap/ToastContainer';
|
||||
|
||||
export interface StaticPopoverProps {
|
||||
title: string | JSX.Element;
|
||||
body: string | JSX.Element;
|
||||
align_center?: boolean;
|
||||
}
|
||||
|
||||
export const StaticPopover: React.FC<StaticPopoverProps> = ({ title, body, align_center }) => {
|
||||
return (
|
||||
<ToastContainer className={"p-3" + (align_center ? " text-center" : "")} position="bottom-end" style={{ zIndex: 1, position: 'fixed' }}>
|
||||
<Toast>
|
||||
<Toast.Header closeButton={false}>
|
||||
{title}
|
||||
</Toast.Header>
|
||||
<Toast.Body>
|
||||
{body}
|
||||
</Toast.Body>
|
||||
</Toast>
|
||||
</ToastContainer>
|
||||
)
|
||||
}
|
8
helpers/grpc.ts
Normal file
8
helpers/grpc.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { createChannel, createClient, Metadata } from 'nice-grpc';
|
||||
import { AmkjServiceClient, AmkjServiceDefinition, GetServerStatusResponse } from '@/helpers/proto/amkj_service';
|
||||
import app_config from '@/app.config';
|
||||
|
||||
const channel = createChannel(`${app_config.grpc_host}:${app_config.grpc_port}`);
|
||||
const amkj_grpc_client: AmkjServiceClient = createClient(AmkjServiceDefinition, channel);
|
||||
|
||||
export { amkj_grpc_client };
|
151
helpers/proto/amkj_service.proto
Normal file
151
helpers/proto/amkj_service.proto
Normal file
|
@ -0,0 +1,151 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package amkj;
|
||||
|
||||
option go_package = "./;grpc_amkj";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
service AmkjService {
|
||||
rpc GetServerStatus(GetServerStatusRequest) returns (GetServerStatusResponse) {}
|
||||
|
||||
rpc StartMaintenance(StartMaintenanceRequest) returns (StartMaintenanceResponse) {}
|
||||
rpc EndMaintenance(EndMaintenanceRequest) returns (EndMaintenanceResponse) {}
|
||||
|
||||
rpc ToggleWhitelist(ToggleWhitelistRequest) returns (ToggleWhitelistResponse) {}
|
||||
rpc GetWhitelist(GetWhitelistRequest) returns (GetWhitelistResponse) {}
|
||||
rpc AddWhitelistUser(AddWhitelistUserRequest) returns (AddWhitelistUserResponse) {}
|
||||
rpc DelWhitelistUser(DelWhitelistUserRequest) returns (DelWhitelistUserResponse) {}
|
||||
|
||||
rpc GetAllUsers(GetAllUsersRequest) returns (GetAllUsersResponse) {}
|
||||
rpc KickUser(KickUserRequest) returns (KickUserResponse) {}
|
||||
rpc KickAllUsers(KickAllUsersRequest) returns (KickAllUsersResponse) {}
|
||||
|
||||
rpc GetAllGatherings(GetAllGatheringsRequest) returns (GetAllGatheringsResponse) {}
|
||||
rpc GetAllTournaments(GetAllTournamentsRequest) returns (GetAllTournamentsResponse) {}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message GetServerStatusRequest {}
|
||||
message GetServerStatusResponse {
|
||||
bool is_online = 1;
|
||||
bool is_maintenance = 2;
|
||||
bool is_whitelist = 3;
|
||||
int32 num_clients = 4;
|
||||
google.protobuf.Timestamp start_maintenance_time = 5;
|
||||
google.protobuf.Timestamp end_maintenance_time = 6;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message StartMaintenanceRequest {
|
||||
google.protobuf.Timestamp projected_utc_end_maintenance_time = 1;
|
||||
}
|
||||
message StartMaintenanceResponse {}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message EndMaintenanceRequest {}
|
||||
message EndMaintenanceResponse {}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message ToggleWhitelistRequest {}
|
||||
message ToggleWhitelistResponse {
|
||||
bool is_whitelist = 1;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message GetWhitelistRequest {}
|
||||
message GetWhitelistResponse {
|
||||
repeated uint32 pids = 1;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message AddWhitelistUserRequest {
|
||||
uint32 pid = 1;
|
||||
}
|
||||
message AddWhitelistUserResponse {}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message DelWhitelistUserRequest {
|
||||
uint32 pid = 1;
|
||||
}
|
||||
message DelWhitelistUserResponse {}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message GetAllUsersRequest {}
|
||||
message GetAllUsersResponse {
|
||||
repeated uint32 pids = 1;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message KickUserRequest {
|
||||
uint32 pid = 1;
|
||||
}
|
||||
message KickUserResponse {
|
||||
bool was_connected = 1;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message KickAllUsersRequest {}
|
||||
message KickAllUsersResponse {
|
||||
int32 num_kicked = 1;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message Gathering {
|
||||
uint32 gid = 1;
|
||||
uint32 host = 2;
|
||||
uint32 owner = 3;
|
||||
repeated uint32 attributes = 4;
|
||||
bytes app_data = 5;
|
||||
repeated int64 players = 6;
|
||||
uint32 min_participants = 7;
|
||||
uint32 max_participants = 8;
|
||||
}
|
||||
|
||||
message GetAllGatheringsRequest {
|
||||
uint32 offset = 1;
|
||||
int32 limit = 2;
|
||||
}
|
||||
|
||||
message GetAllGatheringsResponse {
|
||||
repeated Gathering gatherings = 1;
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
|
||||
message Tournament {
|
||||
uint32 id = 1;
|
||||
uint32 owner = 2;
|
||||
repeated uint32 attributes = 3;
|
||||
bytes app_data = 4;
|
||||
int64 total_participants = 5;
|
||||
int64 season_id = 6;
|
||||
string name = 7;
|
||||
string description = 8;
|
||||
string red_team = 9;
|
||||
string blue_team = 10;
|
||||
uint32 repeat_type = 11;
|
||||
uint32 gameset_num = 12;
|
||||
uint32 icon_type = 13;
|
||||
uint32 battle_time = 14;
|
||||
uint32 update_date = 15;
|
||||
}
|
||||
|
||||
message GetAllTournamentsRequest {
|
||||
uint32 offset = 1;
|
||||
int32 limit = 2;
|
||||
}
|
||||
message GetAllTournamentsResponse {
|
||||
repeated Tournament tournaments = 1;
|
||||
}
|
2225
helpers/proto/amkj_service.ts
Normal file
2225
helpers/proto/amkj_service.ts
Normal file
File diff suppressed because it is too large
Load diff
225
helpers/proto/google/protobuf/timestamp.ts
Normal file
225
helpers/proto/google/protobuf/timestamp.ts
Normal file
|
@ -0,0 +1,225 @@
|
|||
/* eslint-disable */
|
||||
import Long from "long";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "google.protobuf";
|
||||
|
||||
/**
|
||||
* A Timestamp represents a point in time independent of any time zone or local
|
||||
* calendar, encoded as a count of seconds and fractions of seconds at
|
||||
* nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||
* January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||
* Gregorian calendar backwards to year one.
|
||||
*
|
||||
* All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
|
||||
* second table is needed for interpretation, using a [24-hour linear
|
||||
* smear](https://developers.google.com/time/smear).
|
||||
*
|
||||
* The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
|
||||
* restricting to that range, we ensure that we can convert to and from [RFC
|
||||
* 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* Example 1: Compute Timestamp from POSIX `time()`.
|
||||
*
|
||||
* Timestamp timestamp;
|
||||
* timestamp.set_seconds(time(NULL));
|
||||
* timestamp.set_nanos(0);
|
||||
*
|
||||
* Example 2: Compute Timestamp from POSIX `gettimeofday()`.
|
||||
*
|
||||
* struct timeval tv;
|
||||
* gettimeofday(&tv, NULL);
|
||||
*
|
||||
* Timestamp timestamp;
|
||||
* timestamp.set_seconds(tv.tv_sec);
|
||||
* timestamp.set_nanos(tv.tv_usec * 1000);
|
||||
*
|
||||
* Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||
*
|
||||
* FILETIME ft;
|
||||
* GetSystemTimeAsFileTime(&ft);
|
||||
* UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
||||
*
|
||||
* // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
|
||||
* // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
|
||||
* Timestamp timestamp;
|
||||
* timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
|
||||
* timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
|
||||
*
|
||||
* Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||
*
|
||||
* long millis = System.currentTimeMillis();
|
||||
*
|
||||
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
|
||||
* .setNanos((int) ((millis % 1000) * 1000000)).build();
|
||||
*
|
||||
* Example 5: Compute Timestamp from Java `Instant.now()`.
|
||||
*
|
||||
* Instant now = Instant.now();
|
||||
*
|
||||
* Timestamp timestamp =
|
||||
* Timestamp.newBuilder().setSeconds(now.getEpochSecond())
|
||||
* .setNanos(now.getNano()).build();
|
||||
*
|
||||
* Example 6: Compute Timestamp from current time in Python.
|
||||
*
|
||||
* timestamp = Timestamp()
|
||||
* timestamp.GetCurrentTime()
|
||||
*
|
||||
* # JSON Mapping
|
||||
*
|
||||
* In JSON format, the Timestamp type is encoded as a string in the
|
||||
* [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||
* format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
|
||||
* where {year} is always expressed using four digits while {month}, {day},
|
||||
* {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
|
||||
* seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||
* are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||
* is required. A proto3 JSON serializer should always use UTC (as indicated by
|
||||
* "Z") when printing the Timestamp type and a proto3 JSON parser should be
|
||||
* able to accept both UTC and other timezones (as indicated by an offset).
|
||||
*
|
||||
* For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
|
||||
* 01:30 UTC on January 15, 2017.
|
||||
*
|
||||
* In JavaScript, one can convert a Date object to this format using the
|
||||
* standard
|
||||
* [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||
* method. In Python, a standard `datetime.datetime` object can be converted
|
||||
* to this format using
|
||||
* [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
|
||||
* the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
|
||||
* the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||
* http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D
|
||||
* ) to obtain a formatter capable of generating timestamps in this format.
|
||||
*/
|
||||
export interface Timestamp {
|
||||
/**
|
||||
* Represents seconds of UTC time since Unix epoch
|
||||
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||
* 9999-12-31T23:59:59Z inclusive.
|
||||
*/
|
||||
seconds: number;
|
||||
/**
|
||||
* Non-negative fractions of a second at nanosecond resolution. Negative
|
||||
* second values with fractions must still have non-negative nanos values
|
||||
* that count forward in time. Must be from 0 to 999,999,999
|
||||
* inclusive.
|
||||
*/
|
||||
nanos: number;
|
||||
}
|
||||
|
||||
function createBaseTimestamp(): Timestamp {
|
||||
return { seconds: 0, nanos: 0 };
|
||||
}
|
||||
|
||||
export const Timestamp = {
|
||||
encode(message: Timestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.seconds !== 0) {
|
||||
writer.uint32(8).int64(message.seconds);
|
||||
}
|
||||
if (message.nanos !== 0) {
|
||||
writer.uint32(16).int32(message.nanos);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): Timestamp {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseTimestamp();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.seconds = longToNumber(reader.int64() as Long);
|
||||
continue;
|
||||
case 2:
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.nanos = reader.int32();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): Timestamp {
|
||||
return {
|
||||
seconds: isSet(object.seconds) ? Number(object.seconds) : 0,
|
||||
nanos: isSet(object.nanos) ? Number(object.nanos) : 0,
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: Timestamp): unknown {
|
||||
const obj: any = {};
|
||||
message.seconds !== undefined && (obj.seconds = Math.round(message.seconds));
|
||||
message.nanos !== undefined && (obj.nanos = Math.round(message.nanos));
|
||||
return obj;
|
||||
},
|
||||
|
||||
create(base?: DeepPartial<Timestamp>): Timestamp {
|
||||
return Timestamp.fromPartial(base ?? {});
|
||||
},
|
||||
|
||||
fromPartial(object: DeepPartial<Timestamp>): Timestamp {
|
||||
const message = createBaseTimestamp();
|
||||
message.seconds = object.seconds ?? 0;
|
||||
message.nanos = object.nanos ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
declare var self: any | undefined;
|
||||
declare var window: any | undefined;
|
||||
declare var global: any | undefined;
|
||||
var tsProtoGlobalThis: any = (() => {
|
||||
if (typeof globalThis !== "undefined") {
|
||||
return globalThis;
|
||||
}
|
||||
if (typeof self !== "undefined") {
|
||||
return self;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
return window;
|
||||
}
|
||||
if (typeof global !== "undefined") {
|
||||
return global;
|
||||
}
|
||||
throw "Unable to locate global object";
|
||||
})();
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
function longToNumber(long: Long): number {
|
||||
if (long.gt(Number.MAX_SAFE_INTEGER)) {
|
||||
throw new tsProtoGlobalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
return long.toNumber();
|
||||
}
|
||||
|
||||
if (_m0.util.Long !== Long) {
|
||||
_m0.util.Long = Long as any;
|
||||
_m0.configure();
|
||||
}
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
66
helpers/types/JWTTokenPayload.ts
Normal file
66
helpers/types/JWTTokenPayload.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import type { NextRequest } from 'next/server'
|
||||
import app_config from '@/app.config';
|
||||
import { SignJWT, jwtVerify } from 'jose';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export type JWTTokenPayload = {
|
||||
access_level: number;
|
||||
server_access_level: string;
|
||||
pnid: string;
|
||||
pid: number;
|
||||
mii_image_url: string;
|
||||
}
|
||||
|
||||
export async function getMK8Token(request: NextRequest): Promise<JWTTokenPayload | null> {
|
||||
const mk8_token = request.cookies.get("mk8_token")?.value;
|
||||
if (!mk8_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const verifyResult = await jwtVerify(mk8_token, new TextEncoder().encode(app_config.jwt_secret));
|
||||
return verifyResult.payload as JWTTokenPayload;
|
||||
} catch (error) { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getMK8TokenFromAccountAPI(request: NextRequest): Promise<{ token: JWTTokenPayload, jwt_token: string } | null> {
|
||||
|
||||
const token_type = request.cookies.get("token_type")?.value;
|
||||
const access_token = request.cookies.get("access_token")?.value;
|
||||
|
||||
if (!token_type || !access_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
var res = await fetch("https://api.pretendo.cc/v1/user", {
|
||||
headers: {
|
||||
"Authorization": `${token_type} ${access_token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (res.status == 200) {
|
||||
const userData = await res.json();
|
||||
const token_data: JWTTokenPayload = {
|
||||
access_level: userData["access_level"],
|
||||
server_access_level: userData["server_access_level"],
|
||||
pnid: userData["username"],
|
||||
pid: userData["pid"],
|
||||
mii_image_url: userData["mii"]["image_url"],
|
||||
}
|
||||
|
||||
const token = await new SignJWT(token_data)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setJti(nanoid())
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('2h')
|
||||
.sign(new TextEncoder().encode(app_config.jwt_secret));
|
||||
|
||||
return { token: token_data, jwt_token: token };
|
||||
}
|
||||
} catch (error) { }
|
||||
|
||||
return null;
|
||||
}
|
87
middleware.ts
Normal file
87
middleware.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { type JWTTokenPayload, getMK8Token, getMK8TokenFromAccountAPI } from './helpers/types/JWTTokenPayload';
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
|
||||
if (request.nextUrl.pathname.startsWith('/logout')) {
|
||||
const referer: string | null = request.headers.get("referer");
|
||||
const url = new URL(referer ? referer : '/', request.url);
|
||||
|
||||
const response = NextResponse.redirect(url);
|
||||
response.cookies.set("mk8_token", "", { maxAge: 0, domain: ".pretendo.network" });
|
||||
response.cookies.set("access_token", "", { maxAge: 0, domain: ".pretendo.network" });
|
||||
response.cookies.set("refresh_token", "", { maxAge: 0, domain: ".pretendo.network" });
|
||||
response.cookies.set("token_type", "", { maxAge: 0, domain: ".pretendo.network" });
|
||||
return response;
|
||||
}
|
||||
|
||||
const hostname = request.nextUrl.hostname;
|
||||
const redirect_login_url = `https://${hostname.substring(hostname.indexOf('.') + 1)}/account/login?redirect=http://${hostname}`;
|
||||
|
||||
var mk8_token: JWTTokenPayload | null = await getMK8Token(request);
|
||||
let res;
|
||||
if (!mk8_token) {
|
||||
res = await getMK8TokenFromAccountAPI(request);
|
||||
if (!res && request.nextUrl.pathname.startsWith('/admin')) {
|
||||
const response = NextResponse.redirect(redirect_login_url);
|
||||
if (request.cookies.has("mk8_token")) {
|
||||
response.cookies.delete("mk8_token");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
if (res) {
|
||||
mk8_token = res.token;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.nextUrl.pathname.startsWith('/admin')) {
|
||||
if (mk8_token) {
|
||||
if (mk8_token.access_level < 3) {
|
||||
var response = NextResponse.redirect(new URL('/', request.url));
|
||||
} else {
|
||||
var response = NextResponse.next();
|
||||
}
|
||||
|
||||
response.headers.set("X-MK8-Pretendo-SAL", mk8_token.server_access_level);
|
||||
response.headers.set("X-MK8-Pretendo-Username", mk8_token.pnid);
|
||||
response.headers.set("X-MK8-Pretendo-ImageURL", mk8_token.mii_image_url);
|
||||
response.headers.set("X-MK8-Pretendo-PID", mk8_token.pid.toString());
|
||||
if (res) {
|
||||
response.cookies.set("mk8_token", res.jwt_token, { domain: ".pretendo.network" });
|
||||
}
|
||||
return response;
|
||||
} else {
|
||||
const response = NextResponse.redirect(redirect_login_url);
|
||||
if (request.cookies.has("mk8_token")) {
|
||||
response.cookies.delete("mk8_token");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
} else {
|
||||
const response = NextResponse.next();
|
||||
if (mk8_token) {
|
||||
response.headers.set("X-MK8-Pretendo-SAL", mk8_token.server_access_level);
|
||||
response.headers.set("X-MK8-Pretendo-Username", mk8_token.pnid);
|
||||
response.headers.set("X-MK8-Pretendo-ImageURL", mk8_token.mii_image_url);
|
||||
response.headers.set("X-MK8-Pretendo-PID", mk8_token.pid.toString());
|
||||
}
|
||||
if (res) {
|
||||
response.cookies.set("mk8_token", res.jwt_token, { domain: ".pretendo.network" });
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/',
|
||||
'/logout',
|
||||
'/api/:path*',
|
||||
'/admin/:path*',
|
||||
'/dashboard/:path*',
|
||||
'/tournaments/:path*',
|
||||
'/gatherings/:path*',
|
||||
'/rankings/:path*'
|
||||
]
|
||||
}
|
|
@ -1,4 +1,16 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
const nextConfig = {
|
||||
poweredByHeader: false,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'pretendo-cdn.b-cdn.net',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
|
1068
package-lock.json
generated
1068
package-lock.json
generated
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
@ -6,17 +6,30 @@
|
|||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"dev_pretendo": "next dev --hostname mario-kart-8.pretendo.network --port 80 -H 0.0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "20.3.1",
|
||||
"@types/react": "18.2.14",
|
||||
"@types/react-dom": "18.2.6",
|
||||
"bootstrap": "^5.3.0",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-config-next": "13.4.7",
|
||||
"jose": "^4.14.4",
|
||||
"long": "^5.2.3",
|
||||
"next": "13.4.7",
|
||||
"nice-grpc": "^2.1.4",
|
||||
"protobufjs": "^7.2.4",
|
||||
"react": "18.2.0",
|
||||
"react-bootstrap": "^2.8.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^4.10.1",
|
||||
"typescript": "5.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grpc-tools": "^1.12.4",
|
||||
"ts-proto": "^1.150.1"
|
||||
}
|
||||
}
|
||||
}
|
BIN
public/assets/pretendo-logo.png
Normal file
BIN
public/assets/pretendo-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
Before Width: | Height: | Size: 629 B |
Loading…
Reference in a new issue