cms/resources/js/components/admin/Navigation.svelte

272 lines
11 KiB
Svelte
Raw Normal View History

<script>
/**
* Navigation Management Component
*
* This component provides the UI for managing the CMS navigation items.
* It allows adding, reordering, and deleting navigation links, including
* both custom URLs and internal pages.
*/
import { onMount } from 'svelte';
let { items = [], pages = [], parentItems = [], adminPath = '/loom' } = $props();
let navigationItems = $state([]);
$effect(() => {
navigationItems = items;
});
let newLabel = $state('');
let newUrl = $state('');
let newPageId = $state('');
let newParentId = $state('');
let newTarget = $state('_self');
let isSubmitting = $state(false);
// Filtered page list for the selector
let availablePages = $derived(pages);
/**
* Add a new navigation item
*/
async function addItem() {
if (!newLabel || (!newUrl && !newPageId)) {
alert('Please provide a label and either a URL or a Page.');
return;
}
isSubmitting = true;
const formData = new FormData();
formData.append('label', newLabel);
formData.append('target', newTarget);
if (newUrl) formData.append('url', newUrl);
if (newPageId) formData.append('page_id', newPageId);
if (newParentId) formData.append('parent_id', newParentId);
// Get CSRF token from meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
try {
const response = await fetch(`${adminPath}/navigation`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json'
},
body: formData
});
if (response.ok) {
// Refresh items from backend or reload page
window.location.reload();
} else {
const data = await response.json();
alert('Error: ' + (data.message || 'Could not add item.'));
}
} catch (e) {
console.error(e);
alert('A network error occurred.');
} finally {
isSubmitting = false;
}
}
/**
* Delete a navigation item
*/
async function deleteItem(id) {
if (!confirm('Are you sure you want to delete this navigation item?')) return;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
try {
const response = await fetch(`${adminPath}/navigation/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json'
}
});
if (response.ok) {
window.location.reload();
} else {
alert('Could not delete item.');
}
} catch (e) {
console.error(e);
alert('A network error occurred.');
}
}
/**
* Handle item reordering (Simple implementation)
*/
async function moveItem(index, direction) {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= navigationItems.length) return;
const items = [...navigationItems];
const temp = items[index];
items[index] = items[newIndex];
items[newIndex] = temp;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
try {
const response = await fetch(`${adminPath}/navigation/reorder`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
items: items.map((item, idx) => ({ id: item.id, order: idx }))
})
});
if (response.ok) {
navigationItems = items;
} else {
alert('Could not reorder items.');
}
} catch (e) {
console.error(e);
}
}
</script>
<div class="ui container">
<h2 class="ui header">
<i class="sitemap icon"></i>
<div class="content">
Navigation Management
<div class="sub header">Customize your site's navigation menus.</div>
</div>
</h2>
<div class="ui grid">
<div class="ten wide column">
<div class="ui segment">
<h3 class="ui dividing header">Current Menu Items</h3>
{#if navigationItems.length === 0}
<div class="ui placeholder segment">
<div class="ui icon header">
<i class="sitemap icon"></i>
No navigation items found.
</div>
</div>
{:else}
<div class="ui list">
{#each navigationItems as item, i}
<div class="item" style="padding: 10px; border-bottom: 1px solid #eee;">
<div class="right floated content">
<div class="ui small basic icon buttons">
<button class="ui button" onclick={() => moveItem(i, -1)} disabled={i === 0} title="Move Up" aria-label="Move Up">
<i class="up arrow icon"></i>
</button>
<button class="ui button" onclick={() => moveItem(i, 1)} disabled={i === navigationItems.length - 1} title="Move Down" aria-label="Move Down">
<i class="down arrow icon"></i>
</button>
<button class="ui red button" onclick={() => deleteItem(item.id)} title="Delete" aria-label="Delete">
<i class="trash icon"></i>
</button>
</div>
</div>
<i class="large linkify middle aligned icon"></i>
<div class="content">
<div class="header">{item.label}</div>
<div class="description">
{#if item.page_id}
<span class="ui blue tiny label">Page</span> /{item.page?.slug}
{:else}
<span class="ui tiny label">Custom URL</span> {item.url}
{/if}
</div>
</div>
</div>
{#if item.children && item.children.length > 0}
<div class="ui list" style="margin-left: 40px; border-left: 2px solid #eee;">
{#each item.children as child, j}
<div class="item" style="padding: 10px; border-bottom: 1px solid #f9f9f9;">
<div class="right floated content">
<div class="ui small basic icon buttons">
<button class="ui red button" onclick={() => deleteItem(child.id)} title="Delete" aria-label="Delete">
<i class="trash icon"></i>
</button>
</div>
</div>
<i class="linkify middle aligned icon"></i>
<div class="content">
<div class="header">{child.label}</div>
<div class="description">
{#if child.page_id}
<span class="ui blue tiny label">Page</span> /{child.page?.slug}
{:else}
<span class="ui tiny label">Custom URL</span> {child.url}
{/if}
</div>
</div>
</div>
{/each}
</div>
{/if}
{/each}
</div>
{/if}
</div>
</div>
<div class="six wide column">
<div class="ui segment">
<h3 class="ui dividing header">Add New Item</h3>
<div class="ui form">
<div class="field">
<label for="nav-label">Label</label>
<input type="text" id="nav-label" bind:value={newLabel} placeholder="e.g. Home, Contact Us">
</div>
<div class="field">
<label for="nav-page">Link to Page</label>
<select id="nav-page" bind:value={newPageId} onchange={() => { if(newPageId) newUrl = ''; }}>
<option value="">Select a Page (Optional)</option>
{#each availablePages as page}
<option value={page.id}>{page.title}</option>
{/each}
</select>
</div>
<div class="field">
<label for="nav-url">Or Custom URL</label>
<input type="text" id="nav-url" bind:value={newUrl} placeholder="e.g. /custom-page or https://..." oninput={() => { if(newUrl) newPageId = ''; }}>
</div>
<div class="field">
<label for="nav-parent">Parent Item (For Dropdowns)</label>
<select id="nav-parent" bind:value={newParentId}>
<option value="">No Parent (Top Level)</option>
{#each parentItems as parent}
<option value={parent.id}>{parent.label}</option>
{/each}
</select>
</div>
<div class="field">
<label for="nav-target">Target</label>
<select id="nav-target" bind:value={newTarget}>
<option value="_self">Same Window</option>
<option value="_blank">New Window</option>
</select>
</div>
<button class="ui primary fluid button" onclick={addItem} disabled={isSubmitting || !newLabel || (!newUrl && !newPageId)}>
<i class="plus icon"></i> Add to Menu
</button>
</div>
</div>
</div>
</div>
</div>