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
@@ -0,0 +1,90 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemesSubmissionPreviewScreen } from "@/components/waves/memes/submission/preview/MemesSubmissionPreviewScreen";

jest.mock("framer-motion", () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
}));

jest.mock("@/components/memes/drops/MemesLeaderboardDrop", () => ({
MemesLeaderboardDrop: () => <div data-testid="preview-list-card" />,
}));

jest.mock(
"@/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItem",
() => ({
WaveLeaderboardGalleryItem: () => (
<div data-testid="preview-gallery-card" />
),
})
);

jest.mock("@/components/utils/button/SecondaryButton", () => (props: any) => (
<button onClick={props.onClicked} disabled={props.disabled}>
{props.children}
</button>
));

jest.mock("@/components/utils/button/PrimaryButton", () => (props: any) => (
<button
onClick={props.onClicked}
disabled={props.disabled || props.loading}
data-padding={props.padding}
>
{props.children}
</button>
));

describe("MemesSubmissionPreviewScreen", () => {
const previewDrop = { id: "drop-1" } as any;

it("renders isolated preview cases in the expected order", () => {
render(
<MemesSubmissionPreviewScreen
previewDrop={previewDrop}
onBackToEdit={jest.fn()}
onSubmit={jest.fn()}
isSubmitting={false}
/>
);

expect(
screen.getByRole("heading", { name: "Submission Preview" })
).toBeInTheDocument();

const caseTitles = screen
.getAllByRole("heading", { level: 5 })
.map((heading) => heading.textContent?.trim());

expect(caseTitles).toEqual([
"Leaderboard List Card",
"Leaderboard Gallery Card",
]);

expect(screen.getByTestId("preview-list-card")).toBeInTheDocument();
expect(screen.getByTestId("preview-gallery-card")).toBeInTheDocument();
});

it("keeps footer actions wired", async () => {
const user = userEvent.setup();
const onBackToEdit = jest.fn();
const onSubmit = jest.fn();

render(
<MemesSubmissionPreviewScreen
previewDrop={previewDrop}
onBackToEdit={onBackToEdit}
onSubmit={onSubmit}
isSubmitting={false}
/>
);

await user.click(screen.getByRole("button", { name: "Back to Edit" }));
await user.click(screen.getByRole("button", { name: "Submit Artwork" }));

expect(onBackToEdit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PREVIEW_CASE_WIDTHS } from "@/components/waves/memes/submission/preview/presentation/presets";

describe("PREVIEW_CASE_WIDTHS", () => {
it("includes all expected preview cases", () => {
expect(Object.keys(PREVIEW_CASE_WIDTHS).sort()).toEqual(
["leaderboardGalleryItem", "leaderboardList"].sort()
);
});

it("uses production-mimic responsive width presets", () => {
expect(PREVIEW_CASE_WIDTHS).toEqual({
leaderboardList:
"tw-max-w-full sm:tw-max-w-[34rem] lg:tw-max-w-[44rem] xl:tw-max-w-[52rem]",
leaderboardGalleryItem:
"tw-max-w-full sm:tw-max-w-[20rem] lg:tw-max-w-[22rem] xl:tw-max-w-[24rem]",
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { buildPreviewDrop } from "@/components/waves/memes/submission/utils/buildPreviewDrop";

describe("buildPreviewDrop", () => {
it("sets placeholder score and voter count for preview cards", () => {
const previewDrop = buildPreviewDrop({
wave: {
id: "wave-1",
name: "Preview Wave",
picture: null,
description_drop: { id: "drop-description-1" },
voting: {
authenticated_user_eligible: true,
period: { min: 1, max: 2 },
credit_type: "NIC",
forbid_negative_votes: false,
},
participation: { authenticated_user_eligible: true },
chat: { authenticated_user_eligible: true },
wave: { admin_drop_deletion_enabled: false },
pinned: false,
} as any,
traits: {
title: "Preview Title",
description: "Preview Description",
} as any,
operationalData: undefined,
mediaSelection: {
mediaSource: "url",
selectedFile: null,
externalUrl: "https://example.com/art.png",
externalMimeType: "image/png",
isExternalValid: true,
},
uploadArtworkUrl: "",
connectedProfile: null,
});

expect(previewDrop.rating).toBe(6529);
expect(previewDrop.realtime_rating).toBe(6529420);
expect(previewDrop.rating_prediction).toBe(69420);
expect(previewDrop.raters_count).toBe(69);
});
});
105 changes: 76 additions & 29 deletions components/waves/memes/submission/MemesArtSubmissionContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
"use client";

import { useAuth } from "@/components/auth/Auth";
import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import type { ApiWave } from "@/generated/models/ApiWave";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import type { FC } from "react";
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useState } from "react";
import { useArtworkSubmissionForm } from "./hooks/useArtworkSubmissionForm";
import { useArtworkSubmissionMutation } from "./hooks/useArtworkSubmissionMutation";
import { MemesSubmissionPreviewScreen } from "./preview/MemesSubmissionPreviewScreen";
import AdditionalInfoStep from "./steps/AdditionalInfoStep";
import AgreementStep from "./steps/AgreementStep";
import ArtworkStep from "./steps/ArtworkStep";
import { SubmissionStep } from "./types/Steps";
import type { SubmissionPhase } from "./ui/SubmissionProgress";
import { buildPreviewDrop } from "./utils/buildPreviewDrop";

interface MemesArtSubmissionContainerProps {
readonly onClose: () => void;
Expand All @@ -37,7 +41,10 @@ const MemesArtSubmissionContainer: FC<MemesArtSubmissionContainerProps> = ({
}) => {
// Use the form hook to manage all state
const form = useArtworkSubmissionForm();
const { connectedProfile } = useAuth();
const { isSafeWallet, address } = useSeizeConnectContext();
const [isPreviewMode, setIsPreviewMode] = useState(false);
const [previewDrop, setPreviewDrop] = useState<ExtendedDrop | null>(null);

// Use the mutation hook for submission
const {
Expand All @@ -58,6 +65,11 @@ const MemesArtSubmissionContainer: FC<MemesArtSubmissionContainerProps> = ({
return () => clearTimeout(timer);
}, [submissionPhase, onClose]);

const resetPreviewState = useCallback(() => {
setIsPreviewMode(false);
setPreviewDrop(null);
}, []);

// Handle file selection
const handleFileSelect = (file: File) => {
form.handleFileSelect(file);
Expand Down Expand Up @@ -90,6 +102,32 @@ const MemesArtSubmissionContainer: FC<MemesArtSubmissionContainerProps> = ({
console.warn(`Submission phase changed to: ${phase}`);
}, []);

const handleOpenPreview = useCallback(() => {
const { imageUrl, traits, operationalData } = form.getSubmissionData();
const media = form.getMediaSelection();

setPreviewDrop(
buildPreviewDrop({
wave,
traits,
operationalData,
mediaSelection: media,
uploadArtworkUrl: imageUrl,
connectedProfile,
})
);
setIsPreviewMode(true);
}, [connectedProfile, form, wave]);

const handleBackToEdit = useCallback(() => {
setIsPreviewMode(false);
}, []);

const handleBackFromAdditionalInfo = () => {
resetPreviewState();
form.handleBackToArtwork();
};

// Handle final submission
const handleSubmit = async () => {
// Get submission data including all traits
Expand Down Expand Up @@ -218,34 +256,43 @@ const MemesArtSubmissionContainer: FC<MemesArtSubmissionContainerProps> = ({
submissionError={submissionError}
/>
),
[SubmissionStep.ADDITIONAL_INFO]: (
<AdditionalInfoStep
airdropEntries={form.operationalData.airdrop_config}
onAirdropEntriesChange={form.setAirdropConfig}
paymentInfo={form.operationalData.payment_info}
onPaymentInfoChange={form.setPaymentInfo}
allowlistBatches={form.operationalData.allowlist_batches}
supportingMedia={
form.operationalData.additional_media.artwork_commentary_media
}
artworkCommentary={form.operationalData.commentary}
aboutArtist={form.operationalData.about_artist}
previewImage={form.operationalData.additional_media.preview_image}
promoVideo={form.operationalData.additional_media.promo_video}
requiresPreviewImage={requiresPreviewImage}
requiresPromoVideoOption={requiresPromoVideoOption}
previewRequiredMediaType={previewRequiredMediaType}
onBatchesChange={form.setAllowlistBatches}
onSupportingMediaChange={handleArtworkCommentaryMediaChange}
onPreviewImageChange={handlePreviewImageChange}
onPromoVideoChange={handlePromoVideoChange}
onArtworkCommentaryChange={form.setCommentary}
onAboutArtistChange={form.setAboutArtist}
onBack={form.handleBackToArtwork}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
/>
),
[SubmissionStep.ADDITIONAL_INFO]:
isPreviewMode && previewDrop ? (
<MemesSubmissionPreviewScreen
previewDrop={previewDrop}
onBackToEdit={handleBackToEdit}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
/>
) : (
<AdditionalInfoStep
airdropEntries={form.operationalData.airdrop_config}
onAirdropEntriesChange={form.setAirdropConfig}
paymentInfo={form.operationalData.payment_info}
onPaymentInfoChange={form.setPaymentInfo}
allowlistBatches={form.operationalData.allowlist_batches}
supportingMedia={
form.operationalData.additional_media.artwork_commentary_media
}
artworkCommentary={form.operationalData.commentary}
aboutArtist={form.operationalData.about_artist}
previewImage={form.operationalData.additional_media.preview_image}
promoVideo={form.operationalData.additional_media.promo_video}
requiresPreviewImage={requiresPreviewImage}
requiresPromoVideoOption={requiresPromoVideoOption}
previewRequiredMediaType={previewRequiredMediaType}
onBatchesChange={form.setAllowlistBatches}
onSupportingMediaChange={handleArtworkCommentaryMediaChange}
onPreviewImageChange={handlePreviewImageChange}
onPromoVideoChange={handlePromoVideoChange}
onArtworkCommentaryChange={form.setCommentary}
onAboutArtistChange={form.setAboutArtist}
onBack={handleBackFromAdditionalInfo}
onPreview={handleOpenPreview}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
/>
),
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { InteractiveMediaMimeType } from "../constants/media";
import type { TraitsData } from "../types/TraitsData";
import type { SubmissionPhase } from "../ui/SubmissionProgress";
import { validateStrictAddress } from "../utils/addressValidation";
import { objectEntries } from "../utils/objectEntries";

import { OperationalData } from "../types/OperationalData";

Expand Down Expand Up @@ -58,9 +59,9 @@ const transformToApiRequest = (data: {
} = data;

// Create metadata array from trait data
const metadata: ApiDropMetadata[] = Object.entries(traits)
const metadata: ApiDropMetadata[] = objectEntries(traits)
.map(([key, value]) => ({
data_key: key,
data_key: String(key),
data_value: value?.toString(),
}))
.filter(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import SecondaryButton from "@/components/utils/button/SecondaryButton";
import PrimaryButton from "@/components/utils/button/PrimaryButton";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { motion } from "framer-motion";
import { useCallback } from "react";
import { PreviewLeaderboardGalleryCase } from "./components/PreviewLeaderboardGalleryCase";
import { PreviewLeaderboardListCase } from "./components/PreviewLeaderboardListCase";

interface MemesSubmissionPreviewScreenProps {
readonly previewDrop: ExtendedDrop;
readonly onBackToEdit: () => void;
readonly onSubmit: () => void;
readonly isSubmitting: boolean;
}

export function MemesSubmissionPreviewScreen({
previewDrop,
onBackToEdit,
onSubmit,
isSubmitting,
}: MemesSubmissionPreviewScreenProps) {
const onDropClick = useCallback((_drop: ExtendedDrop) => {}, []);

return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="tw-mx-auto tw-flex tw-h-full tw-max-w-6xl tw-flex-col tw-pb-8"
>
<div className="tw-flex-1 tw-space-y-10 tw-overflow-y-auto tw-px-4 tw-py-2 tw-scrollbar-thin tw-scrollbar-track-iron-900 tw-scrollbar-thumb-iron-700">
<div className="tw-space-y-1">
<h4 className="tw-mb-0 tw-text-base tw-font-semibold tw-text-iron-100">
Submission Preview
</h4>
<p className="tw-mb-0 tw-text-sm tw-text-iron-400">
Read-only preview of how your submission may appear in different
views.
</p>
</div>

<PreviewLeaderboardListCase
drop={previewDrop}
onDropClick={onDropClick}
/>
<PreviewLeaderboardGalleryCase
drop={previewDrop}
onDropClick={onDropClick}
/>
</div>

<div className="tw-mt-auto tw-flex tw-items-center tw-justify-between tw-border-t tw-border-iron-800 tw-px-4 tw-pt-6">
<SecondaryButton onClicked={onBackToEdit} disabled={isSubmitting}>
Back to Edit
</SecondaryButton>
<PrimaryButton
onClicked={onSubmit}
disabled={false}
loading={isSubmitting}
padding="tw-px-6 tw-py-3"
>
Submit Artwork
</PrimaryButton>
</div>
</motion.div>
);
}
Loading