Walk Create Community through the Figma screens and restore a chosen logo after reload, so a guest can leave without saving until they reach the email step.

This commit is contained in:
adilallo committed 2026-11-05 16:14:04 -07:00
1 parent ca1f58a50c
commit 2348a9e94f
61 files changed
+1110 -421

No files matched your search

@@ -14,7 +14,6 @@ import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
import { fetchAuthSession } from "../../../../../lib/create/api";
import { ASSETS, getAssetPath } from "../../../../../lib/assetUtils";
import {
createFlowUploadFailureMessageKey,
uploadCreateFlowFile,
@@ -34,7 +33,7 @@ function hasCommunityAvatarUrl(url: string | undefined): boolean {
return typeof url === "string" && url.trim().length > 0;
}
/** Create Community — Figma Flow — Upload `20094:41524`. */
/** Create Community — Figma Flow — Upload. */
export function CommunityUploadScreen() {
const m = useMessages();
const u = m.create.community.communityUpload;
@@ -44,6 +43,7 @@ export function CommunityUploadScreen() {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [signedIn, setSignedIn] = useState<boolean | null>(null);
const [localPreviewUrl, setLocalPreviewUrl] = useState<string | null>(null);
const [localFileName, setLocalFileName] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@@ -104,6 +104,10 @@ export function CommunityUploadScreen() {
if (prev) URL.revokeObjectURL(prev);
return objectUrl;
});
setLocalFileName(file.name);
if (!state.communityAvatarFileName) {
updateState({ communityAvatarFileName: file.name });
}
} catch {
// Missing IndexedDB / quota: leave the picker empty.
}
@@ -111,11 +115,15 @@ export function CommunityUploadScreen() {
return () => {
cancelled = true;
};
}, []);
}, [state.communityAvatarFileName, updateState]);
const resolveUploadError = useCallback(
(err: unknown) => tUpload(createFlowUploadFailureMessageKey(err)),
[tUpload],
(err: unknown) => {
const key = createFlowUploadFailureMessageKey(err);
if (key === "errors.tooLarge") return u.fileTooLarge;
return tUpload(key);
},
[tUpload, u.fileTooLarge],
);
const handleFileChange = useCallback(
@@ -126,7 +134,7 @@ export function CommunityUploadScreen() {
markCreateFlowInteraction();
setErrorMessage(null);
if (signedIn) {
if (signedIn === true) {
setBusy(true);
try {
const { url } = await uploadCreateFlowFile(file, "communityAvatar");
@@ -134,7 +142,11 @@ export function CommunityUploadScreen() {
if (prev) URL.revokeObjectURL(prev);
return null;
});
updateState({ communityAvatarUrl: url });
setLocalFileName(file.name);
updateState({
communityAvatarUrl: url,
communityAvatarFileName: file.name,
});
} catch (err) {
setErrorMessage(resolveUploadError(err));
} finally {
@@ -143,26 +155,28 @@ export function CommunityUploadScreen() {
return;
}
if (signedIn === false) {
const validated = await validateCreateFlowUploadFile(
file,
"communityAvatar",
const validated = await validateCreateFlowUploadFile(
file,
"communityAvatar",
);
if (validated.ok === false) {
setErrorMessage(
validated.reason === "tooLarge"
? u.fileTooLarge
: tUpload(messageKeyForCreateFlowUploadReason(validated.reason)),
);
if (validated.ok === false) {
setErrorMessage(
tUpload(messageKeyForCreateFlowUploadReason(validated.reason)),
);
return;
}
try {
await storePendingCommunityAvatarFile(file);
setLocalPreviewUrl((prev) => {
if (prev) URL.revokeObjectURL(prev);
return URL.createObjectURL(file);
});
} catch {
setErrorMessage(tUpload("errors.generic"));
}
return;
}
try {
await storePendingCommunityAvatarFile(file);
setLocalPreviewUrl((prev) => {
if (prev) URL.revokeObjectURL(prev);
return URL.createObjectURL(file);
});
setLocalFileName(file.name);
updateState({ communityAvatarFileName: file.name });
} catch {
setErrorMessage(tUpload("errors.generic"));
}
},
[
@@ -170,28 +184,23 @@ export function CommunityUploadScreen() {
resolveUploadError,
signedIn,
tUpload,
u.fileTooLarge,
updateState,
],
);
const handleClearPendingUpload = useCallback(() => {
markCreateFlowInteraction();
setErrorMessage(null);
setLocalPreviewUrl((prev) => {
if (prev) URL.revokeObjectURL(prev);
return null;
});
if (hasCommunityAvatarUrl(state.communityAvatarUrl)) {
updateState({ communityAvatarUrl: undefined });
}
void clearPendingCommunityAvatarFile();
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}, [markCreateFlowInteraction, state.communityAvatarUrl, updateState]);
const handlePickFile = useCallback(() => {
if (!busy) fileInputRef.current?.click();
}, [busy]);
const displaySrc = serverAvatarUrl ?? localPreviewUrl;
const hasPreview = typeof displaySrc === "string" && displaySrc.length > 0;
const fileName =
localFileName ??
(typeof state.communityAvatarFileName === "string" &&
state.communityAvatarFileName.trim().length > 0
? state.communityAvatarFileName.trim()
: undefined);
return (
<CreateFlowStepShell
@@ -214,60 +223,28 @@ export function CommunityUploadScreen() {
className="sr-only"
tabIndex={-1}
accept={COMMUNITY_AVATAR_ACCEPT}
aria-label={u.hintText}
aria-label={hasPreview ? u.replaceButton : u.hintText}
onChange={handleFileChange}
/>
<div className="flex w-full flex-col items-center gap-3">
{hasPreview ? (
<div className="relative inline-block max-w-full">
<button
type="button"
onClick={handleClearPendingUpload}
className="absolute right-[8px] top-[8px] z-[1] flex h-[32px] w-[32px] cursor-pointer items-center justify-center rounded-full bg-[var(--color-surface-default-secondary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]"
aria-label={u.clearPendingUploadAriaLabel}
title={u.clearPendingUploadTooltip}
>
{/* eslint-disable-next-line @next/next/no-img-element -- matches ModalHeader close control */}
<img
src={getAssetPath(ASSETS.ICON_CLOSE)}
alt=""
className="h-[16px] w-[16px]"
style={{
filter: "brightness(0) invert(1)",
}}
/>
</button>
{/* eslint-disable-next-line @next/next/no-img-element -- user/device file or same-origin upload URL */}
<img
src={displaySrc ?? ""}
alt={u.previewAlt}
className="max-h-[200px] max-w-full rounded-[var(--radius-200)] object-contain"
/>
</div>
) : (
<Upload
active={!busy}
showHelpIcon={false}
hintText={busy ? u.uploadingLabel : u.hintText}
onClick={() => {
if (!busy) fileInputRef.current?.click();
}}
/>
)}
{signedIn === false ? (
<p className="max-w-[474px] text-center font-[family-name:var(--font-body)] text-[length:var(--font-size-body-s)] text-[var(--color-content-default-tertiary)]">
{u.signInToUploadNote}
</p>
) : null}
{errorMessage ? (
<p
className="max-w-[474px] text-center font-[family-name:var(--font-body)] text-[length:var(--font-size-body-s)] text-[var(--color-content-default-secondary)]"
role="alert"
>
{errorMessage}
</p>
) : null}
</div>
<Upload
hasLabel={false}
active={!busy}
state={
errorMessage ? "error" : busy ? "disabled" : "default"
}
hintText={
errorMessage
? errorMessage
: busy
? u.uploadingLabel
: u.hintText
}
buttonLabel={hasPreview ? u.replaceButton : undefined}
previewSrc={hasPreview ? displaySrc ?? undefined : undefined}
previewAlt={u.previewAlt}
fileName={hasPreview ? fileName : undefined}
onClick={handlePickFile}
/>
</div>
</CreateFlowStepShell>
);