From 552157c0cab19c55120d49bba1c2314424ddcf34 Mon Sep 17 00:00:00 2001 From: Simo Date: Wed, 29 Oct 2025 12:02:18 +0200 Subject: [PATCH 1/2] wip Signed-off-by: Simo --- .../hooks/useArtworkSubmissionForm.test.ts | 9 +- .../waves/memes/traits/schema.test.ts | 5 +- app/api/farcaster/route.ts | 2 +- app/api/open-graph/proxy-image/route.ts | 3 + app/api/open-graph/route.ts | 2 - app/api/pepe/resolve/route.ts | 4 +- app/api/tiktok/route.ts | 1 - app/api/wikimedia-card/route.ts | 1 - .../hooks/useArtworkSubmissionForm.ts | 5 +- .../memes/submission/validation/index.ts | 5 +- .../waves/memes/traits/BooleanTrait.tsx | 12 +- .../waves/memes/traits/DropdownTrait.tsx | 13 +- components/waves/memes/traits/NumberTrait.tsx | 15 +- components/waves/memes/traits/Section.tsx | 7 +- .../waves/memes/traits/TraitWrapper.tsx | 12 +- components/waves/memes/traits/index.ts | 5 - components/waves/memes/traits/schema.ts | 5 +- components/waves/memes/traits/types.ts | 36 -- proxy.ts | 315 ------------------ services/websocket/index.ts | 4 +- 20 files changed, 78 insertions(+), 383 deletions(-) delete mode 100644 proxy.ts diff --git a/__tests__/components/waves/memes/submission/hooks/useArtworkSubmissionForm.test.ts b/__tests__/components/waves/memes/submission/hooks/useArtworkSubmissionForm.test.ts index ebdc817f43..cd163c45b5 100644 --- a/__tests__/components/waves/memes/submission/hooks/useArtworkSubmissionForm.test.ts +++ b/__tests__/components/waves/memes/submission/hooks/useArtworkSubmissionForm.test.ts @@ -1,7 +1,14 @@ import { renderHook, act } from '@testing-library/react'; import { useArtworkSubmissionForm } from '@/components/waves/memes/submission/hooks/useArtworkSubmissionForm'; -jest.mock('@/components/waves/memes/traits/schema', () => ({ initialTraits: { title: '', description: '', artist: '', seizeArtistProfile: '' } })); +jest.mock('@/components/waves/memes/traits/schema', () => ({ + getInitialTraitsValues: () => ({ + title: '', + description: '', + artist: '', + seizeArtistProfile: '', + }), +})); jest.mock('@/components/auth/Auth', () => ({ useAuth: jest.fn() })); const { useAuth } = require('@/components/auth/Auth'); diff --git a/__tests__/components/waves/memes/traits/schema.test.ts b/__tests__/components/waves/memes/traits/schema.test.ts index ae90d5aa7f..07278aeb9f 100644 --- a/__tests__/components/waves/memes/traits/schema.test.ts +++ b/__tests__/components/waves/memes/traits/schema.test.ts @@ -1,4 +1,4 @@ -import { getFormSections, initialTraits } from '@/components/waves/memes/traits/schema'; +import { getFormSections, getInitialTraitsValues } from '@/components/waves/memes/traits/schema'; describe('traits schema helpers', () => { it('replaces profile placeholder', () => { @@ -7,7 +7,8 @@ describe('traits schema helpers', () => { expect(artistField.placeholder).toBe('Bob'); }); - it('provides initial trait values via constant', () => { + it('provides initial trait values via helper', () => { + const initialTraits = getInitialTraitsValues(); expect(initialTraits.pointsPower).toBe(0); expect(initialTraits.punk6529).toBe(false); }); diff --git a/app/api/farcaster/route.ts b/app/api/farcaster/route.ts index f5f2731493..bdbd04a812 100644 --- a/app/api/farcaster/route.ts +++ b/app/api/farcaster/route.ts @@ -755,5 +755,5 @@ export async function GET(request: NextRequest) { } } +// ts-prune-ignore-next-line: Next.js reads this named export as route config export const dynamic = "force-dynamic"; -export const revalidate = 0; diff --git a/app/api/open-graph/proxy-image/route.ts b/app/api/open-graph/proxy-image/route.ts index 4d8f3ad7dc..4ded65e043 100644 --- a/app/api/open-graph/proxy-image/route.ts +++ b/app/api/open-graph/proxy-image/route.ts @@ -94,6 +94,7 @@ function mapGuardErrorToResponse(error: UrlGuardError): NextResponse { } } +// ts-prune-ignore-next: Next.js uses exported HTTP verb handlers via convention. export async function GET(request: NextRequest) { const target = request.nextUrl.searchParams.get("url"); @@ -109,7 +110,9 @@ export async function GET(request: NextRequest) { return proxyImage(remoteResult.url); } +// ts-prune-ignore-next: Next.js framework consumes this export via route conventions. export const dynamic = "force-dynamic"; +// ts-prune-ignore-next: Next.js framework consumes this export via route conventions. export const revalidate = 0; function parseRemoteUrl(target: string): { url: URL } | { response: NextResponse } { diff --git a/app/api/open-graph/route.ts b/app/api/open-graph/route.ts index 3bed156335..f38a08455d 100644 --- a/app/api/open-graph/route.ts +++ b/app/api/open-graph/route.ts @@ -353,5 +353,3 @@ export async function GET(request: NextRequest) { } export const dynamic = "force-dynamic"; - -export const revalidate = 0; diff --git a/app/api/pepe/resolve/route.ts b/app/api/pepe/resolve/route.ts index 5cfbd7b6ac..f1b561fd54 100644 --- a/app/api/pepe/resolve/route.ts +++ b/app/api/pepe/resolve/route.ts @@ -11,7 +11,7 @@ import { const TOKENSCAN_BASE = "https://tokenscan.io/api"; -export type PepeKind = "asset" | "collection" | "artist" | "set"; +type PepeKind = "asset" | "collection" | "artist" | "set"; type Market = { bestAskSats?: number; @@ -814,6 +814,7 @@ async function resolveAsset(slug: string): Promise { }; } +// ts-prune-ignore-next: Next.js uses exported HTTP verb handlers via convention. export async function GET(request: NextRequest) { const kind = (request.nextUrl.searchParams.get("kind") as PepeKind | null) ?? null; @@ -864,4 +865,3 @@ export async function GET(request: NextRequest) { } export const dynamic = "force-dynamic"; -export const revalidate = 0; diff --git a/app/api/tiktok/route.ts b/app/api/tiktok/route.ts index 909eeec7de..d0127c7411 100644 --- a/app/api/tiktok/route.ts +++ b/app/api/tiktok/route.ts @@ -439,4 +439,3 @@ export async function GET(request: NextRequest) { } export const dynamic = "force-dynamic"; -export const revalidate = 0; diff --git a/app/api/wikimedia-card/route.ts b/app/api/wikimedia-card/route.ts index f21e1f6bf9..7c5054a775 100644 --- a/app/api/wikimedia-card/route.ts +++ b/app/api/wikimedia-card/route.ts @@ -1021,4 +1021,3 @@ export async function GET(request: NextRequest) { } export const dynamic = "force-dynamic"; -export const revalidate = 0; diff --git a/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts b/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts index 0c8afa637d..1a95da40a1 100644 --- a/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts +++ b/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts @@ -4,6 +4,7 @@ import { useReducer, useEffect, useCallback } from "react"; import { TraitsData } from "../types/TraitsData"; import { SubmissionStep } from "../types/Steps"; import { useAuth } from "@/components/auth/Auth"; +import { getInitialTraitsValues } from "@/components/waves/memes/traits/schema"; /** * Action types for the form reducer - drastically simplified @@ -81,8 +82,8 @@ function formReducer(state: FormState, action: FormAction): FormState { export function useArtworkSubmissionForm() { const { connectedProfile } = useAuth(); - // Import the pre-computed initial values - const { initialTraits } = require("@/components/waves/memes/traits/schema"); + // Pre-compute initial values for traits without triggering circular dependencies + const initialTraits = getInitialTraitsValues(); // Create the initial state const initialState: FormState = { diff --git a/components/waves/memes/submission/validation/index.ts b/components/waves/memes/submission/validation/index.ts index 380f040959..6ad540c64e 100644 --- a/components/waves/memes/submission/validation/index.ts +++ b/components/waves/memes/submission/validation/index.ts @@ -1,4 +1 @@ -export * from './validationTypes'; -export * from './validationRules'; -export * from './traitsValidation'; -export * from './validationHooks'; \ No newline at end of file +export * from './validationHooks'; diff --git a/components/waves/memes/traits/BooleanTrait.tsx b/components/waves/memes/traits/BooleanTrait.tsx index 192515568f..9bfc7ab4f7 100644 --- a/components/waves/memes/traits/BooleanTrait.tsx +++ b/components/waves/memes/traits/BooleanTrait.tsx @@ -1,9 +1,19 @@ "use client"; import React, { useRef, useEffect, useCallback } from "react"; -import { BooleanTraitProps } from "./types"; +import type { TraitsData } from "../submission/types/TraitsData"; import { TraitWrapper } from "./TraitWrapper"; +type BooleanTraitProps = { + readonly label: string; + readonly field: keyof TraitsData; + readonly className?: string; + readonly error?: string | null; + readonly onBlur?: (field: keyof TraitsData) => void; + readonly traits: TraitsData; + readonly updateBoolean: (field: keyof TraitsData, value: boolean) => void; +}; + /** * Simplified BooleanTrait component using a ref-based approach * Similar to our solution for text and number inputs diff --git a/components/waves/memes/traits/DropdownTrait.tsx b/components/waves/memes/traits/DropdownTrait.tsx index d999a61514..5aa1815f95 100644 --- a/components/waves/memes/traits/DropdownTrait.tsx +++ b/components/waves/memes/traits/DropdownTrait.tsx @@ -1,9 +1,20 @@ "use client"; +import type { TraitsData } from "../submission/types/TraitsData"; import React, { useCallback, useRef } from "react"; -import { DropdownTraitProps } from "./types"; import { TraitWrapper } from "./TraitWrapper"; +interface DropdownTraitProps { + readonly label: string; + readonly field: keyof TraitsData; + readonly className?: string; + readonly error?: string | null; + readonly onBlur?: (field: keyof TraitsData) => void; + readonly options: readonly string[]; + readonly traits: TraitsData; + readonly updateText: (field: keyof TraitsData, value: string) => void; +} + /** * Simplified DropdownTrait component with direct state management */ diff --git a/components/waves/memes/traits/NumberTrait.tsx b/components/waves/memes/traits/NumberTrait.tsx index 5d7863839e..b8d1ff0e7e 100644 --- a/components/waves/memes/traits/NumberTrait.tsx +++ b/components/waves/memes/traits/NumberTrait.tsx @@ -2,9 +2,22 @@ import React, { useRef, useCallback, useMemo } from "react"; import { useDebounce } from "react-use"; -import { NumberTraitProps } from "./types"; +import type { TraitsData } from "../submission/types/TraitsData"; import { TraitWrapper } from "./TraitWrapper"; +interface NumberTraitProps { + readonly label: string; + readonly field: keyof TraitsData; + readonly className?: string; + readonly error?: string | null; + readonly onBlur?: (field: keyof TraitsData) => void; + readonly readOnly?: boolean; + readonly min: number; + readonly max: number; + readonly traits: TraitsData; + readonly updateNumber: (field: keyof TraitsData, value: number) => void; +} + /** * Improved number input component with better UX for handling zero values * Without min/max constraints diff --git a/components/waves/memes/traits/Section.tsx b/components/waves/memes/traits/Section.tsx index 5749aeb4fe..f68f4c8b0a 100644 --- a/components/waves/memes/traits/Section.tsx +++ b/components/waves/memes/traits/Section.tsx @@ -1,5 +1,10 @@ import React, { memo } from "react"; -import { SectionProps } from "./types"; + +interface SectionProps { + readonly title: string; + readonly children: React.ReactNode; + readonly className?: string; +} /** * Base Section component diff --git a/components/waves/memes/traits/TraitWrapper.tsx b/components/waves/memes/traits/TraitWrapper.tsx index 1f84f693dd..55326c25fd 100644 --- a/components/waves/memes/traits/TraitWrapper.tsx +++ b/components/waves/memes/traits/TraitWrapper.tsx @@ -1,8 +1,18 @@ import React from "react"; -import { TraitWrapperProps } from "./types"; import ValidationError from "../submission/ui/ValidationError"; import { CheckCircleIcon } from "@heroicons/react/24/outline"; +interface TraitWrapperProps { + readonly label: string; + readonly readOnly?: boolean; + readonly children: React.ReactNode; + readonly isBoolean?: boolean; + readonly className?: string; + readonly error?: string | null; + readonly id?: string; + readonly isFieldFilled?: boolean; +} + export const TraitWrapper: React.FC = ({ label, readOnly = false, diff --git a/components/waves/memes/traits/index.ts b/components/waves/memes/traits/index.ts index 9313f597ed..24c68ba9be 100644 --- a/components/waves/memes/traits/index.ts +++ b/components/waves/memes/traits/index.ts @@ -1,7 +1,2 @@ -export * from './TextTrait'; -export * from './NumberTrait'; -export * from './DropdownTrait'; -export * from './BooleanTrait'; export * from './Section'; export * from './TraitField'; -export * from './types'; \ No newline at end of file diff --git a/components/waves/memes/traits/schema.ts b/components/waves/memes/traits/schema.ts index 1f765e7a12..e9410b408e 100644 --- a/components/waves/memes/traits/schema.ts +++ b/components/waves/memes/traits/schema.ts @@ -495,7 +495,7 @@ export function getFormSections( } // Get initial trait values -function getInitialTraitsValues(): TraitsData { +export function getInitialTraitsValues(): TraitsData { const initialValues: Record = { title: "", description: "", @@ -521,9 +521,6 @@ function getInitialTraitsValues(): TraitsData { return initialValues as TraitsData; } -// Function to be imported directly in useArtworkSubmissionForm to avoid circular dependency -export const initialTraits: TraitsData = getInitialTraitsValues(); - export const MEME_TRAITS_SORT_ORDER = [ "artist", "memeName", diff --git a/components/waves/memes/traits/types.ts b/components/waves/memes/traits/types.ts index 03083b67f5..3da3065c94 100644 --- a/components/waves/memes/traits/types.ts +++ b/components/waves/memes/traits/types.ts @@ -15,39 +15,3 @@ export interface TextTraitProps extends BaseTraitProps { readonly traits: TraitsData; readonly updateText: (field: keyof TraitsData, value: string) => void; } - -export interface NumberTraitProps extends BaseTraitProps { - readonly readOnly?: boolean; - readonly min: number; - readonly max: number; - readonly traits: TraitsData; - readonly updateNumber: (field: keyof TraitsData, value: number) => void; -} - -export interface DropdownTraitProps extends BaseTraitProps { - readonly options: readonly string[]; - readonly traits: TraitsData; - readonly updateText: (field: keyof TraitsData, value: string) => void; -} - -export interface BooleanTraitProps extends BaseTraitProps { - readonly traits: TraitsData; - readonly updateBoolean: (field: keyof TraitsData, value: boolean) => void; -} - -export interface SectionProps { - readonly title: string; - readonly children: React.ReactNode; - readonly className?: string; -} - -export interface TraitWrapperProps { - readonly label: string; - readonly readOnly?: boolean; - readonly children: React.ReactNode; - readonly isBoolean?: boolean; - readonly className?: string; - readonly error?: string | null; - readonly id?: string; - readonly isFieldFilled?: boolean; -} diff --git a/proxy.ts b/proxy.ts deleted file mode 100644 index 851b9a4934..0000000000 --- a/proxy.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { publicEnv } from "./config/env"; -import { API_AUTH_COOKIE } from "./constants"; -import { - getHomeFeedRoute, - getMessagesBaseRoute, - getNotificationsRoute, - getWaveRoute, - getWavesBaseRoute, -} from "@/helpers/navigation.helpers"; - -const redirectMappings = [ - { url: "/6529-dubai/", target: "/" }, - { url: "/6529-puerto-rico/", target: "/" }, - { url: "/abc1/", target: "/" }, - { url: "/abc2/", target: "/" }, - { url: "/about/contact/", target: "/about/contact-us" }, - { url: "/about/jobs/", target: "/about/contact-us" }, - { url: "/about/news/", target: "/" }, - { url: "/bridge/", target: "/waves" }, - { url: "/collections/", target: "/the-memes" }, - { url: "/collections/6529gradient/", target: "/6529-gradient" }, - { url: "/collections/brand/", target: "/the-memes" }, - { url: "/collections/collections-form/", target: "/about/apply" }, - { url: "/collections/memelab/", target: "/meme-lab" }, - { url: "/collections/memelab/pepiano-allowlist/", target: "/meme-lab/8" }, - { - url: "/collections/memelab/the-great-restoration/", - target: "/meme-lab/12", - }, - { - url: "/collections/memelab/the-nftimes-issue-1-allowlist/", - target: "/meme-lab/7", - }, - { url: "/collections/memelab/wen-summer-allowlist/", target: "/meme-lab/10" }, - { url: "/collections/the-memes/", target: "/the-memes" }, - { - url: "/collections/the-memes/evolution-allowlist/", - target: "/the-memes/73", - }, - { - url: "/collections/the-memes/faces-of-freedom-allowlist/", - target: "/the-memes/72", - }, - { - url: "/collections/the-memes/freedom-fighter-allowlist/", - target: "/the-memes/77", - }, - { - url: "/collections/the-memes/meme4-season2-card6/gm-or-die-allowlist/", - target: "/the-memes/71", - }, - { - url: "/collections/the-memes/metaverse-starter-pack-allowlist/", - target: "/the-memes/78", - }, - { - url: "/collections/the-memes/no-meme-no-life-allowlist/", - target: "/the-memes/75", - }, - { - url: "/collections/the-memes/staying-alive-allowlist/", - target: "/the-memes/76", - }, - { - url: "/collections/the-memes/the-memes-season2/", - target: "/the-memes?szn=2", - }, - { url: "/education/something-else/", target: "/" }, - { url: "/privacy-policy/", target: "/about/privacy-policy" }, - { url: "/studio/", target: "/" }, - { url: "/the-hamily-wagmi-allowlist/", target: "/the-memes/74" }, - { url: "/om/om/", target: "/om" }, - { url: "/om/bug/report/", target: "/about/contact-us" }, -]; - -const STATIC_PATH_PREFIXES = ["/api", "/_next", "/sitemap", "/robots.txt", "/error"] as const; -const STATIC_PATH_SUFFIXES = [ - "favicon.ico", - ".jpeg", - ".png", - ".gif", - ".svg", - ".webp", -] as const; - -function stripTrailingSlashes(value: string): string { - let end = value.length; - while (end > 0 && value[end - 1] === "/") { - end--; - } - return end === value.length ? value : value.slice(0, end); -} - -function removeSingleTrailingSlash(value: string): string { - return value.endsWith("/") ? value.slice(0, -1) : value; -} - -function isDesktopOSFromUserAgent(userAgent: string): boolean { - const isAndroid = userAgent.includes("android"); - const isIOS = - userAgent.includes("iphone") || - userAgent.includes("ipad") || - userAgent.includes("ipod"); - const isMacDesktop = - userAgent.includes("macintosh") || - (userAgent.includes("mac os x") && !userAgent.includes("mobile")); - const isLinuxDesktop = - userAgent.includes("linux") && !isAndroid && !userAgent.includes("mobile"); - const hasDesktopSignal = - userAgent.includes("windows") || - userAgent.includes("x11") || - userAgent.includes("cros"); - - return !isAndroid && !isIOS && (hasDesktopSignal || isMacDesktop || isLinuxDesktop); -} - -function normalizeDropParam(value: string | null): string | undefined { - if (!value) { - return undefined; - } - return stripTrailingSlashes(value); -} - -function normalizeSerialParam(value: string | null): string | undefined { - if (!value) { - return undefined; - } - return removeSingleTrailingSlash(value); -} - -function buildWaveHref({ - wave, - drop, - serialNo, - isDirectMessage, -}: { - wave?: string; - drop?: string; - serialNo?: string; - isDirectMessage: boolean; -}): string { - if (!wave) { - return isDirectMessage - ? getMessagesBaseRoute(false) - : getWavesBaseRoute(false); - } - - const extraParams = drop ? { drop } : undefined; - return getWaveRoute({ - waveId: wave, - serialNo, - extraParams, - isDirectMessage, - isApp: false, - }); -} - -function resolveMyStreamHomeRedirect({ - view, - wave, - drop, - serialNo, -}: { - view?: string; - wave?: string; - drop?: string; - serialNo?: string; -}): string { - if (view === "messages") { - return buildWaveHref({ - wave, - drop, - serialNo, - isDirectMessage: true, - }); - } - - if (view === "waves" || wave) { - return buildWaveHref({ - wave, - drop, - serialNo, - isDirectMessage: false, - }); - } - - if (drop) { - const params = new URLSearchParams(); - params.set("drop", drop); - return `${getWavesBaseRoute(false)}?${params.toString()}`; - } - - return getHomeFeedRoute(); -} - -function resolveMyStreamRedirect( - req: NextRequest, - normalizedPathname: string -): string | undefined { - const userAgent = (req.headers.get("user-agent") || "").toLowerCase(); - if (!isDesktopOSFromUserAgent(userAgent)) { - return undefined; - } - - if (normalizedPathname === "/my-stream/notifications") { - return getNotificationsRoute(false); - } - - if (normalizedPathname !== "/my-stream") { - return undefined; - } - - const params = new URLSearchParams(req.nextUrl.searchParams); - const view = params.get("view") ?? undefined; - const wave = params.get("wave") ?? undefined; - const drop = normalizeDropParam(params.get("drop")); - const serialNo = normalizeSerialParam(params.get("serialNo")); - - return resolveMyStreamHomeRedirect({ - view, - wave, - drop, - serialNo, - }); -} - -function handleRedirects(req: NextRequest): NextResponse | undefined { - let { pathname } = req.nextUrl; - - if (!pathname.endsWith("/")) { - pathname += "/"; - } - - for (const mapping of redirectMappings) { - if (pathname.toLowerCase() === mapping.url.toLowerCase()) { - const url = req.nextUrl.clone(); - url.pathname = mapping.target.split("?")[0]; - const queryString = mapping.target.split("?")[1]; - if (queryString) { - url.search = `?${queryString}`; - } - return NextResponse.redirect(url, 301); - } - } - return undefined; -} - -async function enforceAccessControl( - req: NextRequest, - normalizedPathname: string -): Promise { - if (normalizedPathname === "/access" || normalizedPathname === "/restricted") { - return NextResponse.next(); - } - - const apiAuth = req.cookies.get(API_AUTH_COOKIE) ?? { - value: publicEnv.STAGING_API_KEY ?? "", - }; - const response = await fetch(`${publicEnv.API_ENDPOINT}/api/`, { - headers: apiAuth ? { "x-6529-auth": apiAuth.value } : {}, - }); - - if (response.status === 401) { - req.nextUrl.pathname = "/access"; - req.nextUrl.search = ""; - return NextResponse.redirect(req.nextUrl); - } - - if (response.status === 403) { - req.nextUrl.pathname = "/restricted"; - req.nextUrl.search = ""; - return NextResponse.redirect(req.nextUrl); - } - - return NextResponse.next(); -} - -export default async function proxy(req: NextRequest) { - try { - const redirectResponse = handleRedirects(req); - if (redirectResponse) { - return redirectResponse; - } - - const { pathname } = req.nextUrl; - const normalizedPathname = - pathname.length > 1 && pathname.endsWith("/") - ? pathname.slice(0, -1) - : pathname; - - const redirectTarget = normalizedPathname.startsWith("/my-stream") - ? resolveMyStreamRedirect(req, normalizedPathname) - : undefined; - - if (redirectTarget) { - const clone = req.nextUrl.clone(); - const [pathnamePart, searchPart] = redirectTarget.split("?"); - clone.pathname = pathnamePart || "/"; - clone.search = searchPart ? `?${searchPart}` : ""; - return NextResponse.redirect(clone, 301); - } - - if ( - STATIC_PATH_PREFIXES.some(prefix => normalizedPathname.startsWith(prefix)) || - STATIC_PATH_SUFFIXES.some(suffix => normalizedPathname.endsWith(suffix)) - ) { - return NextResponse.next(); - } - - return enforceAccessControl(req, normalizedPathname); - } catch (error) { - return NextResponse.redirect(new URL("/error", req.url)); - } -} diff --git a/services/websocket/index.ts b/services/websocket/index.ts index 97150b2d69..88448b4a30 100644 --- a/services/websocket/index.ts +++ b/services/websocket/index.ts @@ -10,8 +10,8 @@ import { publicEnv } from "@/config/env"; -// Types -export * from "./WebSocketTypes"; +// Types (re-export only the publicly consumed ones) +export type { WebSocketConfig } from "./WebSocketTypes"; // Context and Provider // Hooks From e38acc222aaa1e86597290fd751c15426188d16a Mon Sep 17 00:00:00 2001 From: Simo Date: Wed, 29 Oct 2025 12:27:39 +0200 Subject: [PATCH 2/2] wip Signed-off-by: Simo --- components/waves/memes/traits/TextTrait.tsx | 14 +- components/waves/memes/traits/types.ts | 17 -- proxy.ts | 315 ++++++++++++++++++++ 3 files changed, 328 insertions(+), 18 deletions(-) delete mode 100644 components/waves/memes/traits/types.ts create mode 100644 proxy.ts diff --git a/components/waves/memes/traits/TextTrait.tsx b/components/waves/memes/traits/TextTrait.tsx index 2bb64a9a25..8bc16c00eb 100644 --- a/components/waves/memes/traits/TextTrait.tsx +++ b/components/waves/memes/traits/TextTrait.tsx @@ -1,10 +1,22 @@ "use client"; +import type { TraitsData } from "../submission/types/TraitsData"; import React, { useRef, useCallback, useMemo } from "react"; import { useDebounce } from "react-use"; -import { TextTraitProps } from "./types"; import { TraitWrapper } from "./TraitWrapper"; +type TextTraitProps = { + readonly label: string; + readonly field: keyof TraitsData; + readonly traits: TraitsData; + readonly updateText: (field: keyof TraitsData, value: string) => void; + readonly readOnly?: boolean; + readonly placeholder?: string; + readonly className?: string; + readonly error?: string | null; + readonly onBlur?: (field: keyof TraitsData) => void; +}; + /** * Extremely simplified TextTrait component using uncontrolled inputs * This approach eliminates React render cycles during typing for maximum performance diff --git a/components/waves/memes/traits/types.ts b/components/waves/memes/traits/types.ts deleted file mode 100644 index 3da3065c94..0000000000 --- a/components/waves/memes/traits/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TraitsData } from "../submission/types/TraitsData"; - -// Component Props Types -interface BaseTraitProps { - readonly label: string; - readonly field: keyof TraitsData; - readonly className?: string; - readonly error?: string | null; - readonly onBlur?: (field: keyof TraitsData) => void; -} - -export interface TextTraitProps extends BaseTraitProps { - readonly readOnly?: boolean; - readonly placeholder?: string; - readonly traits: TraitsData; - readonly updateText: (field: keyof TraitsData, value: string) => void; -} diff --git a/proxy.ts b/proxy.ts new file mode 100644 index 0000000000..851b9a4934 --- /dev/null +++ b/proxy.ts @@ -0,0 +1,315 @@ +import { NextRequest, NextResponse } from "next/server"; +import { publicEnv } from "./config/env"; +import { API_AUTH_COOKIE } from "./constants"; +import { + getHomeFeedRoute, + getMessagesBaseRoute, + getNotificationsRoute, + getWaveRoute, + getWavesBaseRoute, +} from "@/helpers/navigation.helpers"; + +const redirectMappings = [ + { url: "/6529-dubai/", target: "/" }, + { url: "/6529-puerto-rico/", target: "/" }, + { url: "/abc1/", target: "/" }, + { url: "/abc2/", target: "/" }, + { url: "/about/contact/", target: "/about/contact-us" }, + { url: "/about/jobs/", target: "/about/contact-us" }, + { url: "/about/news/", target: "/" }, + { url: "/bridge/", target: "/waves" }, + { url: "/collections/", target: "/the-memes" }, + { url: "/collections/6529gradient/", target: "/6529-gradient" }, + { url: "/collections/brand/", target: "/the-memes" }, + { url: "/collections/collections-form/", target: "/about/apply" }, + { url: "/collections/memelab/", target: "/meme-lab" }, + { url: "/collections/memelab/pepiano-allowlist/", target: "/meme-lab/8" }, + { + url: "/collections/memelab/the-great-restoration/", + target: "/meme-lab/12", + }, + { + url: "/collections/memelab/the-nftimes-issue-1-allowlist/", + target: "/meme-lab/7", + }, + { url: "/collections/memelab/wen-summer-allowlist/", target: "/meme-lab/10" }, + { url: "/collections/the-memes/", target: "/the-memes" }, + { + url: "/collections/the-memes/evolution-allowlist/", + target: "/the-memes/73", + }, + { + url: "/collections/the-memes/faces-of-freedom-allowlist/", + target: "/the-memes/72", + }, + { + url: "/collections/the-memes/freedom-fighter-allowlist/", + target: "/the-memes/77", + }, + { + url: "/collections/the-memes/meme4-season2-card6/gm-or-die-allowlist/", + target: "/the-memes/71", + }, + { + url: "/collections/the-memes/metaverse-starter-pack-allowlist/", + target: "/the-memes/78", + }, + { + url: "/collections/the-memes/no-meme-no-life-allowlist/", + target: "/the-memes/75", + }, + { + url: "/collections/the-memes/staying-alive-allowlist/", + target: "/the-memes/76", + }, + { + url: "/collections/the-memes/the-memes-season2/", + target: "/the-memes?szn=2", + }, + { url: "/education/something-else/", target: "/" }, + { url: "/privacy-policy/", target: "/about/privacy-policy" }, + { url: "/studio/", target: "/" }, + { url: "/the-hamily-wagmi-allowlist/", target: "/the-memes/74" }, + { url: "/om/om/", target: "/om" }, + { url: "/om/bug/report/", target: "/about/contact-us" }, +]; + +const STATIC_PATH_PREFIXES = ["/api", "/_next", "/sitemap", "/robots.txt", "/error"] as const; +const STATIC_PATH_SUFFIXES = [ + "favicon.ico", + ".jpeg", + ".png", + ".gif", + ".svg", + ".webp", +] as const; + +function stripTrailingSlashes(value: string): string { + let end = value.length; + while (end > 0 && value[end - 1] === "/") { + end--; + } + return end === value.length ? value : value.slice(0, end); +} + +function removeSingleTrailingSlash(value: string): string { + return value.endsWith("/") ? value.slice(0, -1) : value; +} + +function isDesktopOSFromUserAgent(userAgent: string): boolean { + const isAndroid = userAgent.includes("android"); + const isIOS = + userAgent.includes("iphone") || + userAgent.includes("ipad") || + userAgent.includes("ipod"); + const isMacDesktop = + userAgent.includes("macintosh") || + (userAgent.includes("mac os x") && !userAgent.includes("mobile")); + const isLinuxDesktop = + userAgent.includes("linux") && !isAndroid && !userAgent.includes("mobile"); + const hasDesktopSignal = + userAgent.includes("windows") || + userAgent.includes("x11") || + userAgent.includes("cros"); + + return !isAndroid && !isIOS && (hasDesktopSignal || isMacDesktop || isLinuxDesktop); +} + +function normalizeDropParam(value: string | null): string | undefined { + if (!value) { + return undefined; + } + return stripTrailingSlashes(value); +} + +function normalizeSerialParam(value: string | null): string | undefined { + if (!value) { + return undefined; + } + return removeSingleTrailingSlash(value); +} + +function buildWaveHref({ + wave, + drop, + serialNo, + isDirectMessage, +}: { + wave?: string; + drop?: string; + serialNo?: string; + isDirectMessage: boolean; +}): string { + if (!wave) { + return isDirectMessage + ? getMessagesBaseRoute(false) + : getWavesBaseRoute(false); + } + + const extraParams = drop ? { drop } : undefined; + return getWaveRoute({ + waveId: wave, + serialNo, + extraParams, + isDirectMessage, + isApp: false, + }); +} + +function resolveMyStreamHomeRedirect({ + view, + wave, + drop, + serialNo, +}: { + view?: string; + wave?: string; + drop?: string; + serialNo?: string; +}): string { + if (view === "messages") { + return buildWaveHref({ + wave, + drop, + serialNo, + isDirectMessage: true, + }); + } + + if (view === "waves" || wave) { + return buildWaveHref({ + wave, + drop, + serialNo, + isDirectMessage: false, + }); + } + + if (drop) { + const params = new URLSearchParams(); + params.set("drop", drop); + return `${getWavesBaseRoute(false)}?${params.toString()}`; + } + + return getHomeFeedRoute(); +} + +function resolveMyStreamRedirect( + req: NextRequest, + normalizedPathname: string +): string | undefined { + const userAgent = (req.headers.get("user-agent") || "").toLowerCase(); + if (!isDesktopOSFromUserAgent(userAgent)) { + return undefined; + } + + if (normalizedPathname === "/my-stream/notifications") { + return getNotificationsRoute(false); + } + + if (normalizedPathname !== "/my-stream") { + return undefined; + } + + const params = new URLSearchParams(req.nextUrl.searchParams); + const view = params.get("view") ?? undefined; + const wave = params.get("wave") ?? undefined; + const drop = normalizeDropParam(params.get("drop")); + const serialNo = normalizeSerialParam(params.get("serialNo")); + + return resolveMyStreamHomeRedirect({ + view, + wave, + drop, + serialNo, + }); +} + +function handleRedirects(req: NextRequest): NextResponse | undefined { + let { pathname } = req.nextUrl; + + if (!pathname.endsWith("/")) { + pathname += "/"; + } + + for (const mapping of redirectMappings) { + if (pathname.toLowerCase() === mapping.url.toLowerCase()) { + const url = req.nextUrl.clone(); + url.pathname = mapping.target.split("?")[0]; + const queryString = mapping.target.split("?")[1]; + if (queryString) { + url.search = `?${queryString}`; + } + return NextResponse.redirect(url, 301); + } + } + return undefined; +} + +async function enforceAccessControl( + req: NextRequest, + normalizedPathname: string +): Promise { + if (normalizedPathname === "/access" || normalizedPathname === "/restricted") { + return NextResponse.next(); + } + + const apiAuth = req.cookies.get(API_AUTH_COOKIE) ?? { + value: publicEnv.STAGING_API_KEY ?? "", + }; + const response = await fetch(`${publicEnv.API_ENDPOINT}/api/`, { + headers: apiAuth ? { "x-6529-auth": apiAuth.value } : {}, + }); + + if (response.status === 401) { + req.nextUrl.pathname = "/access"; + req.nextUrl.search = ""; + return NextResponse.redirect(req.nextUrl); + } + + if (response.status === 403) { + req.nextUrl.pathname = "/restricted"; + req.nextUrl.search = ""; + return NextResponse.redirect(req.nextUrl); + } + + return NextResponse.next(); +} + +export default async function proxy(req: NextRequest) { + try { + const redirectResponse = handleRedirects(req); + if (redirectResponse) { + return redirectResponse; + } + + const { pathname } = req.nextUrl; + const normalizedPathname = + pathname.length > 1 && pathname.endsWith("/") + ? pathname.slice(0, -1) + : pathname; + + const redirectTarget = normalizedPathname.startsWith("/my-stream") + ? resolveMyStreamRedirect(req, normalizedPathname) + : undefined; + + if (redirectTarget) { + const clone = req.nextUrl.clone(); + const [pathnamePart, searchPart] = redirectTarget.split("?"); + clone.pathname = pathnamePart || "/"; + clone.search = searchPart ? `?${searchPart}` : ""; + return NextResponse.redirect(clone, 301); + } + + if ( + STATIC_PATH_PREFIXES.some(prefix => normalizedPathname.startsWith(prefix)) || + STATIC_PATH_SUFFIXES.some(suffix => normalizedPathname.endsWith(suffix)) + ) { + return NextResponse.next(); + } + + return enforceAccessControl(req, normalizedPathname); + } catch (error) { + return NextResponse.redirect(new URL("/error", req.url)); + } +}