feat: ✨ handle image on general config

This commit is contained in:
Abdel LAMRANI 2026-09-24 16:16:23 +02:00
parent 159b96b8db
commit 4c45312724
13 changed files with 311 additions and 4 deletions

View file

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\GeneralConfig;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class GeneralConfigController extends Controller
{
/**
* Display the general configuration edit form.
*
* @return View
*/
public function edit(): View
{
return view('bo.general-config.edit', [
'config' => GeneralConfig::current(),
]);
}
/**
* Upload and store the "Nos métiers" page image.
*
* @param Request $request
* @return RedirectResponse
*/
public function upload(Request $request): RedirectResponse
{
$request->validate(
['image' => 'required|image|mimes:jpeg,png,jpg|max:2048|dimensions:width=742,height=464'],
['image.dimensions' => "L'image doit faire exactement 742 × 464 px."]
);
$config = GeneralConfig::current();
$ancienne = $config->get('image');
$photo = $request->file('image');
$imageName = 'image-' . now()->format('YmdHis') . '.' . $photo->extension();
$path = $photo->storeAs(
sprintf('modelfiles/general-config/%s/%s', date('Y'), date('m')),
$imageName,
'public'
);
$config->put('image', $path)->save();
if ($ancienne && $ancienne !== $path) {
Storage::disk('public')->delete($ancienne);
}
return redirect()
->route('bo.general-config.edit')
->with('success', 'Image mise à jour.');
}
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Fo;
use App\Http\Controllers\Controller;
use App\Models\GeneralConfig;
use App\Models\HomeSliders;
class HomeController extends Controller
@ -16,7 +17,9 @@ class HomeController extends Controller
$slides = HomeSliders::where([
['status', 1]
])->orderBy('order', 'ASC')->get();
$imageMetiers = GeneralConfig::current()->get('image');
return view('fo.home', compact('slides'));
return view('fo.home', compact('slides', 'imageMetiers'));
}
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Fo;
use App\Http\Controllers\Controller;
use App\Models\BusinessSector;
use App\Models\Employees;
use App\Models\GeneralConfig;
class OurTradeController extends Controller
{
@ -24,6 +25,8 @@ class OurTradeController extends Controller
$nine_videos = Employees::query()->select('video', 'photo', 'firstname', 'lastname', 'job_title')
->whereNotNull('video')->inRandomOrder()->limit(9)->get();
return view('fo.ourtrade', compact('secteurs_simples', 'secteurs_complexes', 'nine_videos'));
$imageMetiers = GeneralConfig::current()->get('image');
return view('fo.ourtrade', compact('secteurs_simples', 'secteurs_complexes', 'nine_videos', 'imageMetiers'));
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property array<string, mixed>|null $data
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
*/
class GeneralConfig extends Model
{
protected $fillable = ['data'];
protected $casts = [
'data' => 'array',
];
/**
* Get the unique configuration row.
*
* @return self
*/
public static function current(): self
{
return static::query()->firstOrCreate([], ['data' => []]);
}
/**
* Get a setting value from the JSON data.
*
* @param string $key Setting key.
* @param mixed $default Value returned.
* @return mixed
*/
public function get(string $key, mixed $default = null): mixed
{
return data_get($this->data, $key, $default);
}
/**
* Set a setting value in the JSON data.
*
* @param string $key Setting key.
* @param mixed $value Value to store.
* @return self
*/
public function put(string $key, mixed $value): self
{
$data = $this->data ?? [];
data_set($data, $key, $value);
$this->data = $data;
return $this;
}
}

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('general_configs', function (Blueprint $table) {
$table->id();
$table->json('data')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('general_configs');
}
};

View file

@ -11,3 +11,6 @@
@import "./base/fonts";
@import "./base/classes";
@import "./base/common";
// * PAGE
@import "./pages/general-config";

View file

@ -0,0 +1,42 @@
.gc-image {
width: 464px;
max-width: 100%;
margin: 0 auto;
}
.gc-preview {
position: relative;
overflow: hidden;
border: 1px solid var(--bs-border-color, #dee2e6);
border-radius: var(--bs-border-radius, 0.375rem);
margin-bottom: 1.5rem;
aspect-ratio: 742 / 464;
background-color: var(--bs-light, #f8f9fa);
}
.gc-preview__img {
position: absolute;
width: 100%;
height: 100%;
inset: 0;
object-fit: contain;
}
.gc-preview__empty {
position: absolute;
top: 50%;
left: 50%;
color: var(--bs-secondary-color, #6c757d);
transform: translate(-50%, -50%);
}
.gc-image__actions {
margin-top: 1rem;
text-align: center;
}
#generalConfigAccordion {
--bs-accordion-active-bg: #f8f8f8;
--bs-accordion-active-color: #212529;
--bs-accordion-btn-focus-box-shadow: none;
}

View file

@ -39,6 +39,7 @@
@import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/badge";
@import "bootstrap/scss/offcanvas";
@import "bootstrap/scss/accordion";
// 6. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss`
@import "bootstrap/scss/utilities/api";

View file

@ -0,0 +1,88 @@
@extends('layouts.bo')
@section('content')
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Configuration générale</h5>
<a href="{{ route('bo.dashboard') }}" class="btn btn-sm btn-primary">Retour</a>
</div>
<div class="card-body">
<div class="accordion" id="generalConfigAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="heading-metiers">
<button class="accordion-button" type="button"
data-bs-toggle="collapse"
data-bs-target="#collapse-metiers"
aria-expanded="true"
aria-controls="collapse-metiers">
Image de la page : <strong> Nos métiers</strong>
</button>
</h2>
<div id="collapse-metiers"
class="accordion-collapse collapse show"
aria-labelledby="heading-metiers"
data-bs-parent="#generalConfigAccordion">
<div class="accordion-body">
<form action="{{ route('bo.general-config.upload') }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PATCH')
<div class="gc-image">
<div class="gc-preview">
<img id="preview-image"
src="{{ $config->get('image') ? Storage::url($config->get('image')) : '' }}"
alt="Image de la page Nos métiers"
class="gc-preview__img {{ $config->get('image') ? '' : 'd-none' }}">
<span id="preview-empty"
class="gc-preview__empty {{ $config->get('image') ? 'd-none' : '' }}">
Aucune image
</span>
</div>
<label for="image" class="form-label">
Photo <span class="text-danger">*</span>
</label>
<input type="file" id="image" name="image" accept="image/png, image/jpeg"
class="form-control @error('image') is-invalid @enderror" required>
<div class="form-text">JPEG ou PNG, 2 Mo max., 742 × 464 px.</div>
@error('image')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<div class="gc-image__actions">
<button type="submit" class="btn btn-secondary">Enregistrer</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', () => {
const input = document.getElementById('image');
const img = document.getElementById('preview-image');
const empty = document.getElementById('preview-empty');
const original = img.getAttribute('src');
input.addEventListener('change', () => {
const file = input.files[0];
const src = file ? URL.createObjectURL(file) : original;
if (src) img.src = src;
img.classList.toggle('d-none', !src);
empty.classList.toggle('d-none', Boolean(src));
});
});
</script>
@endpush

View file

@ -60,7 +60,11 @@
</div>
<div class="info">
<h3 class="post-chapter">40 salarié(e)s</h3>
<img src="{{ Vite::asset('resources/assets/images/quarante-salaries.jpg') }}" alt="40 salarié(e)s">
<img src="{{ $imageMetiers
? Storage::url($imageMetiers)
: Vite::asset('resources/assets/images/quarante-salaries.jpg', 'build')
}}" alt="40 salarié(e)s" width="363px" height="227px"
>
<div><a class="second-btn" href="{{ route('fo.ourtrade') }}">Rencontrez l'équipe Jeusselin</a></div>
</div>
</div>

