Starter pack Auth BO + with some modules

This commit is contained in:
Jérémy Munsch 2022-08-02 11:17:03 +00:00 • committed by Vincent BRIDIER
parent df67cc9188
commit f6901950d3
98 changed files with 20689 additions and 476 deletions

View file

@ -14,5 +14,8 @@ trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[*.vue]
indent_size = 2
[docker-compose.yml]
indent_size = 4

3
.gitignore vendored
View file

@ -9,6 +9,7 @@
/public/uploads
/public/assets
/public/cache
/public/mix-manifest.json
/storage/*.key
/vendor
.env
@ -21,4 +22,4 @@ npm-debug.log
yarn-error.log
/.idea
/.vscode
auth.json
auth.json

View file

@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
/**
* Creates a new user
*/
class CreateAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:create';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a backend admin user';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
$email = null;
while (is_null($email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);
$validator->validate();
$email = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid email %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
$name = null;
while (is_null($name)) {
try {
$tmp = $this->ask('Type the wanted user name', 'Admin');
$validator = Validator::make(['name' => $tmp], [
'name' => 'required|min:5,max:255'
]);
$validator->validate();
$name = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
$password = null;
while (
($password = $this->secret(
(!\is_null($password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password'
)) !== $this->secret('Confirm the password')
) {
continue;
};
$admin = new User([
'name' => $name,
'email' => $email,
'password' => $password
]);
$admin->saveOrFail();
$this->info('User created.');
return 0;
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class LoginController extends Controller
{
use AuthenticatesUsers;
use ThrottlesLogins;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:backend')->except('logout');
}
/**
* Show the application's login form.
*
* @return \Illuminate\View\View
*/
public function showLoginForm()
{
return view('bo.auth.login');
}
/**
* Attempt to log the user into the application.
*
* @param \Illuminate\Http\Request $request
* @return boolean
*/
protected function attemptLogin(Request $request)
{
return Auth::guard('backend')->attempt(
$this->credentials($request),
$request->filled('remember')
);
}
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param null $user
* @return mixed
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing, Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
*/
protected function authenticated(Request $request, $user)
{
/** @var \App\Models\User|null */
$user = Auth::guard('backend')->user();
if ($user) {
$user->last_connection = now();
$user->saveOrFail();
Session::flash('info', trans('Vous êtes à présent connecté'));
}
}
/**
* The user has logged out of the application.
*
* @param \Illuminate\Http\Request $request
* @return mixed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
protected function loggedOut(Request $request)
{
Session::flash('info', trans('Vous êtes à présent déconnecté'));
}
/**
* Get the post register / login redirect path.
*
* @return string
*/
public function redirectPath()
{
return response()->redirectToRoute('bo.dashboard')->getTargetUrl();
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
class HomeController extends Controller
{
/**
* Dashboard view
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function dashboard()
{
return view('bo.dashboard');
}
}

View file

@ -2,14 +2,120 @@
namespace App\Http\Controllers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields would be concatenate to make the search.
* @return Builder
*/
protected function searchQuery(
Builder $query,
string $search,
callable $searchQuery = null,
string ...$fields
): Builder {
if (count($fields)) {
// Search using words.
$words = explode(' ', $search);
$query->where(function ($query) use ($searchQuery, $words, $search, $fields) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$this->searchOnFields($query, $word, $fields);
} //end foreach
// Search whole using whole sentence.
$this->searchOnFields($query, $search, $fields);
if ($searchQuery) {
$searchQuery($query);
}
});
} //end if
return $query;
}
/**
* Build query search on all fields
*
* @param Builder $query
* @param string $search
* @param array $fields
* @return Builder
*/
private function searchOnFields(Builder $query, string $search, array $fields): Builder
{
$table = $query->getModel()->getTable();
foreach ($fields as $f) {
$f = str_replace('?', '\?', $f);
if (is_array($f)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
} else {
$query->orWhereRaw(
"UPPER(`$table`.`$f`) LIKE UPPER(?)",
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
}
}
return $query;
}
/**
* Sort columns with a query
*
* @param Builder $query
* @param array $ignore
* @return Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): Builder
{
$rName = request()->route()->getName();
$table = $query->getModel()->getTable();
if (request()->sort_col) {
Session::put("$rName.sort_col", request()->sort_col);
}
if (request()->sort_way) {
Session::put("$rName.sort_way", request()->sort_way);
}
if (request()->rst) {
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (request()->sort_col and request()->sort_way and Schema::hasColumn($table, request()->sort_col)) {
if (
!isset($ignore) or
(isset($ignore) and !in_array(request()->sort_col, $ignore))
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
} elseif (Schema::hasColumn($table, 'order')) {
$query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc');
}
return $query;
}
}

View file

@ -14,8 +14,7 @@ class Kernel extends HttpKernel
* @var array<int, class-string|string>
*/
protected $middleware = [
// phpcs:ignore
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
@ -31,20 +30,16 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
'htmlsanitize',
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// phpcs:ignore
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
\App\Http\Middleware\SecureHeaders::class
]
];
/**
@ -64,5 +59,6 @@ class Kernel extends HttpKernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class
];
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Facades\Session;
class Authenticate extends Middleware
{
@ -15,8 +16,23 @@ class Authenticate extends Middleware
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
$logins = [
'backend' => route('bo.login')
];
if (!$request->expectsJson()) {
// * Notify must login
Session::flash('warning', trans('Veuillez vous connecter pour continuer'));
foreach ($request->route()->gatherMiddleware() as $middleware) {
if (\is_string($middleware) and strpos($middleware, 'auth:') !== false) {
$route = $logins[str_replace('auth:', '', $middleware)] ?? null;
if ($route) {
return $route;
}
}
}
return collect($logins)->first() ?? '/';
}
abort(401);
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Http\Middleware;
use App\Lib\ToolboxHelper;
use Illuminate\Http\Request;
class HtmlSanitizer
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param mixed ...$params
* @return mixed
*/
public function handle(Request $request, \Closure $next, ...$params)
{
$inputs = $request->all();
array_walk_recursive($inputs, function (&$input, $name) use ($params) {
if (!is_string($input)) {
return;
}
if (in_array($name, $params)) {
// ? Is a json input ?
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
// ! Decode each json field recursive.
\array_walk_recursive($isJson, function (&$item) {
if (\is_string($item)) {
$item = clean($item);
$item = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($item);
}
});
} else {
$input = clean($input);
$input = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($input);
}
} else {
$input = strip_tags($input);
}
});
$request->merge($inputs);
return $next($request);
}
}

View file

@ -2,8 +2,6 @@
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -17,13 +15,16 @@ class RedirectIfAuthenticated
* @param string|null ...$guards
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next, ...$guards)
public function handle(Request $request, \Closure $next, ...$guards)
{
$logins = [
'backend' => route('bo.dashboard')
];
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
return \redirect()->to($logins[$guard]);
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
class SecureHeaders
{
/**
* Enumerate headers which you do not want in your application's responses.
* Great starting point would be to go check out @Scott_Helme's:
* https://securityheaders.com/
*
* @var array
*/
private $unwantedHeaderList = [
'X-Powered-By',
'Server',
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
View::share('nonce', $nonce = \uniqid());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-$nonce'";
$reportUri = Route::has('cspReportUri') ?
\sprintf('report-uri %s', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
$this->removeUnwantedHeaders($this->unwantedHeaderList);
$response = $next($request);
$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;');
$cspPolicies = [
"default-src 'none';",
"manifest-src 'self';",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob:;",
"img-src * data: blob:;",
// "frame-src https://www.youtube.com;",
"font-src 'self' data:;",
// ! Using CKEditor will must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline';",
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' $jsDev;"
];
$response->headers->set(
'Content-Security-Policy',
implode(' ', $cspPolicies) . $reportUri
);
$response->headers->set('Feature-Policy', 'fullscreen \'self\';');
return $response;
}
/**
* Remove unwanted headers
*
* @param array $headerList
* @return void
*/
private function removeUnwantedHeaders(array $headerList)
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

499
app/Lib/ToolboxHelper.php Normal file
View file

@ -0,0 +1,499 @@
<?php
namespace App\Lib;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ToolboxHelper
{
/**
* Strip scheme (https) on oembed urls
* Only embed allowed are for MP4 self hosted videos right now
*
* https is force by ckeditor MediaElements which natively supports
* only cloud hosted medias (youtube, spotify etc..)
*
* @param string $input
* @return string
*/
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
{
$input = \preg_replace('/(?<=<oembed url=")(https:\/\/)(?=.*?">)/m', '', $input);
return $input;
}
/**
* Assert That no Back Office url is present on txt
*
* @param string $field
* @param string $text
* @return string
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
*/
public static function assertNoInternalBoUrls(string $field, string $text): string
{
$routes = Route::getRoutes();
// * Url replace and email replace.
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
function ($matches) use ($routes, $field) {
$match = collect($matches)->first() ?? '';
$url = $match;
try {
$request = Request::create($url);
// If a route is match then it do not throws exception.
if (
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo'
) {
// Removes the matched url.
throw ValidationException::withMessages([
$field => sprintf(
'%s : Il est interdit de coller une url du back office dans ce champ',
trans($field)
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Get files path uploaded with ckeditor custom upload plugin (crud-policies)
*
* @param string $text
* @param array $detectExtensions
* @return array
*/
public static function getCkEditorLocalUploadedFiles(
string $text,
array $detectExtensions = ['png', 'svg', 'jpg', 'gif']
): array {
$out = [];
\preg_match_all(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/src="(?P<url>\/storage\/images\/crud-policies\/uploads\/\d{4}-\d{2}-\d{2}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[-a-zA-Z0-9@%._\+~#=]{2,256}\.(?:' . implode('|', $detectExtensions) . '))"/',
$text,
$out
);
return collect($out['url'] ?? [])->map(function (string $urlEncodedFilePath) {
return \urldecode($urlEncodedFilePath);
})->all();
}
/**
* Assert That only external url is present on txt
*
* @param string $field
* @param string $text
* @return string
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
*/
public static function assertNoInternalUrls(string $field, string $text): string
{
self::assertNoInternalBoUrls($field, $text);
$routes = Route::getRoutes();
// * Url replace and email replace.
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
function ($matches) use ($routes, $field) {
$match = collect($matches)->first() ?? '';
$url = $match;
try {
$request = Request::create($url);
// If a route is match then it do not throws exception.
if (
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
$routes->match($request)
) {
// Removes the matched url.
throw ValidationException::withMessages([
$field => sprintf(
'%s : Il est interdit de coller une url de %s dans ce champ',
trans($field),
config('app.name')
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Generates an array from a dot separated string
*
* @param string $path
* @param mixed $value
* @param string $separator
* @return array
*/
public static function dotStringToNestedArray(string $path, $value, string $separator = '.'): array
{
$arr = $value;
$keys = array_reverse(explode($separator, $path));
foreach ($keys as $key) {
$arr = [$key => $arr];
}
return $arr;
}
/**
* Destroy image
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeImage(Model $model, string $attribute)
{
$path = ltrim($model->{$attribute}, '/');
if (File::exists($path) and \strpos($path, 'storage') === 0) {
File::delete($path);
}
}
/**
* Destroy image
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeOldImage(Model $model, string $attribute)
{
$originalPath = ltrim($model->getOriginal($attribute), '/');
if (File::exists($originalPath) and \strpos($originalPath, 'storage') === 0) {
File::delete($originalPath);
}
}
/**
* Remove pictures in ckeditor
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeImagesOnCkEditor(Model $model, string $attribute)
{
// * Remove inline uploaded images
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
->map(function (string $originalPicturePath) {
// * If a picture is not used anymore
if (File::exists(\public_path($originalPicturePath))) {
File::delete(\public_path($originalPicturePath));
}
});
}
/**
* Remove unused pictures in ckeditor
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeUnusedImagesOnCkEditor(Model $model, string $attribute)
{
// * Remove unused inline uploaded images
if ($model->{$attribute} != $model->getOriginal($attribute)) {
$updatedPicturesPath = collect(
ToolboxHelper::getCkEditorLocalUploadedFiles($model->{$attribute})
);
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
->map(function (string $originalPicturePath) use ($updatedPicturesPath) {
// * If a picture is not used anymore
if (!$updatedPicturesPath->contains($originalPicturePath)) {
if (File::exists(\public_path($originalPicturePath))) {
File::delete(\public_path($originalPicturePath));
}
}
});
}
}
/**
* Store an image of different types
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $image
* @return string
* @throws \ErrorException If file copy fails.
*/
public static function storeImage(Model $model, $image): string
{
$table = $model->getTable();
if (\is_object($image) and $image instanceof \Illuminate\Http\UploadedFile) {
/** @var \Illuminate\Http\UploadedFile $image */
$newPath = self::prepareMoveFile($table, $image->getClientOriginalName());
$newPath = str_replace(storage_path('app'), '', $newPath);
$image->storePubliclyAs(
pathinfo($newPath, \PATHINFO_DIRNAME),
pathinfo($newPath, \PATHINFO_BASENAME)
);
return 'storage' . str_replace('/public', '', $newPath);
}
if (\is_object($image) and $image instanceof \SplFileInfo) {
/** @var \SplFileInfo $image */
$newPath = self::prepareMoveFile($table, $image->getBaseName());
if (!File::copy($image->getRealPath(), $newPath)) {
throw new \ErrorException(sprintf(
'Failed to copy file %s to %s',
$image->getRealPath(),
$newPath
));
}
return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
}
return strval($image);
}
/**
* Prepare storage path and return
* the image full path
*
* @param string $table Model table name.
* @param string $filename The basename for the file.
* @return string
*/
private static function prepareMoveFile(string $table, string $filename): string
{
// * Security filter + beautifier
$filename = self::sanitizeFileName($filename);
// * Security filter
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
// * Slugify filename
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
(\strlen($ext) ? ".{$ext}" : '');
$pathFolder = \storage_path(sprintf('app/public/images/%s', $table));
if (!File::exists($pathFolder)) {
File::makeDirectory($pathFolder, 0755, true);
}
$filename = sprintf('%s-%s', Str::uuid(), $filename);
$fullPath = sprintf('%s/%s', $pathFolder, $filename);
if (File::exists($fullPath)) {
File::delete($fullPath);
}
return $fullPath;
}
/**
* Asserts that the field is unique
*
* @param string $table
* @param string $field
* @param mixed $value
* @param integer|null $id
* @param callable|null $query
* @return void
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
*/
public static function assertFieldIsUnique(
string $table,
string $field,
$value,
?int $id = null,
?callable $query = null
) {
if (!$query) {
Validator::make([$field => $value], [
$field => Rule::unique($table, $field)->ignore($id),
], [
$field . '.unique' => trans('La valeur :value pour le champ :attribute est déjà utilisé', [
'attribute' => $field,
'value' => $value
])
])->validate();
return;
}
Validator::make([$field => $value], [
$field => Rule::unique($table)->where($query)->ignore($id),
], [
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
'attribute' => $field
])
])->validate();
}
/**
* Validate recaptcha V3
*
* @param Request $request
* @return void
*/
public static function validateRecaptchaV3(Request $request)
{
Validator::validate($request->all(), [
'g-recaptcha-response' => [
'required', 'string',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) {
try {
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('recaptcha.private'),
'response' => \strval($value),
'remoteip' => app('clientIp')
]);
if ($e = $response->toException()) {
throw $e;
}
if (
!$response->json('success') or
!$response->json('challenge_ts') or
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST) or
!$response->json('score')
) {
throw new \ErrorException();
}
if (\floatval($response->json('score')) < 0.8) {
$fail('Vous n\'avez pas obtenu un score assez élevé lors de la validation du recaptcha');
}
} catch (\Exception $e) {
$fail('Erreur lors de la validation du formulaire veuillez réssayer');
} //end try
}
]
], [], ['g-recaptcha-response' => 'reCaptcha']);
}
/**
* * Register CSP report URI
*
* Put this function inside RouteServiceProvider,
* and call it in RouteServiceProvider::boot
*
* ! Use this in conjonction with kwaadpepper/exception-handler
*
* @return void
*/
public static function cspReportRoute()
{
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
abort(401);
}
// * Flatten if needed
$cspReport = collect(request()->json('csp-report'))
->flatMap(function ($v, $k) {
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
})->all();
// * Filter array keys
$cspReport = collect(array_intersect_key($cspReport, [
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
'referrer' => '', 'violated-directive' => ''
]));
report(new \Illuminate\Http\Client\HttpClientException(sprintf(
"CSP REPORT for %s : \n %s",
$cspReport->get('document-uri'),
$cspReport->implode("\n")
)));
})->middleware('throttle:5,2')
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
->name('cspReportUri');
}
/**
* Sanitize File name for uploaded files
*
* @param string $filename The original uploaded client file name.
* @param boolean $beautify Beautify the filename.
* @return string The sanitized file name.
*/
public static function sanitizeFileName(string $filename, bool $beautify = true): string
{
// * Sanitize filename
// phpcs:disable Generic.Files.LineLength.TooLong
$filename = preg_replace(
'~
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
~x',
'-',
$filename
);
// phpcs:enable
// * Avoids '.' '..' or '.hiddenFiles' .
$filename = Str::of($filename)->ltrim('.-');
// * Optional beautification
if ($beautify) {
$filename = static::beautifyFilename($filename);
}
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
/** @var string */
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
/** @var string */
$filename = pathinfo($filename, \PATHINFO_FILENAME);
$encoding = mb_detect_encoding($filename);
$encoding = $encoding ? $encoding : null;
return \mb_strcut(
$filename,
0,
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
$encoding
) . (!empty($extension) ? ".$extension" : '');
}
/**
* Beautify a filename
*
* @param string $filename
* @return string
*/
private static function beautifyFilename(string $filename): string
{
// Reduce consecutive characters.
$filename = preg_replace([
// * "file name.zip" becomes 'file-name.zip'
'/ +/',
// * "file___name.zip" becomes 'file-name.zip'
'/_+/',
// * "file---name.zip" becomes 'file-name.zip'
'/-+/'
], '-', $filename);
$filename = preg_replace([
// * "file--.--.-.--name.zip" becomes "file.name.zip"
'/-*\.-*/',
// * "file...name..zip" becomes "file.name.zip"
'/\.{2,}/'
], '.', $filename);
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
// * ".file-name.-" becomes "file-name"
$filename = trim($filename, '.-');
return $filename;
}
}

View file

@ -2,14 +2,15 @@
namespace App\Models;
use App\Lib\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class User extends Authenticatable
{
use HasApiTokens;
use HasFactory;
use Notifiable;
@ -41,5 +42,45 @@ class User extends Authenticatable
*/
protected $casts = [
'email_verified_at' => 'datetime',
'last_connection' => 'datetime',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted()
{
// * EVENT CLOSURES.
static::creating(function (self $user) {
static::assertEmailIsUnique($user->email);
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($user->password);
}
$user->remember_token = Str::random(10);
});
static::updating(function (self $user) {
static::assertEmailIsUnique($user->email, $user->id);
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($user->password);
}
$user->remember_token = Str::random(10);
});
}
// * METHODS
/**
* Asserts using validation that the email is unique
*
* @param mixed $email
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If user email as already used.
*/
private static function assertEmailIsUnique($email, ?int $id = null)
{
ToolboxHelper::assertFieldIsUnique((new self())->getTable(), 'email', $email, $id);
}
}

View file

@ -2,6 +2,8 @@
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
@ -24,5 +26,33 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
URL::forceScheme('https');
// * LOCAL FORCE eg: for Carbon
// * Remove numeric to prevent float seperator to breaking numeric transactions
\collect([\LC_TIME, \LC_MESSAGES, \LC_COLLATE, \LC_CTYPE, \LC_MONETARY])
->map(function ($contant) {
setlocale(
$contant,
\sprintf(
'%s_%s.UTF-8',
config('app.locale'),
\strtoupper(config('app.locale'))
)
);
});
Carbon::setLocale(config('app.locale'));
// * Force bootstrap paginator from Alexis with tooltips.
Paginator::defaultView('bo.modules.pagination');
// * HA PROXY FIX
app()->singleton('clientIp', function () {
if (
config('app.env') === 'local'
) {
return request()->getClientIp();
}
return request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
});
}
}

View file

@ -2,7 +2,11 @@
namespace App\Providers;
use App\Lib\ToolboxHelper;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
@ -23,6 +27,10 @@ class RouteServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->configureRateLimiting();
ToolboxHelper::cspReportRoute();
$this->routes(function () {
Route::middleware('web')
->namespace($this->namespace)
@ -33,4 +41,16 @@ class RouteServiceProvider extends ServiceProvider
->group(base_path('routes/web-bo.php'));
});
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for('web', function (Request $request) {
return Limit::perMinute(150)->by(optional($request->user())->id ?: app('clientIp'));
});
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
class TranslationServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
foreach ($this->collectLocalesStrings() as $locale) {
// * Suported locales
Cache::rememberForever(sprintf('translations.%s', $locale), function () use ($locale) {
$translations = [
'php' => $this->phpTranslations($locale),
'json' => $this->jsonTranslations($locale),
];
return $translations;
});
}
}
/**
* Gather provided langs checking from files
*
* @return Collection
*/
private function collectLocalesStrings(): Collection
{
return collect(File::allFiles(resource_path('lang/')))->flatMap(function ($file) {
if ($file->getRelativePath()) {
return [$file->getRelativePath() => ''];
}
})->keys();
}
/**
* Gather all php translations
*
* @param string $locale
* @return array
*/
private function phpTranslations(string $locale): array
{
$path = resource_path("lang/$locale");
return collect(File::allFiles($path))->flatMap(function ($file) use ($locale) {
$translation = $file->getBasename('.php');
return [$translation => trans($translation, [], $locale)];
})->all();
}
/**
* Gather json translations
*
* @param string $locale
* @return array
*/
private function jsonTranslations(string $locale): array
{
$path = resource_path("lang/$locale.json");
if (is_string($path) && is_readable($path)) {
return json_decode(file_get_contents($path), true) ?? [];
}
return [];
}
}

View file

@ -2,27 +2,35 @@
"name": "laravel/laravel",
"type": "project",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"diglactic/laravel-breadcrumbs": "^8.0",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"kwaadpepper/exception-handler": "^0.0.9",
"kwaadpepper/refresh-sitemap": "^1.0",
"laravel/framework": "^8.75",
"laravel/tinker": "^2.5",
"laravel/ui": "^3.4",
"ldc/web-admin-secure-laravel": "1.0.1",
"ldc/web-status-checker-laravel": "1.0.3",
"slevomat/coding-standard": "^8.3"
"mews/purifier": "^3.3",
"spatie/laravel-mix-preload": "^1.3"
},
"require-dev": {
"facade/ignition": "^2.5",
"fakerphp/faker": "^1.9.1",
"laravel-lang/lang": "10.0",
"laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^5.10",
"phpunit/phpunit": "^9.5.10"
"phpunit/phpunit": "^9.5.10",
"slevomat/coding-standard": "^8.3"
},
"autoload": {
"psr-4": {
@ -72,5 +80,4 @@
"url": "https://gitlab.com/api/v4/group/16669037/-/packages/composer/packages.json"
}
}
}

911
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ return [
*/
'defaults' => [
'guard' => 'web',
'guard' => 'backend',
'passwords' => 'users',
],
@ -36,7 +36,7 @@ return [
*/
'guards' => [
'web' => [
'backend' => [
'driver' => 'session',
'provider' => 'users',
],

115
config/purifier.php Normal file
View file

@ -0,0 +1,115 @@
<?php
/**
* Ok, glad you are here
* first we get a config instance, and set the settings
* $config = HTMLPurifier_Config::createDefault();
* $config->set('Core.Encoding', $this->config->get('purifier.encoding'));
* $config->set('Cache.SerializerPath', $this->config->get('purifier.cachePath'));
* if ( ! $this->config->get('purifier.finalize')) {
* $config->autoFinalize = false;
* }
* $config->load [$this->getConfig());
*
* You must NOT delete the default settings
* anything in settings should be compacted with params that needed to instance HTMLPurifier_Config.
*
* @link http://htmlpurifier.org/live/configdoc/plain.html
*/
// phpcs:disable Generic.Files.LineLength.TooLong
return [
'encoding' => 'UTF-8',
'finalize' => true,
'cachePath' => storage_path('app/purifier'),
'cacheFileMode' => 0755,
'settings' => [
'default' => [
'HTML.SafeIframe' => true,
// * For iframes
'URI.SafeIframeRegexp' => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%",
'HTML.Doctype' => 'HTML 4.01 Transitional',
'HTML.Allowed' => 'h1[style],h2[style],h3[style],h4[style],h5[style],h6[style],figure[class|style],oembed[url],div[style],b,strong,i,em,u,a[href|title|target|rel],ul[style],ol[style],dl[style],li,p[style],br,span[style|class],img[width|height|alt|src|style|class|title],iframe[src|width|height|class|frameborder|style],table[style|align|border|cellspacing|cellpadding|width],thead,tbody,th[colspan|align|valign|style],tr[align|valign|style],td[style|colspan|rowspan|scope|align|valign],hr,dl,dt',
'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align,list-style-type,float,width,height,vertical-align,border-color',
'AutoFormat.AutoParagraph' => false,
'AutoFormat.RemoveEmpty' => false,
'AutoFormat.RemoveEmpty.RemoveNbsp' => false,
],
'custom_definition' => [
'id' => 'html5-definitions',
'rev' => 1,
'debug' => true,
'elements' => [
// * http://developers.whatwg.org/sections.html
['section', 'Block', 'Flow', 'Common'],
['nav', 'Block', 'Flow', 'Common'],
['article', 'Block', 'Flow', 'Common'],
['aside', 'Block', 'Flow', 'Common'],
['header', 'Block', 'Flow', 'Common'],
['footer', 'Block', 'Flow', 'Common'],
// * Content model actually excludes several tags, not modelled here
['address', 'Block', 'Flow', 'Common'],
['hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common'],
// * http://developers.whatwg.org/grouping-content.html
['figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common'],
['figcaption', 'Inline', 'Flow', 'Common'],
['oembed', 'Inline', 'Flow', 'Common'],
// * http://developers.whatwg.org/the-video-element.html#the-video-element
['video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
'width' => 'Length',
'height' => 'Length',
'poster' => 'URI',
'preload' => 'Enum#auto,metadata,none',
'controls' => 'Bool',
]],
['source', 'Block', 'Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
]],
// * http://developers.whatwg.org/text-level-semantics.html
['s', 'Inline', 'Inline', 'Common'],
['var', 'Inline', 'Inline', 'Common'],
['sub', 'Inline', 'Inline', 'Common'],
['sup', 'Inline', 'Inline', 'Common'],
['mark', 'Inline', 'Inline', 'Common'],
['wbr', 'Inline', 'Empty', 'Core'],
// * http://developers.whatwg.org/edits.html
['ins', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
['del', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
],
'attributes' => [
['iframe', 'allowfullscreen', 'Bool'],
['table', 'height', 'Text'],
['td', 'border', 'Text'],
['th', 'border', 'Text'],
['tr', 'width', 'Text'],
['tr', 'height', 'Text'],
['tr', 'border', 'Text'],
['figure', 'class', 'Text'],
['figure', 'style', 'Text'],
['oembed', 'url', 'Text'],
['span', 'style', 'Text'],
['img', 'style', 'Text'],
],
],
'custom_attributes' => [
['a', 'target', 'Enum#_blank,_self,_target,_top'],
['ol', 'style', 'Text'],
['figure', 'style', 'Text'],
['img', 'style', 'Text'],
['span', 'style', 'Text'],
['oembed', 'url', 'Text'],
],
'custom_elements' => [
['u', 'Inline', 'Inline', 'Common'],
['oembed', 'Inline', 'Inline', 'Common'],
],
],
];

View file

@ -20,6 +20,7 @@ class CreateUsersTable extends Migration
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->date('last_connection')->nullable();
$table->timestamps();
});
}

5
jsconfig.json Normal file
View file

@ -0,0 +1,5 @@
{
"include": [
"./resources/js/**/*"
]
}

16842
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,9 +10,26 @@
"production": "mix --production"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@types/bootstrap": "^5.2.1",
"@types/webpack-env": "^1.17.0",
"axios": "^0.21",
"bootstrap": "^5.2.0",
"clean-webpack-plugin": "^4.0.0",
"laravel-mix": "^6.0.6",
"lodash": "^4.17.19",
"postcss": "^8.1.14"
"laravel-mix-clean": "^0.1.0",
"laravel-mix-polyfill": "^3.0.1",
"postcss": "^8.1.14",
"resolve-url-loader": "^5.0.0",
"sass": "^1.54.0",
"sass-loader": "^12.6.0",
"sweetalert2": "^11.4.24",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"vue": "^3.2.37",
"vue-loader": "^16.8.3"
}
}

9
public/browserconfig.xml Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/assets/images/mstile-150x150.png"/>
<TileColor>#091c58</TileColor>
</tile>
</msapplication>
</browserconfig>

14
public/site.webmanifest Normal file
View file

@ -0,0 +1,14 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/assets/images/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
}
],
"theme_color": "#091c58",
"background_color": "#091c58",
"display": "standalone"
}

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -0,0 +1,39 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="180.000000pt" height="180.000000pt" viewBox="0 0 180.000000 180.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,180.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1585 1356 l30 -25 -40 -41 c-55 -58 -135 -91 -135 -56 0 8 9 33 20
57 10 24 15 48 11 52 -5 5 -300 7 -657 5 -594 -4 -656 -6 -731 -23 l-83 -19 0
-123 c0 -68 4 -123 9 -123 23 0 33 -64 29 -195 -2 -112 -6 -139 -19 -149 -14
-9 -17 -34 -18 -135 l-2 -124 173 8 c95 4 247 11 338 15 161 8 875 4 935 -5
17 -2 103 -7 193 -11 l162 -7 0 143 c0 79 -3 140 -7 137 -3 -4 -12 -2 -20 4
-17 15 -19 314 -2 325 6 3 9 15 6 25 -3 11 1 26 9 33 10 10 14 39 14 94 l0 79
-52 16 c-29 9 -69 19 -88 23 -19 3 -46 15 -59 25 -13 11 -29 19 -35 19 -6 0 3
-11 19 -24z m-1306 -105 c9 -6 11 -20 6 -50 -4 -22 -7 -149 -7 -281 -1 -254 2
-270 46 -270 23 0 55 29 75 67 6 12 11 68 11 125 0 178 36 276 122 338 71 52
141 35 209 -50 71 -88 79 -350 16 -464 -50 -90 -147 -129 -215 -87 -50 32 -59
62 -33 106 13 22 16 35 9 33 -7 -2 -27 -32 -44 -67 -17 -35 -39 -70 -49 -77
-14 -10 -57 -14 -163 -14 l-145 0 7 48 c11 77 9 564 -2 594 -12 30 -3 43 38
51 44 8 105 7 119 -2z m717 -52 c14 -12 16 -42 13 -252 l-4 -239 28 -24 c33
-28 37 -29 63 -10 18 14 19 29 19 273 l0 258 51 3 c39 2 54 -1 68 -17 16 -17
16 -28 7 -115 -14 -125 -14 -328 0 -360 6 -13 27 -32 45 -42 19 -10 34 -24 34
-30 0 -6 -21 -28 -47 -48 -55 -43 -76 -42 -137 6 -34 27 -40 29 -48 14 -13
-23 -62 -46 -99 -46 -22 0 -41 10 -65 34 l-34 34 0 218 c-1 144 -5 240 -14
282 -12 56 -12 63 3 72 21 14 96 6 117 -11z m599 -33 c36 -28 105 -153 105
-191 0 -39 -37 -72 -117 -105 -87 -36 -123 -63 -123 -94 0 -27 27 -68 63 -96
l28 -22 56 42 c65 50 87 55 91 22 8 -57 -62 -140 -136 -163 -65 -19 -105 -5
-176 61 -35 33 -69 60 -75 60 -8 0 -7 6 3 16 9 8 16 23 16 32 8 332 9 343 62
406 58 71 136 83 203 32z"/>
<path d="M573 1093 c-14 -22 -19 -58 -24 -192 -3 -91 -2 -176 2 -188 13 -35
53 -57 79 -43 34 18 40 52 40 214 0 159 -9 209 -40 226 -28 15 -38 12 -57 -17z"/>
<path d="M1480 1087 c-38 -62 -36 -138 5 -153 21 -8 61 20 80 56 14 26 14 33
0 68 -9 22 -27 44 -41 51 -22 10 -26 8 -44 -22z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1 +0,0 @@
require('./bootstrap');

View file

@ -1,28 +0,0 @@
window._ = require('lodash');
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// forceTLS: true
// });

View file

@ -0,0 +1,18 @@
<?php
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
return [
'failed' => 'Ces identifiants ne correspondent pas à nos enregistrements.',
'password' => 'Le mot de passe fourni est incorrect.',
'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes.',
];

48
resources/lang/fr/fr.json Normal file
View file

@ -0,0 +1,48 @@
{
"A fresh verification link has been sent to your email address.": "Un nouveau lien de vérification a été envoyé à votre adresse email.",
"Before proceeding, please check your email for a verification link.": "Avant de continuer, veuillez vérifier vos emails, vous devriez avoir reçu un lien de vérification.",
"click here to request another": "cliquez ici pour faire une nouvelle demande",
"E-Mail Address": "Adresse email",
"Forbidden": "Interdit",
"Go to page :page": "Aller à la page :page",
"Hello!": "Bonjour !",
"If necessary, you may logout of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.": "Si nécessaire, vous pouvez vous déconnecter de toutes vos sessions de navigateur de tous vos appareils. Certaines de vos sessions sont listées plus bas ; pourtant, cette liste peut ne pas être exhaustive. Si vous sentez que votre compte a été compromis, vous pouvez aussi mettre à jour votre mot de passe.",
"If you did not create an account, no further action is required.": "Si vous n'avez pas créé de compte, vous pouvez ignorer ce message.",
"If you did not receive the email": "Si vous n'avez pas reçu l'email",
"If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si le bouton \":actionText\" ne fonctionne pas, copiez\/collez l'adresse ci-dessous dans votre navigateur :\n",
"Invalid signature.": "Signature invalide",
"Log out": "Déconnexion",
"Logout Other Browser Sessions": "Déconnecter les sessions ouvertes sur d'autres navigateurs",
"Manage and logout your active sessions on other browsers and devices.": "Gérer et déconnecter vos sessions actives sur les autres navigateurs et appareils.",
"Nevermind": "Peu importe",
"Not Found": "Non trouvé",
"Oh no": "Oh non",
"Page Expired": "Page expirée",
"Pagination Navigation": "Pagination",
"Please click the button below to verify your email address.": "Veuillez cliquer sur le bouton ci-dessous pour vérifier votre adresse email :",
"Please confirm your password before continuing.": "Veuillez confirmer votre mot de passe avant de continuer",
"Please enter your password to confirm you would like to logout of your other browser sessions across all of your devices.": "Veuillez entrer votre mot de passe pour confirmer que vous voulez déconnecter toutes les autres sessions navigateur sur l'ensemble de vos appareils.",
"Please provide the email address of the person you would like to add to this team. The email address must be associated with an existing account.": "Veuillez renseigner l'adresse email de la personne que vous voulez ajouter à cette équipe. L'adresse email doit être associée à un compte existant.",
"Regards": "Cordialement",
"results": "résultats",
"Server Error": "Erreur serveur",
"Service Unavailable": "Service indisponible",
"Showing": "Montrant",
"The :attribute must contain at least one letter.": "Le champ :attribute doit avoir au moins une lettre.",
"The :attribute must contain at least one number.": "Le champ :attribute doit avoir au moins un numéro.",
"The :attribute must contain at least one symbol.": "Le champ :attribute doit avoir au moins un symbole.",
"The :attribute must contain at least one uppercase and one lowercase letter.": "Le champ :attribute doit avoir au moins une lettre majuscule et une lettre minuscule.",
"The given :attribute has appeared in a data leak. Please choose a different :attribute.": "La valeur du champ :attribute est apparue dans une fuite de données. Veuillez choisir une valeur différente.",
"This action is unauthorized.": "Cette action n'est pas autorisée.",
"This password reset link will expire in :count minutes.": "Ce lien de réinitialisation du mot de passe expirera dans :count minutes.",
"to": "à",
"Toggle navigation": "Basculer la navigation",
"Too Many Attempts.": "Trop de tentatives.",
"Too Many Requests": "Trop de requêtes",
"Unauthorized": "Non autorisé",
"Verify Email Address": "Vérification de l'adresse email",
"Verify Your Email Address": "Vérifiez votre adresse email",
"We won't ask for your password again for a few hours.": "Nous ne vous demanderons plus votre mot de passe pour quelques heures",
"You are logged in!": "Vous êtes connecté !",
"Your email address is not verified.": "Votre adresse email n'a pas été vérifiée."
}

