Conversation
…operations Introduces a comprehensive Studio feature for creators to manage collections and perform bulk operations. Includes mass minting wizard with template generation, collection admin tools, dashboard components, and bulk operation capabilities for NFT management. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a bulk-operations framework and a multi-step Mass Mint wizard with composables, types, a persistent store, many new UI components/pages for studio/admin flows, selection/studio mode support for NFT grids/cards, small style/package updates, and one .gitignore entry. (≈50 words) Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UploadStep as UploadStep (UI)
participant Wizard as MassMintWizard / useMassMintWizard
participant Store as BulkOperationsStore
participant Review as ReviewStep (UI)
participant Mint as MintStep (UI)
participant API as Backend/API
User->>UploadStep: select / drop files (ZIP allowed)
UploadStep->>Wizard: addFiles(MassMintFile[])
Wizard->>Store: set uploadedFiles / update order
User->>Wizard: choose metadata path (Template or Uniform)
Wizard->>Wizard: applyUniformNames / parseTemplate -> update files
User->>Review: proceed to review
Review->>API: fetch deposit estimates
Review->>User: display cost & validation
User->>Mint: "Mint Now" (continue)
Mint->>API: prepareNftMetadata(nfts, onFileProgress?)
API->>User: request wallet signature
User->>API: sign transaction
API->>Mint: success / error
Mint->>User: show result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/composables/massmint/useMassMint.ts (1)
41-73:⚠️ Potential issue | 🟠 MajorProgress can regress due to concurrent batch completion.
progress.current = index + 1(andonFileProgress?.(index + 1, ...)) can move backward when faster items finish later-indexed tasks first. This makes UI progress jump non‑monotonically. Track completed count instead.✅ Monotonic progress counter
- async function prepareNftMetadata(nfts: NFTToMint[], onFileProgress?: (index: number, total: number) => void) { + async function prepareNftMetadata(nfts: NFTToMint[], onFileProgress?: (index: number, total: number) => void) { + let completed = 0 error.value = null progress.value = { total: nfts.length, current: 0, stage: 'uploading', message: 'Uploading media files...', } const processNft = async (nft: NFTToMint, index: number) => { const imagesCid = await pinDirectory([nft.file]).catch((err) => { errorMessage(`Error pinning media files: [${err.message}]. Please try again later.`) }) const imageUrl = `ipfs://${imagesCid}` @@ - progress.value.current = index + 1 - onFileProgress?.(index + 1, nfts.length) + completed += 1 + progress.value.current = completed + onFileProgress?.(completed, nfts.length)
🤖 Fix all issues with AI agents
In @.gitignore:
- Around line 35-41: The root-level ignore pattern '/*.md' is too broad and may
hide required docs; update the .gitignore to either scope the pattern to a
local/test folder (e.g., change '/*.md' to '/local/*.md' or 'local/*.md') or
keep the root pattern but explicitly unignore mandatory files by adding
exceptions for 'LICENSE', 'CONTRIBUTING.md', 'SECURITY.md' (in addition to the
existing '!README.md' and '!AGENTS.md'); modify the '/*.md' entry accordingly so
required root docs are tracked.
In `@app/components/collection/admin/AdminSidebarTeam.vue`:
- Around line 28-34: The template calls warningMessage but it isn't imported,
causing a runtime ReferenceError; open the <script setup> block in
AdminSidebarTeam.vue and add an import for the helper by importing
warningMessage from '~/utils/notification' (i.e., ensure import { warningMessage
} from '~/utils/notification' is present), then save and repeat the same import
fix for the other affected components (AdminItemDetail.vue,
AdminSidebarVisibility.vue, AdminSidebarDetails.vue).
In `@app/components/collection/admin/AdminSidebarVisibility.vue`:
- Line 7: Add the missing named import for warningMessage from
'~/utils/notification' in the components that call it:
AdminSidebarVisibility.vue (where warningMessage('Coming soon — visibility
settings...') is used), AdminSidebarTeam.vue (call around line 33), and
AdminItemDetail.vue (call around line 114); update each file to include "import
{ warningMessage } from '~/utils/notification'" at the top so the runtime error
is resolved.
In `@app/components/dashboard/DashboardCollectionCard.vue`:
- Around line 12-20: Add an explicit import for sanitizeIpfsUrl (used by the
computed bannerUrl and logoUrl) by importing it from the utils module (i.e., add
an import for sanitizeIpfsUrl from '~/utils/ipfs'), and update the handleView
function to avoid the malformed "?&mock=true" by calling router.push with a path
and a query object (use router.push({ path:
`/${props.collection.chain}/collection/${props.collection.id}`, query:
isMock.value ? { mock: 'true' } : {} }) so the mock param is only present when
isMock is true).
In `@app/components/massmint/wizard/steps/MetadataStep.vue`:
- Around line 124-157: The CSV parsing in parseCsvContent incorrectly splits on
commas and fails for quoted fields (e.g., "Hello, World"); replace the naive
split logic with a proper CSV parser or robust splitter: either integrate a
library like Papa (import Papa from 'papaparse' and use Papa.parse with
header:true and skipEmptyLines:true, then iterate result.data to map
filename/name/description/price to wizard.uploadedFiles.value) or implement a
safe split helper (e.g., splitCsvLine) that respects quotes and escaped quotes,
then use that helper wherever lines[i].split(',') and header.split(',') are
used; ensure errors are surfaced with errorMessage when parsing fails and keep
the existing mapping logic that finds files by filename or by index
(wizard.uploadedFiles.value).
In `@app/composables/dashboard/useCreatorDashboard.ts`:
- Around line 65-105: The watcher currently only watches collectionIds so it
won't refetch when currentChain changes and async results can arrive
out-of-order; change the watcher to watch both collectionIds and currentChain
(e.g., watch([collectionIds, currentChain], ... , { immediate: true })) and
introduce a simple in-flight token (requestId / localVersion) captured before
awaiting Promise.allSettled of ids.map(id => fetchOdaCollection(chain, id));
after results resolve, verify the token matches the latest (or that
currentChain/value hasn't changed) before assigning collections.value and
toggling loading.value to avoid overwriting newer data; reference collectionIds,
currentChain, fetchOdaCollection, collections, and loading in your changes.
In `@app/composables/massmint/useTemplateGenerator.ts`:
- Around line 4-7: The generateCsvTemplate function currently wraps fields in
quotes but doesn't escape embedded quotes/newlines or neutralize
formula-injection values; fix it by creating and using an escape helper (e.g.,
escapeCsvValue) that (1) converts any double-quote " to "" inside the value, (2)
replaces newline characters with a space or literal \n, and (3) neutralizes
values that start with =, +, -, or @ by prefixing a single quote or other safe
character before escaping; then use this helper for each field (including
f.file.name, f.name, f.description) and still wrap the escaped result in quotes
when building rows in generateCsvTemplate.
In `@app/composables/studio/useStudioCollection.ts`:
- Around line 1-27: The StudioCollectionData type uses AssetHubChain and relies
on an implicit ComputedRef import; change the chain property type from
AssetHubChain to SupportedChain and add an explicit import for ComputedRef from
'vue' at the top of the file, and import SupportedChain from the same module
that exported AssetHubChain (e.g., '~/plugins/sdk.client'). Update the
file-level imports and keep the existing identifiers (StudioCollectionData,
STUDIO_COLLECTION_KEY, provideStudioCollection, useStudioCollection) unchanged
so the type and injection usage remain consistent.
In `@app/pages/`[chain]/collection/[collection_id].vue:
- Around line 2-8: The page imports CHAINS from `@kodadot1/static` and uses it in
the definePageMeta validate function; replace that external dependency by
importing isSupportedChain (or chainSpec if you need specs) from ~/utils/chain
and update the validate callback to call isSupportedChain(chain) instead of
checking chain in CHAINS (keep the existing route.params destructuring and
return boolean). Ensure the import statement is changed and any references to
CHAINS are removed so validate uses isSupportedChain (or chainSpec) and the file
complies with internal chain registry/types.
In `@app/pages/`[chain]/studio/[collection_id]/details.vue:
- Around line 24-42: The guard in onBeforeRouteLeave always blocks because
isDirty remains true when confirmLeave calls router.push; add a short-circuit
flag (e.g., allowNavigation) that the guard checks first and resets after use,
or clear isDirty before calling router.push in confirmLeave; specifically,
update onBeforeRouteLeave to allow navigation if allowNavigation is true, and in
confirmLeave set allowNavigation = true (and clear pendingRoute) before calling
router.push so the subsequent navigation is not re-blocked, then reset
allowNavigation after navigation.
🟡 Minor comments (26)
app/assets/css/main.css-24-24 (1)
24-24:⚠️ Potential issue | 🟡 MinorQuote
MenloandConsolasto fix stylelint errors and match existing patterns.Stylelint's
value-keyword-caserule flags these unquoted capitalized font names. Other proper font names in this file are quoted (e.g.,'Cascadia Code','Source Code Pro'), so quoting these single-word font names maintains consistency.Proposed fix
- --font-mono: 'Geist Mono', ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; + --font-mono: 'Geist Mono', ui-monospace, 'Cascadia Code', 'Source Code Pro', 'Menlo', 'Consolas', 'DejaVu Sans Mono', monospace;app/components/collection/admin/AdminSidebarVisibility.vue-6-8 (1)
6-8:⚠️ Potential issue | 🟡 MinorMisleading UX: switches toggle visually but feature is not implemented.
The switches update
isPublished/isNsfwrefs, showing a visual state change, but thenhandleToggledisplays a "coming soon" warning. This can confuse users who see the toggle change and then receive a message that it doesn't work.Consider either:
- Disabling the switches with a tooltip explaining the feature is coming soon
- Preventing the state change by not binding
v-modeluntil the feature is ready♻️ Option 1: Disable switches with explanatory state
- <USwitch v-model="isPublished" size="sm" `@update`:model-value="handleToggle" /> + <USwitch :model-value="isPublished" size="sm" disabled /> + <span class="text-xs text-muted-foreground">(Coming soon)</span>Also applies to: 27-27, 32-32
app/pages/[chain]/collection/[collection_id]/massmint.client.vue-8-12 (1)
8-12:⚠️ Potential issue | 🟡 MinorRoute params may be arrays—ensure string handling.
route.params.chainandroute.params.collection_idare typed asstring | string[]by Vue Router. If these ever resolve to arrays (e.g., from misconfigured routes or edge cases), the template literal would produce malformed URLs like/foo,bar/studio/....Consider using
useRoutewith explicit typing or extracting the first element:🛡️ Suggested defensive handling
-const { chain: chainPrefix, collection_id } = route.params +const chainPrefix = Array.isArray(route.params.chain) ? route.params.chain[0] : route.params.chain +const collection_id = Array.isArray(route.params.collection_id) ? route.params.collection_id[0] : route.params.collection_idapp/composables/studio/useStudioDetails.ts-1-49 (1)
1-49:⚠️ Potential issue | 🟡 MinorLocal state won’t update if collection loads/changes later.
description,royaltyRecipient, andcollaboratorsare initialized once fromcollection.value. If the computed updates after async fetch, the UI stays stale. Consider syncing when collection changes (ideally skipping when dirty).🔄 Suggested sync
export function useStudioDetails(collection: ComputedRef<{ name: string, description: string, image: string, banner: string, owner: string }>) { const description = ref(collection.value.description) const logoFile = ref<File | null>(null) const bannerFile = ref<File | null>(null) const royaltyPercentage = ref(5) const royaltyRecipient = ref(collection.value.owner) const isPublished = ref(true) @@ const isDirty = computed(() => { return description.value !== collection.value.description || logoFile.value !== null || bannerFile.value !== null }) + + watch(collection, (next) => { + if (isDirty.value) + return + description.value = next.description + royaltyRecipient.value = next.owner + collaborators.value = [{ address: next.owner, role: 'Owner' }] + }, { immediate: true })app/components/collection/admin/AdminSidebarDetails.vue-1-50 (1)
1-50:⚠️ Potential issue | 🟡 MinorDescription editor doesn’t show current value.
localDescriptionis initialized to'', so the textarea won’t show an existing description (placeholder isn’t a value). Seed it from props and keep it in sync when props change.🛠️ Suggested initialization
-defineProps<{ +const props = defineProps<{ name?: string description?: string image?: string banner?: string }>() -const localDescription = ref('') +const localDescription = ref(props.description ?? '') +watch(() => props.description, (value) => { + localDescription.value = value ?? '' +})app/components/collection/admin/AdminSidebarActions.vue-2-8 (1)
2-8:⚠️ Potential issue | 🟡 MinorUse
SupportedChainfor public chain identifiers.
chainis a public prop and should useSupportedChainper repo guidelines.
As per coding guidelines: UseSupportedChaintype from~/plugins/sdk.clientfor chain identifiers.♻️ Suggested change
-import type { AssetHubChain } from '~/plugins/sdk.client' +import type { SupportedChain } from '~/plugins/sdk.client' ... const props = defineProps<{ collectionId: string collectionName?: string - chain: AssetHubChain + chain: SupportedChain }>()app/composables/dashboard/useCreatorDashboard.ts-5-11 (1)
5-11:⚠️ Potential issue | 🟡 MinorUse
SupportedChainfor public chain identifiers.
DashboardCollection.chainis a public type and should useSupportedChainper repo guidelines; keepAssetHubChainonly where the API requires it (e.g.,fetchOdaCollection).
As per coding guidelines: UseSupportedChaintype from~/plugins/sdk.clientfor chain identifiers.♻️ Suggested type adjustment
-import type { AssetHubChain } from '~/plugins/sdk.client' +import type { AssetHubChain, SupportedChain } from '~/plugins/sdk.client' ... export interface DashboardCollection { id: string - chain: AssetHubChain + chain: SupportedChain metadata?: OnchainCollection['metadata'] supply?: string claimed?: string floor: number | null }app/composables/studio/useStudioCollection.ts-4-14 (1)
4-14:⚠️ Potential issue | 🟡 MinorUse
SupportedChainfor public chain identifiers.
StudioCollectionData.chainshould useSupportedChainper repo guidelines; keepAssetHubChainonly where an API explicitly requires it.
As per coding guidelines: UseSupportedChaintype from~/plugins/sdk.clientfor chain identifiers.♻️ Suggested change
-import type { AssetHubChain } from '~/plugins/sdk.client' +import type { SupportedChain } from '~/plugins/sdk.client' ... export interface StudioCollectionData { id: string - chain: AssetHubChain + chain: SupportedChain name: string description: string image: string banner: string owner: stringapp/pages/[chain]/studio/[collection_id]/transfer.client.vue-4-9 (1)
4-9:⚠️ Potential issue | 🟡 MinorUse
useChainfor chain handling instead ofroute.params.chain.
This keeps chain normalization/typing consistent across the app when building backlinks.As per coding guidelines: Use `useChain` composable (route-based) as the primary method for chain handling, with `usePrefix` only as a fallback.🔧 Suggested update
const route = useRoute() +const chain = useChain() const collection = useStudioCollection() const backLink = computed(() => - `/${route.params.chain}/studio/${route.params.collection_id}`, + `/${chain.value}/studio/${route.params.collection_id}`, )app/pages/[chain]/studio/[collection_id]/airdrop.client.vue-4-9 (1)
4-9:⚠️ Potential issue | 🟡 MinorUse
useChainfor chain handling instead ofroute.params.chain.
This keeps chain normalization/typing consistent across the app when building backlinks.As per coding guidelines: Use `useChain` composable (route-based) as the primary method for chain handling, with `usePrefix` only as a fallback.🔧 Suggested update
const route = useRoute() +const chain = useChain() const collection = useStudioCollection() const backLink = computed(() => - `/${route.params.chain}/studio/${route.params.collection_id}`, + `/${chain.value}/studio/${route.params.collection_id}`, )app/stores/bulkOperations.ts-86-92 (1)
86-92:⚠️ Potential issue | 🟡 Minor
reset()does not resetmaxStepReached, causing stale navigation state.When resetting the store,
maxStepReachedremains at its previous value. This means if a user completes 3 steps in one session, resets, and starts a new operation, they could navigate to step 3 immediately without completing prior steps.🐛 Proposed fix
function reset() { operationType.value = BulkOperationType.MASS_MINT currentStep.value = 0 + maxStepReached.value = 0 collectionId.value = '' isActive.value = false clearItems() }app/components/collection/admin/AdminSidebarIdentity.vue-17-22 (1)
17-22:⚠️ Potential issue | 🟡 MinorAdd missing import for
sanitizeIpfsUrl.The function is used on line 19 but not imported. Add to the script setup:
import { sanitizeIpfsUrl } from '~/utils/ipfs'app/pages/[chain]/studio/index.client.vue-19-19 (1)
19-19:⚠️ Potential issue | 🟡 MinorMock mode won't update reactively if query changes.
Passing
isMock.valueextracts the primitive boolean at call time. If the user navigates and the?mock=truequery changes without a full page reload, the composable won't receive the updated value.Consider passing a getter or the computed ref if
useCreatorDashboardsupports reactive options, or document that this page requires a full reload for mock mode changes.app/pages/[chain]/studio/[collection_id]/index.vue-62-62 (1)
62-62:⚠️ Potential issue | 🟡 Minor
hasItemsalways returnstruefor non-mock mode.The current logic assumes real collections always have items, which may cause the empty state to never display for actual empty collections. Consider wiring this to real item count data when available.
app/pages/[chain]/studio/[collection_id]/index.vue-51-51 (1)
51-51:⚠️ Potential issue | 🟡 MinorGuard against undefined collection owner.
collection.value.owneris accessed directly, but if the collection data hasn't loaded or is missing, this could beundefined. Consider adding a fallback.🛡️ Proposed fix
- currentOwner: collection.value.owner, + currentOwner: collection.value?.owner ?? '',app/pages/[chain]/studio/[collection_id].vue-4-4 (1)
4-4:⚠️ Potential issue | 🟡 MinorAvoid using
@kodadot1/staticpackage.Same issue as in
index.client.vue— the coding guidelines explicitly prohibit using@kodadot1/static. Use internal chain validation instead.As per coding guidelines: "Do NOT use
@kodadot1/staticpackage; use internal implementations instead."app/pages/[chain]/studio/index.client.vue-2-2 (1)
2-2:⚠️ Potential issue | 🟡 MinorAvoid using
@kodadot1/staticpackage.The coding guidelines explicitly state: "Do NOT use
@kodadot1/staticpackage; use internal implementations instead (useChain,SupportedChain,chainSpec)". Consider using an internal chain validation approach or theuseChaincomposable.As per coding guidelines: "Do NOT use
@kodadot1/staticpackage; use internal implementations instead."app/components/massmint/wizard/FilePreviewModal.vue-47-62 (1)
47-62:⚠️ Potential issue | 🟡 MinorNavigation button visibility may not match actual navigation behavior.
The prev/next button visibility uses
file.order(lines 48, 57), butnavigatePreviewuses the actual array index viafindIndex. If thefilesarray is not sorted by theorderproperty, users may see nav buttons that don't navigate as expected, or buttons may be hidden when navigation is actually possible.Consider using the actual array index for consistency:
🐛 Proposed fix using array index
+const currentIndex = computed(() => { + if (!props.file) return -1 + return props.files.findIndex(f => f.id === props.file!.id) +}) function navigatePreview(direction: 1 | -1) { if (!props.file) return - const currentIndex = props.files.findIndex(f => f.id === props.file!.id) - const nextIndex = currentIndex + direction + const nextIndex = currentIndex.value + direction if (nextIndex >= 0 && nextIndex < props.files.length) { emit('update:file', props.files[nextIndex]!) } }Then in the template:
<!-- Nav: Previous --> <button - v-if="file.order > 0" + v-if="currentIndex > 0" ... > <!-- Nav: Next --> <button - v-if="file.order < files.length - 1" + v-if="currentIndex < files.length - 1" ... >app/components/massmint/wizard/steps/MintStep.vue-2-14 (1)
2-14:⚠️ Potential issue | 🟡 MinorUse SupportedChain for the
chainprop.✅ Suggested update
-import type { AssetHubChain } from '~/plugins/sdk.client' +import type { SupportedChain } from '~/plugins/sdk.client' @@ const props = defineProps<{ wizard: ReturnType<typeof useMassMintWizard> collectionId: string collectionName: string - chain: AssetHubChain + chain: SupportedChain returnRoute?: string }>()As per coding guidelines: Use SupportedChain type from
~/plugins/sdk.clientfor chain identifiers.app/components/studio/ItemSlideOver.vue-2-8 (1)
2-8:⚠️ Potential issue | 🟡 MinorUse SupportedChain for chain identifiers.
This keeps chain typing consistent across the app.✅ Suggested update
-import type { AssetHubChain } from '~/plugins/sdk.client' +import type { SupportedChain } from '~/plugins/sdk.client' const props = defineProps<{ itemId: string | null - chain: AssetHubChain + chain: SupportedChain collectionId: string }>()As per coding guidelines: Use SupportedChain type from
~/plugins/sdk.clientfor chain identifiers.app/components/massmint/wizard/MassMintWizard.vue-2-15 (1)
2-15:⚠️ Potential issue | 🟡 MinorSwap AssetHubChain for SupportedChain.
✅ Suggested update
-import type { AssetHubChain } from '~/plugins/sdk.client' +import type { SupportedChain } from '~/plugins/sdk.client' @@ const props = defineProps<{ collectionId: string - chain: AssetHubChain + chain: SupportedChain collectionName: string existingItemCount: number returnRoute?: string compact?: boolean }>()As per coding guidelines: Use SupportedChain type from
~/plugins/sdk.clientfor chain identifiers.app/types/bulkOperations.ts-1-37 (1)
1-37:⚠️ Potential issue | 🟡 MinorType
chainas SupportedChain for consistency.✅ Suggested update
+import type { SupportedChain } from '~/plugins/sdk.client' + export interface BulkOperationItem { id: string - chain: string + chain: SupportedChain tokenId?: number collectionId?: string name?: string image?: string }As per coding guidelines: Use SupportedChain type from
~/plugins/sdk.clientfor chain identifiers.app/components/studio/StudioSidebar.vue-175-182 (1)
175-182:⚠️ Potential issue | 🟡 MinorDelete Collection action is a no-op.
The button currently does nothing, which is confusing UX. Please either disable/hide it until implemented or wire a real delete flow.🛠️ Minimal stopgap (disable until implemented)
- <button + <button class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-red-500 hover:bg-red-500/10 transition-colors" :class="isCollapsed ? 'justify-center' : ''" - `@click`="handleDeleteCollection" + disabled + aria-disabled="true" + title="Delete collection is not implemented yet" >Do you want me to draft a confirmed delete flow + modal and open a tracking issue?
app/components/massmint/wizard/steps/ReviewStep.vue-2-10 (1)
2-10:⚠️ Potential issue | 🟡 MinorUse SupportedChain for the
chainprop.✅ Suggested update
-import type { AssetHubChain } from '~/plugins/sdk.client' +import type { SupportedChain } from '~/plugins/sdk.client' @@ const props = defineProps<{ wizard: ReturnType<typeof useMassMintWizard> collectionId: string - chain: AssetHubChain + chain: SupportedChain }>()As per coding guidelines: Use SupportedChain type from
~/plugins/sdk.clientfor chain identifiers.app/components/massmint/wizard/steps/MintStep.vue-78-81 (1)
78-81:⚠️ Potential issue | 🟡 MinorAvoid explicit
anyin error handling. Useunknownand narrow the error shape to keep type safety.🛠️ Suggested update
- catch (err: any) { - mintingState.value = MintingState.ERROR - errorMsg.value = err.message || 'An unexpected error occurred' - } + catch (err: unknown) { + mintingState.value = MintingState.ERROR + const message = err instanceof Error + ? err.message + : typeof err === 'string' + ? err + : 'An unexpected error occurred' + errorMsg.value = message + }The catch block at lines 78-81 uses explicit
anytype, bypassing TypeScript's type checking. Usingunknownwith proper type narrowing (as shown above) prevents accidental runtime errors and maintains type safety per the coding guidelines.app/pages/[chain]/collection/[collection_id]/index.vue-202-208 (1)
202-208:⚠️ Potential issue | 🟡 MinorUse
useSeoMetawith reactive values or deferdefineOgImageComponentuntil data loads.
defineOgImageComponentexpects static serializable values at call time and doesn't track reactive refs. Since the code usesuseLazyAsyncData(which doesn't block),collectionData.valuewill beundefinedwhendefineOgImageComponentis called in the script setup, resulting in undefined OG image metadata for crawlers.Either extract the values conditionally once data is available (e.g., in a watcher), or use
useSeoMetawith computed properties instead, which properly handles reactivity for meta tags.
🧹 Nitpick comments (22)
app/pages/[chain]/collection/[collection_id]/airdrop.client.vue (1)
6-12: UseuseChaincomposable for chain handling.The coding guidelines specify using
useChaincomposable as the primary method for chain handling. Direct access toroute.params.chainshould be avoided.Additionally, route params are typed as
string | string[]; consider narrowing tostringfor type safety.♻️ Suggested refactor
const route = useRoute() const router = useRouter() -const { chain: chainPrefix, collection_id } = route.params +const { urlPrefix } = useChain() +const collectionId = String(route.params.collection_id) onMounted(() => { const mockQuery = route.query.mock === 'true' ? '?mock=true' : '' - router.replace(`/${chainPrefix}/studio/${collection_id}/airdrop${mockQuery}`) + router.replace(`/${urlPrefix}/studio/${collectionId}/airdrop${mockQuery}`) })As per coding guidelines: "Use
useChaincomposable (route-based) as the primary method for chain handling, withusePrefixonly as a fallback."app/components/collection/admin/FloatingManageButton.vue (1)
16-23:v-show="true"is redundant and prevents transition animations.The
v-show="true"directive always evaluates to true, making it effectively a no-op. More importantly, the<Transition>wrapper's enter/leave animations will never trigger since the visibility never changes.Consider either:
- Accepting a
showprop to control visibility from the parent- Removing the
v-showand<Transition>if the button should always be visible♻️ Option 1: Add a show prop for conditional visibility
<script setup lang="ts"> +defineProps<{ + show?: boolean +}>() + defineEmits<{ click: [] }>() </script> <template> <Transition ...> <button - v-show="true" + v-show="show" class="..."app/pages/[chain]/collection/[collection_id]/transfer.client.vue (1)
1-18: Consider extracting a shared redirect composable.This file, along with
massmint.client.vueandlist.client.vue, follows an identical pattern. Consider extracting a reusable composable or utility to reduce duplication:♻️ Suggested shared composable
// app/composables/useStudioRedirect.ts export function useStudioRedirect(subPath: string) { const route = useRoute() const router = useRouter() const chainPrefix = Array.isArray(route.params.chain) ? route.params.chain[0] : route.params.chain const collectionId = Array.isArray(route.params.collection_id) ? route.params.collection_id[0] : route.params.collection_id onMounted(() => { const mockQuery = route.query.mock === 'true' ? '?mock=true' : '' router.replace(`/${chainPrefix}/studio/${collectionId}/${subPath}${mockQuery}`) }) }Then each redirect page becomes:
<script setup lang="ts"> definePageMeta({ layout: 'no-footer' }) useStudioRedirect('transfer') </script> <template><div /></template>app/utils/fileFormatting.ts (1)
1-7: Consider adding GB support and edge case handling.The function handles B, KB, and MB but not GB. For very large files (videos, 3D models), this could display awkward values like "2048.0 MB" instead of "2.0 GB". Also, negative byte values would produce unexpected output.
♻️ Suggested improvement
export function formatFileSize(bytes: number): string { + if (bytes < 0) return '0 B' if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` - return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB` }app/composables/collection/useAdminSidebar.ts (1)
1-58: KeepisOpenin sync with the route query (if URL-driven).Right now it’s only initialized from
route.query.admin, so back/forward or programmatic query changes won’t reflect in the sidebar state. If the query param is meant to control visibility, add a watcher to keep them aligned.🔧 Suggested sync
export function useAdminSidebar() { const route = useRoute() const isOpen = ref(route.query.admin === 'true') + watch(() => route.query.admin, (value) => { + isOpen.value = value === 'true' + })app/components/collection/admin/AdminItemDetail.vue (1)
1-120: Make the editor reflect actual item data (avoid hardcoded/blank state).The component always shows
"Item name"and initializes description/traits empty, so it can’t represent existing items. Consider accepting item data as props and initializing local state from them.🧩 Suggested props + init
<script setup lang="ts"> -const emit = defineEmits<{ +const emit = defineEmits<{ back: [] }>() -const localDescription = ref('') -const localTraits = ref<Array<{ trait_type: string, value: string }>>([]) +const props = defineProps<{ + name: string + description?: string + attributes?: Array<{ trait_type: string, value: string }> +}>() + +const localDescription = ref(props.description ?? '') +const localTraits = ref<Array<{ trait_type: string, value: string }>>( + (props.attributes ?? []).map(a => ({ ...a })), +)- model-value="Item name" + :model-value="props.name"app/composables/studio/useStudioKeyboard.ts (1)
35-43: Avoid intercepting Cmd/Ctrl+A in editable/selectable elements.Contenteditable and
<select>are currently not excluded. Consider broadening the guard to avoid hijacking selection in editors.🔧 Guard expansion
if (isMeta && e.key === 'a') { // Only intercept if not in an input/textarea - const tag = (e.target as HTMLElement)?.tagName - if (tag === 'INPUT' || tag === 'TEXTAREA') + const target = e.target as HTMLElement | null + const tag = target?.tagName + if (target?.isContentEditable || tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return e.preventDefault() options?.onSelectAll?.() }app/components/dashboard/DashboardCollectionCard.vue (1)
22-29: Avoid manual query-string concatenation.Line 23–25 builds
?${mockQuery}which yields a trailing?when not in mock mode and?&mock=truewhen in mock mode. Preferrouter.push({ path, query })for clean URLs and consistency withhandleManage.♻️ Suggested change
function handleView() { - const mockQuery = isMock.value ? '&mock=true' : '' - router.push(`/${props.collection.chain}/collection/${props.collection.id}?${mockQuery}`) + router.push({ + path: `/${props.collection.chain}/collection/${props.collection.id}`, + query: isMock.value ? { mock: 'true' } : {}, + }) }app/pages/[chain]/studio/[collection_id]/list.client.vue (1)
4-9: UseuseChainand preserve mock query inbackLink.Line 7 builds the link from
route.params.chaindirectly. PreferuseChain()for chain handling and keepmock=truewhen present so the mode doesn’t drop on back navigation.
As per coding guidelines: UseuseChaincomposable (route-based) as the primary method for chain handling, withusePrefixonly as a fallback.♻️ Suggested change
-const route = useRoute() +const route = useRoute() +const { currentChain } = useChain() +const isMock = computed(() => route.query.mock === 'true') const collection = useStudioCollection() const backLink = computed(() => - `/${route.params.chain}/studio/${route.params.collection_id}`, + ({ + path: `/${currentChain.value}/studio/${route.params.collection_id}`, + query: isMock.value ? { mock: 'true' } : {}, + }), )app/components/collection/admin/AdminSidebarActions.vue (1)
29-31: TODO left in action handler.Line 30 leaves
handleDestroyCollectionunimplemented. If you want, I can sketch theuseOverlaywiring and modal flow.app/components/bulkOperations/BulkWizardFooter.vue (1)
18-22: Consider usingnavigator.userAgentDatafor platform detection.
navigator.platformis deprecated. While it still works in browsers, consider usingnavigator.userAgentData?.platformwith a fallback for better future compatibility.♻️ Suggested improvement
const isMac = computed(() => { if (import.meta.server) return false - return navigator.platform.toUpperCase().includes('MAC') + const platform = navigator.userAgentData?.platform || navigator.platform || '' + return platform.toUpperCase().includes('MAC') })app/pages/[chain]/studio/index.client.vue (1)
14-14: Remove unused variable.
_prefixis declared but never used in this component.🧹 Remove unused variable
const { isLogIn } = useAuth() -const { prefix: _prefix } = usePrefix() const route = useRoute()app/pages/[chain]/studio/[collection_id]/index.vue (1)
28-44: Consider extracting shared mock NFT names.The
namesarray is duplicated inCollectionDisplay.vue. Consider extracting this to a shared constant or utility to reduce duplication and ensure consistency.app/components/collection/admin/AdminSidebarEarnings.vue (1)
12-21: Add accessibility attributes for expandable section.The collapsible button lacks
aria-expandedandaria-controlsattributes for screen readers.♿ Proposed accessibility improvement
<button class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors" + :aria-expanded="isExpanded" + aria-controls="earnings-content" `@click`="isExpanded = !isExpanded" > <span>Earnings</span>And on the content div:
- <div v-if="isExpanded" class="px-4 pb-4 space-y-3"> + <div v-if="isExpanded" id="earnings-content" class="px-4 pb-4 space-y-3">app/components/common/card/TokenCard.client.vue (2)
108-108: Simplify click handler for readability.The nested ternary is difficult to parse. Consider extracting to a method.
♻️ Proposed refactor
Add a method in the script section:
function handleCardClick() { if (selectionMode) { emit('select', tokenId, collectionId) } else if (studioMode) { emit('itemClick', tokenId, collectionId) } }Then in the template:
- `@click`="selectionMode ? emit('select', tokenId, collectionId) : (studioMode ? emit('itemClick', tokenId, collectionId) : undefined)" + `@click`="handleCardClick"
133-133: Applypointer-events-nonefor both selection and studio modes.Currently
pointer-events-noneis only applied whenselectionModeis active, butstudioModealso disables navigation (setstotoundefined). For consistency, disable pointer events in both modes.♻️ Proposed fix
- <NuxtLink :to="selectionMode || studioMode ? undefined : `/${chain}/gallery/${collectionId}-${tokenId}`" class="block" :class="{ 'pointer-events-none': selectionMode }"> + <NuxtLink :to="selectionMode || studioMode ? undefined : `/${chain}/gallery/${collectionId}-${tokenId}`" class="block" :class="{ 'pointer-events-none': selectionMode || studioMode }">app/pages/[chain]/studio/[collection_id].vue (1)
17-21: Clarify variable naming for chain references.Destructuring
chainaschainPrefixthen creating a computedchainis slightly confusing. Consider renaming for clarity.🧹 Proposed clarification
const route = useRoute() -const { chain: chainPrefix, collection_id } = route.params +const { chain: chainParam, collection_id } = route.params -const chain = computed(() => chainPrefix as AssetHubChain) +const chain = computed(() => chainParam as AssetHubChain) const collectionId = computed(() => collection_id?.toString() ?? '')Or alternatively, use
chainPrefixconsistently throughout the file if that's the intent.app/components/massmint/wizard/FilePreviewModal.vue (1)
66-71: Consider adding loading/error state for image preview.The image tag uses
file.thumbnailUrldirectly without a loading state or error fallback. For large images or slow connections, users may see a blank space.app/components/massmint/wizard/steps/UploadStep.vue (2)
168-175: Consider usingv-modelinstead of:model-valuefor proper two-way binding with vuedraggable.The
draggablecomponent receives:model-value(one-way binding) but doesn't receive@update:model-valueto sync changes. WhilehandleDragEndupdates theorderproperty on each item, the array order itself won't be updated by vuedraggable. This means the visual order after drag may not match the actual array order.♻️ Proposed fix
<draggable v-if="viewMode === 'grid'" - :model-value="wizard.uploadedFiles.value" + v-model="wizard.uploadedFiles.value" item-key="id" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-3" ghost-class="opacity-30" `@end`="handleDragEnd" >
66-68: Enhance ZIP extraction error handling with more context.The generic "Failed to extract ZIP file" message doesn't help users understand what went wrong. Consider including the filename or a hint about potential issues (corrupted file, unsupported compression).
♻️ Proposed enhancement
catch { - errorMessage('Failed to extract ZIP file') + errorMessage(`Failed to extract "${zip.name}" — file may be corrupted or use unsupported compression`) }app/pages/[chain]/collection/[collection_id]/index.vue (1)
105-105: Consider defining a proper type for search filters instead of usingany.The
Record<string, any>[]type loses type safety. Consider defining a union type or interface for the expected filter shapes.♻️ Proposed improvement
// In a types file or at the top of this file type SearchFilter = | { id_in: string[] } | Record<string, unknown> // For other filter shapes from buildNftSearchFilters const searchFilters: SearchFilter[] = []As per coding guidelines: "Use strict typing and avoid
anyin TypeScript code."app/components/massmint/wizard/steps/MetadataStep.vue (1)
164-164: Avoidanytype in JSON parsing callback.The
entry: anyloses type safety. Consider defining an interface for the expected template entry structure.♻️ Proposed improvement
+interface TemplateEntry { + name?: string + description?: string + price?: number | string +} function parseJsonContent(content: string) { try { const data = JSON.parse(content) const entries = Array.isArray(data) ? data : Object.values(data) - entries.forEach((entry: any, index: number) => { + entries.forEach((entry: TemplateEntry, index: number) => { const file = wizard.uploadedFiles.value[index] if (file && entry) { if (entry.name) file.name = entry.name if (entry.description) file.description = entry.description if (entry.price !== undefined && entry.price !== '' && Number(entry.price) >= 0) file.price = Number(entry.price) } }) }As per coding guidelines: "Use strict typing and avoid
anyin TypeScript code."
|
cc @Jarsen136 can you please pick it up ? |
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@app/components/collection/admin/AdminSidebarDetails.vue`:
- Around line 5-13: The textarea-backed local state localDescription is never
seeded from the incoming description prop: change the defineProps call to
capture props (e.g. const props = defineProps<{ name?: string; description?:
string; image?: string; banner?: string }>()), initialize localDescription.value
= props.description ?? '' and add a watch on () => props.description to update
localDescription.value when the prop changes; update any existing logic that
previously referenced description directly to use props.description where
needed.
In `@app/components/collection/admin/AdminSidebarVisibility.vue`:
- Around line 29-34: The toggles (USwitch bound to isPublished and isNsfw)
update local state but handleToggle only shows a "coming soon" warning and does
not persist or revert, which is misleading; fix by either disabling the switches
until the feature is implemented (add a disabled prop to the USwitch instances
bound to a feature flag) or by reverting the v-model change inside handleToggle
(accept the new value/event, show the warning, then programmatically reset
isPublished/isNsfw back to their previous boolean), and update the template
bindings (USwitch v-model and `@update`:model-value="handleToggle") to match the
chosen approach so the UI accurately reflects that the setting was not saved.
In `@app/composables/dashboard/useCreatorDashboard.ts`:
- Around line 86-93: The code casts `chain` to `AssetHubChain` and calls
`fetchOdaCollection` for all ids, which can call the wrong API for non-AssetHub
chains; add a guard that validates `chain` is an AssetHubChain (e.g., using an
`isAssetHubChain(chain)` type guard or checking `chain` is one of
'ahp'|'ahk'|'ahpas') before running the Promise.allSettled map, and if it is not
an AssetHubChain either return early (no fetch) or skip fetching (empty ids),
keeping the existing checks for `isCancelled` and `requestId` intact; update
references in this block (the `chain` variable, the Promise.allSettled call,
`fetchOdaCollection`, and the `ids.map` callback) so calls to
`fetchOdaCollection` only happen when the chain is confirmed to be an
AssetHubChain.
- Around line 65-75: The useOwnedCollections query key is missing the chain,
causing stale cached data on chain switches; update the call to
useOwnedCollections so its key includes the current chain (e.g. pass computed(()
=> [ 'ownedCollections', accountId.value || '', currentChain.value || '' ]) or
otherwise ensure the reactive currentChain is part of the query key) so the
query invalidates/refetches when currentChain changes; locate the
useOwnedCollections invocation in useCreatorDashboard and change the key to
include currentChain (reference symbols: useOwnedCollections, computed(() =>
accountId.value || ''), currentChain).
In `@app/composables/massmint/useTemplateGenerator.ts`:
- Around line 38-46: downloadCsvTemplate currently uses raw collectionName for
link.download and calls URL.revokeObjectURL immediately; sanitize collectionName
to remove/replace filesystem-invalid characters (e.g., replace / \ ? % * : | " <
> and control chars, fallback to 'massmint' when empty) before building the
filename and use generateCsvTemplate(files) unchanged; to avoid canceled
downloads defer revocation of the object URL (use a small timeout like
setTimeout(() => URL.revokeObjectURL(url), 500) or remove the URL after the
anchor's click completes) and ensure the anchor is appended to the DOM and
removed after clicking so browsers reliably start the download.
🧹 Nitpick comments (2)
app/components/collection/admin/AdminItemDetail.vue (1)
9-21: Use stable IDs instead of index keys in the traits listKeying by index and removing by index can cause input state to “jump” after deletions. Consider adding a stable
idper trait and key by that.♻️ Suggested update for stable keys
-const localTraits = ref<Array<{ trait_type: string, value: string }>>([]) +const localTraits = ref<Array<{ id: string; trait_type: string; value: string }>>([]) function addTrait() { - localTraits.value.push({ trait_type: '', value: '' }) + localTraits.value.push({ + id: `${Date.now()}-${Math.random()}`, + trait_type: '', + value: '', + }) } -function removeTrait(index: number) { - localTraits.value.splice(index, 1) +function removeTrait(id: string) { + const index = localTraits.value.findIndex(trait => trait.id === id) + if (index >= 0) localTraits.value.splice(index, 1) }-<div - v-for="(trait, index) in localTraits" - :key="index" +<div + v-for="trait in localTraits" + :key="trait.id" class="flex items-center gap-2" > @@ - `@click`="removeTrait(index)" + `@click`="removeTrait(trait.id)" />Also applies to: 68-92
app/components/collection/admin/AdminSidebarVisibility.vue (1)
15-24: Consider addingaria-expandedfor accessibility.The collapsible button could benefit from an
aria-expandedattribute to communicate state to screen readers.♿ Suggested improvement
<button class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors" + :aria-expanded="isExpanded" `@click`="isExpanded = !isExpanded" >
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…board Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@app/composables/dashboard/useCreatorDashboard.ts`:
- Around line 84-95: Replace the local isAssetHubChain type-guard with the
exported isAssetHubChain from ~/utils/chain (remove the inline function) and,
inside the watch callback in useCreatorDashboard, ensure loading.value is reset
before the early return for non-AssetHub chains by setting collections.value =
[] then loading.value = false prior to returning; this guarantees loading is not
left true when exiting early.
In `@app/composables/massmint/useTemplateGenerator.ts`:
- Around line 43-46: The control-character regex in the sanitizedName
replacement (sanitizedName.replace(/[/\\?%*:|"<>\x00-\x1F]/g, '')) triggers
Biome; replace it either by adding a Biome ignore comment before the line (e.g.
// `@biome-ignore-next-line`) or switch to a string-based RegExp to avoid the
literal control-char range — e.g. use sanitizedName = sanitizedName.replace(new
RegExp('[/\\\\?%*:|"<>\x00-\\x1F]', 'g'), '') so the pattern is constructed from
a string; update the call in useTemplateGenerator.ts where sanitizedName and
collectionName are used.
🧹 Nitpick comments (1)
app/components/collection/admin/AdminSidebarVisibility.vue (1)
29-35: Consider removing unused event handlers on disabled switches.When
USwitchhas thedisabledprop, the@update:model-valueevent won't fire, making thehandleTogglebinding unreachable. This is harmless but creates dead code.If keeping the handlers as preparation for future enablement, consider adding a brief comment. Otherwise, remove them for clarity:
🧹 Optional cleanup
- <USwitch v-model="isPublished" size="sm" disabled `@update`:model-value="handleToggle" /> + <USwitch v-model="isPublished" size="sm" disabled /> </div> <div class="flex items-center justify-between"> <span class="text-sm">NSFW</span> - <USwitch v-model="isNsfw" size="sm" disabled `@update`:model-value="handleToggle" /> + <USwitch v-model="isNsfw" size="sm" disabled />
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/composables/dashboard/useCreatorDashboard.ts`:
- Around line 72-76: The early return in the watch callback for
watch([collectionIds, currentChain]) clears collections but doesn't reset
loading, so isLoading can remain true; update the callback in
useCreatorDashboard.ts to set loading.value = false (or the reactive used for
isLoading) before returning when ids is falsy or empty, ensuring any in-flight
invalidated fetch cannot leave loading stuck true; reference the watch callback,
the collectionIds/currentChain watcher, and the collections and loading reactive
values.
Ensure loading state is properly reset to false when the collection IDs array becomes empty, preventing the loading indicator from staying active indefinitely. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Introduces a new “Studio” creator workspace (dashboard + per-collection workspace) and a mass-minting/bulk-ops UI flow, integrating it into existing collection pages and navigation.
Changes:
- Adds Studio routes/layout/components for collection management and creator dashboard.
- Implements a 4-step Mass Mint wizard (upload/metadata/review/mint) with template generation/import and item editing UI.
- Adds bulk-operation state management (Pinia) plus selection-mode UX for future bulk actions.
Reviewed changes
Copilot reviewed 69 out of 71 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds new frontend deps (vuedraggable/sortablejs, papaparse) and updates lock snapshots. |
| package.json | Adds vuedraggable + papaparse; changes dev script. |
| app/utils/fileFormatting.ts | Adds file size/type label helpers for wizard UI. |
| app/types/bulkOperations.ts | Adds enums/types for bulk ops and mass mint step/state. |
| app/stores/bulkOperations.ts | Adds persisted Pinia store for bulk operation state + cart-backed items. |
| app/pages/[chain]/studio/index.client.vue | Studio dashboard page listing creator collections (with mock mode). |
| app/pages/[chain]/studio/[collection_id]/index.vue | Studio “Items” workspace (mock grid, selection mode, slide-over). |
| app/pages/[chain]/studio/[collection_id]/details.vue | Studio collection details editor (dirty-check + leave warning). |
| app/pages/[chain]/studio/[collection_id]/preview.vue | Studio collection preview using CollectionDisplay. |
| app/pages/[chain]/studio/[collection_id]/massmint.client.vue | Studio mass-mint route wrapping MassMintWizard + nav guard modal. |
| app/pages/[chain]/studio/[collection_id]/airdrop.client.vue | Placeholder bulk-airdrop page (WIP). |
| app/pages/[chain]/studio/[collection_id]/list.client.vue | Placeholder bulk-list page (WIP). |
| app/pages/[chain]/studio/[collection_id]/transfer.client.vue | Placeholder bulk-transfer page (WIP). |
| app/pages/[chain]/studio/[collection_id].vue | Studio collection shell: fetch/provide collection data, sidebar, breadcrumb/topbar, keyboard overlay. |
| app/pages/[chain]/collection/[collection_id]/index.vue | Extends collection page with mock mode + “Manage” entrypoint and selection/admin UI integration. |
| app/pages/[chain]/collection/[collection_id]/massmint.client.vue | Redirects legacy massmint route to Studio. |
| app/pages/[chain]/collection/[collection_id]/airdrop.client.vue | Redirects legacy airdrop route to Studio. |
| app/pages/[chain]/collection/[collection_id]/list.client.vue | Redirects legacy list route to Studio. |
| app/pages/[chain]/collection/[collection_id]/transfer.client.vue | Redirects legacy transfer route to Studio. |
| app/pages/[chain]/collection/[collection_id].vue | Converts parent route to wrapper + chain validation update. |
| app/layouts/studio.vue | Adds Studio layout container + page transition styles. |
| app/layouts/no-footer.vue | Adds overflow-hidden to main container. |
| app/composables/useOwnedCollections.ts | Fixes queryKey to include currentChain for cache correctness. |
| app/composables/studio/useStudioNavGuard.ts | Adds route-leave guard for active bulk operations. |
| app/composables/studio/useStudioKeyboard.ts | Adds global studio keyboard shortcuts + overlay toggle. |
| app/composables/studio/useStudioItems.ts | Adds local selection/search state for Studio items view. |
| app/composables/studio/useStudioDetails.ts | Adds editable details state + dirty tracking (currently placeholder save). |
| app/composables/studio/useStudioCollection.ts | Adds provide/inject collection context for Studio subpages. |
| app/composables/studio/useItemSlideOver.ts | Adds slide-over open/close state for item detail panel. |
| app/composables/massmint/useTemplateGenerator.ts | Generates/downloads CSV templates with CSV-injection protection. |
| app/composables/massmint/useMassMintWizard.ts | Adds wizard state (files, metadata mode, shared desc, validation). |
| app/composables/massmint/useMassMintForm.ts | Adds option-based prefill and skips fetching when collection context is provided. |
| app/composables/massmint/useMassMint.ts | Adds per-file progress callback support during metadata prep. |
| app/composables/dashboard/useCreatorDashboard.ts | Adds creator dashboard collection fetching (mock + live). |
| app/composables/collection/useAdminSidebar.ts | Adds local state machine for existing collection admin sidebar UI. |
| app/composables/bulkOperations/useBulkOperationWizard.ts | Adds generic wizard navigation + keyboard handling. |
| app/components/studio/StudioSidebar.vue | Adds Studio sidebar navigation + “active op” warning modal. |
| app/components/studio/StudioKeyboardOverlay.vue | Adds keyboard shortcut help overlay. |
| app/components/studio/StudioEmptyState.vue | Adds empty state prompting mass mint. |
| app/components/studio/StudioActionBar.vue | Adds bottom action bar for selected items (airdrop/list/transfer). |
| app/components/studio/ItemSlideOver.vue | Adds mock item slide-over editor panel. |
| app/components/massmint/wizard/MassMintWizard.vue | Orchestrates mass mint steps via bulk wizard layout + store. |
| app/components/massmint/wizard/steps/UploadStep.vue | Implements upload UI, zip extraction, grid/table views, drag reorder. |
| app/components/massmint/wizard/steps/MetadataStep.vue | Implements template import + uniform naming + per-item editor panel. |
| app/components/massmint/wizard/steps/ReviewStep.vue | Adds validation summary + cost estimate breakdown. |
| app/components/massmint/wizard/steps/MintStep.vue | Implements minting progress UX and success/error states. |
| app/components/massmint/wizard/MetadataPreviewTable.vue | Renders per-file metadata table with readiness indicators. |
| app/components/massmint/wizard/MassMintItemPanel.vue | Adds per-item editing slide-over with unsaved-changes guard. |
| app/components/massmint/wizard/FilePreviewModal.vue | Adds full-screen file preview modal with next/prev. |
| app/components/massmint/types.ts | Adds MassMintFile type for wizard state. |
| app/components/explore/NftsGrid.vue | Extends grid to support selection/studio mode and emits selection events. |
| app/components/dashboard/DashboardCollectionCard.vue | Adds collection card for Studio dashboard grid + manage routing. |
| app/components/create/modal/SuccessCollection.vue | Changes post-create navigation to Studio instead of collection page. |
| app/components/common/card/TokenCard.client.vue | Adds selection mode + studio mode click behavior and checkbox overlay. |
| app/components/collection/admin/FloatingManageButton.vue | Adds floating “Manage” button on collection page. |
| app/components/collection/admin/AdminSidebar.vue | Adds responsive admin sidebar container and view switching. |
| app/components/collection/admin/AdminSidebarIdentity.vue | Adds sidebar identity header. |
| app/components/collection/admin/AdminSidebarDetails.vue | Adds editable (placeholder) collection detail fields. |
| app/components/collection/admin/AdminSidebarEarnings.vue | Adds earnings section (placeholder). |
| app/components/collection/admin/AdminSidebarTeam.vue | Adds team section (placeholder). |
| app/components/collection/admin/AdminSidebarVisibility.vue | Adds visibility section (placeholder). |
| app/components/collection/admin/AdminSidebarActions.vue | Adds actions (mass mint / select items / delete placeholder). |
| app/components/collection/admin/AdminSelectionBar.vue | Adds selection actions that route into bulk ops pages. |
| app/components/collection/admin/AdminItemDetail.vue | Adds placeholder item detail editor view. |
| app/components/collection/CollectionDisplay.vue | Adds reusable collection display (used by Studio preview). |
| app/components/bulkOperations/BulkWizardLayout.vue | Adds reusable wizard layout wrapper with stepper + footer. |
| app/components/bulkOperations/BulkWizardFooter.vue | Adds wizard footer with shortcut hints + actions. |
| app/components/bulkOperations/BulkStepper.vue | Adds clickable stepper UI with reachable/completed states. |
| app/components/Navbar.vue | Adds “Studio” nav item for logged-in users. |
| app/assets/css/main.css | Adds mono font CSS variable. |
| .gitignore | Ignores Playwright MCP artifacts. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Jarsen136
left a comment
There was a problem hiding this comment.
The new studio flow looks good, but there are a few things off and need to be fixed and polished.
Found some issues here:
- I can see some collections in the studio do not belong to me, and also edit the details inside.
- missing chain switch on the studio page
- Missing attribute edit/upload in the massmint section
- JSON/TXT template is missing in the massmint section
- Drag to reorder is not working
- The item amount does not match
There are also some issues I comment alone the code.
| const isMac = computed(() => { | ||
| if (import.meta.server) | ||
| return false | ||
| return navigator.platform.toUpperCase().includes('MAC') | ||
| }) | ||
|
|
||
| const shortcutHint = computed(() => isMac.value ? '⌘+Enter' : 'Ctrl+Enter') |
There was a problem hiding this comment.
The shortcut does not work here
There was a problem hiding this comment.
It has not been solved yet, even with the code changes.
|
|
||
| function handleAirdrop() { | ||
| router.push(`/${props.chain}/collection/${props.collectionId}/airdrop`) | ||
| } | ||
|
|
||
| function handleList() { | ||
| router.push(`/${props.chain}/collection/${props.collectionId}/list`) | ||
| } | ||
|
|
||
| function handleTransfer() { | ||
| router.push(`/${props.chain}/collection/${props.collectionId}/transfer`) | ||
| } |
There was a problem hiding this comment.
It has not been solved yet, even with the code changes.
There was a problem hiding this comment.
they are not meant to be
| function handleDestroyCollection() { | ||
| // TODO: wire up DestroyCollectionModal via useOverlay | ||
| } |
There was a problem hiding this comment.
It has not been solved yet, even with the code changes.
| const isMock = computed(() => route.query.mock === 'true') | ||
|
|
||
| function handleMassMint() { | ||
| const mockQuery = isMock.value ? '?mock=true' : '' | ||
| const url = `/${props.chain}/collection/${props.collectionId}/massmint${mockQuery}` | ||
| router.push(url) | ||
| } |
There was a problem hiding this comment.
It seems that the mock logic is not working.
There was a problem hiding this comment.
It has not been solved yet, even with the code changes.
| const mockCollectionData = { | ||
| metadata: { | ||
| name: 'Cosmic Explorers', | ||
| description: 'A generative art collection exploring the boundaries of digital space. Each piece is a unique composition of cosmic patterns and ethereal forms.', | ||
| image: '', | ||
| banner: '', | ||
| }, | ||
| metadata_uri: null, | ||
| owner: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', | ||
| supply: '200', | ||
| claimed: '47', | ||
| floor: 1500000000, | ||
| } | ||
|
|
||
| const collectionData = computed(() => { | ||
| if (isMock.value && !data.value?.collection?.metadata?.name) { | ||
| return mockCollectionData | ||
| } | ||
| return data.value?.collection | ||
| }) | ||
|
|
||
| const collectionName = computed(() => collectionData.value?.metadata?.name) | ||
| const bannerUrl = computed(() => toOriginalContentUrl(sanitizeIpfsUrl(collectionData.value?.metadata?.banner || collectionData.value?.metadata?.image))) | ||
|
|
||
| const mockNfts = computed(() => { | ||
| if (!isMock.value) | ||
| return [] | ||
| 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(collection_id), | ||
| chain: chain.value, | ||
| name: `${name} #${i + 1}`, | ||
| price: i % 3 === 0 ? String((1.5 + i * 0.25) * 1e10) : null, | ||
| currentOwner: mockCollectionData.owner, | ||
| })) | ||
| }) | ||
|
|
||
| const isOwner = computed(() => { | ||
| if (isMock.value) |
There was a problem hiding this comment.
Could you remove the mock data?
There was a problem hiding this comment.
It has not been solved yet, even with the code changes.
app/pages/[chain]/collection/[collection_id]/massmint.client.vue
Outdated
Show resolved
Hide resolved
👀 I added a few comments for Exez to address them |
|
@Jarsen136 appreciate it "claude will address them" |
Code reviewFound 1 issue:
The app/app/pages/[chain]/collection/[collection_id]/index.vue Lines 228 to 232 in e1531c5 For reference, the studio items page handles this correctly by collecting actual item IDs before calling 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
… and Jarsen - Remove mock data from useCreatorDashboard, collection page, CollectionDisplay, studio page - Reset maxStepReached and chain in bulkOperations store reset() - Add input guard to keyboard shortcuts in useBulkOperationWizard and useStudioKeyboard - Fix AdminSidebarActions: warning toast for destroy, correct massmint route - Fix MetadataStep: parse functions return boolean, only set templateUploaded on success - Fix UploadStep: local wizard alias to allow v-model on draggable without prop mutation lint error - Fix TokenCard: use component :is instead of conditional NuxtLink - Add both biome-ignore and eslint-disable for control chars regex in useTemplateGenerator - Commit ssr-shim.cjs required by dev script Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Update AdminSelectionBar to navigate directly to /studio/ routes - Remove unnecessary redirect pages (collection/massmint, airdrop, list, transfer) - Revert package.json dev script to plain nuxt dev - Remove ssr-shim.cjs Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This reverts commit 19b0464.
- Update AdminSelectionBar to navigate directly to /studio/ routes - Remove unnecessary redirect pages (collection/massmint, airdrop, list, transfer) - Revert package.json dev script to plain nuxt dev - Remove ssr-shim.cjs from repository Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
@exezbcz seems your deployment is failing |
Those issues mentioned above still wait to be solved. |
Can I ask you to finish it, so these issues can be closed |
|
will fix the deploy, then handover |
I like the new studio flow with the fresh design provided in the preview. However, consider the obvious issues and bugs found, and quite a lot of mock logic in the codebase. Also, some pages and functionality are only available with the "todo" tag or "coming soon" toast. I doubt that this vibe code result is being implemented carefully Instead of continuing with the current MR, I would prefer to create a new one, but not from scratch. While we definitely can refer a bit of code from this MR, especially the new design and minting flow. In the meantime, I will remove all the unavailable pages for the feature has not implemented yet. What do you think? |
Sounds fair ^-^ |










Summary
Introduces a comprehensive Studio workspace for NFT creators to manage
their collections and perform bulk operations. This feature provides a
dedicated interface for collection owners to efficiently mint, transfer,
list, and airdrop NFTs at scale.
Key Features
🎨 Studio Workspace
🚀 Mass Minting Wizard
⚡ Bulk Operations
🎯 Collection Management
Components Added
Technical Details
collection data
UI/UX Improvements
Navigation
Test Plan
Things left
Summary by CodeRabbit
New Features
Style/UX
Chores