Compare commits

...

29 Commits

Author SHA1 Message Date
8ea8b3f6b6 Improve current document highlighting with better URL matching
- Add removal of previous highlighting before applying new one
- Support multiple URL matching strategies (exact, full URL, ends with)
- Add debug logging to troubleshoot highlighting issues
- Reset all links before applying highlight to ensure clean state
2025-12-04 02:45:24 +09:00
d52968e697 Add current document highlighting in sidebar navigation
- Highlight active document with indigo background and bold font
- Change icon color to indigo for active document
- Use JavaScript to match current URL path with sidebar links
- Update highlighting on page load and Alpine navigation events
- Active document is visually distinct from other items
2025-12-04 02:42:20 +09:00
bed7137e43 Remove debug console logs from sidebar scroll preservation
- Remove all console.log statements
- Clean up code for production use
- Preserve scroll position functionality without debug output
2025-12-04 02:37:07 +09:00
028e0b11c7 Fix sidebar scroll preservation by correcting link selector
- Change selector from 'a[x-navigate]' to 'a' (no x-navigate attribute)
- Remove sessionStorage.removeItem to prevent clearing saved scroll position
- Add '0' check to prevent restoring scroll position to top
- Add debug logging for troubleshooting
- Now works correctly in all browsers including Chrome
2025-12-04 02:35:29 +09:00
5bf43abab9 Remove x-navigate directive, use native HTML links with scroll preservation
- Remove x-navigate.preserve-scroll from sidebar links
- Use standard <a> tags for navigation
- Implement manual JavaScript-based scroll position management
- Save scroll position to sessionStorage before navigation
- Restore scroll position after page load
- Works consistently across all browsers including Chrome
2025-12-04 02:30:48 +09:00
f96ad4d14f Use manual scroll position management instead of x-navigate.preserve-scroll
- Add JavaScript to save sidebar scroll position before navigation
- Restore scroll position after page load using sessionStorage
- Works consistently in Chrome and other browsers
- Handles both DOMContentLoaded and window load events
- Compatible with Alpine navigate and standard navigation
2025-12-04 02:24:00 +09:00
a4aff43091 Add preserve-scroll modifier to x-navigate directive
- Use x-navigate.preserve-scroll to maintain sidebar scroll position during navigation
- Prevents page from scrolling to top after clicking sidebar links
- Alpine navigate automatically saves and restores scroll position
2025-12-04 02:17:13 +09:00
1e20982e00 Simplify sidebar scroll preservation using only x-navigate directive
- Remove custom sessionStorage scroll management logic
- Rely solely on x-navigate directive from Alpine for scroll preservation
- x-navigate handles automatic scroll position saving and restoring
- Cleaner and simpler implementation
- Keep x-navigate directive on all sidebar links
2025-12-04 02:13:28 +09:00
ec7aaf44a9 Fix sidebar scroll preservation per page with x-navigate directive
- Add x-navigate directive to all sidebar document links for Alpine navigation
- Store scroll position per page using URL path as key in sessionStorage
- Each page now maintains its own scroll position in the sidebar
- Save scroll position before navigation and restore after navigation
- Scroll position is preserved when clicking links in the sidebar
- Works correctly with Alpine navigate events triggered by x-navigate directive
2025-12-04 02:11:06 +09:00
00a5951654 Improve sidebar scroll position preservation with sessionStorage fallback
- Replace localStorage with sessionStorage for session-based scroll restoration
- Add console logging for debugging scroll behavior
- Support both Livewire and Alpine navigate events
- Intercept sidebar link clicks to ensure scroll position is saved before navigation
- Use setTimeout for smoother DOM restoration timing
- Restore scroll position on page load and window load events
- Sidebar now maintains scroll position consistently across navigation
2025-12-04 01:55:59 +09:00
8dba510a6c Fix sidebar scroll position preservation during page navigation
- Replace unstable x-navigate:scroll directive with custom Alpine event handlers
- Use alpine:navigating event to save sidebar scroll position to localStorage
- Use alpine:navigated event to restore sidebar scroll position after navigation
- Sidebar now maintains scroll position when clicking document links
- Fixed 'Element not found' error that was preventing scroll restoration
- Uses requestAnimationFrame for smooth DOM restoration
2025-12-04 01:47:51 +09:00
e66ece71e3 Preserve sidebar scroll position when navigating between documents
- Add x-navigate:scroll directive to sidebar container to maintain scroll position
- Add x-navigate:scroll to all document links in sidebar (tree-item.blade.php)
- Add x-navigate:scroll to 'New Document' button
- When clicking a link in sidebar, the sidebar scroll position is now preserved during page navigation
- Fixes issue where sidebar would scroll to top after loading a document
2025-12-04 01:37:19 +09:00
b96012f598 Add resizable sidebar feature and increase default width to 320px
- Add resizable handle on the right edge of sidebar in desktop view (lg and above)
- Allow dragging to adjust sidebar width between 200px and 600px
- Persist resize settings in localStorage across page reloads
- Keep mobile mode unchanged with fixed 256px width
- Increase default width from 256px to 320px
- Implement visual feedback with color change on mouse hover
2025-12-04 01:24:47 +09:00
e50ed261e1 Show timezone information 2025-11-30 13:58:03 +09:00
79a09430aa Fix timezone issue: Set default timezone to Asia/Tokyo
- Update config/app.php to use APP_TIMEZONE from .env with Asia/Tokyo as default
- Add APP_TIMEZONE to .env.example
- Fixes issue where timestamps were displayed 9 hours behind (UTC vs JST)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:46:16 +09:00
33fef93ce0 Update README with comprehensive project documentation
- Add detailed feature overview (wiki-links, folders, multi-language, responsive)
- Include complete installation and setup instructions
- Document project structure and key concepts
- Add development workflow and common commands
- Include troubleshooting section
- Update technology stack and credits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:19:43 +09:00
e14cc5dd43 Implement responsive design for mobile and tablet devices
- Add hamburger menu for mobile sidebar with slide-out animation
- Make header navigation responsive with icon-only buttons on mobile
- Adjust document viewer, editor, and quick switcher layouts for smaller screens
- Preserve all existing functionality including localStorage folder state persistence
- Use Tailwind responsive utilities (sm:, md:, lg:) for progressive enhancement

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:17:50 +09:00
c9fe1f6ed0 Reorder language dropdown for better UX
New order:
1. Primary: English, Japanese
2. Chinese: Simplified, Traditional
3. Korean
4. Other Asian: Hindi, Vietnamese, Turkish
5. European: German, French, Spanish, Portuguese, Russian,
   Ukrainian, Italian, Polish

This prioritizes the most commonly used languages in the target
market while maintaining a logical regional grouping.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:02:05 +09:00
01c92aeb7c Add translation files for 8 new languages
Created complete message translation files for:
- pt-BR: Portuguese (Brazil) - 250M+ speakers
- ru: Russian - 260M+ speakers
- uk: Ukrainian - 40M+ speakers
- it: Italian - 67M speakers
- hi: Hindi - 600M+ speakers
- vi: Vietnamese - 95M speakers
- tr: Turkish - 80M speakers
- pl: Polish - 45M speakers

All translation files include complete translations for:
- Navigation, documents, quick switcher
- Admin panel, settings, common terms
- Authentication, errors, profile management

Total supported languages: 16 (LTR only)
These translations provide native language support for
1.5+ billion additional speakers worldwide.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 13:01:12 +09:00
211867e2eb Add 8 major languages (LTR only)
Added languages:
- pt-BR: Português (Brasil) - 250M+ speakers
- ru: Русский - 260M+ speakers
- uk: Українська - 40M+ speakers
- it: Italiano - 67M speakers
- hi: हिन्दी - 600M+ speakers
- vi: Tiếng Việt - 95M speakers
- tr: Türkçe - 80M speakers
- pl: Polski - 45M speakers

Total supported languages: 16
RTL languages (Arabic, Hebrew, etc.) excluded for now,
as they require additional layout implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 12:54:14 +09:00
f57d235651 Remove language setting from profile page
Since language switching is now available in the header for all users,
the separate language setting section in the profile page is redundant.

Changes:
- Remove update-locale-form include from profile/edit.blade.php
- Delete profile/partials/update-locale-form.blade.php

Users can now change language using the header dropdown instead.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 12:36:53 +09:00
b419991940 Store guest user language preference in long-lived cookie
Changes:
- LocaleController: Set 1-year cookie when language is changed
- SetLocale middleware: Check cookie after session, before default
  Priority: User DB > Session > Cookie > Default

This allows guest users to retain their language preference
even after closing the browser (persists for 1 year).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 12:32:01 +09:00
61d42d79f1 Enable language switching for guest users
Changes:
- Move locale.update route outside auth middleware
- Update LocaleController to support both authenticated and guest users
  - Guest users: Save locale preference to session only
  - Authenticated users: Save to both session and database
- Add language switcher dropdown to header for all users
  - Display current language with globe icon
  - Show all 8 supported languages in dropdown
  - Highlight currently selected language with checkmark

This allows non-logged-in users to change the interface language,
improving accessibility for international visitors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 12:30:35 +09:00
5b83936c4b livewire config fix 2025-11-29 17:53:18 +09:00
ef238891f5 fix: Set Livewire asset_url to null for subdirectory support
ASSET_URL was being used as the script src, causing:
<script src="https://domain.com/kb">

Setting asset_url to null lets Livewire use the correct default path.
2025-11-29 17:48:12 +09:00
a9b4b93d8c fix: Remove wire:navigate to fix MIME type error
Livewire 3's SPA navigation (wire:navigate) was causing the browser
to register document URLs as JavaScript sources, resulting in:
'Refused to execute script... MIME type text/html is not executable'

Removed wire:navigate from:
- partials/tree-item.blade.php (sidebar links)
- livewire/document-viewer.blade.php (backlinks)
- livewire/quick-switcher.blade.php (search results)

Pages will now do full page loads instead of SPA navigation.
2025-11-29 17:40:59 +09:00
9625268e67 fix: Add Livewire config and custom 404 page for subdirectory support
- Add config/livewire.php with app_url and asset_url settings
- Add custom 404 error page with consistent design
- Add error translations for all 8 languages

For subdirectory deployment:
1. Set APP_URL to include subdirectory (e.g., https://example.com/kb)
2. Set ASSET_URL if using CDN
3. Clear config cache after deployment
2025-11-29 17:27:40 +09:00
ac56889a87 fix: Use slug for Document route model binding
- Add getRouteKeyName() to return 'slug' for Document model
- Add resolveRouteBinding() to support both slug and ID lookups
- Update QuickSwitcher to use slug instead of ID
- Update quick-switcher blade to use slug in routes

This ensures URLs use readable slugs (e.g., /documents/home)
while maintaining backwards compatibility with ID-based URLs
2025-11-29 17:22:17 +09:00
893d3c7a69 fix: Replace hardcoded paths with route/url helpers for subdirectory support
Files updated:
- layouts/knowledge-base.blade.php - Use url('/') for home link
- layouts/navigation.blade.php - Use url('/') for nav links
- layouts/guest.blade.php - Use url('/') for logo link
- Document.php - Use route() for wiki links
- DocumentLink.php - Use route() for URL attribute
- AuthenticatedSessionController.php - Use url('/') for redirects
- DocumentEditor.php - Use url('/') for redirect
- ProfileController.php - Use url('/') for redirect

This ensures the app works when deployed in a subdirectory
2025-11-29 17:14:46 +09:00
41 changed files with 2185 additions and 176 deletions

View File

@@ -12,6 +12,20 @@ Markdown対応のナレッジベースアプリケーションです。Wiki風
- 👥 **ユーザー管理** - 管理者によるユーザーのCRUD操作 - 👥 **ユーザー管理** - 管理者によるユーザーのCRUD操作
- 🔒 **認証** - Laravel Breezeによるログイン/登録機能 - 🔒 **認証** - Laravel Breezeによるログイン/登録機能
- 🎨 **シンタックスハイライト** - コードブロックの自動ハイライト - 🎨 **シンタックスハイライト** - コードブロックの自動ハイライト
- 🌐 **多言語対応** - 8言語サポートユーザーごとに言語設定可能
## 対応言語
| 言語 | コード |
|------|--------|
| English | en |
| 日本語 | ja |
| Deutsch | de |
| Français | fr |
| Español | es |
| 简体中文 | zh-CN |
| 繁體中文 | zh-TW |
| 한국어 | ko |
## 技術スタック ## 技術スタック
@@ -49,6 +63,7 @@ Markdown対応のナレッジベースアプリケーションです。Wiki風
│ │ ├── Livewire/ # Livewireコンポーネント │ │ ├── Livewire/ # Livewireコンポーネント
│ │ ├── Models/ # Eloquentモデル │ │ ├── Models/ # Eloquentモデル
│ │ └── Services/ # ビジネスロジック │ │ └── Services/ # ビジネスロジック
│ ├── lang/ # 言語ファイルi18n
│ ├── resources/views/ │ ├── resources/views/
│ │ ├── layouts/ # レイアウト │ │ ├── layouts/ # レイアウト
│ │ ├── livewire/ # Livewireビュー │ │ ├── livewire/ # Livewireビュー
@@ -165,6 +180,62 @@ DocumentSeederを実行すると以下のドキュメントが作成されます
※ 既存のドキュメントがある場合、DocumentSeederはスキップされます。 ※ 既存のドキュメントがある場合、DocumentSeederはスキップされます。
## 本番環境へのデプロイ
### ⚠️ 重要: サブドメインを使用してください
このアプリケーションは **サブドメイン** でのデプロイを推奨します。
```
✅ 推奨: kb.example.com
❌ 非推奨: example.com/kb (サブディレクトリ)
```
**理由**: Livewire 3はサブディレクトリデプロイに完全対応していません。`/livewire/update` エンドポイントがサブディレクトリを考慮しないため、AJAX通信が失敗します。
### デプロイ手順
1. **サブドメインのDNS設定**
```
kb.example.com → サーバーIPアドレス
```
2. **Webサーバー設定**nginx例
```nginx
server {
listen 80;
server_name kb.example.com;
root /var/www/knowledge-base/src/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
```
3. **環境変数の設定**
```env
APP_URL=https://kb.example.com
APP_ENV=production
APP_DEBUG=false
```
4. **本番用の最適化**
```bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
```
## よく使うコマンド ## よく使うコマンド
```bash ```bash
@@ -181,6 +252,12 @@ docker exec kb_php npm [command]
# コンテナに入る # コンテナに入る
docker exec -it kb_php bash docker exec -it kb_php bash
# キャッシュクリア
docker exec kb_php php artisan config:clear
docker exec kb_php php artisan route:clear
docker exec kb_php php artisan view:clear
docker exec kb_php php artisan cache:clear
``` ```
## 管理者機能 ## 管理者機能
@@ -219,6 +296,15 @@ docker exec -it kb_php bash
[[Laravel/Livewire/Components]] も確認してください。 [[Laravel/Livewire/Components]] も確認してください。
``` ```
## 言語設定
ユーザーは「プロフィール」ページから使用言語を変更できます。
1. 右上のユーザー名をクリック
2. 「プロフィール」を選択
3. 「言語設定」セクションで言語を選択
4. 「保存」をクリック
## トラブルシューティング ## トラブルシューティング
### パーミッションエラー ### パーミッションエラー
@@ -243,6 +329,17 @@ docker compose up -d
`src/.env` の `DB_HOST` が `kb_mysql` になっているか確認してください。 `src/.env` の `DB_HOST` が `kb_mysql` になっているか確認してください。
### Livewireのエラー本番環境
「404 /livewire/update」エラーが出る場合は、サブディレクトリではなくサブドメインでデプロイしてください。
```bash
# キャッシュをクリア
php artisan config:clear
php artisan route:clear
php artisan cache:clear
```
## ライセンス ## ライセンス
MIT License MIT License

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "knowledge-base",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -7,6 +7,7 @@ APP_URL=http://localhost
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US APP_FAKER_LOCALE=en_US
APP_TIMEZONE=Asia/Tokyo
APP_MAINTENANCE_DRIVER=file APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database # APP_MAINTENANCE_STORE=database

View File

@@ -1,59 +1,332 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p> # Knowledge Base System
<p align="center"> An Obsidian-like knowledge base system built with Laravel 12, Livewire v3, and Alpine.js. Create, organize, and link your documents with wiki-style references and a powerful search interface.
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel ## Features
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: ### Core Functionality
- **Markdown-based documents** with live preview using EasyMDE editor
- **Wiki-style linking** with `[[Document Title]]` syntax
- **Automatic backlinks** - see which documents reference the current page
- **Folder organization** - use `/` in titles to auto-organize into folders (e.g., `Laravel/Livewire/Components`)
- **Quick switcher** - Press `Ctrl+K` to instantly search and navigate
- **Full-text search** - MySQL FULLTEXT index with ngram tokenizer for multilingual support
- **ID-based routing** - Clean URLs with guaranteed uniqueness
- [Simple, fast routing engine](https://laravel.com/docs/routing). ### Multi-Language Support
- [Powerful dependency injection container](https://laravel.com/docs/container). Interface available in **16 languages**:
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. - English, 日本語 (Japanese)
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). - 简体中文, 繁體中文 (Simplified/Traditional Chinese)
- Database agnostic [schema migrations](https://laravel.com/docs/migrations). - 한국어 (Korean)
- [Robust background job processing](https://laravel.com/docs/queues). - हिन्दी (Hindi), Tiếng Việt (Vietnamese), Türkçe (Turkish)
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). - Deutsch, Français, Español, Português (Brasil)
- Русский, Українська, Italiano, Polski
Laravel is accessible, powerful, and provides tools required for large, robust applications. Language preferences persist for both authenticated and guest users via cookies.
## Learning Laravel ### Responsive Design
- **Mobile-first** interface with hamburger menu
- **Tablet and desktop** optimized layouts
- **Touch-friendly** navigation
- All features work seamlessly across devices
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. ### User Management
- **Role-based access** - Admin and regular user roles
- **User authentication** - Laravel Breeze integration
- **Profile management** - Update name, email, password, and language preference
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. ## Technology Stack
## Laravel Sponsors - **Backend**: Laravel 12.0 (PHP 8.2+)
- **Frontend**: Livewire v3.7.0, Alpine.js, Tailwind CSS
- **Database**: MySQL 8.0 with FULLTEXT indexing
- **Markdown**: league/commonmark for rendering
- **Editor**: EasyMDE (markdown editor)
- **Docker**: Custom containerized environment
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). ## Prerequisites
### Premium Partners - Docker and Docker Compose
- Node.js 18+ (for asset compilation)
- Git
- **[Vehikl](https://vehikl.com)** ## Installation
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** ### 1. Clone the repository
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)** ```bash
- **[DevSquad](https://devsquad.com/hire-laravel-developers)** git clone <repository-url>
- **[Redberry](https://redberry.international/laravel-development)** cd knowledge-base
- **[Active Logic](https://activelogic.com)** ```
### 2. Start Docker services
```bash
docker compose up -d
```
This starts:
- Nginx: `http://localhost:9700`
- phpMyAdmin: `http://localhost:9701`
- MySQL: `localhost:9702`
- MailHog: `http://localhost:9725`
### 3. Install dependencies
```bash
# Inside the src/ directory
cd src
# Install PHP dependencies
docker compose exec php composer install
# Install Node dependencies
npm install
```
### 4. Configure environment
```bash
# Copy environment file
cp .env.example .env
# Generate application key
docker compose exec php php artisan key:generate
```
### 5. Set up database
```bash
# Run migrations
docker compose exec php php artisan migrate
# Seed initial user (admin@example.com / password)
docker compose exec php php artisan db:seed --class=UserSeeder
# Initialize sample documents (optional)
docker compose exec php php artisan docs:init
```
### 6. Build frontend assets
```bash
# Development mode with hot reload
npm run dev
# Or production build
npm run build
```
### 7. Access the application
Open `http://localhost:9700` in your browser.
**Default credentials**:
- Email: `admin@example.com`
- Password: `password`
## Development
### Running the dev environment
```bash
# Start all services (server, queue, logs, Vite)
docker compose exec php composer dev
```
### Running tests
```bash
docker compose exec php php artisan test
```
### Common commands
```bash
# Access PHP container shell
docker compose exec php bash
# Clear caches
docker compose exec php php artisan config:clear
docker compose exec php php artisan cache:clear
docker compose exec php php artisan view:clear
# Publish Livewire assets (after updates)
docker compose exec php php artisan livewire:publish --assets
```
## Project Structure
```
src/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ └── LocaleController.php # Language switching
│ │ └── Middleware/
│ │ └── SetLocale.php # Multi-language support
│ ├── Livewire/
│ │ ├── DocumentEditor.php # Create/edit documents
│ │ ├── DocumentViewer.php # Display documents
│ │ ├── QuickSwitcher.php # Ctrl+K search modal
│ │ └── SidebarTree.php # Folder tree navigation
│ ├── Models/
│ │ ├── Document.php # Document model
│ │ ├── DocumentLink.php # Wiki-style links
│ │ └── RecentDocument.php # Access history
│ └── Services/
│ └── DocumentService.php # Document business logic
├── database/
│ └── migrations/ # Database schema
├── lang/ # Translation files (16 languages)
├── resources/
│ ├── css/
│ │ └── app.css # Tailwind + custom styles
│ ├── js/
│ │ └── app.js # Alpine.js initialization
│ └── views/
│ ├── layouts/
│ │ └── knowledge-base.blade.php # Main layout
│ └── livewire/ # Livewire component views
└── routes/
└── web.php # Application routes
```
## Key Concepts
### Document Organization
Documents are organized using **virtual paths** derived from titles:
```php
Title: "Laravel/Livewire/Components"
→ Path: "Laravel/Livewire/Components.md"
→ Slug: "components"
→ Sidebar: Nested under Laravel → Livewire → Components
```
No manual directory field needed - just use `/` in the title!
### Wiki-Style Links
Create links between documents using double brackets:
```markdown
See [[Getting Started]] for more information.
Links to [[Uncreated Pages]] appear in red.
```
Links are automatically:
- Extracted and stored in the `document_links` table
- Rendered as clickable HTML anchors
- Displayed as backlinks on target documents
### ID-Based Routing
URLs use document IDs instead of slugs:
```
/documents/123 (instead of /documents/my-document-slug)
```
Benefits:
- Guaranteed uniqueness
- Title changes don't break URLs
- Simpler route model binding
### Folder State Persistence
Sidebar folder expanded/collapsed state is stored in `localStorage`:
```javascript
// Managed by Alpine.js
localStorage.getItem('kb_expanded_folders')
// ["Laravel", "Laravel/Livewire", "Docker"]
```
Survives page navigation and browser sessions.
## Customization
### Adding new languages
1. Add to `SetLocale::SUPPORTED_LOCALES` in `app/Http/Middleware/SetLocale.php`
2. Create translation file at `lang/{code}/messages.php`
3. Copy structure from existing language file
### Changing default locale
Edit `config/app.php`:
```php
'locale' => 'en', // Change to your preferred language code
```
### Customizing markdown styles
Edit `resources/css/app.css`:
```css
@layer components {
.prose .wiki-link {
@apply text-indigo-600 hover:text-indigo-800 underline;
}
}
```
## Troubleshooting
### Livewire assets not loading
```bash
docker compose exec php php artisan livewire:publish --assets
```
### Frontend changes not reflecting
```bash
npm run build
docker compose exec php php artisan view:clear
```
### Database connection errors
Check `.env` file matches Docker Compose settings:
```env
DB_CONNECTION=mysql
DB_HOST=kb_mysql
DB_PORT=3306
DB_DATABASE=knowledge_base
DB_USERNAME=kb_user
DB_PASSWORD=kb_password
```
### Alpine.js errors in console
Ensure scripts are loaded in correct order in `knowledge-base.blade.php`:
1. Livewire scripts first
2. Alpine.js initialization (via Vite)
3. Custom Alpine components
## Contributing ## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). Contributions are welcome! Please ensure:
- Code follows Laravel and PSR-12 conventions
## Code of Conduct - All existing tests pass
- New features include tests
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). - UI changes maintain responsive design
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License ## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
## Credits
Built with:
- [Laravel](https://laravel.com) - The PHP Framework
- [Livewire](https://livewire.laravel.com) - Full-stack framework for Laravel
- [Alpine.js](https://alpinejs.dev) - Lightweight JavaScript framework
- [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS framework
- [EasyMDE](https://github.com/Ionaru/easy-markdown-editor) - Markdown editor
- [league/commonmark](https://commonmark.thephpleague.com) - Markdown parser

View File

@@ -33,7 +33,7 @@ public function store(LoginRequest $request): RedirectResponse
return redirect()->intended(route('dashboard', absolute: false)); return redirect()->intended(route('dashboard', absolute: false));
} }
return redirect()->intended('/'); return redirect()->intended(url('/'));
} }
/** /**
@@ -47,6 +47,6 @@ public function destroy(Request $request): RedirectResponse
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return redirect('/'); return redirect(url('/'));
} }
} }

View File

@@ -10,6 +10,7 @@ class LocaleController extends Controller
{ {
/** /**
* Update the user's locale preference. * Update the user's locale preference.
* Works for both authenticated and guest users.
*/ */
public function update(Request $request) public function update(Request $request)
{ {
@@ -19,12 +20,17 @@ public function update(Request $request)
$locale = $validated['locale']; $locale = $validated['locale'];
// Save to user record // Save to session for immediate effect
Auth::user()->update(['locale' => $locale]);
// Also save to session for immediate effect
$request->session()->put('locale', $locale); $request->session()->put('locale', $locale);
return redirect()->route('profile.edit')->with('success', __('messages.settings.language_updated')); // If authenticated, also save to user record for persistence
if (Auth::check()) {
Auth::user()->update(['locale' => $locale]);
}
// Set long-lived cookie (1 year) for guest users
cookie()->queue('locale', $locale, 525600); // 525600 minutes = 1 year
return redirect()->back()->with('success', __('messages.settings.language_updated'));
} }
} }

View File

@@ -55,6 +55,6 @@ public function destroy(Request $request): RedirectResponse
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return Redirect::to('/'); return Redirect::to(url('/'));
} }
} }

View File