View file

@ -0,0 +1,17 @@
<?php
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
return [
'next' => 'Suivant &raquo;',
'previous' => '&laquo; Précédent',
];

View file

@ -0,0 +1,20 @@
<?php
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
return [
'reset' => 'Votre mot de passe a été réinitialisé !',
'sent' => 'Nous vous avons envoyé par email le lien de réinitialisation du mot de passe !',
'throttled' => 'Veuillez patienter avant de réessayer.',
'token' => 'Ce jeton de réinitialisation du mot de passe n\'est pas valide.',
'user' => 'Aucun utilisateur n\'a été trouvé avec cette adresse email.',
];

View file

@ -0,0 +1,131 @@
<?php
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
return [
'accepted' => 'Ce champ doit être accepté.',
'active_url' => 'Ce n\'est pas une URL valide',
'after' => 'La date doit être postérieure au :date.',
'after_or_equal' => 'La date doit être postérieure ou égale au :date.',
'alpha' => 'Ce champ doit contenir uniquement des lettres',
'alpha_dash' => 'Ce champ doit contenir uniquement des lettres, des chiffres et des tirets.',
'alpha_num' => 'Ce champ doit contenir uniquement des chiffres et des lettres.',
'array' => 'Ce champ doit être un tableau.',
'attached' => 'Ce champ est déjà attaché.',
'before' => 'Ce champ doit être une date antérieure au :date.',
'before_or_equal' => 'Ce champ doit être une date antérieure ou égale au :date.',
'between' => [
'array' => 'Le tableau doit contenir entre :min et :max éléments.',
'file' => 'La taille du fichier doit être comprise entre :min et :max kilo-octets.',
'numeric' => 'La valeur doit être comprise entre :min et :max.',
'string' => 'Le texte doit contenir entre :min et :max caractères.',
],
'boolean' => 'Ce champ doit être vrai ou faux.',
'confirmed' => 'Le champ de confirmation ne correspond pas.',
'date' => 'Ce n\'est pas une date valide.',
'date_equals' => 'La date doit être égale à :date.',
'date_format' => 'Ce champ ne correspond pas au format :format.',
'different' => 'Cette valeur doit être différente de :other.',
'digits' => 'Ce champ doit contenir :digits chiffres.',
'digits_between' => 'Ce champ doit contenir entre :min et :max chiffres.',
'dimensions' => 'La taille de l\'image n\'est pas conforme.',
'distinct' => 'Ce champ a une valeur en double.',
'email' => 'Ce champ doit être une adresse email valide.',
'ends_with' => 'Ce champ doit se terminer par une des valeurs suivantes : :values',
'exists' => 'Ce champ sélectionné est invalide.',
'file' => 'Ce champ doit être un fichier.',
'filled' => 'Ce champ doit avoir une valeur.',
'gt' => [
'array' => 'Le tableau doit contenir plus de :value éléments.',
'file' => 'La taille du fichier doit être supérieure à :value kilo-octets.',
'numeric' => 'La valeur doit être supérieure à :value.',
'string' => 'Le texte doit contenir plus de :value caractères.',
],
'gte' => [
'array' => 'Le tableau doit contenir au moins :value éléments.',
'file' => 'La taille du fichier doit être supérieure ou égale à :value kilo-octets.',
'numeric' => 'La valeur doit être supérieure ou égale à :value.',
'string' => 'Le texte doit contenir au moins :value caractères.',
],
'image' => 'Ce champ doit être une image.',
'in' => 'Ce champ est invalide.',
'in_array' => 'Ce champ n\'existe pas dans :other.',
'integer' => 'Ce champ doit être un entier.',
'ip' => 'Ce champ doit être une adresse IP valide.',
'ipv4' => 'Ce champ doit être une adresse IPv4 valide.',
'ipv6' => 'Ce champ doit être une adresse IPv6 valide.',
'json' => 'Ce champ doit être un document JSON valide.',
'lt' => [
'array' => 'Le tableau doit contenir moins de :value éléments.',
'file' => 'La taille du fichier doit être inférieure à :value kilo-octets.',
'numeric' => 'La valeur doit être inférieure à :value.',
'string' => 'Le texte doit contenir moins de :value caractères.',
],
'lte' => [
'array' => 'Le tableau doit contenir au plus :value éléments.',
'file' => 'La taille du fichier doit être inférieure ou égale à :value kilo-octets.',
'numeric' => 'La valeur doit être inférieure ou égale à :value.',
'string' => 'Le texte doit contenir au plus :value caractères.',
],
'max' => [
'array' => 'Le tableau ne peut contenir plus de :max éléments.',
'file' => 'La taille du fichier ne peut pas dépasser :max kilo-octets.',
'numeric' => 'La valeur ne peut être supérieure à :max.',
'string' => 'Le texte ne peut contenir plus de :max caractères.',
],
'mimes' => 'Le fichier doit être de type : :values.',
'mimetypes' => 'Le fichier doit être de type : :values.',
'min' => [
'array' => 'Le tableau doit contenir au moins :min éléments.',
'file' => 'La taille du fichier doit être supérieure à :min kilo-octets.',
'numeric' => 'La valeur doit être supérieure ou égale à :min.',
'string' => 'Le texte doit contenir au moins :min caractères.',
],
'multiple_of' => 'La valeur doit être un multiple de :value',
'not_in' => 'Le champ sélectionné n\'est pas valide.',
'not_regex' => 'Le format du champ n\'est pas valide.',
'numeric' => 'Ce champ doit contenir un nombre.',
'password' => 'Le mot de passe est incorrect',
'present' => 'Ce champ doit être présent.',
'prohibited' => 'Ce champ est interdit',
'prohibited_if' => 'Ce champ est interdit quand :other a la valeur :value.',
'prohibited_unless' => 'Ce champ est interdit à moins que :other ait l\'une des valeurs :values.',
'regex' => 'Le format du champ est invalide.',
'relatable' => 'Ce champ n\'est sans doute pas associé avec cette donnée.',
'required' => 'Ce champ est obligatoire.',
'required_if' => 'Ce champ est obligatoire quand la valeur de :other est :value.',
'required_unless' => 'Ce champ est obligatoire sauf si :other est :values.',
'required_with' => 'Ce champ est obligatoire quand :values est présent.',
'required_with_all' => 'Ce champ est obligatoire quand :values sont présents.',
'required_without' => 'Ce champ est obligatoire quand :values n\'est pas présent.',
'required_without_all' => 'Ce champ est requis quand aucun de :values n\'est présent.',
'same' => 'Ce champ doit être identique à :other.',
'size' => [
'array' => 'Le tableau doit contenir :size éléments.',
'file' => 'La taille du fichier doit être de :size kilo-octets.',
'numeric' => 'La valeur doit être :size.',
'string' => 'Le texte doit contenir :size caractères.',
],
'starts_with' => 'Ce champ doit commencer avec une des valeurs suivantes : :values',
'string' => 'Ce champ doit être une chaîne de caractères.',
'timezone' => 'Ce champ doit être un fuseau horaire valide.',
'unique' => 'La valeur est déjà utilisée.',
'uploaded' => 'Le fichier n\'a pu être téléversé.',
'url' => 'Le format de l\'URL n\'est pas valide.',
'uuid' => 'Ce champ doit être un UUID valide',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];

