Upgrade app to Next.js 16
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-13 15:50:37 +01:00
parent f14116b56c
commit e32ac4cacb
40 changed files with 1853 additions and 1157 deletions
+5 -5
View File
@@ -1,9 +1,9 @@
DATABASE_URL="postgresql://postgres:postgres@db:5432/moh_sass?schema=public" DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/moh_sass?schema=public"
NEXT_PUBLIC_APP_URL="https://mohfarawati.de" NEXT_PUBLIC_APP_URL="https://mohfarawati.de"
NEXT_PUBLIC_SITE_URL="https://mohfarawati.de" NEXT_PUBLIC_SITE_URL="https://mohfarawati.de"
NEXT_PUBLIC_ADMIN_URL="https://root.mohfarawati.de" NEXT_PUBLIC_ADMIN_URL="https://root.mohfarawati.de"
NEXT_TELEMETRY_DISABLED="1" NEXT_TELEMETRY_DISABLED="1"
ADMIN_PASSWORD="123Yolo!321" ADMIN_PASSWORD="change-me"
ADMIN_AUTH_SECRET="Us0z76jwlTQLOeQWAGGxAxDcc0rHwp4q" ADMIN_AUTH_SECRET="replace-with-a-long-random-secret"
ROOT_BASIC_AUTH_USER="root" ROOT_BASIC_AUTH_USER="change-me"
ROOT_BASIC_AUTH_PASS="123Yolo!321" ROOT_BASIC_AUTH_PASS="change-me"
+6 -6
View File
@@ -14,16 +14,15 @@ import { getLocalizedPath, resolveLocale } from "@/lib/locale";
import { buildLocalizedMetadata } from "@/lib/metadata"; import { buildLocalizedMetadata } from "@/lib/metadata";
type AboutPageProps = { type AboutPageProps = {
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function generateMetadata({ export async function generateMetadata({ params }: AboutPageProps): Promise<Metadata> {
params: { locale }, const { locale } = await params;
}: AboutPageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "aboutPage" }); const t = await getTranslations({ locale: localeKey, namespace: "aboutPage" });
@@ -35,7 +34,8 @@ export async function generateMetadata({
}); });
} }
export default async function AboutPage({ params: { locale } }: AboutPageProps) { export default async function AboutPage({ params }: AboutPageProps) {
const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [t, siteSettings, mediaBindings] = await Promise.all([ const [t, siteSettings, mediaBindings] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "aboutPage" }), getTranslations({ locale: localeKey, namespace: "aboutPage" }),
+1 -1
View File
@@ -1,7 +1,7 @@
"use server"; "use server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { z } from "zod"; import { z } from "zod";
import { enforceContactRateLimit, verifyTurnstileToken } from "@/lib/contact-guard"; import { enforceContactRateLimit, verifyTurnstileToken } from "@/lib/contact-guard";
+6 -6
View File
@@ -15,14 +15,13 @@ import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { submitContactFormAction } from "./actions"; import { submitContactFormAction } from "./actions";
type ContactPageProps = { type ContactPageProps = {
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
export async function generateMetadata({ export async function generateMetadata({ params }: ContactPageProps): Promise<Metadata> {
params: { locale }, const { locale } = await params;
}: ContactPageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "contactPage" }); const t = await getTranslations({ locale: localeKey, namespace: "contactPage" });
@@ -34,7 +33,8 @@ export async function generateMetadata({
}); });
} }
export default async function ContactPage({ params: { locale } }: ContactPageProps) { export default async function ContactPage({ params }: ContactPageProps) {
const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [t, protection] = await Promise.all([ const [t, protection] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "contactPage" }), getTranslations({ locale: localeKey, namespace: "contactPage" }),
+5 -4
View File
@@ -11,23 +11,24 @@ import { getLocalizedPath, resolveLocale } from "@/lib/locale";
type SiteLayoutProps = { type SiteLayoutProps = {
children: ReactNode; children: ReactNode;
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 0; export const revalidate = 0;
export default async function SiteLayout({ children, params: { locale } }: SiteLayoutProps) { export default async function SiteLayout({ children, params }: SiteLayoutProps) {
noStore(); noStore();
const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [maintenanceEnabled, mediaBindings] = await Promise.all([ const [maintenanceEnabled, mediaBindings] = await Promise.all([
getMaintenanceMode(), getMaintenanceMode(),
getSiteSettingsMediaBindings(), getSiteSettingsMediaBindings(),
]); ]);
const authenticated = isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (maintenanceEnabled && !authenticated) { if (maintenanceEnabled && !authenticated) {
redirect(getLocalizedPath(localeKey, "/coming-soon")); redirect(getLocalizedPath(localeKey, "/coming-soon"));
+6 -6
View File
@@ -24,9 +24,9 @@ import { CardContent } from "@/components/ui/card";
import { getLocalizedPath, resolveLocale } from "@/lib/locale"; import { getLocalizedPath, resolveLocale } from "@/lib/locale";
type HomePageProps = { type HomePageProps = {
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
const serviceIcons = [ const serviceIcons = [
@@ -41,9 +41,8 @@ const serviceIcons = [
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function generateMetadata({ export async function generateMetadata({ params }: HomePageProps): Promise<Metadata> {
params: { locale }, const { locale } = await params;
}: HomePageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [t, siteSettings] = await Promise.all([ const [t, siteSettings] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "homepage" }), getTranslations({ locale: localeKey, namespace: "homepage" }),
@@ -59,7 +58,8 @@ export async function generateMetadata({
}); });
} }
export default async function HomePage({ params: { locale } }: HomePageProps) { export default async function HomePage({ params }: HomePageProps) {
const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [t, marqueeSettings] = await Promise.all([ const [t, marqueeSettings] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "homepage" }), getTranslations({ locale: localeKey, namespace: "homepage" }),
@@ -13,17 +13,16 @@ import {
} from "@/lib/portfolio"; } from "@/lib/portfolio";
type PortfolioItemPageProps = { type PortfolioItemPageProps = {
params: { params: Promise<{
locale: string; locale: string;
slug: string; slug: string;
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function generateMetadata({ export async function generateMetadata({ params }: PortfolioItemPageProps): Promise<Metadata> {
params: { locale, slug }, const { locale, slug } = await params;
}: PortfolioItemPageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const item = await getPublishedPortfolioProjectBySlug(slug); const item = await getPublishedPortfolioProjectBySlug(slug);
@@ -45,8 +44,9 @@ export async function generateMetadata({
} }
export default async function PortfolioItemPage({ export default async function PortfolioItemPage({
params: { locale, slug }, params,
}: PortfolioItemPageProps) { }: PortfolioItemPageProps) {
const { locale, slug } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const item = await getPublishedPortfolioProjectBySlug(slug); const item = await getPublishedPortfolioProjectBySlug(slug);
@@ -16,17 +16,16 @@ import {
} from "@/lib/portfolio"; } from "@/lib/portfolio";
type PortfolioCategoryPageProps = { type PortfolioCategoryPageProps = {
params: { params: Promise<{
locale: string; locale: string;
slug: string; slug: string;
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function generateMetadata({ export async function generateMetadata({ params }: PortfolioCategoryPageProps): Promise<Metadata> {
params: { locale, slug }, const { locale, slug } = await params;
}: PortfolioCategoryPageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" }); const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
const category = await getActivePortfolioCategoryBySlug(slug); const category = await getActivePortfolioCategoryBySlug(slug);
@@ -49,8 +48,9 @@ export async function generateMetadata({
} }
export default async function PortfolioCategoryPage({ export default async function PortfolioCategoryPage({
params: { locale, slug }, params,
}: PortfolioCategoryPageProps) { }: PortfolioCategoryPageProps) {
const { locale, slug } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" }); const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
const [categories, category, projects] = await Promise.all([ const [categories, category, projects] = await Promise.all([
@@ -3,14 +3,15 @@ import { permanentRedirect } from "next/navigation";
import { getLocalizedPath, resolveLocale } from "@/lib/locale"; import { getLocalizedPath, resolveLocale } from "@/lib/locale";
type PortfolioCategoryIndexPageProps = { type PortfolioCategoryIndexPageProps = {
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
export default function PortfolioCategoryIndexPage({ export default async function PortfolioCategoryIndexPage({
params: { locale }, params,
}: PortfolioCategoryIndexPageProps) { }: PortfolioCategoryIndexPageProps) {
const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
permanentRedirect(getLocalizedPath(localeKey, "/portfolio")); permanentRedirect(getLocalizedPath(localeKey, "/portfolio"));
+12 -9
View File
@@ -11,19 +11,18 @@ import { getLocalizedPath, resolveLocale } from "@/lib/locale";
import { getActivePortfolioCategories, getPublishedPortfolioProjects } from "@/lib/portfolio"; import { getActivePortfolioCategories, getPublishedPortfolioProjects } from "@/lib/portfolio";
type PortfolioPageProps = { type PortfolioPageProps = {
params: { params: Promise<{
locale: string; locale: string;
}; }>;
searchParams?: { searchParams?: Promise<{
category?: string; category?: string;
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export async function generateMetadata({ export async function generateMetadata({ params }: PortfolioPageProps): Promise<Metadata> {
params: { locale }, const { locale } = await params;
}: PortfolioPageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" }); const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
@@ -36,12 +35,16 @@ export async function generateMetadata({
} }
export default async function PortfolioPage({ export default async function PortfolioPage({
params: { locale }, params,
searchParams, searchParams,
}: PortfolioPageProps) { }: PortfolioPageProps) {
const [{ locale }, resolvedSearchParams] = await Promise.all([
params,
searchParams,
]);
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" }); const t = await getTranslations({ locale: localeKey, namespace: "portfolioPage" });
const selectedCategory = searchParams?.category ?? ""; const selectedCategory = resolvedSearchParams?.category ?? "";
if (selectedCategory) { if (selectedCategory) {
redirect(getLocalizedPath(localeKey, `/portfolio/category/${selectedCategory}`)); redirect(getLocalizedPath(localeKey, `/portfolio/category/${selectedCategory}`));
+6 -6
View File
@@ -13,14 +13,13 @@ import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
type SuccessPageProps = { type SuccessPageProps = {
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
export async function generateMetadata({ export async function generateMetadata({ params }: SuccessPageProps): Promise<Metadata> {
params: { locale }, const { locale } = await params;
}: SuccessPageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "successPage" }); const t = await getTranslations({ locale: localeKey, namespace: "successPage" });
@@ -32,7 +31,8 @@ export async function generateMetadata({
}); });
} }
export default async function SuccessPage({ params: { locale } }: SuccessPageProps) { export default async function SuccessPage({ params }: SuccessPageProps) {
const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "successPage" }); const t = await getTranslations({ locale: localeKey, namespace: "successPage" });
+6 -6
View File
@@ -10,14 +10,13 @@ import { buildLocalizedMetadata } from "@/lib/metadata";
import { resolveLocale } from "@/lib/locale"; import { resolveLocale } from "@/lib/locale";
type ComingSoonPageProps = { type ComingSoonPageProps = {
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
export async function generateMetadata({ export async function generateMetadata({ params }: ComingSoonPageProps): Promise<Metadata> {
params: { locale }, const { locale } = await params;
}: ComingSoonPageProps): Promise<Metadata> {
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" }); const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" });
const siteSettings = await getSiteSettings(); const siteSettings = await getSiteSettings();
@@ -32,8 +31,9 @@ export async function generateMetadata({
} }
export default async function ComingSoonPage({ export default async function ComingSoonPage({
params: { locale }, params,
}: ComingSoonPageProps) { }: ComingSoonPageProps) {
const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" }); const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" });
const isArabic = localeKey === "ar"; const isArabic = localeKey === "ar";
+4 -3
View File
@@ -9,9 +9,9 @@ import { getDirection } from "@/lib/locale";
type LocaleLayoutProps = { type LocaleLayoutProps = {
children: ReactNode; children: ReactNode;
params: { params: Promise<{
locale: string; locale: string;
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -23,9 +23,10 @@ export function generateStaticParams() {
export default async function LocaleLayout({ export default async function LocaleLayout({
children, children,
params: { locale }, params,
}: LocaleLayoutProps) { }: LocaleLayoutProps) {
noStore(); noStore();
const { locale } = await params;
if (!routing.locales.includes(locale as (typeof routing.locales)[number])) { if (!routing.locales.includes(locale as (typeof routing.locales)[number])) {
notFound(); notFound();
+4 -4
View File
@@ -9,15 +9,15 @@ import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"
import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedPath } from "@/lib/locale";
import { setMaintenanceMode } from "@/lib/app-config"; import { setMaintenanceMode } from "@/lib/app-config";
function ensureAdmin() { async function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
} }
export async function updateMaintenanceModeAction(formData: FormData) { export async function updateMaintenanceModeAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
const nextValue = formData.get("enabled") === "true"; const nextValue = formData.get("enabled") === "true";
const redirectPath = String(formData.get("redirectPath") ?? "/"); const redirectPath = String(formData.get("redirectPath") ?? "/");
+2 -2
View File
@@ -28,7 +28,7 @@ const copy = {
}; };
export default async function RootMaintenancePage() { export default async function RootMaintenancePage() {
const authenticated = isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
redirect("/"); redirect("/");
@@ -38,7 +38,7 @@ export default async function RootMaintenancePage() {
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+5 -5
View File
@@ -2,7 +2,7 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
@@ -10,9 +10,9 @@ import { updateMarqueeSettings } from "@/lib/app-config";
import { routing } from "@/i18n/routing"; import { routing } from "@/i18n/routing";
import { getLocalizedPath } from "@/lib/locale"; import { getLocalizedPath } from "@/lib/locale";
function ensureAdmin() { async function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
} }
@@ -35,7 +35,7 @@ async function revalidateMarqueePages() {
} }
export async function saveMarqueeSettingsAction(formData: FormData) { export async function saveMarqueeSettingsAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
try { try {
const germanSettings = { const germanSettings = {
+2 -2
View File
@@ -26,14 +26,14 @@ const copy = {
}; };
export default async function RootMarqueePage() { export default async function RootMarqueePage() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+6 -6
View File
@@ -3,7 +3,7 @@
import { MediaKind } from "@prisma/client"; import { MediaKind } from "@prisma/client";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
@@ -12,9 +12,9 @@ import { createStandaloneMediaAsset, deleteMediaAssetAndFile } from "@/lib/media
import { isManagedMediaFilePath } from "@/lib/media-storage"; import { isManagedMediaFilePath } from "@/lib/media-storage";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
function ensureAdmin() { async function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
} }
@@ -34,7 +34,7 @@ function revalidateMediaPages() {
} }
export async function createMediaAssetAction(formData: FormData) { export async function createMediaAssetAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
try { try {
const kindValue = String(formData.get("kind") ?? "IMAGE"); const kindValue = String(formData.get("kind") ?? "IMAGE");
@@ -59,7 +59,7 @@ export async function createMediaAssetAction(formData: FormData) {
} }
export async function deleteMediaAssetAction(formData: FormData) { export async function deleteMediaAssetAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
const assetId = String(formData.get("assetId") ?? ""); const assetId = String(formData.get("assetId") ?? "");
+2 -2
View File
@@ -21,14 +21,14 @@ const copy = {
}; };
export default async function RootMediaPage() { export default async function RootMediaPage() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+12 -11
View File
@@ -25,9 +25,9 @@ import { getAdminMediaAssets } from "@/lib/media";
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio"; import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
type RootPageProps = { type RootPageProps = {
searchParams?: { searchParams?: Promise<{
error?: string; error?: string;
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -61,25 +61,26 @@ const copy = {
}; };
export default async function RootPage({ searchParams }: RootPageProps) { export default async function RootPage({ searchParams }: RootPageProps) {
const resolvedSearchParams = await searchParams;
const authConfigured = isAdminAuthConfigured(); const authConfigured = isAdminAuthConfigured();
const basicConfigured = Boolean( const basicConfigured = Boolean(
process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS, process.env.ROOT_BASIC_AUTH_USER && process.env.ROOT_BASIC_AUTH_PASS,
); );
const authenticated = isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
const lockState = getAdminLockState(); const lockState = await getAdminLockState();
async function loginAction(formData: FormData) { async function loginAction(formData: FormData) {
"use server"; "use server";
const password = String(formData.get("password") ?? ""); const password = String(formData.get("password") ?? "");
const currentLockState = getAdminLockState(); const currentLockState = await getAdminLockState();
if (currentLockState.locked) { if (currentLockState.locked) {
redirect("/?error=locked"); redirect("/?error=locked");
} }
if (!isAdminAuthConfigured() || !isPasswordValid(password)) { if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
const failState = registerFailedAdminAttempt(); const failState = await registerFailedAdminAttempt();
if (failState.locked) { if (failState.locked) {
redirect("/?error=locked"); redirect("/?error=locked");
} }
@@ -87,15 +88,15 @@ export default async function RootPage({ searchParams }: RootPageProps) {
redirect("/?error=invalid"); redirect("/?error=invalid");
} }
resetAdminFailedAttempts(); await resetAdminFailedAttempts();
setAdminSessionCookie(); await setAdminSessionCookie();
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
@@ -124,13 +125,13 @@ export default async function RootPage({ searchParams }: RootPageProps) {
</p> </p>
) : null} ) : null}
{searchParams?.error === "invalid" ? ( {resolvedSearchParams?.error === "invalid" ? (
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning"> <p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
{copy.invalidLogin} {copy.invalidLogin}
</p> </p>
) : null} ) : null}
{searchParams?.error === "locked" || lockState.locked ? ( {resolvedSearchParams?.error === "locked" || lockState.locked ? (
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning"> <p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
{copy.lockedLogin} {copy.lockedLogin}
</p> </p>
+8 -8
View File
@@ -3,7 +3,7 @@
import { MediaUsageType, Prisma } from "@prisma/client"; import { MediaUsageType, Prisma } from "@prisma/client";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { routing } from "@/i18n/routing"; import { routing } from "@/i18n/routing";
@@ -23,9 +23,9 @@ import {
sectionInputSchema, sectionInputSchema,
} from "@/lib/portfolio-validation"; } from "@/lib/portfolio-validation";
function ensureAdmin() { async function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
} }
@@ -105,7 +105,7 @@ async function removeManagedPaths(paths: string[]) {
} }
export async function upsertCategoryAction(formData: FormData) { export async function upsertCategoryAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
const redirectPath = getRedirectPath(formData, "/portfolio/categories"); const redirectPath = getRedirectPath(formData, "/portfolio/categories");
@@ -155,7 +155,7 @@ export async function upsertCategoryAction(formData: FormData) {
} }
export async function deleteCategoryAction(formData: FormData) { export async function deleteCategoryAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
const redirectPath = getRedirectPath(formData, "/portfolio/categories"); const redirectPath = getRedirectPath(formData, "/portfolio/categories");
const id = String(formData.get("id") ?? ""); const id = String(formData.get("id") ?? "");
@@ -189,7 +189,7 @@ export async function deleteCategoryAction(formData: FormData) {
} }
export async function saveProjectAction(formData: FormData) { export async function saveProjectAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
const fallbackRedirect = String(formData.get("id") ?? "").trim() const fallbackRedirect = String(formData.get("id") ?? "").trim()
? `/portfolio/projects/${String(formData.get("id") ?? "").trim()}` ? `/portfolio/projects/${String(formData.get("id") ?? "").trim()}`
@@ -564,7 +564,7 @@ export async function saveProjectAction(formData: FormData) {
} }
export async function deleteProjectAction(formData: FormData) { export async function deleteProjectAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
const id = String(formData.get("id") ?? ""); const id = String(formData.get("id") ?? "");
+2 -2
View File
@@ -23,14 +23,14 @@ const copy = {
}; };
export default async function RootPortfolioCategoriesPage() { export default async function RootPortfolioCategoriesPage() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+10 -8
View File
@@ -21,31 +21,33 @@ const copy = {
}; };
type RootPortfolioPageProps = { type RootPortfolioPageProps = {
searchParams?: { searchParams?: Promise<{
category?: string; category?: string;
status?: "all" | "draft" | "published"; status?: "all" | "draft" | "published";
success?: string; success?: string;
error?: string; error?: string;
}; }>;
}; };
export default async function RootPortfolioPage({ searchParams }: RootPortfolioPageProps) { export default async function RootPortfolioPage({ searchParams }: RootPortfolioPageProps) {
if (!isAdminAuthenticated()) { const resolvedSearchParams = await searchParams;
if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
const selectedCategory = searchParams?.category && searchParams.category !== "__all__" const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__"
? searchParams.category ? resolvedSearchParams.category
: ""; : "";
const selectedStatus = searchParams?.status === "draft" || searchParams?.status === "published" const selectedStatus = resolvedSearchParams?.status === "draft" || resolvedSearchParams?.status === "published"
? searchParams.status ? resolvedSearchParams.status
: "all"; : "all";
const [categories, projects] = await Promise.all([ const [categories, projects] = await Promise.all([
getAdminPortfolioCategories(), getAdminPortfolioCategories(),
+7 -5
View File
@@ -44,29 +44,31 @@ const copy = {
}; };
type RootPortfolioProjectPageProps = { type RootPortfolioProjectPageProps = {
params: { params: Promise<{
id: string; id: string;
}; }>;
}; };
export default async function RootPortfolioProjectPage({ export default async function RootPortfolioProjectPage({
params, params,
}: RootPortfolioProjectPageProps) { }: RootPortfolioProjectPageProps) {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
const { id } = await params;
const [categories, mediaOptions, project] = await Promise.all([ const [categories, mediaOptions, project] = await Promise.all([
getActivePortfolioCategories(), getActivePortfolioCategories(),
getMediaOptions(), getMediaOptions(),
getAdminPortfolioProjectById(params.id), getAdminPortfolioProjectById(id),
]); ]);
if (!project) { if (!project) {
+2 -2
View File
@@ -25,14 +25,14 @@ const copy = {
}; };
export default async function RootNewPortfolioProjectPage() { export default async function RootNewPortfolioProjectPage() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+10 -8
View File
@@ -21,31 +21,33 @@ const copy = {
}; };
type RootPortfolioProjectsPageProps = { type RootPortfolioProjectsPageProps = {
searchParams?: { searchParams?: Promise<{
category?: string; category?: string;
status?: "all" | "draft" | "published"; status?: "all" | "draft" | "published";
}; }>;
}; };
export default async function RootPortfolioProjectsPage({ export default async function RootPortfolioProjectsPage({
searchParams, searchParams,
}: RootPortfolioProjectsPageProps) { }: RootPortfolioProjectsPageProps) {
if (!isAdminAuthenticated()) { const resolvedSearchParams = await searchParams;
if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
const selectedCategory = searchParams?.category && searchParams.category !== "__all__" const selectedCategory = resolvedSearchParams?.category && resolvedSearchParams.category !== "__all__"
? searchParams.category ? resolvedSearchParams.category
: ""; : "";
const selectedStatus = searchParams?.status === "draft" || searchParams?.status === "published" const selectedStatus = resolvedSearchParams?.status === "draft" || resolvedSearchParams?.status === "published"
? searchParams.status ? resolvedSearchParams.status
: "all"; : "all";
const [categories, projects] = await Promise.all([ const [categories, projects] = await Promise.all([
getAdminPortfolioCategories(), getAdminPortfolioCategories(),
+5 -5
View File
@@ -3,7 +3,7 @@
import { MediaUsageType } from "@prisma/client"; import { MediaUsageType } from "@prisma/client";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { import {
SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY, SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY,
@@ -25,9 +25,9 @@ import { removeManagedMediaFile } from "@/lib/media-storage";
import { mediaFieldInputSchema } from "@/lib/media-validation"; import { mediaFieldInputSchema } from "@/lib/media-validation";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
function ensureAdmin() { async function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
} }
@@ -91,7 +91,7 @@ async function revalidateSiteSettingsPages() {
} }
export async function saveSiteSettingsAction(formData: FormData) { export async function saveSiteSettingsAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
const createdMediaAssetIds: string[] = []; const createdMediaAssetIds: string[] = [];
const uploadedPaths: string[] = []; const uploadedPaths: string[] = [];
+2 -2
View File
@@ -30,14 +30,14 @@ const copy = {
}; };
export default async function RootSiteSettingsPage() { export default async function RootSiteSettingsPage() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+6 -6
View File
@@ -2,7 +2,7 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
@@ -14,9 +14,9 @@ import {
import { sendTestEmail } from "@/lib/mail"; import { sendTestEmail } from "@/lib/mail";
import type { MailSettings } from "@/lib/mail-settings"; import type { MailSettings } from "@/lib/mail-settings";
function ensureAdmin() { async function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
} }
@@ -71,7 +71,7 @@ function parseMailSettingsFormData(
} }
export async function saveMailSettingsAction(formData: FormData) { export async function saveMailSettingsAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
try { try {
const existingMailSettings = await getMailSettings(); const existingMailSettings = await getMailSettings();
@@ -95,7 +95,7 @@ export async function saveMailSettingsAction(formData: FormData) {
} }
export async function sendTestEmailAction() { export async function sendTestEmailAction() {
ensureAdmin(); await ensureAdmin();
try { try {
await sendTestEmail(); await sendTestEmail();
+5 -5
View File
@@ -2,7 +2,7 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { isRedirectError } from "next/dist/client/components/redirect"; import { isRedirectError } from "next/dist/client/components/redirect-error";
import { toInternalAdminPath } from "@/lib/admin-routing"; import { toInternalAdminPath } from "@/lib/admin-routing";
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth"; import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
@@ -13,9 +13,9 @@ import {
} from "@/lib/app-config"; } from "@/lib/app-config";
import type { ContactProtectionSettings } from "@/lib/contact-protection"; import type { ContactProtectionSettings } from "@/lib/contact-protection";
function ensureAdmin() { async function ensureAdmin() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
} }
@@ -79,7 +79,7 @@ function parseContactProtectionFormData(
} }
export async function saveContactProtectionSettingsAction(formData: FormData) { export async function saveContactProtectionSettingsAction(formData: FormData) {
ensureAdmin(); await ensureAdmin();
try { try {
const existingSettings = await getContactProtectionSettings(); const existingSettings = await getContactProtectionSettings();
+2 -2
View File
@@ -26,14 +26,14 @@ const copy = {
}; };
export default async function RootSMTPProtectionPage() { export default async function RootSMTPProtectionPage() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+2 -2
View File
@@ -27,14 +27,14 @@ const copy = {
}; };
export default async function RootSMTPPage() { export default async function RootSMTPPage() {
if (!isAdminAuthenticated()) { if (!(await isAdminAuthenticated())) {
redirect("/"); redirect("/");
} }
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+2 -2
View File
@@ -21,7 +21,7 @@ const copy = {
}; };
export default async function RootUiKitPage() { export default async function RootUiKitPage() {
const authenticated = isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (!authenticated) { if (!authenticated) {
redirect("/"); redirect("/");
@@ -30,7 +30,7 @@ export default async function RootUiKitPage() {
async function logoutAction() { async function logoutAction() {
"use server"; "use server";
clearAdminSessionCookie(); await clearAdminSessionCookie();
redirect("/"); redirect("/");
} }
+4 -3
View File
@@ -5,9 +5,9 @@ import path from "path";
import { resolveMediaUploadPath } from "@/lib/media-storage"; import { resolveMediaUploadPath } from "@/lib/media-storage";
type MediaFileRouteProps = { type MediaFileRouteProps = {
params: { params: Promise<{
segments: string[]; segments: string[];
}; }>;
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -23,7 +23,8 @@ const CONTENT_TYPES: Record<string, string> = {
}; };
export async function GET(_: Request, { params }: MediaFileRouteProps) { export async function GET(_: Request, { params }: MediaFileRouteProps) {
const relativePath = params.segments.join("/"); const { segments } = await params;
const relativePath = segments.join("/");
const publicPath = `/uploads/media/${relativePath}`; const publicPath = `/uploads/media/${relativePath}`;
try { try {
+24
View File
@@ -0,0 +1,24 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypeScript from "eslint-config-next/typescript";
const config = [
...nextCoreWebVitals,
...nextTypeScript,
{
files: ["components/**/*.tsx"],
rules: {
"react-hooks/set-state-in-effect": "off",
},
},
{
files: ["eslint.config.mjs"],
rules: {
"import/no-anonymous-default-export": "off",
},
},
{
ignores: ["prisma/seed.js"],
},
];
export default config;
+12 -12
View File
@@ -64,8 +64,8 @@ export function isPasswordValid(password: string): boolean {
return timingSafeEqual(provided, expected); return timingSafeEqual(provided, expected);
} }
export function setAdminSessionCookie(): void { export async function setAdminSessionCookie(): Promise<void> {
const store = cookies(); const store = await cookies();
store.set(ADMIN_SESSION_COOKIE, buildToken(), { store.set(ADMIN_SESSION_COOKIE, buildToken(), {
httpOnly: true, httpOnly: true,
sameSite: "lax", sameSite: "lax",
@@ -75,8 +75,8 @@ export function setAdminSessionCookie(): void {
}); });
} }
export function clearAdminSessionCookie(): void { export async function clearAdminSessionCookie(): Promise<void> {
const store = cookies(); const store = await cookies();
store.set(ADMIN_SESSION_COOKIE, "", { store.set(ADMIN_SESSION_COOKIE, "", {
httpOnly: true, httpOnly: true,
sameSite: "lax", sameSite: "lax",
@@ -108,8 +108,8 @@ function parseFailState(rawValue: string | undefined): FailState {
} }
} }
export function getAdminLockState(): { locked: boolean; remainingSeconds: number } { export async function getAdminLockState(): Promise<{ locked: boolean; remainingSeconds: number }> {
const store = cookies(); const store = await cookies();
const state = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value); const state = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
const now = Date.now(); const now = Date.now();
@@ -123,8 +123,8 @@ export function getAdminLockState(): { locked: boolean; remainingSeconds: number
return { locked: false, remainingSeconds: 0 }; return { locked: false, remainingSeconds: 0 };
} }
export function registerFailedAdminAttempt(): { locked: boolean; remainingSeconds: number } { export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; remainingSeconds: number }> {
const store = cookies(); const store = await cookies();
const now = Date.now(); const now = Date.now();
const current = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value); const current = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
const attempts = current.lockUntil > now ? current.attempts : current.attempts + 1; const attempts = current.lockUntil > now ? current.attempts : current.attempts + 1;
@@ -145,8 +145,8 @@ export function registerFailedAdminAttempt(): { locked: boolean; remainingSecond
}; };
} }
export function resetAdminFailedAttempts(): void { export async function resetAdminFailedAttempts(): Promise<void> {
const store = cookies(); const store = await cookies();
store.set(ADMIN_FAIL_COOKIE, "", { store.set(ADMIN_FAIL_COOKIE, "", {
httpOnly: true, httpOnly: true,
sameSite: "lax", sameSite: "lax",
@@ -156,12 +156,12 @@ export function resetAdminFailedAttempts(): void {
}); });
} }
export function isAdminAuthenticated(): boolean { export async function isAdminAuthenticated(): Promise<boolean> {
if (!isAdminAuthConfigured()) { if (!isAdminAuthConfigured()) {
return false; return false;
} }
const store = cookies(); const store = await cookies();
const token = store.get(ADMIN_SESSION_COOKIE)?.value; const token = store.get(ADMIN_SESSION_COOKIE)?.value;
if (!token) { if (!token) {
+4 -4
View File
@@ -8,8 +8,8 @@ import {
type ContactProtectionSettings, type ContactProtectionSettings,
} from "@/lib/contact-protection"; } from "@/lib/contact-protection";
function getClientIpFromHeaders() { async function getClientIpFromHeaders() {
const requestHeaders = headers(); const requestHeaders = await headers();
const forwardedFor = requestHeaders.get("x-forwarded-for"); const forwardedFor = requestHeaders.get("x-forwarded-for");
if (forwardedFor) { if (forwardedFor) {
@@ -41,7 +41,7 @@ export async function enforceContactRateLimit(settings: ContactProtectionSetting
return; return;
} }
const ip = getClientIpFromHeaders(); const ip = await getClientIpFromHeaders();
const key = getRateLimitKey(ip, settings.rateLimit.windowMinutes); const key = getRateLimitKey(ip, settings.rateLimit.windowMinutes);
await prisma.$transaction(async (tx) => { await prisma.$transaction(async (tx) => {
@@ -87,7 +87,7 @@ export async function verifyTurnstileToken(
const body = new URLSearchParams(); const body = new URLSearchParams();
body.set("secret", settings.turnstile.secretKey); body.set("secret", settings.turnstile.secretKey);
body.set("response", token); body.set("response", token);
body.set("remoteip", getClientIpFromHeaders()); body.set("remoteip", await getClientIpFromHeaders());
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST", method: "POST",
+1609 -966
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -4,9 +4,9 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build --webpack",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint .",
"test": "vitest run", "test": "vitest run",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"db:migrate": "prisma migrate deploy", "db:migrate": "prisma migrate deploy",
@@ -30,13 +30,13 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.35.0", "framer-motion": "^12.35.0",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"next": "14.2.35", "next": "^16.1.6",
"next-intl": "^4.8.3", "next-intl": "^4.8.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nodemailer": "^8.0.1", "nodemailer": "^8.0.1",
"pg": "^8.20.0", "pg": "^8.20.0",
"react": "^18", "react": "^19.2.4",
"react-dom": "^18", "react-dom": "^19.2.4",
"react-hook-form": "^7.71.2", "react-hook-form": "^7.71.2",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
@@ -45,10 +45,10 @@
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/pg": "^8.18.0", "@types/pg": "^8.18.0",
"@types/react": "^18", "@types/react": "^19.2.14",
"@types/react-dom": "^18", "@types/react-dom": "^19.2.3",
"eslint": "^8", "eslint": "^9.39.4",
"eslint-config-next": "14.2.35", "eslint-config-next": "^16.1.6",
"postcss": "^8", "postcss": "^8",
"prisma": "^7.4.2", "prisma": "^7.4.2",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
+21 -6
View File
@@ -1,6 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -10,7 +14,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -18,9 +22,20 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
} "./*"
]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "target": "ES2017"
"exclude": ["node_modules"] },
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }