Implement ID-based routing and folder auto-generation from titles
Major features:
- Switch from slug-based to ID-based routing (/documents/123)
- Enable title editing with automatic slug/path regeneration
- Auto-generate folder structure from title slashes (e.g., Laravel/Livewire/Components)
- Persist sidebar folder open/close state using localStorage
- Remove slug unique constraint (ID routing makes it unnecessary)
- Implement recursive tree view with multi-level folder support
Architecture changes:
- DocumentService: Add generatePathAndSlug() for title-based path generation
- Routes: Change from {document:slug} to {document} for ID binding
- SidebarTree: Extract recursive rendering to partials/tree-item.blade.php
- Database: Remove unique constraint from documents.slug column
UI improvements:
- Display only last path component in sidebar (Components vs Laravel/Livewire/Components)
- Folder state persists across page navigation via localStorage
- Title field accepts slashes for folder organization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
163
src/resources/views/layouts/knowledge-base.blade.php
Normal file
163
src/resources/views/layouts/knowledge-base.blade.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $title ?? config('app.name', 'Knowledge Base') }}</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
<script>document.addEventListener('livewire:navigated', function() { hljs.highlightAll(); });</script>
|
||||
<style>
|
||||
pre code.hljs {
|
||||
background: #1e1e1e !important; /* VSCode dark と同じ */
|
||||
color: #dcdcdc !important;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Header -->
|
||||
<header class="bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-xl font-semibold text-gray-900">
|
||||
{{ config('app.name', 'Knowledge Base') }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Quick Switcher Trigger -->
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
@click.prevent="console.log('Button clicked'); $dispatch('open-quick-switcher')"
|
||||
>
|
||||
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
Quick Switch
|
||||
<kbd class="ml-2 px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded">
|
||||
Ctrl+K
|
||||
</kbd>
|
||||
</button>
|
||||
|
||||
@auth
|
||||
<!-- User Dropdown -->
|
||||
<div x-data="{ open: false }" @click.away="open = false" class="relative">
|
||||
<button
|
||||
@click="open = !open"
|
||||
class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none"
|
||||
>
|
||||
{{ Auth::user()->name }}
|
||||
<svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition
|
||||
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
Profile
|
||||
</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="w-full text-left block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900">
|
||||
Login
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex h-[calc(100vh-4rem)]">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 overflow-y-auto">
|
||||
@livewire('sidebar-tree')
|
||||
</aside>
|
||||
|
||||
<!-- Main Panel -->
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Switcher Modal -->
|
||||
@livewire('quick-switcher')
|
||||
|
||||
@livewireScripts
|
||||
|
||||
<!-- Global Keyboard Shortcuts -->
|
||||
<script>
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Ctrl+K or Cmd+K for quick switcher
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
window.dispatchEvent(new CustomEvent('open-quick-switcher'));
|
||||
}
|
||||
});
|
||||
|
||||
// Sidebar folder state management
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('sidebarState', () => ({
|
||||
expandedFolders: [],
|
||||
|
||||
initExpandedFolders() {
|
||||
// Load from localStorage
|
||||
const stored = localStorage.getItem('kb_expanded_folders');
|
||||
if (stored) {
|
||||
try {
|
||||
this.expandedFolders = JSON.parse(stored);
|
||||
} catch (e) {
|
||||
this.expandedFolders = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
toggleFolder(path) {
|
||||
const index = this.expandedFolders.indexOf(path);
|
||||
if (index > -1) {
|
||||
this.expandedFolders.splice(index, 1);
|
||||
} else {
|
||||
this.expandedFolders.push(path);
|
||||
}
|
||||
// Save to localStorage
|
||||
localStorage.setItem('kb_expanded_folders', JSON.stringify(this.expandedFolders));
|
||||
},
|
||||
|
||||
isFolderExpanded(path) {
|
||||
return this.expandedFolders.includes(path);
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user