View file

@ -0,0 +1,162 @@
<?php
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
return [
'accepted' => 'Le champ :attribute doit être accepté.',
'active_url' => 'Le champ :attribute n\'est pas une URL valide.',
'after' => 'Le champ :attribute doit être une date postérieure au :date.',
'after_or_equal' => 'Le champ :attribute doit être une date postérieure ou égale au :date.',
'alpha' => 'Le champ :attribute doit contenir uniquement des lettres.',
'alpha_dash' => 'Le champ :attribute doit contenir uniquement des lettres, des chiffres et des tirets.',
'alpha_num' => 'Le champ :attribute doit contenir uniquement des chiffres et des lettres.',
'array' => 'Le champ :attribute doit être un tableau.',
'attached' => ':attribute est déjà attaché(e).',
'before' => 'Le champ :attribute doit être une date antérieure au :date.',
'before_or_equal' => 'Le champ :attribute doit être une date antérieure ou égale au :date.',
'between' => [
'array' => 'Le tableau :attribute doit contenir entre :min et :max éléments.',
'file' => 'La taille du fichier de :attribute doit être comprise entre :min et :max kilo-octets.',
'numeric' => 'La valeur de :attribute doit être comprise entre :min et :max.',
'string' => 'Le texte :attribute doit contenir entre :min et :max caractères.',
],
'boolean' => 'Le champ :attribute doit être vrai ou faux.',
'confirmed' => 'Le champ de confirmation :attribute ne correspond pas.',
'date' => 'Le champ :attribute n\'est pas une date valide.',
'date_equals' => 'Le champ :attribute doit être une date égale à :date.',
'date_format' => 'Le champ :attribute ne correspond pas au format :format.',
'different' => 'Les champs :attribute et :other doivent être différents.',
'digits' => 'Le champ :attribute doit contenir :digits chiffres.',
'digits_between' => 'Le champ :attribute doit contenir entre :min et :max chiffres.',
'dimensions' => 'La taille de l\'image :attribute n\'est pas conforme.',
'distinct' => 'Le champ :attribute a une valeur en double.',
'email' => 'Le champ :attribute doit être une adresse email valide.',
'ends_with' => 'Le champ :attribute doit se terminer par une des valeurs suivantes : :values',
'exists' => 'Le champ :attribute sélectionné est invalide.',
'file' => 'Le champ :attribute doit être un fichier.',
'filled' => 'Le champ :attribute doit avoir une valeur.',
'gt' => [
'array' => 'Le tableau :attribute doit contenir plus de :value éléments.',
'file' => 'La taille du fichier de :attribute doit être supérieure à :value kilo-octets.',
'numeric' => 'La valeur de :attribute doit être supérieure à :value.',
'string' => 'Le texte :attribute doit contenir plus de :value caractères.',
],
'gte' => [
'array' => 'Le tableau :attribute doit contenir au moins :value éléments.',
'file' => 'La taille du fichier de :attribute doit être supérieure ou égale à :value kilo-octets.',
'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :value.',
'string' => 'Le texte :attribute doit contenir au moins :value caractères.',
],
'image' => 'Le champ :attribute doit être une image.',
'in' => 'Le champ :attribute est invalide.',
'in_array' => 'Le champ :attribute n\'existe pas dans :other.',
'integer' => 'Le champ :attribute doit être un entier.',
'ip' => 'Le champ :attribute doit être une adresse IP valide.',
'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.',
'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.',
'json' => 'Le champ :attribute doit être un document JSON valide.',
'lt' => [
'array' => 'Le tableau :attribute doit contenir moins de :value éléments.',
'file' => 'La taille du fichier de :attribute doit être inférieure à :value kilo-octets.',
'numeric' => 'La valeur de :attribute doit être inférieure à :value.',
'string' => 'Le texte :attribute doit contenir moins de :value caractères.',
],
'lte' => [
'array' => 'Le tableau :attribute doit contenir au plus :value éléments.',
'file' => 'La taille du fichier de :attribute doit être inférieure ou égale à :value kilo-octets.',
'numeric' => 'La valeur de :attribute doit être inférieure ou égale à :value.',
'string' => 'Le texte :attribute doit contenir au plus :value caractères.',
],
'max' => [
'array' => 'Le tableau :attribute ne peut contenir plus de :max éléments.',
'file' => 'La taille du fichier de :attribute ne peut pas dépasser :max kilo-octets.',
'numeric' => 'La valeur de :attribute ne peut être supérieure à :max.',
'string' => 'Le texte de :attribute ne peut contenir plus de :max caractères.',
],
'mimes' => 'Le champ :attribute doit être un fichier de type : :values.',
'mimetypes' => 'Le champ :attribute doit être un fichier de type : :values.',
'min' => [
'array' => 'Le tableau :attribute doit contenir au moins :min éléments.',
'file' => 'La taille du fichier de :attribute doit être supérieure à :min kilo-octets.',
'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :min.',
'string' => 'Le texte :attribute doit contenir au moins :min caractères.',
],
'multiple_of' => 'La valeur de :attribute doit être un multiple de :value',
'not_in' => 'Le champ :attribute sélectionné n\'est pas valide.',
'not_regex' => 'Le format du champ :attribute n\'est pas valide.',
'numeric' => 'Le champ :attribute doit contenir un nombre.',
'password' => 'Le mot de passe est incorrect',
'present' => 'Le champ :attribute doit être présent.',
'prohibited' => 'Le champ :attribute est interdit.',
'prohibited_if' => 'Le champ :attribute est interdit quand :other a la valeur :value.',
'prohibited_unless' => 'Le champ :attribute est interdit à moins que :other est l\'une des valeurs :values.',
'regex' => 'Le format du champ :attribute est invalide.',
'relatable' => ':attribute n\'est sans doute pas associé(e) avec cette donnée.',
'required' => 'Le champ :attribute est obligatoire.',
'required_if' => 'Le champ :attribute est obligatoire quand la valeur de :other est :value.',
'required_unless' => 'Le champ :attribute est obligatoire sauf si :other est :values.',
'required_with' => 'Le champ :attribute est obligatoire quand :values est présent.',
'required_with_all' => 'Le champ :attribute est obligatoire quand :values sont présents.',
'required_without' => 'Le champ :attribute est obligatoire quand :values n\'est pas présent.',
'required_without_all' => 'Le champ :attribute est requis quand aucun de :values n\'est présent.',
'same' => 'Les champs :attribute et :other doivent être identiques.',
'size' => [
'array' => 'Le tableau :attribute doit contenir :size éléments.',
'file' => 'La taille du fichier de :attribute doit être de :size kilo-octets.',
'numeric' => 'La valeur de :attribute doit être :size.',
'string' => 'Le texte de :attribute doit contenir :size caractères.',
],
'starts_with' => 'Le champ :attribute doit commencer avec une des valeurs suivantes : :values',
'string' => 'Le champ :attribute doit être une chaîne de caractères.',
'timezone' => 'Le champ :attribute doit être un fuseau horaire valide.',
'unique' => 'La valeur du champ :attribute est déjà utilisée.',
'uploaded' => 'Le fichier du champ :attribute n\'a pu être téléversé.',
'url' => 'Le format de l\'URL de :attribute n\'est pas valide.',
'uuid' => 'Le champ :attribute doit être un UUID valide',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [
'address' => 'adresse',
'age' => 'âge',
'available' => 'disponible',
'city' => 'ville',
'content' => 'contenu',
'country' => 'pays',
'current_password' => 'mot de passe actuel',
'date' => 'date',
'day' => 'jour',
'description' => 'description',
'email' => 'adresse email',
'excerpt' => 'extrait',
'first_name' => 'prénom',
'gender' => 'genre',
'hour' => 'heure',
'last_name' => 'nom',
'minute' => 'minute',
'mobile' => 'portable',
'month' => 'mois',
'name' => 'nom',
'password' => 'mot de passe',
'password_confirmation' => 'confirmation du mot de passe',
'phone' => 'téléphone',
'second' => 'seconde',
'sex' => 'sexe',
'size' => 'taille',
'time' => 'heure',
'title' => 'titre',
'username' => 'nom d\'utilisateur',
'year' => 'année',
],
];

