Skip to content

Collage Page ​

File Information ​

  • Path: app/pages/[brand_name]/collage/[id]/index.vue
  • Route: /{brand_name}/collage/{id}
  • Purpose: Display collage (collection) details with assets, supporting grid/list views, sorting, selection, sharing, and downloading
  • Layout: collage-layout
  • Middleware: check-url

Overview ​

The Collage page displays a specific collage (collection) and its assets. It provides comprehensive collage management features including view mode toggling, sorting, bulk selection, sharing, downloading, and asset navigation. A collage is a curated collection of assets that can be shared and managed as a unit.

Key Features ​

  1. Collage Display

    • Collage metadata (name, description)
    • Asset list in grid or list view
    • Empty state handling
    • Back navigation to collage list
  2. View Modes

    • Grid view with mosaic layout
    • List view with sortable columns
    • View mode preference caching
    • Smooth transitions between views
  3. Sorting

    • Sort by Name, Date Modified, Date Added, File Size, File Type
    • Ascending/descending toggle
    • Visual sort indicators
    • Client-side sorting for loaded assets
  4. Selection & Bulk Actions

    • Single and multi-select (Shift+Click support)
    • Select all functionality
    • Bulk download
    • Bulk share
    • Selection count display
  5. Collage Actions

    • Share entire collage
    • Download entire collage
    • Share selected assets
    • Download selected assets
  6. Infinite Scroll Pagination

    • Automatic loading on scroll
    • Skeleton loaders
    • Scroll position management

Components Used ​

  • AssetCard - Asset card for grid view
  • ShareAssetDialog - Share dialog component
  • AsyncIcon - Async icon component
  • v-skeleton-loader - Skeleton loaders

Composables Used ​

  • useCollageDetails() - Collage data management (app/composables/core/pages/useCollageDetails.ts)
  • useCollage() - Collage operations (app/composables/core/pages/useCollage.ts)
  • useDownloadStore() - Download store
  • useSnackbar() - Snackbar notifications
  • useFileType() - File type utilities
  • useSelection() - Selection utilities

Data Properties ​

Reactive Data (from useCollageDetails composable) ​

typescript
{
  assetList: AssetList,                // Collage data
  files: Asset[],                      // Collage assets
  loading: boolean,                    // Loading state
  contentLoading: boolean,             // Initial content loading
  loadMore: boolean,                   // Load more pagination state
  pageScrolling: boolean,              // Page scrolling state
  viewMode: 'list' | 'grid',          // Current view mode
  selected_items: Asset[],             // Selected assets
  download_items: Asset[],             // Items to download
  share_items: Asset[],                // Items to share
  shareDialog: boolean,                 // Share dialog visibility
  shareAllfiles: boolean,              // Share entire collage flag
  internal_select_all: boolean,        // Internal select all state
  select_all: boolean,                 // Select all checkbox state
  downldCollage: boolean,              // Collage download in progress
  downloadSlctd: boolean,              // Selected download in progress
  showMoreDetails: boolean,            // Description expand state
  truncatedDescription: string,         // Truncated description
  sort_loader: string,                 // Active sort column loader
  page_sort_value: SortField,          // Current sort field
  page_sort_by: SortDirection,         // Current sort direction
  showArrowSort: ShowArrowSort,        // Sort arrow visibility
  hover_name: boolean,                 // Name column hover
  hover_modified_at: boolean,          // Modified date hover
  hover_created_at: boolean,           // Created date hover
  hover_file_size: boolean,            // File size hover
  hover_file_type: boolean,            // File type hover
  fileCardKey: number,                 // File card key for reactivity
  scrollContainer: HTMLElement | null, // Scroll container ref
  listContainer: HTMLElement | null,  // List container ref
  currentPage: number,                 // Current page number
  lastPage: number                     // Last page number
}

Computed Properties ​

selectAllClass ​

Returns CSS class for select all checkbox.

typescript
computed(() => 
  !select_all.value && selectedItemsCount.value > 0 ? "semi-selected" : ""
)

getShareTitle ​

Returns share title based on selection.

typescript
computed(() => {
  if (shareAllfiles.value || internal_select_all.value) {
    return assetList.value?.name || ''
  }
  if (share_items.value.length === 1) {
    return share_items.value[0]?.display_file_name?.replace(
      new RegExp(`\\.${share_items.value[0]?.file_type}$`, "i"),
      ""
    ) || ""
  }
  return ""
})

getShareDescription ​

Returns share description based on selection.

typescript
computed(() => {
  if (shareAllfiles.value || internal_select_all.value) {
    return assetList.value?.description || ''
  }
  if (share_items.value.length === 1) {
    return share_items.value[0]?.description || ""
  }
  return ""
})

truncatedDescription ​

Returns truncated collage description.

