feat(cms): implement complete SysSiteCMS content management system
Initial implementation of the HomeSysPanel CMS built with Laravel 13.0. Includes complete admin panel, content management, user authentication, modular architecture for products/services/chatbot, and marketing tools. Key features: - User authentication with email verification and password reset - Role-based access control (Admin/User with granular permissions) - Content management (pages, news, FAQ, downloads, galleries, banners) - Contact management system with subjects and responses - Marketing tools (email campaigns, visitor analytics, social sharing) - Modular architecture (Products, Services, Chatbot modules) - AI Chatbot integration with OpenRouter.ai and LlamaCpp support - Visitor tracking with geolocation - SEO optimization with meta tags and Open Graph - Responsive design with Bootstrap 5 - Dark mode support - Maintenance mode - Feature request system
This commit is contained in:
Executable
+161
@@ -0,0 +1,161 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', ($news->seo_title ?: $news->title) . \App\Models\Setting::get('seo_title_suffix', ' - SysSiteCMS'))
|
||||
@section('meta_description', $news->seo_description ?: Str::limit($news->summary ?? $news->title, 160))
|
||||
@section('meta_keywords', $news->seo_keywords)
|
||||
@section('og_title', $news->title)
|
||||
@section('og_description', Str::limit($news->summary ?? $news->title, 200))
|
||||
@section('og_image', $news->featured_image ? asset('storage/' . $news->featured_image) : '')
|
||||
|
||||
@section('content')
|
||||
<div class="container py-5">
|
||||
<article>
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-5 fw-bold text-primary">{{ $news->title }}</h1>
|
||||
<div class="d-flex justify-content-center gap-4 text-muted mt-3">
|
||||
@if($news->author)
|
||||
<span><i class="bi bi-person me-1"></i> {{ $news->author }}</span>
|
||||
@endif
|
||||
<span><i class="bi bi-calendar me-1"></i> {{ ($news->published_at ?? $news->created_at)->format('d/m/Y H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Featured Image -->
|
||||
@if($news->featured_image)
|
||||
<div class="text-center mb-5">
|
||||
<img src="{{ asset('storage/' . $news->featured_image) }}" alt="{{ $news->title }}" class="img-fluid rounded shadow-sm" style="max-height: 500px; object-fit: cover;">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Summary -->
|
||||
@if($news->summary)
|
||||
<div class="alert alert-light border-start border-primary border-4 ps-4 mb-4">
|
||||
<p class="lead mb-0">{{ $news->summary }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Content -->
|
||||
@if($news->content)
|
||||
<div class="mb-5" style="line-height: 1.8; font-size: 1.1rem;">
|
||||
{!! nl2br(e($news->content)) !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Additional Images Gallery -->
|
||||
@if($news->images->count() > 0)
|
||||
<div class="mb-5">
|
||||
<h3 class="fw-bold mb-4"><i class="bi bi-images me-2 text-primary"></i> Galeria de Imagens</h3>
|
||||
<div class="row g-3">
|
||||
@foreach($news->images as $index => $image)
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm overflow-hidden" style="cursor: pointer;" onclick="openCarousel({{ $index }})">
|
||||
<img src="{{ asset('storage/' . $image->file_path) }}" class="card-img-top" alt="{{ $image->description }}" style="height: 160px; object-fit: cover; transition: transform 0.3s;">
|
||||
@if($image->description)
|
||||
<div class="card-body p-2 text-center bg-light">
|
||||
<small class="text-muted">{{ $image->description }}</small>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</article>
|
||||
|
||||
<hr class="my-5">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="{{ route('news.index') }}" class="btn btn-outline-primary px-4 rounded-pill fw-bold">
|
||||
<i class="bi bi-arrow-left me-2"></i> Todas as Notícias
|
||||
</a>
|
||||
<a href="{{ route('index') }}" class="btn btn-outline-secondary px-4 rounded-pill">
|
||||
<i class="bi bi-house me-2"></i> Início
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Carousel Modal for Additional Images -->
|
||||
@if($news->images->count() > 0)
|
||||
<div class="modal fade" id="newsCarouselModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-dark border-0">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title text-white" id="carouselImageTitle"></h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fechar"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div id="newsCarousel" class="carousel slide" data-bs-ride="false">
|
||||
<div class="carousel-inner">
|
||||
@foreach($news->images as $index => $image)
|
||||
<div class="carousel-item {{ $index === 0 ? 'active' : '' }}" data-index="{{ $index }}" data-title="{{ $image->description ?? '' }}">
|
||||
<div class="d-flex align-items-center justify-content-center" style="min-height: 400px; max-height: 75vh;">
|
||||
<img src="{{ asset('storage/' . $image->file_path) }}" class="d-block img-fluid" alt="{{ $image->description }}" style="max-height: 75vh; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@if($news->images->count() > 1)
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#newsCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Anterior</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#newsCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Próxima</span>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0 justify-content-center">
|
||||
<span class="text-white-50" id="carouselCounter"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@push('css')
|
||||
<style>
|
||||
.card[style*="cursor: pointer"]:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@if($news->images->count() > 0)
|
||||
@push('js')
|
||||
<script>
|
||||
const totalImages = {{ $news->images->count() }};
|
||||
let carouselInstance = null;
|
||||
|
||||
function openCarousel(index) {
|
||||
if (!carouselInstance) {
|
||||
carouselInstance = new bootstrap.Carousel(document.getElementById('newsCarousel'));
|
||||
}
|
||||
carouselInstance.to(index);
|
||||
updateCounter(index);
|
||||
|
||||
const item = document.querySelector(`#newsCarousel .carousel-item[data-index="${index}"]`);
|
||||
if (item) {
|
||||
document.getElementById('carouselImageTitle').textContent = item.dataset.title;
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('newsCarouselModal')).show();
|
||||
}
|
||||
|
||||
function updateCounter(index) {
|
||||
document.getElementById('carouselCounter').textContent = `${index + 1} / ${totalImages}`;
|
||||
}
|
||||
|
||||
document.getElementById('newsCarousel').addEventListener('slid.bs.carousel', function (e) {
|
||||
const activeItem = document.querySelector('#newsCarousel .carousel-item.active');
|
||||
const index = parseInt(activeItem.dataset.index);
|
||||
updateCounter(index);
|
||||
document.getElementById('carouselImageTitle').textContent = activeItem.dataset.title;
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endif
|
||||
Reference in New Issue
Block a user