View file

@ -0,0 +1,2 @@
@import "./variables";
@import "~bootstrap";

View file

4
resources/scss/bo.scss Normal file
View file

@ -0,0 +1,4 @@
// LIBS
@import "~@fortawesome/fontawesome-svg-core/styles.css";
@import "./common.scss";

2
resources/scss/fo.scss Normal file
View file

@ -0,0 +1,2 @@
// LIBS
@import "./common";

5
resources/ts/bo.ts Normal file
View file

@ -0,0 +1,5 @@
// LIBS
import "./fontawesome";
import "./common";
import "./vue";
import "./confirm";

27
resources/ts/common.ts Normal file
View file

@ -0,0 +1,27 @@
import axios from "axios";
const bootstrap = require("bootstrap");
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
axios.defaults.headers.common = {
Accept: "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-TOKEN": document
.querySelector('meta[name="csrf-token"]')!
.getAttribute("content"),
};
window.axios = axios;
window.bootstrap = bootstrap;
// enable bs tooltips
var tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});

View file

View file

View file

@ -0,0 +1,5 @@
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

111
resources/ts/confirm.ts Normal file
View file

@ -0,0 +1,111 @@
import { SweetAlertResult } from "sweetalert2";
import confirm from "./modules/confirm";
window.addEventListener("DOMContentLoaded", () => {
let elements = document.getElementsByClassName("confirmDeleteJS");
for (const element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous vraiment supprimer cet élément ?",
el as HTMLFormElement,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "error" }
);
return false;
});
}
elements = document.getElementsByClassName("confirmDoneJS");
for (let element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous vraiment marquer cet élément comme traité ?",
el as HTMLFormElement,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "info" }
);
return false;
});
}
elements = document.getElementsByClassName("confirmSubmitJS");
for (let element of elements) {
element.addEventListener("submit", function (e) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLFormElement)) {
throw new Error(
"confirmSubmitJS can only be executed on an exising form"
);
}
confirm.methods.confirm(
"Confirmez",
"Voulez-vous enregistrer cet élément tel-quel ?",
el as HTMLFormElement,
function (response: SweetAlertResult<any>) {
(el as HTMLFormElement).submit();
},
{ icon: "warning" }
);
return false;
});
}
elements = document.getElementsByClassName("confirmJS");
for (let element of elements) {
element.addEventListener(
"click",
function (e) {
if (!element.getAttribute("confirmJS")) {
e.preventDefault();
const el = e.target;
if (!el || !(el instanceof HTMLElement)) {
throw new Error(
"confirmJS can only be executed on a html element"
);
}
(async () => {
const promise = new Promise(
(resolve: (value: boolean) => void, reject) => {
console.log(el);
confirm.methods.confirm(
"Confirmez",
"Êtes vous sure ?",
el,
function (response: SweetAlertResult<any>) {
resolve(response.isConfirmed);
},
{ icon: "warning" }
);
return false;
}
);
if (await promise) {
element.setAttribute("confirmJS", "true");
el.click();
element.removeAttribute("confirmJS");
}
})();
}
},
false
);
}
});

3
resources/ts/fo.ts Normal file
View file

@ -0,0 +1,3 @@
// LIBS
import "./common";
import "./vue";

View file

@ -0,0 +1,8 @@
import { dom, library, config, icon } from "@fortawesome/fontawesome-svg-core";
import { faArrowRightFromBracket } from "@fortawesome/free-solid-svg-icons";
import { faAccessibleIcon } from "@fortawesome/free-brands-svg-icons";
import { faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
// config.autoReplaceSvg = false;
library.add(faArrowRightFromBracket);
dom.watch();

51
resources/ts/index.d.ts vendored Normal file
View file

@ -0,0 +1,51 @@
export {};
import { AxiosStatic } from "axios";
// * Recursive List<string>
type StringList = Record<PropertyKey, string>;
interface NestedStringListInterface
extends Record<PropertyKey, NestedStringListInterface | StringList> {}
// * Recursive List<number>
type NumberList = Record<PropertyKey, number>;
interface NestedNumberListInterface
extends Record<PropertyKey, NestedNumberListInterface | NumberList> {}
// * Recursive List<Model>
type Model = Record<
string,
| string
| number
| object
| NestedStringList
| NestedNumberList
| NestedModelInterface
>;
interface NestedModelInterface extends Model {}
type NestedStringList = NestedStringListInterface | StringList | string;
type NestedNumberList = NestedNumberListInterface | NumberList | number;
type ModelList = Array<Model>;
declare global {
type CustomTag = LaravelModel;
type LaravelModel = Model;
type LaravelModelList = ModelList;
interface Window {
$displayMenu: (show: boolean) => void;
$toggleBasketMenu: () => void;
axios: AxiosStatic;
Swal: Function;
__SYSTEM: {
_asset: string;
_locale: string;
_routes: Record<string, string>;
_translations: {
json: NestedStringList;
php: NestedStringList;
};
};
}
}

View file

@ -0,0 +1,7 @@
export default {
methods: {
asset(path: string) {
return `${window.__SYSTEM._asset}${path}`;
},
},
};

View file

@ -0,0 +1,62 @@
// SweetAlert
import Swal, { SweetAlertResult } from "sweetalert2";
import { SweetAlertOptions } from "sweetalert2";
import * as trans from "./trans";
export default {
methods: {
confirm(
title: string,
text: string = "",
self: Object,
after: (response: SweetAlertResult<any>) => void = (
response: SweetAlertResult<any>
) => {},
options: SweetAlertOptions = {}
) {
if (!self) {
throw Error('self is needed to set "this" on callback');
}
if (!after) {
throw Error("callback is needed when using confirm");
}
let icon = options.icon ?? "warning";
Swal.fire({
title: title,
text: text,
icon: icon,
showCancelButton: true,
confirmButtonText:
options.confirmButtonText ??
trans.default.methods.__("Oui"),
cancelButtonText:
options.cancelButtonText ??
trans.default.methods.__("Annuler"),
reverseButtons: true,
// * Bootstrap Styling
customClass: {
confirmButton: "btn btn-lg btn-success ms-3 me-3",
cancelButton: "btn btn-lg btn-danger ms-3 me-3",
},
buttonsStyling: false,
}).then((result) => {
if (result.isConfirmed) {
if (after) {
after.apply(self, [result]);
}
} else if (
/* Read more about handling dismissals below */
result.dismiss === Swal.DismissReason.cancel
) {
Swal.fire(
trans.default.methods.__(
"L'opération n'as pas été effectuée"
)
);
}
});
return false;
},
},
};

View file

@ -0,0 +1,76 @@
const route = {
methods: {
route(
routeName: string,
parameters: Record<string, number | string> = {}
): string | null {
const routes = route.flattenProperties(window.__SYSTEM._routes);
const routeUrl =
Object.keys(routes).indexOf(routeName) !== -1
? routes[routeName]
: null;
if (!routeUrl) {
return routeUrl;
}
// copy parameters
const iParameters = Object.assign({}, parameters),
origin = window.location.origin,
hash = window.location.hash;
let pathname = routeUrl.replace(origin, ""),
search = window.location.search;
// Replace in path name.
for (const paramName in iParameters) {
const paramValue = parameters[paramName];
if (pathname.indexOf(paramName) !== -1) {
pathname = pathname.replace(
paramName,
paramValue as string
);
}
delete parameters[paramName];
}
for (let paramName in parameters) {
let paramValue = parameters[paramName];
if (search.indexOf(paramName + "=") >= 0) {
const prefix = search.substring(
0,
search.indexOf(paramName + "=")
);
let suffix = search.substring(
search.indexOf(paramName + "=")
);
suffix = suffix.substring(suffix.indexOf("=") + 1);
suffix =
suffix.indexOf("&") >= 0
? suffix.substring(suffix.indexOf("&"))
: "";
search = `${prefix}${paramName}=${paramValue}${suffix}`;
} else if (search.indexOf("?") < 0) {
search += `?${paramName}=${paramValue}`;
} else {
search += `&${paramName}=${paramValue}`;
}
}
return `${origin}${pathname}${search}${hash}`;
},
},
flattenProperties(
obj: Record<PropertyKey, any>,
parent: string | null = null,
res: Record<PropertyKey, any> = {}
): Record<PropertyKey, any> {
for (const key of Object.keys(obj)) {
const propName = parent ? parent + "." + key : key;
if (typeof obj[key] === "object") {
route.flattenProperties(obj[key], propName, res);
} else {
res[propName] = obj[key];
}
}
return res;
},
};
export default route;

View file

@ -0,0 +1,49 @@
export default {
data() {
return {
locale: window.__SYSTEM._locale,
};
},
methods: {
/**
* Translate the given key.
*/
__(key: string, replace: Record<PropertyKey, string> = {}): string {
let translation: string,
translationNotFound = true;
try {
translation = key
.split(".")
.reduce(
(t, i) => t[i as any] ?? null,
window.__SYSTEM._translations.php
) as string;
if (translation) {
translationNotFound = false;
}
} catch (e) {
translation = key;
}
if (translationNotFound) {
translation = (window.__SYSTEM._translations.json[key as any] ??
key) as string;
}
const vars = translation.match(/:[0-9A-Za-z_]+/g);
if (vars) {
for (const match of vars) {
try {
translation = translation.replace(
new RegExp(match, "g"),
replace[match.substring(1)]
);
} catch (e) {
console.error("String replace failed ", e);
}
}
}
return translation;
},
},
};

46
resources/ts/vue.ts Normal file
View file

@ -0,0 +1,46 @@
import { App, createApp } from "vue";
document.addEventListener("DOMContentLoaded", () => {
const setupApp = (app: App<Element>) => {
app.config.globalProperties.window = window;
app.config.globalProperties.document = document;
};
// Register components
const vues = require.context("./components", true, /\.vue$/i);
vues.keys().map((key) => {
let htmlKey = key.split("/").pop();
if (!htmlKey) {
throw new Error(`Cannot load ${key}`);
}
htmlKey = htmlKey
.split(".")[0]
.replace(/\.?([A-Z])/g, function (x, y) {
return "-" + y.toLowerCase();
})
.replace(/^-/, "")
.replace("-component", "");
let el = document.getElementById(htmlKey),
elGroup = document.getElementsByClassName(htmlKey);
// * Init using ID
if (el) {
const app = createApp(vues(key).default, {
json: el.dataset.json ?? {},
});
setupApp(app);
app.mount(el);
}
// * Init Using class
for (const grpEl of elGroup) {
// * Hack to prevent loop weirdness
setTimeout(() => {
const app = createApp(vues(key).default, {
json: (grpEl as HTMLElement).dataset.json ?? {},
});
setupApp(app);
app.mount(grpEl);
}, 1000);
}
});
});

View file

@ -0,0 +1,74 @@
@extends('layouts.bo')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Connexion') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('bo.login') }}">
@csrf
<div class="row mb-3">
<label for="email" class="col-md-4 col-form-label text-md-end">{{ __('Adresse Email') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 col-form-label text-md-end">{{ __('Mode de passe') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label user-select-none" for="remember">
{{ __('Se souvenir de moi') }}
</label>
</div>
</div>
</div>
<div class="row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
<span>{{ __('Connexion') }}</span>
<i class="fa-solid fa-arrow-right-from-bracket"></i>
</button>
@if (Route::has('bo.password.request'))
<a class="btn btn-link" href="{{ route('bo.password.request') }}">
{{ __('Mode de passe oublié ?') }}
</a>
@endif
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,10 @@
@hasSection('breadcrumb')
@if(Breadcrumbs::exists(app()->view->getSections()['breadcrumb']))
{{ call_user_func_array('Breadcrumbs::render', array_merge(
[app()->view->getSections()['breadcrumb']],
!empty($brParam) ? (is_array($brParam) ? $brParam : [$brParam]) : []
)) }}
@endif
@elseif(Breadcrumbs::exists(request()->route()->getName()))
{{ Breadcrumbs::render(request()->route()->getName()) }}
@endif

View file

@ -0,0 +1,18 @@
@if (isset($breadcrumbs))
@unless ($breadcrumbs->isEmpty())
<nav aria-label="breadcrumb" class="d-inline">
<ol class="breadcrumb d-inline-flex">
@foreach ($breadcrumbs as $breadcrumb)
@if (!is_null($breadcrumb->url) && !$loop->last)
<li class="breadcrumb-item"><a href="{{ $breadcrumb->url }}" rel="nofollow">{{ $breadcrumb->title }}</a></li>
@else
<li class="breadcrumb-item active" aria-current="page">{{ $breadcrumb->title }}</li>
@endif
@endforeach
</ol>
</nav>
@endunless
@endif

View file

@ -0,0 +1,15 @@
@extends('layouts.bo')
@section('title', __('Accueil'))
@section('description', __('Description site'))
@section('breadcrumb', 'no')
@section('content')
<div class="container">
<div class="row">
<div class="col">
<h1>{{ __('Panneau de bord') }}</h1>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,32 @@
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{ config('app.name') }} - @yield('title')</title>
@preload('mix-manifest.json')
<meta name="description" content="@yield('description')">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- META INDEX, FOLLOW -->
<meta name="robots" content="index, nofollow">
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('assets/images/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('assets/images/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('assets/images/favicon-16x16.png') }}">
<link rel="shortcut icon" type="image/x-icon" href="{{ asset('assets/images/favicon.ico') }}" />
<link rel="manifest" href="{{ asset('site.webmanifest') }}">
<link rel="mask-icon" href="{{ asset('assets/images/safari-pinned-tab.svg') }}" color="#091c58">
<link rel="shortcut icon" href="{{ asset('assets/images/favicon.ico') }}">
<meta name="msapplication-TileColor" content="#091c58">
<meta name="msapplication-config" content="{{ asset('browserconfig.xml') }}">
<meta name="theme-color" content="#091c58">
<!-- Fonts -->
<!-- STYLES -->
<link rel="stylesheet" href="{{ mix('css/bo.css') }}">
@stack('styles')

View file

@ -0,0 +1,9 @@
@auth('backend')
<ul class="navbar-nav me-auto">
{{-- <li class="nav-item">
<a class="nav-link @if(request()->routeIs('bo.news.*')) active @endif" href="{{ route('bo.news.index') }}" title="{{ __('Administrer les :model', [
'model' => Str::singular(__('models.classes.news'))
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.news') }}</a>
</li> --}}
</ul>
@endauth

View file

@ -0,0 +1,8 @@
@if($errors->has($inputName))
<div class="col-12">
<small class="text-danger">{{ implode(',', $errors->get($inputName)) }}</small>
</div>
@endif
@if(isset($helper))
<small @if(isset($helperId)) id="{{ $helperId }}" @endif class="form-text text-muted">{{ $helper }}</small>
@endif

View file

@ -0,0 +1,83 @@
{{-- MINI CRUD EDIT --}}
<div id="minicrud">
@foreach($model->data as $k => $input)
@if(!$loop->first)
<hr class="w-25">
@endif
@php
$input = json_decode(json_encode($input), true);
@endphp
@switch($input['inputType']['value'])
@case(InputType::url()->value)
@case(InputType::string()->value)
<div class="form-group">
<label for="{{ $k }}"><small>{{ $input['label'] }}*</small></label>
<input id="{{ $k }}" type="text" class="form-control" name="data[{{$k}}][value]" placeholder="{{ $input['label'] }}" value="{{ old("data.$k.value") ?? $input['value'] ?? '' }}" required>
</div>
@break
@case(InputType::number()->value)
<div class="form-group">
<label for="{{ $k }}"><small>{{ $input['label'] }}*</small></label>
<input id="{{ $k }}" type="number" step="0.01" class="form-control" name="data[{{$k}}][value]" placeholder="{{ $input['label'] }}" value="{{ old("data.$k.value") ?? $input['value'] ?? '' }}" required>
</div>
@break
@case(InputType::text()->value)
<div class="form-group vueComponent">
<label class="col-form-label">{{ $input['label'] }}*</label>
<custom-ckeditor name="data[{{$k}}][text]" dbtext="{{ old("data.$k.text") ?? $input['text'] ?? '' }}"></custom-ckeditor>
</div>
@break
@case(InputType::boolean()->value)
<div class="form-group">
<label class="user-select-none">
<input type="checkbox" name="data[{{$k}}][value]" value="1" @if (( old("data.$k.value") ?? $input['value'] ?? 0 ))checked @endif>
{{ $input['label'] }}*
</label>
</div>
@break
@case(InputType::image()->value)
<div class="form-group">
@if(isset($input['label']))
<h3>{{ $input['label'] }}</h3>
@endif
<span id="{{ $k }}imgLABEL">{{ __('Visuel * de type JPEG/PNG/SVG - 2Mo max.')}} - {{ isset($input['dimensions']) ? __('Dimensions :dimensions', ['dimensions' => $input['dimensions']]) : __('Dimensions Maximales :max', ['max' => $input['dimensionsMax']]) }} px</span>
<div class="input-group mb-3">
<div class="custom-file">
<input id="{{ $k }}img" type="file" class="custom-file-input" name="data[{{$k}}][value]" accept="image/png,image/jpeg,.svg" aria-describedby="{{ $k }}imgLABEL">
<label class="custom-file-label" for="{{ $k }}img">{{ __('Choisir un fichier') }}</label>
</div>
</div>
<br>
<label for="{{ $k }}alt"><small>{{ $input['label'] }} - {{ __('Texte alternatif') }}</small></label>
<input id="{{ $k }}alt" type="text" class="form-control" name="data[{{$k}}][alt]" placeholder="{{ __('Texte alternatif à l\'image') }}" value="{{ old("data.$k.alt") ?? $input['alt'] ?? '' }}">
<br>
@if(isset($input['value']) and strlen($input['value']))
<img src="{{ $input['value'] }}" alt="image" style="max-height:200px;max-width:100%;object-fit:contain;margin:0 auto;display:block;">
@endif
</div>
@break
@case(InputType::imageArray()->value)
@case(InputType::labelArray()->value)
@default
@php throw new Exception('Input type not handled'); @endphp
@endswitch
@endforeach
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener('DOMContentLoaded', function() {
let minicrud = document.getElementById('minicrud').closest('form');
if(minicrud) {
minicrud.addEventListener('submit', function(e) {
document.querySelectorAll('input[type="file"]').forEach(function(input) {
if (input.dataset['old-value'] && !input.value) {
input.setAttribute('type', 'text');
input.value = input.getAttribute('old-value');
}
});
});
}
});
</script>
@endpush

View file

@ -0,0 +1,20 @@
<script @if(isset($nonce)) nonce="{{ $nonce }}" @endif>
if (!window.__SYSTEM) {
window.__SYSTEM = {};
}
window.__SYSTEM._locale = '{{ app()->getLocale() }}';
window.__SYSTEM._translations = @json(cache(sprintf('translations.%s', app()->getLocale())) ?? []);
window.__SYSTEM._asset = '{{ asset('') }}';
if(!window.__SYSTEM._routes) {
window.__SYSTEM._routes = {}
}
window.__SYSTEM._routes['crud-policies'] = {
{{-- upload: '{{ route('crud-policies.upload') }}' --}}
};
window.__SYSTEM._routes.bo = {
{{-- tags: {
store: '{{ route('bo.tags.store') }}',
jsonStore: '{{ route('bo.tags.jsonStore') }}'
}, --}}
};
</script>

View file

@ -0,0 +1,46 @@
@php
$iArgs = [];
if(request()->sort_col) {
$iArgs['sort_col'] = request()->sort_col;
}
if(request()->search) {
$iArgs['search'] = request()->sort_way;
}
@endphp
<div class="row mb-3">
<div class="col-12">
<form id="searchForm" action="{{ request()->url() }}">
<div class="input-group">
<label class="input-group-text" for="searchField">{{ $searchFields }}</label>
@if(request()->sort_col)
<input class="d-none" type="text" name="sort_col" value="{{ request()->sort_col }}">
@endif
@if(request()->sort_way)
<input class="d-none" type="text" name="sort_way" value="{{ request()->sort_way }}">
@endif
@if(request()->rangefilter)
<input class="d-none" type="text" name="rangefilter" value="{{ request()->rangefilter }}">
@endif
@if(request()->group)
<input class="d-none" type="text" name="group" value="{{ request()->group }}">
@endif
<input type="search" id="searchField" name="search" class="form-control" value="{{ request()->search }}" placeholder="{{ __('Vos mots clés ici...') }}"/>
<button type="submit" class="btn btn-primary" data-bs-toggle="tooltip" title="{{ __('Lancer la recherche') }}">
<i class="fas fa-search"></i>
</button>
<button id="searchDelete" type="button" class="btn btn-danger" data-bs-toggle="tooltip" title="{{ __('Effacer la recherche') }}">
<i class="fas fa-backspace"></i>
</button>
@push('scripts')
<script nonce="{{ $nonce }}">
document.getElementById('searchDelete').addEventListener('click', function (e) {
document.getElementById('searchField').value = '';
document.getElementById('searchForm').submit();
});
</script>
@endpush
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,52 @@
@php
$route = request()->route();
$routeName = $route->getName();
$rst = !is_null(request()->rst);
$noOrder = Session::get("$routeName.sort_col") !== 'order' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
$noOrder = Session::get("$routeName.sort_col") !== '' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
View::share('noOrder', $noOrder);
$ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
@endphp
<tr>
@foreach($cols as $col => $colname)
@php
$iArgs = ['sort_col' => $col];
if(request()->search) {
$iArgs['search'] = request()->search;
}
@endphp
{{-- NO Sort for order column --}}
@if($col === 'order' or in_array($col, $ignore))
@if($noOrder and !$rst and !in_array($col, $ignore)) @continue @endif
<th scope="col">
<span class="text-nowrap">{{ $colname }}</span>
</th>
@continue
@endif
<th scope="col" @if(isset($mobileHide) and is_array($mobileHide) and in_array($col, $mobileHide)) class="d-none d-md-table-cell" @endif>
<span class="text-nowrap">
{{ $colname }}
@if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'desc' and session("$routeName.sort_col") === $col))
<a class="col_sort text-info lead asc" href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'asc', 'rst' => null])) }}" title="{{ __('Trier :name en ordre ascendant', [
'name' => Str::lower($colname)
]) }}" data-bs-toggle="tooltip">&nbsp;&darr;&nbsp;</a>
@endif
@if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'asc' and session("$routeName.sort_col") === $col))
<a class="col_sort text-info lead desc" href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'desc', 'rst' => null])) }}" title="{{ __('Trier :name en ordre descendant', [
'name' => Str::lower($colname)
]) }}" data-bs-toggle="tooltip">&nbsp;&uarr;&nbsp;</a>
@endif
</span>
</th>
@endforeach
{{-- Buttons Col --}}
<th scope="col">
@if($noOrder and !$rst)
<a class="btn btn-small btn-danger" href="{{ request()->fullUrlWithQuery(['sort_col' => null, 'sort_way' => null, 'rst' => true, 'search' => null]) }}"
data-bs-toggle="tooltip" title="{{ __('Effacer le tri') }}"><i class="fa fa-eraser"></i></a>
@else
@php $text = 'Ici, les flèches servent à trier et non pas à filtrer'; @endphp
<span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __($text) }}"><i class="fas fa-question-circle"></i></span>
@endif
</th>
</tr>

View file

