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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,8 @@ return [
|
|||||||
'title' => 'Placeholder Variables',
|
'title' => 'Placeholder Variables',
|
||||||
'click_to_insert' => 'Click to insert into editor',
|
'click_to_insert' => 'Click to insert into editor',
|
||||||
'special_placeholders' => 'Special Placeholders',
|
'special_placeholders' => 'Special Placeholders',
|
||||||
|
'search_placeholder' => 'Search placeholders...',
|
||||||
|
'no_results' => 'No results found',
|
||||||
|
|
||||||
// Placeholder Types
|
// Placeholder Types
|
||||||
'type_text' => 'Text',
|
'type_text' => 'Text',
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
];
|
||||||
|
|
||||||
@@ -4,6 +4,8 @@ return [
|
|||||||
'title' => 'Placeholder Değişkenleri',
|
'title' => 'Placeholder Değişkenleri',
|
||||||
'click_to_insert' => 'Tıklayarak editöre ekle',
|
'click_to_insert' => 'Tıklayarak editöre ekle',
|
||||||
'special_placeholders' => 'Özel Placeholder\'lar',
|
'special_placeholders' => 'Özel Placeholder\'lar',
|
||||||
|
'search_placeholder' => 'Placeholder ara...',
|
||||||
|
'no_results' => 'Arama sonucu bulunamadı',
|
||||||
|
|
||||||
// Placeholder Tipleri
|
// Placeholder Tipleri
|
||||||
'type_text' => 'Metin',
|
'type_text' => 'Metin',
|
||||||
|
|||||||
@@ -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ı.',
|
||||||
|
];
|
||||||
|
|
||||||
@@ -30,246 +30,358 @@
|
|||||||
$fieldName = $fieldName ?? 'html_content';
|
$fieldName = $fieldName ?? 'html_content';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div
|
<div
|
||||||
x-data="{
|
x-data="placeholderPickerData(@js($placeholderTypes))"
|
||||||
showPicker: false,
|
>
|
||||||
selectedType: null,
|
<x-filament::section
|
||||||
insertPlaceholder(placeholder) {
|
collapsible
|
||||||
const placeholderText = '{' + placeholder + '}';
|
: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>
|
||||||
|
|
||||||
// Monaco Editor instance'ını bul
|
<!-- Özel placeholder'lar -->
|
||||||
const editorContainer = document.querySelector('[data-field-name=\'{{ $fieldName }}\']');
|
<div x-show="hasSpecialPlaceholders" class="pt-3 border-t fi-border-color">
|
||||||
let monacoEditorInstance = null;
|
<h4 class="fi-section-header-heading text-xs font-semibold uppercase tracking-wide mb-2">
|
||||||
let currentValue = '';
|
{{ __('placeholder-picker.special_placeholders') }}
|
||||||
let cursorPosition = null;
|
</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>
|
||||||
|
|
||||||
// Monaco Editor'dan cursor pozisyonunu al
|
<!-- No Results -->
|
||||||
if (editorContainer && window.monaco) {
|
<div
|
||||||
const editorInstances = window.monaco.editor.getEditors();
|
x-show="Object.keys(filteredCategories).length === 0 && !hasSpecialPlaceholders"
|
||||||
for (let editor of editorInstances) {
|
class="flex flex-col items-center justify-center gap-4 py-8"
|
||||||
const container = editor.getContainerDomNode();
|
>
|
||||||
if (container && container.closest && container.closest('[data-field-name=\'{{ $fieldName }}\']')) {
|
<x-filament::icon
|
||||||
monacoEditorInstance = editor;
|
icon="heroicon-o-magnifying-glass"
|
||||||
const model = editor.getModel();
|
class="h-12 w-12 text-gray-400 dark:text-gray-500"
|
||||||
currentValue = model.getValue();
|
/>
|
||||||
const selection = editor.getSelection();
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('placeholder-picker.no_results') }}</p>
|
||||||
if (selection) {
|
</div>
|
||||||
cursorPosition = model.getOffsetAt(selection.getStartPosition());
|
</div>
|
||||||
} else {
|
</div>
|
||||||
cursorPosition = currentValue.length;
|
</x-filament::section>
|
||||||
}
|
</div>
|
||||||
break;
|
|
||||||
|
<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;
|
||||||
|
},
|
||||||
|
|
||||||
// Monaco Editor varsa, doğrudan onu kullan
|
get hasSpecialPlaceholders() {
|
||||||
if (monacoEditorInstance) {
|
if (!this.searchQuery.trim()) return true;
|
||||||
const model = monacoEditorInstance.getModel();
|
const query = this.searchQuery.toLowerCase();
|
||||||
const newValue = currentValue.slice(0, cursorPosition) + placeholderText + currentValue.slice(cursorPosition);
|
return 'menu'.includes(query) || 'staticmenu'.includes(query) || 'özel'.includes(query) || 'special'.includes(query);
|
||||||
|
}
|
||||||
// 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(
|
function insertPlaceholder(fieldName, placeholder) {
|
||||||
model.getPositionAt(cursorPosition).lineNumber,
|
const placeholderText = '{' + placeholder + '}';
|
||||||
model.getPositionAt(cursorPosition).column,
|
|
||||||
model.getPositionAt(cursorPosition).lineNumber,
|
// Monaco Editor instance'ını bul
|
||||||
model.getPositionAt(cursorPosition).column
|
const editorContainer = document.querySelector('[data-field-name=\'' + fieldName + '\']');
|
||||||
),
|
let monacoEditorInstance = null;
|
||||||
text: placeholderText
|
let currentValue = '';
|
||||||
}]);
|
let cursorPosition = null;
|
||||||
|
|
||||||
// Cursor pozisyonunu güncelle
|
// Monaco Editor'dan cursor pozisyonunu al
|
||||||
const newPosition = model.getPositionAt(cursorPosition + placeholderText.length);
|
if (editorContainer && window.monaco) {
|
||||||
monacoEditorInstance.setPosition(newPosition);
|
const editorInstances = window.monaco.editor.getEditors();
|
||||||
monacoEditorInstance.focus();
|
for (let editor of editorInstances) {
|
||||||
|
const container = editor.getContainerDomNode();
|
||||||
// Filament form state'ini güncellemek için Livewire component'ini bul
|
if (container && container.closest && container.closest('[data-field-name=\'' + fieldName + '\']')) {
|
||||||
// Monaco Editor değişikliği otomatik algılar ama form state'ini manuel güncelle
|
monacoEditorInstance = editor;
|
||||||
setTimeout(() => {
|
const model = editor.getModel();
|
||||||
// Filament CodeEditor'ın hidden input'unu bul ve güncelle
|
currentValue = model.getValue();
|
||||||
const hiddenInput = editorContainer.querySelector('input[type="hidden"]');
|
const selection = editor.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
cursorPosition = model.getOffsetAt(selection.getStartPosition());
|
||||||
|
} else {
|
||||||
|
cursorPosition = currentValue.length;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monaco Editor varsa, doğrudan onu kullan
|
||||||
|
if (monacoEditorInstance) {
|
||||||
|
const model = monacoEditorInstance.getModel();
|
||||||
|
const newValue = currentValue.slice(0, cursorPosition) + placeholderText + currentValue.slice(cursorPosition);
|
||||||
|
|
||||||
|
// Monaco Editor'da değeri güncelle
|
||||||
|
monacoEditorInstance.executeEdits('placeholder-insert', [{
|
||||||
|
range: new window.monaco.Range(
|
||||||
|
model.getPositionAt(cursorPosition).lineNumber,
|
||||||
|
model.getPositionAt(cursorPosition).column,
|
||||||
|
model.getPositionAt(cursorPosition).lineNumber,
|
||||||
|
model.getPositionAt(cursorPosition).column
|
||||||
|
),
|
||||||
|
text: placeholderText
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Cursor pozisyonunu güncelle
|
||||||
|
const newPosition = model.getPositionAt(cursorPosition + placeholderText.length);
|
||||||
|
monacoEditorInstance.setPosition(newPosition);
|
||||||
|
monacoEditorInstance.focus();
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
// 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) {
|
if (hiddenInput) {
|
||||||
hiddenInput.value = newValue;
|
hiddenInput.value = newValue;
|
||||||
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Livewire component'ini bul ve güncelle
|
// Filament form'unu bul ve güncelle
|
||||||
const wireElement = editorContainer.closest('[wire\\:id]');
|
// Form component'ini bulmak için editor container'ın parent'larını kontrol et
|
||||||
if (wireElement && window.Livewire) {
|
let formComponent = null;
|
||||||
const wireId = wireElement.getAttribute('wire:id');
|
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);
|
const component = window.Livewire.find(wireId);
|
||||||
|
|
||||||
if (component) {
|
// Component'in form component'i olup olmadığını kontrol et
|
||||||
// Livewire 3 API: component.set() kullan
|
// Sadece field path'lerini kontrol et, 'data' property'sine direkt erişme
|
||||||
const fieldPath = 'data.' + '{{ $fieldName }}';
|
if (component && component.get) {
|
||||||
|
const fieldPath = 'data.' + fieldName;
|
||||||
try {
|
try {
|
||||||
// Önce mevcut değeri kontrol et
|
// Field path'i kontrol et
|
||||||
if (component.get(fieldPath) !== undefined) {
|
if (component.get(fieldPath) !== undefined) {
|
||||||
component.set(fieldPath, newValue);
|
formComponent = component;
|
||||||
} else if (component.get('data.html_content') !== undefined) {
|
} else if (component.get('data.html_content') !== undefined) {
|
||||||
component.set('data.html_content', newValue);
|
formComponent = component;
|
||||||
} else if (component.get('html_content') !== undefined) {
|
} else if (component.get('html_content') !== undefined) {
|
||||||
component.set('html_content', newValue);
|
formComponent = component;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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
|
||||||
return;
|
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);
|
||||||
|
|
||||||
// Livewire component'ini bul ve güncelle (Monaco Editor yoksa)
|
return;
|
||||||
if (window.Livewire) {
|
}
|
||||||
const wireElement = document.querySelector('[wire\\:id]');
|
|
||||||
if (wireElement) {
|
// Livewire component'ini bul ve güncelle (Monaco Editor yoksa)
|
||||||
const wireId = wireElement.getAttribute('wire:id');
|
if (window.Livewire) {
|
||||||
|
// 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);
|
const component = window.Livewire.find(wireId);
|
||||||
|
|
||||||
if (component) {
|
// Component'in form component'i olup olmadığını kontrol et
|
||||||
// Mevcut değeri al
|
if (component && component.get) {
|
||||||
let fieldValue = '';
|
const fieldPath = 'data.' + fieldName;
|
||||||
if (component.get('data.html_content') !== undefined) {
|
try {
|
||||||
fieldValue = component.get('data.html_content') || '';
|
// Field path'lerini kontrol et
|
||||||
} else if (component.get('html_content') !== undefined) {
|
if (component.get(fieldPath) !== undefined ||
|
||||||
fieldValue = component.get('html_content') || '';
|
component.get('data.html_content') !== undefined ||
|
||||||
} else {
|
component.get('html_content') !== undefined) {
|
||||||
const fieldPath = 'data.' + '{{ $fieldName }}';
|
formComponent = component;
|
||||||
fieldValue = component.get(fieldPath) || '';
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Property kontrolü sırasında hata alırsak, component'i kullanma
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor pozisyonu yoksa sona ekle
|
|
||||||
if (cursorPosition === null) {
|
|
||||||
cursorPosition = fieldValue.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder'ı cursor pozisyonuna ekle
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
component.set('data.' + '{{ $fieldName }}', newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Textarea veya input elementini bul
|
if (formComponent) {
|
||||||
const formField = document.querySelector(`input[name*='{{ $fieldName }}'], textarea[name*='{{ $fieldName }}']`);
|
try {
|
||||||
if (formField) {
|
// Mevcut değeri al
|
||||||
const fieldValue = formField.value || '';
|
let fieldValue = '';
|
||||||
const fieldCursorPosition = formField.selectionStart || fieldValue.length;
|
if (formComponent.get('data.html_content') !== undefined) {
|
||||||
const newValue = fieldValue.slice(0, fieldCursorPosition) + placeholderText + fieldValue.slice(fieldCursorPosition);
|
fieldValue = formComponent.get('data.html_content') || '';
|
||||||
|
} else if (formComponent.get('html_content') !== undefined) {
|
||||||
formField.value = newValue;
|
fieldValue = formComponent.get('html_content') || '';
|
||||||
formField.setSelectionRange(fieldCursorPosition + placeholderText.length, fieldCursorPosition + placeholderText.length);
|
} else {
|
||||||
|
const fieldPath = 'data.' + fieldName;
|
||||||
// Form event'lerini tetikle
|
fieldValue = formComponent.get(fieldPath) || '';
|
||||||
formField.dispatchEvent(new Event('input', { bubbles: true }));
|
}
|
||||||
formField.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
|
// Cursor pozisyonu yoksa sona ekle
|
||||||
|
if (cursorPosition === null) {
|
||||||
|
cursorPosition = fieldValue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder'ı cursor pozisyonuna ekle
|
||||||
|
const newValue = fieldValue.slice(0, cursorPosition) + placeholderText + fieldValue.slice(cursorPosition);
|
||||||
|
|
||||||
|
// Livewire state'ini güncelle (Livewire 3 API)
|
||||||
|
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 {
|
||||||
|
const fieldPath = 'data.' + fieldName;
|
||||||
|
formComponent.set(fieldPath, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Livewire form update failed:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"
|
|
||||||
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
|
// Fallback: Textarea veya input elementini bul
|
||||||
x-show="showPicker"
|
const formField = document.querySelector('input[name*=\'' + fieldName + '\'], textarea[name*=\'' + fieldName + '\']');
|
||||||
x-collapse
|
if (formField) {
|
||||||
class="border-t border-gray-200 dark:border-gray-700 max-h-96 overflow-y-auto"
|
const fieldValue = formField.value || '';
|
||||||
>
|
const fieldCursorPosition = formField.selectionStart || fieldValue.length;
|
||||||
<div class="p-4 space-y-4">
|
const newValue = fieldValue.slice(0, fieldCursorPosition) + placeholderText + fieldValue.slice(fieldCursorPosition);
|
||||||
@foreach($placeholderTypes as $type => $info)
|
|
||||||
<div class="space-y-2">
|
formField.value = newValue;
|
||||||
<h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
|
formField.setSelectionRange(fieldCursorPosition + placeholderText.length, fieldCursorPosition + placeholderText.length);
|
||||||
{{ $info['label'] }}
|
|
||||||
</h4>
|
// Form event'lerini tetikle
|
||||||
<div class="flex flex-wrap gap-2">
|
formField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
@foreach($info['examples'] as $example)
|
formField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
@php
|
}
|
||||||
$placeholder = $type . '.' . $example;
|
}
|
||||||
@endphp
|
|
||||||
<button
|
// Global scope'a ekle
|
||||||
type="button"
|
window.insertPlaceholder = insertPlaceholder;
|
||||||
@click="insertPlaceholder('{{ $placeholder }}'); showPicker = false;"
|
</script>
|
||||||
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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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.
|
||||||
|
|
||||||
Reference in New Issue
Block a user