diff --git a/__tests__/components/waves/memes/submission/details/ArtworkDetails.test.tsx b/__tests__/components/waves/memes/submission/details/ArtworkDetails.test.tsx index 41d52eef10..da0525a71d 100644 --- a/__tests__/components/waves/memes/submission/details/ArtworkDetails.test.tsx +++ b/__tests__/components/waves/memes/submission/details/ArtworkDetails.test.tsx @@ -19,8 +19,8 @@ describe('ArtworkDetails', () => { onDescriptionBlur={onDescriptionBlur} /> ); - const title = screen.getByLabelText('Artwork Title'); - const desc = screen.getByLabelText('Description'); + const title = screen.getByLabelText(/Artwork Title/); + const desc = screen.getByLabelText(/Description/); await user.clear(title); await user.type(title, 'new'); await user.tab(); @@ -37,11 +37,34 @@ describe('ArtworkDetails', () => { const { rerender } = render( {}} onDescriptionChange={() => {}} /> ); - const title = screen.getByLabelText('Artwork Title') as HTMLInputElement; + const title = screen.getByLabelText(/Artwork Title/) as HTMLInputElement; expect(title.value).toBe('one'); rerender( {}} onDescriptionChange={() => {}} /> ); expect(title.value).toBe('three'); }); + + it('uses neutral required markers and subtle success rings for filled fields', () => { + render( + {}} + onDescriptionChange={() => {}} + /> + ); + + const requiredMarkers = screen.getAllByText('*'); + requiredMarkers.forEach((marker) => { + expect(marker).toHaveClass('tw-text-iron-500'); + }); + + expect(screen.getByLabelText(/Artwork Title/)).toHaveClass( + 'tw-ring-emerald-500/30' + ); + expect(screen.getByLabelText(/Description/)).toHaveClass( + 'tw-ring-emerald-500/30' + ); + }); }); diff --git a/__tests__/components/waves/memes/traits/DropdownTrait.test.tsx b/__tests__/components/waves/memes/traits/DropdownTrait.test.tsx index 9ea4336012..1efc766478 100644 --- a/__tests__/components/waves/memes/traits/DropdownTrait.test.tsx +++ b/__tests__/components/waves/memes/traits/DropdownTrait.test.tsx @@ -1,36 +1,91 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { DropdownTrait } from '@/components/waves/memes/traits/DropdownTrait'; +import { render, screen, fireEvent } from "@testing-library/react"; +import { DropdownTrait } from "@/components/waves/memes/traits/DropdownTrait"; -jest.mock('components/waves/memes/traits/TraitWrapper', () => ({ - TraitWrapper: ({ children }: any) =>
{children}
+jest.mock("components/waves/memes/traits/TraitWrapper", () => ({ + TraitWrapper: ({ children, labelRightAdornment }: any) => ( +
+
{labelRightAdornment}
+ {children} +
+ ), })); -describe('DropdownTrait', () => { - const traits = { rarity: 'Common' } as any; - const options = ['Common', 'Rare']; +describe("DropdownTrait", () => { + const traits = { rarity: "Common" } as any; + const options = ["Common", "Rare"]; - it('updates value on change and blur', () => { + it("updates value on change and blur", () => { const updateText = jest.fn(); const onBlur = jest.fn(); render( - + ); - const select = screen.getByRole('combobox'); - fireEvent.change(select, { target: { value: 'Rare' } }); - expect(updateText).toHaveBeenCalledWith('rarity', 'Rare'); + const select = screen.getByRole("combobox"); + fireEvent.change(select, { target: { value: "Rare" } }); + expect(updateText).toHaveBeenCalledWith("rarity", "Rare"); fireEvent.blur(select); - expect(onBlur).toHaveBeenCalledWith('rarity'); + expect(onBlur).toHaveBeenCalledWith("rarity"); }); - it('syncs when traits change', () => { + it("syncs when traits change", () => { const { rerender } = render( - + ); - const select = screen.getByRole('combobox') as HTMLSelectElement; - expect(select.value).toBe('Common'); + const select = screen.getByRole("combobox") as HTMLSelectElement; + expect(select.value).toBe("Common"); + expect(select).toHaveClass("tw-ring-emerald-500/30"); rerender( - + ); - expect(select.value).toBe('Rare'); + expect(select.value).toBe("Rare"); + expect(select).toHaveClass("tw-ring-emerald-500/30"); + }); + + it("uses muted text for the empty option and bright text for a selected option", () => { + const updateText = jest.fn(); + render( + + ); + + const select = screen.getByRole("combobox"); + const iconContainer = screen.getByTestId("label-adornment") + .firstElementChild as HTMLElement; + expect((select as HTMLSelectElement).style.color).toBe( + "rgb(132, 132, 144)" + ); + expect(iconContainer).toHaveClass("tw-hidden"); + + fireEvent.change(select, { target: { value: "Rare" } }); + + expect((select as HTMLSelectElement).style.color).toBe( + "rgb(239, 239, 241)" + ); + expect(iconContainer).not.toHaveClass("tw-hidden"); + expect(updateText).toHaveBeenCalledWith("rarity", "Rare"); }); }); diff --git a/__tests__/components/waves/memes/traits/TextTrait.test.tsx b/__tests__/components/waves/memes/traits/TextTrait.test.tsx index f0b85e1659..22f99bf97e 100644 --- a/__tests__/components/waves/memes/traits/TextTrait.test.tsx +++ b/__tests__/components/waves/memes/traits/TextTrait.test.tsx @@ -42,8 +42,10 @@ test("syncs input when traits change", () => { ); const input = screen.getByRole("textbox") as HTMLInputElement; expect(input.value).toBe("one"); + expect(input).toHaveClass("tw-ring-emerald-500/30"); rerender( - + ); - expect(input.value).toBe("two"); + expect(input.value).toBe(""); + expect(input).toHaveClass("tw-ring-iron-700"); }); diff --git a/__tests__/components/waves/memes/traits/TraitWrapper.test.tsx b/__tests__/components/waves/memes/traits/TraitWrapper.test.tsx new file mode 100644 index 0000000000..03ae5ee21a --- /dev/null +++ b/__tests__/components/waves/memes/traits/TraitWrapper.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import { TraitWrapper } from "@/components/waves/memes/traits/TraitWrapper"; + +describe("TraitWrapper", () => { + it("shows a neutral required marker for standard fields", () => { + render( + + + + ); + + const requiredMarker = screen.getByText("*"); + expect(requiredMarker).toHaveClass("tw-text-iron-500"); + }); + + it("shows a neutral required marker for boolean fields", () => { + render( + + + + ); + + const requiredMarker = screen.getByText("*"); + expect(requiredMarker).toHaveClass("tw-text-iron-500"); + }); + + it("hides the required marker for read-only fields", () => { + render( + + + + ); + + expect(screen.queryByText("*")).not.toBeInTheDocument(); + }); +}); diff --git a/components/waves/memes/MemesArtSubmissionFile.tsx b/components/waves/memes/MemesArtSubmissionFile.tsx index 93e7323994..1b8c44030a 100644 --- a/components/waves/memes/MemesArtSubmissionFile.tsx +++ b/components/waves/memes/MemesArtSubmissionFile.tsx @@ -12,6 +12,8 @@ import { motion } from "framer-motion"; import { AuthContext } from "@/components/auth/Auth"; import { TabToggle } from "@/components/common/TabToggle"; import SandboxedExternalIframe from "@/components/common/SandboxedExternalIframe"; +import type { CommonSelectItem } from "@/components/utils/select/CommonSelect"; +import CommonTabs from "@/components/utils/select/tabs/CommonTabs"; import FilePreview from "./file-upload/components/FilePreview"; import UploadArea from "./file-upload/components/UploadArea"; @@ -28,12 +30,17 @@ import { ALLOWED_INTERACTIVE_MEDIA_MIME_TYPES, DEFAULT_INTERACTIVE_MEDIA_MIME_TYPE, INTERACTIVE_MEDIA_PROVIDERS, + type InteractiveMediaProvider, } from "./submission/constants/media"; const renderPreviewMessage = (primary: string, secondary: string) => ( -
- {primary} - {secondary} +
+ + {primary} + + + {secondary} +
); @@ -164,25 +171,25 @@ const MemesArtSubmissionFile: React.FC = ({ [setMediaSource, onExternalMimeTypeChange] ); - const providerOptions = useMemo( + const providerOptions = useMemo< + readonly CommonSelectItem[] + >( () => INTERACTIVE_MEDIA_PROVIDERS.map((provider) => ({ key: provider.key, label: provider.label, - panelId: `memes-art-submission-provider-${provider.key}`, + value: provider.key, })), [] ); const handleProviderSelect = useCallback( - (key: string) => { - if (key === externalProvider) { + (provider: InteractiveMediaProvider) => { + if (provider === externalProvider) { return; } - if (key === "ipfs" || key === "arweave") { - onExternalProviderChange(key); - } + onExternalProviderChange(provider); }, [externalProvider, onExternalProviderChange] ); @@ -262,166 +269,179 @@ const MemesArtSubmissionFile: React.FC = ({ }, [externalMimeType]); return ( -
-
+
+
- - {showUploadUi ? ( - - - - {!browserSupport.supported && browserSupport.reason && ( - - )} - - {!artworkUploaded ? ( - - ) : ( - - )} - - ) : ( -
-
- - Hosting Network - -
- -
-
- -
- +
+ {showUploadUi ? ( + onExternalHashChange(event.target.value)} - aria-invalid={Boolean(externalError)} + ref={fileInputRef} + type="file" + accept={SUBMISSION_FILE_INPUT_ACCEPT} + className="tw-hidden" + onChange={handleFileInputChange} + data-testid="artwork-file-input" /> - {externalError && ( -

{externalError}

- )} - {externalConstructedUrl && !externalError && ( -

- Resulting URL{" "} - - {externalConstructedUrl} - -

+ + {!browserSupport.supported && browserSupport.reason && ( + )} -
-
- - Media Type - -
- {mediaTypeLabel} - - Fixed to interactive HTML (text/html) + {!artworkUploaded ? ( + + ) : ( + + )} + + ) : ( +
+
+ + Hosting Network +
+ +
-
-
- -
+
+ + onExternalHashChange(event.target.value)} + aria-invalid={Boolean(externalError)} + /> + {externalError && ( +

+ {externalError} +

+ )} + {externalConstructedUrl && !externalError && ( +

+ Resulting URL{" "} + + {externalConstructedUrl} + +

+ )} +
+ +
+
+ + Media Type + +
+ {mediaTypeLabel} + + Fixed to interactive HTML (text/html) + +
+
+ +
+ +
-
- {isExternalMediaValid ? ( - + {isExternalMediaValid ? ( + + ) : ( +
+ {previewFallback} +
)} - /> - ) : ( -
- {previewFallback}
- )} +
-
- )} + )} +
); }; diff --git a/components/waves/memes/MemesArtSubmissionModal.tsx b/components/waves/memes/MemesArtSubmissionModal.tsx index 9c78353e8d..254947fabc 100644 --- a/components/waves/memes/MemesArtSubmissionModal.tsx +++ b/components/waves/memes/MemesArtSubmissionModal.tsx @@ -42,7 +42,7 @@ const MemesArtSubmissionModal: React.FC = ({ exit={{ opacity: 0 }} transition={{ duration: 0.25, ease: "easeOut" }} data-testid="memes-art-submission-modal-panel" - className="tw-flex tw-h-[100dvh] tw-max-h-[100dvh] tw-w-full tw-flex-col md:tw-h-full md:tw-max-h-none" + className="tw-flex tw-h-[100dvh] tw-max-h-[100dvh] tw-w-full tw-max-w-screen-xl tw-flex-col md:tw-h-full md:tw-max-h-none" ref={modalRef} onClick={(e) => { e.stopPropagation(); diff --git a/components/waves/memes/MemesArtSubmissionTraits.tsx b/components/waves/memes/MemesArtSubmissionTraits.tsx index 775156e004..52f1fab937 100644 --- a/components/waves/memes/MemesArtSubmissionTraits.tsx +++ b/components/waves/memes/MemesArtSubmissionTraits.tsx @@ -15,6 +15,8 @@ interface MemesArtSubmissionTraitsProps { | undefined; readonly onFieldBlur?: ((field: keyof TraitsData) => void) | undefined; readonly readOnlyOverrides?: Partial>; + readonly showRequiredMarkers?: boolean | undefined; + readonly size?: "default" | "sm" | undefined; } /** @@ -29,6 +31,8 @@ const MemesArtSubmissionTraits: React.FC = ({ validationErrors = {}, onFieldBlur, readOnlyOverrides, + showRequiredMarkers = false, + size, }) => { const { connectedProfile } = useAuth(); @@ -63,42 +67,99 @@ const MemesArtSubmissionTraits: React.FC = ({ ); return ( -
+
{showTitle && ( -

+

Artwork Traits

)} -
- {formSections.map((section, sectionIndex) => ( -
-
- {section.fields.map((field, fieldIndex) => { - const readOnlyOverride = readOnlyOverrides?.[field.field]; - const traitFieldOverrideProps = - readOnlyOverride === undefined - ? {} - : { readOnlyOverride: Boolean(readOnlyOverride) }; - return ( - onFieldBlur(field.field) : undefined - } - /> - ); - })} -
-
- ))} +
+ {formSections.map((section) => { + const sectionKey = + section.title || + section.fields.map((field) => field.field).join("|"); + + const renderField = (field: (typeof section.fields)[number]) => { + const readOnlyOverride = readOnlyOverrides?.[field.field]; + const traitFieldOverrideProps = + readOnlyOverride === undefined + ? {} + : { readOnlyOverride: Boolean(readOnlyOverride) }; + return ( + onFieldBlur(field.field) : undefined + } + showRequiredMarkers={showRequiredMarkers} + size={size} + /> + ); + }; + + // Basic Information: artist + seizeArtistProfile side-by-side, memeName below + if (section.title === "Basic Information") { + const artistField = section.fields.find( + (f) => f.field === "artist" + ); + const profileField = section.fields.find( + (f) => f.field === "seizeArtistProfile" + ); + const remainingFields = section.fields.filter( + (f) => f.field !== "artist" && f.field !== "seizeArtistProfile" + ); + + return ( +
+
+
+ {artistField && renderField(artistField)} + {profileField && renderField(profileField)} +
+ {remainingFields.map((field) => renderField(field))} +
+
+ ); + } + + // Card Points: all 4 fields in one row + if (section.title === "Card Points") { + return ( +
+
+ {section.fields.map((field) => renderField(field))} +
+
+ ); + } + + // Card Attributes: 2-column grid for boolean toggles + if (section.title === "Card Attributes") { + return ( +
+
+ {section.fields.map((field) => renderField(field))} +
+
+ ); + } + + return ( +
+
+ {section.fields.map((field) => renderField(field))} +
+
+ ); + })}
); diff --git a/components/waves/memes/file-upload/components/FilePreview.tsx b/components/waves/memes/file-upload/components/FilePreview.tsx index 17e2845468..a55e8bf417 100644 --- a/components/waves/memes/file-upload/components/FilePreview.tsx +++ b/components/waves/memes/file-upload/components/FilePreview.tsx @@ -150,7 +150,7 @@ const FilePreview: React.FC = ({
diff --git a/components/waves/memes/file-upload/components/FileTypeIndicator.tsx b/components/waves/memes/file-upload/components/FileTypeIndicator.tsx index 7c9f007087..ad72a1ec23 100644 --- a/components/waves/memes/file-upload/components/FileTypeIndicator.tsx +++ b/components/waves/memes/file-upload/components/FileTypeIndicator.tsx @@ -1,10 +1,17 @@ +import type { SubmissionMediaCategory } from '@/constants/submission-media.constants'; +import { + CubeIcon, + FilmIcon, + PhotoIcon, +} from '@heroicons/react/24/outline'; import React from 'react'; /** * Props for the FileTypeIndicator component */ interface FileTypeIndicatorProps { - readonly format: string; + readonly kind: SubmissionMediaCategory; + readonly label: string; } /** @@ -16,12 +23,30 @@ interface FileTypeIndicatorProps { * @param props Component props * @returns JSX Element */ -const FileTypeIndicator: React.FC = ({ format }) => ( - - {format} - -); +const ICON_BY_KIND: Record>> = { + image: PhotoIcon, + video: FilmIcon, + interactive: CubeIcon, +}; -export default FileTypeIndicator; \ No newline at end of file +const FileTypeIndicator: React.FC = ({ + kind, + label, +}) => { + const Icon = ICON_BY_KIND[kind]; + + return ( + + + + ); +}; + +export default FileTypeIndicator; diff --git a/components/waves/memes/file-upload/components/UploadArea.tsx b/components/waves/memes/file-upload/components/UploadArea.tsx index cbc459e92c..11c303e7c0 100644 --- a/components/waves/memes/file-upload/components/UploadArea.tsx +++ b/components/waves/memes/file-upload/components/UploadArea.tsx @@ -3,7 +3,7 @@ import { motion } from 'framer-motion'; import type { UploadAreaProps } from '../reducers/types'; import ErrorMessage from './ErrorMessage'; import FileTypeIndicator from './FileTypeIndicator'; -import { SUBMISSION_UI_FORMAT_CATEGORIES } from '@/constants/submission-media.constants'; +import { SUBMISSION_UI_FORMAT_GROUPS } from '@/constants/submission-media.constants'; /** * Upload Area Component @@ -77,10 +77,14 @@ const UploadArea: React.FC = ({
{/* File type indicators */} -
-
- {SUBMISSION_UI_FORMAT_CATEGORIES.map((format) => ( - +
+
+ {SUBMISSION_UI_FORMAT_GROUPS.map((formatGroup) => ( + ))}
@@ -112,4 +116,4 @@ const UploadArea: React.FC = ({
); -export default UploadArea; \ No newline at end of file +export default UploadArea; diff --git a/components/waves/memes/submission/MemesArtSubmissionContainer.tsx b/components/waves/memes/submission/MemesArtSubmissionContainer.tsx index cfecf82926..4529758f3f 100644 --- a/components/waves/memes/submission/MemesArtSubmissionContainer.tsx +++ b/components/waves/memes/submission/MemesArtSubmissionContainer.tsx @@ -298,22 +298,13 @@ const MemesArtSubmissionContainer: FC = ({ return (
-
-
-
-
-
-
+
-
+
- + Submit Work to The Memes = ({ ) => (
- + {title}