- Added standard Laravel directory structure and configuration. - Included Svelte and Tailwind configuration for the admin interface. - Added core PHPUnit and testing scripts.
272 lines
11 KiB
Svelte
272 lines
11 KiB
Svelte
<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>
|