Skip to content

Comments

feat(studio): add Studio workspace with mass minting - Frontend#791

Open
exezbcz wants to merge 15 commits intomainfrom
massmint
Open

feat(studio): add Studio workspace with mass minting - Frontend#791
exezbcz wants to merge 15 commits intomainfrom
massmint

Conversation

@exezbcz
Copy link
Collaborator

@exezbcz exezbcz commented Feb 16, 2026

image image image image image image

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

  • Dedicated studio layout with collection navigation
  • Collection overview dashboard with stats and earnings
  • Keyboard shortcuts support with overlay guide
  • Breadcrumb navigation and context-aware routing

🚀 Mass Minting Wizard

  • 4-step wizard: Upload → Metadata → Review → Mint
  • Drag-and-drop file upload with ZIP extraction support
  • CSV template generation and import for metadata
  • Bulk metadata editing with shared descriptions
  • Live preview of NFT metadata before minting
  • Individual item editing panel with attributes support

⚡ Bulk Operations

  • Selection mode for multi-item operations
  • Bulk airdrop to multiple addresses
  • Bulk listing on marketplace
  • Bulk transfer functionality
  • Admin sidebar for quick access to actions

🎯 Collection Management

  • Collection admin tools and sidebar
  • Item detail views with editing capabilities
  • Collection visibility and team management
  • Earnings tracking and analytics
  • Floating "Manage" button on collection pages

Components Added

  • Mass minting wizard with 4 step components
  • Studio sidebar and navigation
  • Admin selection bar and item detail panels
  • Dashboard collection cards
  • Bulk operation stepper and layout components
  • Template generator utilities
  • File formatting helpers

Technical Details

  • New studio layout for full-screen workspace
  • Composables for wizard state management, keyboard shortcuts, and
    collection data
  • Bulk operations store using Pinia
  • Type definitions for bulk operations and mass minting
  • Integration with existing collection and NFT systems

UI/UX Improvements

  • Full-width studio layout (fixed edge-to-edge display)
  • Responsive design for sidebar and content areas
  • Loading states and progress indicators
  • Empty states for new collections
  • Keyboard navigation support

Navigation

  • Studio accessible from navbar (for logged-in users)
  • Manage button on collection pages (for owners)
  • Route: /{chain}/studio - Dashboard
  • Route: /{chain}/studio/{collection_id} - Collection workspace
  • Sub-routes for massmint, airdrop, list, transfer operations

Test Plan

  • Navigate to Studio from navbar
  • Create/access collection in Studio
  • Upload files via mass mint wizard
  • Generate and import CSV template
  • Edit metadata for individual items
  • Complete minting process
  • Select multiple items in collection
  • Perform bulk airdrop/list/transfer
  • Test keyboard shortcuts (? to open overlay)
  • Verify full-width layout on all screen sizes
  • you can use "?mock=true? for mock data

Things left

  • clean up of old files - create etc
  • Collection creation integrated better with the ui
  • transfer, list, airdrop moved to the studio
  • better studio landing - right now its just collection cards

Summary by CodeRabbit

  • New Features

    • Studio: dashboard, sidebar, keyboard overlay, item slide‑over, action bars, studio pages and routing.
    • Mass Mint: full multi‑step wizard (upload, metadata, review, mint), file preview/editor, CSV template generation/download, ZIP import, per‑file editing.
    • Bulk operations & selection: wizard, stepper, footer, selection mode, action flows (airdrop/list/transfer).
    • Collection admin & display: admin sidebar, item editor, selection controls, collection cards.
  • Style/UX

    • Added monospace font variable, keyboard hints, responsive layouts, transitions.
  • Chores

    • Expanded .gitignore for Playwright MCP artifacts.

exezbcz and others added 2 commits February 16, 2026 13:57
…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>
@railway-app
Copy link

railway-app bot commented Feb 16, 2026

This PR was not deployed automatically as @exezbcz does not have access to the Railway project.

In order to get automatic PR deploys, please add @exezbcz to your workspace on Railway.

@vercel
Copy link

