This article will show you how to use the Trackmania API in a Next.js 13 app using TypeScript. The API uses access
and refresh
tokens, so it is not as easily accessible as other APIs, like jsonplaceholder
, or an API where you can include the same key(s) with every request.
So, we will use Supabase as a Postgres database to store the tokens and Prisma as our ORM of choice to communicate with our database.
The main focus of this article is to show you how to store / retrieve these tokens for API use, NOT for user authentication. If you’re looking for user authentication, I’d recommend looking at an example here over on NextAuth’s website to get you started.
Let’s get started!
Initial Setup
Quickly download and setup the latest Next.js TypeScript starter:
npx create-next-app@latest --ts .
If you are getting warnings in your CSS file complaining about unknown CSS rules, follow these steps here
Still in globals.css
, update the code with this reset from Josh Comeau
/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
Update tsconfig.json
to this:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "Node",
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Update src/app/page.tsx
to this:
// src/app/page.tsx
const HomePage = () => {
return (
<div>
<h1>HomePage</h1>
</div>
);
};
export default HomePage;
Update src/app/layout.tsx
to this:
// src/app/layout.tsx
import { Inter } from "next/font/google";
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
description: "Generated by create next app",
title: "Trackmania API Demo"
};
type RootLayoutProps = {
children: ReactNode;
};
const RootLayout = ({ children }: RootLayoutProps) => {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
};
export default RootLayout;
GitHub & Vercel
I’m a fan of setting up a GitHub repo and deploying to Vercel early, and committing + pushing often in smaller chunks
This way, if the site should not build on Vercel, it’s a lot easier to figure when the issue started occurring.
I’m going to assume that since you’re doing this advanced tutorial that you:
- Already know how to create a repo on GitHub and push your code up to it
- Deploy a website from your Vercel dashboard by selecting the repo you’ve created
Please do both of these things before continuing.
Getting API Access Token
In order to use Nadeo’s Trackmania API, we need to get an access_token
(which also comes with a refresh_token
)
There are 2 approaches to get this:
- Use a dedicated Ubisoft account
- Create Server Account on trackmania.com
While it is true that to login to trackmania.com, you need a Ubisoft account anyways, the flow for refreshing the access_token
is much simpler using a Server Account.
On trackmania.com, once you are signed in, click on your user name in the top right corner
In the dropdown, select My Server Accounts
Enter a value for the Server Login (you can think of this is a project name)
I’ll call mine: trackmania-api-demo
Click Submit
Once you do, a message will appear to Please copy the password
Copy the Login (the name you just entered, ie trackmania-api-demo
) and password into a text file for now
Let’s use Postman (or a similar platform of your choice) to send requests first to make sure things will work before coding it all up.
Create a new request with the following:
- Request type to
POST
- URL to https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic
- Under
Authorization
:- Set the
Type
toBasic Auth
- For the
Username
andPassword
, copy / paste the Login and Password fields from the Server Account
- Set the
- Under
Headers
, add- Content-Type:
application/json
- Content-Type:
-
Under
Body
- Set the type to
raw
, then selectJSON
- Add this:
{ "audience": "NadeoLiveServices" }
- NOTE: We have to include this JSON object in the body because we need a way of telling the API what to authenticate with. You can see a mapping of APIs and available audiences here.
- Set the type to
Click Send, and you should see a response like this:
{
"accessToken":"eyJhbGciOiJIUzI1NiIsImVudiI6InRyYWNrbWFuaWEtcHJvZCIsInZlciI6IjEifQ.PAYLOAD.SIGNATURE",
"refreshToken":"eyJhbGciOiJIUzI1NiIsImVudiI6InRyYWNrbWFuaWEtcHJvZCIsInZlciI6IjEifQ.PAYLOAD.SIGNATURE"
}
The start of the access_tokens
, known as the HEADER
, is always the same, so it’s safe to display them publicly like this. However, it’s the PAYLOAD
and SIGNATURE
parts of the tokens that are unique.
If you URL-base64-decode the PAYLOAD
part of the accessToken
, you get the following JSON object:
{
"jti": "UUID_HERE",
"iss": "NadeoServices",
"iat": 1595191016,
"rat": 1595192816,
"exp": 1595194616,
"aud": "NadeoLiveServices",
"usg": "Server",
"sid": "UUID_HERE",
"sub": "UUID_HERE",
"aun": "mm",
"rtk": false,
"pce": false
}
Here, exp
defines the expiration time, and rat
defines the time after which you are able to refresh the token.
To URL-base64-decode something in TypeScript, you can use a function like this:
const urlBase64Decode = (input: string) => {
// decode the base64 string
const base64Decoded = atob(input);
// URL-decode the data
const urlDecoded = decodeURIComponent(base64Decoded);
// parse the JSON data
return JSON.parse(urlDecoded);
};
Using the Access Token
To keep things simple, let’s try and fetch the data for a single map for now to make sure our access_token
is working
Let’s fetch data for the first map in the current campaign (which at the time of writing, is Summer 2023-01)
We will need send a GET request to the following URL:
<https://live-services.trackmania.nadeo.live/api/token/map/MAP_UID>
To get a MAP_UID
, head back to the official Trackmania site: https://www.trackmania.com/
Once there, click on Campaigns
Then click SUMMER 2023 - 01 on the next page
The URL will be this: https://www.trackmania.com/tracks/7hk8IflYsbMbpJv2gyYzx48Zvt7
The last part in the path, 7hk8IflYsbMbpJv2gyYzx48Zvt7
, is the MAP_UID
Back in Postman, create another request tab and set the following:
- Request type to
GET
- URL to https://live-services.trackmania.nadeo.live/api/token/map/7hk8IflYsbMbpJv2gyYzx48Zvt7
- Under
Headers
, add- Content-Type:
application/json
- Authorization:
nadeo_v1 t=YOUR_FULL_ACCESS_TOKEN
- Content-Type:
-
Under
Body
- Set the type to
raw
, then selectJSON
- Add this:
{ "audience": "NadeoLiveServices" }
- Set the type to
Click Send, and you should see a response like this:
{
"uid": "7hk8IflYsbMbpJv2gyYzx48Zvt7",
"mapId": "a93f902b-16a1-4448-ad34-65cf32d41425",
"name": "Summer 2023 - 01",
"author": "d2372a08-a8a1-46cb-97fb-23a161d85ad0",
"submitter": "d2372a08-a8a1-46cb-97fb-23a161d85ad0",
"authorTime": 23997,
"goldTime": 26000,
"silverTime": 29000,
"bronzeTime": 36000,
"nbLaps": 0,
"valid": false,
"downloadUrl": "https://prod.trackmania.core.nadeo.online/storageObjects/5e1cd15d-0b55-4db7-98ad-a82656ceb112",
"thumbnailUrl": "https://prod.trackmania.core.nadeo.online/storageObjects/fa836805-f906-41c1-9ca2-e52a63c4b44f.jpg",
"uploadTimestamp": 1687274471,
"updateTimestamp": 1687274472,
"fileSize": null,
"public": false,
"favorite": false,
"playable": true,
"mapStyle": "",
"mapType": "TrackMania\\TM_Race",
"collectionName": "Stadium",
"gamepadEditor": false
}
Hazah! If you’ve made it this far, congrats! This is a very good milestone to hit.
Planning Out the App
Ok, so now that we’ve tested some queries in Postman to ensure we get the responses we want, we should pause here and plan out how we want to do code wise:
- Store the
access_
andrefresh_token
in Supabase database - Check whether or not the current
access_token
is expired - If it is, make a request with the
refresh_token
to get a new one - Store the new
access_token
in the Supabase database
Most resources online I’ve seen suggest using a JWT to store the refresh_token
. However, using a JWT to store the refresh_token
is less secure than saving it in a database.
Instead, we will use Supabase to store our tokens, and use Prisma as our ORM of choice to read and write data to / from the database.
Install and Setup Prisma
First, let’s install Prisma as a devDependency
npm i -D prisma
Next, we can initialize Prisma by running:
npx prisma init
This will create an .env
file as well as a prisma folder, both at the root level
NOTES:
- Do NOT change
.env
file name to.env.local
, it must stay as.env
- The
DATABASE_URL
will need to be updated with the URL you get when creating a DB with Supabase later on
Setup PostgreSQL on Supabase
Create an account if you do not have one already
Once logged in, go to your Dashboard
On the dashboard page, click New project
Give it a name, something in kebal-case-like-this
Click generate a password
Copy and paste the generated password into the .env file as a comment for now
Change your region accordingly
Click Create new project
Navigate to the project settings (gear icon, very bottom of sidebar nav)
Click on Database
Under Connection string, click URI
Copy this value
Back in the .env
file, replace the default DATABASE_URL
with the URI you just copied.
Also update the [YOUR-PASSWORD]
placeholder with the generated password from before.
Back in Supabase, click on the Database icon.
Under tables, there should be nothing there. But not for long!
Create Prisma Model Schema
Open the schema.prisma
file generated earlier
We’ll add a simple ApiToken
model:
model ApiToken {
tokenId String @id @default(cuid())
accessToken String
expirationDate DateTime
refreshToken String
}
This will be sufficient because we are not performing any auth, we are simply saving the keys in a secure location for when we want to reach out to the Trackmania API
While we’re at it, let’s also update the .env
file with Trackmania Server Account login and password from earlier:
DATABASE_URL="..."
TM_SERVER_ACCOUNT_LOGIN="..."
TM_SERVER_ACCOUNT_PASSWORD="..."
We’ll need these whenever we make the initial POST request to get the tokens
Now we need to migrate
- NOTE: also any time the schema is changed, you’ll have to migrate
Run this command:
npx prisma migrate dev --name init
# "init" at the end is the name of the migration
Go back to Supabase, and click on the Table Editor icon
You should now see a User table!
Install Prisma Client
Link to Prisma Client docs here
Install it:
npm install @prisma/client
Next, inside the src
folder, create a folder called lib
In the lib
folder, create a file called prisma.ts
There are some common / well known issues when working with Next.js and Prisma
There is a docs page about it here
Copy and paste this snippet (taken from the docs) into prisma.ts
:
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
}
type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClientSingleton | undefined
}
const prisma = globalForPrisma.prisma ?? prismaClientSingleton()
export default prisma
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
What this code does is when we are not in the production environment, and the Prisma client has already run, the PrismaClient won’t be initialized again and again every time we restart the server.
At this point, it’s a good idea to update our Environment Variables on Vercel with the values we’ve added.
From your dashboard, select the project you’ve created / deployed.
Click on Settings, then Environment Variables.
Add them here (you can actually copy / paste multiple at a time!).
Adding a Row in Supabase
Head over to Supabase, and enter in a row manually.
Perform a POST
request in Postman to get an up-to-date access
and refresh
token
You can then use the urlBase64Decode
function shown earlier to get the exp
value
You can then use this function to create a Date string from that value:
const convertUnixTimestampToDate = (timestamp) => new Date(timestamp * 1000);
// example
convertUnixTimestampToDate(1695344641)
"Thu Sep 21 2023 18:04:01 GMT-0700 (Pacific Daylight Time)"
Use this value for the expirationDate
For the tokenId
, you can also use this generateCUID
function in a browser console to generate an id for you (thanks ChatGPT!):
function generateCUID() {
// Define a list of characters that can be used in the CUID
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// Length of the CUID
const length = 25;
let cuid = 'c'; // Start with the letter 'c' to mark it as a CUID
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
cuid += characters[randomIndex];
}
return cuid;
}
console.log(generateCUID());
// cVuBo6oMxjNbc7WKpMrT6lqQg3
Using Next.js middleware
Since we need to always be checking whether or not the access_token
is valid or not, we need some code to run on every page
A simple way to implement this is by using middleware
Inside the src
directory, create a file called middleware.ts
Add this code to it for now:
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// This function can be marked `async` if using `await` inside
const middleware = (request: NextRequest) => {
console.log("middleware is running!");
// return NextResponse.json(request)
};
export { middleware };
Then if you run npm run dev
, you should see the console.log
when you visit / refresh the home page.
Next, let’s update this code to fetch the example row from Supabase we added earlier:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import prisma from "./lib/prisma";
// This function can be marked `async` if using `await` inside
const middleware = async (request: NextRequest) => {
// console.log("middleware running!!!");
// return NextResponse.json(request)
const apiTokenTest = await prisma.apiToken.findUnique({
where: {
tokenId: "c1Z1oSjpTT3REWxoeuK0xcWQBj"
}
});
console.log("apiTokenTest: ", apiTokenTest);
};
export { middleware };
However, when we do this, we will get this error:
- Error: PrismaClient is unable to run in Vercel Edge Functions. As an alternative, try Accelerate: https://pris.ly/d/accelerate. If this is unexpected, please open an issue: https://github.com/prisma/prisma/issues
At the time of writing, as the error says, PrismaClient
is unable to run in Vercel Edge Functions.
Thankfully, it is being discussed and it looks like a solution is being worked on.
To get around this, we can use Prisma DataProxy.
To start, head over to https://cloud.prisma.io/ and create an account with your GitHub login.
Once you’ve authorized Prisma, go to your projects dashboard here (you also might automatically be directed to the create a project flow).
Click on Create classic project.
Copy and paste your DATABASE_URL
from the .env
file into the Connection string field.
Click Create project.
Add your GitHub profile if not selectable.
Select your repo, and make sure the branch is set to main
.
Create a Data Proxy connection string, give it a name.
Copy the value, and head back to the code and open the .env
file.
Rename the existing DATABASE_URL
variable to MIGRATE_DATABASE_URL
.
Then create a DATABASE_URL
, this time setting it to the proxy connection string you just created.
The MIGRATE_DATABASE_URL
will be used to apply and pending migration to the database as the app will be deployed on Vercel.
Next, open up the terminal and run:
npx prisma generate --data-proxy
This will generate a Prisma client that will use Prisma data proxy
Next, go to prisma.ts
and adjust the import path of the PrismaClient
like so:
// before
import { PrismaClient } from "@prisma/client";
// after
import { PrismaClient } from "@prisma/client/edge";
After that, run npm run dev in the terminal, and you should now see the response:
apiTokenTest: {
tokenId: 'TOKEN_ID_HERE',
accessToken: 'ACCESS_TOKEN_HERE',
expirationDate: 2023-09-21T18:04:01.000Z,
refreshToken: 'REFRESH_TOKEN_HERE'
}
Updating the Prisma Schema
We should update the ApiToken
model to also have a addedOn
field
This will help us get the row in the database that was added most recently
model ApiToken {
tokenId String @id @default(cuid())
accessToken String
expirationDate DateTime
refreshToken String
addedOn DateTime @default(now())
}
We will also need to add a directUrl
property to the datasource db
:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("MIGRATE_DATABASE_URL") // <---
}
Again, the MIGRATE_DATABASE_URL
is the old DATABASE_URL
that was copied from Supabase.
Make sure to also add / update your Environment Variables on Vercel too!
Now we can run migrate command:
npx prisma migrate dev --name added_on_field
If you didn’t specify the directUrl
, you would get an error. So that is why we added it.
Link to the docs page about using Prisma Migrate here
Finally, we will need to add this script
to our package.json
(as per Prisma docs here):
"postinstall": "prisma generate"
We do this because you would run into the following error message on deployment otherwise:
Prisma has detected that this project was built on Vercel, which caches dependencies.
This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered.
To fix this, make sure to run the `prisma generate` command during the build process.
Learn how: <https://pris.ly/d/vercel-build>
Updating the middleware
So now we can begin writing the necessary code to check our tokens in the database
But first, let’s setup a simple request on the home page knowing it will fail:
const getMapInfo = async () => {
const mapUId = "7hk8IflYsbMbpJv2gyYzx48Zvt7";
const url = `https://live-services.trackmania.nadeo.live/api/token/map/${mapUId}`;
const res = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
if (!res.ok) {
const text = await res.text(); // get the response body for more information
throw new Error(`
Failed to fetch data
Status: ${res.status}
Response: ${text}
`);
}
return res.json();
};
const HomePage = async () => {
const mapInfo = await getMapInfo();
console.log("mapInfo: ", mapInfo);
return (
<div>
<h1>HomePage</h1>
<p>Track name:</p>
</div>
);
};
As you can see, we are missing the Authorization
along with the accessToken
. This is where middleware
can help us.
We can use it to set headers on requests. We will use it to set the access_token
on each request to the Trackmania API. In the end, it looks like this:
// src/middleware.ts
import { NextRequest, NextResponse } from "next/server";
import prisma from "./lib/prisma";
import { unixTimestampToDate, urlBase64Decode } from "./lib/utils";
import type { AuthTokenResp } from "./types";
const middleware = async (request: NextRequest) => {
try {
const mostRecentApiToken = await prisma.apiToken.findFirst({
orderBy: {
addedOn: "desc"
}
});
if (mostRecentApiToken) {
const { accessToken, expirationDate, refreshToken } = mostRecentApiToken;
const tokenExpirationDate = new Date(expirationDate);
const currentDate = new Date();
// compare the current date to the expiration date
if (currentDate > tokenExpirationDate) {
console.log("the token is expired - get a new one");
const url = "https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic";
const login = process.env.TM_SERVER_ACCOUNT_LOGIN;
const password = process.env.TM_SERVER_ACCOUNT_PASSWORD;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${btoa(`${login}:${password}`)}`
},
body: JSON.stringify({
audience: "NadeoLiveServices"
})
});
if (!res.ok) {
const text = await res.text(); // get the response body for more information
throw new Error(`
Failed to fetch data
Status: ${res.status}
Response: ${text}
`);
}
const newTokens: AuthTokenResp = await res.json();
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = newTokens;
const [header, payload, structure] = newAccessToken.split(".");
const { exp, rat } = urlBase64Decode(payload);
const newExpirationDate = unixTimestampToDate(rat);
await prisma.apiToken.create({
data: {
accessToken: newAccessToken,
expirationDate: newExpirationDate,
refreshToken: newRefreshToken,
addedOn: new Date()
}
});
}
console.log("the token is valid - you good!");
// set a new response header "Authorization"
const response = NextResponse.next();
response.headers.set("Authorization", `nadeo_v1 t=${accessToken}`);
return response;
}
} catch (error) {
return NextResponse.json(
{
message: "Something went wrong!"
},
{
status: 500
}
);
}
};
export { middleware };
That’s quite a lot! Let’s break it down:
- First, we the most recent row in the database by using the
addedOn
field - Then, we check we have something we can work with
- If we do, destructure the
accessToken
,expirationDate
, andrefreshToken
variables out ofmostRecentApiToken
- We then check if the
currentDate
is greater thantokenExpirationDate
-
If it is, that means the
access_token
is expired and we need to get a new one- We then make the necessary
POST
request, using the refresh token for theAuthorization
header - We then add on a type annotation of
AuthTokenResp
, seen below, to thenewTokens
:
// src/types/index.ts type AuthTokenResp = { accessToken: string; refreshToken: string; };
- When the destructure again, this time using aliases to avoid naming conflicts
- We then split the
access_token
into 3 parts, mostly just for thepayload
- We then run the payload through our
urlBase64Decode
function, which itself now has an updated return typeDecodedPayload
. That type looks like this:
// src/types/index.ts type DecodedPayload = { aud: string; aun: string; exp: number; iat: number; iss: string; jti: string; pce: boolean; rat: number; rtk: boolean; sid: string; sub: string; usg: string; };
- We can then destructure yet again, this time getting the value for
rat
- We then run the
rat
value through aunixTimestampToDate
conversion function, which looks like this:
const unixTimestampToDate = (unixTimestamp: number): Date => { return new Date(unixTimestamp * 1000); };
- Finally, after all that, we can properly setup the create call to add the new tokens to the database
- We then make the necessary
And outside this expiration check, we set new a
Authorization
header using the most recent access token
Whew!
Now we can go back to the home page and adjust our query:
// src/app/page.tsx
import { headers } from "next/headers";
import type { TrackmaniaTrack } from "@/types";
const getMapInfo = async (): Promise<TrackmaniaTrack> => {
const mapUId = "7hk8IflYsbMbpJv2gyYzx48Zvt7";
const url = `https://live-services.trackmania.nadeo.live/api/token/map/${mapUId}`;
const auth = headers().get("Authorization") || "";
const res = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: auth
}
});
if (!res.ok) {
const text = await res.text(); // get the response body for more information
throw new Error(`
Failed to fetch data
Status: ${res.status}
Response: ${text}
`);
}
return res.json();
};
const HomePage = async () => {
const mapInfo = await getMapInfo();
console.log("mapInfo: ", mapInfo);
return (
<div>
<h1>HomePage</h1>
<p>Track name: {mapInfo.name}</p>
</div>
);
};
Here, we leverage the added on headers using the headers
function from next/headers
. It is read only, but that is perfectly fine since we just want to include it with our request anyways. If you don’t include this, the request fails.
Here is what the TrackmaniaTrack
type looks like:
type TrackmaniaTrack = {
author: string;
authorTime: number;
bronzeTime: number;
collectionName: string;
downloadUrl: string;
favorite: boolean;
fileSize: null;
gamepadEditor: boolean;
goldTime: number;
mapId: string;
mapStyle: string;
mapType: string;
name: string;
nbLaps: number;
playable: boolean;
public: boolean;
silverTime: number;
submitter: string;
thumbnailUrl: string;
uid: string;
updateTimestamp: number;
uploadTimestamp: number;
valid: boolean;
};
And now, if you check the terminal, you should get the correct response, as well as the map name should appear on the screen!
Minor Adjustments
One thing I noticed was that sometimes the site errors out instead of using the most recent access_token
. The solution is to simply refresh the page. So, as a temporary fix, I’ve created some basic back up pages:
// src/app/error.tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
const RootErrorPage = () => {
const router = useRouter();
return (
<div>
<h1 className="text-4xl">Oops!</h1>
<p>Something went wrong there. You can try refreshing the page, or go back home.</p>
<button
className="bg-teal-600 text-white mr-6 py-2 px-4"
onClick={() => router.refresh()}
type="button"
>
Refresh
</button>
<Link className="border-teal-600 border-2 py-2 px-4" href="/">
Go Home
</Link>
</div>
);
};
export default RootErrorPage;
// src/app/not-found.tsx
import Link from "next/link";
const RootNotFound = () => {
return (
<div>
<h1 className="text-4xl">Oops!</h1>
<p>Looks like you tried accessing a page that doesn't exist.</p>
<Link className="border-teal-600 border-2 inline-block py-2 px-4" href="/">
Go Home
</Link>
</div>
);
};
export default RootNotFound;
I also created a root level loading.tsx
so something is shown while the site is loading, which provides a better UX:
// src/app/loading.tsx
const RootLoading = () => {
return <p>Loading...</p>;
};
export default RootLoading;
Outro
I cannot stress how helpful the documentation over on Openplanet was in building this app. Big thanks to them! You can also view a list of the most common endpoints here.
And that’s it! As stated in the beginning, this was mainly to demonstrate how to use Supabase to store and retrieve tokens in a secure fashion instead of using JWT for API use. Most resources I found were revolving around authorization / user accounts, not using just an API. And I wanted to push myself and reinforce what I’ve been learning lately, and I’m very proud of myself for figuring this out.
The next steps are, well, up to you! If you want to continue building out your own Trackmania web app, you now have the proper foundation to do so. If not, you have the knowledge for storing tokens if the API you are using has a similar setup.
You could perhaps display maps from the current season on the home page, and create dynamic routes to each of them. Then, on an individual map page, display some information about that map.
The next major feature would be to include NextAuth using Ubisoft as a custom provider. Buuuttt I’m gonna leave that for another day ;)
I hope you found this article helpful. As always, you can view the full source here in case you get stuck.
If you have any feedback for me on how I could improve the code, please let me know! This was only the second time I’ve built a project using this stack, so any suggestions you may have are greatly appreciated!
Cheers, and happy coding!
Top comments (0)