Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ const baseProfile: ApiIdentity = {
function renderComponent(profile: Partial<ApiIdentity>, mainAddress = '0xabc') {
const combined = { ...baseProfile, ...profile } as ApiIdentity;
return render(
<UserPageHeaderName profile={combined} canEdit={false} mainAddress={mainAddress} />
<UserPageHeaderName
profile={combined}
canEdit={false}
mainAddress={mainAddress}
level={combined.level}
profileEnabledAt={null}
/>
);
}

Expand Down
2 changes: 1 addition & 1 deletion components/user/layout/UserPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function UserPageLayout({
handleOrWallet={normalizedHandleOrWallet}
fallbackMainAddress={mainAddress}
/>
<div className="tw-px-2 lg:tw-px-6 xl:tw-px-8 tw-mx-auto">
<div className="tw-px-4 sm:tw-px-6 md:tw-px-8 tw-mx-auto">
<UserPageTabs />
<div className="tw-mt-6 lg:tw-mt-8">{children}</div>
</div>
Expand Down
97 changes: 97 additions & 0 deletions components/user/settings/UserSettingsBannerImageInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client";

import Image from "next/image";
import { ACCEPTED_FORMATS_DISPLAY } from "./imageValidation";
import { useImageUpload } from "./useImageUpload";

export default function UserSettingsBannerImageInput({
imageToShow,
setFile,
}: {
readonly imageToShow: string | null;
readonly setFile: (file: File) => void;
}) {
const { error, shake, dragging, onFileChange, dragHandlers } = useImageUpload(
{
maxSizeBytes: 2097152,
maxSizeLabel: "2MB",
setFile,
}
);

return (
<div
{...dragHandlers}
className="tw-group tw-flex tw-w-full tw-items-center tw-justify-center"
>
<label
className={` ${
dragging
? "tw-border-iron-600 tw-bg-iron-800"
: "tw-border-iron-700 tw-bg-iron-900"
} tw-relative tw-flex tw-h-64 tw-w-full tw-cursor-pointer tw-flex-col tw-items-center tw-justify-center tw-rounded-lg tw-border-2 tw-border-dashed tw-transition tw-duration-300 tw-ease-out hover:tw-border-iron-600 hover:tw-bg-iron-800 ${
shake ? "tw-animate-shake" : ""
} `}
>
<div className="tw-flex tw-flex-col tw-items-center tw-justify-center tw-gap-3 tw-px-4">
{imageToShow ? (
<div className="tw-relative tw-h-40 tw-w-40">
<Image
src={imageToShow}
alt="Banner preview"
fill
className="tw-rounded-sm tw-object-contain"
/>
</div>
) : (
<>
<div className="tw-flex tw-h-10 tw-w-10 tw-items-center tw-justify-center tw-rounded-lg tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900 tw-transition tw-duration-300 tw-ease-out group-hover:tw-bg-iron-800">
<div className="tw-flex tw-h-5 tw-w-5 tw-flex-shrink-0 tw-items-center tw-justify-center tw-text-iron-50">
<svg
className="tw-h-6 tw-w-6 tw-flex-shrink-0 tw-text-iron-50"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 16.2422C2.79401 15.435 2 14.0602 2 12.5C2 10.1564 3.79151 8.23129 6.07974 8.01937C6.54781 5.17213 9.02024 3 12 3C14.9798 3 17.4522 5.17213 17.9203 8.01937C20.2085 8.23129 22 10.1564 22 12.5C22 14.0602 21.206 15.435 20 16.2422M8 16L12 12M12 12L16 16M12 12V21"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>
<div className="tw-flex tw-flex-col tw-items-center tw-justify-center">
<p className="tw-mb-1 tw-text-sm tw-font-normal tw-text-iron-400">
<span className="tw-font-medium tw-text-white">
Click to upload
</span>{" "}
or drag and drop
</p>
<p className="tw-text-xs tw-font-normal tw-text-iron-400">
JPEG, JPG, PNG, GIF — max 2MB
</p>
</div>
</>
)}
{error && (
<p className="tw-text-xs tw-font-medium tw-text-red">{error}</p>
)}
</div>
<input
id="banner-upload-input"
type="file"
className="tw-hidden"
accept={ACCEPTED_FORMATS_DISPLAY}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0];
if (f) onFileChange(f);
e.target.value = ""; // allow selecting same file again
}}
/>
</label>
</div>
);
}
96 changes: 18 additions & 78 deletions components/user/settings/UserSettingsImgSelectFile.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
"use client";

import { useContext, useRef, useState, useEffect } 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 = 2097152;
import Image from "next/image";
import { useImageUpload } from "./useImageUpload";
import { ACCEPTED_FORMATS_DISPLAY } from "./imageValidation";

