Appearance
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 ​
Collage Display
- Collage metadata (name, description)
- Asset list in grid or list view
- Empty state handling
- Back navigation to collage list
View Modes
- Grid view with mosaic layout
- List view with sortable columns
- View mode preference caching
- Smooth transitions between views
Sorting
- Sort by Name, Date Modified, Date Added, File Size, File Type
- Ascending/descending toggle
- Visual sort indicators
- Client-side sorting for loaded assets
Selection & Bulk Actions
- Single and multi-select (Shift+Click support)
- Select all functionality
- Bulk download
- Bulk share
- Selection count display
Collage Actions
- Share entire collage
- Download entire collage
- Share selected assets
- Download selected assets
Infinite Scroll Pagination
- Automatic loading on scroll
- Skeleton loaders
- Scroll position management
Components Used ​
AssetCard- Asset card for grid viewShareAssetDialog- Share dialog componentAsyncIcon- Async icon componentv-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 storeuseSnackbar()- Snackbar notificationsuseFileType()- File type utilitiesuseSelection()- 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:
- Checks pagination limits
- Increments page number
- Fetches collage details
- Updates asset list
- Appends new files (avoiding duplicates)
- 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 pathformatDate(date)- Formats date for displaytoHumanlySize(size)- Converts bytes to human-readable size
User Interactions ​
Collage Display
- View collage metadata (name, description)
- View collage assets
- Navigate back to collage list
View Modes
- Toggle grid/list view
- View mode preference cached
- Smooth transitions
Sorting
- Sort by Name, Date Modified, Date Added, File Size, File Type
- Click column headers to sort (client-side)
- Visual sort indicators
Selection
- Single click to select
- Shift+Click for range selection
- Select all functionality
- Selection count display
Bulk Actions
- Download selected assets
- Share selected assets
- Download entire collage
- Share entire collage
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}(viagetCollageDetailsApi()) - 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 ​
- User navigates to collage page via URL or navigation
- Page fetches collage details and assets
- 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
- Infinite scroll loads more assets automatically
- Bulk actions work on selected assets
Performance Optimizations ​
- Pagination: Loads assets in pages (36 per page)
- Duplicate Prevention: Checks existing IDs before adding new files
- Client-Side Sorting: Sorts loaded assets without API call
- Scroll Throttling: Scroll events handled efficiently
- 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
Related Documentation ​
- useCollageDetails Composable - Collage details composable
- Collection API - Collection API endpoints
- Download Store - Download state management
- ShareAssetDialog Component - Share dialog component
- AssetCard Component - Asset card component