diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 42edd93..bb98ba3 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -14,9 +14,9 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: 16 + node-version: 20 cache: yarn - run: yarn install diff --git a/.gitignore b/.gitignore index 3c38c10..7e06756 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,6 @@ yarn-error.log* # Sentry .sentryclirc -public/sitemap*.xml \ No newline at end of file +public/sitemap*.xml +# Sentry Config File +.sentryclirc diff --git a/.prettierrc b/.prettierrc index 2abf6eb..707b59b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "semi": true, "importOrder": ["", "@/", "^[./]"], "importOrderSeparation": true, - "importOrderSortSpecifiers": true + "importOrderSortSpecifiers": true, + "plugins": ["@trivago/prettier-plugin-sort-imports"] } diff --git a/app/[sound_id]/audio-player.tsx b/app/[sound_id]/audio-player.tsx new file mode 100644 index 0000000..99da65c --- /dev/null +++ b/app/[sound_id]/audio-player.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { sendGAEvent } from "@next/third-parties/google"; + +/* eslint-disable jsx-a11y/media-has-caption */ + +export interface AudioSource { + ext: string; + mime: string; + url: string; +} + +interface AudioPlayerProps { + readonly soundId: string; + readonly sources: AudioSource[]; +} + +export const AudioPlayer = ({ soundId, sources }: AudioPlayerProps) => ( + +); diff --git a/app/[sound_id]/page.tsx b/app/[sound_id]/page.tsx new file mode 100644 index 0000000..f5bfadb --- /dev/null +++ b/app/[sound_id]/page.tsx @@ -0,0 +1,54 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; + +import { SAYINGS } from "@/constants/sayings"; +import { getSayingData } from "@/lib/sayings"; + +import { AudioPlayer } from "./audio-player"; +import { RandomSayings } from "./random-sayings"; + +interface PlayPageProps { + readonly params: { + sound_id: string; + }; +} + +export const generateMetadata = ({ params }: PlayPageProps): Metadata => { + const data = getSayingData(params.sound_id); + + if (!data) { + return {}; + } + + return { + title: `${data.text} - Duke Nukem Says`, + description: `Duke Nukem Says "${data.text}". Play Duke Nukem quotes on Duke Nukem Says!`, + }; +}; + +export const generateStaticParams = () => { + return SAYINGS.map((saying) => ({ + sound_id: saying.id, + })); +}; + +const PlayPage = ({ params }: PlayPageProps) => { + const data = getSayingData(params.sound_id); + + if (!data) { + notFound(); + } + + const { soundId, text, sources } = data; + + return ( + <> +

Duke Nukem Says...

+

{`"${text}"`}

+ + + + ); +}; + +export default PlayPage; diff --git a/components/RandomSayings.module.css b/app/[sound_id]/random-sayings.module.css similarity index 100% rename from components/RandomSayings.module.css rename to app/[sound_id]/random-sayings.module.css diff --git a/components/RandomSayings.tsx b/app/[sound_id]/random-sayings.tsx similarity index 75% rename from components/RandomSayings.tsx rename to app/[sound_id]/random-sayings.tsx index 70eae16..d27fe55 100644 --- a/components/RandomSayings.tsx +++ b/app/[sound_id]/random-sayings.tsx @@ -1,10 +1,12 @@ +"use client"; + import Link from "next/link"; import { useEffect, useState } from "react"; import type { Saying } from "@/constants/sayings"; import { randomSayings } from "@/lib/random-sayings"; -import styles from "./RandomSayings.module.css"; +import styles from "./random-sayings.module.css"; export const RandomSayings = () => { const [sayings, setSayings] = useState([]); @@ -19,15 +21,11 @@ export const RandomSayings = () => { diff --git a/app/global-error.tsx b/app/global-error.tsx new file mode 100644 index 0000000..0fdc756 --- /dev/null +++ b/app/global-error.tsx @@ -0,0 +1,25 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import Error from "next/error"; +import { useEffect } from "react"; + +interface GlobalErrorProps { + readonly error: Error; +} + +const GlobalError = ({ error }: GlobalErrorProps) => { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +}; + +export default GlobalError; diff --git a/styles/global.css b/app/global.css similarity index 88% rename from styles/global.css rename to app/global.css index e4c0367..cef5bb2 100644 --- a/styles/global.css +++ b/app/global.css @@ -1,5 +1,7 @@ body { - font: 18px "Verdana", sans-serif; + font: + 18px "Verdana", + sans-serif; background-color: #000000; } diff --git a/components/Layout.module.css b/app/layout.module.css similarity index 100% rename from components/Layout.module.css rename to app/layout.module.css diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..102dc7a --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,65 @@ +/* eslint-disable react/no-danger */ +import { GoogleAnalytics } from "@next/third-parties/google"; +import type { Metadata, Viewport } from "next"; +import Image from "next/image"; +import Link from "next/link"; + +import { WEBSITE_INFO } from "@/constants/website-info"; + +import Logo from "../public/logo.webp"; +import "./global.css"; +import styles from "./layout.module.css"; + +export const metadata: Metadata = { + openGraph: { + siteName: "Duke Nukem Says", + type: "website", + images: ["/logo.webp"], + }, + authors: [{ name: "Allan Legemaate" }], + manifest: "/manifest.json", + metadataBase: new URL(WEBSITE_INFO.url), +}; + +export const viewport: Viewport = { + themeColor: "#000", +}; + +interface LayoutProps { + readonly children: React.ReactNode; +} + +const Layout = ({ children }: LayoutProps) => ( + + + + + +