From ac4654329b189b1e37e6cbbf0817128716e6365a Mon Sep 17 00:00:00 2001 From: sawinez Date: Thu, 2 Apr 2026 11:34:21 -0500 Subject: [PATCH 1/2] coming soon functionality Added global coming soon mode with route-level control --- apps/web/src/app/coming-soon/page.tsx | 10 ++++ apps/web/src/components/comingSoon.tsx | 64 ++++++++++++++++++++++++++ apps/web/src/env.ts | 41 +++++++++++------ apps/web/src/lib/feature_flags.ts | 17 +++++++ apps/web/src/middleware.ts | 27 ++++++++--- 5 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/app/coming-soon/page.tsx create mode 100644 apps/web/src/components/comingSoon.tsx create mode 100644 apps/web/src/lib/feature_flags.ts diff --git a/apps/web/src/app/coming-soon/page.tsx b/apps/web/src/app/coming-soon/page.tsx new file mode 100644 index 00000000..cf258b00 --- /dev/null +++ b/apps/web/src/app/coming-soon/page.tsx @@ -0,0 +1,10 @@ +import ComingSoon from "@/components/comingSoon"; + +export default function ComingSoonPage() { + return ( + + ); +} \ No newline at end of file diff --git a/apps/web/src/components/comingSoon.tsx b/apps/web/src/components/comingSoon.tsx new file mode 100644 index 00000000..eff70865 --- /dev/null +++ b/apps/web/src/components/comingSoon.tsx @@ -0,0 +1,64 @@ +import Image from "next/image"; +import Link from "next/link"; + +type ComingSoonProps = { + title?: string; + description?: string; +}; + +export default function ComingSoon({ + title = "Coming Soon", + description = "We’re still building this page. Check back soon.", +}: ComingSoonProps) { + return ( +
+
+
+
+ +
+
+
+
+ +
+

+ {title} +

+ +

+ {description} +

+ +
+ + Back to Home + +
+
+
+ +
+
+
+ HackKit Logo + +

+ This page is temporarily unavailable while setup is in progress. +

+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 4c9ec8d4..38ec3146 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -1,31 +1,42 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; +const isComingSoonMode = process.env.COMING_SOON_MODE === "true"; + +const requiredWhenLive = (schema: T) => + isComingSoonMode ? schema.optional() : schema; + export const env = createEnv({ server: { - CLERK_SECRET_KEY: z.string(), - INTERNAL_AUTH_KEY: z.string().min(64, { - message: "INTERNAL_AUTH_KEY must be at least 64 characters", - }), - BOT_API_URL: z.string(), + COMING_SOON_MODE: z.enum(["true", "false"]).default("false"), + + CLERK_SECRET_KEY: requiredWhenLive(z.string()), + INTERNAL_AUTH_KEY: requiredWhenLive( + z.string().min(64, { + message: "INTERNAL_AUTH_KEY must be at least 64 characters", + }), + ), + BOT_API_URL: requiredWhenLive(z.string()), NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), - CLOUDFLARE_ACCOUNT_ID: z.string(), - R2_ACCESS_KEY_ID: z.string(), - R2_SECRET_ACCESS_KEY: z.string(), - TURSO_AUTH_TOKEN: z.string(), - TURSO_DATABASE_URL: z.string(), - UPSTASH_REDIS_REST_TOKEN: z.string(), - UPSTASH_REDIS_REST_URL: z.string(), + CLOUDFLARE_ACCOUNT_ID: requiredWhenLive(z.string()), + R2_ACCESS_KEY_ID: requiredWhenLive(z.string()), + R2_SECRET_ACCESS_KEY: requiredWhenLive(z.string()), + TURSO_AUTH_TOKEN: requiredWhenLive(z.string()), + TURSO_DATABASE_URL: requiredWhenLive(z.string()), + UPSTASH_REDIS_REST_TOKEN: requiredWhenLive(z.string()), + UPSTASH_REDIS_REST_URL: requiredWhenLive(z.string()), }, client: { - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: isComingSoonMode + ? z.string().optional() + : z.string(), }, experimental__runtimeEnv: { + COMING_SOON_MODE: process.env.COMING_SOON_MODE, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, }, - // Enable the flag to treat empty strings as undefined emptyStringAsUndefined: true, -}); +}); \ No newline at end of file diff --git a/apps/web/src/lib/feature_flags.ts b/apps/web/src/lib/feature_flags.ts new file mode 100644 index 00000000..bb8393ea --- /dev/null +++ b/apps/web/src/lib/feature_flags.ts @@ -0,0 +1,17 @@ +export const featureFlags = { + comingSoonMode: process.env.COMING_SOON_MODE === "true", +}; + +export const comingSoonRoutes = [ + "/faq", + "/register", +] as const; + +export function isComingSoonRoute(pathname: string) { + return ( + featureFlags.comingSoonMode && + comingSoonRoutes.some( + (route) => pathname === route || pathname.startsWith(`${route}/`), + ) + ); +} \ No newline at end of file diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index d79f757c..db14e82d 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -4,26 +4,41 @@ import { publicRoutes } from "config"; import { bannedUsers } from "db/schema"; import { db } from "db"; import { eq } from "db/drizzle"; +import { isComingSoonRoute } from "@/lib/feature_flags"; const isPublicRoute = createRouteMatcher(publicRoutes); export default clerkMiddleware(async (auth, req) => { - if (req.nextUrl.pathname.startsWith("/@")) { + const pathname = req.nextUrl.pathname; + + if (pathname.startsWith("/@")) { return NextResponse.rewrite( - new URL(`/user/${req.nextUrl.pathname.replace("/@", "")}`, req.url), + new URL(`/user/${pathname.replace("/@", "")}`, req.url), ); } - if (req.nextUrl.pathname.startsWith("/~")) { + + if (pathname.startsWith("/~")) { return NextResponse.rewrite( - new URL(`/team/${req.nextUrl.pathname.replace("/~", "")}`, req.url), + new URL(`/team/${pathname.replace("/~", "")}`, req.url), ); } + if (pathname === "/coming-soon") { + return NextResponse.next(); + } + + // rewrite selected routes to the coming soon page before auth runs + if (isComingSoonRoute(pathname)) { + return NextResponse.rewrite(new URL("/coming-soon", req.url)); + } + if (!isPublicRoute(req)) { await auth.protect(); + const authData = await auth(); + const isBanned = !!(await db.query.bannedUsers.findFirst({ - where: eq(bannedUsers.userID, (await auth()).userId!), + where: eq(bannedUsers.userID, authData.userId!), })); if (isBanned) { @@ -36,4 +51,4 @@ export default clerkMiddleware(async (auth, req) => { export const config = { matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api)(.*)"], -}; +}; \ No newline at end of file From c12cdba3e1d47fc3fb541c7f7cbd8f55a1e39b64 Mon Sep 17 00:00:00 2001 From: sawinez Date: Thu, 2 Apr 2026 14:39:24 -0500 Subject: [PATCH 2/2] Update feature_flags.ts works w/o env --- apps/web/src/lib/feature_flags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/lib/feature_flags.ts b/apps/web/src/lib/feature_flags.ts index bb8393ea..7cfe797d 100644 --- a/apps/web/src/lib/feature_flags.ts +++ b/apps/web/src/lib/feature_flags.ts @@ -1,5 +1,5 @@ export const featureFlags = { - comingSoonMode: process.env.COMING_SOON_MODE === "true", + comingSoonMode: true }; export const comingSoonRoutes = [