typescript
computed(() => {
  const description = assetList.value?.description
  if (!description) return ""
  if (showMoreDetails.value && description.length > 250) {
    return description.slice(0, 250)
  }
  return description
})

selectedItemsCount ​

Returns count of selected items.

typescript
computed(() => {
  let count = 0
  if (internal_select_all.value && select_all.value) {
    count = assetList.value?.assets_count ?? 0
  } else if (internal_select_all.value) {
    const notSelected = files.value.filter((obj) => !obj.is_selected).length
    count = (assetList.value?.assets_count ?? 0) - notSelected
  } else {
    count = selected_items.value.length
  }
  return Math.max(count, 0)
})

selectedItemAssets ​

Returns array of selected asset IDs.

typescript
computed(() => {
  let selectedItemsA: (string | number)[] = []
  if (select_all.value && assetList.value.assets_id) {
    selectedItemsA = assetList.value.assets_id
  } else if (internal_select_all.value && assetList.value.assets_id) {
    selectedItemsA = assetList.value.assets_id.filter((id) => {
      const matchingAsset = files.value.find((asset) => asset.id === id)
      return !matchingAsset || matchingAsset.is_selected
    })
  }
  return selectedItemsA
})

Methods ​

loadCollageAssets() ​

Loads collage assets from API.

typescript
async function loadCollageAssets() {
  if (loadMore.value || currentPage.value >= lastPage.value) {
    return
  }
  
  if (lastPage.value) {
    currentPage.value += 1
    loadMore.value = currentPage.value > 1
    loading.value = !loadMore.value
  }
  
  try {
    const response = await getCollageDetailsApi(route.params.id as string, {
      page: currentPage.value,
      url_workspace_id: workspaceId,
      total_record: 36,
    })
    
    assetList.value = response.data
    lastPage.value = response.data.assets.last_page
    const data = response.data.assets.data
    
    if (data?.length) {
      const existingIds = new Set(files.value.map((f) => f.id))
      const newFiles = data.filter(
        (newFile: Asset) => !existingIds.has(newFile.id)
      )
      files.value.push(
        ...newFiles.map((file: Asset) => ({
          ...file,
          is_selected: internal_select_all.value,
          is_downloading: false,
        }))
      )
    }
  } catch (error) {
    snackbar.showError(getErrorMessage(error as Error))
  } finally {
    loading.value = false
    loadMore.value = false
  }
}

Flow:

  1. Checks pagination limits
  2. Increments page number
  3. Fetches collage details
  4. Updates asset list
  5. Appends new files (avoiding duplicates)
  6. Handles errors

handleScroll(flag: "main" | "list") ​

Handles scroll events for infinite scroll.

typescript
async function handleScroll(flag: "main" | "list" = "main") {
  try {
    const container =
      flag === "list" ? listContainer.value : scrollContainer.value
    if (!container) return
    
    const scrollTop = container.scrollTop
    const containerHeight = container.scrollHeight - container.clientHeight
    const scrollPercent = (scrollTop / containerHeight) * 100
    
    if (scrollPercent >= 85 && files.value.length) {
      await loadCollageAssets()
    } else if (scrollTop >= 36 && files.value.length) {
      pageScrolling.value = true
    } else {
      pageScrolling.value = false
    }
  } catch (error) {
    snackbar.showError(getErrorMessage(error as Error))
  }
}

Triggers: Loads more assets when scroll reaches 85% of container height.

toggleSelectAll() ​

Toggles select all state.

typescript
function toggleSelectAll() {
  internal_select_all.value = select_all.value
  selected_items.value = select_all.value ? [...files.value] : []
  files.value = files.value.map((file) => ({
    ...file,
    is_selected: select_all.value,
  }))
  lastSelectedIndex.value = -1
}

toggleSelect(file, index, event) ​

Toggles individual asset selection with Shift+Click support.

typescript
function toggleSelect(file: any, index: number, event: any) {
  try {
    const context: any = {
      files: files.value,
      selected_items: selected_items.value,
      lastPage: lastPage.value,
      currentPage: currentPage.value,
    }
    const isSelected = file.is_selected
    const findIndex = files.value.findIndex((obj) => obj.id === file.id)
    if (findIndex > -1) {
      files.value[findIndex].is_selected = isSelected
    }
    toggleSelection(context, file, index, event, "collage")
    selected_items.value = context.selected_items
  } catch (error) {
    snackbar.showError(getErrorMessage(error as Error))
    lastSelectedIndex.value = -1
  }
}

changeView(data: ViewMode) ​

Changes view mode and caches preference.

typescript
function changeView(data: ViewMode) {
  if (data !== viewMode.value) {
    setCachedViewMode(data as any)
  }
}

handleSort(field: string) ​

Handles column sorting (client-side).

