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:
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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user