From bf583099df26f35a22c28f8e5494ef6c67d1ddaa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Znamen=C3=A1=C4=8Dek?=
Date: Thu, 11 Dec 2025 10:34:08 +0100
Subject: [PATCH 1/8] Introduce routing groups to enable layout exceptions
---
app/{ => (app)}/MobileNav.tsx | 0
app/{ => (app)}/Navigation.tsx | 0
app/{ => (app)}/SessionToolbar.tsx | 0
app/{ => (app)}/SiteNav.tsx | 0
app/{ => (app)}/account/AvatarUploader.tsx | 0
app/{ => (app)}/account/NotificationsTab.tsx | 0
app/{ => (app)}/account/SignedInPage.tsx | 0
app/{ => (app)}/account/SignedOutPage.tsx | 0
app/{ => (app)}/account/UserProfileTab.tsx | 2 +-
app/{ => (app)}/account/me/route.ts | 0
app/{ => (app)}/account/newsletters/route.ts | 0
.../account/newsletters/subscribe/route.ts | 0
.../account/newsletters/subscribe/test.e2e.ts | 0
.../notifications/unsubscribe/route.ts | 0
app/{ => (app)}/account/page.tsx | 0
.../account/profile-picture/route.ts | 0
app/{ => (app)}/auth/error/RetryButton.tsx | 0
app/{ => (app)}/auth/error/page.tsx | 0
app/{ => (app)}/auth/layout.tsx | 0
app/{ => (app)}/auth/sent/RetryButton.tsx | 0
app/{ => (app)}/auth/sent/page.tsx | 0
app/{ => (app)}/auth/sign-in/SignInForm.tsx | 0
app/{ => (app)}/auth/sign-in/page.tsx | 0
.../events/[slug]/QuickRegistrationButton.tsx | 0
app/{ => (app)}/events/[slug]/ical/route.ts | 0
app/{ => (app)}/events/[slug]/page.tsx | 3 ++-
.../[slug]/registration-status/route.ts | 0
app/{ => (app)}/events/feed.ical/route.ts | 0
app/{ => (app)}/events/page.tsx | 0
app/{ => (app)}/join/SignUpForm.tsx | 0
app/{ => (app)}/join/arrows.svg | 0
app/{ => (app)}/join/check-email/route.ts | 0
app/{ => (app)}/join/form-state.test.ts | 0
app/{ => (app)}/join/form-state.ts | 0
app/{ => (app)}/join/form.e2e.ts | 0
app/{ => (app)}/join/page.tsx | 0
app/{ => (app)}/layout.tsx | 2 +-
app/{ => (app)}/not-found.tsx | 0
.../opportunities/NotificationPrefs.tsx | 0
.../opportunities/[slug]/ResponseButton.tsx | 0
app/{ => (app)}/opportunities/[slug]/page.tsx | 2 +-
app/{ => (app)}/opportunities/not-found.tsx | 0
app/{ => (app)}/opportunities/page.tsx | 0
app/{ => (app)}/page.tsx | 0
.../people/ProfileVisibilityPrefs.tsx | 0
.../people/SearchablePeopleBox.tsx | 0
app/{ => (app)}/people/[id]/ContactInfo.tsx | 0
app/{ => (app)}/people/[id]/InfoRow.tsx | 0
.../people/[id]/UpdateProfileButton.tsx | 0
app/{ => (app)}/people/[id]/page.tsx | 0
app/{ => (app)}/people/matching.ts | 0
app/{ => (app)}/people/page.tsx | 6 +++---
.../projects/[slug]/ProjectTeamSection.tsx | 0
app/{ => (app)}/projects/[slug]/page.tsx | 0
app/{ => (app)}/projects/page.tsx | 0
app/{ => (app)}/stats/DetailedChart.tsx | 0
app/{ => (app)}/stats/OverviewChart.tsx | 0
app/{ => (app)}/stats/[slug]/csv/route.ts | 0
app/{ => (app)}/stats/[slug]/page.tsx | 2 +-
app/{ => (app)}/stats/page.tsx | 2 +-
app/{ => (app)}/stats/readme.md | 0
app/{ => (app)}/upload/UploadForm.tsx | 0
app/{ => (app)}/upload/page.tsx | 0
app/{ => (app)}/upload/upload/route.ts | 0
app/(standalone)/layout.tsx | 21 +++++++++++++++++++
65 files changed, 31 insertions(+), 9 deletions(-)
rename app/{ => (app)}/MobileNav.tsx (100%)
rename app/{ => (app)}/Navigation.tsx (100%)
rename app/{ => (app)}/SessionToolbar.tsx (100%)
rename app/{ => (app)}/SiteNav.tsx (100%)
rename app/{ => (app)}/account/AvatarUploader.tsx (100%)
rename app/{ => (app)}/account/NotificationsTab.tsx (100%)
rename app/{ => (app)}/account/SignedInPage.tsx (100%)
rename app/{ => (app)}/account/SignedOutPage.tsx (100%)
rename app/{ => (app)}/account/UserProfileTab.tsx (99%)
rename app/{ => (app)}/account/me/route.ts (100%)
rename app/{ => (app)}/account/newsletters/route.ts (100%)
rename app/{ => (app)}/account/newsletters/subscribe/route.ts (100%)
rename app/{ => (app)}/account/newsletters/subscribe/test.e2e.ts (100%)
rename app/{ => (app)}/account/notifications/unsubscribe/route.ts (100%)
rename app/{ => (app)}/account/page.tsx (100%)
rename app/{ => (app)}/account/profile-picture/route.ts (100%)
rename app/{ => (app)}/auth/error/RetryButton.tsx (100%)
rename app/{ => (app)}/auth/error/page.tsx (100%)
rename app/{ => (app)}/auth/layout.tsx (100%)
rename app/{ => (app)}/auth/sent/RetryButton.tsx (100%)
rename app/{ => (app)}/auth/sent/page.tsx (100%)
rename app/{ => (app)}/auth/sign-in/SignInForm.tsx (100%)
rename app/{ => (app)}/auth/sign-in/page.tsx (100%)
rename app/{ => (app)}/events/[slug]/QuickRegistrationButton.tsx (100%)
rename app/{ => (app)}/events/[slug]/ical/route.ts (100%)
rename app/{ => (app)}/events/[slug]/page.tsx (98%)
rename app/{ => (app)}/events/[slug]/registration-status/route.ts (100%)
rename app/{ => (app)}/events/feed.ical/route.ts (100%)
rename app/{ => (app)}/events/page.tsx (100%)
rename app/{ => (app)}/join/SignUpForm.tsx (100%)
rename app/{ => (app)}/join/arrows.svg (100%)
rename app/{ => (app)}/join/check-email/route.ts (100%)
rename app/{ => (app)}/join/form-state.test.ts (100%)
rename app/{ => (app)}/join/form-state.ts (100%)
rename app/{ => (app)}/join/form.e2e.ts (100%)
rename app/{ => (app)}/join/page.tsx (100%)
rename app/{ => (app)}/layout.tsx (98%)
rename app/{ => (app)}/not-found.tsx (100%)
rename app/{ => (app)}/opportunities/NotificationPrefs.tsx (100%)
rename app/{ => (app)}/opportunities/[slug]/ResponseButton.tsx (100%)
rename app/{ => (app)}/opportunities/[slug]/page.tsx (98%)
rename app/{ => (app)}/opportunities/not-found.tsx (100%)
rename app/{ => (app)}/opportunities/page.tsx (100%)
rename app/{ => (app)}/page.tsx (100%)
rename app/{ => (app)}/people/ProfileVisibilityPrefs.tsx (100%)
rename app/{ => (app)}/people/SearchablePeopleBox.tsx (100%)
rename app/{ => (app)}/people/[id]/ContactInfo.tsx (100%)
rename app/{ => (app)}/people/[id]/InfoRow.tsx (100%)
rename app/{ => (app)}/people/[id]/UpdateProfileButton.tsx (100%)
rename app/{ => (app)}/people/[id]/page.tsx (100%)
rename app/{ => (app)}/people/matching.ts (100%)
rename app/{ => (app)}/people/page.tsx (84%)
rename app/{ => (app)}/projects/[slug]/ProjectTeamSection.tsx (100%)
rename app/{ => (app)}/projects/[slug]/page.tsx (100%)
rename app/{ => (app)}/projects/page.tsx (100%)
rename app/{ => (app)}/stats/DetailedChart.tsx (100%)
rename app/{ => (app)}/stats/OverviewChart.tsx (100%)
rename app/{ => (app)}/stats/[slug]/csv/route.ts (100%)
rename app/{ => (app)}/stats/[slug]/page.tsx (98%)
rename app/{ => (app)}/stats/page.tsx (98%)
rename app/{ => (app)}/stats/readme.md (100%)
rename app/{ => (app)}/upload/UploadForm.tsx (100%)
rename app/{ => (app)}/upload/page.tsx (100%)
rename app/{ => (app)}/upload/upload/route.ts (100%)
create mode 100644 app/(standalone)/layout.tsx
diff --git a/app/MobileNav.tsx b/app/(app)/MobileNav.tsx
similarity index 100%
rename from app/MobileNav.tsx
rename to app/(app)/MobileNav.tsx
diff --git a/app/Navigation.tsx b/app/(app)/Navigation.tsx
similarity index 100%
rename from app/Navigation.tsx
rename to app/(app)/Navigation.tsx
diff --git a/app/SessionToolbar.tsx b/app/(app)/SessionToolbar.tsx
similarity index 100%
rename from app/SessionToolbar.tsx
rename to app/(app)/SessionToolbar.tsx
diff --git a/app/SiteNav.tsx b/app/(app)/SiteNav.tsx
similarity index 100%
rename from app/SiteNav.tsx
rename to app/(app)/SiteNav.tsx
diff --git a/app/account/AvatarUploader.tsx b/app/(app)/account/AvatarUploader.tsx
similarity index 100%
rename from app/account/AvatarUploader.tsx
rename to app/(app)/account/AvatarUploader.tsx
diff --git a/app/account/NotificationsTab.tsx b/app/(app)/account/NotificationsTab.tsx
similarity index 100%
rename from app/account/NotificationsTab.tsx
rename to app/(app)/account/NotificationsTab.tsx
diff --git a/app/account/SignedInPage.tsx b/app/(app)/account/SignedInPage.tsx
similarity index 100%
rename from app/account/SignedInPage.tsx
rename to app/(app)/account/SignedInPage.tsx
diff --git a/app/account/SignedOutPage.tsx b/app/(app)/account/SignedOutPage.tsx
similarity index 100%
rename from app/account/SignedOutPage.tsx
rename to app/(app)/account/SignedOutPage.tsx
diff --git a/app/account/UserProfileTab.tsx b/app/(app)/account/UserProfileTab.tsx
similarity index 99%
rename from app/account/UserProfileTab.tsx
rename to app/(app)/account/UserProfileTab.tsx
index 7c4d7581e..0c4d1a604 100644
--- a/app/account/UserProfileTab.tsx
+++ b/app/(app)/account/UserProfileTab.tsx
@@ -10,7 +10,7 @@ import Link from "next/link";
import clsx from "clsx";
import { useSession } from "next-auth/react";
-import { AvatarUploader } from "~/app/account/AvatarUploader";
+import { AvatarUploader } from "~/app/(app)/account/AvatarUploader";
import { CopyToClipboardButton } from "~/components/CopyToClipboardButton";
import { DistrictSelect } from "~/components/districts/DistrictSelect";
import { FormError } from "~/components/form/FormError";
diff --git a/app/account/me/route.ts b/app/(app)/account/me/route.ts
similarity index 100%
rename from app/account/me/route.ts
rename to app/(app)/account/me/route.ts
diff --git a/app/account/newsletters/route.ts b/app/(app)/account/newsletters/route.ts
similarity index 100%
rename from app/account/newsletters/route.ts
rename to app/(app)/account/newsletters/route.ts
diff --git a/app/account/newsletters/subscribe/route.ts b/app/(app)/account/newsletters/subscribe/route.ts
similarity index 100%
rename from app/account/newsletters/subscribe/route.ts
rename to app/(app)/account/newsletters/subscribe/route.ts
diff --git a/app/account/newsletters/subscribe/test.e2e.ts b/app/(app)/account/newsletters/subscribe/test.e2e.ts
similarity index 100%
rename from app/account/newsletters/subscribe/test.e2e.ts
rename to app/(app)/account/newsletters/subscribe/test.e2e.ts
diff --git a/app/account/notifications/unsubscribe/route.ts b/app/(app)/account/notifications/unsubscribe/route.ts
similarity index 100%
rename from app/account/notifications/unsubscribe/route.ts
rename to app/(app)/account/notifications/unsubscribe/route.ts
diff --git a/app/account/page.tsx b/app/(app)/account/page.tsx
similarity index 100%
rename from app/account/page.tsx
rename to app/(app)/account/page.tsx
diff --git a/app/account/profile-picture/route.ts b/app/(app)/account/profile-picture/route.ts
similarity index 100%
rename from app/account/profile-picture/route.ts
rename to app/(app)/account/profile-picture/route.ts
diff --git a/app/auth/error/RetryButton.tsx b/app/(app)/auth/error/RetryButton.tsx
similarity index 100%
rename from app/auth/error/RetryButton.tsx
rename to app/(app)/auth/error/RetryButton.tsx
diff --git a/app/auth/error/page.tsx b/app/(app)/auth/error/page.tsx
similarity index 100%
rename from app/auth/error/page.tsx
rename to app/(app)/auth/error/page.tsx
diff --git a/app/auth/layout.tsx b/app/(app)/auth/layout.tsx
similarity index 100%
rename from app/auth/layout.tsx
rename to app/(app)/auth/layout.tsx
diff --git a/app/auth/sent/RetryButton.tsx b/app/(app)/auth/sent/RetryButton.tsx
similarity index 100%
rename from app/auth/sent/RetryButton.tsx
rename to app/(app)/auth/sent/RetryButton.tsx
diff --git a/app/auth/sent/page.tsx b/app/(app)/auth/sent/page.tsx
similarity index 100%
rename from app/auth/sent/page.tsx
rename to app/(app)/auth/sent/page.tsx
diff --git a/app/auth/sign-in/SignInForm.tsx b/app/(app)/auth/sign-in/SignInForm.tsx
similarity index 100%
rename from app/auth/sign-in/SignInForm.tsx
rename to app/(app)/auth/sign-in/SignInForm.tsx
diff --git a/app/auth/sign-in/page.tsx b/app/(app)/auth/sign-in/page.tsx
similarity index 100%
rename from app/auth/sign-in/page.tsx
rename to app/(app)/auth/sign-in/page.tsx
diff --git a/app/events/[slug]/QuickRegistrationButton.tsx b/app/(app)/events/[slug]/QuickRegistrationButton.tsx
similarity index 100%
rename from app/events/[slug]/QuickRegistrationButton.tsx
rename to app/(app)/events/[slug]/QuickRegistrationButton.tsx
diff --git a/app/events/[slug]/ical/route.ts b/app/(app)/events/[slug]/ical/route.ts
similarity index 100%
rename from app/events/[slug]/ical/route.ts
rename to app/(app)/events/[slug]/ical/route.ts
diff --git a/app/events/[slug]/page.tsx b/app/(app)/events/[slug]/page.tsx
similarity index 98%
rename from app/events/[slug]/page.tsx
rename to app/(app)/events/[slug]/page.tsx
index 7e9a77fea..3bd3fc377 100644
--- a/app/events/[slug]/page.tsx
+++ b/app/(app)/events/[slug]/page.tsx
@@ -2,7 +2,6 @@ import { type Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
-import { QuickRegistrationButton } from "~/app/events/[slug]/QuickRegistrationButton";
import { Breadcrumbs } from "~/components/Breadcrumbs";
import { EventCard } from "~/components/EventCard";
import { ImageLabel, ProjectImageLabel } from "~/components/ImageLabel";
@@ -20,6 +19,8 @@ import { getUserProfile, type UserProfile } from "~/src/data/user-profile";
import { Route } from "~/src/routing";
import { defaultAvatarUrl } from "~/src/utils";
+import { QuickRegistrationButton } from "./QuickRegistrationButton";
+
type Params = {
slug: string;
};
diff --git a/app/events/[slug]/registration-status/route.ts b/app/(app)/events/[slug]/registration-status/route.ts
similarity index 100%
rename from app/events/[slug]/registration-status/route.ts
rename to app/(app)/events/[slug]/registration-status/route.ts
diff --git a/app/events/feed.ical/route.ts b/app/(app)/events/feed.ical/route.ts
similarity index 100%
rename from app/events/feed.ical/route.ts
rename to app/(app)/events/feed.ical/route.ts
diff --git a/app/events/page.tsx b/app/(app)/events/page.tsx
similarity index 100%
rename from app/events/page.tsx
rename to app/(app)/events/page.tsx
diff --git a/app/join/SignUpForm.tsx b/app/(app)/join/SignUpForm.tsx
similarity index 100%
rename from app/join/SignUpForm.tsx
rename to app/(app)/join/SignUpForm.tsx
diff --git a/app/join/arrows.svg b/app/(app)/join/arrows.svg
similarity index 100%
rename from app/join/arrows.svg
rename to app/(app)/join/arrows.svg
diff --git a/app/join/check-email/route.ts b/app/(app)/join/check-email/route.ts
similarity index 100%
rename from app/join/check-email/route.ts
rename to app/(app)/join/check-email/route.ts
diff --git a/app/join/form-state.test.ts b/app/(app)/join/form-state.test.ts
similarity index 100%
rename from app/join/form-state.test.ts
rename to app/(app)/join/form-state.test.ts
diff --git a/app/join/form-state.ts b/app/(app)/join/form-state.ts
similarity index 100%
rename from app/join/form-state.ts
rename to app/(app)/join/form-state.ts
diff --git a/app/join/form.e2e.ts b/app/(app)/join/form.e2e.ts
similarity index 100%
rename from app/join/form.e2e.ts
rename to app/(app)/join/form.e2e.ts
diff --git a/app/join/page.tsx b/app/(app)/join/page.tsx
similarity index 100%
rename from app/join/page.tsx
rename to app/(app)/join/page.tsx
diff --git a/app/layout.tsx b/app/(app)/layout.tsx
similarity index 98%
rename from app/layout.tsx
rename to app/(app)/layout.tsx
index 8a04787ae..d8d8b6607 100644
--- a/app/layout.tsx
+++ b/app/(app)/layout.tsx
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
-import "./globals.css";
+import "../globals.css";
import { SessionProvider } from "components/SessionProvider";
import { Toaster } from "react-hot-toast";
diff --git a/app/not-found.tsx b/app/(app)/not-found.tsx
similarity index 100%
rename from app/not-found.tsx
rename to app/(app)/not-found.tsx
diff --git a/app/opportunities/NotificationPrefs.tsx b/app/(app)/opportunities/NotificationPrefs.tsx
similarity index 100%
rename from app/opportunities/NotificationPrefs.tsx
rename to app/(app)/opportunities/NotificationPrefs.tsx
diff --git a/app/opportunities/[slug]/ResponseButton.tsx b/app/(app)/opportunities/[slug]/ResponseButton.tsx
similarity index 100%
rename from app/opportunities/[slug]/ResponseButton.tsx
rename to app/(app)/opportunities/[slug]/ResponseButton.tsx
diff --git a/app/opportunities/[slug]/page.tsx b/app/(app)/opportunities/[slug]/page.tsx
similarity index 98%
rename from app/opportunities/[slug]/page.tsx
rename to app/(app)/opportunities/[slug]/page.tsx
index cfb1487ab..d03795b65 100644
--- a/app/opportunities/[slug]/page.tsx
+++ b/app/(app)/opportunities/[slug]/page.tsx
@@ -2,7 +2,7 @@ import { type Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
-import { ResponseButton } from "~/app/opportunities/[slug]/ResponseButton";
+import { ResponseButton } from "~/app/(app)/opportunities/[slug]/ResponseButton";
import { Breadcrumbs } from "~/components/Breadcrumbs";
import { ImageLabel, ProjectImageLabel } from "~/components/ImageLabel";
import { MarkdownContent } from "~/components/MarkdownContent";
diff --git a/app/opportunities/not-found.tsx b/app/(app)/opportunities/not-found.tsx
similarity index 100%
rename from app/opportunities/not-found.tsx
rename to app/(app)/opportunities/not-found.tsx
diff --git a/app/opportunities/page.tsx b/app/(app)/opportunities/page.tsx
similarity index 100%
rename from app/opportunities/page.tsx
rename to app/(app)/opportunities/page.tsx
diff --git a/app/page.tsx b/app/(app)/page.tsx
similarity index 100%
rename from app/page.tsx
rename to app/(app)/page.tsx
diff --git a/app/people/ProfileVisibilityPrefs.tsx b/app/(app)/people/ProfileVisibilityPrefs.tsx
similarity index 100%
rename from app/people/ProfileVisibilityPrefs.tsx
rename to app/(app)/people/ProfileVisibilityPrefs.tsx
diff --git a/app/people/SearchablePeopleBox.tsx b/app/(app)/people/SearchablePeopleBox.tsx
similarity index 100%
rename from app/people/SearchablePeopleBox.tsx
rename to app/(app)/people/SearchablePeopleBox.tsx
diff --git a/app/people/[id]/ContactInfo.tsx b/app/(app)/people/[id]/ContactInfo.tsx
similarity index 100%
rename from app/people/[id]/ContactInfo.tsx
rename to app/(app)/people/[id]/ContactInfo.tsx
diff --git a/app/people/[id]/InfoRow.tsx b/app/(app)/people/[id]/InfoRow.tsx
similarity index 100%
rename from app/people/[id]/InfoRow.tsx
rename to app/(app)/people/[id]/InfoRow.tsx
diff --git a/app/people/[id]/UpdateProfileButton.tsx b/app/(app)/people/[id]/UpdateProfileButton.tsx
similarity index 100%
rename from app/people/[id]/UpdateProfileButton.tsx
rename to app/(app)/people/[id]/UpdateProfileButton.tsx
diff --git a/app/people/[id]/page.tsx b/app/(app)/people/[id]/page.tsx
similarity index 100%
rename from app/people/[id]/page.tsx
rename to app/(app)/people/[id]/page.tsx
diff --git a/app/people/matching.ts b/app/(app)/people/matching.ts
similarity index 100%
rename from app/people/matching.ts
rename to app/(app)/people/matching.ts
diff --git a/app/people/page.tsx b/app/(app)/people/page.tsx
similarity index 84%
rename from app/people/page.tsx
rename to app/(app)/people/page.tsx
index 8173f70d3..e95efc50d 100644
--- a/app/people/page.tsx
+++ b/app/(app)/people/page.tsx
@@ -1,9 +1,9 @@
import { Suspense } from "react";
import { type Metadata } from "next";
-import { stripNonPublicFields } from "~/app/people/matching";
-import { ProfileVisibilityPrefs } from "~/app/people/ProfileVisibilityPrefs";
-import { SearchablePeopleBox } from "~/app/people/SearchablePeopleBox";
+import { stripNonPublicFields } from "~/app/(app)/people/matching";
+import { ProfileVisibilityPrefs } from "~/app/(app)/people/ProfileVisibilityPrefs";
+import { SearchablePeopleBox } from "~/app/(app)/people/SearchablePeopleBox";
import { Breadcrumbs } from "~/components/Breadcrumbs";
import { getAllUserProfiles } from "~/src/data/user-profile";
import { strip } from "~/src/utils";
diff --git a/app/projects/[slug]/ProjectTeamSection.tsx b/app/(app)/projects/[slug]/ProjectTeamSection.tsx
similarity index 100%
rename from app/projects/[slug]/ProjectTeamSection.tsx
rename to app/(app)/projects/[slug]/ProjectTeamSection.tsx
diff --git a/app/projects/[slug]/page.tsx b/app/(app)/projects/[slug]/page.tsx
similarity index 100%
rename from app/projects/[slug]/page.tsx
rename to app/(app)/projects/[slug]/page.tsx
diff --git a/app/projects/page.tsx b/app/(app)/projects/page.tsx
similarity index 100%
rename from app/projects/page.tsx
rename to app/(app)/projects/page.tsx
diff --git a/app/stats/DetailedChart.tsx b/app/(app)/stats/DetailedChart.tsx
similarity index 100%
rename from app/stats/DetailedChart.tsx
rename to app/(app)/stats/DetailedChart.tsx
diff --git a/app/stats/OverviewChart.tsx b/app/(app)/stats/OverviewChart.tsx
similarity index 100%
rename from app/stats/OverviewChart.tsx
rename to app/(app)/stats/OverviewChart.tsx
diff --git a/app/stats/[slug]/csv/route.ts b/app/(app)/stats/[slug]/csv/route.ts
similarity index 100%
rename from app/stats/[slug]/csv/route.ts
rename to app/(app)/stats/[slug]/csv/route.ts
diff --git a/app/stats/[slug]/page.tsx b/app/(app)/stats/[slug]/page.tsx
similarity index 98%
rename from app/stats/[slug]/page.tsx
rename to app/(app)/stats/[slug]/page.tsx
index 14d43e650..957846f90 100644
--- a/app/stats/[slug]/page.tsx
+++ b/app/(app)/stats/[slug]/page.tsx
@@ -1,6 +1,6 @@
import { notFound } from "next/navigation";
-import { DetailedChart } from "~/app/stats/DetailedChart";
+import { DetailedChart } from "~/app/(app)/stats/DetailedChart";
import { Breadcrumbs } from "~/components/Breadcrumbs";
import { ImageLabel } from "~/components/ImageLabel";
import { Sidebar, SidebarCTA, SidebarSection } from "~/components/Sidebar";
diff --git a/app/stats/page.tsx b/app/(app)/stats/page.tsx
similarity index 98%
rename from app/stats/page.tsx
rename to app/(app)/stats/page.tsx
index feadf5965..bacfe3080 100644
--- a/app/stats/page.tsx
+++ b/app/(app)/stats/page.tsx
@@ -1,6 +1,6 @@
import { type Metadata } from "next";
-import { OverviewChart } from "~/app/stats/OverviewChart";
+import { OverviewChart } from "~/app/(app)/stats/OverviewChart";
import { Breadcrumbs } from "~/components/Breadcrumbs";
import {
getAllMetricDefinitions,
diff --git a/app/stats/readme.md b/app/(app)/stats/readme.md
similarity index 100%
rename from app/stats/readme.md
rename to app/(app)/stats/readme.md
diff --git a/app/upload/UploadForm.tsx b/app/(app)/upload/UploadForm.tsx
similarity index 100%
rename from app/upload/UploadForm.tsx
rename to app/(app)/upload/UploadForm.tsx
diff --git a/app/upload/page.tsx b/app/(app)/upload/page.tsx
similarity index 100%
rename from app/upload/page.tsx
rename to app/(app)/upload/page.tsx
diff --git a/app/upload/upload/route.ts b/app/(app)/upload/upload/route.ts
similarity index 100%
rename from app/upload/upload/route.ts
rename to app/(app)/upload/upload/route.ts
diff --git a/app/(standalone)/layout.tsx b/app/(standalone)/layout.tsx
new file mode 100644
index 000000000..f607a997c
--- /dev/null
+++ b/app/(standalone)/layout.tsx
@@ -0,0 +1,21 @@
+import "../globals.css";
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {children}
+
+ );
+}
From ed6e2abe77eb90f086e5dc013ea1a1374ad9e34f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Znamen=C3=A1=C4=8Dek?=
Date: Fri, 12 Dec 2025 14:20:15 +0100
Subject: [PATCH 2/8] Add support for creating CRM leads and querying contacts
by e-mail
---
src/espocrm/espo.ts | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/src/espocrm/espo.ts b/src/espocrm/espo.ts
index f44232d62..7e7324182 100644
--- a/src/espocrm/espo.ts
+++ b/src/espocrm/espo.ts
@@ -185,6 +185,7 @@ type Entity =
| "Contact"
| "Account"
| "TargetList"
+ | "Lead"
| "Meeting"
| "CProject"
| "CTeamEngagement"
@@ -305,6 +306,24 @@ export const espoUpdateContact = updateObject("Contact", decodeContact);
/** Get all contacts */
export const espoGetAllContacts = getAllObjectsOfType("Contact", decodeContact);
+/**
+ * Get all contacts with a given e-mail address
+ *
+ * The e-mail address must be an exact string match.
+ * Both main e-mail address and `emailAddressData` are checked.
+ */
+export const espoGetContactsByEmail = (apiKey: string, email: string) =>
+ espoFetchAll({
+ path: "Contact",
+ searchParams: {
+ "whereGroup[0][type]": "equals",
+ "whereGroup[0][attribute]": "emailAddress",
+ "whereGroup[0][value]": email,
+ },
+ decodeResponse: decodeContact,
+ apiKey,
+ });
+
//
// Account
//
@@ -429,6 +448,21 @@ export const espoAddAccountsToTargetList = (
apiKey,
});
+//
+// Leads
+//
+
+export type Lead = decodeType;
+export const decodeLead = intersection(
+ decodePersonEntity,
+ record({
+ targetListsIds: maybe(array(string)),
+ }),
+);
+
+/** Create a new lead */
+export const espoCreateLead = createObject("Lead", decodeLead);
+
//
// Meetings
//
From 33720024d7e1282b2cf576f15071791ec65eb7e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Znamen=C3=A1=C4=8Dek?=
Date: Fri, 12 Dec 2025 15:02:56 +0100
Subject: [PATCH 3/8] Add first version of newsletter subscribe form
---
app/(standalone)/subscribe/SubscribePage.tsx | 236 +++++++++++++++++++
app/(standalone)/subscribe/page.tsx | 21 ++
app/(standalone)/subscribe/submit/route.ts | 66 ++++++
3 files changed, 323 insertions(+)
create mode 100644 app/(standalone)/subscribe/SubscribePage.tsx
create mode 100644 app/(standalone)/subscribe/page.tsx
create mode 100644 app/(standalone)/subscribe/submit/route.ts
diff --git a/app/(standalone)/subscribe/SubscribePage.tsx b/app/(standalone)/subscribe/SubscribePage.tsx
new file mode 100644
index 000000000..e2145c188
--- /dev/null
+++ b/app/(standalone)/subscribe/SubscribePage.tsx
@@ -0,0 +1,236 @@
+"use client";
+
+import { useState } from "react";
+
+import { RequiredFieldMarker } from "~/components/form/RequiredFieldMarker";
+import { looksLikeEmailAdress } from "~/src/utils";
+
+//
+// Model
+//
+
+type TargetGroupId = "cist.digital" | "nezisk.digital" | "sluzby.digital";
+
+type TargetGroup = {
+ id: TargetGroupId;
+ name: string;
+ description: string;
+};
+
+//
+// Subscribe Page
+//
+
+export const SubscribePage = () => {
+ const targetGroups: TargetGroup[] = [
+ {
+ id: "cist.digital",
+ name: "Číst.Digital",
+ description: "Jednou měsíčně hlavní novinky z Česko.Digital",
+ },
+ {
+ id: "nezisk.digital",
+ name: "Nezisk.Digital",
+ description: "Digitální transformace nezisku",
+ },
+ {
+ id: "sluzby.digital",
+ name: "Služby.Digital",
+ description: "Digitální transformace veřejné správy",
+ },
+ ];
+
+ return (
+
+ {state.errorMessage && }
+
{canSubmit(state) && (
-
);
};
-
-const sleep = (seconds: number) =>
- new Promise((resolve) => setTimeout(resolve, seconds * 1000));
From 4424ce3c9ec80bba94d5221af354bc8f5836179c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Znamen=C3=A1=C4=8Dek?=
Date: Mon, 15 Dec 2025 14:18:46 +0100
Subject: [PATCH 7/8] Trivial refactoring
---
app/(standalone)/subscribe/SubscribePage.tsx | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/app/(standalone)/subscribe/SubscribePage.tsx b/app/(standalone)/subscribe/SubscribePage.tsx
index 8662a9cc2..5c752c84c 100644
--- a/app/(standalone)/subscribe/SubscribePage.tsx
+++ b/app/(standalone)/subscribe/SubscribePage.tsx
@@ -43,6 +43,12 @@ const targetGroups: TargetGroup[] = [
type PageState = "filling-form" | "submitted-successfully";
+const encodeTargetGroupIds = (list: TargetGroupId[]) =>
+ new URLSearchParams(list.map((val) => ["g", val]));
+
+const decodeTargetGroupIds = (params: URLSearchParams) =>
+ params.getAll("g") as TargetGroupId[];
+
export const SubscribePage = () => {
const [pageState, setPageState] = useState("filling-form");
@@ -136,12 +142,6 @@ const initialFormState: FormState = {
email: "",
};
-const encodeTargetGroupIds = (list: TargetGroupId[]) =>
- new URLSearchParams(list.map((val) => ["g", val]));
-
-const decodeTargetGroupIds = (params: URLSearchParams) =>
- params.getAll("g") as TargetGroupId[];
-
const canSubmit = (state: FormState) =>
state.firstName !== "" &&
state.lastName !== "" &&
From 2a7465d26a79cb42b497b3af251cfc10e8c5695c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Znamen=C3=A1=C4=8Dek?=
Date: Mon, 15 Dec 2025 14:23:19 +0100
Subject: [PATCH 8/8] Normalize e-mail address before inserting
---
app/(standalone)/subscribe/submit/route.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/(standalone)/subscribe/submit/route.ts b/app/(standalone)/subscribe/submit/route.ts
index 610ec91a7..d37d5db7a 100644
--- a/app/(standalone)/subscribe/submit/route.ts
+++ b/app/(standalone)/subscribe/submit/route.ts
@@ -7,7 +7,7 @@ import {
espoCreateLead,
espoGetContactsByEmail,
} from "~/src/espocrm/espo";
-import { notEmpty } from "~/src/utils";
+import { normalizeEmailAddress, notEmpty } from "~/src/utils";
const targetListIds: Record = {
"cist.digital": "693c0cd42b2d08580",
@@ -42,7 +42,7 @@ export async function POST(request: NextRequest): Promise {
await espoCreateLead(crmApiKey, {
firstName: subscription.firstName,
lastName: subscription.lastName,
- emailAddress: subscription.email,
+ emailAddress: normalizeEmailAddress(subscription.email),
targetListsIds: selectedTargetListIds,
});
} else {