typescript
function handleSort(field: string) {
  try {
    page_sort_value.value = field as SortField
    page_sort_by.value = page_sort_by.value === "ASC" ? "DESC" : "ASC"
    showArrowSort[field] = true
    
    // Reset other sort arrows
    for (const key in showArrowSort) {
      if (key !== field) {
        showArrowSort[key] = false
      }
    }
    
    // Perform client-side sorting
    files.value.sort((a: any, b: any) => {
      let fieldA = a[field]
      let fieldB = b[field]
      
      // Handle date fields
      if (field === "modified_at" || field === "created_at") {
        fieldA = new Date(fieldA as string)
        fieldB = new Date(fieldB as string)
      }
      
      // Handle string fields
      if (["file_type", "display_file_name"].includes(field)) {
        fieldA = (fieldA as string).toLowerCase()
        fieldB = (fieldB as string).toLowerCase()
      }
      
      // Handle numeric fields
      if (field === "file_size") {
        fieldA = Number(fieldA) || 0
        fieldB = Number(fieldB) || 0
      }
      
      // Compare based on sort direction
      if (page_sort_by.value === "ASC") {
        return fieldA > fieldB ? 1 : fieldA < fieldB ? -1 : 0
      } else {
        return fieldA < fieldB ? 1 : fieldA > fieldB ? -1 : 0
      }
    })
  } catch (error) {
    console.error("handleSort - Error:", error)
    snackbar.showError(getErrorMessage(error as Error))
  }
}

downloadFile(flag: "selected" | "download_all") ​

Handles asset downloads.

typescript
async function downloadFile(
  flag: "selected" | "download_all" = "selected"
) {
  try {
    const isDownloadAll = flag === "download_all" && files.value.length > 1
    const hasSingleItem =
      download_items.value.length === 1 ||
      selected_items.value.length === 1 ||
      (flag === "download_all" && files.value.length === 1)
    
    const itemsToDownload = download_items.value.length
      ? download_items.value
      : selected_items.value
    
    if (isDownloadAll) {
      downldCollage.value = true
      const fileIds: any =
        flag !== "download_all"
          ? files.value.map(({ id }) => id)
          : assetList.value.assets_id || []
      
      await downloadStore?.downloadMultipleFiles({
        files: fileIds,
        folders: [],
        collection_id: null,
        download_name: assetList.value?.name,
      })
      
      dispatchAnalytics({
        asset_id: fileIds,
        collection_id: route.params.id as string,
        event: "collage" as const,
        sub_event: "downloaded" as const,
        environment: "dealer" as const,
      })
    } else if (hasSingleItem) {
      // Download single file
      const singleItem = itemsToDownload[0]
      const { isImage } = useFileType(singleItem || {})
      
      if (isImage.value) {
        await downloadStore.downloadImageAsset({
          assetId: String(singleItem.id),
          fileName: singleItem.display_file_name,
          fileType: singleItem.file_type,
        })
      } else {
        downloadStore.downloadFile({
          id: String(singleItem.id),
          url: singleItem.download_url,
          name: singleItem.display_file_name,
        })
      }
    } else if (selected_items.value.length) {
      // Download multiple files
      downloadSlctd.value = true
      await downloadStore?.downloadMultipleFiles({
        files: internal_select_all.value
          ? selectedItemAssets.value
          : selected_items.value.map(({ id }) => Number(id)),
        folders: [],
        collection_id: null,
        download_name: `download-files-${Date.now()}`,
      })
    }
  } catch (error) {
    snackbar.showError(getErrorMessage(error as Error))
    files.value = files.value.map((obj) => ({
      ...obj,
      is_downloading: false,
    }))
    downloadSlctd.value = false
    downldCollage.value = false
  }
}

openShareCollage() ​

Opens share dialog for entire collage.

typescript
function openShareCollage() {
  if (files.value.length) {
    shareAllfiles.value = true
    shareDialog.value = true
  }
}

openBulkShare() ​

Opens share dialog for selected assets.

typescript
function openBulkShare() {
  share_items.value = selected_items.value
  shareDialog.value = true
}

closeShareDialog() ​

Closes share dialog and resets state.

typescript
function closeShareDialog() {
  shareDialog.value = false
  shareAllfiles.value = false
  share_selected_collage_items.value = false
  share_items.value = []
}

collageCardDownload(file: Asset) ​

Handles download from asset card.

typescript
async function collageCardDownload(file: Asset) {
  download_items.value = [file]
  await downloadFile()
}

toggleSelectCard(file, index, payload) ​

Handles selection from asset card.

typescript
function toggleSelectCard(file: Asset, index: number, payload: any) {
  const newFile = {
    ...file,
    is_selected: !payload.fileData.is_selected,
  }
  toggleSelect(newFile, index, payload.event)
}

handleCheckboxClick(file, index, event) ​

Handles checkbox click in list view.

