Add template preview functionality: Introduced TemplatePreviewController and TemplatePreviewService for rendering dynamic previews of header, footer, and section templates. Updated form schemas to include preview components and enhanced real-time content updates. Added configuration for template assets and integrated preview functionality into the admin panel.
This commit is contained in:
@@ -6,6 +6,7 @@ use Filament\Forms\Components\CodeEditor;
|
|||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\View;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
class FooterTemplateForm
|
class FooterTemplateForm
|
||||||
@@ -22,19 +23,31 @@ class FooterTemplateForm
|
|||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
// Preview Component
|
||||||
|
View::make('components.template-preview')
|
||||||
|
->extraAttributes([
|
||||||
|
'data-type' => 'footer',
|
||||||
|
'data-field-name' => 'html_content',
|
||||||
|
])
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
CodeEditor::make('html_content')
|
CodeEditor::make('html_content')
|
||||||
->label(__('footer-templates.field_html_content'))
|
->label(__('footer-templates.field_html_content'))
|
||||||
->required()
|
->required()
|
||||||
//->lineNumbers()
|
->live(onBlur: false) // Real-time updates
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->helperText(__('footer-templates.field_html_content_help')),
|
->helperText(__('footer-templates.field_html_content_help'))
|
||||||
|
->extraAttributes([
|
||||||
|
'style' => 'min-height: 400px;',
|
||||||
|
'data-field-name' => 'html_content',
|
||||||
|
]),
|
||||||
|
|
||||||
Toggle::make('is_active')
|
Toggle::make('is_active')
|
||||||
->label(__('footer-templates.field_is_active'))
|
->label(__('footer-templates.field_is_active'))
|
||||||
->default(true)
|
->default(true)
|
||||||
->inline(false),
|
->inline(false),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columnSpanFull(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Filament\Forms\Components\CodeEditor;
|
|||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\View;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
class HeaderTemplateForm
|
class HeaderTemplateForm
|
||||||
@@ -22,19 +23,31 @@ class HeaderTemplateForm
|
|||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
// Preview Component
|
||||||
|
View::make('components.template-preview')
|
||||||
|
->extraAttributes([
|
||||||
|
'data-type' => 'header',
|
||||||
|
'data-field-name' => 'html_content',
|
||||||
|
])
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
CodeEditor::make('html_content')
|
CodeEditor::make('html_content')
|
||||||
->label(__('header-templates.field_html_content'))
|
->label(__('header-templates.field_html_content'))
|
||||||
->required()
|
->required()
|
||||||
//->lineNumbers()
|
->live(onBlur: false) // Real-time updates
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->helperText(__('header-templates.field_html_content_help')),
|
->helperText(__('header-templates.field_html_content_help'))
|
||||||
|
->extraAttributes([
|
||||||
|
'style' => 'min-height: 400px;',
|
||||||
|
'data-field-name' => 'html_content',
|
||||||
|
]),
|
||||||
|
|
||||||
Toggle::make('is_active')
|
Toggle::make('is_active')
|
||||||
->label(__('header-templates.field_is_active'))
|
->label(__('header-templates.field_is_active'))
|
||||||
->default(true)
|
->default(true)
|
||||||
->inline(false),
|
->inline(false),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columnSpanFull(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Filament\Forms\Components\CodeEditor;
|
|||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\View;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
class SectionTemplateForm
|
class SectionTemplateForm
|
||||||
@@ -22,19 +23,31 @@ class SectionTemplateForm
|
|||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
// Preview Component
|
||||||
|
View::make('components.template-preview')
|
||||||
|
->extraAttributes([
|
||||||
|
'data-type' => 'section',
|
||||||
|
'data-field-name' => 'html_content',
|
||||||
|
])
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
CodeEditor::make('html_content')
|
CodeEditor::make('html_content')
|
||||||
->label(__('section-templates.field_html_content'))
|
->label(__('section-templates.field_html_content'))
|
||||||
->required()
|
->required()
|
||||||
//->lineNumbers()
|
->live(onBlur: false) // Real-time updates
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->helperText(__('section-templates.field_html_content_help')),
|
->helperText(__('section-templates.field_html_content_help'))
|
||||||
|
->extraAttributes([
|
||||||
|
'style' => 'min-height: 400px;',
|
||||||
|
'data-field-name' => 'html_content',
|
||||||
|
]),
|
||||||
|
|
||||||
Toggle::make('is_active')
|
Toggle::make('is_active')
|
||||||
->label(__('section-templates.field_is_active'))
|
->label(__('section-templates.field_is_active'))
|
||||||
->default(true)
|
->default(true)
|
||||||
->inline(false),
|
->inline(false),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columnSpanFull(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\TemplatePreviewService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TemplatePreviewController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Render template preview
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function preview(Request $request)
|
||||||
|
{
|
||||||
|
$content = $request->input('content', '');
|
||||||
|
$type = $request->input('type', 'section'); // header, footer, section
|
||||||
|
|
||||||
|
// Validate type
|
||||||
|
if (!in_array($type, ['header', 'footer', 'section'])) {
|
||||||
|
$type = 'section';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = TemplatePreviewService::getPreviewHtml($content, $type);
|
||||||
|
|
||||||
|
return response($html)
|
||||||
|
->header('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
|
||||||
|
class TemplatePreviewService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all CSS assets for preview
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getCssAssets(): array
|
||||||
|
{
|
||||||
|
$config = config('template-preview.css', []);
|
||||||
|
|
||||||
|
if (config('template-preview.use_settings_override', false)) {
|
||||||
|
$settingsKeys = config('template-preview.settings_keys', []);
|
||||||
|
|
||||||
|
foreach ($settingsKeys as $assetKey => $settingKey) {
|
||||||
|
if (str_starts_with($assetKey, 'css_')) {
|
||||||
|
$setting = Setting::get($settingKey);
|
||||||
|
if ($setting) {
|
||||||
|
$configKey = str_replace('css_', '', $assetKey);
|
||||||
|
$config[$configKey] = $setting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all JavaScript assets for preview
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getJsAssets(): array
|
||||||
|
{
|
||||||
|
$config = config('template-preview.js', []);
|
||||||
|
|
||||||
|
if (config('template-preview.use_settings_override', false)) {
|
||||||
|
$settingsKeys = config('template-preview.settings_keys', []);
|
||||||
|
|
||||||
|
foreach ($settingsKeys as $assetKey => $settingKey) {
|
||||||
|
if (str_starts_with($assetKey, 'js_')) {
|
||||||
|
$setting = Setting::get($settingKey);
|
||||||
|
if ($setting) {
|
||||||
|
$configKey = str_replace('js_', '', $assetKey);
|
||||||
|
$config[$configKey] = $setting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted CSS link tags
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function getCssLinks(): string
|
||||||
|
{
|
||||||
|
$assets = self::getCssAssets();
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
foreach ($assets as $key => $path) {
|
||||||
|
if ($key === 'fonts') {
|
||||||
|
// Preload for fonts
|
||||||
|
$html .= sprintf(
|
||||||
|
'<link rel="preload" href="%s" as="style" onload="this.rel=\'stylesheet\'">' . "\n",
|
||||||
|
asset($path)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$html .= sprintf(
|
||||||
|
'<link rel="stylesheet" href="%s">' . "\n",
|
||||||
|
asset($path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted JavaScript script tags
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function getJsScripts(): string
|
||||||
|
{
|
||||||
|
$assets = self::getJsAssets();
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
foreach ($assets as $path) {
|
||||||
|
$html .= sprintf(
|
||||||
|
'<script src="%s"></script>' . "\n",
|
||||||
|
asset($path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full HTML structure for preview
|
||||||
|
*
|
||||||
|
* @param string $content The template HTML content
|
||||||
|
* @param string $type The template type: 'header', 'footer', or 'section'
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function getPreviewHtml(string $content, string $type = 'section'): string
|
||||||
|
{
|
||||||
|
$cssLinks = self::getCssLinks();
|
||||||
|
$jsScripts = self::getJsScripts();
|
||||||
|
$meta = config('template-preview.meta', []);
|
||||||
|
|
||||||
|
$viewport = $meta['viewport'] ?? 'width=device-width, initial-scale=1';
|
||||||
|
$charset = $meta['charset'] ?? 'utf-8';
|
||||||
|
|
||||||
|
// Wrap content based on type
|
||||||
|
$bodyContent = match($type) {
|
||||||
|
'header' => $content,
|
||||||
|
'footer' => $content,
|
||||||
|
'section' => '<div class="container py-8">' . $content . '</div>',
|
||||||
|
default => $content,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="tr">
|
||||||
|
<head>
|
||||||
|
<meta charset="{$charset}">
|
||||||
|
<meta name="viewport" content="{$viewport}">
|
||||||
|
<title>Template Preview</title>
|
||||||
|
{$cssLinks}
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0;">
|
||||||
|
{$bodyContent}
|
||||||
|
{$jsScripts}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Template Preview Asset Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration file contains all asset paths that should be loaded
|
||||||
|
| in the template preview iframe. These assets are used to ensure the
|
||||||
|
| preview displays correctly with the same styling and scripts as the
|
||||||
|
| actual site.
|
||||||
|
|
|
||||||
|
| You can override these values in Settings table if needed, or modify
|
||||||
|
| this config file for project-specific asset paths.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CSS Assets (Stylesheets)
|
||||||
|
'css' => [
|
||||||
|
'style' => 'html/style.css',
|
||||||
|
'unicons' => 'assets/fonts/unicons/unicons.css',
|
||||||
|
'plugins' => 'assets/css/plugins.css',
|
||||||
|
'colors' => 'assets/css/colors/grape.css',
|
||||||
|
'fonts' => 'assets/css/fonts/urbanist.css',
|
||||||
|
],
|
||||||
|
|
||||||
|
// JavaScript Assets
|
||||||
|
'js' => [
|
||||||
|
'plugins' => 'assets/js/plugins.js',
|
||||||
|
'theme' => 'assets/js/theme.js',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Additional meta tags and head content
|
||||||
|
'meta' => [
|
||||||
|
'viewport' => 'width=device-width, initial-scale=1',
|
||||||
|
'charset' => 'utf-8',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Whether to load assets from Settings table (overrides config)
|
||||||
|
'use_settings_override' => false,
|
||||||
|
|
||||||
|
// Settings keys for asset paths (if use_settings_override is true)
|
||||||
|
'settings_keys' => [
|
||||||
|
'css_style' => 'template_preview_css_style',
|
||||||
|
'css_unicons' => 'template_preview_css_unicons',
|
||||||
|
'css_plugins' => 'template_preview_css_plugins',
|
||||||
|
'css_colors' => 'template_preview_css_colors',
|
||||||
|
'css_fonts' => 'template_preview_css_fonts',
|
||||||
|
'js_plugins' => 'template_preview_js_plugins',
|
||||||
|
'js_theme' => 'template_preview_js_theme',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
@php
|
||||||
|
$previewUrl = route('template.preview');
|
||||||
|
// Get type and fieldName from component attributes or defaults
|
||||||
|
$type = $attributes->get('data-type') ?? 'section';
|
||||||
|
$fieldName = $attributes->get('data-field-name') ?? 'html_content';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-data="templatePreview(@js($previewUrl), @js($type), @js($fieldName))"
|
||||||
|
class="template-preview-wrapper w-full"
|
||||||
|
style="width: 100%;"
|
||||||
|
wire:ignore>
|
||||||
|
<div class="fi-fo-field-wrp-label">
|
||||||
|
<label class="fi-fo-field-wrp-label-label text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ __('Preview') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 border border-gray-300 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800"
|
||||||
|
style="min-height: 400px; position: relative; width: 100%;">
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div
|
||||||
|
x-show="isLoading || !iframeSrc"
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-gray-50 dark:bg-gray-900"
|
||||||
|
style="z-index: 10;"
|
||||||
|
x-transition>
|
||||||
|
<div class="text-center">
|
||||||
|
<svg class="animate-spin h-8 w-8 text-gray-400 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<span x-show="isLoading">Yükleniyor...</span>
|
||||||
|
<span x-show="!isLoading && !iframeSrc">Önizleme için içerik girin</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview iframe -->
|
||||||
|
<iframe
|
||||||
|
x-show="!isLoading && iframeSrc"
|
||||||
|
:src="iframeSrc"
|
||||||
|
class="w-full border-0"
|
||||||
|
style="min-height: 400px; width: 100%; display: block;"
|
||||||
|
frameborder="0"
|
||||||
|
loading="lazy"
|
||||||
|
x-transition>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ __('Önizleme gerçek zamanlı olarak güncellenir') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function templatePreview(previewUrl, type, fieldName) {
|
||||||
|
return {
|
||||||
|
previewUrl: previewUrl,
|
||||||
|
type: type,
|
||||||
|
fieldName: fieldName,
|
||||||
|
iframeSrc: '',
|
||||||
|
isLoading: false,
|
||||||
|
content: '',
|
||||||
|
debounceTimer: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Watch Livewire events for Filament form updates
|
||||||
|
if (window.Livewire) {
|
||||||
|
// Listen for form state updates
|
||||||
|
Livewire.on('filament-forms::update-state', ({ statePath, state }) => {
|
||||||
|
if (statePath === this.fieldName || statePath?.endsWith(this.fieldName)) {
|
||||||
|
this.content = state || '';
|
||||||
|
this.debouncedUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for any Livewire updates
|
||||||
|
document.addEventListener('livewire:update', () => {
|
||||||
|
this.debouncedUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch CodeMirror editor directly
|
||||||
|
this.watchCodeMirror();
|
||||||
|
|
||||||
|
// Watch for direct input changes (fallback for code editor)
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
this.debouncedUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe form changes
|
||||||
|
const form = document.querySelector('form[wire\\:submit]');
|
||||||
|
if (form) {
|
||||||
|
observer.observe(form, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial update after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updatePreview();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Periodic update fallback (every 3 seconds)
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
this.updatePreview();
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
watchCodeMirror() {
|
||||||
|
// Try to find CodeMirror editor instance
|
||||||
|
const findCodeMirror = () => {
|
||||||
|
// Look for CodeMirror editor instances
|
||||||
|
const editors = document.querySelectorAll('.cm-editor, [data-code-editor]');
|
||||||
|
editors.forEach(editor => {
|
||||||
|
// Check if this is our field
|
||||||
|
const fieldWrapper = editor.closest('[data-field-name], .fi-fo-field-wrp');
|
||||||
|
if (fieldWrapper) {
|
||||||
|
const fieldLabel = fieldWrapper.querySelector('label');
|
||||||
|
if (fieldLabel && fieldLabel.textContent.includes('HTML Content')) {
|
||||||
|
// Found our editor, try to get CodeMirror instance
|
||||||
|
const cmInstance = editor.__cm || editor.cm || editor;
|
||||||
|
if (cmInstance && cmInstance.getValue) {
|
||||||
|
// Watch for changes
|
||||||
|
if (cmInstance.on) {
|
||||||
|
cmInstance.on('change', () => {
|
||||||
|
this.content = cmInstance.getValue();
|
||||||
|
this.debouncedUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try immediately and after a delay
|
||||||
|
findCodeMirror();
|
||||||
|
setTimeout(findCodeMirror, 1000);
|
||||||
|
setTimeout(findCodeMirror, 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
debouncedUpdate() {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
this.updatePreview();
|
||||||
|
}, 500); // 500ms debounce
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePreview() {
|
||||||
|
// Get content from Livewire - try different methods
|
||||||
|
let content = this.content || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Method 1: Try CodeMirror editor directly (most reliable for CodeEditor)
|
||||||
|
// Find editor by data-field-name attribute
|
||||||
|
const fieldWrapper = document.querySelector(`[data-field-name="${this.fieldName}"]`);
|
||||||
|
if (fieldWrapper) {
|
||||||
|
const cmEditor = fieldWrapper.querySelector('.cm-editor');
|
||||||
|
if (cmEditor) {
|
||||||
|
const cmInstance = cmEditor.__cm || cmEditor.cm || window.CodeMirror?.fromTextArea?.(cmEditor.querySelector('textarea'));
|
||||||
|
if (cmInstance && typeof cmInstance.getValue === 'function') {
|
||||||
|
content = cmInstance.getValue() || '';
|
||||||
|
} else if (cmEditor.querySelector('.cm-content')) {
|
||||||
|
// Fallback: get text from content div
|
||||||
|
content = cmEditor.querySelector('.cm-content').textContent || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 1b: Try all CodeMirror editors if specific one not found
|
||||||
|
if (!content) {
|
||||||
|
const cmEditors = document.querySelectorAll('.cm-editor');
|
||||||
|
for (const cmEditor of cmEditors) {
|
||||||
|
const fieldWrapper = cmEditor.closest('[data-field-name]');
|
||||||
|
if (fieldWrapper && fieldWrapper.getAttribute('data-field-name') === this.fieldName) {
|
||||||
|
const cmInstance = cmEditor.__cm || cmEditor.cm;
|
||||||
|
if (cmInstance && typeof cmInstance.getValue === 'function') {
|
||||||
|
content = cmInstance.getValue() || '';
|
||||||
|
if (content) break;
|
||||||
|
} else if (cmEditor.querySelector('.cm-content')) {
|
||||||
|
content = cmEditor.querySelector('.cm-content').textContent || '';
|
||||||
|
if (content) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Try to find CodeEditor textarea/input directly
|
||||||
|
if (!content) {
|
||||||
|
const codeEditor = document.querySelector(`textarea[name="${this.fieldName}"],
|
||||||
|
textarea[wire\\:model*="${this.fieldName}"],
|
||||||
|
.fi-fo-field-wrp textarea,
|
||||||
|
.cm-content`);
|
||||||
|
|
||||||
|
if (codeEditor) {
|
||||||
|
content = codeEditor.value || codeEditor.textContent || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Try Livewire $wire
|
||||||
|
if (!content && window.Livewire) {
|
||||||
|
// Find the Livewire component
|
||||||
|
const livewireComponent = document.querySelector('[wire\\:id], form[wire\\:submit]');
|
||||||
|
if (livewireComponent) {
|
||||||
|
const wireId = livewireComponent.getAttribute('wire:id');
|
||||||
|
if (wireId) {
|
||||||
|
const livewire = window.Livewire.find(wireId);
|
||||||
|
if (livewire && livewire.get) {
|
||||||
|
content = livewire.get(this.fieldName) || '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try Alpine.js $wire
|
||||||
|
const alpineData = Alpine.$data(livewireComponent);
|
||||||
|
if (alpineData && alpineData.$wire && alpineData.$wire.get) {
|
||||||
|
content = alpineData.$wire.get(this.fieldName) || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not get content:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing content if new fetch failed
|
||||||
|
if (!content && this.content) {
|
||||||
|
content = this.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
if (!this.content || this.content.trim() === '') {
|
||||||
|
if (this.iframeSrc) {
|
||||||
|
URL.revokeObjectURL(this.iframeSrc);
|
||||||
|
this.iframeSrc = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create form data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('content', this.content);
|
||||||
|
formData.append('type', this.type);
|
||||||
|
formData.append('_token', document.querySelector('meta[name="csrf-token"]')?.content || '');
|
||||||
|
|
||||||
|
// Fetch preview HTML
|
||||||
|
const response = await fetch(this.previewUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'text/html',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Preview request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
// Create blob URL for iframe
|
||||||
|
const blob = new Blob([html], { type: 'text/html; charset=utf-8' });
|
||||||
|
|
||||||
|
// Revoke old URL if exists
|
||||||
|
if (this.iframeSrc) {
|
||||||
|
URL.revokeObjectURL(this.iframeSrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iframeSrc = URL.createObjectURL(blob);
|
||||||
|
this.isLoading = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Preview error:', error);
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\PageController;
|
use App\Http\Controllers\PageController;
|
||||||
use App\Http\Controllers\BlogController;
|
use App\Http\Controllers\BlogController;
|
||||||
|
use App\Http\Controllers\TemplatePreviewController;
|
||||||
use App\Models\Page;
|
use App\Models\Page;
|
||||||
|
|
||||||
// Debug Route - Veritabanı kontrolü
|
// Debug Route - Veritabanı kontrolü
|
||||||
@@ -60,5 +61,10 @@ Route::get('/', [PageController::class, 'index'])->name('homepage');
|
|||||||
Route::get('/blog', [BlogController::class, 'index'])->name('blog.index');
|
Route::get('/blog', [BlogController::class, 'index'])->name('blog.index');
|
||||||
Route::get('/blog/{slug}', [BlogController::class, 'show'])->name('blog.show');
|
Route::get('/blog/{slug}', [BlogController::class, 'show'])->name('blog.show');
|
||||||
|
|
||||||
|
// Template Preview (admin panel için)
|
||||||
|
Route::post('/admin/template-preview', [TemplatePreviewController::class, 'preview'])
|
||||||
|
->middleware(['auth'])
|
||||||
|
->name('template.preview');
|
||||||
|
|
||||||
// Pages (en sonda olmalı - catch-all)
|
// Pages (en sonda olmalı - catch-all)
|
||||||
Route::get('/{slug}', [PageController::class, 'show'])->name('page.show');
|
Route::get('/{slug}', [PageController::class, 'show'])->name('page.show');
|
||||||
|
|||||||
Reference in New Issue
Block a user