Add User Guide functionality: Created UserGuide page and GuideService to manage and display user guides. Implemented localization for user guide labels and integrated a search feature for easy navigation. Added markdown support for guide content and established a structured layout for guide selection and display.

This commit is contained in:
Ümit Tunç
2025-11-03 17:08:15 -03:00
parent 6a5e26638c
commit f9730a64f7
9 changed files with 786 additions and 209 deletions
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace App\Filament\Admin\Pages;
use App\Services\GuideService;
use BackedEnum;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
class UserGuide extends Page
{
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedBookOpen;
protected string $view = 'filament.admin.pages.user-guide';
protected static ?int $navigationSort = 999;
public ?string $selectedGuide = null;
public function mount(): void
{
$guideService = app(GuideService::class);
$firstGuide = $guideService->getFirstGuide();
if ($firstGuide) {
$this->selectedGuide = $firstGuide['slug'];
}
}
public static function getNavigationLabel(): string
{
return __('user-guide.navigation_label');
}
public function getTitle(): string
{
return __('user-guide.title');
}
public function getGuides(): array
{
$guideService = app(GuideService::class);
return $guideService->getAllGuides();
}
public function getSelectedGuideContent(): ?array
{
if (!$this->selectedGuide) {
return null;
}
$guideService = app(GuideService::class);
return $guideService->getGuide($this->selectedGuide);
}
public function selectGuide(string $slug): void
{
$guideService = app(GuideService::class);
if ($guideService->guideExists($slug)) {
$this->selectedGuide = $slug;
}
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class GuideService
{
/**
* Guide dosyalarının bulunduğu dizin
*/
protected string $guidePath;
public function __construct()
{
$this->guidePath = resource_path('views/guide');
}
/**
* Tüm markdown dosyalarını son güncellenme tarihine göre sıralı olarak getir
*
* @return array
*/
public function getAllGuides(): array
{
if (!File::exists($this->guidePath)) {
return [];
}
$files = File::glob($this->guidePath . '/*.md');
$guides = [];
foreach ($files as $file) {
$filename = File::basename($file);
$name = Str::before($filename, '.md');
$modifiedTime = File::lastModified($file);
$guides[] = [
'filename' => $filename,
'name' => $name,
'slug' => Str::slug($name),
'path' => $file,
'modified_time' => $modifiedTime,
'modified_date' => date('Y-m-d H:i:s', $modifiedTime),
];
}
// Son güncellenenden ilk güncellenene göre sırala
usort($guides, function ($a, $b) {
return $b['modified_time'] <=> $a['modified_time'];
});
return $guides;
}
/**
* Belirli bir guide dosyasını getir
*
* @param string $slug
* @return array|null
*/
public function getGuide(string $slug): ?array
{
$guides = $this->getAllGuides();
foreach ($guides as $guide) {
if ($guide['slug'] === $slug) {
$guide['content'] = File::get($guide['path']);
return $guide;
}
}
return null;
}
/**
* İlk guide'ı getir (varsayılan olarak gösterilecek)
*
* @return array|null
*/
public function getFirstGuide(): ?array
{
$guides = $this->getAllGuides();
if (empty($guides)) {
return null;
}
$firstGuide = $guides[0];
$firstGuide['content'] = File::get($firstGuide['path']);
return $firstGuide;
}
/**
* Guide dosyasının var olup olmadığını kontrol et
*
* @param string $slug
* @return bool
*/
public function guideExists(string $slug): bool
{
return $this->getGuide($slug) !== null;
}
}
+2
View File
@@ -4,6 +4,8 @@ return [
'title' => 'Placeholder Variables',
'click_to_insert' => 'Click to insert into editor',
'special_placeholders' => 'Special Placeholders',
'search_placeholder' => 'Search placeholders...',
'no_results' => 'No results found',
// Placeholder Types
'type_text' => 'Text',
+15
View File
@@ -0,0 +1,15 @@
<?php
return [
'title' => 'User Guide',
'navigation_label' => 'User Guide',
'menu_title' => 'Guides',
'search_placeholder' => 'Search guides...',
'no_guides' => 'No guide files found yet.',
'no_guide_selected' => 'No Guide Selected',
'select_guide_from_menu' => 'Please select a guide from the left menu.',
'last_updated' => 'Last updated',
'no_results' => 'No results found.',
];
+2
View File
@@ -4,6 +4,8 @@ return [
'title' => 'Placeholder Değişkenleri',
'click_to_insert' => 'Tıklayarak editöre ekle',
'special_placeholders' => 'Özel Placeholder\'lar',
'search_placeholder' => 'Placeholder ara...',
'no_results' => 'Arama sonucu bulunamadı',
// Placeholder Tipleri
'type_text' => 'Metin',
+15
View File
@@ -0,0 +1,15 @@
<?php
return [
'title' => 'Kullanıcı Kılavuzu',
'navigation_label' => 'Kullanıcı Kılavuzu',
'menu_title' => 'Kılavuzlar',
'search_placeholder' => 'Kılavuz ara...',
'no_guides' => 'Henüz kılavuz dosyası bulunmamaktadır.',
'no_guide_selected' => 'Kılavuz Seçilmedi',
'select_guide_from_menu' => 'Lütfen sol menüden bir kılavuz seçin.',
'last_updated' => 'Son güncelleme',
'no_results' => 'Arama sonucu bulunamadı.',
];
@@ -31,14 +31,153 @@
@endphp
<div
x-data="{
showPicker: false,
selectedType: null,
insertPlaceholder(placeholder) {
x-data="placeholderPickerData(@js($placeholderTypes))"
>
<x-filament::section
collapsible
:collapsed="true"
:description="__('placeholder-picker.click_to_insert')"
icon="heroicon-o-tag"
>
<x-slot name="heading">
{{ __('placeholder-picker.title') }}
</x-slot>
<div class="space-y-4">
<!-- Search Bar -->
<x-filament::input.wrapper>
<x-slot name="prefix">
<x-filament::icon
icon="heroicon-o-magnifying-glass"
class="h-5 w-5"
/>
</x-slot>
<x-filament::input
type="text"
x-model="searchQuery"
:placeholder="__('placeholder-picker.search_placeholder')"
/>
<x-slot name="suffix">
<button
x-show="searchQuery"
@click="searchQuery = ''"
type="button"
class="fi-icon-btn fi-icon-btn-color-gray fi-icon-btn-size-md"
>
<x-filament::icon
icon="heroicon-o-x-mark"
class="h-4 w-4"
/>
</button>
</x-slot>
</x-filament::input.wrapper>
<!-- Placeholder List -->
<div class="max-h-96 overflow-y-auto space-y-4">
<template x-for="(info, type) in filteredCategories" x-bind:key="type">
<div class="space-y-2">
<h4 class="fi-section-header-heading text-xs font-semibold uppercase tracking-wide">
<span x-text="info.label"></span>
</h4>
<div class="flex flex-wrap gap-2">
<template x-for="example in info.examples" x-bind:key="example">
<x-filament::button
size="sm"
color="gray"
outlined
@click="insertPlaceholder('{{ $fieldName }}', type + '.' + example)"
x-bind:tooltip="'{{ __('placeholder-picker.click_to_insert') }}'"
>
<code class="text-xs font-mono" x-text="'{' + type + '.' + example + '}'"></code>
</x-filament::button>
</template>
</div>
</div>
</template>
<!-- Özel placeholder'lar -->
<div x-show="hasSpecialPlaceholders" class="pt-3 border-t fi-border-color">
<h4 class="fi-section-header-heading text-xs font-semibold uppercase tracking-wide mb-2">
{{ __('placeholder-picker.special_placeholders') }}
</h4>
<div class="flex flex-wrap gap-2">
<x-filament::button
size="sm"
color="primary"
outlined
@click="insertPlaceholder('{{ $fieldName }}', 'menu')"
>
<code class="text-xs font-mono">{menu}</code>
</x-filament::button>
<x-filament::button
size="sm"
color="primary"
outlined
@click="insertPlaceholder('{{ $fieldName }}', 'staticMenu')"
>
<code class="text-xs font-mono">{staticMenu}</code>
</x-filament::button>
</div>
</div>
<!-- No Results -->
<div
x-show="Object.keys(filteredCategories).length === 0 && !hasSpecialPlaceholders"
class="flex flex-col items-center justify-center gap-4 py-8"
>
<x-filament::icon
icon="heroicon-o-magnifying-glass"
class="h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('placeholder-picker.no_results') }}</p>
</div>
</div>
</div>
</x-filament::section>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('placeholderPickerData', (placeholderTypes) => ({
searchQuery: '',
placeholderTypes: placeholderTypes,
get filteredCategories() {
if (!this.searchQuery.trim()) {
return this.placeholderTypes;
}
const query = this.searchQuery.toLowerCase();
const filtered = {};
for (const [type, info] of Object.entries(this.placeholderTypes)) {
const filteredExamples = info.examples.filter(example => {
const placeholder = type + '.' + example;
return placeholder.toLowerCase().includes(query) ||
example.toLowerCase().includes(query) ||
info.label.toLowerCase().includes(query);
});
if (filteredExamples.length > 0) {
filtered[type] = {
label: info.label,
examples: filteredExamples
};
}
}
return filtered;
},
get hasSpecialPlaceholders() {
if (!this.searchQuery.trim()) return true;
const query = this.searchQuery.toLowerCase();
return 'menu'.includes(query) || 'staticmenu'.includes(query) || 'özel'.includes(query) || 'special'.includes(query);
}
}));
});
function insertPlaceholder(fieldName, placeholder) {
const placeholderText = '{' + placeholder + '}';
// Monaco Editor instance'ını bul
const editorContainer = document.querySelector('[data-field-name=\'{{ $fieldName }}\']');
const editorContainer = document.querySelector('[data-field-name=\'' + fieldName + '\']');
let monacoEditorInstance = null;
let currentValue = '';
let cursorPosition = null;
@@ -48,7 +187,7 @@
const editorInstances = window.monaco.editor.getEditors();
for (let editor of editorInstances) {
const container = editor.getContainerDomNode();
if (container && container.closest && container.closest('[data-field-name=\'{{ $fieldName }}\']')) {
if (container && container.closest && container.closest('[data-field-name=\'' + fieldName + '\']')) {
monacoEditorInstance = editor;
const model = editor.getModel();
currentValue = model.getValue();
@@ -69,7 +208,6 @@
const newValue = currentValue.slice(0, cursorPosition) + placeholderText + currentValue.slice(cursorPosition);
// Monaco Editor'da değeri güncelle
// pushEditOperations kullanarak undo/redo desteği ile güncelle
monacoEditorInstance.executeEdits('placeholder-insert', [{
range: new window.monaco.Range(
model.getPositionAt(cursorPosition).lineNumber,
@@ -85,62 +223,121 @@
monacoEditorInstance.setPosition(newPosition);
monacoEditorInstance.focus();
// Filament form state'ini güncellemek için Livewire component'ini bul
// Monaco Editor değişikliği otomatik algılar ama form state'ini manuel güncelle
// Filament form state'ini güncellemek için Monaco Editor'ın change event'ini tetikle
// Filament CodeEditor otomatik olarak değişiklikleri algılar
setTimeout(() => {
// Filament CodeEditor'ın hidden input'unu bul ve güncelle
const hiddenInput = editorContainer.querySelector('input[type="hidden"]');
// Monaco Editor model değişikliğini tetikle
// Bu, Filament'in CodeEditor component'inin otomatik olarak algılamasını sağlar
if (monacoEditorInstance) {
// Model'in değerini tekrar set et (zaten set ettik ama event'i tetiklemek için)
const model = monacoEditorInstance.getModel();
// Monaco Editor'ın onDidChangeContent event'ini manuel tetikle
// Filament CodeEditor bu event'i dinliyor ve form state'ini güncelliyor
model.onDidChangeContent(() => {
// Bu event zaten tetiklenmiş olacak, sadece emin olmak için
});
// Hidden input'u bul ve güncelle (eğer varsa)
const hiddenInput = editorContainer.querySelector('input[type=\'hidden\']');
if (hiddenInput) {
hiddenInput.value = newValue;
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Livewire component'ini bul ve güncelle
const wireElement = editorContainer.closest('[wire\\:id]');
if (wireElement && window.Livewire) {
const wireId = wireElement.getAttribute('wire:id');
// Filament form'unu bul ve güncelle
// Form component'ini bulmak için editor container'ın parent'larını kontrol et
let formComponent = null;
const formWireElement = editorContainer.closest('form [wire\\:id], [wire\\:id][wire\\:key*="form"]');
if (formWireElement && window.Livewire) {
const wireId = formWireElement.getAttribute('wire:id');
const component = window.Livewire.find(wireId);
if (component) {
// Livewire 3 API: component.set() kullan
const fieldPath = 'data.' + '{{ $fieldName }}';
// Component'in form component'i olup olmadığını kontrol et
// Sadece field path'lerini kontrol et, 'data' property'sine direkt erişme
if (component && component.get) {
const fieldPath = 'data.' + fieldName;
try {
// Önce mevcut değeri kontrol et
// Field path'i kontrol et
if (component.get(fieldPath) !== undefined) {
component.set(fieldPath, newValue);
formComponent = component;
} else if (component.get('data.html_content') !== undefined) {
component.set('data.html_content', newValue);
formComponent = component;
} else if (component.get('html_content') !== undefined) {
component.set('html_content', newValue);
formComponent = component;
}
} catch (e) {
console.warn('Livewire set failed:', e);
// Property kontrolü sırasında hata alırsak, component'i kullanma
}
}
}
}, 100);
// Sadece form component'i bulunduysa güncelle
if (formComponent) {
try {
const fieldPath = 'data.' + fieldName;
if (formComponent.get(fieldPath) !== undefined) {
formComponent.set(fieldPath, newValue);
} else if (formComponent.get('data.html_content') !== undefined) {
formComponent.set('data.html_content', newValue);
} else if (formComponent.get('html_content') !== undefined) {
formComponent.set('html_content', newValue);
}
} catch (e) {
// Hata durumunda sadece console'a yaz, Monaco Editor zaten güncellenmiş
console.debug('Livewire form state update skipped:', e.message);
}
}
}
}, 150);
return;
}
// Livewire component'ini bul ve güncelle (Monaco Editor yoksa)
if (window.Livewire) {
const wireElement = document.querySelector('[wire\\:id]');
if (wireElement) {
const wireId = wireElement.getAttribute('wire:id');
// Form component'ini bulmak için field name ile input elementini bul
const formField = document.querySelector('input[name*=\'' + fieldName + '\'], textarea[name*=\'' + fieldName + '\']');
let formComponent = null;
if (formField) {
// Input field'ın parent'ındaki wire element'i bul
const formWireElement = formField.closest('[wire\\:id]');
if (formWireElement) {
const wireId = formWireElement.getAttribute('wire:id');
const component = window.Livewire.find(wireId);
if (component) {
// Component'in form component'i olup olmadığını kontrol et
if (component && component.get) {
const fieldPath = 'data.' + fieldName;
try {
// Field path'lerini kontrol et
if (component.get(fieldPath) !== undefined ||
component.get('data.html_content') !== undefined ||
component.get('html_content') !== undefined) {
formComponent = component;
}
} catch (e) {
// Property kontrolü sırasında hata alırsak, component'i kullanma
}
}
}
}
if (formComponent) {
try {
// Mevcut değeri al
let fieldValue = '';
if (component.get('data.html_content') !== undefined) {
fieldValue = component.get('data.html_content') || '';
} else if (component.get('html_content') !== undefined) {
fieldValue = component.get('html_content') || '';
if (formComponent.get('data.html_content') !== undefined) {
fieldValue = formComponent.get('data.html_content') || '';
} else if (formComponent.get('html_content') !== undefined) {
fieldValue = formComponent.get('html_content') || '';
} else {
const fieldPath = 'data.' + '{{ $fieldName }}';
fieldValue = component.get(fieldPath) || '';
const fieldPath = 'data.' + fieldName;
fieldValue = formComponent.get(fieldPath) || '';
}
// Cursor pozisyonu yoksa sona ekle
@@ -152,21 +349,24 @@
const newValue = fieldValue.slice(0, cursorPosition) + placeholderText + fieldValue.slice(cursorPosition);
// Livewire state'ini güncelle (Livewire 3 API)
if (component.get('data.html_content') !== undefined) {
component.set('data.html_content', newValue);
} else if (component.get('html_content') !== undefined) {
component.set('html_content', newValue);
if (formComponent.get('data.html_content') !== undefined) {
formComponent.set('data.html_content', newValue);
} else if (formComponent.get('html_content') !== undefined) {
formComponent.set('html_content', newValue);
} else {
component.set('data.' + '{{ $fieldName }}', newValue);
const fieldPath = 'data.' + fieldName;
formComponent.set(fieldPath, newValue);
}
return;
} catch (e) {
console.warn('Livewire form update failed:', e);
}
}
}
// Fallback: Textarea veya input elementini bul
const formField = document.querySelector(`input[name*='{{ $fieldName }}'], textarea[name*='{{ $fieldName }}']`);
const formField = document.querySelector('input[name*=\'' + fieldName + '\'], textarea[name*=\'' + fieldName + '\']');
if (formField) {
const fieldValue = formField.value || '';
const fieldCursorPosition = formField.selectionStart || fieldValue.length;
@@ -180,96 +380,8 @@
formField.dispatchEvent(new Event('change', { bubbles: true }));
}
}
}"
class="fi-input-wrp"
>
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
<div
@click="showPicker = !showPicker"
class="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
</svg>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('placeholder-picker.title') }}
</span>
</div>
<svg
x-show="!showPicker"
class="w-4 h-4 text-gray-400 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
<svg
x-show="showPicker"
class="w-4 h-4 text-gray-400 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
</svg>
</div>
<div
x-show="showPicker"
x-collapse
class="border-t border-gray-200 dark:border-gray-700 max-h-96 overflow-y-auto"
>
<div class="p-4 space-y-4">
@foreach($placeholderTypes as $type => $info)
<div class="space-y-2">
<h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
{{ $info['label'] }}
</h4>
<div class="flex flex-wrap gap-2">
@foreach($info['examples'] as $example)
@php
$placeholder = $type . '.' . $example;
@endphp
<button
type="button"
@click="insertPlaceholder('{{ $placeholder }}'); showPicker = false;"
class="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 transition-colors border border-gray-300 dark:border-gray-600"
title="{{ __('placeholder-picker.click_to_insert') }}"
>
<code class="text-xs">{{ '{' . $placeholder . '}' }}</code>
</button>
@endforeach
</div>
</div>
@endforeach
<!-- Özel placeholder'lar -->
<div class="pt-2 border-t border-gray-200 dark:border-gray-700">
<h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-2">
{{ __('placeholder-picker.special_placeholders') }}
</h4>
<div class="flex flex-wrap gap-2">
<button
type="button"
@click="insertPlaceholder('menu'); showPicker = false;"
class="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 transition-colors border border-blue-300 dark:border-blue-700"
>
<code class="text-xs">{menu}</code>
</button>
<button
type="button"
@click="insertPlaceholder('staticMenu'); showPicker = false;"
class="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 transition-colors border border-blue-300 dark:border-blue-700"
>
<code class="text-xs">{staticMenu}</code>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
// Global scope'a ekle
window.insertPlaceholder = insertPlaceholder;
</script>
@@ -0,0 +1,221 @@
<x-filament-panels::page>
<div class="flex gap-6" x-data="{ search: '' }">
{{-- Sol Navigation Bar --}}
<aside class="w-80 flex-shrink-0">
<x-filament::section>
<x-slot name="heading">
<div class="flex items-center gap-2">
<x-filament::icon
icon="heroicon-o-book-open"
class="w-5 h-5"
/>
{{ __('user-guide.menu_title') }}
</x-slot>
{{-- Search Input --}}
<div class="mb-4">
<x-filament::input.wrapper>
<x-slot name="prefix">
<x-filament::icon
icon="heroicon-o-magnifying-glass"
class="h-5 w-5 text-gray-400"
/>
</x-slot>
<x-filament::input
type="text"
x-model="search"
:placeholder="__('user-guide.search_placeholder')"
/>
</x-filament::input.wrapper>
</div>
{{-- Guide List --}}
<nav class="space-y-2">
@foreach($this->getGuides() as $guide)
<div
data-guide-item
data-guide-name="{{ strtolower(\Illuminate\Support\Str::title(str_replace(['-', '_'], ' ', $guide['name']))) }}"
x-show="!search || '{{ strtolower(\Illuminate\Support\Str::title(str_replace(['-', '_'], ' ', $guide['name']))) }}'.includes(search.toLowerCase())"
x-transition
>
<x-filament::button
wire:click="selectGuide('{{ $guide['slug'] }}')"
class="w-full justify-start group"
:color="$selectedGuide === $guide['slug'] ? 'primary' : 'gray'"
:variant="$selectedGuide === $guide['slug'] ? 'filled' : 'outline'"
>
<div class="flex items-center justify-between w-full min-w-0">
<div class="flex items-center gap-2 flex-1 min-w-0">
<x-filament::icon
icon="heroicon-o-document-text"
class="w-5 h-5 flex-shrink-0"
/>
<span class="font-medium text-sm truncate">
{{ \Illuminate\Support\Str::title(str_replace(['-', '_'], ' ', $guide['name'])) }}
</span>
</div>
@if($selectedGuide === $guide['slug'])
<x-filament::icon
icon="heroicon-o-check-circle"
class="w-5 h-5 ml-2 flex-shrink-0"
/>
@endif
</div>
@if(isset($guide['modified_date']))
<div class="w-full mt-1.5 text-xs opacity-75">
<div class="flex items-center gap-1">
<x-filament::icon
icon="heroicon-o-clock"
class="w-3 h-3"
/>
<span class="truncate">
{{ \Carbon\Carbon::parse($guide['modified_date'])->locale(app()->getLocale())->diffForHumans() }}
</span>
</div>
</div>
@endif
</x-filament::button>
</div>
@endforeach
@if(empty($this->getGuides()))
<div class="text-center py-8">
<x-filament::icon
icon="heroicon-o-book-open"
class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600 mb-3"
/>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ __('user-guide.no_guides') }}
</p>
</div>
@endif
</nav>
</x-filament::section>
</aside>
{{-- Sağ İçerik Alanı --}}
<main class="flex-1 min-w-0">
@if($selectedGuide && $this->getSelectedGuideContent())
@php
$guide = $this->getSelectedGuideContent();
@endphp
<x-filament::section>
<x-slot name="heading">
<div class="space-y-1">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{{ \Illuminate\Support\Str::title(str_replace(['-', '_'], ' ', $guide['name'])) }}
</h2>
@if(isset($guide['modified_date']))
<p class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1">
<x-filament::icon
icon="heroicon-o-clock"
class="w-4 h-4"
/>
{{ __('user-guide.last_updated') }}:
{{ \Carbon\Carbon::parse($guide['modified_date'])->locale(app()->getLocale())->format('d F Y, H:i') }}
</p>
@endif
</div>
</x-slot>
<x-filament::card>
<div class="guide-content prose prose-lg dark:prose-invert max-w-none">
{!! \Illuminate\Support\Str::markdown($guide['content'], [
'html_input' => 'allow',
'allow_unsafe_links' => false,
]) !!}
</div>
</x-filament::card>
</x-filament::section>
@else
<x-filament::section>
<x-slot name="heading">
{{ __('user-guide.title') }}
</x-slot>
<x-filament::card>
<div class="flex flex-col items-center justify-center py-12 text-center">
<x-filament::icon
icon="heroicon-o-book-open"
class="w-16 h-16 mx-auto text-gray-400 dark:text-gray-500 mb-4"
/>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
{{ __('user-guide.no_guide_selected') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ __('user-guide.select_guide_from_menu') }}
</p>
</div>
</x-filament::card>
</x-filament::section>
@endif
</main>
</div>
@push('styles')
<style>
.guide-content {
@apply text-gray-900 dark:text-gray-100;
}
.guide-content h1 {
@apply text-3xl font-bold mb-4 mt-8 text-gray-900 dark:text-gray-100;
}
.guide-content h2 {
@apply text-2xl font-semibold mb-3 mt-6 text-gray-900 dark:text-gray-100;
}
.guide-content h3 {
@apply text-xl font-semibold mb-2 mt-4 text-gray-900 dark:text-gray-100;
}
.guide-content p {
@apply mb-4 text-gray-700 dark:text-gray-300 leading-relaxed;
}
.guide-content ul, .guide-content ol {
@apply mb-4 ml-6 text-gray-700 dark:text-gray-300;
}
.guide-content li {
@apply mb-2;
}
.guide-content code {
@apply bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono text-gray-900 dark:text-gray-100;
}
.guide-content pre {
@apply bg-gray-100 dark:bg-gray-800 p-4 rounded-lg overflow-x-auto mb-4;
}
.guide-content pre code {
@apply bg-transparent p-0;
}
.guide-content blockquote {
@apply border-l-4 border-primary-500 pl-4 italic my-4 text-gray-600 dark:text-gray-400;
}
.guide-content a {
@apply text-primary-600 dark:text-primary-400 hover:underline;
}
.guide-content table {
@apply w-full border-collapse mb-4;
}
.guide-content th,
.guide-content td {
@apply border border-gray-300 dark:border-gray-600 px-4 py-2 text-left;
}
.guide-content th {
@apply bg-gray-100 dark:bg-gray-800 font-semibold;
}
</style>
@endpush
</x-filament-panels::page>
+37
View File
@@ -0,0 +1,37 @@
# Başlangıç Kılavuzu
Bu kılavuz, Citrus Platform'a başlangıç yapmak için gerekli adımları içerir.
## Giriş
Citrus Platform, modüler yapısı ve güçlü özellikleri ile modern web uygulamaları geliştirmenizi sağlar.
## Kurulum
### Gereksinimler
- PHP 8.1 veya üzeri
- Composer
- MySQL/MariaDB
- Node.js ve NPM
### Adımlar
1. Projeyi klonlayın
2. Bağımlılıkları yükleyin: `composer install`
3. Ortam değişkenlerini ayarlayın: `.env` dosyasını düzenleyin
4. Veritabanını oluşturun: `php artisan migrate`
5. Uygulamayı başlatın: `php artisan serve`
## İlk Adımlar
Sisteme giriş yaptıktan sonra:
- Dashboard'u keşfedin
- Kullanıcı yönetimini inceleyin
- Sayfa oluşturma özelliklerini test edin
## Yardım
Sorularınız için lütfen dokümantasyonu inceleyin veya destek ekibi ile iletişime geçin.