@@ -11,36 +11,64 @@
class SetLocale class SetLocale
{ {
/** /**
* Supported locales * Supported locales (LTR languages only)
* Order: English, Japanese, Chinese, Korean, Other Asian, European
*/ */
public const SUPPORTED_LOCALES = [ public const SUPPORTED_LOCALES = [
// Primary languages
'en' => 'English', 'en' => 'English',
'ja' => '日本語', 'ja' => '日本語',
// Chinese variants
'zh-CN' => '简体中文',
'zh-TW' => '繁體中文',
// Korean
'ko' => '한국어',
// Other Asian languages
'hi' => 'हिन्दी',
'vi' => 'Tiếng Việt',
'tr' => 'Türkçe',
// European languages
'de' => 'Deutsch', 'de' => 'Deutsch',
'fr' => 'Français', 'fr' => 'Français',
'es' => 'Español', 'es' => 'Español',
'zh-CN' => '简体中文', 'pt-BR' => 'Português (Brasil)',
'zh-TW' => '繁體中文', 'ru' => 'Русский',
'ko' => '한국어', 'uk' => 'Українська',
'it' => 'Italiano',
'pl' => 'Polski',
]; ];
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* Priority order:
* 1. Authenticated user's database preference
* 2. Session (for immediate effect after changing)
* 3. Cookie (long-term storage for guest users)
* 4. Default locale from config
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/ */
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
$locale = config('app.locale', 'en'); $locale = config('app.locale', 'en');
// Check authenticated user's preference // Priority 1: Check authenticated user's preference
if (Auth::check() && Auth::user()->locale) { if (Auth::check() && Auth::user()->locale) {
$locale = Auth::user()->locale; $locale = Auth::user()->locale;
} }
// Check session (for immediate effect after changing) // Priority 2: Check session (for immediate effect after changing)
elseif ($request->session()->has('locale')) { elseif ($request->session()->has('locale')) {
$locale = $request->session()->get('locale'); $locale = $request->session()->get('locale');
} }
// Priority 3: Check cookie (long-term storage for guest users)
elseif ($request->cookie('locale')) {
$locale = $request->cookie('locale');
}
// Validate locale // Validate locale
if (!array_key_exists($locale, self::SUPPORTED_LOCALES)) { if (!array_key_exists($locale, self::SUPPORTED_LOCALES)) {

View File

@@ -80,7 +80,7 @@ public function delete(DocumentService $documentService)
if ($homeDocument) { if ($homeDocument) {
return redirect()->route('documents.show', $homeDocument); return redirect()->route('documents.show', $homeDocument);
} }
return redirect('/'); return redirect(url('/'));
} catch (\Exception $e) { } catch (\Exception $e) {
session()->flash('error', 'Error deleting document: ' . $e->getMessage()); session()->flash('error', 'Error deleting document: ' . $e->getMessage());
} }

View File

@@ -93,9 +93,9 @@ public function selectDocument()
if (isset($results[$this->selectedIndex])) { if (isset($results[$this->selectedIndex])) {
$document = $results[$this->selectedIndex]; $document = $results[$this->selectedIndex];
// id が存在することを確認 // slug が存在することを確認
if (!empty($document['id'])) { if (!empty($document['slug'])) {
return $this->redirect(route('documents.show', $document['id'])); return $this->redirect(route('documents.show', $document['slug']));
} }
} }
} }

View File

@@ -19,6 +19,37 @@ class Document extends Model
{ {
use SoftDeletes; use SoftDeletes;
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName(): string
{
return 'slug';
}
/**
* Retrieve the model for a bound value.
* Supports both slug and ID for backwards compatibility.
*
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
// First try to find by slug
$document = $this->where('slug', $value)->first();
// If not found by slug, try by ID (for backwards compatibility)
if (!$document && is_numeric($value)) {
$document = $this->where('id', $value)->first();
}
return $document;
}
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
* *
@@ -154,9 +185,9 @@ function ($matches) {
->first(); ->first();
if ($targetDocument) { if ($targetDocument) {
return '<a href="/documents/' . $targetDocument->slug . '" class="wiki-link">' . e($linkTitle) . '</a>'; return '<a href="' . route('documents.show', $targetDocument->slug) . '" class="wiki-link">' . e($linkTitle) . '</a>';
} else { } else {
return '<a href="/documents/create?title=' . urlencode($linkTitle) . '" class="wiki-link wiki-link-new">' . e($linkTitle) . '</a>'; return '<a href="' . route('documents.create') . '?title=' . urlencode($linkTitle) . '" class="wiki-link wiki-link-new">' . e($linkTitle) . '</a>';
} }
}, },
$this->rendered_html $this->rendered_html

View File

@@ -57,9 +57,9 @@ public function isBroken(): bool
public function getUrlAttribute(): string public function getUrlAttribute(): string
{ {
if ($this->isBroken()) { if ($this->isBroken()) {
return '/documents/create?title=' . urlencode($this->target_title); return route('documents.create') . '?title=' . urlencode($this->target_title);
} }
return '/documents/' . $this->targetDocument->slug; return route('documents.show', $this->targetDocument->slug);
} }
} }

View File

@@ -65,7 +65,7 @@
| |
*/ */
'timezone' => 'UTC', 'timezone' => env('APP_TIMEZONE', 'Asia/Tokyo'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

125
src/config/livewire.php Normal file
View File

@@ -0,0 +1,125 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Livewire Assets URL
|--------------------------------------------------------------------------
|
| This value sets the path to Livewire JavaScript assets, for cases where
| your app's domain root is not the correct path. By default, Livewire
| will load its JavaScript assets from the app's "relative root".
|
| Examples: "/assets", "myurl.com/app".
|
*/
'asset_url' => null,
/*
|--------------------------------------------------------------------------
| Livewire App URL
|--------------------------------------------------------------------------
|
| This value should be used if livewire assets are served from CDN.
| Livewire will communicate with an app through this url.
|
| Examples: "https://my-app.com", "myurl.com/app".
|
*/
'app_url' => env('APP_URL'),
/*
|--------------------------------------------------------------------------
| Livewire Update Endpoint
|--------------------------------------------------------------------------
|
| The endpoint for Livewire AJAX requests.
|
*/
'update_endpoint' => '/livewire/update',
/*
|--------------------------------------------------------------------------
| Livewire Endpoint Middleware Group
|--------------------------------------------------------------------------
|
| This value sets the middleware group that will be applied to the main
| Livewire "message" endpoint (the endpoint that gets hit everytime
| a Livewire component updates). It is set to "web" by default.
|
*/
'middleware_group' => 'web',
/*
|--------------------------------------------------------------------------
| Livewire Temporary File Uploads Endpoint Configuration
|--------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is stored permanently. All file uploads are directed
| to a global endpoint for temporary storage. Here you may configure.
|
*/
'temporary_file_upload' => [
'disk' => null,
'rules' => null,
'directory' => null,
'middleware' => null,
'preview_mimes' => [
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5,
'cleanup' => true,
],
/*
|--------------------------------------------------------------------------
| Render On Redirect
|--------------------------------------------------------------------------
|
| This value determines if Livewire will render before it's redirected
| or not. Setting it to "false" will mean the render method is skipped
| when performing a redirect, potentially improving performances.
|
*/
'render_on_redirect' => false,
/*
|--------------------------------------------------------------------------
| Navigate (SPA mode)
|--------------------------------------------------------------------------
|
| By default, Livewire uses the "replace" strategy for SPA navigation
| which can sometimes cause issues with subdirectory deployments.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|--------------------------------------------------------------------------
| Inject Assets
|--------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and styles
| into the <head> and <body> of your pages. If you want to disable
| this behavior and manually include assets, set this to false.
|
*/
'inject_assets' => true,
];

View File

@@ -115,6 +115,14 @@
'already_registered' => 'Bereits registriert?', 'already_registered' => 'Bereits registriert?',
], ],
// Errors
'errors' => [
'404_title' => 'Seite nicht gefunden',
'page_not_found' => 'Seite nicht gefunden',
'page_not_found_description' => 'Die gesuchte Seite konnte nicht gefunden werden.',
'back_to_home' => 'Zurück zur Startseite',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => 'Profil', 'title' => 'Profil',

View File

@@ -115,6 +115,14 @@
'already_registered' => 'Already registered?', 'already_registered' => 'Already registered?',
], ],
// Errors
'errors' => [
'404_title' => 'Page Not Found',
'page_not_found' => 'Page Not Found',
'page_not_found_description' => 'The page you are looking for could not be found.',
'back_to_home' => 'Back to Home',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => 'Profile', 'title' => 'Profile',

View File

@@ -115,6 +115,14 @@
'already_registered' => '¿Ya está registrado?', 'already_registered' => '¿Ya está registrado?',
], ],
// Errors
'errors' => [
'404_title' => 'Página no encontrada',
'page_not_found' => 'Página no encontrada',
'page_not_found_description' => 'No se pudo encontrar la página que está buscando.',
'back_to_home' => 'Volver al inicio',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => 'Perfil', 'title' => 'Perfil',

View File

@@ -115,6 +115,14 @@
'already_registered' => 'Déjà inscrit ?', 'already_registered' => 'Déjà inscrit ?',
], ],
// Errors
'errors' => [
'404_title' => 'Page non trouvée',
'page_not_found' => 'Page non trouvée',
'page_not_found_description' => 'La page que vous recherchez est introuvable.',
'back_to_home' => 'Retour à l\'accueil',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => 'Profil', 'title' => 'Profil',

148
src/lang/hi/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'डैशबोर्ड',
'knowledge_base' => 'ज्ञान आधार',
'profile' => 'प्रोफ़ाइल',
'user_management' => 'उपयोगकर्ता प्रबंधन',
'logout' => 'लॉग आउट',
'login' => 'लॉगिन',
'register' => 'रजिस्टर',
],
// Documents
'documents' => [
'title' => 'दस्तावेज़',
'new_document' => 'नया दस्तावेज़',
'edit_document' => 'दस्तावेज़ संपादित करें',
'edit' => 'संपादित करें',
'delete' => 'हटाएं',
'save' => 'सहेजें',
'cancel' => 'रद्द करें',
'created_by' => 'द्वारा बनाया गया',
'modified_by' => 'द्वारा',
'updated' => 'अपडेट किया गया',
'path' => 'पथ',
'last_modified' => 'अंतिम संशोधन',
'no_documents' => 'कोई दस्तावेज़ नहीं मिला',
'search_placeholder' => 'दस्तावेज़ खोजें...',
'create_success' => 'दस्तावेज़ सफलतापूर्वक बनाया गया!',
'update_success' => 'दस्तावेज़ सफलतापूर्वक अपडेट किया गया!',
'delete_success' => 'दस्तावेज़ सफलतापूर्वक हटाया गया!',
'delete_confirm' => 'क्या आप वाकई इस दस्तावेज़ को हटाना चाहते हैं?',
'linked_references' => 'लिंक किए गए संदर्भ',
'title_label' => 'शीर्षक',
'title_placeholder' => 'दस्तावेज़ शीर्षक (फ़ोल्डर के लिए / का उपयोग करें, जैसे Laravel/Livewire/Components)',
'title_hint' => 'सुझाव: दस्तावेज़ों को स्वचालित रूप से फ़ोल्डर में व्यवस्थित करने के लिए शीर्षक में स्लैश (/) का उपयोग करें',
'content_label' => 'सामग्री',
'content_placeholder' => 'यहां अपना मार्कडाउन लिखें...',
'saving' => 'सहेजा जा रहा है...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'त्वरित स्विच',
'placeholder' => 'दस्तावेज़ खोजें...',
'no_results' => 'कोई दस्तावेज़ नहीं मिला',
'navigate' => 'नेविगेट करने के लिए',
'select' => 'चुनने के लिए',
'close' => 'बंद करने के लिए',
],
// Admin
'admin' => [
'user_management' => 'उपयोगकर्ता प्रबंधन',
'new_user' => 'नया उपयोगकर्ता',
'edit_user' => 'उपयोगकर्ता संपादित करें',
'create_user' => 'उपयोगकर्ता बनाएं',
'users' => 'उपयोगकर्ता',
'name' => 'नाम',
'email' => 'ईमेल',
'password' => 'पासवर्ड',
'password_confirmation' => 'पासवर्ड की पुष्टि करें',
'password_hint' => 'वर्तमान पासवर्ड रखने के लिए खाली छोड़ दें।',
'role' => 'भूमिका',
'admin' => 'प्रशासक',
'user' => 'उपयोगकर्ता',
'grant_admin' => 'प्रशासक विशेषाधिकार प्रदान करें',
'created_at' => 'बनाया गया',
'actions' => 'क्रियाएं',
'edit' => 'संपादित करें',
'delete' => 'हटाएं',
'no_users' => 'कोई उपयोगकर्ता नहीं मिला।',
'create_success' => 'उपयोगकर्ता सफलतापूर्वक बनाया गया।',
'update_success' => 'उपयोगकर्ता सफलतापूर्वक अपडेट किया गया।',
'delete_success' => 'उपयोगकर्ता सफलतापूर्वक हटाया गया।',
'cannot_delete_self' => 'आप स्वयं को हटा नहीं सकते।',
'self_admin_warning' => 'अपने स्वयं के प्रशासक विशेषाधिकारों को हटाने से आपका प्रशासन पैनल तक पहुंच अवरुद्ध हो जाएगी।',
],
// Settings
'settings' => [
'language' => 'भाषा',
'select_language' => 'भाषा चुनें',
'language_updated' => 'भाषा सफलतापूर्वक अपडेट की गई।',
'change_language' => 'भाषा बदलें',
],
// Common
'common' => [
'save' => 'सहेजें',
'cancel' => 'रद्द करें',
'delete' => 'हटाएं',
'edit' => 'संपादित करें',
'create' => 'बनाएं',
'update' => 'अपडेट करें',
'back' => 'वापस',
'confirm' => 'पुष्टि करें',
'yes' => 'हां',
'no' => 'नहीं',
'loading' => 'लोड हो रहा है...',
'error' => 'त्रुटि',
'success' => 'सफलता',
],
// Auth
'auth' => [
'login' => 'लॉगिन',
'register' => 'रजिस्टर',
'email' => 'ईमेल',
'password' => 'पासवर्ड',
'remember_me' => 'मुझे याद रखें',
'forgot_password' => 'पासवर्ड भूल गए?',
'confirm_password' => 'पासवर्ड की पुष्टि करें',
'already_registered' => 'पहले से रजिस्टर्ड?',
],
// Errors
'errors' => [
'404_title' => 'पेज नहीं मिला',
'page_not_found' => 'पेज नहीं मिला',
'page_not_found_description' => 'आप जिस पेज की तलाश कर रहे हैं वह नहीं मिला।',
'back_to_home' => 'होम पर वापस जाएं',
],
// Profile
'profile' => [
'title' => 'प्रोफ़ाइल',
'information' => 'प्रोफ़ाइल जानकारी',
'information_description' => 'अपने खाते की प्रोफ़ाइल जानकारी और ईमेल पता अपडेट करें।',
'name' => 'नाम',
'email' => 'ईमेल',
'email_unverified' => 'आपका ईमेल पता सत्यापित नहीं है।',
'resend_verification' => 'सत्यापन ईमेल फिर से भेजने के लिए यहां क्लिक करें।',
'verification_sent' => 'आपके ईमेल पते पर एक नया सत्यापन लिंक भेजा गया है।',
'saved' => 'सहेजा गया।',
'update_password' => 'पासवर्ड अपडेट करें',
'update_password_description' => 'सुनिश्चित करें कि आपका खाता सुरक्षित रहने के लिए लंबे, यादृच्छिक पासवर्ड का उपयोग कर रहा है।',
'current_password' => 'वर्तमान पासवर्ड',
'new_password' => 'नया पासवर्ड',
'confirm_password' => 'पासवर्ड की पुष्टि करें',
'delete_account' => 'खाता हटाएं',
'delete_account_description' => 'एक बार आपका खाता हटा दिए जाने के बाद, इसके सभी संसाधन और डेटा स्थायी रूप से हटा दिए जाएंगे। अपना खाता हटाने से पहले, कृपया कोई भी डेटा या जानकारी डाउनलोड करें जिसे आप बनाए रखना चाहते हैं।',
'delete_account_confirm' => 'क्या आप वाकई अपना खाता हटाना चाहते हैं?',
'delete_account_confirm_description' => 'एक बार आपका खाता हटा दिए जाने के बाद, इसके सभी संसाधन और डेटा स्थायी रूप से हटा दिए जाएंगे। कृपया पुष्टि करने के लिए अपना पासवर्ड दर्ज करें कि आप स्थायी रूप से अपना खाता हटाना चाहते हैं।',
],
];

148
src/lang/it/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'Pannello',
'knowledge_base' => 'Base di Conoscenza',
'profile' => 'Profilo',
'user_management' => 'Gestione Utenti',
'logout' => 'Esci',
'login' => 'Accedi',
'register' => 'Registrati',
],
// Documents
'documents' => [
'title' => 'Documenti',
'new_document' => 'Nuovo Documento',
'edit_document' => 'Modifica Documento',
'edit' => 'Modifica',
'delete' => 'Elimina',
'save' => 'Salva',
'cancel' => 'Annulla',
'created_by' => 'Creato da',
'modified_by' => 'da',
'updated' => 'Aggiornato',
'path' => 'Percorso',
'last_modified' => 'Ultima modifica',
'no_documents' => 'Nessun documento trovato',
'search_placeholder' => 'Cerca documenti...',
'create_success' => 'Documento creato con successo!',
'update_success' => 'Documento aggiornato con successo!',
'delete_success' => 'Documento eliminato con successo!',
'delete_confirm' => 'Sei sicuro di voler eliminare questo documento?',
'linked_references' => 'Riferimenti Collegati',
'title_label' => 'Titolo',
'title_placeholder' => 'Titolo del Documento (usa / per le cartelle, es: Laravel/Livewire/Components)',
'title_hint' => 'Suggerimento: Usa le barre (/) nel titolo per organizzare automaticamente i documenti in cartelle',
'content_label' => 'Contenuto',
'content_placeholder' => 'Scrivi il tuo markdown qui...',
'saving' => 'Salvataggio...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'Cambio Rapido',
'placeholder' => 'Cerca documenti...',
'no_results' => 'Nessun documento trovato',
'navigate' => 'per navigare',
'select' => 'per selezionare',
'close' => 'per chiudere',
],
// Admin
'admin' => [
'user_management' => 'Gestione Utenti',
'new_user' => 'Nuovo Utente',
'edit_user' => 'Modifica Utente',
'create_user' => 'Crea Utente',
'users' => 'Utenti',
'name' => 'Nome',
'email' => 'Email',
'password' => 'Password',
'password_confirmation' => 'Conferma Password',
'password_hint' => 'Lascia vuoto per mantenere la password attuale.',
'role' => 'Ruolo',
'admin' => 'Amministratore',
'user' => 'Utente',
'grant_admin' => 'Concedi privilegi di amministratore',
'created_at' => 'Creato',
'actions' => 'Azioni',
'edit' => 'Modifica',
'delete' => 'Elimina',
'no_users' => 'Nessun utente trovato.',
'create_success' => 'Utente creato con successo.',
'update_success' => 'Utente aggiornato con successo.',
'delete_success' => 'Utente eliminato con successo.',
'cannot_delete_self' => 'Non puoi eliminare te stesso.',
'self_admin_warning' => 'Rimuovere i propri privilegi di amministratore bloccherà il tuo accesso al pannello di amministrazione.',
],
// Settings
'settings' => [
'language' => 'Lingua',
'select_language' => 'Seleziona Lingua',
'language_updated' => 'Lingua aggiornata con successo.',
'change_language' => 'Cambia lingua',
],
// Common
'common' => [
'save' => 'Salva',
'cancel' => 'Annulla',
'delete' => 'Elimina',
'edit' => 'Modifica',
'create' => 'Crea',
'update' => 'Aggiorna',
'back' => 'Indietro',
'confirm' => 'Conferma',
'yes' => 'Sì',
'no' => 'No',
'loading' => 'Caricamento...',
'error' => 'Errore',
'success' => 'Successo',
],
// Auth
'auth' => [
'login' => 'Accedi',
'register' => 'Registrati',
'email' => 'Email',
'password' => 'Password',
'remember_me' => 'Ricordami',
'forgot_password' => 'Password dimenticata?',
'confirm_password' => 'Conferma Password',
'already_registered' => 'Già registrato?',
],
// Errors
'errors' => [
'404_title' => 'Pagina Non Trovata',
'page_not_found' => 'Pagina Non Trovata',
'page_not_found_description' => 'La pagina che stai cercando non è stata trovata.',
'back_to_home' => 'Torna alla Home',
],
// Profile
'profile' => [
'title' => 'Profilo',
'information' => 'Informazioni Profilo',
'information_description' => 'Aggiorna le informazioni del profilo e l\'indirizzo email del tuo account.',
'name' => 'Nome',
'email' => 'Email',
'email_unverified' => 'Il tuo indirizzo email non è verificato.',
'resend_verification' => 'Clicca qui per inviare nuovamente l\'email di verifica.',
'verification_sent' => 'Un nuovo link di verifica è stato inviato al tuo indirizzo email.',
'saved' => 'Salvato.',
'update_password' => 'Aggiorna Password',
'update_password_description' => 'Assicurati che il tuo account utilizzi una password lunga e casuale per rimanere sicuro.',
'current_password' => 'Password Attuale',
'new_password' => 'Nuova Password',
'confirm_password' => 'Conferma Password',
'delete_account' => 'Elimina Account',
'delete_account_description' => 'Una volta eliminato il tuo account, tutte le sue risorse e dati saranno cancellati permanentemente. Prima di eliminare il tuo account, scarica eventuali dati o informazioni che desideri conservare.',
'delete_account_confirm' => 'Sei sicuro di voler eliminare il tuo account?',
'delete_account_confirm_description' => 'Una volta eliminato il tuo account, tutte le sue risorse e dati saranno cancellati permanentemente. Inserisci la tua password per confermare che desideri eliminare permanentemente il tuo account.',
],
];

View File

@@ -115,6 +115,14 @@
'already_registered' => '既にアカウントをお持ちですか?', 'already_registered' => '既にアカウントをお持ちですか?',
], ],
// Errors
'errors' => [
'404_title' => 'ページが見つかりません',
'page_not_found' => 'ページが見つかりません',
'page_not_found_description' => 'お探しのページは見つかりませんでした。',
'back_to_home' => 'ホームに戻る',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => 'プロフィール', 'title' => 'プロフィール',

View File

@@ -115,6 +115,14 @@
'already_registered' => '이미 계정이 있으신가요?', 'already_registered' => '이미 계정이 있으신가요?',
], ],
// Errors
'errors' => [
'404_title' => '페이지를 찾을 수 없습니다',
'page_not_found' => '페이지를 찾을 수 없습니다',
'page_not_found_description' => '찾고 계신 페이지를 찾을 수 없습니다.',
'back_to_home' => '홈으로 돌아가기',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => '프로필', 'title' => '프로필',

148
src/lang/pl/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'Panel',
'knowledge_base' => 'Baza wiedzy',
'profile' => 'Profil',
'user_management' => 'Zarządzanie użytkownikami',
'logout' => 'Wyloguj',
'login' => 'Zaloguj',
'register' => 'Zarejestruj',
],
// Documents
'documents' => [
'title' => 'Dokumenty',
'new_document' => 'Nowy dokument',
'edit_document' => 'Edytuj dokument',
'edit' => 'Edytuj',
'delete' => 'Usuń',
'save' => 'Zapisz',
'cancel' => 'Anuluj',
'created_by' => 'Utworzony przez',
'modified_by' => 'przez',
'updated' => 'Zaktualizowano',
'path' => 'Ścieżka',
'last_modified' => 'Ostatnia modyfikacja',
'no_documents' => 'Nie znaleziono dokumentów',
'search_placeholder' => 'Szukaj dokumentów...',
'create_success' => 'Dokument został utworzony!',
'update_success' => 'Dokument został zaktualizowany!',
'delete_success' => 'Dokument został usunięty!',
'delete_confirm' => 'Czy na pewno chcesz usunąć ten dokument?',
'linked_references' => 'Powiązane odniesienia',
'title_label' => 'Tytuł',
'title_placeholder' => 'Tytuł dokumentu (użyj / dla folderów, np. Laravel/Livewire/Components)',
'title_hint' => 'Wskazówka: Użyj ukośników (/) w tytule, aby automatycznie organizować dokumenty w foldery',
'content_label' => 'Treść',
'content_placeholder' => 'Napisz swój markdown tutaj...',
'saving' => 'Zapisywanie...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'Szybkie przełączanie',
'placeholder' => 'Szukaj dokumentów...',
'no_results' => 'Nie znaleziono dokumentów',
'navigate' => 'aby nawigować',
'select' => 'aby wybrać',
'close' => 'aby zamknąć',
],
// Admin
'admin' => [
'user_management' => 'Zarządzanie użytkownikami',
'new_user' => 'Nowy użytkownik',
'edit_user' => 'Edytuj użytkownika',
'create_user' => 'Utwórz użytkownika',
'users' => 'Użytkownicy',
'name' => 'Nazwa',
'email' => 'E-mail',
'password' => 'Hasło',
'password_confirmation' => 'Potwierdź hasło',
'password_hint' => 'Pozostaw puste, aby zachować obecne hasło.',
'role' => 'Rola',
'admin' => 'Administrator',
'user' => 'Użytkownik',
'grant_admin' => 'Przyznaj uprawnienia administratora',
'created_at' => 'Utworzono',
'actions' => 'Działania',
'edit' => 'Edytuj',
'delete' => 'Usuń',
'no_users' => 'Nie znaleziono użytkowników.',
'create_success' => 'Użytkownik został utworzony.',
'update_success' => 'Użytkownik został zaktualizowany.',
'delete_success' => 'Użytkownik został usunięty.',
'cannot_delete_self' => 'Nie możesz usunąć samego siebie.',
'self_admin_warning' => 'Usunięcie własnych uprawnień administratora zablokuje Twój dostęp do panelu administratora.',
],
// Settings
'settings' => [
'language' => 'Język',
'select_language' => 'Wybierz język',
'language_updated' => 'Język został zaktualizowany.',
'change_language' => 'Zmień język',
],
// Common
'common' => [
'save' => 'Zapisz',
'cancel' => 'Anuluj',
'delete' => 'Usuń',
'edit' => 'Edytuj',
'create' => 'Utwórz',
'update' => 'Aktualizuj',
'back' => 'Wstecz',
'confirm' => 'Potwierdź',
'yes' => 'Tak',
'no' => 'Nie',
'loading' => 'Ładowanie...',
'error' => 'Błąd',
'success' => 'Sukces',
],
// Auth
'auth' => [
'login' => 'Zaloguj',
'register' => 'Zarejestruj',
'email' => 'E-mail',
'password' => 'Hasło',
'remember_me' => 'Zapamiętaj mnie',
'forgot_password' => 'Zapomniałeś hasła?',
'confirm_password' => 'Potwierdź hasło',
'already_registered' => 'Już zarejestrowany?',
],
// Errors
'errors' => [
'404_title' => 'Strona nie znaleziona',
'page_not_found' => 'Strona nie znaleziona',
'page_not_found_description' => 'Strona, której szukasz, nie została znaleziona.',
'back_to_home' => 'Wróć do strony głównej',
],
// Profile
'profile' => [
'title' => 'Profil',
'information' => 'Informacje o profilu',
'information_description' => 'Zaktualizuj informacje o profilu i adres e-mail swojego konta.',
'name' => 'Nazwa',
'email' => 'E-mail',
'email_unverified' => 'Twój adres e-mail nie został zweryfikowany.',
'resend_verification' => 'Kliknij tutaj, aby ponownie wysłać e-mail weryfikacyjny.',
'verification_sent' => 'Nowy link weryfikacyjny został wysłany na Twój adres e-mail.',
'saved' => 'Zapisano.',
'update_password' => 'Zaktualizuj hasło',
'update_password_description' => 'Upewnij się, że Twoje konto używa długiego, losowego hasła, aby pozostać bezpiecznym.',
'current_password' => 'Obecne hasło',
'new_password' => 'Nowe hasło',
'confirm_password' => 'Potwierdź hasło',
'delete_account' => 'Usuń konto',
'delete_account_description' => 'Po usunięciu konta wszystkie jego zasoby i dane zostaną trwale usunięte. Przed usunięciem konta pobierz wszelkie dane lub informacje, które chcesz zachować.',
'delete_account_confirm' => 'Czy na pewno chcesz usunąć swoje konto?',
'delete_account_confirm_description' => 'Po usunięciu konta wszystkie jego zasoby i dane zostaną trwale usunięte. Wprowadź swoje hasło, aby potwierdzić, że chcesz trwale usunąć swoje konto.',
],
];

148
src/lang/pt-BR/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'Painel',
'knowledge_base' => 'Base de Conhecimento',
'profile' => 'Perfil',
'user_management' => 'Gerenciamento de Usuários',
'logout' => 'Sair',
'login' => 'Entrar',
'register' => 'Registrar',
],
// Documents
'documents' => [
'title' => 'Documentos',
'new_document' => 'Novo Documento',
'edit_document' => 'Editar Documento',
'edit' => 'Editar',
'delete' => 'Excluir',
'save' => 'Salvar',
'cancel' => 'Cancelar',
'created_by' => 'Criado por',
'modified_by' => 'por',
'updated' => 'Atualizado',
'path' => 'Caminho',
'last_modified' => 'Última modificação',
'no_documents' => 'Nenhum documento encontrado',
'search_placeholder' => 'Pesquisar documentos...',
'create_success' => 'Documento criado com sucesso!',
'update_success' => 'Documento atualizado com sucesso!',
'delete_success' => 'Documento excluído com sucesso!',
'delete_confirm' => 'Tem certeza de que deseja excluir este documento?',
'linked_references' => 'Referências Vinculadas',
'title_label' => 'Título',
'title_placeholder' => 'Título do Documento (use / para pastas, ex: Laravel/Livewire/Components)',
'title_hint' => 'Dica: Use barras (/) no título para organizar documentos em pastas automaticamente',
'content_label' => 'Conteúdo',
'content_placeholder' => 'Escreva seu markdown aqui...',
'saving' => 'Salvando...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'Troca Rápida',
'placeholder' => 'Pesquisar documentos...',
'no_results' => 'Nenhum documento encontrado',
'navigate' => 'para navegar',
'select' => 'para selecionar',
'close' => 'para fechar',
],
// Admin
'admin' => [
'user_management' => 'Gerenciamento de Usuários',
'new_user' => 'Novo Usuário',
'edit_user' => 'Editar Usuário',
'create_user' => 'Criar Usuário',
'users' => 'Usuários',
'name' => 'Nome',
'email' => 'E-mail',
'password' => 'Senha',
'password_confirmation' => 'Confirmar Senha',
'password_hint' => 'Deixe em branco para manter a senha atual.',
'role' => 'Função',
'admin' => 'Administrador',
'user' => 'Usuário',
'grant_admin' => 'Conceder privilégios de administrador',
'created_at' => 'Criado',
'actions' => 'Ações',
'edit' => 'Editar',
'delete' => 'Excluir',
'no_users' => 'Nenhum usuário encontrado.',
'create_success' => 'Usuário criado com sucesso.',
'update_success' => 'Usuário atualizado com sucesso.',
'delete_success' => 'Usuário excluído com sucesso.',
'cannot_delete_self' => 'Você não pode excluir a si mesmo.',
'self_admin_warning' => 'Remover seus próprios privilégios de administrador bloqueará seu acesso ao painel de administração.',
],
// Settings
'settings' => [
'language' => 'Idioma',
'select_language' => 'Selecionar Idioma',
'language_updated' => 'Idioma atualizado com sucesso.',
'change_language' => 'Alterar idioma',
],
// Common
'common' => [
'save' => 'Salvar',
'cancel' => 'Cancelar',
'delete' => 'Excluir',
'edit' => 'Editar',
'create' => 'Criar',
'update' => 'Atualizar',
'back' => 'Voltar',
'confirm' => 'Confirmar',
'yes' => 'Sim',
'no' => 'Não',
'loading' => 'Carregando...',
'error' => 'Erro',
'success' => 'Sucesso',
],
// Auth
'auth' => [
'login' => 'Entrar',
'register' => 'Registrar',
'email' => 'E-mail',
'password' => 'Senha',
'remember_me' => 'Lembrar-me',
'forgot_password' => 'Esqueceu sua senha?',
'confirm_password' => 'Confirmar Senha',
'already_registered' => 'Já registrado?',
],
// Errors
'errors' => [
'404_title' => 'Página Não Encontrada',
'page_not_found' => 'Página Não Encontrada',
'page_not_found_description' => 'A página que você está procurando não pôde ser encontrada.',
'back_to_home' => 'Voltar para Início',
],
// Profile
'profile' => [
'title' => 'Perfil',
'information' => 'Informações do Perfil',
'information_description' => 'Atualize as informações do perfil e endereço de e-mail da sua conta.',
'name' => 'Nome',
'email' => 'E-mail',
'email_unverified' => 'Seu endereço de e-mail não está verificado.',
'resend_verification' => 'Clique aqui para reenviar o e-mail de verificação.',
'verification_sent' => 'Um novo link de verificação foi enviado para seu endereço de e-mail.',
'saved' => 'Salvo.',
'update_password' => 'Atualizar Senha',
'update_password_description' => 'Certifique-se de que sua conta esteja usando uma senha longa e aleatória para se manter segura.',
'current_password' => 'Senha Atual',
'new_password' => 'Nova Senha',
'confirm_password' => 'Confirmar Senha',
'delete_account' => 'Excluir Conta',
'delete_account_description' => 'Uma vez que sua conta seja excluída, todos os seus recursos e dados serão permanentemente excluídos. Antes de excluir sua conta, faça o download de quaisquer dados ou informações que deseja reter.',
'delete_account_confirm' => 'Tem certeza de que deseja excluir sua conta?',
'delete_account_confirm_description' => 'Uma vez que sua conta seja excluída, todos os seus recursos e dados serão permanentemente excluídos. Por favor, insira sua senha para confirmar que deseja excluir permanentemente sua conta.',
],
];

148
src/lang/ru/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'Панель управления',
'knowledge_base' => 'База знаний',
'profile' => 'Профиль',
'user_management' => 'Управление пользователями',
'logout' => 'Выйти',
'login' => 'Войти',
'register' => 'Регистрация',
],
// Documents
'documents' => [
'title' => 'Документы',
'new_document' => 'Новый документ',
'edit_document' => 'Редактировать документ',
'edit' => 'Редактировать',
'delete' => 'Удалить',
'save' => 'Сохранить',
'cancel' => 'Отмена',
'created_by' => 'Создано',
'modified_by' => '',
'updated' => 'Обновлено',
'path' => 'Путь',
'last_modified' => 'Последнее изменение',
'no_documents' => 'Документы не найдены',
'search_placeholder' => 'Поиск документов...',
'create_success' => 'Документ успешно создан!',
'update_success' => 'Документ успешно обновлён!',
'delete_success' => 'Документ успешно удалён!',
'delete_confirm' => 'Вы уверены, что хотите удалить этот документ?',
'linked_references' => 'Связанные ссылки',
'title_label' => 'Название',
'title_placeholder' => 'Название документа (используйте / для папок, например Laravel/Livewire/Components)',
'title_hint' => 'Совет: Используйте слэши (/) в названии для автоматической организации документов в папки',
'content_label' => 'Содержимое',
'content_placeholder' => 'Напишите здесь ваш markdown...',
'saving' => 'Сохранение...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'Быстрый переход',
'placeholder' => 'Поиск документов...',
'no_results' => 'Документы не найдены',
'navigate' => 'для навигации',
'select' => 'для выбора',
'close' => 'для закрытия',
],
// Admin
'admin' => [
'user_management' => 'Управление пользователями',
'new_user' => 'Новый пользователь',
'edit_user' => 'Редактировать пользователя',
'create_user' => 'Создать пользователя',
'users' => 'Пользователи',
'name' => 'Имя',
'email' => 'Email',
'password' => 'Пароль',
'password_confirmation' => 'Подтверждение пароля',
'password_hint' => 'Оставьте пустым, чтобы сохранить текущий пароль.',
'role' => 'Роль',
'admin' => 'Администратор',
'user' => 'Пользователь',
'grant_admin' => 'Предоставить права администратора',
'created_at' => 'Создан',
'actions' => 'Действия',
'edit' => 'Редактировать',
'delete' => 'Удалить',
'no_users' => 'Пользователи не найдены.',
'create_success' => 'Пользователь успешно создан.',
'update_success' => 'Пользователь успешно обновлён.',
'delete_success' => 'Пользователь успешно удалён.',
'cannot_delete_self' => 'Вы не можете удалить себя.',
'self_admin_warning' => 'Снятие собственных прав администратора заблокирует вам доступ к панели администратора.',
],
// Settings
'settings' => [
'language' => 'Язык',
'select_language' => 'Выбрать язык',
'language_updated' => 'Язык успешно обновлён.',
'change_language' => 'Изменить язык',
],
// Common
'common' => [
'save' => 'Сохранить',
'cancel' => 'Отмена',
'delete' => 'Удалить',
'edit' => 'Редактировать',
'create' => 'Создать',
'update' => 'Обновить',
'back' => 'Назад',
'confirm' => 'Подтвердить',
'yes' => 'Да',
'no' => 'Нет',
'loading' => 'Загрузка...',
'error' => 'Ошибка',
'success' => 'Успех',
],
// Auth
'auth' => [
'login' => 'Войти',
'register' => 'Регистрация',
'email' => 'Email',
'password' => 'Пароль',
'remember_me' => 'Запомнить меня',
'forgot_password' => 'Забыли пароль?',
'confirm_password' => 'Подтверждение пароля',
'already_registered' => 'Уже зарегистрированы?',
],
// Errors
'errors' => [
'404_title' => 'Страница не найдена',
'page_not_found' => 'Страница не найдена',
'page_not_found_description' => 'Страница, которую вы ищете, не найдена.',
'back_to_home' => 'Вернуться на главную',
],
// Profile
'profile' => [
'title' => 'Профиль',
'information' => 'Информация профиля',
'information_description' => 'Обновите информацию профиля и адрес электронной почты вашей учётной записи.',
'name' => 'Имя',
'email' => 'Email',
'email_unverified' => 'Ваш адрес электронной почты не подтверждён.',
'resend_verification' => 'Нажмите здесь, чтобы отправить письмо с подтверждением повторно.',
'verification_sent' => 'Новая ссылка для подтверждения отправлена на ваш адрес электронной почты.',
'saved' => 'Сохранено.',
'update_password' => 'Обновить пароль',
'update_password_description' => 'Убедитесь, что ваша учётная запись использует длинный случайный пароль для обеспечения безопасности.',
'current_password' => 'Текущий пароль',
'new_password' => 'Новый пароль',
'confirm_password' => 'Подтверждение пароля',
'delete_account' => 'Удалить учётную запись',
'delete_account_description' => 'После удаления вашей учётной записи все её ресурсы и данные будут удалены безвозвратно. Перед удалением учётной записи загрузите любые данные или информацию, которые вы хотите сохранить.',
'delete_account_confirm' => 'Вы уверены, что хотите удалить свою учётную запись?',
'delete_account_confirm_description' => 'После удаления вашей учётной записи все её ресурсы и данные будут удалены безвозвратно. Пожалуйста, введите свой пароль, чтобы подтвердить, что вы хотите навсегда удалить свою учётную запись.',
],
];