vercel bot commented Feb 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
koda Ready Ready Preview, Comment Feb 17, 2026 4:45pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Config & deps
/.gitignore, package.json
Adds .playwright-mcp/ to gitignore; updates dev script and adds vuedraggable + papaparse typings/deps.
Global styles & layouts
app/assets/css/main.css, app/layouts/no-footer.vue, app/layouts/studio.vue
Adds --font-mono CSS variable; main layout gains overflow-hidden; adds minimal studio layout with page transitions.
Types & Store
app/types/bulkOperations.ts, app/stores/bulkOperations.ts
New bulk-op enums/interfaces and a persistent Pinia store managing operation type, steps, progress, and lifecycle actions.
Wizard helpers & composables
app/composables/bulkOperations/..., app/composables/massmint/...
New useBulkOperationWizard, useMassMintWizard, useMassMintForm (signature change to accept options), useMassMint (prepareNftMetadata gains optional progress callback), useTemplateGenerator and related mass‑mint helpers.
Studio & collection composables
app/composables/collection/..., app/composables/studio/..., app/composables/dashboard/useCreatorDashboard.ts
New composables for admin-sidebar state, studio collection provider, item slide-over, keyboard shortcuts, nav guard, studio details/items, and creator dashboard with mock support.
Mass-mint UI & wizard
app/components/massmint/..., app/components/massmint/wizard/steps/*
Large set of new components: FilePreviewModal, MassMintItemPanel, MetadataPreviewTable, Upload/Metadata/Review/Mint steps, MassMintWizard, and related panels.
Bulk UI primitives
app/components/bulkOperations/*
New BulkStepper, BulkWizardFooter, BulkWizardLayout components used by multi-step flows.
Collection admin UI
app/components/collection/admin/...
New AdminSidebar and many subcomponents (Actions, Details, Earnings, Team, Visibility, Identity), AdminSelectionBar, AdminItemDetail, FloatingManageButton.
Studio UI & pages
app/components/studio/*, app/pages/.../studio/*
New studio components (ItemSlideOver, StudioActionBar, StudioEmptyState, StudioKeyboardOverlay, StudioSidebar) and numerous studio pages (index, details, preview, massmint, list, airdrop, transfer).
Collection pages & routing
app/pages/[chain]/collection/...
New rich collection page at .../index.vue, plus small client redirect wrappers (airdrop/list/massmint/transfer) and a simplified placeholder page elsewhere.
Selection & grids
app/components/common/card/TokenCard.client.vue, app/components/explore/NftsGrid.vue
TokenCard and NftsGrid gain selectionMode, isSelected, studioMode props and emits select/itemClick; add selection UI and disable nav while selecting/studio.
Collection display & dashboard
app/components/collection/CollectionDisplay.vue, app/components/dashboard/DashboardCollectionCard.vue
New read-only collection display component and dashboard card with mock handling.
Mass-mint types & file utils
app/components/massmint/types.ts, app/utils/fileFormatting.ts
Adds MassMintFile type and file-formatting helpers (formatFileSize, getFileTypeLabel).
Small UI tweaks
app/components/Navbar.vue, app/components/create/modal/SuccessCollection.vue
Adds conditional Studio nav link when accountId present; changes success modal route to studio path for non-special prefix.
Misc pages/layout edits
app/pages/...
Various new pages and many additions under studio and collection routes; a major simplification of one collection page replaced by a minimal NuxtPage render.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code at break of dawn,

Steps and wizards stitched and drawn,
Sidebars, slides, and mass‑mint cheer,
Selections, previews — now they're here,
A tiny rabbit clap: hop on!

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (17 files):

⚔️ .gitignore (content)
⚔️ app/assets/css/main.css (content)
⚔️ app/components/Navbar.vue (content)
⚔️ app/components/common/card/TokenCard.client.vue (content)
⚔️ app/components/create/modal/SuccessCollection.vue (content)
⚔️ app/components/explore/NftsGrid.vue (content)
⚔️ app/components/massmint/types.ts (content)
⚔️ app/composables/massmint/useMassMint.ts (content)
⚔️ app/composables/massmint/useMassMintForm.ts (content)
⚔️ app/composables/useInfiniteNfts.ts (content)
⚔️ app/composables/useOwnedCollections.ts (content)
⚔️ app/graphql/queries/explore.ts (content)
⚔️ app/layouts/no-footer.vue (content)
⚔️ app/pages/[chain]/collection/[collection_id].vue (content)
⚔️ app/types/index.ts (content)
⚔️ package.json (content)
⚔️ pnpm-lock.yaml (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(studio): add Studio workspace with mass minting - Frontend' is clear, specific, and accurately reflects the main objective of adding a comprehensive Studio workspace with mass minting functionality to the frontend.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch massmint

Comment @coderabbitai help to get the list of available commands and usage tips.

@exezbcz exezbcz changed the title feat(studio): add Studio workspace with mass minting feat(studio): add Studio workspace with mass minting - Frontend Feb 16, 2026
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 16, 2026

Deploying app with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7b61ddc
Status:🚫  Build failed.

View logs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Progress can regress due to concurrent batch completion.

progress.current = index + 1 (and onFileProgress?.(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 | 🟡 Minor

Quote Menlo and Consolas to fix stylelint errors and match existing patterns.

Stylelint's value-keyword-case rule 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 | 🟡 Minor

Misleading UX: switches toggle visually but feature is not implemented.

The switches update isPublished/isNsfw refs, showing a visual state change, but then handleToggle displays 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:

  1. Disabling the switches with a tooltip explaining the feature is coming soon
  2. Preventing the state change by not binding v-model until 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 | 🟡 Minor

Route params may be arrays—ensure string handling.

route.params.chain and route.params.collection_id are typed as string | 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 useRoute with 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_id
app/composables/studio/useStudioDetails.ts-1-49 (1)

1-49: ⚠️ Potential issue | 🟡 Minor

Local state won’t update if collection loads/changes later.

description, royaltyRecipient, and collaborators are initialized once from collection.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 | 🟡 Minor

Description editor doesn’t show current value.

localDescription is 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 | 🟡 Minor

Use SupportedChain for public chain identifiers.

chain is a public prop and should use SupportedChain per repo guidelines.
As per coding guidelines: Use SupportedChain type from ~/plugins/sdk.client for 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 | 🟡 Minor

Use SupportedChain for public chain identifiers.

DashboardCollection.chain is a public type and should use SupportedChain per repo guidelines; keep AssetHubChain only where the API requires it (e.g., fetchOdaCollection).
As per coding guidelines: Use SupportedChain type from ~/plugins/sdk.client for 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 | 🟡 Minor

Use SupportedChain for public chain identifiers.

StudioCollectionData.chain should use SupportedChain per repo guidelines; keep AssetHubChain only where an API explicitly requires it.
As per coding guidelines: Use SupportedChain type from ~/plugins/sdk.client for 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: string
app/pages/[chain]/studio/[collection_id]/transfer.client.vue-4-9 (1)

4-9: ⚠️ Potential issue | 🟡 Minor

Use useChain for chain handling instead of route.params.chain.
This keeps chain normalization/typing consistent across the app when building backlinks.

🔧 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}`,
 )
As per coding guidelines: Use `useChain` composable (route-based) as the primary method for chain handling, with `usePrefix` only as a fallback.
app/pages/[chain]/studio/[collection_id]/airdrop.client.vue-4-9 (1)

4-9: ⚠️ Potential issue | 🟡 Minor

Use useChain for chain handling instead of route.params.chain.
This keeps chain normalization/typing consistent across the app when building backlinks.

🔧 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}`,
 )
As per coding guidelines: Use `useChain` composable (route-based) as the primary method for chain handling, with `usePrefix` only as a fallback.
app/stores/bulkOperations.ts-86-92 (1)

86-92: ⚠️ Potential issue | 🟡 Minor

reset() does not reset maxStepReached, causing stale navigation state.

When resetting the store, maxStepReached remains 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 | 🟡 Minor

Add 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 | 🟡 Minor

Mock mode won't update reactively if query changes.

Passing isMock.value extracts the primitive boolean at call time. If the user navigates and the ?mock=true query changes without a full page reload, the composable won't receive the updated value.

Consider passing a getter or the computed ref if useCreatorDashboard supports 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

hasItems always returns true for 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 | 🟡 Minor

Guard against undefined collection owner.

collection.value.owner is accessed directly, but if the collection data hasn't loaded or is missing, this could be undefined. 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 | 🟡 Minor

Avoid using @kodadot1/static package.

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/static package; use internal implementations instead."

app/pages/[chain]/studio/index.client.vue-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Avoid using @kodadot1/static package.

The coding guidelines explicitly state: "Do NOT use @kodadot1/static package; use internal implementations instead (useChain, SupportedChain, chainSpec)". Consider using an internal chain validation approach or the useChain composable.

As per coding guidelines: "Do NOT use @kodadot1/static package; use internal implementations instead."

app/components/massmint/wizard/FilePreviewModal.vue-47-62 (1)

47-62: ⚠️ Potential issue | 🟡 Minor

Navigation button visibility may not match actual navigation behavior.

The prev/next button visibility uses file.order (lines 48, 57), but navigatePreview uses the actual array index via findIndex. If the files array is not sorted by the order property, 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 | 🟡 Minor

Use SupportedChain for the chain prop.

✅ 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.client for chain identifiers.

app/components/studio/ItemSlideOver.vue-2-8 (1)

2-8: ⚠️ Potential issue | 🟡 Minor

Use 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.client for chain identifiers.

app/components/massmint/wizard/MassMintWizard.vue-2-15 (1)

2-15: ⚠️ Potential issue | 🟡 Minor

Swap 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.client for chain identifiers.

app/types/bulkOperations.ts-1-37 (1)

1-37: ⚠️ Potential issue | 🟡 Minor

Type chain as 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.client for chain identifiers.

app/components/studio/StudioSidebar.vue-175-182 (1)

175-182: ⚠️ Potential issue | 🟡 Minor

Delete 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 | 🟡 Minor

Use SupportedChain for the chain prop.

✅ 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.client for chain identifiers.

app/components/massmint/wizard/steps/MintStep.vue-78-81 (1)

78-81: ⚠️ Potential issue | 🟡 Minor

Avoid explicit any in error handling. Use unknown and 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 any type, bypassing TypeScript's type checking. Using unknown with 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 | 🟡 Minor

Use useSeoMeta with reactive values or defer defineOgImageComponent until data loads.

defineOgImageComponent expects static serializable values at call time and doesn't track reactive refs. Since the code uses useLazyAsyncData (which doesn't block), collectionData.value will be undefined when defineOgImageComponent is 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 useSeoMeta with 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: Use useChain composable for chain handling.

The coding guidelines specify using useChain composable as the primary method for chain handling. Direct access to route.params.chain should be avoided.

Additionally, route params are typed as string | string[]; consider narrowing to string for 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 useChain composable (route-based) as the primary method for chain handling, with usePrefix only 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:

  1. Accepting a show prop to control visibility from the parent
  2. Removing the v-show and <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.vue and list.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: Keep isOpen in 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=true when in mock mode. Prefer router.push({ path, query }) for clean URLs and consistency with handleManage.

♻️ 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: Use useChain and preserve mock query in backLink.

Line 7 builds the link from route.params.chain directly. Prefer useChain() for chain handling and keep mock=true when present so the mode doesn’t drop on back navigation.
As per coding guidelines: Use useChain composable (route-based) as the primary method for chain handling, with usePrefix only 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 handleDestroyCollection unimplemented. If you want, I can sketch the useOverlay wiring and modal flow.

app/components/bulkOperations/BulkWizardFooter.vue (1)

18-22: Consider using navigator.userAgentData for platform detection.

navigator.platform is deprecated. While it still works in browsers, consider using navigator.userAgentData?.platform with 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.

_prefix is 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 names array is duplicated in CollectionDisplay.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-expanded and aria-controls attributes 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: Apply pointer-events-none for both selection and studio modes.

Currently pointer-events-none is only applied when selectionMode is active, but studioMode also disables navigation (sets to to undefined). 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 chain as chainPrefix then creating a computed chain is 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 chainPrefix consistently 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.thumbnailUrl directly 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 using v-model instead of :model-value for proper two-way binding with vuedraggable.

The draggable component receives :model-value (one-way binding) but doesn't receive @update:model-value to sync changes. While handleDragEnd updates the order property 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 using any.

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 any in TypeScript code."

app/components/massmint/wizard/steps/MetadataStep.vue (1)

164-164: Avoid any type in JSON parsing callback.

The entry: any loses 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 any in TypeScript code."

@vikiival vikiival requested a review from Jarsen136 February 16, 2026 13:24
@vikiival
Copy link

cc @Jarsen136 can you please pick it up ?

exezbcz and others added 3 commits February 16, 2026 14:49
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>
@JustLuuuu
Copy link

My Feedback

1) Back/ Continue button

Back/ Continue button: they are different, Im also not sure if Continue should not be slightly more visible. (Took me 3 secs to find it 😊)

Screenshot 2026-02-16 at 13 46 50

2) Base name

  • what is base name, base name of what - would adjust workding (name for NFT?) - maybe have a hover text with details
Screenshot 2026-02-16 at 13 44 53

3) Empty space

Not sure about all the space around, maybe its because of mobile support? But in the same time, feels like unused space and the sidebar is smoll

Screenshot 2026-02-16 at 13 44 26

4) If good then green

Screenshot 2026-02-16 at 13 49 59 do you think it would make sense if these changed colour to green shade when done?

5) Estimated cost

Would add estimated costs above the NFTs, so people dont have to scroll to bottom to see it, it can be pinned to top.
Screenshot 2026-02-16 at 13 54 03

6) popup - "saved"

Add somewhere text or popup that description is "saved" when you click on save
Screenshot 2026-02-16 at 13 53 38

Love it

Upload page is really great. I like how it works, overall I love it

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 list

Keying by index and removing by index can cause input state to “jump” after deletions. Consider adding a stable id per 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 adding aria-expanded for accessibility.

The collapsible button could benefit from an aria-expanded attribute 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"
     >

exezbcz and others added 3 commits February 16, 2026 15:43
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>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 USwitch has the disabled prop, the @update:model-value event won't fire, making the handleToggle binding 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 />

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@Jarsen136 Jarsen136 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new studio flow looks good, but there are a few things off and need to be fixed and polished.

Found some issues here:

  1. I can see some collections in the studio do not belong to me, and also edit the details inside.
Image
  1. missing chain switch on the studio page
  2. Missing attribute edit/upload in the massmint section
  3. JSON/TXT template is missing in the massmint section
  4. Drag to reorder is not working

Image

  1. The item amount does not match
Image

There are also some issues I comment alone the code.

Comment on lines +18 to +24
const isMac = computed(() => {
if (import.meta.server)
return false
return navigator.platform.toUpperCase().includes('MAC')
})

const shortcutHint = computed(() => isMac.value ? '⌘+Enter' : 'Ctrl+Enter')
Copy link
Contributor

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

Copy link
Contributor

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.

Comment on lines 17 to 28

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`)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These pages are not available

Image

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are not meant to be

Comment on lines 29 to 31
function handleDestroyCollection() {
// TODO: wire up DestroyCollectionModal via useOverlay
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

Copy link
Contributor

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.

Comment on lines 17 to 23
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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the mock logic is not working.

Copy link
Contributor

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.

Comment on lines 126 to 178
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remove the mock data?

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

@Jarsen136
Copy link
Contributor

cc @Jarsen136 can you please pick it up ?

👀 I added a few comments for Exez to address them

@exezbcz
Copy link
Collaborator Author

exezbcz commented Feb 17, 2026

@Jarsen136 appreciate it

"claude will address them"

@exezbcz
Copy link
Collaborator Author

exezbcz commented Feb 17, 2026

Code review

Found 1 issue:

  1. selectAll([]) always passes an empty array — the "Select All" action is broken

The @select-all handler is wired with a hardcoded empty array, so it always clears the selection rather than selecting all items. The selectAll function creates a new Set(ids), so passing [] produces an empty set on every invocation.

:selected-count="selectedCount"
@toggle-selection="toggleSelectionMode"
@select-all="selectAll([])"
@clear-selection="clearSelection"
@close-item-detail="closeItemDetail"

For reference, the studio items page handles this correctly by collecting actual item IDs before calling selectAll.

🤖 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>
- 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>
@vikiival
Copy link

@exezbcz seems your deployment is failing

17:45:24.292 | [success] [nitro] Generated public dist
-- | --
17:45:24.480 | [info] [nitro] Building Nuxt Nitro server (preset: `cloudflare-pages`, compatibility date: `2024-11-27`)
17:45:40.517 | [error] [nitro] RollupError: Expected ',', got 'undefined' in /opt/buildhome/repo/node_modules/.pnpm/papaparse@5.5.3/node_modules/papaparse/papaparse.js
17:45:40.517 |  
17:45:40.517 |  
17:45:40.518 | 50:     var URL = global.URL \|\| global.webkitURL \|\| null;
17:45:40.518 | 51:     var code = moduleFactory.toString();
17:45:40.518 | 52:     return Papa.BLOB_URL \|\| (Papa.BLOB_URL = URL.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if ("undefined"...
17:45:40.518 | ^
17:45:40.518 | 53:   }
17:45:40.520 | [error] Expected ',', got 'undefined' in /opt/buildhome/repo/node_modules/.pnpm/papaparse@5.5.3/node_modules/papaparse/papaparse.js
17:45:40.520 | at getRollupError (node_modules/.pnpm/rollup@4.52.5/node_modules/rollup/dist/es/shared/parseAst.js:401:41)
17:45:40.520 | at convertProgram (node_modules/.pnpm/rollup@4.52.5/node_modules/rollup/dist/es/shared/parseAst.js:1098:26)
17:45:40.520 | at parseAst (node_modules/.pnpm/rollup@4.52.5/node_modules/rollup/dist/es/shared/parseAst.js:2083:87)
17:45:40.520 | at tryParse (node_modules/.pnpm/@rollup+plugin-commonjs@28.0.9_rollup@4.52.5/node_modules/@rollup/plugin-commonjs/dist/es/index.js:17:12)
17:45:40.520 | at analyzeTopLevelStatements (node_modules/.pnpm/@rollup+plugin-commonjs@28.0.9_rollup@4.52.5/node_modules/@rollup/plugin-commonjs/dist/es/index.js:37:15)
17:45:40.520 | at Object.transformAndCheckExports (node_modules/.pnpm/@rollup+plugin-commonjs@28.0.9_rollup@4.52.5/node_modules/@rollup/plugin-commonjs/dist/es/index.js:2161:68)
17:45:40.520 | at Object.transform (node_modules/.pnpm/@rollup+plugin-commonjs@28.0.9_rollup@4.52.5/node_modules/@rollup/plugin-commonjs/dist/es/index.js:2368:41)
17:45:40.520 | at node_modules/.pnpm/rollup@4.52.5/node_modules/rollup/dist/es/shared/node-entry.js:22426:40
17:45:40.521 |  
17:45:40.521 | [error] Expected ',', got 'undefined' in /opt/buildhome/repo/node_modules/.pnpm/papaparse@5.5.3/node_modules/papaparse/papaparse.js
17:45:40.697 | ELIFECYCLE  Command failed with exit code 1.
17:45:40.720 | Failed: Error while executing user command. Exited with error code: 1
17:45:40.730 | Failed: build command exited with code: 1
17:45:42.506 | Failed: error occurred while running build command

+there are conflicts
Screenshot 2026-02-17 at 21 03 57

@JustLuuuu
Copy link

image

@Jarsen136
Copy link
Contributor

The new studio flow looks good, but there are a few things off and need to be fixed and polished.

Found some issues here:

Those issues mentioned above still wait to be solved.

@vikiival
Copy link

The new studio flow looks good, but there are a few things off and need to be fixed and polished.
Found some issues here:

Those issues mentioned above still wait to be solved.

Can I ask you to finish it, so these issues can be closed

#791 (comment)

@exezbcz
Copy link
Collaborator Author

exezbcz commented Feb 24, 2026

will fix the deploy, then handover

@Jarsen136
Copy link
Contributor

The new studio flow looks good, but there are a few things off and need to be fixed and polished.
Found some issues here:

Those issues mentioned above still wait to be solved.

Can I ask you to finish it, so these issues can be closed

#791 (comment)

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
and well tested. The edge case hunting and bug fixing could cost a lot of time.

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?

@vikiival
Copy link

What do you think?

Sounds fair ^-^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants