Compare commits

...

23 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
21 changed files with 1913 additions and 158 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

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

@@ -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

@@ -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

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

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.',
],
];

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

@@ -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="{{ url('/') }}" 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

@@ -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>
@@ -69,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') }}
@@ -102,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

@@ -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