148
src/lang/tr/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'Panel',
'knowledge_base' => 'Bilgi Tabanı',
'profile' => 'Profil',
'user_management' => 'Kullanıcı Yönetimi',
'logout' => ıkış',
'login' => 'Giriş',
'register' => 'Kayıt',
],
// Documents
'documents' => [
'title' => 'Belgeler',
'new_document' => 'Yeni Belge',
'edit_document' => 'Belgeyi Düzenle',
'edit' => 'Düzenle',
'delete' => 'Sil',
'save' => 'Kaydet',
'cancel' => 'İptal',
'created_by' => 'Oluşturan',
'modified_by' => 'tarafından',
'updated' => 'Güncellendi',
'path' => 'Yol',
'last_modified' => 'Son değişiklik',
'no_documents' => 'Belge bulunamadı',
'search_placeholder' => 'Belge ara...',
'create_success' => 'Belge başarıyla oluşturuldu!',
'update_success' => 'Belge başarıyla güncellendi!',
'delete_success' => 'Belge başarıyla silindi!',
'delete_confirm' => 'Bu belgeyi silmek istediğinizden emin misiniz?',
'linked_references' => 'Bağlantılı Referanslar',
'title_label' => 'Başlık',
'title_placeholder' => 'Belge Başlığı (klasörler için / kullanın, örn: Laravel/Livewire/Components)',
'title_hint' => 'İpucu: Belgeleri otomatik olarak klasörlere düzenlemek için başlıkta eğik çizgi (/) kullanın',
'content_label' => 'İçerik',
'content_placeholder' => 'Markdown\'ınızı buraya yazın...',
'saving' => 'Kaydediliyor...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'Hızlı Geçiş',
'placeholder' => 'Belge ara...',
'no_results' => 'Belge bulunamadı',
'navigate' => 'gezinmek için',
'select' => 'seçmek için',
'close' => 'kapatmak için',
],
// Admin
'admin' => [
'user_management' => 'Kullanıcı Yönetimi',
'new_user' => 'Yeni Kullanıcı',
'edit_user' => 'Kullanıcıyı Düzenle',
'create_user' => 'Kullanıcı Oluştur',
'users' => 'Kullanıcılar',
'name' => 'Ad',
'email' => 'E-posta',
'password' => 'Şifre',
'password_confirmation' => 'Şifre Onayı',
'password_hint' => 'Mevcut şifreyi korumak için boş bırakın.',
'role' => 'Rol',
'admin' => 'Yönetici',
'user' => 'Kullanıcı',
'grant_admin' => 'Yönetici ayrıcalıkları ver',
'created_at' => 'Oluşturuldu',
'actions' => 'İşlemler',
'edit' => 'Düzenle',
'delete' => 'Sil',
'no_users' => 'Kullanıcı bulunamadı.',
'create_success' => 'Kullanıcı başarıyla oluşturuldu.',
'update_success' => 'Kullanıcı başarıyla güncellendi.',
'delete_success' => 'Kullanıcı başarıyla silindi.',
'cannot_delete_self' => 'Kendinizi silemezsiniz.',
'self_admin_warning' => 'Kendi yönetici ayrıcalıklarınızı kaldırmak yönetici paneline erişiminizi engelleyecektir.',
],
// Settings
'settings' => [
'language' => 'Dil',
'select_language' => 'Dil Seç',
'language_updated' => 'Dil başarıyla güncellendi.',
'change_language' => 'Dili değiştir',
],
// Common
'common' => [
'save' => 'Kaydet',
'cancel' => 'İptal',
'delete' => 'Sil',
'edit' => 'Düzenle',
'create' => 'Oluştur',
'update' => 'Güncelle',
'back' => 'Geri',
'confirm' => 'Onayla',
'yes' => 'Evet',
'no' => 'Hayır',
'loading' => 'Yükleniyor...',
'error' => 'Hata',
'success' => 'Başarılı',
],
// Auth
'auth' => [
'login' => 'Giriş',
'register' => 'Kayıt',
'email' => 'E-posta',
'password' => 'Şifre',
'remember_me' => 'Beni hatırla',
'forgot_password' => 'Şifrenizi mi unuttunuz?',
'confirm_password' => 'Şifre Onayı',
'already_registered' => 'Zaten kayıtlı mısınız?',
],
// Errors
'errors' => [
'404_title' => 'Sayfa Bulunamadı',
'page_not_found' => 'Sayfa Bulunamadı',
'page_not_found_description' => 'Aradığınız sayfa bulunamadı.',
'back_to_home' => 'Ana Sayfaya Dön',
],
// Profile
'profile' => [
'title' => 'Profil',
'information' => 'Profil Bilgileri',
'information_description' => 'Hesabınızın profil bilgilerini ve e-posta adresini güncelleyin.',
'name' => 'Ad',
'email' => 'E-posta',
'email_unverified' => 'E-posta adresiniz doğrulanmamış.',
'resend_verification' => 'Doğrulama e-postasını yeniden göndermek için buraya tıklayın.',
'verification_sent' => 'E-posta adresinize yeni bir doğrulama bağlantısı gönderildi.',
'saved' => 'Kaydedildi.',
'update_password' => 'Şifreyi Güncelle',
'update_password_description' => 'Hesabınızın güvende kalması için uzun, rastgele bir şifre kullandığından emin olun.',
'current_password' => 'Mevcut Şifre',
'new_password' => 'Yeni Şifre',
'confirm_password' => 'Şifre Onayı',
'delete_account' => 'Hesabı Sil',
'delete_account_description' => 'Hesabınız silindikten sonra, tüm kaynakları ve verileri kalıcı olarak silinecektir. Hesabınızı silmeden önce, saklamak istediğiniz tüm verileri veya bilgileri indirin.',
'delete_account_confirm' => 'Hesabınızı silmek istediğinizden emin misiniz?',
'delete_account_confirm_description' => 'Hesabınız silindikten sonra, tüm kaynakları ve verileri kalıcı olarak silinecektir. Hesabınızı kalıcı olarak silmek istediğinizi onaylamak için lütfen şifrenizi girin.',
],
];

148
src/lang/uk/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'Панель керування',
'knowledge_base' => 'База знань',
'profile' => 'Профіль',
'user_management' => 'Управління користувачами',
'logout' => 'Вийти',
'login' => 'Увійти',
'register' => 'Реєстрація',
],
// Documents
'documents' => [
'title' => 'Документи',
'new_document' => 'Новий документ',
'edit_document' => 'Редагувати документ',
'edit' => 'Редагувати',
'delete' => 'Видалити',
'save' => 'Зберегти',
'cancel' => 'Скасувати',
'created_by' => 'Створено',
'modified_by' => '',
'updated' => 'Оновлено',
'path' => 'Шлях',
'last_modified' => 'Остання зміна',
'no_documents' => 'Документи не знайдено',
'search_placeholder' => 'Пошук документів...',
'create_success' => 'Документ успішно створено!',
'update_success' => 'Документ успішно оновлено!',
'delete_success' => 'Документ успішно видалено!',
'delete_confirm' => 'Ви впевнені, що хочете видалити цей документ?',
'linked_references' => "Пов'язані посилання",
'title_label' => 'Назва',
'title_placeholder' => 'Назва документа (використовуйте / для папок, наприклад Laravel/Livewire/Components)',
'title_hint' => 'Порада: Використовуйте слеші (/) у назві для автоматичної організації документів у папки',
'content_label' => 'Вміст',
'content_placeholder' => 'Напишіть тут ваш markdown...',
'saving' => 'Збереження...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'Швидкий перехід',
'placeholder' => 'Пошук документів...',
'no_results' => 'Документи не знайдено',
'navigate' => 'для навігації',
'select' => 'для вибору',
'close' => 'для закриття',
],
// Admin
'admin' => [
'user_management' => 'Управління користувачами',
'new_user' => 'Новий користувач',
'edit_user' => 'Редагувати користувача',
'create_user' => 'Створити користувача',
'users' => 'Користувачі',
'name' => "Ім'я",
'email' => 'Email',
'password' => 'Пароль',
'password_confirmation' => 'Підтвердження пароля',
'password_hint' => 'Залиште порожнім, щоб зберегти поточний пароль.',
'role' => 'Роль',
'admin' => 'Адміністратор',
'user' => 'Користувач',
'grant_admin' => 'Надати права адміністратора',
'created_at' => 'Створено',
'actions' => 'Дії',
'edit' => 'Редагувати',
'delete' => 'Видалити',
'no_users' => 'Користувачі не знайдені.',
'create_success' => 'Користувача успішно створено.',
'update_success' => 'Користувача успішно оновлено.',
'delete_success' => 'Користувача успішно видалено.',
'cannot_delete_self' => 'Ви не можете видалити себе.',
'self_admin_warning' => 'Зняття власних прав адміністратора заблокує вам доступ до панелі адміністратора.',
],
// Settings
'settings' => [
'language' => 'Мова',
'select_language' => 'Вибрати мову',
'language_updated' => 'Мову успішно оновлено.',
'change_language' => 'Змінити мову',
],
// Common
'common' => [
'save' => 'Зберегти',
'cancel' => 'Скасувати',
'delete' => 'Видалити',
'edit' => 'Редагувати',
'create' => 'Створити',
'update' => 'Оновити',
'back' => 'Назад',
'confirm' => 'Підтвердити',
'yes' => 'Так',
'no' => 'Ні',
'loading' => 'Завантаження...',
'error' => 'Помилка',
'success' => 'Успіх',
],
// Auth
'auth' => [
'login' => 'Увійти',
'register' => 'Реєстрація',
'email' => 'Email',
'password' => 'Пароль',
'remember_me' => "Запам'ятати мене",
'forgot_password' => 'Забули пароль?',
'confirm_password' => 'Підтвердження пароля',
'already_registered' => 'Вже зареєстровані?',
],
// Errors
'errors' => [
'404_title' => 'Сторінка не знайдена',
'page_not_found' => 'Сторінка не знайдена',
'page_not_found_description' => 'Сторінку, яку ви шукаєте, не знайдено.',
'back_to_home' => 'Повернутися на головну',
],
// Profile
'profile' => [
'title' => 'Профіль',
'information' => 'Інформація профілю',
'information_description' => 'Оновіть інформацію профілю та адресу електронної пошти вашого облікового запису.',
'name' => "Ім'я",
'email' => 'Email',
'email_unverified' => 'Ваша адреса електронної пошти не підтверджена.',
'resend_verification' => 'Натисніть тут, щоб повторно відправити лист з підтвердженням.',
'verification_sent' => 'Нове посилання для підтвердження відправлено на вашу адресу електронної пошти.',
'saved' => 'Збережено.',
'update_password' => 'Оновити пароль',
'update_password_description' => 'Переконайтеся, що ваш обліковий запис використовує довгий випадковий пароль для забезпечення безпеки.',
'current_password' => 'Поточний пароль',
'new_password' => 'Новий пароль',
'confirm_password' => 'Підтвердження пароля',
'delete_account' => 'Видалити обліковий запис',
'delete_account_description' => 'Після видалення вашого облікового запису всі його ресурси та дані будуть видалені назавжди. Перед видаленням облікового запису завантажте будь-які дані або інформацію, які ви хочете зберегти.',
'delete_account_confirm' => 'Ви впевнені, що хочете видалити свій обліковий запис?',
'delete_account_confirm_description' => 'Після видалення вашого облікового запису всі його ресурси та дані будуть видалені назавжди. Будь ласка, введіть свій пароль, щоб підтвердити, що ви хочете назавжди видалити свій обліковий запис.',
],
];