export default function UserSettingsImgSelectFile({
imageToShow,
Expand All @@ -24,63 +11,17 @@ export default function UserSettingsImgSelectFile({
readonly imageToShow: string | null;
readonly setFile: (file: File) => void;
}) {
const { setToast } = useContext(AuthContext);
const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = useState<string | null>(null);
const [shake, setShake] = useState<boolean>(false);
const onFileChange = (file: File) => {
setError(null);
if (ACCEPTED_FORMATS.indexOf(file.type) === -1) {
setError(null);
setToast({
type: "error",
message: "Invalid file type",
});
} else if (file.size > FILE_SIZE_LIMIT) {
setError("File size must be less than 2MB");
setShake(true);
} else {
setError(null);
setFile(file);
const { error, shake, dragging, onFileChange, dragHandlers } = useImageUpload(
{
maxSizeBytes: 2097152,
maxSizeLabel: "2MB",
setFile,
}
};

const handleDrop = (e: any) => {
e.preventDefault();
e.stopPropagation();
if (e?.dataTransfer?.files?.length) {
onFileChange(e.dataTransfer.files[0]);
}
};

const [dragging, setDragging] = useState(false);

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 (
<div
onDrop={handleDrop}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
{...dragHandlers}
Comment thread
ragnep marked this conversation as resolved.
className="tw-group tw-flex tw-w-full tw-items-center tw-justify-center"
>
<label
Expand All @@ -94,11 +35,12 @@ export default function UserSettingsImgSelectFile({
>
<div className="tw-flex tw-flex-col tw-items-center tw-justify-center tw-pb-6 tw-pt-5">
{imageToShow && (
<div className="tw-h-40 tw-w-40">
<img
<div className="tw-relative tw-h-40 tw-w-40">
<Image
src={imageToShow}
alt="Profile image"
className="tw-h-full tw-w-full tw-rounded-sm tw-object-contain"
fill
className="tw-rounded-sm tw-object-contain"
/>
</div>
)}
Expand Down Expand Up @@ -141,15 +83,13 @@ export default function UserSettingsImgSelectFile({
</div>
<input
id="pfp-upload-input"
ref={inputRef}
type="file"
className="tw-hidden"
accept={ACCEPTED_FORMATS_DISPLAY}
onChange={(e: any) => {
if (e.target.files) {
const f = e.target.files[0];
onFileChange(f);
}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0];
if (f) onFileChange(f);
e.target.value = ""; // allow selecting same file again
}}
/>
</label>
Expand Down
27 changes: 27 additions & 0 deletions components/user/settings/imageValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getFileExtension } from "@/components/waves/memes/file-upload/utils/formatHelpers";

const ACCEPTED_FORMATS = [
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/webp",
];

export const ACCEPTED_FORMATS_DISPLAY = ACCEPTED_FORMATS.map(
(format) => `.${format.replace("image/", "")}`
).join(", ");

const ALLOWED_EXTENSIONS = new Set(["JPG", "JPEG", "PNG", "GIF", "WEBP"]);

export const isValidImageType = (file: File): boolean => {
// Primary check: MIME type
if (ACCEPTED_FORMATS.includes(file.type)) {
return true;
}
// Fallback: extension check when MIME is empty (some OS/browser combos)
if (file.type === "") {
return ALLOWED_EXTENSIONS.has(getFileExtension(file));
}
return false;
};
95 changes: 95 additions & 0 deletions components/user/settings/useImageUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client";

import { useCallback, useContext, useRef, useState } from "react";
import { AuthContext } from "@/components/auth/Auth";
import { isValidImageType } from "./imageValidation";

interface UseImageUploadOptions {
maxSizeBytes: number;
maxSizeLabel: string;
setFile: (file: File) => void;
}

export function useImageUpload({
maxSizeBytes,
maxSizeLabel,
setFile,
}: UseImageUploadOptions) {
const { setToast } = useContext(AuthContext);
const dragDepth = useRef(0);
const [error, setError] = useState<string | null>(null);
const [shake, setShake] = useState<boolean>(false);
const [dragging, setDragging] = useState(false);

const onFileChange = useCallback(
(file: File) => {
setError(null);
if (!isValidImageType(file)) {
setToast({
type: "error",
message: "Invalid file type",
});
return;
}

if (file.size > maxSizeBytes) {
setError(`File size must be less than ${maxSizeLabel}`);
setShake(true);
setTimeout(() => setShake(false), 300);
return;
}

setFile(file);
},
[setFile, setToast, maxSizeBytes, maxSizeLabel]
);

const onDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragDepth.current += 1;
setDragging(true);
}, []);

const onDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragDepth.current -= 1;
if (dragDepth.current <= 0) {
dragDepth.current = 0;
setDragging(false);
}
}, []);

const onDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);

const onDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragDepth.current = 0;
setDragging(false);
const file = e.dataTransfer.files[0];
if (file) {
onFileChange(file);
}
},
[onFileChange]
);

return {
error,
shake,
dragging,
onFileChange,
dragHandlers: {
onDrop,
onDragEnter,
onDragLeave,
onDragOver,
},
};
}
Loading