fix: upgrade to nextjs 16 (#888)

* Upgrade to nextjs 16

* Update sentry config

* Force dynamic for health route

* Upgrade eslint config

* Upgrade jest

* Move types to dev dependencies

* Remove pages from tailwind config

* Replace img with next image
This commit is contained in:
Sergey Mankovsky
2026-02-27 17:18:03 +01:00
committed by GitHub
parent 7f9ce7f13a
commit f6cc03286b
20 changed files with 1077 additions and 916 deletions

1
www/.gitignore vendored
View File

@@ -46,3 +46,4 @@ openapi-ts-error-*.log
# pnpm # pnpm
.pnpm-store .pnpm-store
/v10

View File

@@ -1,5 +1,6 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClose } from "@fortawesome/free-solid-svg-icons"; import { faClose } from "@fortawesome/free-solid-svg-icons";
import type { JSX } from "react";
import { MouseEventHandler } from "react"; import { MouseEventHandler } from "react";
type ModalProps = { type ModalProps = {

View File

@@ -1,3 +1,5 @@
"use client";
import React from "react"; import React from "react";
import { Box, Stack, Link, Heading } from "@chakra-ui/react"; import { Box, Stack, Link, Heading } from "@chakra-ui/react";
import NextLink from "next/link"; import NextLink from "next/link";

View File

@@ -1,3 +1,5 @@
"use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
Box, Box,

View File

@@ -1,4 +1,4 @@
import { Container, Flex, Link } from "@chakra-ui/react"; import { Container, Flex } from "@chakra-ui/react";
import { featureEnabled } from "../lib/features"; import { featureEnabled } from "../lib/features";
import NextLink from "next/link"; import NextLink from "next/link";
import Image from "next/image"; import Image from "next/image";
@@ -30,7 +30,7 @@ export default async function AppLayout({
mt="1" mt="1"
> >
{/* Logo on the left */} {/* Logo on the left */}
<Link as={NextLink} href="/" className="flex"> <NextLink href="/" className="flex">
<Image <Image
src="/reach.svg" src="/reach.svg"
width={32} width={32}
@@ -46,22 +46,18 @@ export default async function AppLayout({
Capture the signal, not the noise Capture the signal, not the noise
</p> </p>
</div> </div>
</Link> </NextLink>
<div> <div>
{/* Text link on the right */} {/* Text link on the right */}
<Link <NextLink href={RECORD_A_MEETING_URL} className="font-light px-2">
as={NextLink}
href={RECORD_A_MEETING_URL}
className="font-light px-2"
>
Create Create
</Link> </NextLink>
{featureEnabled("browse") ? ( {featureEnabled("browse") ? (
<> <>
&nbsp;·&nbsp; &nbsp;·&nbsp;
<Link href="/browse" as={NextLink} className="font-light px-2"> <NextLink href="/browse" className="font-light px-2">
Browse Browse
</Link> </NextLink>
</> </>
) : ( ) : (
<></> <></>
@@ -69,9 +65,9 @@ export default async function AppLayout({
{featureEnabled("rooms") ? ( {featureEnabled("rooms") ? (
<> <>
&nbsp;·&nbsp; &nbsp;·&nbsp;
<Link href="/rooms" as={NextLink} className="font-light px-2"> <NextLink href="/rooms" className="font-light px-2">
Rooms Rooms
</Link> </NextLink>
</> </>
) : ( ) : (
<></> <></>
@@ -79,13 +75,9 @@ export default async function AppLayout({
{featureEnabled("requireLogin") ? ( {featureEnabled("requireLogin") ? (
<> <>
&nbsp;·&nbsp; &nbsp;·&nbsp;
<Link <NextLink href="/settings/api-keys" className="font-light px-2">
href="/settings/api-keys"
as={NextLink}
className="font-light px-2"
>
Settings Settings
</Link> </NextLink>
&nbsp;·&nbsp; &nbsp;·&nbsp;
<UserInfo /> <UserInfo />
</> </>

View File

@@ -28,7 +28,7 @@ function WherebyConsentDialogButton({
meetingId: MeetingId; meetingId: MeetingId;
recordingType: Meeting["recording_type"]; recordingType: Meeting["recording_type"];
skipConsent: boolean; skipConsent: boolean;
wherebyRef: React.RefObject<HTMLElement>; wherebyRef: React.RefObject<HTMLElement | null>;
}) { }) {
const previousFocusRef = useRef<HTMLElement | null>(null); const previousFocusRef = useRef<HTMLElement | null>(null);

View File

@@ -49,8 +49,8 @@ export type RoomDetails = {
// stages: we focus on the consent, then whereby steals focus, then we focus on the consent again, then return focus to whoever stole it initially // stages: we focus on the consent, then whereby steals focus, then we focus on the consent again, then return focus to whoever stole it initially
const useConsentWherebyFocusManagement = ( const useConsentWherebyFocusManagement = (
acceptButtonRef: RefObject<HTMLButtonElement>, acceptButtonRef: RefObject<HTMLButtonElement | null>,
wherebyRef: RefObject<HTMLElement>, wherebyRef: RefObject<HTMLElement | null>,
) => { ) => {
const currentFocusRef = useRef<HTMLElement | null>(null); const currentFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => { useEffect(() => {
@@ -87,7 +87,7 @@ const useConsentWherebyFocusManagement = (
const useConsentDialog = ( const useConsentDialog = (
meetingId: MeetingId, meetingId: MeetingId,
wherebyRef: RefObject<HTMLElement> /*accessibility*/, wherebyRef: RefObject<HTMLElement | null> /*accessibility*/,
) => { ) => {
const { state: consentState, touch, hasAnswered } = useRecordingConsent(); const { state: consentState, touch, hasAnswered } = useRecordingConsent();
// toast would open duplicates, even with using "id=" prop // toast would open duplicates, even with using "id=" prop
@@ -220,7 +220,7 @@ function ConsentDialogButton({
wherebyRef, wherebyRef,
}: { }: {
meetingId: MeetingId; meetingId: MeetingId;
wherebyRef: React.RefObject<HTMLElement>; wherebyRef: React.RefObject<HTMLElement | null>;
}) { }) {
const { showConsentModal, consentState, hasAnswered, consentLoading } = const { showConsentModal, consentState, hasAnswered, consentLoading } =
useConsentDialog(meetingId, wherebyRef); useConsentDialog(meetingId, wherebyRef);

View File

@@ -1,6 +1,14 @@
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import { authOptions } from "../../../lib/authBackend"; import { authOptions } from "../../../lib/authBackend";
const handler = NextAuth(authOptions()); export const dynamic = "force-dynamic";
export { handler as GET, handler as POST }; // authOptions() is deferred to request time to avoid calling getNextEnvVar
// during Turbopack's build-phase module evaluation (Next.js 16+)
export function GET(req: Request, ctx: any) {
return NextAuth(authOptions())(req as any, ctx);
}
export function POST(req: Request, ctx: any) {
return NextAuth(authOptions())(req as any, ctx);
}

View File

@@ -1,5 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() { export async function GET() {
const health = { const health = {
status: "healthy", status: "healthy",

View File

@@ -24,10 +24,9 @@ export const viewport: Viewport = {
maximumScale: 1, maximumScale: 1,
}; };
const SITE_URL = getNextEnvVar("SITE_URL"); export function generateMetadata(): Metadata {
const env = getClientEnv(); const SITE_URL = getNextEnvVar("SITE_URL");
return {
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL), metadataBase: new URL(SITE_URL),
title: { title: {
template: "%s Reflector", template: "%s Reflector",
@@ -38,7 +37,9 @@ export const metadata: Metadata = {
applicationName: "Reflector", applicationName: "Reflector",
referrer: "origin-when-cross-origin", referrer: "origin-when-cross-origin",
keywords: ["Reflector", "Monadical", "AI", "Meetings", "Transcription"], keywords: ["Reflector", "Monadical", "AI", "Meetings", "Transcription"],
authors: [{ name: "Monadical Team", url: "https://monadical.com/team.html" }], authors: [
{ name: "Monadical Team", url: "https://monadical.com/team.html" },
],
formatDetection: { formatDetection: {
email: false, email: false,
address: false, address: false,
@@ -65,14 +66,21 @@ export const metadata: Metadata = {
shortcut: "/r-icon.png", shortcut: "/r-icon.png",
apple: "/r-icon.png", apple: "/r-icon.png",
}, },
robots: { index: false, follow: false, noarchive: true, noimageindex: true }, robots: {
}; index: false,
follow: false,
noarchive: true,
noimageindex: true,
},
};
}
export default async function RootLayout({ export default async function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const env = getClientEnv();
return ( return (
<html lang="en" className={poppins.className} suppressHydrationWarning> <html lang="en" className={poppins.className} suppressHydrationWarning>
<body <body

View File

@@ -84,7 +84,7 @@ export const getClientEnvServer = (): ClientEnvCommon => {
if (isBuildPhase) { if (isBuildPhase) {
return { return {
API_URL: getNextEnvVar("API_URL"), API_URL: parseNonEmptyString(process.env.API_URL ?? ""),
WEBSOCKET_URL: parseMaybeNonEmptyString(process.env.WEBSOCKET_URL ?? ""), WEBSOCKET_URL: parseMaybeNonEmptyString(process.env.WEBSOCKET_URL ?? ""),
AUTH_PROVIDER: parseAuthProvider(), AUTH_PROVIDER: parseAuthProvider(),
SENTRY_DSN: parseMaybeNonEmptyString( SENTRY_DSN: parseMaybeNonEmptyString(

View File

@@ -1,3 +1,5 @@
import type { JSX } from "react";
type SimpleProps = { type SimpleProps = {
children: JSX.Element | string | (JSX.Element | string)[]; children: JSX.Element | string | (JSX.Element | string)[];
className?: string; className?: string;

View File

@@ -159,7 +159,7 @@ export default function WebinarPage(details: WebinarDetails) {
<div className="max-w-4xl mx-auto px-2 py-8 bg-gray-50"> <div className="max-w-4xl mx-auto px-2 py-8 bg-gray-50">
<div className="bg-white rounded-3xl px-4 md:px-36 py-4 shadow-md mx-auto"> <div className="bg-white rounded-3xl px-4 md:px-36 py-4 shadow-md mx-auto">
<Link href="https://www.monadical.com" target="_blank"> <Link href="https://www.monadical.com" target="_blank">
<img <Image
src="/monadical-black-white 1.svg" src="/monadical-black-white 1.svg"
alt="Monadical Logo" alt="Monadical Logo"
className="mx-auto mb-8" className="mx-auto mb-8"
@@ -355,7 +355,7 @@ export default function WebinarPage(details: WebinarDetails) {
<div className="max-w-4xl mx-auto px-2 py-8 bg-gray-50"> <div className="max-w-4xl mx-auto px-2 py-8 bg-gray-50">
<div className="bg-white rounded-3xl px-4 md:px-36 py-4 shadow-md mx-auto"> <div className="bg-white rounded-3xl px-4 md:px-36 py-4 shadow-md mx-auto">
<Link href="https://www.monadical.com" target="_blank"> <Link href="https://www.monadical.com" target="_blank">
<img <Image
src="/monadical-black-white 1.svg" src="/monadical-black-white 1.svg"
alt="Monadical Logo" alt="Monadical Logo"
className="mx-auto mb-8" className="mx-auto mb-8"

View File

@@ -4,47 +4,20 @@ const nextConfig = {
env: { env: {
IS_CI: process.env.IS_CI, IS_CI: process.env.IS_CI,
}, },
};
module.exports = nextConfig;
// Injected content via Sentry wizard below
const { withSentryConfig } = require("@sentry/nextjs");
module.exports = withSentryConfig(
module.exports,
{
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
// Suppresses source map uploading logs during build
silent: true,
org: "monadical",
project: "reflector-www",
},
{
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
experimental: { experimental: {
optimizePackageImports: ["@chakra-ui/react"], optimizePackageImports: ["@chakra-ui/react"],
}, },
};
const { withSentryConfig } = require("@sentry/nextjs");
module.exports = withSentryConfig(nextConfig, {
silent: true,
org: "monadical",
project: "reflector-www",
widenClientFileUpload: true,
tunnelRoute: "/monitoring",
bundleSizeOptimizations: {
excludeDebugStatements: true,
}, },
); });

View File

@@ -21,17 +21,16 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@sentry/nextjs": "^10.40.0", "@sentry/nextjs": "^10.40.0",
"@tanstack/react-query": "^5.85.9", "@tanstack/react-query": "^5.85.9",
"@types/ioredis": "^5.0.0",
"@whereby.com/browser-sdk": "^3.3.4", "@whereby.com/browser-sdk": "^3.3.4",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"axios": "^1.13.5", "axios": "^1.13.5",
"eslint": "^9.33.0", "eslint": "^9.33.0",
"eslint-config-next": "^15.5.3", "eslint-config-next": "^16.1.6",
"fontawesome": "^5.6.3", "fontawesome": "^5.6.3",
"ioredis": "^5.7.0", "ioredis": "^5.7.0",
"jest-worker": "^29.6.2", "jest-worker": "^29.6.2",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"next": "^15.5.10", "next": "^16.1.6",
"next-auth": "^4.24.12", "next-auth": "^4.24.12",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nuqs": "^2.4.3", "nuqs": "^2.4.3",
@@ -39,8 +38,8 @@
"openapi-react-query": "^0.5.0", "openapi-react-query": "^0.5.0",
"postcss": "8.4.31", "postcss": "8.4.31",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^19.2.4",
"react-dom": "^18.2.0", "react-dom": "^19.2.4",
"react-dropdown": "^1.11.0", "react-dropdown": "^1.11.0",
"react-icons": "^5.0.1", "react-icons": "^5.0.1",
"react-markdown": "^9.0.0", "react-markdown": "^9.0.0",
@@ -61,12 +60,14 @@
"author": "Andreas <andreas@monadical.com>", "author": "Andreas <andreas@monadical.com>",
"license": "All Rights Reserved", "license": "All Rights Reserved",
"devDependencies": { "devDependencies": {
"@types/ioredis": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/react": "18.2.20", "@types/react": "19.2.14",
"jest": "^30.1.3", "@types/react-dom": "^19.2.3",
"jest": "^30.2.0",
"openapi-typescript": "^7.9.1", "openapi-typescript": "^7.9.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"ts-jest": "^29.4.1" "ts-jest": "^29.4.6"
}, },
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
} }

1754
www/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ module.exports = {
preflight: false, preflight: false,
}, },
content: [ content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}",
], ],

View File

@@ -13,7 +13,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
@@ -22,6 +22,12 @@
"strictNullChecks": true, "strictNullChecks": true,
"downlevelIteration": true "downlevelIteration": true
}, },
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], "include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1