Packages
phoenix_kit
1.5.2
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/users/media.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Media"
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="p-6">
<div class="mb-8">
<h1 class="text-3xl font-bold text-base-content">Media</h1>
<p class="text-base-content/70 mt-1">Manage user media and uploads</p>
</div>
<%!-- Upload Section --%>
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body">
<h2 class="card-title text-xl mb-4">Upload Media</h2>
<p class="text-sm text-base-content/70 mb-4">
Upload images, videos, or PDFs. Files are automatically processed and optimized.
</p>
<.file_upload
upload={@uploads.media_files}
label="Upload Media Files"
icon="hero-cloud-arrow-up"
accept_description="JPG, PNG, WebP, MP4, WebM, PDF"
max_size_description="100MB per file"
/>
</div>
</div>
<%!-- Image Preview Modal (Client-side controlled) --%>
<dialog id="image-modal" class="modal">
<div class="modal-box max-w-4xl">
<%!-- Large Image Preview --%>
<figure class="mb-6 flex items-center justify-center bg-base-300 rounded-lg p-4 relative min-h-96">
<img
id="modal-image"
src=""
alt="Image preview"
class="max-h-96 w-auto rounded"
onerror="this.style.display='none'; document.getElementById('modal-image-error').style.display='flex';"
/>
<div
id="modal-image-error"
class="hidden flex-col items-center justify-center w-full h-full absolute inset-0"
style="display: none;"
>
<.icon name="hero-photo" class="w-20 h-20 mb-4 text-error" />
<p class="text-lg font-semibold text-base-content">Image Missing or Broken</p>
<p class="text-sm text-base-content/70 mt-2">The image file could not be loaded</p>
</div>
</figure>
<%!-- File Info --%>
<h3 id="modal-filename" class="font-bold text-xl mb-2"></h3>
<div class="flex gap-3 mb-6">
<span id="modal-file-type" class="badge badge-lg"></span>
<span id="modal-file-status" class="badge badge-lg"></span>
<span id="modal-file-size" class="badge badge-lg"></span>
</div>
<%!-- Download Links --%>
<div class="mb-6">
<p class="text-sm font-semibold text-base-content mb-3">Download variants:</p>
<div id="modal-variants" class="flex gap-2 flex-wrap"></div>
</div>
<%!-- Close Button --%>
<div class="modal-action">
<button
type="button"
class="btn btn-primary"
onclick="document.getElementById('image-modal').close()"
>
Close
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<%!-- Modal Control Script --%>
<script>
const fileBadgeMap = {
'image': 'badge-info',
'video': 'badge-warning',
'pdf': 'badge-error',
'document': 'badge-ghost'
};
const statusBadgeMap = {
'active': 'badge-success',
'processing': 'badge-info',
'failed': 'badge-error'
};
// Format file size
function formatFileSize(bytes) {
if (bytes >= 1_000_000_000) return (bytes / 1_000_000_000).toFixed(2) + ' GB';
if (bytes >= 1_000_000) return (bytes / 1_000_000).toFixed(2) + ' MB';
if (bytes >= 1_000) return (bytes / 1_000).toFixed(2) + ' KB';
return bytes + ' B';
}
// Show modal with file data
function showImageModal(fileData) {
const modal = document.getElementById('image-modal');
const imageEl = document.getElementById('modal-image');
const imageErrorEl = document.getElementById('modal-image-error');
const filenameEl = document.getElementById('modal-filename');
const fileTypeEl = document.getElementById('modal-file-type');
const statusEl = document.getElementById('modal-file-status');
const sizeEl = document.getElementById('modal-file-size');
const variantsEl = document.getElementById('modal-variants');
// Reset image error state
imageEl.style.display = 'block';
imageErrorEl.style.display = 'none';
// Set image and basic info
imageEl.src = fileData.urls.original || fileData.urls.medium || '';
imageEl.alt = fileData.filename;
filenameEl.textContent = fileData.filename;
// Set badges
const badgeClass = fileBadgeMap[fileData.file_type] || 'badge-ghost';
fileTypeEl.className = `badge badge-lg ${badgeClass}`;
fileTypeEl.textContent = fileData.file_type.toUpperCase();
const statusBadgeClass = statusBadgeMap[fileData.status] || 'badge-warning';
statusEl.className = `badge badge-lg ${statusBadgeClass}`;
statusEl.textContent = fileData.status.charAt(0).toUpperCase() + fileData.status.slice(1);
sizeEl.textContent = formatFileSize(fileData.size);
// Generate download links - sorted with original first
const variantOrder = ['original', 'large', 'medium', 'small', 'thumbnail'];
const sortedEntries = Object.entries(fileData.urls).sort(([a], [b]) => {
const aIndex = variantOrder.indexOf(a);
const bIndex = variantOrder.indexOf(b);
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex);
});
variantsEl.innerHTML = sortedEntries.map(([variant, url]) => {
const isOriginal = variant === 'original';
const buttonClass = isOriginal ? 'btn btn-md btn-primary' : 'btn btn-sm btn-outline';
return `
<a
href="${url}"
target="_blank"
rel="noopener noreferrer"
class="${buttonClass}"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
${variant.charAt(0).toUpperCase() + variant.slice(1)}
</a>
`;
}).join('');
// Show modal
modal.showModal();
}
// Attach click handlers to file thumbnails
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-file-id]').forEach(el => {
el.addEventListener('click', function(e) {
e.preventDefault();
const fileData = JSON.parse(this.getAttribute('data-file-data'));
showImageModal(fileData);
});
});
});
</script>
<%!-- Uploaded Files --%>
<%= if @total_count > 0 do %>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title text-xl">Uploaded Files ({@total_count})</h2>
<.pagination_info
page={@current_page}
per_page={@per_page}
total_count={@total_count}
/>
</div>
<div class="grid grid-cols-3 md:grid-cols-5 lg:grid-cols-6 gap-3">
<%= for file <- @uploaded_files do %>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/users/media/#{file.file_id}")}
class="group relative aspect-square bg-base-300 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
data-file-id={file.file_id}
data-file-data={Jason.encode!(file)}
>
<% image_url = file.urls["medium"] || file.urls["original"] %>
<%= if image_url do %>
<img
src={image_url}
alt={file.filename}
class="w-full h-full object-cover pointer-events-none group-hover:opacity-75 transition-opacity"
onerror="this.parentElement.querySelector('.image-error-placeholder').style.display='grid';"
/>
<div class="image-error-placeholder hidden absolute inset-0 bg-base-300 grid place-items-center text-base-content/50">
<div class="flex flex-col items-center justify-center">
<.icon name="hero-photo" class="w-12 h-12 mb-2 text-error" />
<p class="text-xs font-semibold">Image Missing</p>
</div>
</div>
<% else %>
<div class="flex flex-col items-center justify-center w-full h-full text-base-content/50 group-hover:bg-base-200 transition-colors">
<.icon name={file_icon(file.file_type)} class="w-12 h-12 mb-2" />
<p class="text-sm font-semibold">{String.upcase(file.file_type)}</p>
</div>
<% end %>
<%!-- File size in corner --%>
<div class="absolute bottom-2 right-2 bg-black/60 text-white text-xs px-2 py-1 rounded pointer-events-none">
{format_file_size(file.size)}
</div>
</.link>
<% end %>
</div>
<%!-- Pagination Controls --%>
<%= if @total_count > @per_page do %>
<.pagination
current_page={@current_page}
total_pages={@total_pages}
base_path={@url_path}
/>
<% end %>
</div>
</div>
<% else %>
<div class="bg-base-200 rounded-lg p-8 text-center">
<p class="text-base-content/70">No media uploaded yet. Upload your first file above!</p>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>