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
@@ -30,246 +30,358 @@
$fieldName = $fieldName ?? 'html_content';
@endphp
<div
x-data="{
showPicker: false,
selectedType: null,
insertPlaceholder(placeholder) {
const placeholderText = '{' + placeholder + '}';
<div
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>
// Monaco Editor instance'ını bul
const editorContainer = document.querySelector('[data-field-name=\'{{ $fieldName }}\']');
let monacoEditorInstance = null;
let currentValue = '';
let cursorPosition = null;
<!-- Ö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>
// Monaco Editor'dan cursor pozisyonunu al
if (editorContainer && window.monaco) {
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 }}\']')) {
monacoEditorInstance = editor;
const model = editor.getModel();
currentValue = model.getValue();
const selection = editor.getSelection();
if (selection) {
cursorPosition = model.getOffsetAt(selection.getStartPosition());
} else {
cursorPosition = currentValue.length;
}
break;
<!-- 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;
},
// 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
// pushEditOperations kullanarak undo/redo desteği ile 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 Livewire component'ini bul
// Monaco Editor değişikliği otomatik algılar ama form state'ini manuel güncelle
setTimeout(() => {
// Filament CodeEditor'ın hidden input'unu bul ve güncelle
const hiddenInput = editorContainer.querySelector('input[type="hidden"]');
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 + '\']');
let monacoEditorInstance = null;
let currentValue = '';
let cursorPosition = null;
// Monaco Editor'dan cursor pozisyonunu al
if (editorContainer && window.monaco) {
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 + '\']')) {
monacoEditorInstance = editor;
const model = editor.getModel();
currentValue = model.getValue();
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) {
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);
return;
}
// 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);
// 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');
return;
}
// Livewire component'ini bul ve güncelle (Monaco Editor yoksa)
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);
if (component) {
// 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') || '';
} else {
const fieldPath = 'data.' + '{{ $fieldName }}';
fieldValue = component.get(fieldPath) || '';
// 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
}
// 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
const formField = document.querySelector(`input[name*='{{ $fieldName }}'], textarea[name*='{{ $fieldName }}']`);
if (formField) {
const fieldValue = formField.value || '';
const fieldCursorPosition = formField.selectionStart || fieldValue.length;
const newValue = fieldValue.slice(0, fieldCursorPosition) + placeholderText + fieldValue.slice(fieldCursorPosition);
formField.value = newValue;
formField.setSelectionRange(fieldCursorPosition + placeholderText.length, fieldCursorPosition + placeholderText.length);
// Form event'lerini tetikle
formField.dispatchEvent(new Event('input', { bubbles: true }));
formField.dispatchEvent(new Event('change', { bubbles: true }));
if (formComponent) {
try {
// Mevcut değeri al
let fieldValue = '';
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 = formComponent.get(fieldPath) || '';
}
// 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
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>
// Fallback: Textarea veya input elementini bul
const formField = document.querySelector('input[name*=\'' + fieldName + '\'], textarea[name*=\'' + fieldName + '\']');
if (formField) {
const fieldValue = formField.value || '';
const fieldCursorPosition = formField.selectionStart || fieldValue.length;
const newValue = fieldValue.slice(0, fieldCursorPosition) + placeholderText + fieldValue.slice(fieldCursorPosition);
formField.value = newValue;
formField.setSelectionRange(fieldCursorPosition + placeholderText.length, fieldCursorPosition + placeholderText.length);
// Form event'lerini tetikle
formField.dispatchEvent(new Event('input', { bubbles: true }));
formField.dispatchEvent(new Event('change', { bubbles: true }));
}
}
// Global scope'a ekle
window.insertPlaceholder = insertPlaceholder;
</script>