Additional Media
-
+
{displayedMedia.map((item, index) => (
About the Artist
-
+
{aboutArtist}
@@ -200,7 +236,7 @@ export const WaveDropAdditionalInfo = ({
Artwork Commentary
-
+
{commentary}
diff --git a/components/waves/memes/submission/MemesArtSubmissionContainer.tsx b/components/waves/memes/submission/MemesArtSubmissionContainer.tsx
index cdcba02d04..01b7ccba88 100644
--- a/components/waves/memes/submission/MemesArtSubmissionContainer.tsx
+++ b/components/waves/memes/submission/MemesArtSubmissionContainer.tsx
@@ -77,6 +77,13 @@ const MemesArtSubmissionContainer: FC
= ({
[form.setAdditionalMedia]
);
+ const handlePromoVideoChange = useCallback(
+ (url: string) => {
+ form.setAdditionalMedia({ promo_video: url });
+ },
+ [form.setAdditionalMedia]
+ );
+
// Phase change handler
const handlePhaseChange = useCallback((phase: SubmissionPhase) => {
// Any additional phase-specific handling can be done here
@@ -140,7 +147,7 @@ const MemesArtSubmissionContainer: FC = ({
}
: null;
- const previewRequiredMediaType = (() => {
+ const mediaTypeInfo = (() => {
const getMediaTypeLabel = (mimeType: string): string | null => {
if (mimeType.startsWith("video/")) return "Video";
if (mimeType === "text/html") return "HTML";
@@ -148,16 +155,26 @@ const MemesArtSubmissionContainer: FC = ({
return null;
};
+ const isInteractiveMedia = (mimeType: string): boolean => {
+ return mimeType === "text/html" || mimeType === "model/gltf-binary";
+ };
+
if (form.mediaSource === "upload" && form.selectedFile) {
- return getMediaTypeLabel(form.selectedFile.type);
+ const label = getMediaTypeLabel(form.selectedFile.type);
+ const isInteractive = isInteractiveMedia(form.selectedFile.type);
+ return { label, isInteractive };
}
if (form.mediaSource === "url" && form.isExternalMediaValid) {
- return getMediaTypeLabel(form.externalMediaMimeType);
+ const label = getMediaTypeLabel(form.externalMediaMimeType);
+ const isInteractive = isInteractiveMedia(form.externalMediaMimeType);
+ return { label, isInteractive };
}
- return null;
+ return { label: null, isInteractive: false };
})();
+ const previewRequiredMediaType = mediaTypeInfo.label;
const requiresPreviewImage = previewRequiredMediaType !== null;
+ const requiresPromoVideoOption = mediaTypeInfo.isInteractive;
// Map of steps to their corresponding components
const stepComponents = {
@@ -214,11 +231,14 @@ const MemesArtSubmissionContainer: FC = ({
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}
diff --git a/components/waves/memes/submission/components/AdditionalMediaUpload.tsx b/components/waves/memes/submission/components/AdditionalMediaUpload.tsx
index cdffe5dd7c..62e6b2566e 100644
--- a/components/waves/memes/submission/components/AdditionalMediaUpload.tsx
+++ b/components/waves/memes/submission/components/AdditionalMediaUpload.tsx
@@ -14,10 +14,13 @@ interface AdditionalMediaUploadProps {
readonly artworkCommentary: string;
readonly aboutArtist: string;
readonly previewImage: string;
+ readonly promoVideo: string;
readonly requiresPreviewImage: boolean;
+ readonly requiresPromoVideoOption: boolean;
readonly previewRequiredMediaType: string | null;
readonly onSupportingMediaChange: (media: string[]) => void;
readonly onPreviewImageChange: (url: string) => void;
+ readonly onPromoVideoChange: (url: string) => void;
readonly onArtworkCommentaryChange: (commentary: string) => void;
readonly onAboutArtistChange: (aboutArtist: string) => void;
readonly errors?: {
@@ -34,19 +37,24 @@ const AdditionalMediaUpload: FC = ({
artworkCommentary,
aboutArtist,
previewImage: _previewImage,
+ promoVideo: _promoVideo,
requiresPreviewImage,
+ requiresPromoVideoOption,
previewRequiredMediaType,
onSupportingMediaChange,
onPreviewImageChange,
+ onPromoVideoChange,
onArtworkCommentaryChange,
onAboutArtistChange,
errors,
}) => {
const mediaInputRef = useRef(null);
const previewInputRef = useRef(null);
+ const promoVideoInputRef = useRef(null);
const mediaUpload = useMediaUpload(MAX_FILES);
const previewUpload = useMediaUpload(1);
+ const promoVideoUpload = useMediaUpload(1);
const prevMediaUrlsRef = useRef("");
const prevPreviewUrlRef = useRef("");
@@ -61,6 +69,11 @@ const AdditionalMediaUpload: FC = ({
return urls[0] || "";
}, [previewUpload.items]);
+ const promoVideoServerUrl = useMemo(() => {
+ const urls = promoVideoUpload.getServerUrls();
+ return urls[0] || "";
+ }, [promoVideoUpload.items]);
+
useEffect(() => {
const urlsKey = mediaServerUrls.join(",");
if (urlsKey !== prevMediaUrlsRef.current) {
@@ -76,6 +89,15 @@ const AdditionalMediaUpload: FC = ({
}
}, [previewServerUrl, onPreviewImageChange]);
+ const prevPromoVideoUrlRef = useRef("");
+
+ useEffect(() => {
+ if (promoVideoServerUrl !== prevPromoVideoUrlRef.current) {
+ prevPromoVideoUrlRef.current = promoVideoServerUrl;
+ onPromoVideoChange(promoVideoServerUrl);
+ }
+ }, [promoVideoServerUrl, onPromoVideoChange]);
+
const handleFileChange = useCallback(
(upload: ReturnType) =>
(e: ChangeEvent) => {
@@ -217,7 +239,7 @@ const AdditionalMediaUpload: FC = ({
{description && (
-
{description}
+
{description}
)}
{upload.items.length > 0 && (
@@ -236,13 +258,26 @@ const AdditionalMediaUpload: FC
= ({
{requiresPreviewImage && (
{renderMediaSection(
- "Preview Image *",
+ "Preview *",
previewUpload,
previewInputRef,
1,
errors?.previewImage,
"image/*",
- `${previewRequiredMediaType} submissions require a preview image or GIF. This will be used as the NFT's image field, while your main media is served via animation_url.`
+ `${previewRequiredMediaType} submissions require a preview image or GIF. This will be displayed as the thumbnail for your artwork.`
+ )}
+
+ )}
+ {requiresPromoVideoOption && (
+
+ {renderMediaSection(
+ "Promo Video",
+ promoVideoUpload,
+ promoVideoInputRef,
+ 1,
+ undefined,
+ "video/*",
+ `For ${previewRequiredMediaType} submissions, we recommend providing a promo video for social media sharing. If no promo video is provided, the preview image will be used instead.`
)}
)}
diff --git a/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts b/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts
index 3f64359746..a1d27e618a 100644
--- a/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts
+++ b/components/waves/memes/submission/hooks/useArtworkSubmissionForm.ts
@@ -424,6 +424,7 @@ export function useArtworkSubmissionForm() {
artist_profile_media: [],
artwork_commentary_media: [],
preview_image: "",
+ promo_video: "",
},
commentary: "",
about_artist: "",
diff --git a/components/waves/memes/submission/steps/AdditionalInfoStep.tsx b/components/waves/memes/submission/steps/AdditionalInfoStep.tsx
index a94719a640..e68b3c7285 100644
--- a/components/waves/memes/submission/steps/AdditionalInfoStep.tsx
+++ b/components/waves/memes/submission/steps/AdditionalInfoStep.tsx
@@ -28,11 +28,14 @@ interface AdditionalInfoStepProps {
readonly artworkCommentary: string;
readonly aboutArtist: string;
readonly previewImage: string;
+ readonly promoVideo: string;
readonly requiresPreviewImage: boolean;
+ readonly requiresPromoVideoOption: boolean;
readonly previewRequiredMediaType: string | null;
readonly onBatchesChange: (batches: AllowlistBatchRaw[]) => void;
readonly onSupportingMediaChange: (media: string[]) => void;
readonly onPreviewImageChange: (url: string) => void;
+ readonly onPromoVideoChange: (url: string) => void;
readonly onArtworkCommentaryChange: (commentary: string) => void;
readonly onAboutArtistChange: (aboutArtist: string) => void;
readonly onBack: () => void;
@@ -50,11 +53,14 @@ const AdditionalInfoStep: FC = ({
artworkCommentary,
aboutArtist,
previewImage,
+ promoVideo,
requiresPreviewImage,
+ requiresPromoVideoOption,
previewRequiredMediaType,
onBatchesChange,
onSupportingMediaChange,
onPreviewImageChange,
+ onPromoVideoChange,
onArtworkCommentaryChange,
onAboutArtistChange,
onBack,
@@ -181,10 +187,13 @@ const AdditionalInfoStep: FC = ({
artworkCommentary={artworkCommentary}
aboutArtist={aboutArtist}
previewImage={previewImage}
+ promoVideo={promoVideo}
requiresPreviewImage={requiresPreviewImage}
+ requiresPromoVideoOption={requiresPromoVideoOption}
previewRequiredMediaType={previewRequiredMediaType}
onSupportingMediaChange={onSupportingMediaChange}
onPreviewImageChange={onPreviewImageChange}
+ onPromoVideoChange={onPromoVideoChange}
onArtworkCommentaryChange={onArtworkCommentaryChange}
onAboutArtistChange={onAboutArtistChange}
/>
diff --git a/components/waves/memes/submission/types/OperationalData.ts b/components/waves/memes/submission/types/OperationalData.ts
index e9bcbd20d5..74a1ef577f 100644
--- a/components/waves/memes/submission/types/OperationalData.ts
+++ b/components/waves/memes/submission/types/OperationalData.ts
@@ -39,6 +39,7 @@ export interface AdditionalMedia {
artist_profile_media: string[];
artwork_commentary_media: string[];
preview_image: string;
+ promo_video: string;
}
export interface OperationalData {
diff --git a/helpers/waves/drop.helpers.ts b/helpers/waves/drop.helpers.ts
index 909f9e40ae..9553669224 100644
--- a/helpers/waves/drop.helpers.ts
+++ b/helpers/waves/drop.helpers.ts
@@ -183,3 +183,22 @@ export const getDropPreviewImageUrl = (
return null;
}
};
+
+export const getDropPromoVideoUrl = (
+ metadata: ApiDropMetadata[] | undefined
+): string | null => {
+ const additionalMediaEntry = metadata?.find(
+ (m) => m.data_key === MemesSubmissionAdditionalInfoKey.ADDITIONAL_MEDIA
+ );
+ if (!additionalMediaEntry?.data_value) return null;
+
+ try {
+ const parsed = JSON.parse(additionalMediaEntry.data_value) as {
+ promo_video?: string;
+ };
+ if (!parsed?.promo_video) return null;
+ return parseIpfsUrl(parsed.promo_video);
+ } catch {
+ return null;
+ }
+};