diff --git a/__tests__/components/waves/memes/submission/preview/MemesSubmissionPreviewScreen.test.tsx b/__tests__/components/waves/memes/submission/preview/MemesSubmissionPreviewScreen.test.tsx
new file mode 100644
index 0000000000..6a9f56ee0c
--- /dev/null
+++ b/__tests__/components/waves/memes/submission/preview/MemesSubmissionPreviewScreen.test.tsx
@@ -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) =>
{children}
,
+ },
+}));
+
+jest.mock("@/components/memes/drops/MemesLeaderboardDrop", () => ({
+ MemesLeaderboardDrop: () => ,
+}));
+
+jest.mock(
+ "@/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItem",
+ () => ({
+ WaveLeaderboardGalleryItem: () => (
+
+ ),
+ })
+);
+
+jest.mock("@/components/utils/button/SecondaryButton", () => (props: any) => (
+
+));
+
+jest.mock("@/components/utils/button/PrimaryButton", () => (props: any) => (
+
+));
+
+describe("MemesSubmissionPreviewScreen", () => {
+ const previewDrop = { id: "drop-1" } as any;
+
+ it("renders isolated preview cases in the expected order", () => {
+ render(
+
+ );
+
+ 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(
+
+ );
+
+ 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);
+ });
+});
diff --git a/__tests__/components/waves/memes/submission/preview/presets.test.ts b/__tests__/components/waves/memes/submission/preview/presets.test.ts
new file mode 100644
index 0000000000..776301a442
--- /dev/null
+++ b/__tests__/components/waves/memes/submission/preview/presets.test.ts
@@ -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]",
+ });
+ });
+});
diff --git a/__tests__/components/waves/memes/submission/utils/buildPreviewDrop.test.ts b/__tests__/components/waves/memes/submission/utils/buildPreviewDrop.test.ts
new file mode 100644
index 0000000000..61578545b8
--- /dev/null
+++ b/__tests__/components/waves/memes/submission/utils/buildPreviewDrop.test.ts
@@ -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);
+ });
+});
diff --git a/components/waves/memes/submission/MemesArtSubmissionContainer.tsx b/components/waves/memes/submission/MemesArtSubmissionContainer.tsx
index 01b7ccba88..f713bd4a30 100644
--- a/components/waves/memes/submission/MemesArtSubmissionContainer.tsx
+++ b/components/waves/memes/submission/MemesArtSubmissionContainer.tsx
@@ -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;
@@ -37,7 +41,10 @@ const MemesArtSubmissionContainer: FC = ({
}) => {
// 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(null);
// Use the mutation hook for submission
const {
@@ -58,6 +65,11 @@ const MemesArtSubmissionContainer: FC = ({
return () => clearTimeout(timer);
}, [submissionPhase, onClose]);
+ const resetPreviewState = useCallback(() => {
+ setIsPreviewMode(false);
+ setPreviewDrop(null);
+ }, []);
+
// Handle file selection
const handleFileSelect = (file: File) => {
form.handleFileSelect(file);
@@ -90,6 +102,32 @@ const MemesArtSubmissionContainer: FC = ({
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
@@ -218,34 +256,43 @@ const MemesArtSubmissionContainer: FC = ({
submissionError={submissionError}
/>
),
- [SubmissionStep.ADDITIONAL_INFO]: (
-
- ),
+ [SubmissionStep.ADDITIONAL_INFO]:
+ isPreviewMode && previewDrop ? (
+
+ ) : (
+
+ ),
};
return (
diff --git a/components/waves/memes/submission/hooks/useArtworkSubmissionMutation.ts b/components/waves/memes/submission/hooks/useArtworkSubmissionMutation.ts
index e9d03a039a..30cf0d3e93 100644
--- a/components/waves/memes/submission/hooks/useArtworkSubmissionMutation.ts
+++ b/components/waves/memes/submission/hooks/useArtworkSubmissionMutation.ts
@@ -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";
@@ -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(
diff --git a/components/waves/memes/submission/preview/MemesSubmissionPreviewScreen.tsx b/components/waves/memes/submission/preview/MemesSubmissionPreviewScreen.tsx
new file mode 100644
index 0000000000..3f4aaf8df8
--- /dev/null
+++ b/components/waves/memes/submission/preview/MemesSubmissionPreviewScreen.tsx
@@ -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 (
+
+
+
+
+ Submission Preview
+
+
+ Read-only preview of how your submission may appear in different
+ views.
+
+
+
+
+
+
+
+
+
+ Back to Edit
+
+
+ Submit Artwork
+
+
+
+ );
+}
diff --git a/components/waves/memes/submission/preview/components/PreviewCaseSection.tsx b/components/waves/memes/submission/preview/components/PreviewCaseSection.tsx
new file mode 100644
index 0000000000..fc26c5670b
--- /dev/null
+++ b/components/waves/memes/submission/preview/components/PreviewCaseSection.tsx
@@ -0,0 +1,25 @@
+import type { ReactNode } from "react";
+
+interface PreviewCaseSectionProps {
+ readonly title: string;
+ readonly description?: string;
+ readonly children: ReactNode;
+}
+
+export function PreviewCaseSection({
+ title,
+ description,
+ children,
+}: PreviewCaseSectionProps) {
+ return (
+
+
+ {title}
+
+ {description && (
+ {description}
+ )}
+ {children}
+
+ );
+}
diff --git a/components/waves/memes/submission/preview/components/PreviewLeaderboardGalleryCase.tsx b/components/waves/memes/submission/preview/components/PreviewLeaderboardGalleryCase.tsx
new file mode 100644
index 0000000000..fcfcedbd1d
--- /dev/null
+++ b/components/waves/memes/submission/preview/components/PreviewLeaderboardGalleryCase.tsx
@@ -0,0 +1,25 @@
+import { WaveLeaderboardGalleryItem } from "@/components/waves/leaderboard/gallery/WaveLeaderboardGalleryItem";
+import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
+import { PREVIEW_CASE_WIDTHS } from "../presentation/presets";
+import { PreviewCaseSection } from "./PreviewCaseSection";
+import { PreviewWidthFrame } from "./PreviewWidthFrame";
+
+interface PreviewLeaderboardGalleryCaseProps {
+ readonly drop: ExtendedDrop;
+ readonly onDropClick: (drop: ExtendedDrop) => void;
+}
+
+export function PreviewLeaderboardGalleryCase({
+ drop,
+ onDropClick,
+}: PreviewLeaderboardGalleryCaseProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/components/waves/memes/submission/preview/components/PreviewLeaderboardListCase.tsx b/components/waves/memes/submission/preview/components/PreviewLeaderboardListCase.tsx
new file mode 100644
index 0000000000..2069e88a61
--- /dev/null
+++ b/components/waves/memes/submission/preview/components/PreviewLeaderboardListCase.tsx
@@ -0,0 +1,23 @@
+import { MemesLeaderboardDrop } from "@/components/memes/drops/MemesLeaderboardDrop";
+import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
+import { PREVIEW_CASE_WIDTHS } from "../presentation/presets";
+import { PreviewCaseSection } from "./PreviewCaseSection";
+import { PreviewWidthFrame } from "./PreviewWidthFrame";
+
+interface PreviewLeaderboardListCaseProps {
+ readonly drop: ExtendedDrop;
+ readonly onDropClick: (drop: ExtendedDrop) => void;
+}
+
+export function PreviewLeaderboardListCase({
+ drop,
+ onDropClick,
+}: PreviewLeaderboardListCaseProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/components/waves/memes/submission/preview/components/PreviewWidthFrame.tsx b/components/waves/memes/submission/preview/components/PreviewWidthFrame.tsx
new file mode 100644
index 0000000000..1ca5b17ab7
--- /dev/null
+++ b/components/waves/memes/submission/preview/components/PreviewWidthFrame.tsx
@@ -0,0 +1,35 @@
+import type { ReactNode } from "react";
+
+interface PreviewWidthFrameProps {
+ readonly maxWidthClass: string;
+ readonly minWidthClass?: string;
+ readonly contentClassName?: string;
+ readonly disablePointerEvents?: boolean;
+ readonly children: ReactNode;
+}
+
+export function PreviewWidthFrame({
+ maxWidthClass,
+ minWidthClass,
+ contentClassName,
+ disablePointerEvents = true,
+ children,
+}: PreviewWidthFrameProps) {
+ const frameClassName = ["tw-w-full", maxWidthClass, minWidthClass]
+ .filter(Boolean)
+ .join(" ");
+ const contentClasses = [
+ disablePointerEvents ? "tw-pointer-events-none" : "",
+ contentClassName,
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ return (
+
+ );
+}
diff --git a/components/waves/memes/submission/preview/presentation/presets.ts b/components/waves/memes/submission/preview/presentation/presets.ts
new file mode 100644
index 0000000000..783be435cf
--- /dev/null
+++ b/components/waves/memes/submission/preview/presentation/presets.ts
@@ -0,0 +1,6 @@
+export const PREVIEW_CASE_WIDTHS = {
+ 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]",
+} as const;
diff --git a/components/waves/memes/submission/steps/AdditionalInfoStep.tsx b/components/waves/memes/submission/steps/AdditionalInfoStep.tsx
index e68b3c7285..ee766abfff 100644
--- a/components/waves/memes/submission/steps/AdditionalInfoStep.tsx
+++ b/components/waves/memes/submission/steps/AdditionalInfoStep.tsx
@@ -7,13 +7,13 @@ import type { FC } from "react";
import AdditionalMediaUpload from "../components/AdditionalMediaUpload";
import AirdropConfig from "../components/AirdropConfig";
import AllowlistBatchManager, {
- AllowlistBatchRaw,
+ type AllowlistBatchRaw,
} from "../components/AllowlistBatchManager";
import PaymentConfig from "../components/PaymentConfig";
import {
AIRDROP_TOTAL,
- AirdropEntry,
- PaymentInfo,
+ type AirdropEntry,
+ type PaymentInfo,
} from "../types/OperationalData";
import { validateStrictAddress } from "../utils/addressValidation";
import { validateTokenIdFormat } from "../utils/tokenParsing";
@@ -39,6 +39,7 @@ interface AdditionalInfoStepProps {
readonly onArtworkCommentaryChange: (commentary: string) => void;
readonly onAboutArtistChange: (aboutArtist: string) => void;
readonly onBack: () => void;
+ readonly onPreview: () => void;
readonly onSubmit: () => void;
readonly isSubmitting: boolean;
}
@@ -64,6 +65,7 @@ const AdditionalInfoStep: FC = ({
onArtworkCommentaryChange,
onAboutArtistChange,
onBack,
+ onPreview,
onSubmit,
isSubmitting,
}) => {
@@ -76,7 +78,7 @@ const AdditionalInfoStep: FC = ({
};
const getTokenIdsError = (tokenIds: string) => {
- return validateTokenIdFormat(tokenIds) || undefined;
+ return validateTokenIdFormat(tokenIds) ?? undefined;
};
const isFormValid = () => {
@@ -87,10 +89,7 @@ const AdditionalInfoStep: FC = ({
);
if (hasInvalidCount) return false;
- const totalAllocated = airdropEntries.reduce(
- (sum, e) => sum + (e.count ?? 0),
- 0
- );
+ const totalAllocated = airdropEntries.reduce((sum, e) => sum + e.count, 0);
if (totalAllocated !== AIRDROP_TOTAL) return false;
// Every entry with count > 0 must have a valid address
@@ -146,6 +145,8 @@ const AdditionalInfoStep: FC = ({
return true;
};
+ const formValid = isFormValid();
+
return (
= ({
Back
-
- Submit Artwork
-
+
+
+ Preview
+
+
+ Submit Artwork
+
+
);
diff --git a/components/waves/memes/submission/utils/buildPreviewDrop.ts b/components/waves/memes/submission/utils/buildPreviewDrop.ts
new file mode 100644
index 0000000000..08aaa317d7
--- /dev/null
+++ b/components/waves/memes/submission/utils/buildPreviewDrop.ts
@@ -0,0 +1,232 @@
+"use client";
+
+import type { ApiIdentity } from "@/generated/models/ApiIdentity";
+import type { ApiDropMetadata } from "@/generated/models/ApiDropMetadata";
+import { ApiDropType } from "@/generated/models/ApiDropType";
+import type { ApiWave } from "@/generated/models/ApiWave";
+import { getBannerColorValue } from "@/helpers/profile-banner.helpers";
+import { DropSize, getOptimisticDropId } from "@/helpers/waves/drop.helpers";
+import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
+import type { OperationalData } from "../types/OperationalData";
+import type { TraitsData } from "../types/TraitsData";
+import { validateStrictAddress } from "./addressValidation";
+import { objectEntries } from "./objectEntries";
+
+interface PreviewMediaSelection {
+ readonly mediaSource: "upload" | "url";
+ readonly selectedFile: File | null;
+ readonly externalUrl: string;
+ readonly externalMimeType: string;
+ readonly isExternalValid: boolean;
+}
+
+interface BuildPreviewDropInput {
+ readonly wave: ApiWave;
+ readonly traits: TraitsData;
+ readonly operationalData?: OperationalData | undefined;
+ readonly mediaSelection: PreviewMediaSelection;
+ readonly uploadArtworkUrl: string;
+ readonly connectedProfile: ApiIdentity | null;
+}
+
+const FALLBACK_MEDIA_URL =
+ "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
+
+const buildMetadata = (
+ traits: TraitsData,
+ operationalData?: OperationalData
+): ApiDropMetadata[] => {
+ const metadata: ApiDropMetadata[] = objectEntries(traits)
+ .map(([key, value]) => ({
+ data_key: String(key),
+ data_value: value.toString(),
+ }))
+ .filter((item) => item.data_value.length > 0);
+
+ if (!operationalData) {
+ return metadata;
+ }
+
+ metadata.push(
+ {
+ data_key: "payment_info",
+ data_value: JSON.stringify(operationalData.payment_info),
+ },
+ {
+ data_key: "commentary",
+ data_value: operationalData.commentary,
+ },
+ {
+ data_key: "about_artist",
+ data_value: operationalData.about_artist,
+ }
+ );
+
+ if (operationalData.airdrop_config.length > 0) {
+ const validEntries = operationalData.airdrop_config.filter((entry) => {
+ const trimmedAddress = entry.address.trim();
+ return validateStrictAddress(trimmedAddress) && entry.count > 0;
+ });
+
+ if (validEntries.length > 0) {
+ metadata.push({
+ data_key: "airdrop_config",
+ data_value: JSON.stringify(validEntries),
+ });
+ }
+ }
+
+ if (operationalData.allowlist_batches.length > 0) {
+ metadata.push({
+ data_key: "allowlist_batches",
+ data_value: JSON.stringify(
+ operationalData.allowlist_batches.map((batch) => ({
+ contract: batch.contract,
+ token_ids: batch.token_ids_raw || "",
+ }))
+ ),
+ });
+ }
+
+ metadata.push({
+ data_key: "additional_media",
+ data_value: JSON.stringify(operationalData.additional_media),
+ });
+
+ return metadata;
+};
+
+const buildPreviewMedia = ({
+ mediaSelection,
+ uploadArtworkUrl,
+}: {
+ readonly mediaSelection: PreviewMediaSelection;
+ readonly uploadArtworkUrl: string;
+}) => {
+ if (
+ mediaSelection.mediaSource === "upload" &&
+ mediaSelection.selectedFile &&
+ uploadArtworkUrl
+ ) {
+ return {
+ url: uploadArtworkUrl,
+ mime_type: mediaSelection.selectedFile.type || "image/jpeg",
+ };
+ }
+
+ if (
+ mediaSelection.mediaSource === "url" &&
+ mediaSelection.isExternalValid &&
+ mediaSelection.externalUrl
+ ) {
+ return {
+ url: mediaSelection.externalUrl,
+ mime_type: mediaSelection.externalMimeType || "text/html",
+ };
+ }
+
+ return {
+ url: FALLBACK_MEDIA_URL,
+ mime_type: "image/gif",
+ };
+};
+
+export const buildPreviewDrop = ({
+ wave,
+ traits,
+ operationalData,
+ mediaSelection,
+ uploadArtworkUrl,
+ connectedProfile,
+}: BuildPreviewDropInput): ExtendedDrop => {
+ const id = getOptimisticDropId();
+ const now = Date.now();
+ const media = buildPreviewMedia({ mediaSelection, uploadArtworkUrl });
+ const metadata = buildMetadata(traits, operationalData);
+ const primaryAddress =
+ connectedProfile?.primary_wallet ??
+ "0x0000000000000000000000000000000000000000";
+
+ return {
+ type: DropSize.FULL,
+ stableKey: id,
+ stableHash: id,
+ id,
+ serial_no: now,
+ drop_type: ApiDropType.Participatory,
+ rank: null,
+ wave: {
+ id: wave.id,
+ name: wave.name,
+ picture: wave.picture,
+ description_drop_id: wave.description_drop.id,
+ authenticated_user_eligible_to_vote:
+ wave.voting.authenticated_user_eligible,
+ authenticated_user_eligible_to_participate:
+ wave.participation.authenticated_user_eligible,
+ authenticated_user_eligible_to_chat:
+ wave.chat.authenticated_user_eligible,
+ authenticated_user_admin: false,
+ visibility_group_id: null,
+ participation_group_id: null,
+ chat_group_id: null,
+ voting_group_id: null,
+ admin_group_id: null,
+ voting_period_start: wave.voting.period?.min ?? null,
+ voting_period_end: wave.voting.period?.max ?? null,
+ voting_credit_type: wave.voting.credit_type,
+ admin_drop_deletion_enabled: wave.wave.admin_drop_deletion_enabled,
+ forbid_negative_votes: wave.voting.forbid_negative_votes,
+ pinned: wave.pinned,
+ },
+ author: {
+ id: connectedProfile?.id ?? "preview-user",
+ handle: connectedProfile?.handle ?? "preview-user",
+ pfp: connectedProfile?.pfp ?? null,
+ banner1_color: getBannerColorValue(connectedProfile?.banner1),
+ banner2_color: getBannerColorValue(connectedProfile?.banner2),
+ cic: connectedProfile?.cic ?? 0,
+ rep: connectedProfile?.rep ?? 0,
+ tdh: connectedProfile?.tdh ?? 0,
+ tdh_rate: connectedProfile?.tdh_rate ?? 0,
+ xtdh: connectedProfile?.xtdh ?? 0,
+ xtdh_rate: connectedProfile?.xtdh_rate ?? 0,
+ level: connectedProfile?.level ?? 0,
+ primary_address: primaryAddress,
+ subscribed_actions: [],
+ archived: false,
+ active_main_stage_submission_ids:
+ connectedProfile?.active_main_stage_submission_ids ?? [],
+ winner_main_stage_drop_ids:
+ connectedProfile?.winner_main_stage_drop_ids ?? [],
+ is_wave_creator: connectedProfile?.is_wave_creator ?? false,
+ },
+ created_at: now,
+ updated_at: null,
+ title: traits.title || "Untitled Submission",
+ parts: [
+ {
+ part_id: 1,
+ content: traits.description || null,
+ media: [media],
+ quoted_drop: null,
+ },
+ ],
+ parts_count: 1,
+ referenced_nfts: [],
+ mentioned_users: [],
+ mentioned_waves: [],
+ metadata,
+ rating: 6529,
+ realtime_rating: 6529420,
+ rating_prediction: 69420,
+ top_raters: [],
+ raters_count: 69,
+ context_profile_context: null,
+ subscribed_actions: [],
+ is_signed: false,
+ reactions: [],
+ boosts: 0,
+ hide_link_preview: false,
+ };
+};
diff --git a/components/waves/memes/submission/utils/objectEntries.ts b/components/waves/memes/submission/utils/objectEntries.ts
new file mode 100644
index 0000000000..4987321495
--- /dev/null
+++ b/components/waves/memes/submission/utils/objectEntries.ts
@@ -0,0 +1,6 @@
+type ObjectEntries = {
+ [K in keyof T]-?: [K, T[K]];
+}[keyof T][];
+
+export const objectEntries = (obj: T): ObjectEntries =>
+ Object.entries(obj) as ObjectEntries;