Heute lernen wir, wie man eine Tennis-Trivia-App mit Next.js und Netlify erstellt. Dieser Technologie-Stack ist für mich bei vielen Projekten zur ersten Wahl geworden. Er ermöglicht schnelle Entwicklung und einfache Bereitstellung.
Ohne weitere Umschweife legen wir los!
Was wir verwenden
- Next.js
- Netlify
- TypeScript
- Tailwind CSS
Warum Next.js und Netlify
Man könnte denken, dass dies eine einfache App ist, die kein React-Framework benötigt. Die Wahrheit ist, dass Next.js mir eine Menge Funktionen auf Knopfdruck bietet, die es mir ermöglichen, einfach mit der Programmierung des Hauptteils meiner App zu beginnen. Dinge wie Webpack-Konfiguration, getServerSideProps und die automatische Erstellung von serverlosen Funktionen durch Netlify sind nur einige Beispiele.
Netlify macht auch das Bereitstellen eines Next.js-Git-Repositorys super einfach. Mehr zur Bereitstellung später.
Was wir bauen
Grundsätzlich werden wir ein Trivia-Spiel erstellen, das Ihnen zufällig den Namen eines Tennisspielers anzeigt und Sie müssen erraten, aus welchem Land dieser Spieler stammt. Es besteht aus fünf Runden und behält eine laufende Punktzahl bei, wie viele Sie richtig erraten haben.
Die Daten, die wir für diese Anwendung benötigen, sind eine Liste von Spielern zusammen mit ihrem Land. Anfangs dachte ich daran, eine Live-API abzufragen, aber nach reiflicher Überlegung entschied ich mich, einfach eine lokale JSON-Datei zu verwenden. Ich habe einen Snapshot von RapidAPI genommen und ihn in das Starter-Repository aufgenommen.
Das Endergebnis sieht ungefähr so aus

Die endgültige bereitgestellte Version auf Netlify können Sie hier finden.
Tour durch das Starter-Repository
Wenn Sie mitmachen möchten, können Sie dieses Repository klonen und dann zum start Branch wechseln.
git clone [email protected]:brenelz/tennis-trivia.git
cd tennis-trivia
git checkout start
In diesem Starter-Repository habe ich bereits etwas Boilerplate-Code geschrieben, um loszulegen. Ich habe eine Next.js-App mit dem Befehl npx create-next-app tennis-trivia erstellt. Anschließend habe ich manuell ein paar JavaScript-Dateien in .ts und .tsx geändert. Überraschenderweise hat Next.js automatisch erkannt, dass ich TypeScript verwenden möchte. Das war zu einfach! Ich habe auch Tailwind CSS mit diesem Artikel als Leitfaden konfiguriert.
Genug geredet, legen wir los!
Erste Einrichtung
Der erste Schritt ist die Einrichtung von Umgebungsvariablen. Für die lokale Entwicklung tun wir dies mit einer .env.local Datei. Sie können die .env.sample aus dem Starter-Repository kopieren.
cp .env.sample .env.local
Beachten Sie, dass sie derzeit einen Wert hat, nämlich den Pfad unserer Anwendung. Wir werden diesen im Frontend unserer App verwenden, daher müssen wir ihn mit NEXT_PUBLIC_ präfixieren.
Schließlich installieren wir mit den folgenden Befehlen die Abhängigkeiten und starten den Entwicklungsserver:
npm install
npm run dev
Nun greifen wir auf unsere Anwendung unter https://:3000 zu. Wir sollten eine ziemlich leere Seite mit nur einer Überschrift sehen.

