added asset manegement
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
---
|
||||
interface Props {
|
||||
mode?: 'select' | 'manage';
|
||||
}
|
||||
|
||||
const { mode = 'manage' } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="space-y-8">
|
||||
<div id="asset-alert" class="hidden p-4 rounded-lg mb-6 text-sm"></div>
|
||||
|
||||
<!-- Upload Zone -->
|
||||
<div class="glass p-6 border-dashed border-2 border-surface1 hover:border-mauve transition-colors group relative">
|
||||
<input type="file" id="zone-file-upload" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" multiple />
|
||||
<div class="text-center py-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-4 text-subtext0 group-hover:text-mauve transition-colors"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
|
||||
<p class="text-lg font-bold text-lavender">Click or drag to upload assets</p>
|
||||
<p class="text-xs text-subtext0 mt-1">Any file type up to 50MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Grid -->
|
||||
<div id="manager-assets-grid" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<!-- Assets injected here -->
|
||||
</div>
|
||||
<div id="manager-assets-empty" class="hidden text-center py-20 text-subtext0">
|
||||
No assets uploaded yet.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script is:inline define:vars={{ mode }}>
|
||||
const token = localStorage.getItem('admin_token');
|
||||
const grid = document.getElementById('manager-assets-grid');
|
||||
const empty = document.getElementById('manager-assets-empty');
|
||||
const fileInput = document.getElementById('zone-file-upload');
|
||||
const alertEl = document.getElementById('asset-alert');
|
||||
|
||||
let allAssets = [];
|
||||
|
||||
async function fetchAssets() {
|
||||
try {
|
||||
const res = await fetch('/api/uploads', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
allAssets = await res.json();
|
||||
renderAssets();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch assets", e);
|
||||
}
|
||||
}
|
||||
|
||||
function showLocalAlert(msg, type) {
|
||||
if (!alertEl) return;
|
||||
alertEl.textContent = msg;
|
||||
alertEl.className = `p-4 rounded-lg mb-6 text-sm ${type === 'success' ? 'bg-green/20 text-green border border-green/30' : 'bg-red/20 text-red border border-red/30'}`;
|
||||
alertEl.classList.remove('hidden');
|
||||
setTimeout(() => alertEl.classList.add('hidden'), 4000);
|
||||
}
|
||||
|
||||
async function uploadFiles(files) {
|
||||
let successCount = 0;
|
||||
for (const file of files) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: formData
|
||||
});
|
||||
if (res.ok) successCount++;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (successCount > 0) {
|
||||
showLocalAlert(`Successfully uploaded ${successCount} file(s).`, 'success');
|
||||
fetchAssets();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAsset(filename) {
|
||||
if (!confirm(`Delete "${filename}" permanently?`)) return;
|
||||
try {
|
||||
const res = await fetch(`/api/uploads/${encodeURIComponent(filename)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
showLocalAlert('File deleted.', 'success');
|
||||
fetchAssets();
|
||||
} else {
|
||||
showLocalAlert('Failed to delete file.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showLocalAlert('Connection error.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderAssets() {
|
||||
if (!grid || !empty) return;
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (allAssets.length === 0) {
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
allAssets.forEach(asset => {
|
||||
const div = document.createElement('div');
|
||||
div.className = "group relative aspect-square bg-crust rounded-xl overflow-hidden border border-white/5 transition-all hover:scale-105 shadow-lg flex flex-col";
|
||||
|
||||
const isImage = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(asset.name);
|
||||
|
||||
// Preview Container
|
||||
const preview = document.createElement('div');
|
||||
preview.className = "flex-1 overflow-hidden bg-surface0/20 relative cursor-pointer";
|
||||
|
||||
if (isImage) {
|
||||
const img = document.createElement('img');
|
||||
img.src = asset.url;
|
||||
img.className = "w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity";
|
||||
preview.appendChild(img);
|
||||
} else {
|
||||
preview.className += " flex flex-col items-center justify-center text-subtext0";
|
||||
preview.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
<span class="text-[8px] font-mono px-2 truncate w-full text-center">${asset.name.split('.').pop().toUpperCase()}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Action Overlays
|
||||
const actions = document.createElement('div');
|
||||
actions.className = "absolute inset-0 bg-crust/60 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2";
|
||||
|
||||
if (mode === 'select') {
|
||||
const selectBtn = document.createElement('button');
|
||||
selectBtn.className = "bg-mauve text-crust px-3 py-1 rounded-md text-xs font-bold";
|
||||
selectBtn.textContent = "Insert";
|
||||
selectBtn.onclick = () => {
|
||||
document.dispatchEvent(new CustomEvent('asset-selected', { detail: asset }));
|
||||
};
|
||||
actions.appendChild(selectBtn);
|
||||
}
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = "bg-red/80 hover:bg-red text-white p-1.5 rounded-md transition-colors";
|
||||
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>';
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
deleteAsset(asset.name);
|
||||
};
|
||||
actions.appendChild(deleteBtn);
|
||||
|
||||
preview.appendChild(actions);
|
||||
|
||||
// Label
|
||||
const label = document.createElement('div');
|
||||
label.className = "p-2 bg-crust text-[10px] truncate border-t border-white/5 text-subtext1";
|
||||
label.textContent = asset.name;
|
||||
|
||||
div.appendChild(preview);
|
||||
div.appendChild(label);
|
||||
grid.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
fileInput?.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) uploadFiles(e.target.files);
|
||||
});
|
||||
|
||||
// Initialize
|
||||
fetchAssets();
|
||||
|
||||
// Re-fetch on global upload events if needed
|
||||
document.addEventListener('assets-updated', fetchAssets);
|
||||
</script>
|
||||
Reference in New Issue
Block a user