typescript
function handleCheckboxClick(file: any, index: number, event: any) {
  event.preventDefault()
  file.is_selected = !file.is_selected
  toggleSelect(file, index, event)
}

openAsset(asset: Asset) ​

Opens asset detail page.

typescript
function openAsset(asset: Asset) {
  if (asset?.id && !asset.is_selected) {
    trackAsset(asset)
    router.push({
      name: "brand_name-files-id",
      params: {
        brand_name: getBrandName() || "",
        id: asset.id as string,
      },
    })
  }
}

trackAsset(asset: Asset) ​

Tracks asset view analytics.

typescript
function trackAsset(asset: Asset) {
  dispatchAnalytics({
    asset_id: [Number(asset.id)],
    collection_id: route.params.id as string,
    event: "collage" as const,
    sub_event: "viewed" as const,
    environment: "dealer" as const,
  })
}

isActiveArrow(field: SortField) ​

Returns CSS class for active sort arrow.

typescript
function isActiveArrow(field: SortField): string {
  if (field) {
    if (showArrowSort[field] && page_sort_by.value === "DESC")
      return "active"
  }
  return ""
}

Utility Methods ​

  • getSrcPathRef(file) - Returns image source path
  • formatDate(date) - Formats date for display
  • toHumanlySize(size) - Converts bytes to human-readable size

User Interactions ​

  1. Collage Display

    • View collage metadata (name, description)
    • View collage assets
    • Navigate back to collage list
  2. View Modes

    • Toggle grid/list view
    • View mode preference cached
    • Smooth transitions
  3. Sorting

    • Sort by Name, Date Modified, Date Added, File Size, File Type
    • Click column headers to sort (client-side)
    • Visual sort indicators
  4. Selection

    • Single click to select
    • Shift+Click for range selection
    • Select all functionality
    • Selection count display
  5. Bulk Actions

    • Download selected assets
    • Share selected assets
    • Download entire collage
    • Share entire collage
  6. Asset Actions

    • Download single asset
    • Share single asset
    • Navigate to asset details

Real-time Features ​

  • Collage data cached for performance
  • Real-time updates via Pusher (if configured)

Styling ​

  • Uses custom CSS from assets/scss/custom.scss
  • Responsive breakpoints for mobile/tablet/desktop
  • Skeleton loaders for loading states
  • Grid/list view styling
  • Custom scrollbar styling

Watchers ​

None - uses event handlers and lifecycle hooks.

Lifecycle Hooks ​

onMounted ​

Initializes collage data and sets up event listeners.

typescript
onMounted(async () => {
  await loadCollageAssets()
  
  // Listen to download events
  mittBus.on("download-all-folder", () => {
    downldCollage.value = false
    downloadSlctd.value = false
    files.value = files.value.map((obj) => ({
      ...obj,
      is_downloading: false,
    }))
  })
  
  mittBus.on("download-file-collage", (asset_id: string | number) => {
    const found = files.value.findIndex((obj) => obj.id === asset_id)
    if (found > -1) files.value[found].is_downloading = false
  })
  
  nextTick(() => {
    contentLoading.value = false
  })
})

onBeforeUnmount ​

Cleans up event listeners.

typescript
onBeforeUnmount(() => {
  mittBus.off("download-all-folder")
  mittBus.off("download-file-collage")
})

API Endpoints ​

Get Collage Details ​

  • Endpoint: GET /collections/{id} (via getCollageDetailsApi())
  • Parameters:
    typescript
    {
      page: number,
      url_workspace_id: string,
      total_record: number
    }
  • Response:
    typescript
    {
      id: number,
      name: string,
      description: string,
      assets_count: number,
      assets_id: (string | number)[],
      assets: {
        data: Asset[],
        last_page: number,
        total: number
      }
    }

User Flow ​

  1. User navigates to collage page via URL or navigation
  2. Page fetches collage details and assets
  3. User can:
    • Toggle grid/list view
    • Sort by columns (client-side)
    • Select assets (single or multi-select with Shift)
    • Download assets (single, multiple, or entire collage)
    • Share assets (selected or entire collage)
    • Navigate to asset detail pages
    • Scroll for more assets
  4. Infinite scroll loads more assets automatically
  5. Bulk actions work on selected assets

Performance Optimizations ​

  1. Pagination: Loads assets in pages (36 per page)
  2. Duplicate Prevention: Checks existing IDs before adding new files
  3. Client-Side Sorting: Sorts loaded assets without API call
  4. Scroll Throttling: Scroll events handled efficiently
  5. View Mode Caching: User preference cached in localStorage

Notes for Development ​

  • Collage is a collection of assets that can be shared as a unit
  • Empty state shown when collage has no assets
  • Share dialog supports sharing entire collage or selected assets
  • Download supports single file, multiple files, or entire collage
  • Selection state persists across pagination
  • Client-side sorting only affects loaded assets
  • Analytics tracked for asset views and downloads