Erstellung des UI-Markups
In pages/index.tsx fügen wir der bestehenden Home() Funktion den folgenden Markup hinzu.
export default function Home() {
return (
<div className="bg-blue-500">
<div className="max-w-2xl mx-auto text-center py-16 px-4 sm:py-20 sm:px-6 lg:px-8">
<h2 className="text-3xl font-extrabold text-white sm:text-4xl">
<span className="block">Tennis Trivia - Next.js Netlify</span>
</h2>
<div>
<p className="mt-4 text-lg leading-6 text-blue-200">
What country is the following tennis player from?
</p>
<h2 className="text-lg font-extrabold text-white my-5">
Roger Federer
</h2>
<form>
<input
list="countries"
type="text"
className="p-2 outline-none"
placeholder="Choose Country"
/>
<datalist id="countries">
<option>Switzerland</option>
</datalist>
<p>
<button
className="mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"
type="submit"
>
Guess
</button>
</p>
</form>
<p className="mt-4 text-lg leading-6 text-white">
<strong>Current score:</strong> 0
</p>
</div>
</div>
</div>
);
Dies bildet das Gerüst für unsere Benutzeroberfläche. Wie Sie sehen können, verwenden wir viele Utility-Klassen von Tailwind CSS, um die Dinge etwas schöner zu gestalten. Wir haben auch ein einfaches Autovervollständigungsfeld und eine Absendetaste. Hier wählen Sie das Land aus, von dem Sie glauben, dass der Spieler stammt, und drücken dann auf die Schaltfläche. Zuletzt gibt es am unteren Rand eine Punktzahl, die sich je nach richtigen oder falschen Antworten ändert.
Einrichtung unserer Daten
Wenn Sie sich den data Ordner ansehen, sollte dort eine Datei tennisPlayers.json mit allen Daten sein, die wir für diese Anwendung benötigen. Erstellen Sie einen lib Ordner im Stammverzeichnis und darin eine Datei players.ts. Denken Sie daran, dass die Dateiendung .ts erforderlich ist, da es sich um eine TypeScript-Datei handelt. Definieren wir einen Typ, der zu unseren JSON-Daten passt.
export type Player = {
id: number,
first_name: string,
last_name: string,
full_name: string,
country: string,
ranking: number,
movement: string,
ranking_points: number,
};
So erstellen wir einen Typ in TypeScript. Auf der linken Seite haben wir den Namen der Eigenschaft und auf der rechten Seite den Typ. Dies können einfache Typen oder sogar andere Typen selbst sein.
Von hier aus erstellen wir spezifische Variablen, die unsere Daten darstellen.
export const playerData: Player[] = require("../data/tennisPlayers.json");
export const top100Players = playerData.slice(0, 100);
const allCountries = playerData.map((player) => player.country).sort();
export const uniqueCountries = [...Array.from(new Set(allCountries))];
Ein paar Dinge, die man beachten sollte: Wir geben an, dass unsere playerData ein Array von Player-Typen ist. Dies wird durch den Doppelpunkt gefolgt vom Typ angezeigt. Tatsächlich, wenn wir über playerData schweben, können wir seinen Typ sehen.

In der letzten Zeile erhalten wir eine eindeutige Liste von Ländern, die wir für unsere Länderauswahlliste verwenden können. Wir übergeben unsere Länder an ein JavaScript Set, das doppelte Werte entfernt. Dann erstellen wir daraus ein Array und verteilen es in ein neues Array. Es mag unnötig erscheinen, aber dies wurde getan, um TypeScript zufriedenzustellen.
Ob Sie es glauben oder nicht, das sind wirklich alle Daten, die wir für unsere Anwendung benötigen!
Machen wir unsere Benutzeroberfläche dynamisch!
Alle unsere Werte sind derzeit fest codiert, aber ändern wir das. Die dynamischen Teile sind der Name des Tennisspielers, die Liste der Länder und die Punktzahl.
Zurück in pages/index.tsx modifizieren wir unsere getServerSideProps Funktion, um eine Liste von fünf zufälligen Spielern zu erstellen und unsere uniqueCountries Variable abzurufen.
import { Player, uniqueCountries, top100Players } from "../lib/players";
...
export async function getServerSideProps() {
const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());
const players = randomizedPlayers.slice(0, 5);
return {
props: {
players,
countries: uniqueCountries,
},
};
}
Alles, was sich improps Objekt befindet, das wir zurückgeben, wird an unsere React-Komponente übergeben. Verwenden wir diese auf unserer Seite.
type HomeProps = {
players: Player[];
countries: string[];
};
export default function Home({ players, countries }: HomeProps) {
const player = players[0];
...
}
Wie Sie sehen können, definieren wir einen weiteren Typ für unsere Seitenkomponente. Dann fügen wir den HomeProps Typ zur Home() Funktion hinzu. Wir haben wieder angegeben, dass players ein Array vom Typ Player ist.
Jetzt können wir diese Props weiter unten in unserer Benutzeroberfläche verwenden. Ersetzen Sie "Roger Federer" durch {player.full_name} (er ist übrigens mein Lieblings-Tennisspieler). Sie sollten eine schöne Autovervollständigung für die Spieler-Variable erhalten, da diese alle Eigenschaftsnamen auflistet, auf die wir dank der von uns definierten Typen zugreifen können.

Weiter unten aktualisieren wir die Liste der Länder wie folgt:
<datalist id="countries">
{countries.map((country, i) => (
<option key={i}>{country}</option>
))}
</datalist>
Nachdem wir zwei der drei dynamischen Teile in Position gebracht haben, müssen wir uns der Punktzahl widmen. Insbesondere müssen wir ein Stück Zustand für die aktuelle Punktzahl erstellen.
export default function Home({ players, countries }: HomeProps) {
const [score, setScore] = useState(0);
...
}
Sobald dies geschehen ist, ersetzen Sie die 0 durch {score} in unserer Benutzeroberfläche.
Sie können nun unseren Fortschritt überprüfen, indem Sie zu https://:3000 gehen. Sie sehen, dass jedes Mal, wenn die Seite neu geladen wird, ein neuer Name angezeigt wird; und wenn Sie in das Eingabefeld tippen, werden alle verfügbaren eindeutigen Länder aufgelistet.
Hinzufügen von Interaktivität
Wir sind schon weit gekommen, aber wir müssen etwas Interaktivität hinzufügen.
Verknüpfen des "Raten"-Buttons
Dafür benötigen wir eine Möglichkeit zu wissen, welches Land ausgewählt wurde. Dies tun wir, indem wir zusätzlichen Zustand hinzufügen und ihn mit unserem Eingabefeld verknüpfen.
export default function Home({ players, countries }: HomeProps) {
const [score, setScore] = useState(0);
const [pickedCountry, setPickedCountry] = useState("");
...
return (
...
<input
list="countries"
type="text"
value={pickedCountry}
onChange={(e) => setPickedCountry(e.target.value)}
className="p-2 outline-none"
placeholder="Choose Country"
/>
...
);
}
Als Nächstes fügen wir eine Funktion guessCountry hinzu und verknüpfen sie mit der Formularübermittlung.
const guessCountry = () => {
if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {
setScore(score + 1);
} else {
alert(‘incorrect’);
}
};
...
<form
onSubmit={(e) => {
e.preventDefault();
guessCountry();
}}
>
Wir vergleichen im Grunde nur das aktuelle Land des Spielers mit dem erratenen Land. Wenn wir nun zurück zur App gehen und das Land richtig erraten, erhöht sich die Punktzahl wie erwartet.
Hinzufügen eines Statusindikators
Um dies etwas schöner zu gestalten, können wir Benutzeroberflächenelemente rendern, je nachdem, ob die Vermutung richtig oder falsch ist.
Lassen Sie uns also ein weiteres Stück Zustand für den Status erstellen und die Methode guess country aktualisieren.
const [status, setStatus] = useState(null);
...
const guessCountry = () => {
if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {
setStatus({ status: "correct", country: player.country });
setScore(score + 1);
} else {
setStatus({ status: "incorrect", country: player.country });
}
};
Dann rendern Sie diese Benutzeroberfläche unter dem Spielernamen.
{status && (
<div className="mt-4 text-lg leading-6 text-white">
<p>
You are {status.status}. It is {status.country}
</p>
<p>
<button
autoFocus
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"
>
Next Player
</button>
</p>
</div>
)}
Zuletzt möchten wir sicherstellen, dass unser Eingabefeld nicht angezeigt wird, wenn wir uns in einem richtigen oder falschen Status befinden. Dies erreichen wir, indem wir das Formular mit Folgendem umschließen:
{!status && (
<form>
...
</form>
)}
Wenn wir nun zur App zurückkehren und das Land des Spielers erraten, erhalten wir eine nette Nachricht mit dem Ergebnis der Vermutung.
Fortschritt durch die Spieler
Nun kommt wahrscheinlich der schwierigste Teil: Wie wechseln wir von einem Spieler zum nächsten?
Als Erstes müssen wir currentStep im Zustand speichern, damit wir ihn mit einer Zahl von 0 bis 4 aktualisieren können. Wenn er dann 5 erreicht, möchten wir einen abgeschlossenen Zustand anzeigen, da das Trivia-Spiel vorbei ist.
Auch hier fügen wir die folgenden Zustandsvariablen hinzu.
const [currentStep, setCurrentStep] = useState(0);
const [playersData, setPlayersData] = useState(players);
…dann ersetzen wir unsere vorherige Spieler-Variable durch:
const player = playersData[currentStep];
Als Nächstes erstellen wir eine Funktion nextStep und verknüpfen sie mit der Benutzeroberfläche.
const nextStep = () => {
setPickedCountry("");
setCurrentStep(currentStep + 1);
setStatus(null);
};
...
<button
autoFocus
onClick={nextStep}
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"
>
Next Player
</button>
Wenn wir nun eine Vermutung abgeben und auf die Schaltfläche "Nächster Schritt" klicken, gelangen wir zu einem neuen Tennisspieler. Erraten Sie es erneut und wir sehen den nächsten usw.
Was passiert, wenn wir bei der letzten Spielerin auf "Weiter" klicken? Im Moment erhalten wir einen Fehler. Lassen Sie uns das beheben, indem wir eine Bedingung hinzufügen, die anzeigt, dass das Spiel abgeschlossen ist. Dies geschieht, wenn die Spieler-Variable undefiniert ist.
{player ? (
<div>
<p className="mt-4 text-lg leading-6 text-blue-200">
What country is the following tennis player from?
</p>
...
<p className="mt-4 text-lg leading-6 text-white">
<strong>Current score:</strong> {score}
</p>
</div>
) : (
<div>
<button
autoFocus
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto"
>
Play Again
</button>
</div>
)}
Jetzt sehen wir am Ende des Spiels einen schönen abgeschlossenen Zustand.
"Nochmal spielen"-Button
Wir sind fast fertig! Für unseren "Nochmal spielen"-Button wollen wir den Zustand des gesamten Spiels zurücksetzen. Wir wollen auch eine neue Spielerliste vom Server abrufen, ohne neu laden zu müssen. Das machen wir so:
const playAgain = async () => {
setPickedCountry("");
setPlayersData([]);
const response = await fetch(
process.env.NEXT_PUBLIC_API_URL + "/api/newGame"
);
const data = await response.json();
setPlayersData(data.players);
setCurrentStep(0);
setScore(0);
};
<button
autoFocus
onClick={playAgain}
className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto"
>
Play Again
</button>
Beachten Sie, dass wir die Umgebungsvariable, die wir zuvor über das process.env-Objekt eingerichtet haben, verwenden. Wir aktualisieren auch unsere playersData, indem wir unseren Serverzustand mit unserem gerade abgerufenen Client-Zustand überschreiben.
Wir haben unsere newGame-Route noch nicht ausgefüllt, aber das ist mit Next.js und Netlify Serverless Functions einfach. Wir müssen nur die Datei in pages/api/newGame.ts bearbeiten.
import { NextApiRequest, NextApiResponse } from "next"
import { top100Players } from "../../lib/players";
export default (req: NextApiRequest, res: NextApiResponse) => {
const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());
const top5Players = randomizedPlayers.slice(0, 5);
res.status(200).json({players: top5Players});
}
Dies sieht unserer getServerSideProps sehr ähnlich, da wir unsere schönen Hilfsvariablen wiederverwenden können.
Wenn wir zur App zurückkehren, stellen wir fest, dass der Button "Nochmal spielen" wie erwartet funktioniert.
Verbesserung der Fokus-Zustände
Eine letzte Sache, die wir tun können, um unsere Benutzererfahrung zu verbessern, ist das Setzen des Fokus auf das Länder-Eingabefeld, jedes Mal wenn sich der Schritt ändert. Das ist nur eine nette Geste und praktisch für den Benutzer. Dies tun wir mit einem ref und einem useEffect.
const inputRef = useRef(null);
...
useEffect(() => {
inputRef?.current?.focus();
}, [currentStep]);
<input
list="countries"
type="text"
value={pickedCountry}
onChange={(e) => setPickedCountry(e.target.value)}
ref={inputRef}
className="p-2 outline-none"
placeholder="Choose Country"
/>
Jetzt können wir viel einfacher navigieren, indem wir nur die Enter-Taste verwenden und ein Land eingeben.
Bereitstellung auf Netlify
Sie fragen sich vielleicht, wie wir das hier bereitstellen. Nun, mit Netlify ist das so einfach, dass es eine Next.js-Anwendung automatisch erkennt und konfiguriert.
Alles, was ich getan habe, war, ein GitHub-Repository einzurichten und mein GitHub-Konto mit meinem Netlify-Konto zu verbinden. Von dort wähle ich einfach ein Repository zur Bereitstellung aus und verwende alle Standardeinstellungen.

Das Einzige, was zu beachten ist, ist, dass Sie die Umgebungsvariable NEXT_PUBLIC_API_URL hinzufügen und erneut bereitstellen müssen, damit sie wirksam wird.

Meine endgültige bereitgestellte Version finden Sie hier.
Beachten Sie auch, dass Sie einfach auf den Button "Deploy to Netlify" im GitHub-Repository klicken können.
Fazit
Juhu, Sie haben es geschafft! Das war eine Reise und ich hoffe, Sie haben dabei etwas über React, Next.js und Netlify gelernt.
Ich habe Pläne, diese Tennis-Trivia-App in naher Zukunft um Supabase zu erweitern, also bleiben Sie dran!
Wenn Sie Fragen/Kommentare haben, können Sie mich gerne auf Twitter kontaktieren.
Schöner Artikel!
Ich habe eine Frage, rein aus Neugier: Ist das ein Tippfehler oder eine Methode, um einen Randfall abzudecken?
const uniqueCountries = [...Array.from(new Set(allCountries))];Statt
const uniqueCountries = Array.from(new Set(allCountries));Möglicherweise eine unbeabsichtigte Überkomplizierung von
[...new Set(allCountries)]!