Packages
phoenix_kit
1.7.95
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}
>
<script>
(function() {
var m = localStorage.getItem("phoenix_kit_media_view_mode");
if (m) document.documentElement.dataset.mediaView = m;
})();
</script>
<style>
[data-media-view="list"] [data-media-grid] { display: none !important; }
[data-media-view="grid"] [data-media-list] { display: none !important; }
@keyframes rename-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.renaming-preview {
animation: rename-pulse 1.5s ease-in-out infinite;
color: oklch(var(--p));
font-style: italic;
}
</style>
<div class="container flex-col mx-auto px-4 py-6">
<.admin_page_header
back={PhoenixKit.Utils.Routes.path("/admin")}
title="Media"
subtitle="Manage user media and uploads"
/>
<%!-- Upload Section --%>
<%= if @show_upload do %>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center gap-3 mb-4">
<h2 class="card-title text-xl">Upload Media</h2>
<button phx-click="toggle_upload" class="btn btn-sm btn-error gap-2">
<.icon name="hero-x-mark" class="w-4 h-4" /> Cancel
</button>
</div>
<%= if @has_buckets do %>
<.file_upload
upload={@uploads.media_files}
label="Upload Media Files"
icon="hero-cloud-arrow-up"
accept_description="All file types supported"
max_size_description={"#{@max_upload_size_mb}MB per file"}
/>
<% else %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-6 h-6" />
<div>
<h3 class="font-bold">No Storage Buckets Configured</h3>
<div class="text-sm mt-1">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media")}
class="link link-primary font-semibold"
>
Configure Storage Buckets
</.link>
</div>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<%!-- Main Layout: Sidebar + Content --%>
<div class="flex gap-0">
<%!-- Folder Sidebar --%>
<div
class="hidden lg:block shrink-0"
style={if !@sidebar_collapsed, do: "width: 240px; max-width: 240px;"}
>
<%= if @sidebar_collapsed do %>
<%!-- Collapsed strip --%>
<div class="sticky top-4 w-10">
<button
phx-click="toggle_sidebar"
class="btn btn-ghost btn-sm w-full"
title="Show folders"
>
<.icon name="hero-chevron-right" class="w-4 h-4" />
</button>
</div>
<% else %>
<%!-- Expanded sidebar --%>
<div
class="sticky top-4 border-r border-base-200 pr-3 mr-3 overflow-hidden"
style="width: 240px; max-width: 240px;"
>
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-sm text-base-content/70 uppercase tracking-wider">
Folders
</h3>
<div class="flex gap-0.5">
<button
phx-click="toggle_new_folder"
class="btn btn-ghost btn-xs"
title="New folder"
>
<.icon name="hero-folder-plus" class="w-4 h-4" />
</button>
<button
phx-click="toggle_sidebar"
class="btn btn-ghost btn-xs"
title="Collapse sidebar"
>
<.icon name="hero-chevron-left" class="w-4 h-4" />
</button>
</div>
</div>
<%!-- All Files --%>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/media")}
data-drop-folder="root"
class={[
"flex items-center gap-2 px-2 py-1.5 rounded-lg text-sm transition-colors mb-1",
if(@current_folder == nil,
do: "bg-primary/10 font-semibold text-primary",
else: "hover:bg-base-200"
)
]}
>
<.icon name="hero-inbox" class="w-4 h-4 shrink-0" /> All Files
</.link>
<div class="divider my-1 h-0"></div>
<%!-- Folder Tree --%>
<ul class="space-y-0.5 w-full overflow-hidden">
<%= for node <- @folder_tree do %>
<.folder_tree_node
node={node}
current_folder={@current_folder}
expanded_folders={@expanded_folders}
renaming_folder={@renaming_folder}
renaming_source={@renaming_source}
renaming_text={@renaming_text}
show_new_folder={@show_new_folder}
depth={0}
/>
<% end %>
<%= if @show_new_folder && @current_folder == nil do %>
<li>
<form
phx-submit="create_folder"
class="flex items-center gap-1 px-1 py-1"
>
<span class="text-warning">
<.icon name="hero-folder-plus" class="w-4 h-4" />
</span>
<input
type="text"
name="name"
placeholder="Folder name"
class="input input-bordered input-xs flex-1 min-w-0"
phx-mounted={JS.focus()}
required
phx-keydown="toggle_new_folder"
phx-key="Escape"
/>
</form>
</li>
<% end %>
</ul>
<%= if @folder_tree == [] && !@show_new_folder do %>
<p class="text-xs text-base-content/40 text-center py-4">No folders yet</p>
<% end %>
</div>
<% end %>
</div>
<%!-- Content Area --%>
<div
class="flex-1 min-w-0"
id="media-drag-drop"
phx-hook="MediaDragDrop"
>
<%!-- Breadcrumbs (mobile + desktop) --%>
<%= if @breadcrumbs != [] do %>
<div class="text-sm breadcrumbs mb-4">
<ul>
<li>
<.link navigate={PhoenixKit.Utils.Routes.path("/admin/media")} class="gap-1">
<.icon name="hero-home" class="w-4 h-4" /> Root
</.link>
</li>
<%= for folder <- @breadcrumbs do %>
<li>
<.link navigate={
PhoenixKit.Utils.Routes.path("/admin/media?folder=#{folder.uuid}")
}>
{folder.name}
</.link>
</li>
<% end %>
</ul>
</div>
<% end %>
<%!-- Search Bar --%>
<form phx-change="search" phx-submit="search" class="mb-4">
<label class="input input-bordered input-sm w-full flex items-center gap-2">
<.icon name="hero-magnifying-glass" class="w-4 h-4 text-base-content/40" />
<input
type="text"
name="q"
value={@search_query}
placeholder="Search files..."
class="grow bg-transparent border-none outline-none focus:outline-none"
phx-debounce="300"
autocomplete="off"
/>
<%= if @search_query != "" do %>
<button
type="button"
phx-click="clear_search"
class="btn btn-ghost btn-xs btn-circle"
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
<% end %>
</label>
</form>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<%!-- Header --%>
<div class="flex justify-between items-center mb-4">
<%= if @select_mode do %>
<%!-- Selection Mode Header --%>
<div class="flex items-center gap-2 flex-wrap">
<button phx-click="toggle_select_mode" class="btn btn-sm btn-error gap-2">
<.icon name="hero-x-mark" class="w-4 h-4" /> Cancel
</button>
<span class="text-sm font-semibold">
{MapSet.size(@selected_files) + MapSet.size(@selected_folders)} selected
</span>
<button phx-click="show_move_modal" class="btn btn-sm btn-primary gap-1">
<.icon name="hero-folder-arrow-down" class="w-4 h-4" /> Move
</button>
<%= if MapSet.size(@selected_folders) > 0 do %>
<button
phx-click="delete_selected"
data-confirm={"Delete #{MapSet.size(@selected_folders)} folder(s)? Files will be moved to parent."}
class="btn btn-sm btn-error gap-1"
>
<.icon name="hero-trash" class="w-4 h-4" /> Delete folders
</button>
<% end %>
<button phx-click="select_all" class="btn btn-sm btn-ghost">Select all</button>
<button phx-click="deselect_all" class="btn btn-sm btn-ghost">Clear</button>
</div>
<% else %>
<%!-- Normal Header --%>
<div class="flex items-center gap-3 flex-wrap">
<h2 class="card-title text-xl">
<%= if @filter_orphaned do %>
Orphaned Files ({@total_count})
<% else %>
<%= if @current_folder do %>
{@current_folder.name} ({@total_count})
<% else %>
All Files ({@total_count})
<% end %>
<% end %>
</h2>
<%= unless @show_upload do %>
<button phx-click="toggle_upload" class="btn btn-sm btn-primary gap-2">
<.icon name="hero-plus" class="w-4 h-4" /> Add Media
</button>
<% end %>
<button
phx-click="toggle_select_mode"
class="btn btn-sm btn-secondary btn-outline gap-2"
phx-disable-with="Select"
>
<.icon name="hero-check-circle" class="w-4 h-4" /> Select
</button>
<button
phx-click="toggle_new_folder"
class="btn btn-sm btn-ghost gap-2 lg:hidden"
>
<.icon name="hero-folder-plus" class="w-4 h-4" /> New Folder
</button>
<button
phx-click="toggle_orphan_filter"
class={[
"btn btn-sm gap-2",
if(@filter_orphaned, do: "btn-warning", else: "btn-ghost")
]}
>
<.icon name="hero-funnel" class="w-4 h-4" />
<%= if @filter_orphaned do %>
Showing orphaned ({@orphaned_count})
<% else %>
Find orphaned ({@orphaned_count})
<% end %>
</button>
<%= if @filter_orphaned && @total_count > 0 do %>
<button
phx-click="delete_all_orphaned"
data-confirm={"Delete all #{@total_count} orphaned files?"}
class="btn btn-sm btn-error gap-2"
>
<.icon name="hero-trash" class="w-4 h-4" /> Delete all orphaned
</button>
<% end %>
</div>
<% end %>
<div class="flex items-center gap-2">
<%= if @total_count > 0 do %>
<div class="join">
<button
phx-click="set_view_mode"
phx-value-mode="grid"
data-view-mode="grid"
class={["btn btn-xs join-item", if(@view_mode == "grid", do: "btn-active")]}
title="Grid view"
>
<.icon name="hero-squares-2x2" class="w-3 h-3" />
</button>
<button
phx-click="set_view_mode"
phx-value-mode="list"
data-view-mode="list"
class={["btn btn-xs join-item", if(@view_mode == "list", do: "btn-active")]}
title="List view"
>
<.icon name="hero-bars-3-bottom-left" class="w-3 h-3" />
</button>
</div>
<.pagination_info
page={@current_page}
per_page={@per_page}
total_count={@total_count}
/>
<% end %>
</div>
</div>
<%= if @uploaded_files == [] && @folders == [] do %>
<%!-- Empty state --%>
<div class="bg-base-200 rounded-lg p-8 text-center">
<.icon
name="hero-folder-open"
class="w-12 h-12 mx-auto text-base-content/30 mb-2"
/>
<p class="text-base-content/70">
<%= if @current_folder do %>
This folder is empty.
<% else %>
No media uploaded yet. Upload your first file above!
<% end %>
</p>
</div>
<% else %>
<%= if @view_mode == "grid" do %>
<%!-- Grid View --%>
<div
data-media-grid
class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3"
>
<%!-- New Folder Input (Grid) --%>
<%= if @show_new_folder do %>
<div class="flex flex-col items-center justify-center aspect-square bg-base-200 rounded-lg border-2 border-dashed border-primary/30">
<span class="text-warning mb-2">
<.icon name="hero-folder-plus" class="w-10 h-10" />
</span>
<form phx-submit="create_folder" class="w-full px-3">
<input
type="text"
name="name"
placeholder="Folder name"
class="input input-bordered input-xs w-full text-center"
phx-mounted={JS.focus()}
required
phx-keydown="toggle_new_folder"
phx-key="Escape"
/>
</form>
</div>
<% end %>
<%!-- Folders --%>
<%= for folder <- @folders do %>
<div
data-drop-folder={folder.uuid}
class={[
"group relative flex flex-col items-center justify-center aspect-square bg-base-200 rounded-lg cursor-pointer hover:bg-base-300 hover:shadow-lg transition-colors",
@select_mode && MapSet.member?(@selected_folders, folder.uuid) &&
"ring-2 ring-primary"
]}
style={folder_bg_style(folder.color)}
phx-click={if @select_mode, do: "toggle_select_folder"}
phx-value-folder-uuid={if @select_mode, do: folder.uuid}
>
<%= if @select_mode do %>
<label class="absolute top-1 left-1 z-10 cursor-pointer">
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-sm"
checked={MapSet.member?(@selected_folders, folder.uuid)}
phx-click="toggle_select_folder"
phx-value-folder-uuid={folder.uuid}
/>
</label>
<% end %>
<%= if @renaming_folder == folder.uuid && @renaming_source == "content" do %>
<%!-- Inline rename --%>
<span style={folder_icon_style(folder.color)}>
<.icon name="hero-folder" class="w-12 h-12 mb-2" />
</span>
<form
phx-submit="rename_folder"
phx-change="rename_folder_input"
class="flex items-center gap-1 px-2 w-full"
>
<input type="hidden" name="folder_uuid" value={folder.uuid} />
<input
type="text"
name="name"
value={@renaming_text}
class="input input-bordered input-xs w-full text-center"
phx-mounted={JS.focus()}
required
phx-keydown="cancel_rename_folder"
phx-key="Escape"
phx-debounce="50"
/>
</form>
<% else %>
<.link
navigate={
PhoenixKit.Utils.Routes.path("/admin/media?folder=#{folder.uuid}")
}
class="flex flex-col items-center overflow-hidden w-full"
>
<span style={folder_icon_style(folder.color)}>
<.icon name="hero-folder" class="w-12 h-12 mb-1" />
</span>
<p class={[
"text-xs font-semibold text-center px-2 truncate w-full",
@renaming_folder == folder.uuid && @renaming_source != "content" &&
"renaming-preview"
]}>
<%= if @renaming_folder == folder.uuid && @renaming_text != "" do %>
{@renaming_text}
<% else %>
{folder.name}
<% end %>
</p>
</.link>
<div class="dropdown dropdown-end absolute top-1 right-1 opacity-0 group-hover:opacity-100">
<label tabindex="0" class="btn btn-ghost btn-xs p-0 min-h-0 h-5 w-5">
<.icon name="hero-ellipsis-vertical" class="w-4 h-4" />
</label>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box shadow-lg z-20 w-36 p-1"
>
<li>
<button
phx-click="start_rename_folder"
phx-value-folder-uuid={folder.uuid}
>
<.icon name="hero-pencil" class="w-4 h-4" /> Rename
</button>
</li>
<li>
<div class="px-3 py-1.5">
<span class="text-xs font-semibold text-base-content/50 mb-1 block">
Color
</span>
<div class="flex flex-wrap gap-1">
<%= for color <- PhoenixKit.Modules.Storage.Folder.colors() do %>
<button
phx-click="change_folder_color"
phx-value-folder-uuid={folder.uuid}
phx-value-color={color}
class={[
"w-4 h-4 rounded-full border-2 transition-transform hover:scale-125",
if(
folder.color == color ||
(folder.color == nil && color == "default"),
do: "border-base-content scale-110",
else: "border-transparent"
)
]}
style={"background-color: #{folder_color_hex(color) || "oklch(var(--wa))"}"}
title={color}
>
</button>
<% end %>
</div>
</div>
</li>
<li>
<button
phx-click="delete_folder"
phx-value-id={folder.uuid}
data-confirm={"Delete '#{folder.name}'? Files move to parent."}
class="text-error"
>
<.icon name="hero-trash" class="w-4 h-4" /> Delete
</button>
</li>
</ul>
</div>
<% end %>
</div>
<% end %>
<%= for file <- @uploaded_files do %>
<div
data-draggable-file={file.file_uuid}
class={[
"group relative aspect-square bg-base-300 rounded-lg overflow-hidden hover:shadow-lg transition-shadow",
@select_mode && MapSet.member?(@selected_files, file.file_uuid) &&
"ring-2 ring-primary"
]}
>
<%= if @select_mode do %>
<label class="absolute top-1 left-1 z-10 cursor-pointer">
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-sm"
checked={MapSet.member?(@selected_files, file.file_uuid)}
phx-click="toggle_select"
phx-value-file-uuid={file.file_uuid}
/>
</label>
<% end %>
<div
phx-click="click_file"
phx-value-file-uuid={file.file_uuid}
class="block w-full h-full cursor-pointer"
>
<.thumbnail_url :let={url} file={file} size={:small}>
<%= if url do %>
<img
src={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="grid place-items-center w-full aspect-square text-base-content/50 group-hover:bg-base-200 transition-colors">
<div class="flex flex-col items-center">
<.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>
</div>
<% end %>
</.thumbnail_url>
<%= if file.file_type == "video" do %>
<div class="absolute top-2 left-2 bg-black/60 text-white p-1.5 rounded pointer-events-none flex items-center justify-center">
<.icon name="hero-play-solid" class="w-4 h-4" />
</div>
<% end %>
<%= if file.mime_type == "application/pdf" do %>
<div class="absolute top-2 left-2 bg-error/80 text-white px-2 py-0.5 rounded text-xs font-bold pointer-events-none">
PDF
</div>
<% end %>
<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>
</div>
</div>
<% end %>
</div>
<% end %>
<%= if @view_mode == "list" do %>
<%!-- List View --%>
<div
data-media-list
class="overflow-x-auto"
>
<table class="table table-sm">
<thead>
<tr>
<%= if @select_mode do %>
<th class="w-8"></th>
<% end %>
<th class="w-12"></th>
<th>Name</th>
<th>Path</th>
<th>Type</th>
<th>Size</th>
<th>Date</th>
<th class="w-10"></th>
</tr>
</thead>
<tbody>
<%!-- New Folder Input (List) --%>
<%= if @show_new_folder do %>
<tr class="bg-base-200">
<%= if @select_mode do %>
<td></td>
<% end %>
<td>
<div class="w-10 h-10 grid place-items-center">
<span class="text-warning">
<.icon name="hero-folder-plus" class="w-6 h-6" />
</span>
</div>
</td>
<td colspan="5">
<form phx-submit="create_folder" class="flex items-center gap-2">
<input
type="text"
name="name"
placeholder="Folder name"
class="input input-bordered input-sm flex-1"
phx-mounted={JS.focus()}
required
phx-keydown="toggle_new_folder"
phx-key="Escape"
/>
<button type="submit" class="btn btn-sm btn-primary">
<.icon name="hero-check" class="w-4 h-4" />
</button>
<button
type="button"
phx-click="toggle_new_folder"
class="btn btn-sm btn-ghost"
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</form>
</td>
<td></td>
</tr>
<% end %>
<%!-- Folders --%>
<%= for folder <- @folders do %>
<%= if @renaming_folder == folder.uuid && @renaming_source == "content" do %>
<tr style={
folder_bg_style(folder.color) || "background-color: oklch(var(--b2))"
}>
<%= if @select_mode do %>
<td></td>
<% end %>
<td>
<div class="w-10 h-10 grid place-items-center">
<span style={folder_icon_style(folder.color)}>
<.icon name="hero-folder" class="w-6 h-6" />
</span>
</div>
</td>
<td colspan="5">
<form
phx-submit="rename_folder"
phx-change="rename_folder_input"
class="flex items-center gap-2"
>
<input type="hidden" name="folder_uuid" value={folder.uuid} />
<input
type="text"
name="name"
value={@renaming_text}
class="input input-bordered input-sm flex-1"
phx-mounted={JS.focus()}
required
phx-keydown="cancel_rename_folder"
phx-key="Escape"
phx-debounce="50"
/>
<button type="submit" class="btn btn-sm btn-ghost">
<.icon name="hero-check" class="w-4 h-4 text-success" />
</button>
<button
type="button"
phx-click="cancel_rename_folder"
class="btn btn-sm btn-ghost"
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</form>
</td>
<td></td>
</tr>
<% else %>
<tr
phx-click={
if @select_mode, do: "toggle_select_folder", else: "navigate_folder"
}
phx-value-folder-uuid={folder.uuid}
data-drop-folder={folder.uuid}
class={[
"hover cursor-pointer",
@select_mode && MapSet.member?(@selected_folders, folder.uuid) &&
"bg-primary/10"
]}
style={folder_bg_style(folder.color)}
>
<%= if @select_mode do %>
<td>
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-xs"
checked={MapSet.member?(@selected_folders, folder.uuid)}
phx-click="toggle_select_folder"
phx-value-folder-uuid={folder.uuid}
/>
</td>
<% end %>
<td>
<div class="w-10 h-10 grid place-items-center">
<span style={folder_icon_style(folder.color)}>
<.icon name="hero-folder" class="w-6 h-6" />
</span>
</div>
</td>
<td colspan="5">
<span class={[
"font-medium text-sm",
@renaming_folder == folder.uuid && @renaming_source != "content" &&
"renaming-preview"
]}>
<%= if @renaming_folder == folder.uuid && @renaming_text != "" do %>
{@renaming_text}
<% else %>
{folder.name}
<% end %>
</span>
</td>
<td></td>
<td>
<div class="dropdown dropdown-end" phx-click="noop">
<label tabindex="0" class="btn btn-ghost btn-xs">
<.icon name="hero-ellipsis-vertical" class="w-4 h-4" />
</label>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box shadow-lg z-20 w-36 p-1"
>
<li>
<button
phx-click="start_rename_folder"
phx-value-folder-uuid={folder.uuid}
>
<.icon name="hero-pencil" class="w-4 h-4" /> Rename
</button>
</li>
<li>
<div class="px-3 py-1.5">
<span class="text-xs font-semibold text-base-content/50 mb-1 block">
Color
</span>
<div class="flex flex-wrap gap-1">
<%= for color <- PhoenixKit.Modules.Storage.Folder.colors() do %>
<button
phx-click="change_folder_color"
phx-value-folder-uuid={folder.uuid}
phx-value-color={color}
class={[
"w-4 h-4 rounded-full border-2 transition-transform hover:scale-125",
if(
folder.color == color ||
(folder.color == nil && color == "default"),
do: "border-base-content scale-110",
else: "border-transparent"
)
]}
style={"background-color: #{folder_color_hex(color) || "oklch(var(--wa))"}"}
title={color}
>
</button>
<% end %>
</div>
</div>
</li>
<li>
<button
phx-click="delete_folder"
phx-value-id={folder.uuid}
data-confirm={"Delete '#{folder.name}'? Files move to parent."}
class="text-error"
>
<.icon name="hero-trash" class="w-4 h-4" /> Delete
</button>
</li>
</ul>
</div>
</td>
</tr>
<% end %>
<% end %>
<%!-- Files --%>
<%= for file <- @uploaded_files do %>
<tr
data-draggable-file={file.file_uuid}
phx-click="click_file"
phx-value-file-uuid={file.file_uuid}
class={[
"hover cursor-pointer",
@select_mode &&
MapSet.member?(@selected_files, file.file_uuid) && "bg-primary/10"
]}
>
<%= if @select_mode do %>
<td>
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-xs"
checked={MapSet.member?(@selected_files, file.file_uuid)}
phx-click="toggle_select"
phx-value-file-uuid={file.file_uuid}
/>
</td>
<% end %>
<td>
<div class="w-10 h-10 rounded overflow-hidden bg-base-300 shrink-0">
<.thumbnail_url :let={url} file={file} size={:small}>
<%= if url do %>
<img
src={url}
class="w-full h-full object-cover"
alt={file.filename}
/>
<% else %>
<div class="grid place-items-center w-full h-full text-base-content/40">
<.icon
name={file_icon(file.file_type)}
class="w-5 h-5"
/>
</div>
<% end %>
</.thumbnail_url>
</div>
</td>
<td>
<span class="font-medium text-sm">{file.filename}</span>
</td>
<td>
<%= if file.folder_path do %>
<span class="text-xs text-base-content/50">{file.folder_path}</span>
<% else %>
<span class="text-xs text-base-content/30">/</span>
<% end %>
</td>
<td>
<span class="badge badge-ghost badge-sm h-auto">
{String.upcase(file.file_type)}
</span>
</td>
<td class="text-sm text-base-content/70">
{format_file_size(file.size)}
</td>
<td class="text-sm text-base-content/70">
{Calendar.strftime(file.inserted_at, "%b %d, %Y")}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<% end %>
<%!-- Pagination --%>
<%= if @total_count > @per_page do %>
<.pagination
current_page={@current_page}
total_pages={@total_pages}
base_path={
if @current_folder,
do: @url_path <> "?folder=#{@current_folder.uuid}",
else: @url_path
}
/>
<% end %>
</div>
</div>
</div>
</div>
</div>
<%!-- Move to Folder Modal --%>
<%= if @show_move_modal do %>
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">
Move {MapSet.size(@selected_files)} file(s) to folder
</h3>
<ul class="menu bg-base-200 rounded-lg max-h-64 overflow-y-auto">
<li>
<button
phx-click="move_selected_to_folder"
phx-value-folder_uuid=""
class={if @current_folder == nil, do: "active", else: ""}
>
<.icon name="hero-home" class="w-4 h-4" /> Root (no folder)
</button>
</li>
<%= for node <- @folder_tree do %>
<.move_folder_option node={node} depth={0} />
<% end %>
</ul>
<div class="modal-action">
<button phx-click="close_move_modal" class="btn btn-ghost">Cancel</button>
</div>
</div>
<div class="modal-backdrop" phx-click="close_move_modal"></div>
</div>
<% end %>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>