diff --git a/messages/en.json b/messages/en.json index 57dc734..35f5b9c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6,6 +6,11 @@ "complaint": "Complaint", "documents": "Documents" }, + "metaTags": { + "time": "Time", + "location": "Location", + "description": "A student organisation that aims to create an inclusive environment around gaming and e-sports for the students at UiO" + }, "buttons": { "back": "back", "flagImageAlt": "Norwegian flag" diff --git a/messages/no.json b/messages/no.json index af8d428..c623872 100644 --- a/messages/no.json +++ b/messages/no.json @@ -6,6 +6,11 @@ "complaint": "Klage", "documents": "Dokumenter" }, + "metaTags": { + "time": "Tid", + "location": "Sted", + "description": "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!" + }, "buttons": { "back": "tilbake", "flagImageAlt": "Britisk flagg" diff --git a/next.config.ts b/next.config.ts index 66e7bc6..0fdc31e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next" import createNextIntlPlugin from "next-intl/plugin" -const withNextIntl = createNextIntlPlugin() +const withNextIntl = createNextIntlPlugin("./src/i18n.ts") const nextConfig: NextConfig = { images: { diff --git a/src/app/blog/[slug]/page.module.css b/src/app/[locale]/blog/[slug]/page.module.css similarity index 100% rename from src/app/blog/[slug]/page.module.css rename to src/app/[locale]/blog/[slug]/page.module.css diff --git a/src/app/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx similarity index 93% rename from src/app/blog/[slug]/page.tsx rename to src/app/[locale]/blog/[slug]/page.tsx index b20dd21..d896cc2 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -1,4 +1,4 @@ -import Layout from "@/app/layout" +import Layout from "@/app/[locale]/layout" import Footer from "@/components/page-sections/footer" import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" @@ -9,6 +9,7 @@ import { getBlogPost } from "@/lib/sanity" import { PortableText, PortableTextComponents } from "@portabletext/react" import { Metadata, Viewport } from "next" +import { notFound } from "next/navigation" import { getLocale } from "next-intl/server" import Image from "next/image" @@ -17,6 +18,7 @@ import * as styles from "./page.module.css" interface BlogPageParams { params: Promise<{ slug: string + locale: string }> } @@ -39,12 +41,6 @@ export async function generateMetadata({ const { slug } = await params const data = await getBlogPost(slug) - if (!data) { - return { - title: "Blog Post Not Found | UiO Gaming", - } - } - return generateSeoMetadata({ title: data.title, description: data.excerpt, @@ -103,9 +99,7 @@ const BlogPost = async ({ params }: BlogPageParams) => { const data = await getBlogPost(slug) - if (!data) { - throw new Error("Blog post not found") - } + if (!data) return notFound() const createdAt = new Date(data._createdAt).toLocaleDateString(locale, { day: "numeric", @@ -136,7 +130,7 @@ const BlogPost = async ({ params }: BlogPageParams) => { {data.author?.image && ( {`Forfatteren } @@ -21,11 +25,8 @@ export async function generateMetadata({ const { slug } = await params const data = await getEvent(slug) - if (!data) { - return { - title: "Event Not Found | UiO Gaming", - } - } + const locale = await getLocale() + const t = await getTranslations("metaTags") const formatDate = (dateString: string) => { const date = new Date(dateString) @@ -38,12 +39,12 @@ export async function generateMetadata({ minute: "2-digit", timeZone: "Europe/Oslo", } - return date.toLocaleString("no", options) + return date.toLocaleString(locale, options) } return generateSeoMetadata({ title: data.title, - description: `Tid: ${formatDate(data.date)}\nSted: ${data.location}\n\n${data.description}`, + description: `${t("time")}: ${formatDate(data.date)}\n${t("location")}: ${data.location}\n\n${data.description}`, }) } @@ -57,9 +58,7 @@ const Event = async ({ params }: EventPageParams) => { const data = await getEvent(slug) - if (!data) { - throw new Error("Event not found") - } + if (!data) return notFound() const formatDate = (dateString: string) => { const date = new Date(dateString) @@ -77,21 +76,23 @@ const Event = async ({ params }: EventPageParams) => { } return ( -
- -
-

{data.title}

-
- -

{data.location}

+ +
+ +
+

{data.title}

+
+ +

{data.location}

+
+
+ +

{formatDate(data.date)}

+
+

{data.description}

-
- -

{formatDate(data.date)}

-
-

{data.description}

-
+ ) } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..a33eefe --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,32 @@ +// app/[locale]/layout.tsx +import { NextIntlClientProvider } from "next-intl" +import { getLocale } from "next-intl/server" + +import LanguageSelector from "@components/ui/languageSelector" +import UmamiAnalytics from "@/components/umamiAnalytics" +import { getAnalyticsConfig } from "@/lib/analyticsConfig" + +export default async function LocaleLayout({ + children, +}: { + children: React.ReactNode +}) { + const locale = (await getLocale()) as "no" | "en" + const messages = (await import(`../../../messages/${locale}.json`)).default + + const analyticsConfig = getAnalyticsConfig() + + return ( + + +
{children}
+ {analyticsConfig?.enabled && ( + + )} +
+ ) +} diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx similarity index 89% rename from src/app/page.tsx rename to src/app/[locale]/page.tsx index 43cd7f4..67d2da4 100644 --- a/src/app/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,4 +1,4 @@ -import Layout from "@/app/layout" +import Layout from "@/app/[locale]/layout" import About from "@/components/page-sections/about" import BoardMembers from "@/components/page-sections/boardMembers" @@ -13,7 +13,7 @@ import { Metadata, Viewport } from "next" export const revalidate = 3600 // 1 hour -export function generateMetadata(): Metadata { +export async function generateMetadata(): Promise { return generateSeoMetadata({}) } diff --git a/src/app/global-not-found.tsx b/src/app/global-not-found.tsx index 98cab81..3aeecbe 100644 --- a/src/app/global-not-found.tsx +++ b/src/app/global-not-found.tsx @@ -3,14 +3,17 @@ import Layout from "@/app/layout" import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import { Metadata, Viewport } from "next" +import { getTranslations } from "next-intl/server" import { useTranslations } from "next-intl" import Link from "next/link" import * as styles from "./global-not-found.module.css" -export function generateMetadata(): Metadata { +export async function generateMetadata(): Promise { + const t = await getTranslations("404") + return generateSeoMetadata({ - title: "404: Ikke funnet", + title: t("title"), description: "Uh oh :(", }) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3852315..0635cba 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,6 @@ import "@/app/globals.css" -import { LocaleProvider } from "@/components/localeProvider" -import UmamiAnalytics from "@/components/umamiAnalytics" - -import { getAnalyticsConfig } from "@/lib/analyticsConfig" - import type { Metadata } from "next" -import { getLocale } from "next-intl/server" export const metadata: Metadata = { title: "UiO Gaming", @@ -19,30 +13,14 @@ export const metadata: Metadata = { }, } -export default async function RootLayout({ +export default function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode -}>) { - const locale = (await getLocale()) as "no" | "en" - const messages = (await import(`../../messages/${locale}.json`)).default // TODO: duplicate of request.ts? - - const analyticsConfig = getAnalyticsConfig() - +}) { return ( - - - -
{children}
- {analyticsConfig?.enabled && ( - - )} -
- + + {children} ) } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..fe0937a --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation" + +export default function RootPage() { + redirect("/en/404") +} diff --git a/src/components/localeProvider.module.css b/src/components/localeProvider.module.css deleted file mode 100644 index d919919..0000000 --- a/src/components/localeProvider.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.button { - display: flex; - gap: 0.25rem; - - position: fixed; - top: 0.5rem; - right: 0.5rem; - z-index: 0; - - background: pink; - color: black; - border: 1px solid black; - font-size: 1.1rem; - border-radius: 0.5rem; - padding-inline: 0.5em; -} -.button:hover { - background: #ff7b7b; - color: white; -} - -.flag { - width: 1.2rem; -} diff --git a/src/components/localeProvider.tsx b/src/components/localeProvider.tsx deleted file mode 100644 index 4ec7441..0000000 --- a/src/components/localeProvider.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client" - -import { GB, NO } from "country-flag-icons/react/3x2" -import { NextIntlClientProvider } from "next-intl" -import { ReactNode, useEffect, useState } from "react" - -import * as styles from "./localeProvider.module.css" - -type LocaleProviderProps = { - locale: "no" | "en" - messages: Record - children: ReactNode -} - -export function LocaleProvider({ - locale: serverLocale, - messages, - children, -}: LocaleProviderProps) { - const [locale, setLocale] = useState<"no" | "en">(serverLocale) - const [loadedMessages, setLoadedMessages] = - useState>(messages) - - useEffect(() => { - const match = document.cookie.match(/(?:^|; )locale=([^;]+)/) - const saved = match ? decodeURIComponent(match[1]) : null - - if (saved && saved !== locale) { - setLocale(saved as "no" | "en") - import(`../../messages/${saved}.json`).then(module => { - setLoadedMessages(module.default) - }) - } - }, [locale]) - - const changeLocale = (newLocale: "no" | "en") => { - setLocale(newLocale) - document.cookie = `locale=${encodeURIComponent( - newLocale - )}; path=/; max-age=31536000; SameSite=Lax` - window.location.reload() - } - - return ( - - {children} - -
- {locale === "no" ? ( - - ) : ( - - )} -
-
- ) -} diff --git a/src/components/page-sections/events.tsx b/src/components/page-sections/events.tsx index 28fcded..f8c6df6 100644 --- a/src/components/page-sections/events.tsx +++ b/src/components/page-sections/events.tsx @@ -50,7 +50,7 @@ const Events = async () => { const mapEvents = (events: Event[]) => { if (events.length !== 0) { return events.map((event: Event) => ( - +

{event.title}

diff --git a/src/components/seo.tsx b/src/components/seo.tsx index 759127e..44bfb22 100644 --- a/src/components/seo.tsx +++ b/src/components/seo.tsx @@ -1,4 +1,5 @@ import { Metadata, Viewport } from "next" +import { getLocale, getTranslations } from "next-intl/server" type SeoProps = { title?: string @@ -7,16 +8,17 @@ type SeoProps = { author?: string } -export function generateSeoMetadata({ +export async function generateSeoMetadata({ title, description, imageURL, author, -}: SeoProps): Metadata { +}: SeoProps): Promise { + const t = await getTranslations("metaTags") + const locale = await getLocale() + const defaultTitle = "UiO Gaming" - const metaDescription = - description || - "En hobbyforening som ønsker skape et sosialt miljö rundt gaming og e-sport på UiO!" + const metaDescription = description || t("description") const siteUrl = "https://uiogaming.no" const themeColor = "#7d36e7" @@ -24,11 +26,16 @@ export function generateSeoMetadata({ title: title ? `${title} | ${defaultTitle}` : defaultTitle, description: metaDescription, authors: author ? [{ name: author }] : [{ name: "UiO Gaming" }], + icons: { + icon: "/logo_2024_neon_pink.png", + shortcut: "/logo_2024_neon_pink.png", + apple: "/logo_2024_neon_pink.png", + }, openGraph: { title: title || defaultTitle, description: metaDescription, type: "website", - locale: "no", + locale: locale, siteName: defaultTitle, images: imageURL ? [ diff --git a/src/components/ui/back.module.css b/src/components/ui/back.module.css index 3d6da57..ee3eb3b 100644 --- a/src/components/ui/back.module.css +++ b/src/components/ui/back.module.css @@ -9,6 +9,7 @@ background: none; border: none; color: var(--link-color); + cursor: pointer; } .button:hover, .button *:hover { diff --git a/src/components/ui/back.tsx b/src/components/ui/back.tsx index dedb982..a32d1aa 100644 --- a/src/components/ui/back.tsx +++ b/src/components/ui/back.tsx @@ -1,11 +1,14 @@ "use client" -import { useTranslations } from "next-intl" +import { useLocale, useTranslations } from "next-intl" +import { useRouter } from "next/navigation" import { FaArrowLeft } from "react-icons/fa" import * as styles from "./back.module.css" const Back = () => { + const locale = useLocale() + const router = useRouter() const t = useTranslations("buttons") const handleBack = () => { @@ -18,7 +21,7 @@ const Back = () => { if (window.history.length > 1 && isSameDomain) { window.history.back() } else { - window.location.href = "/" + router.push(`/${locale}`) } } diff --git a/src/components/ui/languageSelector.module.css b/src/components/ui/languageSelector.module.css new file mode 100644 index 0000000..930c0ca --- /dev/null +++ b/src/components/ui/languageSelector.module.css @@ -0,0 +1,30 @@ +.languageSwitcher { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; +} + +.button { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: rgba(0, 0, 0, 0.35); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s ease; +} + +.button:hover { + background: rgba(0, 0, 0, 1); +} + +.flag { + width: 20px; + height: 15px; + border-radius: 2px; +} diff --git a/src/components/ui/languageSelector.tsx b/src/components/ui/languageSelector.tsx new file mode 100644 index 0000000..6212f27 --- /dev/null +++ b/src/components/ui/languageSelector.tsx @@ -0,0 +1,37 @@ +"use client" + +import { GB, NO } from "country-flag-icons/react/3x2" +import { useLocale } from "next-intl" +import { useRouter, usePathname } from "next/navigation" + +import * as styles from "./languageSelector.module.css" + +const LanguageSelector = () => { + const locale = useLocale() + const router = useRouter() + const pathname = usePathname() + + const changeLocale = (newLocale: "no" | "en") => { + // Remove the current locale from the pathname + const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}/, "") + router.push(`/${newLocale}${pathWithoutLocale}`) + } + + return ( +
+ {locale === "no" ? ( + + ) : ( + + )} +
+ ) +} + +export default LanguageSelector diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..0af1ace --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,15 @@ +import { notFound } from "next/navigation" +import { getRequestConfig } from "next-intl/server" + +const locales = ["no", "en"] + +export default getRequestConfig(async ({ requestLocale }) => { + const locale = await requestLocale + + if (!locale || !locales.includes(locale as "no" | "en")) notFound() + + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + } +}) diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..12e7b68 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,12 @@ +import createMiddleware from "next-intl/middleware" + +export default createMiddleware({ + locales: ["no", "en"], + defaultLocale: "no", + localePrefix: "always", +}) + +export const config = { + // Match only internationalized pathnames + matcher: ["/", "/(no|en)/:path*"], +}