148
src/lang/vi/messages.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return [
// Navigation
'nav' => [
'dashboard' => 'Bảng điều khiển',
'knowledge_base' => 'Cơ sở tri thức',
'profile' => 'Hồ sơ',
'user_management' => 'Quản lý người dùng',
'logout' => 'Đăng xuất',
'login' => 'Đăng nhập',
'register' => 'Đăng ký',
],
// Documents
'documents' => [
'title' => 'Tài liệu',
'new_document' => 'Tài liệu mới',
'edit_document' => 'Chỉnh sửa tài liệu',
'edit' => 'Chỉnh sửa',
'delete' => 'Xóa',
'save' => 'Lưu',
'cancel' => 'Hủy',
'created_by' => 'Được tạo bởi',
'modified_by' => 'bởi',
'updated' => 'Đã cập nhật',
'path' => 'Đường dẫn',
'last_modified' => 'Sửa đổi lần cuối',
'no_documents' => 'Không tìm thấy tài liệu',
'search_placeholder' => 'Tìm kiếm tài liệu...',
'create_success' => 'Tạo tài liệu thành công!',
'update_success' => 'Cập nhật tài liệu thành công!',
'delete_success' => 'Xóa tài liệu thành công!',
'delete_confirm' => 'Bạn có chắc chắn muốn xóa tài liệu này?',
'linked_references' => 'Tham chiếu liên kết',
'title_label' => 'Tiêu đề',
'title_placeholder' => 'Tiêu đề tài liệu (sử dụng / cho thư mục, ví dụ: Laravel/Livewire/Components)',
'title_hint' => 'Mẹo: Sử dụng dấu gạch chéo (/) trong tiêu đề để tự động sắp xếp tài liệu vào các thư mục',
'content_label' => 'Nội dung',
'content_placeholder' => 'Viết markdown của bạn ở đây...',
'saving' => 'Đang lưu...',
],
// Quick Switcher
'quick_switcher' => [
'title' => 'Chuyển đổi nhanh',
'placeholder' => 'Tìm kiếm tài liệu...',
'no_results' => 'Không tìm thấy tài liệu',
'navigate' => 'để điều hướng',
'select' => 'để chọn',
'close' => 'để đóng',
],
// Admin
'admin' => [
'user_management' => 'Quản lý người dùng',
'new_user' => 'Người dùng mới',
'edit_user' => 'Chỉnh sửa người dùng',
'create_user' => 'Tạo người dùng',
'users' => 'Người dùng',
'name' => 'Tên',
'email' => 'Email',
'password' => 'Mật khẩu',
'password_confirmation' => 'Xác nhận mật khẩu',
'password_hint' => 'Để trống để giữ mật khẩu hiện tại.',
'role' => 'Vai trò',
'admin' => 'Quản trị viên',
'user' => 'Người dùng',
'grant_admin' => 'Cấp quyền quản trị viên',
'created_at' => 'Đã tạo',
'actions' => 'Hành động',
'edit' => 'Chỉnh sửa',
'delete' => 'Xóa',
'no_users' => 'Không tìm thấy người dùng.',
'create_success' => 'Tạo người dùng thành công.',
'update_success' => 'Cập nhật người dùng thành công.',
'delete_success' => 'Xóa người dùng thành công.',
'cannot_delete_self' => 'Bạn không thể xóa chính mình.',
'self_admin_warning' => 'Xóa quyền quản trị viên của chính bạn sẽ khóa quyền truy cập của bạn vào bảng quản trị.',
],
// Settings
'settings' => [
'language' => 'Ngôn ngữ',
'select_language' => 'Chọn ngôn ngữ',
'language_updated' => 'Cập nhật ngôn ngữ thành công.',
'change_language' => 'Thay đổi ngôn ngữ',
],
// Common
'common' => [
'save' => 'Lưu',
'cancel' => 'Hủy',
'delete' => 'Xóa',
'edit' => 'Chỉnh sửa',
'create' => 'Tạo',
'update' => 'Cập nhật',
'back' => 'Quay lại',
'confirm' => 'Xác nhận',
'yes' => 'Có',
'no' => 'Không',
'loading' => 'Đang tải...',
'error' => 'Lỗi',
'success' => 'Thành công',
],
// Auth
'auth' => [
'login' => 'Đăng nhập',
'register' => 'Đăng ký',
'email' => 'Email',
'password' => 'Mật khẩu',
'remember_me' => 'Ghi nhớ tôi',
'forgot_password' => 'Quên mật khẩu?',
'confirm_password' => 'Xác nhận mật khẩu',
'already_registered' => 'Đã đăng ký?',
],
// Errors
'errors' => [
'404_title' => 'Không tìm thấy trang',
'page_not_found' => 'Không tìm thấy trang',
'page_not_found_description' => 'Trang bạn đang tìm kiếm không thể tìm thấy.',
'back_to_home' => 'Quay lại trang chủ',
],
// Profile
'profile' => [
'title' => 'Hồ sơ',
'information' => 'Thông tin hồ sơ',
'information_description' => 'Cập nhật thông tin hồ sơ và địa chỉ email của tài khoản của bạn.',
'name' => 'Tên',
'email' => 'Email',
'email_unverified' => 'Địa chỉ email của bạn chưa được xác minh.',
'resend_verification' => 'Nhấp vào đây để gửi lại email xác minh.',
'verification_sent' => 'Một liên kết xác minh mới đã được gửi đến địa chỉ email của bạn.',
'saved' => 'Đã lưu.',
'update_password' => 'Cập nhật mật khẩu',
'update_password_description' => 'Đảm bảo tài khoản của bạn đang sử dụng mật khẩu dài, ngẫu nhiên để luôn an toàn.',
'current_password' => 'Mật khẩu hiện tại',
'new_password' => 'Mật khẩu mới',
'confirm_password' => 'Xác nhận mật khẩu',
'delete_account' => 'Xóa tài khoản',
'delete_account_description' => 'Sau khi tài khoản của bạn bị xóa, tất cả tài nguyên và dữ liệu của nó sẽ bị xóa vĩnh viễn. Trước khi xóa tài khoản của bạn, vui lòng tải xuống bất kỳ dữ liệu hoặc thông tin nào mà bạn muốn giữ lại.',
'delete_account_confirm' => 'Bạn có chắc chắn muốn xóa tài khoản của mình?',
'delete_account_confirm_description' => 'Sau khi tài khoản của bạn bị xóa, tất cả tài nguyên và dữ liệu của nó sẽ bị xóa vĩnh viễn. Vui lòng nhập mật khẩu của bạn để xác nhận rằng bạn muốn xóa vĩnh viễn tài khoản của mình.',
],
];

View File

@@ -115,6 +115,14 @@
'already_registered' => '已有账号?', 'already_registered' => '已有账号?',
], ],
// Errors
'errors' => [
'404_title' => '页面未找到',
'page_not_found' => '页面未找到',
'page_not_found_description' => '您要查找的页面不存在。',
'back_to_home' => '返回首页',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => '个人资料', 'title' => '个人资料',

View File

@@ -115,6 +115,14 @@
'already_registered' => '已有帳號?', 'already_registered' => '已有帳號?',
], ],
// Errors
'errors' => [
'404_title' => '頁面未找到',
'page_not_found' => '頁面未找到',
'page_not_found_description' => '您要查找的頁面不存在。',
'back_to_home' => '返回首頁',
],
// Profile // Profile
'profile' => [ 'profile' => [
'title' => '個人資料', 'title' => '個人資料',

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ __('messages.errors.404_title') }} - {{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-9xl font-bold text-gray-300">404</h1>
<h2 class="text-2xl font-semibold text-gray-700 mt-4">
{{ __('messages.errors.page_not_found') }}
</h2>
<p class="text-gray-500 mt-2">
{{ __('messages.errors.page_not_found_description') }}
</p>
<div class="mt-8">
<a href="{{ url('/') }}" class="inline-flex items-center px-6 py-3 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 transition">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
{{ __('messages.errors.back_to_home') }}
</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -17,7 +17,7 @@
<body class="font-sans text-gray-900 antialiased"> <body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100"> <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div> <div>
<a href="/"> <a href="{{ url('/') }}">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" /> <x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a> </a>
</div> </div>

View File

@@ -27,38 +27,118 @@
@livewireStyles @livewireStyles
@stack('styles') @stack('styles')
</head> </head>
<body class="font-sans antialiased"> <body class="font-sans antialiased" x-data="{
mobileMenuOpen: false,
sidebarWidth: localStorage.getItem('kb_sidebar_width') || 320,
isResizing: false,
startResize(e) {
if (window.innerWidth < 1024) return; // lg breakpoint
this.isResizing = true;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
},
resize(e) {
if (!this.isResizing) return;
const newWidth = Math.max(200, Math.min(600, e.clientX));
this.sidebarWidth = newWidth;
localStorage.setItem('kb_sidebar_width', newWidth);
},
stopResize() {
if (this.isResizing) {
this.isResizing = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
}
}" @mousemove.window="resize($event)" @mouseup.window="stopResize()">
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50">
<!-- Header --> <!-- Header -->
<header class="bg-white border-b border-gray-200 sticky top-0 z-10"> <header class="bg-white border-b border-gray-200 sticky top-0 z-20">
<div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8"> <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 justify-between h-16">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<a href="/" class="flex items-center space-x-3"> <!-- Mobile Menu Toggle -->
<x-application-logo class="block h-8 w-auto fill-current text-gray-800" /> <button
<h1 class="text-xl font-semibold text-gray-900"> @click="mobileMenuOpen = !mobileMenuOpen"
class="lg:hidden p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500"
aria-label="Toggle menu"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path x-show="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
<path x-show="mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<a href="{{ url('/') }}" class="flex items-center space-x-2 sm:space-x-3">
<x-application-logo class="block h-6 sm:h-8 w-auto fill-current text-gray-800" />
<h1 class="text-lg sm:text-xl font-semibold text-gray-900 hidden xs:block">
{{ config('app.name', 'Knowledge Base') }} {{ config('app.name', 'Knowledge Base') }}
</h1> </h1>
</a> </a>
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-2 sm:space-x-4">
<!-- Quick Switcher Trigger --> <!-- Quick Switcher Trigger -->
<button <button
type="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" class="inline-flex items-center px-2 sm: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"
x-data x-data
@click.prevent="$dispatch('open-quick-switcher')" @click.prevent="$dispatch('open-quick-switcher')"
> >
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 sm: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> <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> </svg>
{{ __('messages.quick_switcher.title') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.title') }}</span>
<kbd class="ml-2 px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded"> <kbd class="hidden md:inline-flex ml-2 px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded">
Ctrl+K Ctrl+K
</kbd> </kbd>
</button> </button>
<!-- Language Switcher (for all users) -->
<div x-data="{ open: false }" @click.away="open = false" class="relative">
<button
@click="open = !open"
class="flex items-center px-2 sm:px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none"
title="{{ __('messages.settings.change_language') }}"
>
<svg class="w-4 h-4 sm:w-5 sm:h-5 sm:mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"></path>
</svg>
@php
$currentLocale = app()->getLocale();
$locales = \App\Http\Middleware\SetLocale::SUPPORTED_LOCALES;
@endphp
<span class="hidden sm:inline">{{ $locales[$currentLocale] ?? 'English' }}</span>
<svg class="ml-1 h-4 w-4 hidden sm:block" 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 max-h-96 overflow-y-auto z-50"
>
@foreach($locales as $code => $name)
<form method="POST" action="{{ route('locale.update') }}" class="inline-block w-full">
@csrf
<input type="hidden" name="locale" value="{{ $code }}">
<button
type="submit"
class="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 {{ $currentLocale === $code ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-gray-700' }}"
>
{{ $name }}
@if($currentLocale === $code)
<svg class="inline-block w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
@endif
</button>
</form>
@endforeach
</div>
</div>
@auth @auth
<!-- User Dropdown --> <!-- User Dropdown -->
<div x-data="{ open: false }" @click.away="open = false" class="relative"> <div x-data="{ open: false }" @click.away="open = false" class="relative">
@@ -66,8 +146,11 @@ class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-
@click="open = !open" @click="open = !open"
class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none" class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none"
> >
{{ Auth::user()->name }} <span class="hidden md:inline">{{ Auth::user()->name }}</span>
<svg class="ml-1 h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> <span class="md:hidden w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-700 font-semibold">
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
</span>
<svg class="ml-1 h-4 w-4 hidden md:block" 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> <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> </svg>
</button> </button>
@@ -75,7 +158,7 @@ class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900 f
<div <div
x-show="open" x-show="open"
x-transition 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" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50"
> >
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"> <a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
{{ __('messages.nav.profile') }} {{ __('messages.nav.profile') }}
@@ -99,9 +182,14 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
</div> </div>
</div> </div>
@else @else
<a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900"> <a href="{{ route('login') }}" class="text-sm text-gray-700 hover:text-gray-900 hidden sm:block">
{{ __('messages.nav.login') }} {{ __('messages.nav.login') }}
</a> </a>
<a href="{{ route('login') }}" class="sm:hidden p-2 text-gray-700 hover:bg-gray-100 rounded-md" title="{{ __('messages.nav.login') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
</a>
@endauth @endauth
</div> </div>
</div> </div>
@@ -110,8 +198,50 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
<!-- Main Content --> <!-- Main Content -->
<div class="flex h-[calc(100vh-4rem)]"> <div class="flex h-[calc(100vh-4rem)]">
<!-- Sidebar --> <!-- Sidebar - Desktop -->
<aside class="w-64 bg-white border-r border-gray-200 overflow-y-auto"> <aside
id="kb-sidebar"
class="hidden lg:block bg-white border-r border-gray-200 overflow-y-auto relative"
:style="'width: ' + sidebarWidth + 'px'"
>
@livewire('sidebar-tree')
<!-- Resize Handle -->
<div
@mousedown="startResize($event)"
class="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-indigo-500 transition-colors group"
title="ドラッグして幅を変更"
>
<div class="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 w-1.5 h-12 bg-gray-300 rounded-full group-hover:bg-indigo-500 transition-colors"></div>
</div>
</aside>
<!-- Sidebar - Mobile Overlay -->
<div
x-show="mobileMenuOpen"
@click="mobileMenuOpen = false"
x-transition:enter="transition-opacity ease-linear duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-linear duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-gray-600 bg-opacity-75 z-30 lg:hidden"
style="display: none;"
></div>
<aside
x-show="mobileMenuOpen"
@click.away="mobileMenuOpen = false"
x-transition:enter="transition ease-in-out duration-300 transform"
x-transition:enter-start="-translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transition ease-in-out duration-300 transform"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="-translate-x-full"
class="fixed inset-y-0 left-0 top-16 w-64 bg-white border-r border-gray-200 overflow-y-auto z-40 lg:hidden"
style="display: none;"
>
@livewire('sidebar-tree') @livewire('sidebar-tree')
</aside> </aside>
@@ -129,6 +259,91 @@ class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring
<!-- Global Keyboard Shortcuts --> <!-- Global Keyboard Shortcuts -->
<script> <script>
// Preserve sidebar scroll position during navigation
document.addEventListener('click', function(e) {
const sidebar = document.getElementById('kb-sidebar');
if (!sidebar) return;
const link = e.target.closest('a');
if (link && sidebar.contains(link)) {
const scrollPos = sidebar.scrollTop;
sessionStorage.setItem('kb_sidebar_scroll', scrollPos);
}
}, true);
// Restore scroll position after page load
function restoreSidebarScroll() {
const sidebar = document.getElementById('kb-sidebar');
if (!sidebar) return;
const savedPos = sessionStorage.getItem('kb_sidebar_scroll');
if (savedPos !== null && savedPos !== '0') {
sidebar.scrollTop = parseInt(savedPos, 10);
}
}
// Highlight current document in sidebar
function highlightCurrentDocument() {
const sidebar = document.getElementById('kb-sidebar');
if (!sidebar) {
console.log('Sidebar not found for highlighting');
return;
}
const currentPath = window.location.pathname;
const links = sidebar.querySelectorAll('a');
console.log('Current path:', currentPath);
console.log('Found links in sidebar:', links.length);
links.forEach(link => {
const href = link.getAttribute('href');
// Remove previous highlighting
link.classList.remove('bg-indigo-50', 'text-indigo-700', 'font-semibold');
link.classList.add('text-gray-700');
const icon = link.querySelector('svg');
if (icon) {
icon.classList.remove('text-indigo-600');
icon.classList.add('text-gray-400', 'group-hover:text-gray-600');
}
// Check if this is the current page
if (href === currentPath || href === window.location.href ||
(href && currentPath && href.endsWith(currentPath))) {
console.log('Matched link:', href, 'with current path:', currentPath);
link.classList.add('bg-indigo-50', 'text-indigo-700', 'font-semibold');
link.classList.remove('text-gray-700');
if (icon) {
icon.classList.remove('text-gray-400', 'group-hover:text-gray-600');
icon.classList.add('text-indigo-600');
}
}
});
}
// Restore on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
restoreSidebarScroll();
highlightCurrentDocument();
});
} else {
restoreSidebarScroll();
highlightCurrentDocument();
}
// Also restore on window load (for safety)
window.addEventListener('load', () => {
restoreSidebarScroll();
highlightCurrentDocument();
});
// Update highlight after Alpine navigation
document.addEventListener('alpine:navigated', highlightCurrentDocument);
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault(); e.preventDefault();

View File

@@ -15,7 +15,7 @@
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('messages.nav.dashboard') }} {{ __('messages.nav.dashboard') }}
</x-nav-link> </x-nav-link>
<x-nav-link href="/" :active="false"> <x-nav-link :href="url('/')" :active="false">
<svg class="w-4 h-4 mr-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"></path>
</svg> </svg>
@@ -82,7 +82,7 @@
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('messages.nav.dashboard') }} {{ __('messages.nav.dashboard') }}
</x-responsive-nav-link> </x-responsive-nav-link>
<x-responsive-nav-link href="/" :active="false"> <x-responsive-nav-link :href="url('/')" :active="false">
{{ __('messages.nav.knowledge_base') }} {{ __('messages.nav.knowledge_base') }}
</x-responsive-nav-link> </x-responsive-nav-link>
</div> </div>

