Add placeholder picker component to template forms: Integrated a new placeholder picker component in Footer, Header, Menu, and Section template forms to enhance user experience by allowing easy insertion of placeholder variables. Updated the TemplateService to support custom blade components and added localization for the new placeholder picker functionality.
This commit is contained in:
@@ -29,6 +29,13 @@ class FooterTemplateForm
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Placeholder Picker Component
|
||||
View::make('components.placeholder-picker')
|
||||
->viewData([
|
||||
'fieldName' => 'html_content',
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
CodeEditor::make('html_content')
|
||||
->label(__('footer-templates.field_html_content'))
|
||||
->required()
|
||||
|
||||
@@ -28,6 +28,13 @@ class HeaderTemplateForm
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Placeholder Picker Component
|
||||
View::make('components.placeholder-picker')
|
||||
->viewData([
|
||||
'fieldName' => 'html_content',
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
CodeEditor::make('html_content')
|
||||
->label(__('header-templates.field_html_content'))
|
||||
->required()
|
||||
|
||||
@@ -25,6 +25,13 @@ class MenuTemplateForm
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Placeholder Picker Component
|
||||
View::make('components.placeholder-picker')
|
||||
->viewData([
|
||||
'fieldName' => 'html_content',
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
CodeEditor::make('html_content')
|
||||
->label(__('menu-templates.field_html_content'))
|
||||
->required()
|
||||
|
||||
@@ -28,6 +28,13 @@ class SectionTemplateForm
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Placeholder Picker Component
|
||||
View::make('components.placeholder-picker')
|
||||
->viewData([
|
||||
'fieldName' => 'html_content',
|
||||
])
|
||||
->columnSpanFull(),
|
||||
|
||||
CodeEditor::make('html_content')
|
||||
->label(__('section-templates.field_html_content'))
|
||||
->required()
|
||||
|
||||
@@ -39,6 +39,11 @@ class TemplateService
|
||||
|
||||
[$type, $name] = $parts;
|
||||
|
||||
// Skip custom.* placeholders - they are blade components, not form fields
|
||||
if ($type === 'custom') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = str($name)->title()->replace('_', ' ')->toString();
|
||||
|
||||
// Get existing value from existingData if provided
|
||||
@@ -268,6 +273,7 @@ class TemplateService
|
||||
* Replace placeholders in HTML with actual data
|
||||
* Supports both {text.title} and {{text.title}} formats
|
||||
* Also supports {menu} and {staticMenu} placeholders for menu rendering
|
||||
* Also supports {custom.component_name} for custom blade components
|
||||
*/
|
||||
public static function replacePlaceholders(string $html, array $data): string
|
||||
{
|
||||
@@ -283,10 +289,39 @@ class TemplateService
|
||||
$html = str_replace('{staticMenu}', $renderedStaticMenu, $html);
|
||||
}
|
||||
|
||||
// Handle custom blade components: {custom.component_name}
|
||||
preg_match_all('/\{custom\.([a-z_]+)\}/i', $html, $customMatches);
|
||||
if (!empty($customMatches[0])) {
|
||||
foreach ($customMatches[0] as $index => $fullMatch) {
|
||||
$componentName = $customMatches[1][$index] ?? null;
|
||||
if ($componentName) {
|
||||
$viewPath = "components.custom.{$componentName}";
|
||||
if (view()->exists($viewPath)) {
|
||||
try {
|
||||
$renderedComponent = view($viewPath)->render();
|
||||
$html = str_replace($fullMatch, $renderedComponent, $html);
|
||||
} catch (\Exception $e) {
|
||||
// If rendering fails, remove the placeholder or show error
|
||||
$html = str_replace($fullMatch, "<!-- Custom component '{$componentName}' could not be rendered: {$e->getMessage()} -->", $html);
|
||||
}
|
||||
} else {
|
||||
// Component doesn't exist, remove placeholder or show notice
|
||||
$html = str_replace($fullMatch, "<!-- Custom component '{$componentName}' not found -->", $html);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First, parse all placeholders in the format {type.field_name}
|
||||
// Exclude custom.* from this regex to avoid double processing
|
||||
preg_match_all('/\{([a-z]+\.[a-z_]+)\}/i', $html, $matches);
|
||||
$placeholders = array_unique($matches[1] ?? []);
|
||||
|
||||
// Filter out custom.* placeholders as they're already handled above
|
||||
$placeholders = array_filter($placeholders, function($placeholder) {
|
||||
return !str_starts_with($placeholder, 'custom.');
|
||||
});
|
||||
|
||||
// Replace each placeholder
|
||||
foreach ($placeholders as $placeholder) {
|
||||
// Try to find the value in data with various key formats
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Placeholder Variables',
|
||||
'click_to_insert' => 'Click to insert into editor',
|
||||
'special_placeholders' => 'Special Placeholders',
|
||||
|
||||
// Placeholder Types
|
||||
'type_text' => 'Text',
|
||||
'type_email' => 'Email',
|
||||
'type_url' => 'URL',
|
||||
'type_tel' => 'Telephone',
|
||||
'type_number' => 'Number',
|
||||
'type_textarea' => 'Textarea',
|
||||
'type_richtext' => 'Rich Text',
|
||||
'type_markdown' => 'Markdown',
|
||||
'type_code' => 'Code',
|
||||
'type_date' => 'Date',
|
||||
'type_datetime' => 'Date & Time',
|
||||
'type_time' => 'Time',
|
||||
'type_image' => 'Image',
|
||||
'type_images' => 'Images (Multiple)',
|
||||
'type_file' => 'File',
|
||||
'type_files' => 'Files (Multiple)',
|
||||
'type_select' => 'Select',
|
||||
'type_multiselect' => 'Multi Select',
|
||||
'type_checkbox' => 'Checkbox',
|
||||
'type_radio' => 'Radio',
|
||||
'type_toggle' => 'Toggle',
|
||||
'type_color' => 'Color',
|
||||
'type_tags' => 'Tags',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Placeholder Değişkenleri',
|
||||
'click_to_insert' => 'Tıklayarak editöre ekle',
|
||||
'special_placeholders' => 'Özel Placeholder\'lar',
|
||||
|
||||
// Placeholder Tipleri
|
||||
'type_text' => 'Metin',
|
||||
'type_email' => 'E-posta',
|
||||
'type_url' => 'URL',
|
||||
'type_tel' => 'Telefon',
|
||||
'type_number' => 'Sayı',
|
||||
'type_textarea' => 'Çok Satırlı Metin',
|
||||
'type_richtext' => 'Zengin Metin',
|
||||
'type_markdown' => 'Markdown',
|
||||
'type_code' => 'Kod',
|
||||
'type_date' => 'Tarih',
|
||||
'type_datetime' => 'Tarih ve Saat',
|
||||
'type_time' => 'Saat',
|
||||
'type_image' => 'Resim',
|
||||
'type_images' => 'Resimler (Çoklu)',
|
||||
'type_file' => 'Dosya',
|
||||
'type_files' => 'Dosyalar (Çoklu)',
|
||||
'type_select' => 'Seçim',
|
||||
'type_multiselect' => 'Çoklu Seçim',
|
||||
'type_checkbox' => 'Onay Kutusu',
|
||||
'type_radio' => 'Radyo Butonu',
|
||||
'type_toggle' => 'Aç/Kapa',
|
||||
'type_color' => 'Renk',
|
||||
'type_tags' => 'Etiketler',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="custom-example-component my-4 p-4 bg-gray-100 rounded-lg">
|
||||
<h4 class="text-lg font-bold mb-2">Örnek Custom Component</h4>
|
||||
<p class="text-gray-700">Bu component'i template içinde <code>{custom.example}</code> yazarak kullanabilirsiniz.</p>
|
||||
<p class="text-sm text-gray-500 mt-2">Bu dosyayı dilediğiniz gibi özelleştirebilir ve yeniden kullanabilirsiniz.</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="custom-test-component">
|
||||
<h3>Custom Test Component</h3>
|
||||
<p>Bu bir örnek custom component'tir. Bu dosyayı dilediğiniz gibi özelleştirebilirsiniz.</p>
|
||||
<p>Template içinde <code>{custom.test}</code> yazarak bu component'i çağırabilirsiniz.</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
@php
|
||||
// Desteklenen placeholder tipleri ve açıklamaları
|
||||
$placeholderTypes = [
|
||||
'text' => ['label' => __('placeholder-picker.type_text'), 'examples' => ['company_name', 'title', 'description', 'subtitle']],
|
||||
'email' => ['label' => __('placeholder-picker.type_email'), 'examples' => ['contact', 'support', 'info', 'sales']],
|
||||
'url' => ['label' => __('placeholder-picker.type_url'), 'examples' => ['website', 'facebook', 'twitter', 'linkedin']],
|
||||
'tel' => ['label' => __('placeholder-picker.type_tel'), 'examples' => ['phone', 'mobile', 'fax', 'hotline']],
|
||||
'number' => ['label' => __('placeholder-picker.type_number'), 'examples' => ['price', 'quantity', 'age', 'year']],
|
||||
'textarea' => ['label' => __('placeholder-picker.type_textarea'), 'examples' => ['content', 'about', 'description', 'notes']],
|
||||
'richtext' => ['label' => __('placeholder-picker.type_richtext'), 'examples' => ['content', 'article', 'post', 'body']],
|
||||
'markdown' => ['label' => __('placeholder-picker.type_markdown'), 'examples' => ['content', 'documentation', 'readme']],
|
||||
'code' => ['label' => __('placeholder-picker.type_code'), 'examples' => ['snippet', 'example', 'script']],
|
||||
'date' => ['label' => __('placeholder-picker.type_date'), 'examples' => ['publish_date', 'event_date', 'birthday']],
|
||||
'datetime' => ['label' => __('placeholder-picker.type_datetime'), 'examples' => ['created_at', 'updated_at', 'event_time']],
|
||||
'time' => ['label' => __('placeholder-picker.type_time'), 'examples' => ['start_time', 'end_time', 'opening_time']],
|
||||
'image' => ['label' => __('placeholder-picker.type_image'), 'examples' => ['logo', 'banner', 'avatar', 'thumbnail']],
|
||||
'images' => ['label' => __('placeholder-picker.type_images'), 'examples' => ['gallery', 'photos', 'slider_images']],
|
||||
'file' => ['label' => __('placeholder-picker.type_file'), 'examples' => ['document', 'pdf', 'download']],
|
||||
'files' => ['label' => __('placeholder-picker.type_files'), 'examples' => ['documents', 'attachments']],
|
||||
'select' => ['label' => __('placeholder-picker.type_select'), 'examples' => ['category', 'status', 'type']],
|
||||
'multiselect' => ['label' => __('placeholder-picker.type_multiselect'), 'examples' => ['tags', 'categories', 'features']],
|
||||
'checkbox' => ['label' => __('placeholder-picker.type_checkbox'), 'examples' => ['is_active', 'is_featured', 'is_published']],
|
||||
'radio' => ['label' => __('placeholder-picker.type_radio'), 'examples' => ['gender', 'type', 'status']],
|
||||
'toggle' => ['label' => __('placeholder-picker.type_toggle'), 'examples' => ['is_active', 'is_enabled', 'is_visible']],
|
||||
'color' => ['label' => __('placeholder-picker.type_color'), 'examples' => ['primary_color', 'background_color', 'text_color']],
|
||||
'tags' => ['label' => __('placeholder-picker.type_tags'), 'examples' => ['tags', 'keywords', 'labels']],
|
||||
];
|
||||
|
||||
// Field name'i component'ten al
|
||||
$fieldName = $fieldName ?? 'html_content';
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
showPicker: false,
|
||||
selectedType: null,
|
||||
insertPlaceholder(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
|
||||
// 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"]');
|
||||
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');
|
||||
const component = window.Livewire.find(wireId);
|
||||
|
||||
if (component) {
|
||||
// Livewire 3 API: component.set() kullan
|
||||
const fieldPath = 'data.' + '{{ $fieldName }}';
|
||||
try {
|
||||
// Önce mevcut değeri kontrol et
|
||||
if (component.get(fieldPath) !== undefined) {
|
||||
component.set(fieldPath, newValue);
|
||||
} else 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);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Livewire set failed:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
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');
|
||||
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) || '';
|
||||
}
|
||||
|
||||
// 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 }));
|
||||
}
|
||||
}
|
||||
}"
|
||||
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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user