@ -1,132 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Styles -->
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f7fafc;background-color:rgba(247,250,252,var(--bg-opacity))}.border-gray-200{--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity))}.border-t{border-top-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.text-center{text-align:center}.text-gray-200{--text-opacity:1;color:#edf2f7;color:rgba(237,242,247,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#e2e8f0;color:rgba(226,232,240,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#718096;color:rgba(113,128,150,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--bg-opacity:1;background-color:#2d3748;background-color:rgba(45,55,72,var(--bg-opacity))}.dark\:bg-gray-900{--bg-opacity:1;background-color:#1a202c;background-color:rgba(26,32,44,var(--bg-opacity))}.dark\:border-gray-700{--border-opacity:1;border-color:#4a5568;border-color:rgba(74,85,104,var(--border-opacity))}.dark\:text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.dark\:text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.dark\:text-gray-500{--tw-text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--tw-text-opacity))}}
</style>
<style>
body {
font-family: 'Nunito', sans-serif;
}
</style>
</head>
<body class="antialiased">
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0">
@if (Route::has('login'))
<div class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
@auth
<a href="{{ url('/home') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Home</a>
@else
<a href="{{ route('login') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Log in</a>
@if (Route::has('register'))
<a href="{{ route('register') }}" class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">Register</a>
@endif
@endauth
</div>
@endif
<div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-center pt-8 sm:justify-start sm:pt-0">
<svg viewBox="0 0 651 192" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-16 w-auto text-gray-700 sm:h-20">
<g clip-path="url(#clip0)" fill="#EF3B2D">
<path d="M248.032 44.676h-16.466v100.23h47.394v-14.748h-30.928V44.676zM337.091 87.202c-2.101-3.341-5.083-5.965-8.949-7.875-3.865-1.909-7.756-2.864-11.669-2.864-5.062 0-9.69.931-13.89 2.792-4.201 1.861-7.804 4.417-10.811 7.661-3.007 3.246-5.347 6.993-7.016 11.239-1.672 4.249-2.506 8.713-2.506 13.389 0 4.774.834 9.26 2.506 13.459 1.669 4.202 4.009 7.925 7.016 11.169 3.007 3.246 6.609 5.799 10.811 7.66 4.199 1.861 8.828 2.792 13.89 2.792 3.913 0 7.804-.955 11.669-2.863 3.866-1.908 6.849-4.533 8.949-7.875v9.021h15.607V78.182h-15.607v9.02zm-1.431 32.503c-.955 2.578-2.291 4.821-4.009 6.73-1.719 1.91-3.795 3.437-6.229 4.582-2.435 1.146-5.133 1.718-8.091 1.718-2.96 0-5.633-.572-8.019-1.718-2.387-1.146-4.438-2.672-6.156-4.582-1.719-1.909-3.032-4.152-3.938-6.73-.909-2.577-1.36-5.298-1.36-8.161 0-2.864.451-5.585 1.36-8.162.905-2.577 2.219-4.819 3.938-6.729 1.718-1.908 3.77-3.437 6.156-4.582 2.386-1.146 5.059-1.718 8.019-1.718 2.958 0 5.656.572 8.091 1.718 2.434 1.146 4.51 2.674 6.229 4.582 1.718 1.91 3.054 4.152 4.009 6.729.953 2.577 1.432 5.298 1.432 8.162-.001 2.863-.479 5.584-1.432 8.161zM463.954 87.202c-2.101-3.341-5.083-5.965-8.949-7.875-3.865-1.909-7.756-2.864-11.669-2.864-5.062 0-9.69.931-13.89 2.792-4.201 1.861-7.804 4.417-10.811 7.661-3.007 3.246-5.347 6.993-7.016 11.239-1.672 4.249-2.506 8.713-2.506 13.389 0 4.774.834 9.26 2.506 13.459 1.669 4.202 4.009 7.925 7.016 11.169 3.007 3.246 6.609 5.799 10.811 7.66 4.199 1.861 8.828 2.792 13.89 2.792 3.913 0 7.804-.955 11.669-2.863 3.866-1.908 6.849-4.533 8.949-7.875v9.021h15.607V78.182h-15.607v9.02zm-1.432 32.503c-.955 2.578-2.291 4.821-4.009 6.73-1.719 1.91-3.795 3.437-6.229 4.582-2.435 1.146-5.133 1.718-8.091 1.718-2.96 0-5.633-.572-8.019-1.718-2.387-1.146-4.438-2.672-6.156-4.582-1.719-1.909-3.032-4.152-3.938-6.73-.909-2.577-1.36-5.298-1.36-8.161 0-2.864.451-5.585 1.36-8.162.905-2.577 2.219-4.819 3.938-6.729 1.718-1.908 3.77-3.437 6.156-4.582 2.386-1.146 5.059-1.718 8.019-1.718 2.958 0 5.656.572 8.091 1.718 2.434 1.146 4.51 2.674 6.229 4.582 1.718 1.91 3.054 4.152 4.009 6.729.953 2.577 1.432 5.298 1.432 8.162 0 2.863-.479 5.584-1.432 8.161zM650.772 44.676h-15.606v100.23h15.606V44.676zM365.013 144.906h15.607V93.538h26.776V78.182h-42.383v66.724zM542.133 78.182l-19.616 51.096-19.616-51.096h-15.808l25.617 66.724h19.614l25.617-66.724h-15.808zM591.98 76.466c-19.112 0-34.239 15.706-34.239 35.079 0 21.416 14.641 35.079 36.239 35.079 12.088 0 19.806-4.622 29.234-14.688l-10.544-8.158c-.006.008-7.958 10.449-19.832 10.449-13.802 0-19.612-11.127-19.612-16.884h51.777c2.72-22.043-11.772-40.877-33.023-40.877zm-18.713 29.28c.12-1.284 1.917-16.884 18.589-16.884 16.671 0 18.697 15.598 18.813 16.884h-37.402zM184.068 43.892c-.024-.088-.073-.165-.104-.25-.058-.157-.108-.316-.191-.46-.056-.097-.137-.176-.203-.265-.087-.117-.161-.242-.265-.345-.085-.086-.194-.148-.29-.223-.109-.085-.206-.182-.327-.252l-.002-.001-.002-.002-35.648-20.524a2.971 2.971 0 00-2.964 0l-35.647 20.522-.002.002-.002.001c-.121.07-.219.167-.327.252-.096.075-.205.138-.29.223-.103.103-.178.228-.265.345-.066.089-.147.169-.203.265-.083.144-.133.304-.191.46-.031.085-.08.162-.104.25-.067.249-.103.51-.103.776v38.979l-29.706 17.103V24.493a3 3 0 00-.103-.776c-.024-.088-.073-.165-.104-.25-.058-.157-.108-.316-.191-.46-.056-.097-.137-.176-.203-.265-.087-.117-.161-.242-.265-.345-.085-.086-.194-.148-.29-.223-.109-.085-.206-.182-.327-.252l-.002-.001-.002-.002L40.098 1.396a2.971 2.971 0 00-2.964 0L1.487 21.919l-.002.002-.002.001c-.121.07-.219.167-.327.252-.096.075-.205.138-.29.223-.103.103-.178.228-.265.345-.066.089-.147.169-.203.265-.083.144-.133.304-.191.46-.031.085-.08.162-.104.25-.067.249-.103.51-.103.776v122.09c0 1.063.568 2.044 1.489 2.575l71.293 41.045c.156.089.324.143.49.202.078.028.15.074.23.095a2.98 2.98 0 001.524 0c.069-.018.132-.059.2-.083.176-.061.354-.119.519-.214l71.293-41.045a2.971 2.971 0 001.489-2.575v-38.979l34.158-19.666a2.971 2.971 0 001.489-2.575V44.666a3.075 3.075 0 00-.106-.774zM74.255 143.167l-29.648-16.779 31.136-17.926.001-.001 34.164-19.669 29.674 17.084-21.772 12.428-43.555 24.863zm68.329-76.259v33.841l-12.475-7.182-17.231-9.92V49.806l12.475 7.182 17.231 9.92zm2.97-39.335l29.693 17.095-29.693 17.095-29.693-17.095 29.693-17.095zM54.06 114.089l-12.475 7.182V46.733l17.231-9.92 12.475-7.182v74.537l-17.231 9.921zM38.614 7.398l29.693 17.095-29.693 17.095L8.921 24.493 38.614 7.398zM5.938 29.632l12.475 7.182 17.231 9.92v79.676l.001.005-.001.006c0 .114.032.221.045.333.017.146.021.294.059.434l.002.007c.032.117.094.222.14.334.051.124.088.255.156.371a.036.036 0 00.004.009c.061.105.149.191.222.288.081.105.149.22.244.314l.008.01c.084.083.19.142.284.215.106.083.202.178.32.247l.013.005.011.008 34.139 19.321v34.175L5.939 144.867V29.632h-.001zm136.646 115.235l-65.352 37.625V148.31l48.399-27.628 16.953-9.677v33.862zm35.646-61.22l-29.706 17.102V66.908l17.231-9.92 12.475-7.182v33.841z"/>
</g>
</svg>
</div>
<div class="mt-8 bg-white dark:bg-gray-800 overflow-hidden shadow sm:rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-2">
<div class="p-6">
<div class="flex items-center">
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
<div class="ml-4 text-lg leading-7 font-semibold"><a href="https://laravel.com/docs" class="underline text-gray-900 dark:text-white">Documentation BO</a></div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 dark:border-gray-700 md:border-t-0 md:border-l">
<div class="flex items-center">
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
<div class="ml-4 text-lg leading-7 font-semibold"><a href="https://laracasts.com" class="underline text-gray-900 dark:text-white">Laracasts</a></div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"></path></svg>
<div class="ml-4 text-lg leading-7 font-semibold"><a href="https://laravel-news.com/" class="underline text-gray-900 dark:text-white">Laravel News</a></div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 dark:border-gray-700 md:border-l">
<div class="flex items-center">
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div class="ml-4 text-lg leading-7 font-semibold text-gray-900 dark:text-white">Vibrant Ecosystem</div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="underline">Forge</a>, <a href="https://vapor.laravel.com" class="underline">Vapor</a>, <a href="https://nova.laravel.com" class="underline">Nova</a>, and <a href="https://envoyer.io" class="underline">Envoyer</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="underline">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="underline">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="underline">Echo</a>, <a href="https://laravel.com/docs/horizon" class="underline">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="underline">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="underline">Telescope</a>, and more.
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-center mt-4 sm:items-center sm:justify-between">
<div class="text-center text-sm text-gray-500 sm:text-left">
<div class="flex items-center">
<svg fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor" class="-mt-px w-5 h-5 text-gray-400">
<path d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
<a href="https://laravel.bigcartel.com" class="ml-1 underline">
Shop
</a>
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="ml-4 -mt-px w-5 h-5 text-gray-400">
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path>
</svg>
<a href="https://github.com/sponsors/taylorotwell" class="ml-1 underline">
Sponsor
</a>
</div>
</div>
<div class="ml-4 text-center text-sm text-gray-500 sm:text-right sm:ml-0">
Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }})
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,5 @@
@extends('layouts.error')
@section('errorCode', 'code401')
@section('title', __('Accès non authorisé'))
@section('message', __('Vous n\'êtes pas autorisé à accéder à cette page.'))

View file

@ -0,0 +1,5 @@
@extends('layouts.error')
@section('errorCode', 'code403')
@section('title', __('Refusé'))
@section('message', __($exception->getMessage() ?: 'Vous n\'êtes pas autorisé à accéder à cette page.'))

View file

@ -0,0 +1,5 @@
@extends('layouts.error')
@section('errorCode', 'code404')
@section('title', __('Vous êtes perdu ?'))
@section('message', __('Erreur 404, malheureusement cette page n\'existe pas ou n\'existe plus.'))

View file

@ -0,0 +1,5 @@
@extends('layouts.error')
@section('errorCode', 'code405')
@section('title', __('Vous êtes perdu ?'))
@section('message', __('Erreur 405, Vous n\'êtes pas autorisé à accéder à cette page.'))

View file

@ -0,0 +1,5 @@
@extends('layouts.error')
@section('errorCode', 'code419')
@section('title', __('La page à expiré'))
@section('message', __('La page à expiré, redirigez vous vers l\'accueil'))

View file

@ -0,0 +1,5 @@
@extends('layouts.error-minimal')
@section('errorCode', 'code429')
@section('title', __('Trop de requêtes'))
@section('message', __('Trop de requêtes, cessez immédiatement.'))

View file

@ -0,0 +1,5 @@
@extends('layouts.error-minimal')
@section('errorCode', 'code500')
@section('title', __('Oups...'))
@section('message', __('Une erreur est survenue. Merci de ressayer plus tard.'))

View file

@ -0,0 +1,5 @@
@extends('layouts.error-minimal')
@section('title', __('Service non disponible'))
@section('code', '503')
@section('message', __('Le site est en maintenance, merci de votre compréhension'))

View file

@ -0,0 +1,10 @@
@hasSection('breadcrumb')
@if(Breadcrumbs::exists(app()->view->getSections()['breadcrumb']))
{{ call_user_func_array('Breadcrumbs::render', array_merge(
[app()->view->getSections()['breadcrumb']],
!empty($brParam) ? (is_array($brParam) ? $brParam : [$brParam]) : []
)) }}
@endif
@elseif(Breadcrumbs::exists(request()->route()->getName()))
{{ Breadcrumbs::render(request()->route()->getName()) }}
@endif

View file

@ -0,0 +1,10 @@
@hasSection('breadcrumb')
@if(Breadcrumbs::exists(app()->view->getSections()['breadcrumb']))
{{ call_user_func_array('Breadcrumbs::view', array_merge(
['breadcrumbs::json-ld', app()->view->getSections()['breadcrumb']],
!empty($brParam) ? (is_array($brParam) ? $brParam : [$brParam]) : []
)) }}
@endif
@elseif(Breadcrumbs::exists(request()->route()->getName()))
{{ Breadcrumbs::view('breadcrumbs::json-ld', request()->route()->getName()) }}
@endif

View file

@ -0,0 +1,21 @@
@unless ($breadcrumbs->isEmpty())
<div class="breadcrumb">
<ul>
@foreach ($breadcrumbs as $breadcrumb)
@if (!is_null($breadcrumb->url) && !$loop->last)
<li class="breadcrumb-item"><a href="{{ $breadcrumb->url }}" rel="nofollow">{{ $breadcrumb->title }}</a></li>
@else
<li class="breadcrumb-item active">{{ $breadcrumb->title }}</li>
@endif
{{-- @if(count($breadcrumbs) > 1 and !$loop->last)
<span class="separator"> &gt; </span>
@endif --}}
@endforeach
</ul>
</div>
@endunless

