-
Notifications
You must be signed in to change notification settings - Fork 2
feat(studio): add Studio workspace with mass minting - Frontend #791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
exezbcz
wants to merge
15
commits into
main
Choose a base branch
from
massmint
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 10 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
c2df287
feat(studio): add Studio workspace with mass minting wizard and bulk …
exezbcz ff492cb
fix(studio): ensure full-width layout for studio pages
exezbcz 5b92525
fix: prevent navigation guard from re-triggering after confirmation
exezbcz 7c156d2
fix: watch chain changes and prevent async race conditions
exezbcz d023472
fix: improve CSV parsing and add proper escaping
exezbcz 4e1ab8b
fix: use internal chain validation and fix import order
exezbcz 0c298c6
fix: sanitize CSV filename and defer URL revocation
exezbcz ca41950
fix: sync localDescription with description prop in AdminSidebarDetails
exezbcz dedfc43
fix: add chain validation and improve query caching in useCreatorDash…
exezbcz 3fc9ff3
fix: use exported isAssetHubChain and fix regex linting
exezbcz e1531c5
fix(dashboard): reset loading state when collection IDs become empty
exezbcz 4e88d33
fix(pr-791): address all PR review comments from CodeRabbit, Copilot,…
exezbcz 19b0464
fix(pr-791): address remaining Jarsen review comments
exezbcz 54e1589
Revert "fix(pr-791): address remaining Jarsen review comments"
exezbcz 7b61ddc
fix(pr-791): re-apply Jarsen review fixes
exezbcz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,3 +28,6 @@ app/graphql/*.d.ts | |
|
|
||
| # Descriptors | ||
| app/descriptors | ||
|
|
||
| # Playwright MCP | ||
| .playwright-mcp/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <script setup lang="ts"> | ||
| import type { StepConfig } from '~/types/bulkOperations' | ||
|
|
||
| const props = defineProps<{ | ||
| steps: StepConfig[] | ||
| currentStep: number | ||
| completedSteps: number[] | ||
| maxStepReached?: number | ||
| }>() | ||
|
|
||
| const emit = defineEmits<{ | ||
| stepClick: [step: number] | ||
| }>() | ||
|
|
||
| function isCompleted(index: number) { | ||
| return props.completedSteps.includes(index) | ||
| } | ||
|
|
||
| function isCurrent(index: number) { | ||
| return props.currentStep === index | ||
| } | ||
|
|
||
| function isReachable(index: number) { | ||
| return !isCurrent(index) && !isCompleted(index) && props.maxStepReached !== undefined && index <= props.maxStepReached | ||
| } | ||
|
|
||
| function isClickable(index: number) { | ||
| return isCompleted(index) || isReachable(index) | ||
| } | ||
|
|
||
| function handleClick(index: number) { | ||
| if (isClickable(index)) { | ||
| emit('stepClick', index) | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="flex items-center gap-2"> | ||
| <template v-for="(step, index) in steps" :key="index"> | ||
| <button | ||
| class="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200" | ||
| :class="{ | ||
| 'bg-primary text-white': isCurrent(index), | ||
| 'bg-primary/10 text-primary cursor-pointer hover:bg-primary/20': isCompleted(index) && !isCurrent(index), | ||
| 'bg-muted text-foreground cursor-pointer hover:bg-muted/80': isReachable(index), | ||
| 'bg-muted text-muted-foreground': !isCurrent(index) && !isCompleted(index) && !isReachable(index), | ||
| 'cursor-default': !isClickable(index) && !isCurrent(index), | ||
| }" | ||
| :disabled="!isClickable(index) && !isCurrent(index)" | ||
| @click="handleClick(index)" | ||
| > | ||
| <UIcon | ||
| v-if="isCompleted(index) && !isCurrent(index)" | ||
| name="i-heroicons-check-20-solid" | ||
| class="w-4 h-4" | ||
| /> | ||
| <UIcon | ||
| v-else-if="step.icon" | ||
| :name="step.icon" | ||
| class="w-4 h-4" | ||
| /> | ||
| <span v-else class="w-5 h-5 rounded-full border-2 flex items-center justify-center text-xs" :class="isCurrent(index) ? 'border-white' : 'border-current'"> | ||
| {{ index + 1 }} | ||
| </span> | ||
| <span class="hidden sm:inline">{{ step.label }}</span> | ||
| </button> | ||
|
|
||
| <div | ||
| v-if="index < steps.length - 1" | ||
| class="h-px flex-1 max-w-8 transition-colors duration-200" | ||
| :class="isCompleted(index) ? 'bg-primary' : 'bg-border'" | ||
| /> | ||
| </template> | ||
| </div> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| <script setup lang="ts"> | ||
| withDefaults(defineProps<{ | ||
| backDisabled?: boolean | ||
| continueDisabled?: boolean | ||
| continueLabel?: string | ||
| loading?: boolean | ||
| showBack?: boolean | ||
| }>(), { | ||
| continueLabel: 'Continue', | ||
| showBack: true, | ||
| }) | ||
|
|
||
| const emit = defineEmits<{ | ||
| back: [] | ||
| continue: [] | ||
| }>() | ||
|
|
||
| const isMac = computed(() => { | ||
| if (import.meta.server) | ||
| return false | ||
| return navigator.platform.toUpperCase().includes('MAC') | ||
| }) | ||
|
|
||
| const shortcutHint = computed(() => isMac.value ? '⌘+Enter' : 'Ctrl+Enter') | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="shrink-0 flex items-center justify-between px-6 py-4 border-t border-border bg-background shadow-[0_-2px_10px_rgba(0,0,0,0.05)]"> | ||
| <div> | ||
| <UButton | ||
| v-if="showBack" | ||
| variant="ghost" | ||
| icon="i-heroicons-arrow-left" | ||
| :disabled="backDisabled" | ||
| @click="emit('back')" | ||
| > | ||
| Back | ||
| </UButton> | ||
| </div> | ||
|
|
||
| <div class="flex items-center gap-3"> | ||
| <slot name="cost-estimate" /> | ||
|
|
||
| <UButton | ||
| color="primary" | ||
| :loading="loading" | ||
| :disabled="continueDisabled || loading" | ||
| @click="emit('continue')" | ||
| > | ||
| {{ continueLabel }} | ||
| <template #trailing> | ||
| <kbd class="hidden sm:inline text-xs opacity-60 ml-1">{{ shortcutHint }}</kbd> | ||
| </template> | ||
| </UButton> | ||
| </div> | ||
| </div> | ||
| </template> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| <script setup lang="ts"> | ||
| import type { StepConfig } from '~/types/bulkOperations' | ||
|
|
||
| const props = withDefaults(defineProps<{ | ||
| title: string | ||
| steps: StepConfig[] | ||
| currentStep: number | ||
| completedSteps: number[] | ||
| maxStepReached?: number | ||
| canProceed?: boolean | ||
| continueLabel?: string | ||
| loading?: boolean | ||
| backLink?: string | ||
| showFooter?: boolean | ||
| compact?: boolean | ||
| }>(), { | ||
| canProceed: false, | ||
| continueLabel: 'Continue', | ||
| showFooter: true, | ||
| }) | ||
|
|
||
| const emit = defineEmits<{ | ||
| back: [] | ||
| continue: [] | ||
| stepClick: [step: number] | ||
| }>() | ||
|
|
||
| const router = useRouter() | ||
|
|
||
| function handleBackLink() { | ||
| if (props.backLink) { | ||
| router.push(props.backLink) | ||
| } | ||
| else { | ||
| router.back() | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="flex flex-col h-full"> | ||
| <div class="flex items-center gap-4 px-6 py-4 border-b border-border bg-background shrink-0"> | ||
| <template v-if="!compact"> | ||
| <UButton | ||
| variant="ghost" | ||
| icon="i-heroicons-arrow-left" | ||
| size="sm" | ||
| @click="handleBackLink" | ||
| /> | ||
|
|
||
| <h1 class="text-lg font-semibold shrink-0"> | ||
| {{ title }} | ||
| </h1> | ||
| </template> | ||
|
|
||
| <BulkStepper | ||
| :steps="steps" | ||
| :current-step="currentStep" | ||
| :completed-steps="completedSteps" | ||
| :max-step-reached="maxStepReached" | ||
| class="flex-1 justify-center" | ||
| @step-click="emit('stepClick', $event)" | ||
| /> | ||
| </div> | ||
|
|
||
| <div class="flex-1 overflow-y-auto"> | ||
| <slot /> | ||
| </div> | ||
|
|
||
| <BulkWizardFooter | ||
| v-if="showFooter" | ||
| :back-disabled="currentStep === 0" | ||
| :continue-disabled="!canProceed" | ||
| :continue-label="continueLabel" | ||
| :loading="loading" | ||
| :show-back="currentStep > 0" | ||
| @back="emit('back')" | ||
| @continue="emit('continue')" | ||
| > | ||
| <template #cost-estimate> | ||
| <slot name="cost-estimate" /> | ||
| </template> | ||
| </BulkWizardFooter> | ||
| </div> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| <script setup lang="ts"> | ||
| import type { AssetHubChain } from '~/plugins/sdk.client' | ||
|
|
||
| const props = defineProps<{ | ||
| name: string | ||
| description: string | ||
| image: string | ||
| banner: string | ||
| owner: string | ||
| supply: string | ||
| claimed: string | ||
| floor: number | null | ||
| chain: AssetHubChain | ||
| collectionId: string | ||
| readOnly?: boolean | ||
| }>() | ||
|
|
||
| const bannerUrl = computed(() => { | ||
| return props.banner ? toOriginalContentUrl(sanitizeIpfsUrl(props.banner)) : '' | ||
| }) | ||
|
|
||
| const mockNfts = computed(() => { | ||
| const names = [ | ||
| 'Nebula Drift', | ||
| 'Solar Whisper', | ||
| 'Quantum Bloom', | ||
| 'Astral Echo', | ||
| 'Cosmic Seed', | ||
| 'Void Walker', | ||
| 'Star Forge', | ||
| 'Lunar Tide', | ||
| 'Photon Veil', | ||
| 'Dark Matter', | ||
| 'Celestial Shard', | ||
| 'Plasma Wave', | ||
| ] | ||
| return names.map((name, i) => ({ | ||
| tokenId: i + 1, | ||
| collectionId: Number(props.collectionId), | ||
| chain: props.chain, | ||
| name: `${name} #${i + 1}`, | ||
| price: i % 3 === 0 ? String((1.5 + i * 0.25) * 1e10) : null, | ||
| currentOwner: props.owner, | ||
| })) | ||
| }) | ||
exezbcz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </script> | ||
|
|
||
| <template> | ||
| <div> | ||
| <!-- Banner Section --> | ||
| <div class="relative w-full min-h-[340px] flex flex-col justify-end rounded-xl overflow-hidden"> | ||
| <div | ||
| class="absolute inset-0 w-full h-full bg-muted" | ||
| :style="bannerUrl ? { | ||
| backgroundImage: `url('${bannerUrl}')`, | ||
| backgroundSize: 'cover', | ||
| backgroundPosition: 'center', | ||
| } : {}" | ||
| /> | ||
|
|
||
| <div class="relative flex items-center px-8 py-8 z-10"> | ||
| <div class="flex flex-col items-center"> | ||
| <div class="w-36 h-36 rounded-xl overflow-hidden bg-linear-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 border-4 border-white dark:border-gray-900 shadow-xl"> | ||
| <img | ||
| v-if="image" | ||
| :src="sanitizeIpfsUrl(image)" | ||
| :alt="name" | ||
| class="w-full h-full object-cover" | ||
| > | ||
| <div v-else class="w-full h-full flex items-center justify-center"> | ||
| <UIcon name="i-heroicons-photo" class="w-12 h-12 text-gray-400" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="w-full"> | ||
| <div class="flex justify-between flex-col md:flex-row gap-12"> | ||
| <div class="flex flex-col flex-1"> | ||
| <div class="my-4"> | ||
| <div class="text-2xl font-bold mb-2"> | ||
| {{ name }} | ||
| </div> | ||
| <div v-if="owner" class="flex items-center gap-1 text-muted-foreground"> | ||
| <UserInfo :avatar-size="26" :address="owner" class="min-w-0" /> | ||
| </div> | ||
| </div> | ||
| <MarkdownPreview v-if="description" :source="description" /> | ||
| </div> | ||
|
|
||
| <!-- Quick Stats --> | ||
| <div class="pt-4 w-auto md:w-60 space-y-3"> | ||
| <div class="flex justify-between items-center"> | ||
| <span class="text-sm text-gray-500 dark:text-gray-400">Minted</span> | ||
| <span class="font-medium font-mono text-gray-900 dark:text-white">{{ claimed || 0 }} / {{ Number(supply) >= Number.MAX_SAFE_INTEGER ? '∞' : (supply || 0) }}</span> | ||
| </div> | ||
|
|
||
| <div class="flex justify-between items-center"> | ||
| <span class="text-sm text-gray-500 dark:text-gray-400">Floor Price</span> | ||
| <span class="font-medium text-gray-900 dark:text-white"> | ||
| <Money v-if="floor" inline :value="floor" /> | ||
| <span v-else class="text-gray-400 dark:text-gray-500">--</span> | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <USeparator class="my-12" /> | ||
|
|
||
| <!-- Items Grid (read-only, no selection/hover) --> | ||
| <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4 md:gap-6"> | ||
| <TokenCard | ||
| v-for="nft in mockNfts" | ||
| :key="`preview-${nft.tokenId}`" | ||
| :token-id="nft.tokenId" | ||
| :collection-id="nft.collectionId" | ||
| :chain="nft.chain" | ||
| :name="nft.name" | ||
| :price="nft.price" | ||
| :current-owner="nft.currentOwner" | ||
| :hide-hover-action="readOnly" | ||
| /> | ||
| </div> | ||
| </div> | ||
| </template> | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shortcut does not work here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has not been solved yet, even with the code changes.