From 03511a295ba61ca3cbe86808a5693aed2a583503 Mon Sep 17 00:00:00 2001 From: ragnep Date: Thu, 29 Jan 2026 17:46:54 +0200 Subject: [PATCH 01/16] wip Signed-off-by: ragnep --- .../name/UserPageHeaderName.test.tsx | 8 +- components/user/layout/UserPageLayout.tsx | 2 +- .../settings/UserSettingsBannerImageInput.tsx | 156 +++++++++++++ .../user-page-header/UserPageHeaderClient.tsx | 210 +++++++++++++----- .../about/UserPageHeaderAbout.tsx | 43 ++-- .../about/UserPageHeaderAboutEdit.tsx | 2 +- .../about/UserPageHeaderAboutStatement.tsx | 32 ++- .../banner/UserPageHeaderBanner.tsx | 66 ++++-- .../banner/UserPageHeaderEditBanner.tsx | 172 +++++++++++--- .../name/UserPageHeaderEditName.tsx | 2 +- .../name/UserPageHeaderName.tsx | 76 +++++-- .../UserPageClassificationWrapper.tsx | 2 +- .../UserPageHeaderEditClassification.tsx | 2 +- .../pfp/UserPageHeaderEditPfp.tsx | 2 +- .../pfp/UserPageHeaderPfp.tsx | 39 ++-- .../pfp/UserPageHeaderPfpWrapper.tsx | 4 +- .../stats/UserPageHeaderStats.tsx | 2 +- components/user/utils/UserFollowBtn.tsx | 2 +- components/user/utils/level/UserLevel.tsx | 4 +- components/user/utils/stats/UserStatsRow.tsx | 52 ++--- components/waves/CreateDropContent.tsx | 5 +- components/waves/drops/reaction-utils.ts | 5 +- helpers/ProfileHelpers.ts | 43 ++-- helpers/profile-banner.helpers.ts | 24 ++ 24 files changed, 731 insertions(+), 224 deletions(-) create mode 100644 components/user/settings/UserSettingsBannerImageInput.tsx create mode 100644 helpers/profile-banner.helpers.ts diff --git a/__tests__/components/user/user-page-header/name/UserPageHeaderName.test.tsx b/__tests__/components/user/user-page-header/name/UserPageHeaderName.test.tsx index 6b7eb190dc..09444c5d76 100644 --- a/__tests__/components/user/user-page-header/name/UserPageHeaderName.test.tsx +++ b/__tests__/components/user/user-page-header/name/UserPageHeaderName.test.tsx @@ -40,7 +40,13 @@ const baseProfile: ApiIdentity = { function renderComponent(profile: Partial, mainAddress = '0xabc') { const combined = { ...baseProfile, ...profile } as ApiIdentity; return render( - + ); } diff --git a/components/user/layout/UserPageLayout.tsx b/components/user/layout/UserPageLayout.tsx index 1a9b7eee74..49e0bdf7ee 100644 --- a/components/user/layout/UserPageLayout.tsx +++ b/components/user/layout/UserPageLayout.tsx @@ -29,7 +29,7 @@ export default function UserPageLayout({ handleOrWallet={normalizedHandleOrWallet} fallbackMainAddress={mainAddress} /> -
+
{children}
diff --git a/components/user/settings/UserSettingsBannerImageInput.tsx b/components/user/settings/UserSettingsBannerImageInput.tsx new file mode 100644 index 0000000000..82cb88a861 --- /dev/null +++ b/components/user/settings/UserSettingsBannerImageInput.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useContext, useEffect, useState } from "react"; +import { AuthContext } from "@/components/auth/Auth"; + +const ACCEPTED_FORMATS = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", +]; + +const ACCEPTED_FORMATS_DISPLAY = ACCEPTED_FORMATS.map( + (format) => `.${format.replace("image/", "")}` +).join(", "); + +const FILE_SIZE_LIMIT = 10485760; + +export default function UserSettingsBannerImageInput({ + imageToShow, + setFile, +}: { + readonly imageToShow: string | null; + readonly setFile: (file: File) => void; +}) { + const { setToast } = useContext(AuthContext); + const [error, setError] = useState(null); + const [shake, setShake] = useState(false); + const [dragging, setDragging] = useState(false); + + const onFileChange = (file: File) => { + setError(null); + if (ACCEPTED_FORMATS.indexOf(file.type) === -1) { + setToast({ + type: "error", + message: "Invalid file type", + }); + return; + } + + if (file.size > FILE_SIZE_LIMIT) { + setError("File size must be less than 10MB"); + setShake(true); + return; + } + + setError(null); + setFile(file); + }; + + const handleDrop = (e: any) => { + e.preventDefault(); + e.stopPropagation(); + if (e?.dataTransfer?.files?.length) { + onFileChange(e.dataTransfer.files[0]); + } + }; + + const handleDrag = (e: any) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragging(true); + } else if (e.type === "dragleave") { + setDragging(false); + } else if (e.type === "drop") { + setDragging(false); + } + }; + + useEffect(() => { + if (shake) { + const timeout = setTimeout(() => setShake(false), 300); + return () => clearTimeout(timeout); + } + return; + }, [shake]); + + return ( +
+ +
+ ); +} diff --git a/components/user/user-page-header/UserPageHeaderClient.tsx b/components/user/user-page-header/UserPageHeaderClient.tsx index 5cda0f356c..36bd69a86e 100644 --- a/components/user/user-page-header/UserPageHeaderClient.tsx +++ b/components/user/user-page-header/UserPageHeaderClient.tsx @@ -13,10 +13,10 @@ import { amIUser } from "@/helpers/Helpers"; import { navigateToDirectMessage } from "@/helpers/navigation.helpers"; import { STATEMENT_GROUP, STATEMENT_TYPE } from "@/helpers/Types"; import { createDirectMessageWave } from "@/helpers/waves/waves.helpers"; +import { getBannerColorValue } from "@/helpers/profile-banner.helpers"; import useDeviceInfo from "@/hooks/useDeviceInfo"; import { useIdentity } from "@/hooks/useIdentity"; import { commonApiFetch } from "@/services/api/common-api"; -import UserLevel from "../utils/level/UserLevel"; import UserFollowBtn from "../utils/UserFollowBtn"; import UserPageHeaderAbout from "./about/UserPageHeaderAbout"; import UserPageHeaderBanner from "./banner/UserPageHeaderBanner"; @@ -24,7 +24,6 @@ import UserPageHeaderName from "./name/UserPageHeaderName"; import UserPageHeaderPfp from "./pfp/UserPageHeaderPfp"; import UserPageHeaderPfpWrapper from "./pfp/UserPageHeaderPfpWrapper"; import UserPageHeaderStats from "./stats/UserPageHeaderStats"; -import UserPageHeaderProfileEnabledAt from "./UserPageHeaderProfileEnabledAt"; type Props = { readonly profile: ApiIdentity; @@ -69,6 +68,11 @@ export default function UserPageHeaderClient({ [hydratedProfile, initialProfile] ); + const banner1Color = + getBannerColorValue(profile?.banner1) ?? defaultBanner1; + const banner2Color = + getBannerColorValue(profile?.banner2) ?? defaultBanner2; + const mainAddress = useMemo(() => { const primaryWallet = profile?.primary_wallet; if (primaryWallet) { @@ -156,65 +160,165 @@ export default function UserPageHeaderClient({ return (
-
- -
-
-
-
- - +
+ +
+ +
+
+
+
+
+ + + +
+ +
+
+ +
+ +
+
+ +
+ {!isMyProfile && + profile.handle && + connectedProfile?.handle && ( + + handleCreateDirectMessage( + profile.primary_wallet + ) + : undefined + } + directMessageLoading={directMessageLoading} + /> + )} +
+
+
+ + {showAbout && ( +
+ - +
+ )} + +
+ +
+
+ +
+
+
+ + + +
+ +
+ {!isMyProfile && + profile.handle && + connectedProfile?.handle && ( + + handleCreateDirectMessage( + profile.primary_wallet + ) + : undefined + } + directMessageLoading={directMessageLoading} + /> + )} +
-
- {!isMyProfile && profile.handle && connectedProfile?.handle && ( - - handleCreateDirectMessage(profile.primary_wallet) - : undefined - } - directMessageLoading={directMessageLoading} + +
+
+ - )} +
+ +
+
-
- + {showAbout && ( +
+ +
+ )} -
- +
+ +
- {showAbout && ( - - )} - -
diff --git a/components/user/user-page-header/about/UserPageHeaderAbout.tsx b/components/user/user-page-header/about/UserPageHeaderAbout.tsx index 529a55b0e8..a3f4ee7c50 100644 --- a/components/user/user-page-header/about/UserPageHeaderAbout.tsx +++ b/components/user/user-page-header/about/UserPageHeaderAbout.tsx @@ -46,30 +46,39 @@ export default function UserPageHeaderAbout({ return (
{view === AboutStatementView.STATEMENT && ( -
- + ) : ( + + )} {canEdit && ( -
+
+ )} - +
)} {view === AboutStatementView.EDIT && ( -
+