View file

@ -0,0 +1,15 @@
@extends('layouts.fo')
@section('title', __('Accueil'))
@section('description', __('Description site'))
@section('breadcrumb', 'no')
@section('content')
<div class="container">
<div class="row">
<div class="col">
<h1>{{ __('SKEL LOUE') }}</h1>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,6 @@
<footer class="container-fluid">
<div class="row">
<div class="col">
</div>
</div>
</footer>

View file

@ -0,0 +1,64 @@
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>@yield('title') - {{ config('app.name') }}</title>
@preload('mix-manifest.json')
<meta name="description" content="@yield('description')">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
@hasSection('canonical')
<link rel="canonical" href="@yield('canonical')" />
@else
<link rel="canonical" href="{{ url()->current() }}" />
@endif
<!-- META INDEX, FOLLOW -->
@hasSection('metaIndex')
<meta name="robots" content="@yield('metaIndex')">
@else
<meta name="robots" content="index, follow">
@endif
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('assets/images/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('assets/images/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('assets/images/favicon-16x16.png') }}">
<link rel="shortcut icon" type="image/x-icon" href="{{ asset('assets/images/favicon.ico') }}" />
<link rel="manifest" href="{{ asset('site.webmanifest') }}">
<link rel="mask-icon" href="{{ asset('assets/images/safari-pinned-tab.svg') }}" color="#091c58">
<link rel="shortcut icon" href="{{ asset('assets/images/favicon.ico') }}">
<meta name="msapplication-TileColor" content="#091c58">
<meta name="msapplication-config" content="{{ asset('browserconfig.xml') }}">
<meta name="theme-color" content="#091c58">
<!-- Fonts -->
<!-- META FACEBOOK -->
{{-- <meta property="og:title" content="@yield('title')">
<meta property="og:description" content="@yield('description')">
<meta property="og:type" content="website">
@section('facebook_share_img')
<meta property="og:image" content="{{ asset('assets/images/socialshare/facebook-home.jpg') }}">
@show
<meta property="og:site_name" content="Le Gaulois">
<meta property="og:url" content="{{ url()->current() }}"> --}}
<!-- META TWITTER -->
{{-- <meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:site" content="Le Gaulois">
<meta name="twitter:title" content="@yield('title')">
<meta name="twitter:description" content="@yield('description')">
@section('twitter_share_img')
<meta property="og:image" content="{{ asset('assets/images/socialshare/twitter-home.jpg') }}">
@show
<meta name="twitter:url" content="{{ url()->current() }}"> --}}
<!-- STYLES -->
<link rel="stylesheet" href="{{ mix('css/fo.css') }}">
@stack('styles')

View file

@ -0,0 +1,5 @@
<header class="header text-white text-center d-inline-block align-middle w-100 pb-5">
<span class="siteTitle">
<i><a class="text-reset text-decoration-none" href="{{ route('home') }}">{{ config('app.name') }}</a></i>
</span>
</header>

View file

@ -0,0 +1,74 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@include('bo.layouts.head')
@if(config('app.env') === 'local')
<script nonce="{{ $nonce }}">window.vueDebug = true;</script>
@endif
</head>
<body>
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm fixed-top">
<div class="container">
<a class="navbar-brand rounded bg-loue-blue text-loue px-2 py-1" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- Left Side Of Navbar -->
@include('bo.layouts.nav-menu')
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ms-auto">
<!-- Authentication Links -->
@guest
@if (Route::has('bo.login'))
<li class="nav-item">
<a class="nav-link" href="{{ route('bo.login') }}">{{ __('Connexion') }}</a>
</li>
@endif
@if (Route::has('bo.register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('bo.register') }}">{{ __('Créer un compte') }}</a>
</li>
@endif
@else
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }}
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<form id="logout-form" action="{{ route('bo.logout') }}" method="POST" class="text-center">
@csrf
<button class="btn btn-link p-0 text-decoration-none text-muted text-capitalize" type="submit">{{ __('Déconnexion') }}</button>
</form>
</div>
</li>
@endguest
</ul>
</div>
</div>
</nav>
<main class="mt-5 container-fluid @yield('bg-color')">
<div class="row justify-content-center">
<div class="col col-md-6 mt-3">
@include('modules.flashMessage')
</div>
</div>
<div class="row p-md-3 justify-content-center">
<div class="col-12 col-md-10 col-lg-8 py-4">
@yield('content')
</div>
</div>
</main>
@stack('scriptsConstants')
@include('bo.modules.scriptConstants')
<script src="{{ mix('js/bo.js') }}"></script>
@stack('scripts')
</body>
</html>

View file

@ -0,0 +1,29 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@section('metaIndex', 'noindex, nofollow')
@include('fo.layouts.head')
</head>
<body>
@include('fo.layouts.header')
<div class="container-fluid p-0 m-0">
<div class="row p-0 m-0">
<div class="col-12 content container">
<div class="row mt-5">
<div class="col text-center">
<div class="content error @yield('errorCode')">
<h1>@yield('title')</h1>
<h4>@yield('message')</h4>
@if((app()->view->getSections()['code'] ?? '') !== '503')
<a href="{{ route('home') }}" class="btn btn-yellow btn-arronded-20 text-uppercase">
{{ __('Retour à la page d\'accueil') }}
</a>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@section('metaIndex', 'noindex, nofollow')
@include('fo.layouts.head')
</head>
<body>
@include('fo.layouts.header')
<div class="container-fluid p-0 m-0">
<div class="row p-0 m-0">
<div class="col-12 content container">
<div class="row mt-5">
<div class="col text-center">
<div class="content error @yield('errorCode')">
<h1>@yield('title')</h1>
<h4>@yield('message')</h4>
@if((app()->view->getSections()['code'] ?? '') !== '503')
<a href="{{ route('home') }}" class="btn btn-yellow btn-arronded-20 text-uppercase">
{{ __('Retour à la page d\'accueil') }}
</a>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,42 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@include('fo.layouts.head')
@include('fo.breadcrumb.breadcrumb-head')
@yield('microdata')
@if(config('app.env') === 'local')
<script nonce="{{ $nonce }}">window.vueDebug = true;</script>
@endif
</head>
<body>
@include('fo.layouts.header')
<div class="container-fluid p-0 m-0 overflow-x-hidden @yield('bg-color')">
<div class="row p-0 m-0">
<div class="col-12 col-sm-8 col-md-9 col-lg-10 m-0 pb-3 content container position-relative">
<div id="contentContainer" class="animate__animated">
<div class="row justify-content-center mt-3">
<div class="col">
@include('modules.flashMessage')
</div>
</div>
<div class="row mt-3">
<div class="col">
@include('fo.breadcrumb.breadcrumb-body')
</div>
</div>
<div class="row">
<div class="col">
@yield('content')
</div>
</div>
</div>
</div>
</div>
</div>
@include('fo.layouts.footer')
@stack('scriptsConstants')
@include('bo.modules.scriptConstants')
<script src="{{ mix('js/fo.js') }}"></script>
@stack('scripts')
</body>
</html>

View file

@ -0,0 +1,68 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="check-circle-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</symbol>
<symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</symbol>
<symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</symbol>
</svg>
@if ($message = Session::get('success'))
<div class="alert alert-success d-flex align-items-center alert-dismissible fade show" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Success:"><use xlink:href="#check-circle-fill"/></svg>
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if ($message = Session::get('error'))
<div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:"><use xlink:href="#exclamation-triangle-fill"/></svg>
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if ($message = Session::get('warning'))
<div class="alert alert-warning d-flex align-items-center alert-dismissible fade show" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Warning:"><use xlink:href="#exclamation-triangle-fill"/></svg>
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if ($message = Session::get('info'))
<div class="alert alert-info d-flex align-items-center alert-dismissible fade show" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if ($message = Session::get('message'))
<div class="alert alert-info d-flex align-items-center alert-dismissible fade show" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
<strong>{!! nl2br(e($message)) !!}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
@if ($errors->any())
<div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:"><use xlink:href="#exclamation-triangle-fill"/></svg>
@if ($errors->any())
<ul class="list-unstyled">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,60 @@
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" data-bs="tooltip" data-bs-placement="top"
title="{{ __('pagination.previous') }}" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
@else
<li class="page-item">
<a class="page-link" data-bs="tooltip" data-bs-placement="top"
title="{{ __('pagination.previous') }}" href="{{ $paginator->previousPageUrl() }}"
rel="prev" aria-label="@lang('pagination.previous')">&lsaquo;</a>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span
class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active" data-bs="tooltip" data-bs-placement="top"
title="{{ __('pagination.current_page') }}" aria-current="page">
<span class="page-link">{{ $page }}</span>
</li>
@else
<li class="page-item">
<a class="page-link" data-bs="tooltip" data-bs-placement="top"
title="{{ __('pagination.specific_page', ['id' => $page]) }}"
href="{{ $url }}">{{ $page }}</a>
</li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" data-bs="tooltip" data-bs-placement="top" title="{{ __('pagination.next') }}"
href="{{ $paginator->nextPageUrl() }}" rel="next"
aria-label="@lang('pagination.next')">&rsaquo;</a>
</li>
@else
<li class="page-item disabled" data-bs="tooltip" data-bs-placement="top"
title="{{ __('pagination.next') }}" aria-disabled="true" aria-label="@lang('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</nav>
@endif

View file

@ -0,0 +1,6 @@
<form action="{{ route('search') }}" class="m-2">
<div class="input-group justify-content-end">
<input class="input-group-text" type="text" name="s" value="{{ clean(request()->s) }}" placeholder="{{ __('Votre recherche') }}">
<button class="btn btn-outline-primary">{{ __('Chercher') }}</button>
</div>
</form>

View file

@ -1,20 +1,22 @@
<?php
use App\Http\Controllers\Bo\HomeController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::prefix('bo')->name('bo.')->middleware(['admin-secure'])->group(function () {
Route::namespace('\App\Http\Controllers\Bo')->group(function () {
Auth::routes([
'login' => true,
'logout' => true,
'reset' => false,
'register' => false,
'confirm' => false,
'verify' => false
]);
});
Route::prefix('bo')->name('bo')->middleware(['admin-secure'])->group(function () {
Route::get('/', function () {
return view('bo.welcome');
})->name('home');
Route::middleware('auth:backend')->group(function () {
Route::get('/', [HomeController::class, 'dashboard'])->name('dashboard');
});
});

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"es5", "es6", "dom", "dom.iterable"
],
"module": "esnext",
// "isolatedModules": false,
// "experimentalDecorators": true,
// "strictNullChecks": true,
// "removeComments": true,
// "allowSyntheticDefaultImports": true,
// this enables stricter inference for data properties on `this`
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"typeRoots": ["resources/ts/@types", "node_modules/@types"],
},
"include": [
"resources/**/*.js",
"resources/**/*.ts",
"resources/**/*.vue"
],
"compileOnSave": false
}

View file

@ -1,17 +1,97 @@
const mix = require('laravel-mix');
const path = require('path')
const mix = require('laravel-mix')
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel applications. By default, we are compiling the CSS
| file for the application as well as bundling up all the JS files.
|
*/
require('laravel-mix-polyfill')
require('laravel-mix-clean')
mix.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css', [
//
]);
const assetDir = 'assets'
const polyfill = {
enabled: true,
bugfixes: true,
useBuiltIns: 'usage',
targets: 'last 3 version, not dead, >0.3%'
}
mix.webpackConfig({
resolve: {
extensions: ["*", ".js", ".vue", ".ts"],
alias: {
'@': path.resolve('resources/assets')
}
},
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
esModule: true
}
},
],
},
// Pour debug compilation errors
// stats: {
// children: true,
// warningsFilter: [
// /\-\-underline\-color/,
// ]
// },
})
mix.options({
fileLoaderDirs: {
fonts: `${assetDir}/fonts`,
images: `${assetDir}/images`
},
postCss: [
require('postcss-discard-comments')({
removeAll: true
})
],
})
mix.clean({
cleanOnceBeforeBuildPatterns: [
'css/*',
'js/*',
'images/*',
'fonts/*',
'assets/*',
'webfonts/*'
]
})
// * BO
mix.ts('resources/ts/bo.ts', 'public/js/bo.js', { transpileOnly: true })
.sass('resources/scss/bo.scss', 'public/css/bo.css')
.vue({
runtimeOnly: true,
extractStyles: true,
globalStyles: false
})
.polyfill(polyfill)
.sourceMaps(false)
.version()
// * FO
mix.ts('resources/ts/fo.ts', 'public/js/fo.js', { transpileOnly: true })
.sass('resources/scss/fo.scss', 'public/css/fo.css')
.vue({
runtimeOnly: true,
extractStyles: true,
globalStyles: false
})
.polyfill(polyfill)
.sourceMaps(false)
.version()
// * Copy Blade Images Assets
mix.copyDirectory('resources/assets', 'public/assets')