View File

@@ -1,4 +1,4 @@
<div class="max-w-5xl mx-auto p-8"> <div class="max-w-5xl mx-auto p-4 sm:p-6 lg:p-8">
<!-- Flash Messages --> <!-- Flash Messages -->
@if (session()->has('message')) @if (session()->has('message'))
<div class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded"> <div class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded">
@@ -13,16 +13,16 @@
@endif @endif
<!-- Header --> <!-- Header -->
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 class="text-3xl font-bold text-gray-900"> <h1 class="text-2xl sm:text-3xl font-bold text-gray-900">
{{ $isEditMode ? __('messages.documents.edit_document') : __('messages.documents.new_document') }} {{ $isEditMode ? __('messages.documents.edit_document') : __('messages.documents.new_document') }}
</h1> </h1>
<div class="flex space-x-3"> <div class="flex flex-wrap gap-2 sm:gap-3">
@if($isEditMode && $document) @if($isEditMode && $document)
<a <a
href="{{ route('documents.show', $document) }}" href="{{ route('documents.show', $document) }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 flex-1 sm:flex-none"
> >
{{ __('messages.common.cancel') }} {{ __('messages.common.cancel') }}
</a> </a>
@@ -30,14 +30,14 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text
<button <button
wire:click="delete" wire:click="delete"
wire:confirm="{{ __('messages.documents.delete_confirm') }}" wire:confirm="{{ __('messages.documents.delete_confirm') }}"
class="inline-flex items-center px-4 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-white hover:bg-red-50" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-white hover:bg-red-50 flex-1 sm:flex-none"
> >
{{ __('messages.documents.delete') }} {{ __('messages.documents.delete') }}
</button> </button>
@else @else
<a <a
href="{{ route('documents.show', 'home') }}" href="{{ route('documents.show', 'home') }}"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 flex-1 sm:flex-none"
> >
{{ __('messages.common.cancel') }} {{ __('messages.common.cancel') }}
</a> </a>
@@ -45,12 +45,13 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text
<button <button
wire:click="save" wire:click="save"
class="inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700 flex-1 sm:flex-none"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 sm:mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
</svg> </svg>
{{ __('messages.documents.save') }} <span class="hidden sm:inline">{{ __('messages.documents.save') }}</span>
<span class="sm:hidden">{{ __('messages.documents.save') }}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,15 +1,15 @@
<div class="max-w-4xl mx-auto p-8"> <div class="max-w-4xl mx-auto p-4 sm:p-6 lg:p-8">
<!-- Document Header --> <!-- Document Header -->
<div class="mb-8"> <div class="mb-6 sm:mb-8">
<div class="flex items-center justify-between mb-4"> <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-4">
<h1 class="text-4xl font-bold text-gray-900"> <h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 break-words">
{{ $document->title }} {{ $document->title }}
</h1> </h1>
@auth @auth
<a <a
href="{{ route('documents.edit', $document) }}" href="{{ route('documents.edit', $document) }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" class="inline-flex items-center justify-center px-3 sm:px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 whitespace-nowrap"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
@@ -19,9 +19,9 @@ class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-
@endauth @endauth
</div> </div>
<div class="flex items-center text-sm text-gray-500 space-x-4"> <div class="flex flex-col sm:flex-row sm:items-center text-xs sm:text-sm text-gray-500 gap-2 sm:gap-4">
<span> <span>
{{ __('messages.documents.updated') }} {{ $document->updated_at->diffForHumans() }} {{ __('messages.documents.updated') }} {{ $document->updated_at->diffForHumans() }}{{ config('app.timezone') }}
</span> </span>
@if($document->updated_by && $document->updater) @if($document->updated_by && $document->updater)
@@ -37,7 +37,7 @@ class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-
</div> </div>
<!-- Document Content --> <!-- Document Content -->
<div class="prose prose-lg max-w-none mb-12"> <div class="prose prose-sm sm:prose-base lg:prose-lg max-w-none mb-8 sm:mb-12">
{!! $renderedContent !!} {!! $renderedContent !!}
</div> </div>
@@ -53,7 +53,6 @@ class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-
<a <a
href="{{ route('documents.show', $backlink) }}" href="{{ route('documents.show', $backlink) }}"
class="block p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition" class="block p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition"
wire:navigate
> >
<div class="flex items-center"> <div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -70,15 +69,15 @@ class="block p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition"
@endif @endif
<!-- Document Metadata --> <!-- Document Metadata -->
<div class="mt-12 pt-8 border-t border-gray-200"> <div class="mt-8 sm:mt-12 pt-6 sm:pt-8 border-t border-gray-200">
<div class="grid grid-cols-2 gap-4 text-sm text-gray-500"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs sm:text-sm text-gray-500">
<div> <div class="break-all">
<span class="font-medium">{{ __('messages.documents.path') }}:</span> <span class="font-medium">{{ __('messages.documents.path') }}:</span>
<code class="ml-2 text-xs bg-gray-100 px-2 py-1 rounded">{{ $document->path }}</code> <code class="ml-2 text-xs bg-gray-100 px-2 py-1 rounded">{{ $document->path }}</code>
</div> </div>
<div> <div>
<span class="font-medium">{{ __('messages.documents.last_modified') }}:</span> <span class="font-medium">{{ __('messages.documents.last_modified') }}:</span>
<span class="ml-2">{{ $document->updated_at->format('Y-m-d H:i:s') }}</span> <span class="ml-2">{{ $document->updated_at->format('Y-m-d H:i:s') }}{{ config('app.timezone') }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@ class="fixed inset-0 z-50 overflow-y-auto"
style="display: none;" style="display: none;"
@click="open = false" @click="open = false"
> >
<div class="flex min-h-full items-start justify-center p-4 pt-[10vh]"> <div class="flex min-h-full items-start justify-center p-2 sm:p-4 pt-[5vh] sm:pt-[10vh]">
<div <div
class="w-full max-w-2xl bg-white rounded-lg shadow-2xl" class="w-full max-w-2xl bg-white rounded-lg shadow-2xl"
@click.stop @click.stop
@@ -41,16 +41,16 @@ class="w-full max-w-2xl bg-white rounded-lg shadow-2xl"
wire:keydown.enter.prevent="selectDocument" wire:keydown.enter.prevent="selectDocument"
> >
<!-- Search Input --> <!-- Search Input -->
<div class="p-4 border-b border-gray-200"> <div class="p-3 sm:p-4 border-b border-gray-200">
<div class="relative"> <div class="relative">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="absolute left-2 sm:left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 sm:h-5 sm:w-5 text-gray-400" 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> <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> </svg>
<input <input
x-ref="searchInput" x-ref="searchInput"
type="text" type="text"
wire:model.live="search" wire:model.live="search"
class="w-full pl-10 pr-4 py-3 border-0 focus:ring-0 text-lg" class="w-full pl-8 sm:pl-10 pr-4 py-2 sm:py-3 border-0 focus:ring-0 text-base sm:text-lg"
placeholder="{{ __('messages.quick_switcher.placeholder') }}" placeholder="{{ __('messages.quick_switcher.placeholder') }}"
autocomplete="off" autocomplete="off"
> >
@@ -58,7 +58,7 @@ class="w-full pl-10 pr-4 py-3 border-0 focus:ring-0 text-lg"
</div> </div>
<!-- Results --> <!-- Results -->
<div class="max-h-96 overflow-y-auto"> <div class="max-h-60 sm:max-h-96 overflow-y-auto">
@if(empty($this->results)) @if(empty($this->results))
<div class="p-8 text-center text-gray-500"> <div class="p-8 text-center text-gray-500">
{{ __('messages.quick_switcher.no_results') }} {{ __('messages.quick_switcher.no_results') }}
@@ -68,9 +68,8 @@ class="w-full pl-10 pr-4 py-3 border-0 focus:ring-0 text-lg"
@foreach($this->results as $index => $result) @foreach($this->results as $index => $result)
<li> <li>
<a <a
href="{{ route('documents.show', $result['id']) }}" href="{{ route('documents.show', $result['slug']) }}"
class="block px-4 py-3 hover:bg-gray-50 transition {{ $index === $selectedIndex ? 'bg-indigo-50' : '' }}" class="block px-4 py-3 hover:bg-gray-50 transition {{ $index === $selectedIndex ? 'bg-indigo-50' : '' }}"
wire:navigate
@click="open = false" @click="open = false"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -103,21 +102,21 @@ class="block px-4 py-3 hover:bg-gray-50 transition {{ $index === $selectedIndex
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="px-4 py-3 bg-gray-50 border-t border-gray-200 text-xs text-gray-500"> <div class="px-3 sm:px-4 py-2 sm:py-3 bg-gray-50 border-t border-gray-200 text-xs text-gray-500">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-2 sm:space-x-4 flex-wrap gap-y-1">
<span class="flex items-center"> <span class="flex items-center">
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1"></kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1"></kbd>
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-2"></kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1 sm:mr-2"></kbd>
{{ __('messages.quick_switcher.navigate') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.navigate') }}</span>
</span> </span>
<span class="flex items-center"> <span class="flex items-center">
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-2"></kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1 sm:mr-2"></kbd>
{{ __('messages.quick_switcher.select') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.select') }}</span>
</span> </span>
<span class="flex items-center"> <span class="flex items-center">
<kbd class="px-2 py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-2">esc</kbd> <kbd class="px-1.5 sm:px-2 py-0.5 sm:py-1 bg-white border border-gray-300 rounded text-xs font-semibold mr-1 sm:mr-2">esc</kbd>
{{ __('messages.quick_switcher.close') }} <span class="hidden sm:inline">{{ __('messages.quick_switcher.close') }}</span>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -10,7 +10,6 @@
<a <a
href="{{ route('documents.show', $file['document']) }}" href="{{ route('documents.show', $file['document']) }}"
class="flex items-center px-2 py-1.5 text-sm text-gray-700 rounded hover:bg-gray-100 group" class="flex items-center px-2 py-1.5 text-sm text-gray-700 rounded hover:bg-gray-100 group"
wire:navigate
> >
<svg class="w-4 h-4 mr-2 text-gray-400 group-hover:text-gray-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2 text-gray-400 group-hover:text-gray-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>

View File

@@ -13,12 +13,6 @@
</div> </div>
</div> </div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-locale-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl"> <div class="max-w-xl">
@include('profile.partials.update-password-form') @include('profile.partials.update-password-form')

View File

@@ -1,46 +0,0 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('messages.settings.language') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('messages.settings.select_language') }}
</p>
</header>
<form method="post" action="{{ route('locale.update') }}" class="mt-6 space-y-6">
@csrf
<div>
<x-input-label for="locale" :value="__('messages.settings.language')" />
<select
id="locale"
name="locale"
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
>
@foreach(\App\Http\Middleware\SetLocale::SUPPORTED_LOCALES as $code => $name)
<option value="{{ $code }}" {{ (auth()->user()->locale ?? 'en') === $code ? 'selected' : '' }}>
{{ $name }}
</option>
@endforeach
</select>
<x-input-error class="mt-2" :messages="$errors->get('locale')" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('messages.common.save') }}</x-primary-button>
@if (session('success'))
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ session('success') }}</p>
@endif
</div>
</form>
</section>

View File

@@ -22,11 +22,13 @@
return view('dashboard'); return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard'); })->middleware(['auth', 'verified'])->name('dashboard');
// Locale switcher - available for all users (both authenticated and guest)
Route::post('/locale', [LocaleController::class, 'update'])->name('locale.update');
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::post('/locale', [LocaleController::class, 'update'])->name('locale.update');
}); });
// Admin routes // Admin routes