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
14 changes: 10 additions & 4 deletions app/components/common/card/CollectionCard.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { AssetHubChain } from '~/plugins/sdk.client'
import { fetchOdaCollection } from '~/services/oda'
import { sanitizeIpfsUrl } from '~/utils/ipfs'

type ActionVariant = 'link' | 'view-only'
type ActionVariant = 'link' | 'studio-mode'

interface Props {
item: ReturnType<typeof useInfiniteCollections>['collections']['value'][number]
Expand Down Expand Up @@ -214,16 +214,22 @@ onMounted(async () => {
</div>
</div>
</div>
<div class="px-4 pb-4">
<div class="px-4 pb-4 flex gap-3">
<UButton
:to="`/${prefix}/collection/${item.id}`"
variant="outline"
color="neutral"
block
class="border border-border"
class="flex-1 border border-border"
>
View
</UButton>
<UButton
:to="`/${prefix}/studio/${item.id}`"
color="neutral"
class="flex-1"
>
Manage
</UButton>
</div>
</div>
</template>
Expand Down
2 changes: 1 addition & 1 deletion app/components/explore/CollectionsGrid.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { AssetHubChain } from '~/plugins/sdk.client'

type CardActionVariant = 'link' | 'view-only'
type CardActionVariant = 'link' | 'studio-mode'

interface Props {
variables?: Record<string, any>
Expand Down
122 changes: 78 additions & 44 deletions app/components/massmint/MassMint.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { StepConfig } from '~/components/common/Stepper.vue'
import type { NFT, NFTToMint } from '~/components/massmint/types'
import type { TemplateFormat } from '~/components/massmint/utils'
import type { AssetHubChain } from '~/plugins/sdk.client'
import { LazyReviewMassMintModal } from '#components'
import { useQuery } from '@tanstack/vue-query'
import CommonStepper from '~/components/common/Stepper.vue'
Expand All @@ -14,6 +15,20 @@ import { blockchains } from '~/composables/create/useCollectionForm'
import { useMassMint } from '~/composables/massmint/useMassMint'
import { useMassMintForm } from '~/composables/massmint/useMassMintForm'

const props = withDefaults(
defineProps<{
collectionId?: string
blockchain?: AssetHubChain
}>(),
{ collectionId: undefined, blockchain: undefined },
)

const studioOptions = computed(() =>
props.collectionId && props.blockchain
? { collectionId: props.collectionId, blockchain: props.blockchain }
: undefined,
)

// State
const NFTS = ref<{ [nftId: string]: NFT }>({})
const mediaLoaded = ref(false)
Expand All @@ -26,7 +41,7 @@ const { getBalance } = useBalances()
const reviewMassMintModal = overlay.create(LazyReviewMassMintModal)

// Form and minting composables
const { state, collections, collectionsLoading } = useMassMintForm()
const { state, collections, collectionsLoading, isStudioContext } = useMassMintForm(studioOptions)
const { massMint, progress, isLoading: isMinting } = useMassMint()
const { itemDeposit, metadataDeposit, attributeDeposit, existentialDeposit } = useDeposit(computed(() => state.blockchain))
const selectedCollection = computed(() => state.collection)
Expand Down Expand Up @@ -78,38 +93,48 @@ const totalAttribute = computed(() => (Object.values(NFTS.value) as NFT[]).reduc
const attributeDepositTotal = computed(() => totalAttribute.value * attributeDeposit.value)

const estimatedCostOpen = ref(true)
const steps = [
{ id: 1, title: 'Collection', description: 'Choose chain and collection' },
{ id: 2, title: 'Upload', description: 'Upload media files' },
{ id: 3, title: 'Metadata', description: 'Prepare and upload metadata file' },
{ id: 4, title: 'Review', description: 'Check items and estimated cost' },
{ id: 5, title: 'Mint', description: 'Submit your mass mint transaction' },
] as const

const stepConfig: StepConfig[] = [
{ label: 'Collection', icon: 'i-heroicons-folder' },
{ label: 'Upload', icon: 'i-heroicons-arrow-up-tray' },
{ label: 'Metadata', icon: 'i-heroicons-document-text' },
{ label: 'Review', icon: 'i-heroicons-clipboard-document-list' },
{ label: 'Mint', icon: 'i-heroicons-sparkles' },
]
const steps = computed(() =>
isStudioContext.value
? [
{ id: 1, title: 'Upload', description: 'Upload media files' },
{ id: 2, title: 'Metadata', description: 'Prepare and upload metadata file' },
{ id: 3, title: 'Review', description: 'Check items and estimated cost' },
{ id: 4, title: 'Mint', description: 'Submit your mass mint transaction' },
] as const
: [
{ id: 1, title: 'Collection', description: 'Choose chain and collection' },
{ id: 2, title: 'Upload', description: 'Upload media files' },
{ id: 3, title: 'Metadata', description: 'Prepare and upload metadata file' },
{ id: 4, title: 'Review', description: 'Check items and estimated cost' },
{ id: 5, title: 'Mint', description: 'Submit your mass mint transaction' },
] as const,
)

const stepConfig = computed<StepConfig[]>(() =>
isStudioContext.value
? [
{ label: 'Upload', icon: 'i-heroicons-arrow-up-tray' },
{ label: 'Metadata', icon: 'i-heroicons-document-text' },
{ label: 'Review', icon: 'i-heroicons-clipboard-document-list' },
{ label: 'Mint', icon: 'i-heroicons-sparkles' },
]
: [
{ label: 'Collection', icon: 'i-heroicons-folder' },
{ label: 'Upload', icon: 'i-heroicons-arrow-up-tray' },
{ label: 'Metadata', icon: 'i-heroicons-document-text' },
{ label: 'Review', icon: 'i-heroicons-clipboard-document-list' },
{ label: 'Mint', icon: 'i-heroicons-sparkles' },
],
)

const currentStep = ref<number>(1)
const stepCount = computed(() => steps.value.length)

const stepperCompletedSteps = computed(() =>
Array.from({ length: Math.max(0, currentStep.value - 1) }, (_, i) => i),
)

const stepperMaxStepReached = computed(() => {
let max = 0
for (let i = 0; i < steps.length; i++) {
if (canNavigateTo(i + 1))
max = i
}
return max
})

const canGoToUploadStep = computed(() => !!selectedCollection.value)
const canGoToUploadStep = computed(() => isStudioContext.value || !!selectedCollection.value)
const canGoToMetadataStep = computed(
() => !!selectedCollection.value && mediaLoaded.value,
)
Expand All @@ -120,32 +145,46 @@ const canGoToMintStep = computed(
() => canGoToReviewStep.value && hasEnoughBalance.value,
)

const stepperMaxStepReached = computed(() => {
let max = 0
for (let i = 0; i < stepCount.value; i++) {
if (canNavigateTo(i + 1))
max = i
}
return max
})

function canNavigateTo(stepId: number) {
if (stepId <= currentStep.value) {
if (stepId < 1 || stepId > stepCount.value) {
return false
}
const logicalStepId = isStudioContext.value ? stepId + 1 : stepId
const currentLogical = isStudioContext.value ? currentStep.value + 1 : currentStep.value
if (logicalStepId <= currentLogical) {
return true
}

if (stepId === 2) {
if (logicalStepId === 2) {
return canGoToUploadStep.value
}

if (stepId === 3) {
if (logicalStepId === 3) {
return canGoToMetadataStep.value
}

if (stepId === 4) {
if (logicalStepId === 4) {
return canGoToReviewStep.value
}

if (stepId === 5) {
if (logicalStepId === 5) {
return canGoToMintStep.value
}

return false
}

function goToStep(stepId: number) {
if (stepId < 1 || stepId > steps.length) {
if (stepId < 1 || stepId > stepCount.value) {
return
}

Expand Down Expand Up @@ -368,9 +407,8 @@ function applySharedDescriptionToAll() {
/>
</section>

<!-- Step 1: Collection -->
<section
v-if="currentStep === 1"
v-if="!isStudioContext && currentStep === 1"
class="border border-border rounded-lg shadow-sm"
>
<div class="space-y-4 p-6">
Expand Down Expand Up @@ -433,7 +471,7 @@ function applySharedDescriptionToAll() {

<div class="flex items-center justify-between border-t border-border px-6 py-4">
<span class="text-sm text-muted-foreground">
Step 1 of 5 · Select a chain and collection to continue.
Step 1 of {{ stepCount }} · Select a chain and collection to continue.
</span>
<UButton
size="sm"
Expand All @@ -445,9 +483,8 @@ function applySharedDescriptionToAll() {
</div>
</section>

<!-- Step 2: Upload media -->
<MassMintUploadStep
v-else-if="currentStep === 2"
v-else-if="(isStudioContext && currentStep === 1) || (!isStudioContext && currentStep === 2)"
:selected-collection="state.collection || undefined"
:nfts="NFTS"
:media-loaded="mediaLoaded"
Expand All @@ -458,9 +495,8 @@ function applySharedDescriptionToAll() {
@next="goNextStep"
/>

<!-- Step 3: Metadata -->
<section
v-else-if="currentStep === 3"
v-else-if="(isStudioContext && currentStep === 2) || (!isStudioContext && currentStep === 3)"
class="border border-border rounded-lg shadow-sm"
>
<div class="space-y-4 p-6">
Expand Down Expand Up @@ -559,9 +595,8 @@ function applySharedDescriptionToAll() {
</div>
</section>

<!-- Step 4: Review -->
<section
v-else-if="currentStep === 4"
v-else-if="(isStudioContext && currentStep === 3) || (!isStudioContext && currentStep === 4)"
class="border border-border rounded-lg shadow-sm"
>
<div class="space-y-6 p-6">
Expand Down Expand Up @@ -708,9 +743,8 @@ function applySharedDescriptionToAll() {
</div>
</section>

<!-- Step 5: Mint -->
<section
v-else-if="currentStep === 5"
v-else-if="(isStudioContext && currentStep === 4) || (!isStudioContext && currentStep === 5)"
class="border border-border rounded-lg shadow-sm"
>
<div class="space-y-6 p-6">
Expand Down Expand Up @@ -770,7 +804,7 @@ function applySharedDescriptionToAll() {
Back
</UButton>
<span class="text-xs text-muted-foreground">
Step 5 of 5
Step {{ currentStep }} of {{ stepCount }}
</span>
</div>
</section>
Expand Down
5 changes: 1 addition & 4 deletions app/components/massmint/MassMintUploadStep.vue
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,7 @@ function onNext() {
</div>
</div>

<div class="flex items-center justify-between border-t border-border px-6 py-4">
<span class="text-sm text-muted-foreground">
Step 2 of 5 · Upload media files to continue.
</span>
<div class="flex items-center justify-end border-t border-border px-6 py-4">
<UButton
size="sm"
:disabled="!canGoToMetadataStep"
Expand Down
44 changes: 44 additions & 0 deletions app/components/studio/StudioHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
defineProps<{
studioIndexPath: string
collectionName: string
currentTab: string
}>()
</script>

<template>
<header class="shrink-0 border-b border-border bg-background">
<div class="flex items-center justify-between gap-4 px-4 py-3 md:px-6">
<nav aria-label="Breadcrumb" class="flex items-center gap-2 min-w-0">
<NuxtLink
:to="studioIndexPath"
class="flex items-center text-muted hover:text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary rounded"
aria-label="Back to collections"
>
<UIcon name="i-heroicons-arrow-left" class="size-5 shrink-0" />
</NuxtLink>
<span class="text-muted">/</span>
<NuxtLink
:to="studioIndexPath"
class="text-muted hover:text-foreground truncate transition-colors focus:outline-none focus:ring-2 focus:ring-primary rounded"
>
studio
</NuxtLink>
<span class="text-muted">/</span>
<span class="font-semibold text-foreground truncate">{{ collectionName }}</span>
<span class="text-muted">/</span>
<span class="text-foreground capitalize truncate">{{ currentTab }}</span>
</nav>
<UButton
:to="studioIndexPath"
variant="ghost"
color="neutral"
size="sm"
class="shrink-0"
aria-label="Close and return to studio"
>
Close
</UButton>
</div>
</header>
</template>
Loading