View file

@ -8,7 +8,14 @@
<section class="work--head container flex">
<h1 class="post-title">Nos métiers</h1>
<div class="work--head--detail grid">
<div class="detail--img"><img src="{{ Vite::asset('resources/assets/images/quarante-salaries-large.jpg', 'build') }}" alt="L'Equipe Jeusselin"></div>
<div class="detail--img">
<img src="{{ $imageMetiers
? Storage::url($imageMetiers)
: Vite::asset('resources/assets/images/quarante-salaries-large.jpg', 'build') }}"
alt="L'Equipe Jeusselin">
</div>
{{-- <div class="detail--txt">
<h2 class="post-chapter">Rencontrez l'équipe en vidéo</h2>
<p>Progressively disseminate high-payoff niche markets without enterprise action items. Completely provide access to cutting-edge methods of empowerment before synergistic networks. Conveniently orchestrate user friendly data before.</p>

View file

@ -70,6 +70,7 @@
<div class="dropdown-menu">
<a class="dropdown-item" href="{{route('bo.business-sector.index')}}">Secteurs</a>
<a class="dropdown-item" href="{{route('bo.employees.index')}}">Employés</a>
<a class="dropdown-item" href="{{route('bo.general-config.edit')}}">Configuration générale</a>
</div>
</li>
<a class="nav-link" href="{{route('bo.home-sliders.index')}}">Slider de l'accueil</a>

View file

@ -6,6 +6,7 @@ use App\Http\Controllers\Bo\CotationsController;
use App\Http\Controllers\Bo\CustomerController;
use App\Http\Controllers\Bo\DashboardController;
use App\Http\Controllers\Bo\EmployeesController;
use App\Http\Controllers\Bo\GeneralConfigController;
use App\Http\Controllers\Bo\HomeSlidersController;
use App\Http\Controllers\Bo\ObjectivesController;
use App\Http\Controllers\Bo\PublicPdfsController;
@ -82,5 +83,10 @@ Route::prefix('bo')->name('bo.')->middleware('admin-secure')->group(function ()
->only(['index', 'edit', 'update'])
->withoutMiddleware('htmlsanitize')
->middleware('htmlsanitize:content');
// * GENERAL CONFIG
Route::get('/config-general', [GeneralConfigController::class, 'edit'])->name('general-config.edit');
Route::patch('/config-general', [GeneralConfigController::class, 'upload'])->name('general-config.upload');
});
});