This commit is contained in:
JonatanRek 2024-07-30 18:13:21 +02:00
parent f04bf74026
commit 571df9a0a4
119 changed files with 6411 additions and 143 deletions

View File

@ -0,0 +1,37 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
protected function context(): array
{
return array_merge(parent::context(), [
'current_url' => request()->url(),
]);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Helpers;
class AbstractHelper
{
public static function classes_in_namespace($namespace)
{
$namespace .= '\\';
$myClasses = array_filter(get_declared_classes(), function ($item) use ($namespace) {
return substr($item, 0, strlen($namespace)) === $namespace;
});
$theClasses = [];
foreach ($myClasses as $class) {
$theParts = explode('\\', $class);
$theClasses[] = end($theParts);
}
return $theClasses;
}
public static function getClassNames($path)
{
$out = [];
$results = scandir($path);
foreach ($results as $result) {
if ($result === '.' or $result === '..') continue;
$filename = $path . '/' . $result;
if (is_dir($filename)) {
$out = array_merge($out, self::getClassNames($filename));
} else {
$classFilePath = explode('/',$filename);
$out[] = substr($classFilePath[count($classFilePath)-1], 0, -4);
}
}
return $out;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Auth\UpdateUserRequest;
use App\Http\Requests\Auth\CreateApiTokenRequest;
use App\Http\Requests\Auth\RemoveApiTokenRequest;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Hash;
class ProfileController extends BaseController
{
public function index(Request $request)
{
return view('auth.profile', [
'user' => $request->user(),
'sessions' => [],
]);
}
public function update(UpdateUserRequest $request): RedirectResponse
{
$validated = $request->validated();
if (!Hash::check($validated['password'], auth()->user()->password))
{
return redirect()->route('profile.index')->with('error', __('boilerplate::ui.incorect.old.password'));
}
if (!empty($validated['newPassword']) && !empty($validated['password'])) {
$validated['password'] = Hash::make($validated['newPassword']);
unset($validated['newPassword']);
} else {
unset($validated['newPassword']);
unset($validated['password']);
}
$request->user()->update($validated);
return redirect()->route('profile.index')->with('success', __('boilerplate::ui.updated'));
}
public function api(Request $request)
{
return view('auth.profile_api', [
'user' => $request->user(),
'tokens' => $request->user()->tokens->all(),
]);
}
public function createApiToken(CreateApiTokenRequest $request): RedirectResponse
{
$validated = $request->validated();
if (empty($validated['expire_at'])) {
$validated['expire_at'] = null;
}
$newToken = $request->user()->createToken($validated['token_name'], ['*'], Carbon::parse($validated['expire_at']))->plainTextToken;
return redirect()->route('profile.api')->with([
'success'=> __('boilerplate::ui.created'),
'secret'=> $newToken,
]);
}
public function removeApiToken(RemoveApiTokenRequest $request): RedirectResponse
{
$validated = $request->validated();
$request->user()->tokens()->where('id', $validated['token_id'])->first()->delete();
return redirect()->route('profile.api')->with('success', __('boilerplate::ui.removed'));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
class BaseController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
}

View File

@ -2,7 +2,11 @@
namespace App\Http\Controllers;
abstract class Controller
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
//
use AuthorizesRequests, ValidatesRequests;
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers;
class HomeController extends BaseController
{
public function index()
{
return view('home');
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers;
class HostController extends BaseController
{
public function index()
{
return view('hosts.index');
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers;
class MaintenanceController extends BaseController
{
public function index()
{
return view('maintenance.index');
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\System;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
class ApiController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$routes = [];
$routesCollection = Route::getRoutes();
foreach ($routesCollection as $route) {
if (!str_starts_with($route->uri(), "api"))
continue;
if (!str_starts_with($route->getActionName(), "@"))
continue;
[$class, $method] = explode("@", $route->getActionName());
$reflectionClass = new ReflectionClass($class);
$reflectionMethod = $reflectionClass->getMethod($method);
$routes[] = [
"Method" => $route->methods()[0],
"Uri" => $route->uri(),
"Description" => $this->phpDocsDescription($reflectionMethod),
"Parameters" => $this->phpDocsParameters($reflectionMethod),
"Returns" => $reflectionMethod->getReturnType() ? $reflectionMethod->getReturnType()->getName() : "NULL",
];
}
return view('system.api.index', [
'routes' => $routes,
]);
}
private function phpDocsParameters(ReflectionMethod $method): array
{
// Retrieve the full PhpDoc comment block
$doc = $method->getDocComment();
// Trim each line from space and star chars
$lines = array_map(function ($line) {
return trim($line, " *");
}, explode("\n", $doc));
// Retain lines that start with an @
$lines = array_filter($lines, function ($line) {
return strpos($line, "@param") === 0;
});
$args = [];
// Push each value in the corresponding @param array
foreach ($lines as $line) {
[$null, $type, $name, $comment] = explode(' ', $line, 4);
$args[] = [
'type' => $type,
'name' => $name,
'comment' => $comment
];
}
return $args;
}
private function phpDocsDescription(ReflectionMethod $method): string
{
$doc = $method->getDocComment();
$lines = [];
foreach (explode("\n", $doc) as $i =>$line) {
$trimedLine =trim(trim($line, " *"), "/");
if (str_starts_with($trimedLine, "@")) {
break;
}
$lines[$i] = $trimedLine;
}
return implode("\n", $lines);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\System;
use App\Http\Controllers\BaseController;
use App\Models\Activity;
use Illuminate\Support\Str;
class AuditController extends BaseController
{
public function index()
{
//TODO: Clean Up and pagination
$activities = Activity::with(["affected", "user"])->orderByDesc("created_at")->get();
$urls = [];
foreach ($activities as $activity) {
if (!Str::contains($activity->lang_text, "delete")) {
// if(!empty($activity->user)) {
// $urls[$activity->id] = route('admin.users.update', ['user' => $activity->user]);
// continue;
// }
$urls[$activity->id] = "";
}
}
return view('system.audit.index', [
'activities' => $activities,
'urls' => $urls,
]);
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\System;
use App\Http\Controllers\BaseController;
use App\Jobs\Backup;
use Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class BackupController extends BaseController
{
public function run()
{
Backup::dispatchSync();
return redirect()->back()->with('success', __('boilerplate::ui.backup-running'));
}
public function index()
{
$backups = [];
$path = storage_path('backups');
if (file_exists($path)) {
foreach (File::allFiles($path. "/") as $file) {
if (!Str::endsWith($file->getFilename(), ".zip")) {
continue;
}
$date = explode("_", str_replace(".zip", "", $file->getFilename()))[0];
if (empty($backups[$date]['fileSize'])) {
$backups[$date]['fileSize'] = $file->getSize();
} else {
$backups[$date]['fileSize'] += $file->getSize();
}
$backups[$date]['fileName'][] = $file->getFilename();
}
foreach ($backups as $key => $backup) {
$backups[$key]['fileSize'] = $this->humanFileSize($backup['fileSize']);
}
}
return view('system.backup.index', ['backups' => $backups]);
}
public function download($file_name = null)
{
if (!empty($file_name)) {
$path = storage_path('backups/' . $file_name);
if (!\File::exists($path)) {
abort(404);
}
return response()->download($path);
}
abort(404);
}
public function delete($backup_date)
{
foreach ([$backup_date . '_database.zip',$backup_date . '_storage.zip'] as $file) {
$path = storage_path('backups/' . $file);
if (!empty($path)) {
File::delete($path);
}
}
return redirect()->back()->with('success', __('boilerplate::ui.deleted'));
}
private function humanFileSize($bytes, $decimals = 2)
{
$sz = 'BKMGTP';
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor];
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\System;
use App\Http\Controllers\BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
class CacheController extends BaseController
{
public function index()
{
// Cache::put('test', 'test');
// Cache::remember('users', 200, function () {
// return DB::table('users')->get();
// });
$cache_driver = config('cache.default');
$cache_items = [];
$storage = Cache::getStore(); // will return instance of FileStore
if ($cache_driver == 'redis') {
$redisConnection = $storage->connection();
foreach ($redisConnection->command('keys', ['*']) as $full_key) {
$cache_items[] = str_replace($storage->getPrefix(), "", $full_key);
}
} elseif ($cache_driver == 'file') {
$cachePath = $storage->getDirectory();
$items = File::allFiles($cachePath);
foreach ($items as $file2) {
$cache_items[] = $file2->getFilename();
}
}
//TODO: ADD SUPPORT FOR MEM CASH AND DB
return view('system.cache.index', [
'cache_items' => $cache_items,
'cache_driver' => $cache_driver,
]);
}
public function clear()
{
Cache::flush();
return redirect()->route('system.cache.index')->with('success', __('boilerplate::ui.cache-cleared'));
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\System;
use App\Helpers\AbstractHelper;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
class JobsController extends BaseController
{
public function index()
{
$rules = AbstractHelper::getClassNames(app_path() . "/Jobs");
$job_actions = $rules;
$failed_jobs = DB::table('failed_jobs')->select(['id', 'uuid', 'queue', 'exception', 'failed_at'])->selectRaw('SUBSTRING(payload, 1, 150) AS payload')->get();
$jobs = DB::table('jobs')->select(['id', 'queue', 'available_at'])->selectRaw('SUBSTRING(payload, 1, 150) AS payload')->get();
return view('system.jobs.index', [
'failed_jobs' => $failed_jobs,
'jobs' => $jobs,
'job_actions' => $job_actions,
]);
}
public function clear()
{
DB::table('failed_jobs')->delete();
return redirect()->route('system.jobs.index')->with('success', __('boilerplate::ui.jobs-cleared'));
}
public function call($job)
{
$class = '\\App\\Jobs\\' . $job;
dispatch(new $class());
return redirect()->route('system.jobs.index')->with('success', __('Job přidán do fronty'));
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\System;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\File;
class LogController extends BaseController
{
public function index()
{
$items = [];
$path = storage_path('logs');
foreach (File::allFiles($path) as $file) {
$items[] = [
'fileName' => $file->getFilename(),
'humanReadableSize' => $this->getHumanReadableSize($file->getSize()),
];
}
$items = array_reverse($items);
$todayStats = [
'ERROR' => 0,
'WARNING' => 0,
'INFO' => 0,
];
$todayLog = storage_path('logs/laravel.log');
if (config('logging.default') == "daily") {
$today = date('Y-m-d');
$todayLog = storage_path('logs/laravel-' . $today . '.log');
}
if (File::exists($todayLog)) {
if (File::size($todayLog) > 1000 * 1000 * 1000 * 1000) {
$todayStats = [
'ERROR' => '??',
'WARNING' => '??',
'INFO' => '??',
];
} else {
$content = File::get($todayLog);
$todayStats['ERROR'] = substr_count($content, '.ERROR:');
$todayStats['WARNING'] = substr_count($content, '.WARNING:');
$todayStats['INFO'] = substr_count($content, '.INFO:');
}
}
return view('system.log.index', [
'items' => $items,
'todayStats' => $todayStats,
]);
}
public function detail($filename)
{
$path = storage_path('logs/' . $filename);
if (File::exists($path)) {
if (File::size($path) > 1000 * 1000 * 1000 * 1000) {
return response()->download($path);
}
return view('system.log.detail', [
'content' => File::get($path),
'filename' => $filename,
]);
} else {
abort(404);
}
}
public function download($filename){
$path = storage_path('logs/' . $filename);
if (File::exists($path)) {
return response()->download($path);
} else {
abort(404);
}
}
private function getHumanReadableSize($bytes)
{
if ($bytes > 0) {
$base = floor(log($bytes) / log(1024));
$units = array("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"); //units of measurement
return number_format(($bytes / pow(1024, floor($base))), 3) . " $units[$base]";
} else {
return "0 bytes";
}
}
public function clear()
{
$path = storage_path('logs');
$files = glob($path.'/lar*.log');
foreach ($files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
return redirect()->route('system.log.index')->with('success', __('boilerplate::ui.jobs-cleared'));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers\System;
use App\Http\Controllers\BaseController;
class SubscriptionController extends BaseController
{
public function index()
{
return view('system.subscription.index');
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers\System;
use App\Http\Controllers\BaseController;
class UserController extends BaseController
{
public function index()
{
return view('system.user.index');
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
'theme',
];
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use SteelAnts\LaravelBoilerplate\Facades\Menu;
use Symfony\Component\HttpFoundation\Response;
class GenerateMenus
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->route()->getName() === 'livewire.message') {
return $next($request);
}
if (!auth()->check()) {
return $next($request);
}
Menu::make('main-menu', function ($menu) {
$systemRoutes = [
'boilerplate::ui.home' => [' fas fa-home', 'home'],
'boilerplate::ui.host' => [' fas fa-server', 'host'],
'boilerplate::ui.maintenance' => [' fas fa-calendar', 'maintenance'],
];
foreach ($systemRoutes as $title => $route_data) {
$icon = $route_data[0];
$route = $route_data[1];
$menu = $menu->add($title, [
'id' => strtolower($title),
'icon' => $icon,
'route' => $route,
]);
}
});
//CHECK IF USER IS SYSTEM ADMIN
Menu::make('system-menu', function ($menu) {
$systemRoutes = [
'boilerplate::ui.audit' => ['fas fa-eye', 'system.audit.index'],
'boilerplate::ui.api' => ['fas fa-file-archive', 'system.api.index'],
'boilerplate::ui.user' => ['fas fa-users', 'system.user.index'],
'boilerplate::subscriptions.title' => ['fas fa-dollar-sign', 'system.subscription.index'],
'boilerplate::ui.log' => ['fas fa-bug', 'system.log.index'],
'boilerplate::ui.jobs' => ['fas fa-business-time', 'system.jobs.index'],
'boilerplate::ui.cache' => ['fas fa-box', 'system.cache.index'],
'boilerplate::ui.backup' => ['fas fa-file-archive', 'system.backup.index']
];
foreach ($systemRoutes as $title => $route_data) {
$icon = $route_data[0];
$route = $route_data[1];
$menu = $menu->add($title, [
'id' => strtolower($title),
'icon' => $icon,
'route' => $route,
]);
}
});
return $next($request);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class CreateApiTokenRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'token_name' => ['required', 'string'],
'expire_at' => ['nullable', 'date', 'after_or_equal:' . date('Y-m-d')],
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class RemoveApiTokenRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'token_id' => ['required', 'exists:personal_access_tokens,id'],
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'email' => ['sometimes', 'string', 'email', 'max:255', 'unique:users'],
'newPassword' => ['sometimes', 'nullable', 'string', 'min:8', 'confirmed'],
];
}
}

137
app/Jobs/Backup.php Normal file
View File

@ -0,0 +1,137 @@
<?php
namespace App\Jobs;
use Directory;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class Backup implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct()
{
}
public function handle()
{
//PREPARATION
ini_set('date.timezone', 'Europe/Prague');
$days = 3;
$dbName = config('database.connections.mysql.database');
$dbUserName = config('database.connections.mysql.username');
$dbPassword = config('database.connections.mysql.password');
$fs_backup_path = storage_path('backups/tmp/storage');
$db_backup_path = storage_path('backups/tmp/db');
//TODO: verifi all folders exists
foreach ([$db_backup_path, $fs_backup_path] as $backupPath) {
if (!File::exists($backupPath)) {
File::makeDirectory($backupPath, 0755, true);
} else {
$command = "rm -r -f " . $backupPath . "/*";
exec($command, $output);
Log::Info('Clean Old Temp ' . $backupPath);
Log::Debug($output);
}
}
//REMOVE OLD BACKUPS
$command = "rm -f " . storage_path('app/backups') . "/" . date("Y-m-d", strtotime('-' . $days . ' days')) . ".zip";
exec($command, $output);
Log::info('Clean Old backups ' . $days . ' old');
///DATABASE
if (config('database.default') == 'sqlite') {
$dbFile = database_path('database.sqlite');
$backupFile = $db_backup_path . '/' . $dbName . '_' . date("Y-m-d", time()) . '.sqlite';
$command = "cp $dbFile $backupFile 2>&1";
exec($command, $output);
Log::info('Backup ' . $dbName . ' db ');
Log::Debug($output);
} else {
foreach (['data', 'scheme'] as $type) {
$parameters = "--no-data";
if ($type == "data") {
$parameters = "--no-create-info";
}
$backupFile = $db_backup_path . '/' . $dbName . '_' . $type . '_' . date("Y-m-d", time()) . '.sql';
$command = "mysqldump --skip-comments " . $parameters . " -h localhost -u " . $dbUserName . " -p" . $dbPassword . " " . $dbName . " -r $backupFile 2>&1";
exec($command, $output);
Log::info('Backup ' . $dbName . ' db ' . $type);
Log::Debug($output);
}
}
//STORAGE
$command = "cp -R " . storage_path('app') . " " . storage_path('backups/tmp/storage');
exec($command, $output);
Log::info('storage backup done');
Log::Debug($output);
//Backupo .env
$envBackupFile = storage_path("backups/tmp/storage/env.backup");
$envSourceFile = app()->environmentFilePath();
$command = "cp " . $envSourceFile . " " . $envBackupFile;
exec($command, $output);
Log::info('Backup .env');
//Clear previouse backups from same day
$command = "rm -f " . storage_path('backups') . "/" . date("Y-m-d", time()) . ".zip";
exec($command, $output);
Log::info('Clean previous backup');
foreach (['database' => $db_backup_path, 'storage' => $fs_backup_path] as $filename => $backupPath) {
$zippedFilePath = storage_path('backups/' . date("Y-m-d", time()) . '_' . $filename . ".zip");
if (File::exists($zippedFilePath)) {
$command = "rm -r -f " . $zippedFilePath;
exec($command, $output);
Log::Info('Clean Old Backup File' . $zippedFilePath);
Log::Debug($output);
}
$command = "cd ".$backupPath."; zip -rm ".$zippedFilePath." ./*" ;
exec($command, $output);
Log::info($backupPath . '=>' . $zippedFilePath);
$command = "md5sum ". $zippedFilePath;
exec($command, $output);
Log::info('Zipping hash');
$charSet = preg_replace(array('/\s{2,}/', '/[\t\n]/'), ' ', $output[count($output)-1]);
$charSet = rtrim($charSet);
$fileMD5Hash = explode(" ", $charSet)[0];
Log::debug($fileMD5Hash);
Log::info($backupPath . '=>'.$zippedFilePath.'=>' .$fileMD5Hash);
}
if (!empty(env('APP_ADMIN'))) {
Mail::raw(__('Backup Run successfully'), function ($message) {
$message->to('vasek@steelants.cz')->subject(_('Backup Run successfully ') . env('APP_NAME'));
});
Log::info('Sending Notification');
}
}
private function execShellCommand($command, &$output)
{
$output = $null;
exec($command, $output);
Log::debug($output);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Livewire\Audit;
use App\Models\Activity;
use SteelAnts\DataTable\Livewire\DataTableComponent;
use Illuminate\Database\Eloquent\Builder;
class DataTable extends DataTableComponent
{
public bool $paginated = true;
public int $itemsPerPage = 100;
public function query(): Builder
{
return Activity::with(["affected", "user"])->orderByDesc("created_at");
}
public function row($row): array
{
$affectedJson = json_encode([
'id'=> $row->affected->id ?? '',
'name'=> ($row->affected->title ??($row->affected->name ?? ($row->affected->description ?? ''))),
], JSON_UNESCAPED_UNICODE);
return [
'created_at' => $row->created_at,
'ip_address' => $row->ip,
'note' => $row->lang_text ,
'user_id' => ($row->user->name ?? 'Unknown'),
'affected_id' => $affectedJson,
];
}
public function headers(): array
{
return [
'created_at' => "Created",
'ip_address' => "IP Address",
'note' => "Note",
'user_id' => "Author",
'affected_id' => "Model"
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Livewire\Host;
use App\Models\Host;
use SteelAnts\DataTable\Livewire\DataTableComponent;
use Illuminate\Database\Eloquent\Builder;
class DataTable extends DataTableComponent
{
public $listeners = [
'hostAdded' => '$refresh',
'closeModal' => '$refresh',
];
public function query(): Builder
{
return Host::query();
}
public function headers(): array
{
return [
'hostname' => 'hostname',
];
}
public function remove($host_id){
Host::find($host_id)->delete();
}
public function actions($item)
{
return [
[
'type' => "livewire",
'action' => "edit",
'text' => "edit",
'parameters' => $item['id']
],
[
'type' => "livewire",
'action' => "remove",
'text' => "remove",
'parameters' => $item['id']
]
];
}
public function edit($host_id)
{
$this->dispatch('openModal', 'host.form', __('boilerplate::host.edit'), ['model' => $host_id]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Livewire\Host;
use Livewire\Component;
use App\Models\Host;
class Form extends Component
{
public $model;
public string $hostname;
public $action = 'store';
protected function rules()
{
return [
'hostname' => 'required',
];
}
public function mount ($model = null){
if (!empty($model)) {
$host = Host::find($model);
$this->model = $model;
$this->hostname = $host->hostname;
$this->action = 'update';
}
}
public function store()
{
$validatedData = $this->validate();
Host::create($validatedData);
$this->dispatch('closeModal');
}
public function update()
{
$validatedData = $this->validate();
$host = Host::find($this->model);
if (!empty($host)) {
$host->update($validatedData);
}
$this->dispatch('closeModal');
}
public function render()
{
return view('livewire.host.form');
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Livewire\Maintenance;
use App\Models\Maintenance;
use SteelAnts\DataTable\Livewire\DataTableComponent;
use Illuminate\Database\Eloquent\Builder;
class DataTable extends DataTableComponent
{
public $listeners = [
'maintenanceAdded' => '$refresh',
'closeModal' => '$refresh',
];
public function query(): Builder
{
return Maintenance::query();
}
public function headers(): array
{
return [
'name' => 'name',
'description' => 'description',
'schedule' => 'schedule',
];
}
public function remove($maintenance_id){
Maintenance::find($maintenance_id)->delete();
}
public function actions($item)
{
return [
[
'type' => "livewire",
'action' => "edit",
'text' => "edit",
'parameters' => $item['id']
],
[
'type' => "livewire",
'action' => "remove",
'text' => "remove",
'parameters' => $item['id']
]
];
}
public function edit($maintenance_id)
{
$this->dispatch('openModal', 'maintenance.form', __('boilerplate::maintenances.edit'), ['model' => $maintenance_id]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Livewire\Maintenance;
use Livewire\Component;
use App\Models\Maintenance;
class Form extends Component
{
public $model;
public string $name = "";
public string $description = "";
public string $schedule = "";
public $action = 'store';
protected function rules()
{
return [
'name' => 'required',
'description' => 'required',
'schedule' => 'required',
];
}
public function mount ($model = null){
if (!empty($model)) {
$maintenance = Maintenance::find($model);
$this->model = $model;
$this->name = $maintenance->name;
$this->description = $maintenance->description;
$this->schedule = $maintenance->schedule;
$this->action = 'update';
}
}
public function store()
{
$validatedData = $this->validate();
Maintenance::create($validatedData);
$this->dispatch('closeModal');
}
public function update()
{
$validatedData = $this->validate();
$maintenance = Maintenance::find($this->model);
if (!empty($maintenance)) {
$maintenance->update($validatedData);
}
$this->dispatch('closeModal');
}
public function render()
{
return view('livewire.maintenance.form');
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Livewire\Session;
use App\Models\Session;
use SteelAnts\DataTable\Livewire\DataTableComponent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class DataTable extends DataTableComponent
{
public bool $paginated = false;
public bool $sortable = false;
public function query(): Builder
{
return request()->user()->sessions()->orderByDesc("last_activity")->getQuery();
}
public function row($row): array
{
return [
'id' => $row->id,
'ip_address' => $row->ip_address,
'last_activity' => $row->last_activity->format('d. m. Y H:m'),
'browser_os_name' => $row->browser_o_s_name,
'browser_name' => $row->browser_name,
'last_activity' => $row->last_activity->format('d. m. Y H:m'),
];
}
public function headers(): array
{
return [
'ip_address' => "IP Address",
'browser_os_name' => "OS Name",
'browser_name' => "Browser",
'last_activity' => "Last Activity"
];
}
public function actions($item)
{
return [
[
'type' => "livewire",
'action' => "logout",
'parameters' => $item['id'],
'text' => "Logout",
'actionClass' => 'text-danger',
'iconClass' => 'fas fa-trash',
]
];
}
public function logout($session_id)
{
request()->user()->sessions()->find($session_id)->delete();
return redirect(request()->header('Referer'));
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Livewire\Subscription;
use App\Models\Subscription;
use App\Models\User;
use App\Types\SubscriptionTier;
use SteelAnts\DataTable\Livewire\DataTableComponent;
use Illuminate\Database\Eloquent\Builder;
class DataTable extends DataTableComponent
{
public $listeners = [
'subscriptionRefresh' => '$refresh'
];
public function query(): Builder
{
return Subscription::query();
}
public function row($row): array
{
return [
'id' => $row->id,
'tier' => SubscriptionTier::getName($row->tier),
'valid_to' => $row->valid_to->format('d. m. Y'),
];
}
public function headers(): array
{
return ["id", "tier", "valid_to"];
}
public function actions($item)
{
return [
[
'type' => "livewire",
'action' => "edit",
'text' => "edit",
'parameters' => $item['id']
]
];
}
public function edit($id)
{
$this->dispatch('openModal', 'subscription.form', __('boilerplate::subscriptions.edit'), $id);
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace App\Livewire\Subscription;
use App\Models\Subscription;
use Livewire\Component;
use App\Types\SubscriptionTier;
class Form extends Component
{
public $model;
public $tier;
public $valid_to;
public $tiers;
public $action = 'store';
protected function rules()
{
return [
'tier' => 'required|integer|min:1',
'valid_to' => 'required|date',
];
}
public function mount($model = null)
{
$this->tiers = SubscriptionTier::getNames();
if (!empty($model)) {
$sub = Subscription::find($model);
if (empty($sub)) {
return;
}
$this->model = $model;
$this->tier = $sub->tier;
$this->valid_to = $sub->valid_to->format('Y-m-d');
$this->action = 'update';
}
}
public function render()
{
return view('livewire.subscription.form');
}
public function store()
{
$validatedData = $this->validate();
Subscription::create($validatedData);
$this->dispatch('close-modal');
$this->dispatch('snackbar', ['message' => __('boilerplate::ui.item-created'), 'type' => 'success', 'icon' => 'fas fa-check']);
$this->dispatch('subscriptionRefresh');
$this->reset('tier');
$this->reset('valid_to');
}
public function update()
{
$validatedData = $this->validate();
$sub = Subscription::find($this->model);
if (!empty($sub)) {
$sub->update($validatedData);
}
$this->dispatch('close-modal');
$this->dispatch('snackbar', ['message' => __('boilerplate::ui.item-updated'), 'type' => 'success', 'icon' => 'fas fa-check']);
$this->dispatch('subscriptionRefresh');
$this->reset('tier');
$this->reset('valid_to');
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Livewire\User;
use App\Models\User;
use SteelAnts\DataTable\Livewire\DataTableComponent;
use Illuminate\Database\Eloquent\Builder;
class DataTable extends DataTableComponent
{
public $listeners = [
'userAdded' => '$refresh'
];
public function query(): Builder
{
return User::query();
}
public function headers(): array
{
return [
'id' => 'ID',
'name' => 'Name',
'email' => 'E-mail',
];
}
public function actions($item)
{
if ($item['id'] == auth()->user()->id) {
return [];
}
return [
[
'type' => "livewire",
'action' => "remove",
'parameters' => $item['id'],
'text' => "Remove",
'actionClass' => 'text-danger',
'iconClass' => 'fas fa-trash',
]
];
}
public function remove($user_id)
{
User::find($user_id)->delete();
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Livewire\User;
use App\Http\Requests\System\CreateUserRequest;
use Livewire\Component;
use App\Models\User;
class Form extends Component
{
public string $name ='';
public string $email ='';
public string $password ='';
public string $password_confirmation ='';
protected function rules()
{
return [
'name' => 'required|max:255|unique:users,name',
'email' => 'required|string|email|max:255|unique:users,email',
'password' => 'required|string|min:8|max:255|confirmed',
];
}
public function render()
{
return view('livewire.user.form');
}
public function store()
{
$validatedData = $this->validate();
User::create($validatedData);
$this->dispatch('close-modal');
$this->dispatch('snackbar', ['message' => __('boilerplate::ui.create'), 'type' => 'success', 'icon' => 'fas fa-check']);
$this->dispatch('userAdded');
$this->reset('name');
$this->reset('email');
$this->reset('password');
$this->reset('password_confirmation');
}
}

46
app/Models/Activity.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Observers\ActivityObserver;
class Activity extends Model
{
use HasFactory;
protected $casts = [
'data' => 'array',
];
const UPDATED_AT = null;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'ip',
'user_id',
'affected_type',
'affected_id',
'lang_text',
'data',
];
protected static function booted()
{
Activity::observe(ActivityObserver::class);
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function affected()
{
return $this->morphTo('affected');
}
}

View File

@ -8,4 +8,8 @@ use Illuminate\Database\Eloquent\Model;
class Host extends Model
{
use HasFactory;
protected $fillable = [
'hostname',
];
}

View File

@ -8,4 +8,10 @@ use Illuminate\Database\Eloquent\Model;
class Maintenance extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'schedule',
];
}

63
app/Models/Session.php Normal file
View File

@ -0,0 +1,63 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Session extends Model
{
use HasFactory;
protected $keyType = 'string';
public $incrementing = false;
public $timestamps = false;
protected $casts = [
'last_activity' => 'datetime',
];
protected $appends = [
'browser_name',
'browser_os_name',
];
public function user()
{
return $this->belongsTo(User::class);
}
protected function browserName(): Attribute
{
return Attribute::make(
get: function () {
$pattern = '/\b(Edge|Safari|Chrome|Firefox|Opera)\b/i'; // Regular expression pattern to match common browser names
$matches = [];
preg_match($pattern, $this->user_agent, $matches);
if (isset($matches[1])) {
$browserName = $matches[1];
return $browserName;
}
return 'Unknown';
},
);
}
protected function browserOSName(): Attribute
{
return Attribute::make(
get: function () {
$pattern = '/\b(Android|Linux|Windows|iOS|MacOS)\b/i'; // Regular expression pattern to match common browser names
$matches = [];
preg_match($pattern, $this->user_agent, $matches);
if (isset($matches[1])) {
$browserName = $matches[1];
return $browserName;
}
return 'Unknown';
},
);
}
}

17
app/Models/Setting.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Setting extends Model
{
use HasFactory;
public function settingable(): MorphTo
{
return $this->morphTo();
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Subscription extends Model
{
use HasFactory;
protected $fillable = [
'tier',
'valid_to',
];
protected $casts = [
'valid_to' => 'datetime',
];
}

21
app/Models/Tenant.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
use HasFactory;
protected $fillable = [
'name',
'domain',
];
public function setDomainAttribute($value)
{
$this->attributes['domain'] = strtolower($value);
}
}

View File

@ -6,10 +6,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use App\Traits\Auditable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
use HasFactory, Notifiable, HasApiTokens, Auditable;
/**
* The attributes that are mass assignable.
@ -44,4 +46,9 @@ class User extends Authenticatable
'password' => 'hashed',
];
}
public function sessions()
{
return $this->hasMany(Session::class);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Observers;
use App\Models\Activity;
class ActivityObserver
{
/**
* Handle the Activity "created" event.
*/
public function creating(Activity $activity): void
{
if (!app()->runningInConsole()) {
$activity->ip = $this->getIp();
$activity->user_id = auth()->id();
} else {
$activity->ip = "localhost";
$activity->user_id = 0;
}
}
public function getIp()
{
foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key) {
if (array_key_exists($key, $_SERVER) === true) {
foreach (explode(',', $_SERVER[$key]) as $ip) {
$ip = trim($ip); // just to be safe
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
return $ip;
}
}
}
}
return request()->ip(); // it will return server ip when no client ip found
}
}

View File

@ -19,6 +19,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
if (config('app.env') != 'local'){
\URL::forceScheme('https');
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Providers;
use App\Models\Tenant;
use App\Services\TenantManager;
use Illuminate\Support\ServiceProvider;
class TenantServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
$this->app->singleton(TenantManager::class, function () {
$tenant = null;
if (!app()->runningInConsole()) {
$tenant = Tenant::where('domain', explode(".", request()->getHost())[0])
->with(['users', 'settings'])
->first();
if (is_null($tenant)) {
abort(404, 'Tenant ' . explode(".", request()->getHost())[0] . ' not found (' . request()->getHost() . ')');
die();
}
}
return new TenantManager($tenant);
});
}
}

35
app/Traits/Auditable.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace App\Traits;
use App\Models\Activity;
trait Auditable
{
public static function bootAuditable()
{
if (app()->runningInConsole()) {
return;
}
static::created(function ($model) {
$activity = new Activity();
$activity->lang_text = __('boilerplate::ui.created', ["model" => class_basename($model) . " " . $model->name]);
$activity->affected()->associate($model);
$activity->save();
});
static::updating(function ($model) {
$activity = new Activity();
$activity->lang_text = __('boilerplate::ui.updated', ["model" => class_basename($model) . " " . $model->name]);
$activity->affected()->associate($model);
$activity->save();
});
static::deleting(function ($model) {
$activity = new Activity();
$activity->lang_text = __('boilerplate::ui.deleted', ["model" => class_basename($model) . " " . $model->name]);
$activity->save();
});
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Traits;
use App\Models\Activity;
use App\Models\Setting;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait HasSettings
{
public function settings() : MorphMany
{
return $this->morphMany(Setting::class, 'settable');
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Types;
class SubscriptionTier
{
const TIER_1 = 1;
const TIER_2 = 2;
const TIER_3 = 3;
// Add more tiers if needed
public function getLimits()
{
return [
self::TIER_1 => [
'limit_name' => 100,
// Add more limits if needed
],
self::TIER_2 => [
'limit_name' => 1000,
],
self::TIER_3 => [
'limit_name' => 10000,
],
];
}
public static function getNames()
{
return [
self::TIER_1 => __('boilerplate::subscriptions.tier_1.title'),
self::TIER_2 => __('boilerplate::subscriptions.tier_2.title'),
self::TIER_3 => __('boilerplate::subscriptions.tier_3.title'),
];
}
public static function getName($type)
{
return self::getNames()[$type] ?? 'undefined';
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Alerts extends Component
{
/**
* Create a new component instance.
*/
public function __construct(
public ?array $alerts = [],
)
{
$types = [
'success',
'error',
'warning',
'info',
'message',
];
foreach($types as $type){
$message = session()->get($type);
if(!empty($message)){
$this->alerts[] = [
'type' => $type,
'message' => $message
];
}
}
if(session()->has('errors')){
$items = session()->get('errors')->toArray();
foreach($items as $item){
if(!empty($item['error'])){
$this->alerts[] = [
'type' => 'error',
'message' => $item['error']
];
}
}
}
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.alerts');
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\View\Components;
use App\Menus\MainMenu;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
use SteelAnts\LaravelBoilerplate\Facades\Menu;
class Navigation extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.navigation', [
'mainMenuItems' => Menu::get('main-menu')->items() ?? [],
'systemMenuItems' => Menu::get('system-menu')->items() ?? [],
]);
}
}

View File

@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)

View File

@ -7,6 +7,7 @@
"require": {
"php": "^8.2",
"laravel/framework": "^11.9",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.9",
"steelants/laravel-boilerplate": "^1.2"
},

66
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e715a272525d211a1790b5afb115f9be",
"content-hash": "d901703e92531876ccae1dd19ef6f1cb",
"packages": [
{
"name": "brick/math",
@ -1314,6 +1314,70 @@
},
"time": "2024-06-17T13:58:22+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.0.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/9cfc0ce80cabad5334efff73ec856339e8ec1ac1",
"reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0",
"illuminate/contracts": "^11.0",
"illuminate/database": "^11.0",
"illuminate/support": "^11.0",
"php": "^8.2",
"symfony/console": "^7.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2024-04-10T19:39:58+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v1.3.3",

158
config/livewire.php Normal file
View File

@ -0,0 +1,158 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Class Namespace
|--------------------------------------------------------------------------
|
| This value sets the root namespace for Livewire component classes in
| your application. This value affects component auto-discovery and
| any Livewire file helper commands, like `artisan make:livewire`.
|
| After changing this item, run: `php artisan livewire:discover`.
|
*/
'class_namespace' => 'App\\Livewire',
/*
|--------------------------------------------------------------------------
| View Path
|--------------------------------------------------------------------------
|
| This value sets the path for Livewire component views. This affects
| file manipulation helper commands like `artisan make:livewire`.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|--------------------------------------------------------------------------
| Layout
|--------------------------------------------------------------------------
| The default layout view that will be used when rendering a component via
| Route::get('/some-endpoint', SomeComponent::class);. In this case the
| the view returned by SomeComponent will be wrapped in "layouts.app"
|
*/
'layout' => 'components.layout-app',
/*
|--------------------------------------------------------------------------
| Livewire Assets URL
|--------------------------------------------------------------------------
|
| This value sets the path to Livewire JavaScript assets, for cases where
| your app's domain root is not the correct path. By default, Livewire
| will load its JavaScript assets from the app's "relative root".
|
| Examples: "/assets", "myurl.com/app".
|
*/
'asset_url' => env('ASSET_URL', null),
/*
|--------------------------------------------------------------------------
| Livewire App URL
|--------------------------------------------------------------------------
|
| This value should be used if livewire assets are served from CDN.
| Livewire will communicate with an app through this url.
|
| Examples: "https://my-app.com", "myurl.com/app".
|
*/
'app_url' => env('ASSET_URL', null),
/*
|--------------------------------------------------------------------------
| Livewire Endpoint Middleware Group
|--------------------------------------------------------------------------
|
| This value sets the middleware group that will be applied to the main
| Livewire "message" endpoint (the endpoint that gets hit everytime
| a Livewire component updates). It is set to "web" by default.
|
*/
'middleware_group' => 'web',
/*
|--------------------------------------------------------------------------
| Livewire Temporary File Uploads Endpoint Configuration
|--------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is validated and stored permanently. All file uploads
| are directed to a global endpoint for temporary storage. The config
| items below are used for customizing the way the endpoint works.
|
*/
'temporary_file_upload' => [
'disk' => null, // Example: 'local', 's3' Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' Default 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs.
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload gets invalidated.
],
/*
|--------------------------------------------------------------------------
| Manifest File Path
|--------------------------------------------------------------------------
|
| This value sets the path to the Livewire manifest file.
| The default should work for most cases (which is
| "<app_root>/bootstrap/cache/livewire-components.php"), but for specific
| cases like when hosting on Laravel Vapor, it could be set to a different value.
|
| Example: for Laravel Vapor, it would be "/tmp/storage/bootstrap/cache/livewire-components.php".
|
*/
'manifest_path' => null,
/*
|--------------------------------------------------------------------------
| Back Button Cache
|--------------------------------------------------------------------------
|
| This value determines whether the back button cache will be used on pages
| that contain Livewire. By disabling back button cache, it ensures that
| the back button shows the correct state of components, instead of
| potentially stale, cached data.
|
| Setting it to "false" (default) will disable back button cache.
|
*/
'back_button_cache' => false,
/*
|--------------------------------------------------------------------------
| Render On Redirect
|--------------------------------------------------------------------------
|
| This value determines whether Livewire will render before it's redirected
| or not. Setting it to "false" (default) will mean the render method is
| skipped when redirecting. And "true" will mean the render method is
| run before redirecting. Browsers bfcache can store a potentially
| stale view if render is skipped on redirect.
|
*/
'render_on_redirect' => false,
];

View File

@ -12,13 +12,13 @@ return [
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
| This option defines the default log channel that gets used when writing
| messages to the logs. The name specified in this option should match
| one of the channels defined in the "channels" configuration array.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
'default' => env('LOG_CHANNEL', 'daily'),
/*
|--------------------------------------------------------------------------
@ -33,7 +33,7 @@ return [
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
'trace' => false,
],
/*
@ -41,20 +41,20 @@ return [
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
| Here you may configure the log channels for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
| Available Drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog",
| "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', env('LOG_STACK', 'single')),
'channels' => ['single'],
'ignore_exceptions' => false,
],
@ -69,15 +69,15 @@ return [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'days' => 14,
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
@ -108,7 +108,7 @@ return [
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'facility' => LOG_USER,
'replace_placeholders' => true,
],
@ -126,7 +126,6 @@ return [
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

83
config/sanctum.php Normal file
View File

@ -0,0 +1,83 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort()
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\User;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('activities', function (Blueprint $table) {
$table->id();
$table->string('ip')->nullable();
$table->foreignId("user_id")->nullable();
$table->foreign("user_id")->references("id")->on("users")->constrained();
$table->nullableMorphs('affected');
$table->string('lang_text')->nullable();
$table->json('data')->nullable();
$table->timestamp("created_at");
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('activities');
}
};

View File

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

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->nullableMorphs('settable');
$table->string('index')->indexed();
$table->string('type', 50)->default('string'); //TODO: @Vasek Maybe use Set
$table->text('value');
$table->timestamps();
$table->unique(['settable_type', 'settable_id', 'index']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('settings');
}
};

View File

@ -2,7 +2,7 @@
use App\Models\Host;
use App\Models\MaintenanceHistory;
use Illuminate\Console\View\Components\Task;
use App\Models\MaintenanceTask;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@ -16,7 +16,7 @@ return new class extends Migration
{
Schema::create('maintenance_task_histories', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Task::class);
$table->foreignIdFor(MaintenanceTask::class);
$table->foreignIdFor(MaintenanceHistory::class);
$table->foreignIdFor(Host::class);
$table->string('status');

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

296
package-lock.json generated
View File

@ -4,6 +4,17 @@
"requires": true,
"packages": {
"": {
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.3",
"jquery": "^3.6.1",
"laravel-vite-plugin": "^1.0.0",
"quill": "2.0.0-rc.2",
"quill-table-ui": "^1.0.7",
"sass": "^1.56.1",
"vite": "^5.0.0"
},
"devDependencies": {
"axios": "^1.6.4",
"laravel-vite-plugin": "^1.0",
@ -378,6 +389,24 @@
"node": ">=12"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz",
@ -592,6 +621,18 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -609,6 +650,69 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -668,6 +772,27 @@
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
@ -706,7 +831,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@ -716,6 +840,65 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
},
"node_modules/laravel-vite-plugin": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.5.tgz",
@ -735,6 +918,21 @@
"vite": "^5.0.0"
}
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -774,6 +972,19 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/parchment": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
"integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A=="
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
@ -784,7 +995,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
@ -792,6 +1002,11 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/positioning": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/positioning/-/positioning-2.0.1.tgz",
"integrity": "sha512-DsAgM42kV/ObuwlRpAzDTjH9E8fGKkMDJHWFX+kfNXSxh7UCCQxEmdjv/Ws5Ft1XDnt3JT8fIDYeKNSE2TbttA=="
},
"node_modules/postcss": {
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
@ -826,6 +1041,55 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/quill": {
"version": "2.0.0-rc.2",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.0-rc.2.tgz",
"integrity": "sha512-3uh7uZqXpN4t0HmHCjBHXSaeSgYNij6LP1t8ncEjr6+JMq5zR6svpH5Frx7Lxmxv1DUHYDE28eV7R3f4r8Z2Kw==",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0-alpha.2",
"quill-delta": "^5.1.0"
},
"engines": {
"npm": ">=8.2.3"
}
},
"node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
"dependencies": {
"fast-diff": "^1.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/quill-table-ui": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/quill-table-ui/-/quill-table-ui-1.0.7.tgz",
"integrity": "sha512-Zr/KWiLmCkaaS1eybwkQX17FUGJEBvpHAOSP7A8J+E2P4R56S+Uvs+U2i+fIxaydfnPksDV/sDJ8EWsFg37dsQ==",
"dependencies": {
"positioning": "^2.0.0"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/rollup": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz",
@ -861,15 +1125,41 @@
"fsevents": "~2.3.2"
}
},
"node_modules/sass": {
"version": "1.77.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/vite": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",

View File

@ -9,5 +9,16 @@
"axios": "^1.6.4",
"laravel-vite-plugin": "^1.0",
"vite": "^5.0"
},
"dependencies": {
"jquery": "^3.6.1",
"sass": "^1.56.1",
"bootstrap": "^5.3",
"@fortawesome/fontawesome-free": "^5.15.4",
"@popperjs/core": "^2.11.6",
"vite": "^5.0.0",
"laravel-vite-plugin": "^1.0.0",
"quill": "2.0.0-rc.2",
"quill-table-ui": "^1.0.7"
}
}

View File

@ -1 +1,3 @@
import './bootstrap';
import './functions';
import './quill';

View File

@ -1,4 +1,41 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* 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.
*/
// import $ from 'jquery';
// window.$ = $;
import $ from 'jquery';
window.$ = window.jQuery = $;
import * as bootstrap from 'bootstrap';
window.bootstrap = bootstrap;
// import axios from 'axios';
// window.axios = 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';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });

86
resources/js/functions.js Normal file
View File

@ -0,0 +1,86 @@
window.setCookie = function(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
window.getCookie = function(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
window.eraseCookie = function(name) {
document.cookie = name +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
}
window.toggleDatkTheme = function(){
if(document.getElementById('datk-theme').checked){
document.documentElement.setAttribute('data-bs-theme', 'dark');
setCookie('theme', 'dark', 360);
}else{
document.documentElement.setAttribute('data-bs-theme', 'light');
setCookie('theme', 'light', 360);
}
}
window.snackbar = function (message, details = false, type = false, icon = false){
var template = `
<div class="snackbar alert border-0">
<button type="button" class="btn-close btn-close-white close ${details ? '' : 'mt-2'}" data-bs-dismiss="alert"></button>
<div class="alert-content">`;
if(icon){
template += `<i class="alert-ico ${icon} text-${type}"></i>`;
}
template += `
<div>
<div class="alert-title ${details ? '' : 'mt-2'}">${message}</div>`
if(details){
template += `<div class="alert-text">${details}</div>`
}
template += `
</div>
</div>
</div>`;
document.querySelector('.snackbar-container').insertAdjacentHTML('beforeend', template);
}
window.addEventListener('snackbar', function(e){
snackbar(e.detail.message, e.detail.details || false, e.detail.type || false, e.detail.icon || false);
});
window.copyToClipboard = function (text, el = false) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {})
.catch(() => {snackbar('something went wrong');});
} else {
let textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
new Promise((res, rej) => {
document.execCommand('copy') ? res() : rej();
textArea.remove();
});
}
snackbar('Copied to clipboard');
}

77
resources/js/quill.js Normal file
View File

@ -0,0 +1,77 @@
import Quill from 'quill';
window.Quill = Quill;
import * as quillTableUI from 'quill-table-ui'
Quill.register({
'modules/tableUI': quillTableUI.default
}, true);
// Quill.register(Quill.import('attributors/attribute/direction'), true);
// Quill.register(Quill.import('attributors/class/align'), true);
// Quill.register(Quill.import('attributors/class/background'), true);
// Quill.register(Quill.import('attributors/class/color'), true);
// Quill.register(Quill.import('attributors/class/direction'), true);
// Quill.register(Quill.import('attributors/class/font'), true);
// Quill.register(Quill.import('attributors/class/size'), true);
Quill.register(Quill.import('attributors/style/align'), true);
Quill.register(Quill.import('attributors/style/background'), true);
Quill.register(Quill.import('attributors/style/color'), true);
Quill.register(Quill.import('attributors/style/direction'), true);
Quill.register(Quill.import('attributors/style/font'), true);
Quill.register(Quill.import('attributors/style/size'), true);
window.loadQuill = function(){
document.querySelectorAll('.quill-editor:not(.ready)').forEach(function(element){
let container = element.closest('.quill-container');
let textarea = container.querySelector('.quill-textarea');
const toolbarOptions = [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
['link', 'image'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],
['table'],
['clean'],
];
let quill = new Quill(element, {
theme: 'snow',
modules: {
table: true,
tableUI: true,
toolbar: {
container: toolbarOptions,
handlers: {
table: function() {
this.quill.getModule('table').insertTable(3, 3);
}
}
},
clipboard: {
matchVisual: false
}
}
});
// let table = quill.getModule('table')
// container.querySelector('.ql-insert-table').addEventListener('click', function(){
// console.log('click');
// table.insertTable(2, 2);
// });
quill.root.innerHTML = textarea.value;
quill.on('text-change', function () {
let value = quill.root.innerHTML;
textarea.value = value;
textarea.dispatchEvent(new Event('input'));
console.log(quill.getContents());
});
element.classList.add('ready');
container.querySelector('.quill-loading').remove();
});
}
window.loadQuill();

View File

@ -0,0 +1,90 @@
@import "./quill/snow.scss";
@import "quill-table-ui/src/index.scss";
.quill-editor-wrap{
position: relative;
min-height: 9rem;
display: flex;
flex-direction: column;
}
.quill-editor-wrap textarea{
display: none;
}
.quill-editor{
flex-grow: 1;
display: flex;
flex-direction: column;
}
.quill-editor .ql-editor{
flex-grow: 1;
}
.quill-loading{
position: absolute;
z-index: 10;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
display: flex;
border-radius: var(--bs-border-radius);
border: var(--bs-border-width) solid var(--bs-border-color);
}
.quill-editor.ready + .quill-loading{
display: none;
}
.ql-toolbar.ql-snow{
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
}
.ql-container.ql-snow{
border: var(--bs-border-width) solid var(--bs-border-color);
border-top: 0;
border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
}
.ql-table-menu{
background: var(--bs-body-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
}
.ql-table-menu__item-icon svg *{
fill: var(--bs-secondary);
}
.ql-table-menu__item:hover{
background: var(--bs-tertiary-bg);
}
// .ql-snow.ql-toolbar button:hover, .ql-snow .ql-toolbar button:hover, .ql-snow.ql-toolbar button:focus, .ql-snow .ql-toolbar button:focus, .ql-snow.ql-toolbar button.ql-active, .ql-snow .ql-toolbar button.ql-active, .ql-snow.ql-toolbar .ql-picker-label:hover, .ql-snow .ql-toolbar .ql-picker-label:hover, .ql-snow.ql-toolbar .ql-picker-label.ql-active, .ql-snow .ql-toolbar .ql-picker-label.ql-active, .ql-snow.ql-toolbar .ql-picker-item:hover, .ql-snow .ql-toolbar .ql-picker-item:hover, .ql-snow.ql-toolbar .ql-picker-item.ql-selected, .ql-snow .ql-toolbar .ql-picker-item.ql-selected{
// color: var(--bs-primary);
// }
// .ql-snow.ql-toolbar button:hover .ql-stroke, .ql-snow .ql-toolbar button:hover .ql-stroke, .ql-snow.ql-toolbar button:focus .ql-stroke, .ql-snow .ql-toolbar button:focus .ql-stroke, .ql-snow.ql-toolbar button.ql-active .ql-stroke, .ql-snow .ql-toolbar button.ql-active .ql-stroke, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, .ql-snow.ql-toolbar button:hover .ql-stroke-miter, .ql-snow .ql-toolbar button:hover .ql-stroke-miter, .ql-snow.ql-toolbar button:focus .ql-stroke-miter, .ql-snow .ql-toolbar button:focus .ql-stroke-miter, .ql-snow.ql-toolbar button.ql-active .ql-stroke-miter, .ql-snow .ql-toolbar button.ql-active .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter{
// stroke: var(--bs-primary);
// }
// .ql-snow .ql-stroke{
// stroke: var(--bs-body-color);
// }
// .ql-snow .ql-picker{
// color: var(--bs-body-color);
// }
// .ql-snow .ql-picker-options{
// background-color: var(--bs-body-bg);
// color: var(--bs-body-color);
// }
// .ql-toolbar.ql-snow .ql-picker-options{
// border: var(--bs-dropdown-border-width) solid var(--bs-border-color-translucent);
// }

View File

@ -0,0 +1,280 @@
a{
// text-decoration: none;
}
.card-header{
font-weight: 500;
font-size: 1rem;
}
.logincard{
width: 430px;
max-width: 100%;
}
.stat{
display: flex;
align-items: start;
line-height: 1.2;
}
.stat-ico{
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
border-radius: 10px;
background: rgba(0, 0, 0, .7);
&.is-green{
background: rgba(40, 199, 111,.7);
}
&.is-red{
background: rgba(234, 84, 85, .7);
}
&.is-purple{
background: rgba(99, 132, 255, .7)
}
}
.stat-name{
font-size: 14px;
color: #6c757d;
}
.stat-value{
font-size: 20px;
font-weight: 700;
}
.nav-item .stat{
padding-left: 1rem;
}
.btn-facebook{
color: white;
background: #4267B2;
&:hover,
&:focus{
color: white;
background: #3a5b9c;
}
}
.table-actions{
text-align: right;
}
.list-notifications{
width: 360px;
max-width: 80vw;
max-height: 90vh;
}
// .alert{
// position: fixed;
// z-index: 100;
// bottom: 0;
// width: 500px;
// right: 2rem;
// max-width: calc(100% - 4rem);
// }
.card-action {
padding: 0 $card-spacer-x $card-spacer-y;
display: flex;
justify-content: flex-end;
}
.custom-checkbox{
cursor: pointer;
.custom-control-label{
cursor: pointer;
}
}
.form-group label{
font-weight: 500;
}
.invalid-feedback strong{
font-weight: 500;
}
.table th{
font-weight: 500;
vertical-align: middle;
}
.form-group{
margin-bottom: 1rem;
}
.editor{
min-height: 80px;
}
.image-thumb{
cursor: pointer;
position: relative;
display: inline-block;
&:hover::before{
content: '';
display: block;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0,0,0,.2);
z-index: 1;
}
}
.image-thumb-ico{
display: none;
pointer-events: none;
color: white;
font-size: 2rem;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.image-thumb:hover .image-thumb-ico{
display: block;
}
.collapsed .collapse-indicator{
transform: rotate(180deg);
}
.ql-toolbar.ql-snow,
.ql-container.ql-snow{
background: white;
}
@include color-mode(dark) {
.ql-toolbar.ql-snow,
.ql-container.ql-snow{
background: transparent;
border-color: #495057;
}
.ql-editor.ql-blank::before{
color: #495057;
}
.ql-snow .ql-stroke{
stroke: #a5a5a5;
}
}
.hide-mobile{
@include media-breakpoint-down(md) {
display: none !important;
}
}
select[multiple]{
height: 50px;
}
.select2{
width: 100% !important;
}
.breadcrumb-item a{
text-decoration: none;
}
.btn-danger{
color: white;
&:hover{
color: white;
}
}
.bi{
width: 1em;
height: 1em;
display: inline-block;
vertical-align: -.125em;
fill: currentcolor;
}
.random-bg-1{
background-color: rgba($tw-green-500, .5);
}
.random-bg-2{
background-color: rgba($tw-sky-400, .5);
}
.random-bg-3{
background-color: rgba($tw-red-500, .5);
}
.random-bg-4{
background-color: rgba($tw-yellow-500, .5);
}
.random-bg-5{
background-color: rgba($tw-indigo-500, .5);
}
.random-bg-6{
background-color: rgba($tw-orange-500, .5);
}
.dropdown-menu{
box-shadow: var(--bs-dropdown-box-shadow);
}
@include color-mode(dark) {
.dropdown-menu{
background-color: #0f0f0f;
}
}
.dropdown-item{
display: flex;
align-items: center;
}
.dropdown-ico{
width: 1.25rem;
height: 1rem;
text-align: center;
line-height: 1rem;
color: var(--bs-tertiary-color);
margin-right: .5rem;
}
.dropdown-img{
width: 2rem;
height: 2rem;
border-radius: 4px;
overflow: hidden;
font-weight: 600;
text-align: center;
line-height: 2rem;
margin-right: .75rem;
font-size: .875rem;
img{
width: 100%;
height: 100%;
}
}
.page-link{
border-radius: $pagination-border-radius;
}

View File

@ -0,0 +1,5 @@
$body-color-dark: $gray-300;
$body-bg-dark: #000;
$body-secondary-bg-dark: $gray-800;
$body-tertiary-bg-dark: $gray-900;

View File

@ -0,0 +1,113 @@
$enable-container-classes: false;
$container-max-widths: (
xs: 360px,
sm: 540px,
md: 720px,
lg: 960px,
xl: 1140px,
xxl: 1320px
);
$gray-100: $tw-neutral-100;
$gray-200: $tw-neutral-200;
$gray-300: $tw-neutral-300;
$gray-400: $tw-neutral-400;
$gray-500: $tw-neutral-500;
$gray-600: $tw-neutral-600;
$gray-700: $tw-neutral-700;
$gray-800: $tw-neutral-800;
$gray-900: $tw-neutral-900;
$blue: $tw-blue-500 !default;
$indigo: $tw-indigo-500 !default;
$purple: $tw-purple-500 !default;
$pink: $tw-pink-500 !default;
$red: $tw-red-500 !default;
$orange: $tw-amber-500 !default;
$yellow: $tw-yellow-500 !default;
$green: $tw-green-500 !default;
$teal: $tw-teal-500 !default;
$cyan: $tw-cyan-500 !default;
// Body
$body-color: $gray-900;
$body-bg: #fff;
$primary: $tw-indigo-600;
// $body-secondary-bg: #ededed;
$body-secondary-bg: #f0f0f0;
$body-tertiary-bg: $tw-neutral-050;
// Typography
$font-family-sans-serif: 'Inter', sans-serif;
$h1-font-size: 2.25rem;
$h2-font-size: 1.875rem;
$h3-font-size: 1.5rem;
$h4-font-size: 1.25rem;
$h5-font-size: 1.125rem;
$card-cap-bg: #f9fbfc;
$border-radius: 6px;
$card-spacer-y: 1rem;
$headings-font-weight: 700;
$text-muted: #a7abc3;
$border-color-translucent: rgba($gray-900, .075);
$box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
$box-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
$box-shadow-lg: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
// $btn-font-size: 0.875rem;
$btn-font-weight: 500;
$input-btn-padding-x: .75rem;
$input-btn-padding-y: .375rem;
$navbar-brand-font-size: 1rem;
$nav-link-font-weight: 500;
$nav-tabs-link-active-color: $primary;
$nav-tabs-link-active-border-color: $primary;
$nav-tabs-link-hover-border-color: var(--bs-border-color);
$nav-tabs-border-width: 2px;
$nav-tabs-border-radius: 0;
$nav-tabs-link-active-bg: transparent;
$nav-tabs-border-color: var(--bs-secondary-bg);
$nav-pills-link-active-color: var(--bs-body-color);
$nav-pills-link-active-bg: var(--bs-body-bg);
$label-margin-bottom: .25rem;
$alert-padding-x: 1rem;
$paragraph-margin-bottom: .5rem;
$badge-font-weight: 500;
$breadcrumb-divider: '';
$dropdown-padding-x: .5rem;
$dropdown-padding-y: .5rem;
$dropdown-item-padding-y: .4rem;
$dropdown-item-padding-x: .4rem;
$dropdown-border-radius: calc(4px + .5rem);
:root{
--bs-dropdown-item-border-radius: 4px;
}
$dropdown-link-active-bg: var(--bs-secondary-bg);
$dropdown-link-active-color: var(--bs-body-color);
$dropdown-border-color: rgba($gray-500, .075);
$dropdown-divider-bg: rgba($gray-500, .1);
$pagination-color: var(--bs-body-color);
$pagination-border-width: 0;
$pagination-bg: transparent;
$pagination-active-bg: var(--bs-secondary-bg);
$pagination-active-color: var(--bs-body-color);
$pagination-hover-color: var(--bs-body-color);
$pagination-focus-color: var(--bs-body-color);
$pagination-disabled-color: var(--bs-tertiary-color);
$pagination-disabled-bg: transparent;

View File

@ -0,0 +1,144 @@
.app-nav{
flex-direction: column;
}
.app-nav-header,
.app-nav-user{
display: flex;
align-items: center;
cursor: pointer;
color: var(--bs-body-color);
text-decoration: none;
padding-right: .5rem;
padding-left: .5rem;
border-radius: 6px;
&:hover,
&.show{
background-color: var(--bs-secondary-bg);
}
&+.dropdown-menu{
width: 100%;
max-width: 100%;
}
}
.app-nav-header-content{
line-height: 1;
padding: .5rem .25rem;
overflow: hidden;
text-overflow: ellipsis;
& > *{
overflow: hidden;
text-overflow: ellipsis;
}
}
.app-nav-logo,
.app-nav-profile{
width: 32px;
height: 32px;
border-radius: 4px;
overflow: hidden;
font-weight: 600;
text-align: center;
line-height: 32px;
margin: 0.5rem;
margin-left: 0;
img{
display: block;
width: 100%;
height: 100%;
}
}
.app-nav-profile{
border-radius: 100%;
font-size: .875rem;
}
.app-nav .nav-link{
border-radius: 6px;
color: var(--bs-body-color);
padding: 0.5rem .75rem;
display: flex;
align-items: center;
cursor: pointer;
&:hover{
color: var(--bs-body-color);
background: var(--bs-secondary-bg);
}
}
.nav-link-ico{
margin-right: .75rem;
width: 1.25rem;
height: 1.25rem;
text-align: center;
line-height: 1.25rem;
color: var(--bs-tertiary-color);
}
.app-nav .is-active{
.nav-link{
background: var(--bs-secondary-bg);
}
.nav-link-ico{
color: var(--bs-secodnary-color);
}
}
.nav-tabs{
.nav-link{
border-top: none !important;
border-left: none !important;
border-right: none !important;
color: var(--bs-tertiary-color);
&.disabled,
&:disabled{
color: var(--bs-tertiary-color);
opacity: .5;
}
}
.nav-link:hover,
.nav-link.active,
.nav-item.show .nav-link {
color: var(--bs-body-color);
}
}
.nav-pills{
border-radius: calc($nav-pills-border-radius + 3px);
background-color: var(--bs-tertiary-bg);
padding: 3px;
width: max-content;
align-items: center;
.nav-link{
padding: calc(var(--#{$prefix}nav-link-padding-y) - 2px) calc(var(--#{$prefix}nav-link-padding-x) - 1px);
color: var(--bs-tertiary-color);
&:hover{
color: var(--bs-body-color);
}
&.disabled,
&:disabled{
color: var(--bs-tertiary-color);
opacity: .5;
}
}
.nav-link.active,
.nav-item.show .nav-link {
color: var(--bs-body-color);
box-shadow: 0 0 1px 1px var(--bs-secondary-bg), inset 0 0 1px $gray-500;
background-color: var(--bs-body-bg);
}
}

View File

@ -0,0 +1,64 @@
.snackbar-container {
position: fixed;
top: 65px;
right: 2rem;
height: auto;
// display: flex;
// align-items: center;
// justify-content: center;
// flex-direction: column;
z-index: 200;
}
.snackbar {
position: relative;
color: var(--bs-body-color);
background: var(--bs-body-bg);
max-width: 360px;
font-size: 0.875rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0/0.1), 0 2px 4px -2px rgb(0 0 0/0.1);
border: 1px solid var(--bs-secondary-bg);
margin-right: -400px;
opacity: 0;
animation: slideSnackbar 3s forwards;
animation-delay: 0.2s;
}
@keyframes slideSnackbar {
6% {
margin-right: 0;
opacity: 1;
}
94% {
margin-right: 0;
opacity: 1;
}
99% {
opacity: 0;
}
100% {
opacity: 0;
display: none;
}
}
.alert-content {
display: flex;
align-items: baseline;
padding-right: 1rem;
}
.alert-ico {
font-size: 1.1rem;
margin-right: 0.75rem;
display: inline-block;
}
.alert-title {
font-weight: 500;
}
.alert .close {
float: right;
}

View File

@ -0,0 +1,68 @@
.table thead th{
border-top: none;
border-bottom: none;
padding: 0.5rem 0.75rem;
font-weight: 500;
color: var(--bs-secondary-color);
background: var(--bs-tertiary-bg);
&:first-child{
border-radius: $border-radius 0 0 $border-radius;
}
&:last-child{
border-radius: 0 $border-radius $border-radius 0;
}
}
.table thead + tbody tr:first-child td{
border-top: none;
}
.table tbody tr:last-child td{
&:first-child{
border-radius: 0 0 0 $border-radius;
}
&:last-child{
border-radius: 0 0 $border-radius 0;
}
}
.table-selectable th:first-child,
.table-selectable td:first-child{
padding-right: 0;
width: 1px;
}
.table-selectable th:last-child,
.table-selectable td:last-child{
width: 1px;
padding-top: 0;
padding-bottom: 0;
vertical-align: middle;
}
.table-selectable tbody tr:hover td{
cursor: pointer;
background: hsl(220, 20%, 99%);
}
.table-selectable tbody tr.selected td{
background: #f6f5ff;
}
.table-selectable .custom-control{
display: inline-block;
}
.datatable-head-sort{
cursor: pointer;
border-radius: 4px;
padding: 4px 6px;
margin: -4px -6px;
&:hover{
background: var(--bs-secondary-bg);
}
}

View File

@ -0,0 +1,10 @@
.container{
@include make-container();
}
@each $breakpoint, $container-max-width in $container-max-widths {
.container-#{$breakpoint} {
@extend .container;
max-width: $container-max-width;
}
}

View File

@ -0,0 +1,27 @@
.layout-nav{
width: 260px;
flex-basis: 260px;
flex-shrink: 0;
background: var(--bs-tertiary-bg);
padding: .75rem;
border-right: 1px solid var(--bs-secondary-bg);
display: flex;
flex-direction: column;
overflow: auto;
@include media-breakpoint-down(lg) {
display: none;
z-index: 99;
height: 100%;
max-height: calc(100vh - 60px);
border: none;
position: fixed;
top: 60px;
width: 100%;
&.layout-nav-open{
display: flex;
}
}
}

View File

@ -0,0 +1,52 @@
.layout-nav-mobile{
display: none;
}
@include media-breakpoint-down(lg) {
.layout-nav-mobile{
display: block;
position: fixed;
left: 0;
bottom: 0;
z-index: 90;
width: 100%;
flex-basis: 100%;
padding: .5rem;
padding-bottom: calc(.5rem + env(safe-area-inset-bottom, 0));
background: var(--bs-tertiary-bg);
border-top: 1px solid var(--bs-secondary-bg);
box-shadow: 0 0px 6px -1px rgb(0 0 0/0.1),0 2px 4px -2px rgb(0 0 0/0.1);
.app-nav{
flex-direction: row;
}
.app-nav > *:not(.nav-item-mobile) {
display: none;
}
.app-nav .nav-item-mobile{
flex-grow: 1;
}
.app-nav .nav-item-mobile .nav-link{
padding: .5rem;
display: flex;
flex-direction: column;
align-items: center;
}
.app-nav .nav-item-mobile .nav-link-ico{
margin-right: 0;
margin-bottom: .25rem;
margin-top: .25rem;
}
.app-nav .nav-item-mobile .nav-link-content{
font-size: 10px;
line-height: 1;
opacity: .7;
}
}
}

View File

@ -0,0 +1,75 @@
html{
@include media-breakpoint-up(lg) {
font-size: 0.875rem;
}
}
body{
height: 100vh;
padding-top: env(safe-area-inset-top, 0);
}
#app{
height: 100%;
// display: flex;
// flex-direction: column;
& > .layout{
flex-grow: 1;
}
@include media-breakpoint-down(lg) {
padding-top: 60px;
}
}
.layout{
display: flex;
height: 100%;
position: relative;
}
.layout-content{
flex-grow: 1;
max-height: 100%;
overflow: auto;
}
.layout-content .content{
padding-top: 1.5rem;
padding-bottom: 1.5rem;
@include media-breakpoint-down(lg) {
padding-bottom: 100px;
}
}
.navbar-main{
display: none;
@include media-breakpoint-down(lg) {
display: block;
height: 60px;
flex-basis: 60px;
background-color: var(--bs-body-bg);
border-bottom: 1px solid var(--bs-secondary-bg);
position: fixed;
top: 0;
width: 100%;
}
}
.page-header{
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
h1{
margin-bottom: 0;
font-size: $h2-font-size;
padding: .25rem 0;
}
}

23
resources/sass/app.scss Normal file
View File

@ -0,0 +1,23 @@
@import "@fortawesome/fontawesome-free/css/all.css";
// Variables
@import "./tailwind/variables/colors";
@import "./admin/variables";
@import "./admin/variables-dark";
// Bootstrap
@import "bootstrap/scss/bootstrap";
@import "./quill";
// Other
@import "./admin/misc";
@import "./admin/layout/layout";
@import "./admin/layout/containers";
@import "./admin/layout/layout-nav-default";
@import "./admin/layout/layout-nav-mobile";
@import "./admin/components/nav";
@import "./admin/components/tables";
@import "./admin/components/snackbar";

View File

@ -0,0 +1,471 @@
// Styles shared between snow and bubble
$controlHeight: 24px !default;
$inputPaddingWidth: 5px !default;
$inputPaddingHeight: 3px !default;
$colorItemMargin: 2px !default;
$colorItemSize: 16px !default;
$colorItemsPerRow: 7 !default;
.ql-#{$themeName}.ql-toolbar,
.ql-#{$themeName} .ql-toolbar {
&:after {
clear: both;
content: '';
display: table;
}
button {
background: none;
border: none;
cursor: pointer;
display: inline-block;
float: left;
height: $controlHeight;
padding: $inputPaddingHeight $inputPaddingWidth;
width: $controlHeight + ($inputPaddingWidth - $inputPaddingHeight) * 2;
svg {
float: left;
height: 100%;
}
&:active:hover {
outline: none;
}
}
input.ql-image[type='file'] {
display: none;
}
button:hover,
button:focus,
button.ql-active,
.ql-picker-label:hover,
.ql-picker-label.ql-active,
.ql-picker-item:hover,
.ql-picker-item.ql-selected {
color: var(--ql-active-color);
.ql-fill,
.ql-stroke.ql-fill {
fill: var(--ql-active-color);
}
.ql-stroke,
.ql-stroke-miter {
stroke: var(--ql-active-color);
}
}
}
// Fix for iOS not losing hover on touch
@media (pointer: coarse) {
.ql-#{$themeName}.ql-toolbar,
.ql-#{$themeName} .ql-toolbar {
button:hover:not(.ql-active) {
color: var(--ql-inactive-color);
.ql-fill,
.ql-stroke.ql-fill {
fill: var(--ql-inactive-color);
}
.ql-stroke,
.ql-stroke-miter {
stroke: var(--ql-inactive-color);
}
}
}
}
.ql-#{$themeName} {
box-sizing: border-box;
* {
box-sizing: border-box;
}
.ql-hidden {
display: none;
}
.ql-out-bottom,
.ql-out-top {
visibility: hidden;
}
.ql-tooltip {
position: absolute;
transform: translateY(10px);
a {
cursor: pointer;
text-decoration: none;
}
}
.ql-tooltip.ql-flip {
transform: translateY(-10px);
}
.ql-formats {
display: inline-block;
vertical-align: middle;
&:after {
clear: both;
content: '';
display: table;
}
}
.ql-stroke {
fill: none;
stroke: var(--ql-inactive-color);
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
}
.ql-stroke-miter {
fill: none;
stroke: var(--ql-inactive-color);
stroke-miterlimit: 10;
stroke-width: 2;
}
.ql-fill,
.ql-stroke.ql-fill {
fill: var(--ql-inactive-color);
}
.ql-empty {
fill: none;
}
.ql-even {
fill-rule: evenodd;
}
.ql-thin,
.ql-stroke.ql-thin {
stroke-width: 1;
}
.ql-transparent {
opacity: 0.4;
}
.ql-direction {
svg:last-child {
display: none;
}
}
.ql-direction.ql-active {
svg:last-child {
display: inline;
}
svg:first-child {
display: none;
}
}
.ql-picker {
color: var(--ql-inactive-color);
display: inline-block;
float: left;
font-size: 14px;
font-weight: 500;
height: $controlHeight;
position: relative;
vertical-align: middle;
&.ql-expanded {
.ql-picker-label {
color: var(--ql-border-color);
z-index: 2;
.ql-fill {
fill: var(--ql-border-color);
}
.ql-stroke {
stroke: var(--ql-border-color);
}
}
.ql-picker-options {
display: block;
margin-top: -1px;
top: 100%;
z-index: 1;
}
}
}
.ql-picker-label {
cursor: pointer;
display: inline-block;
height: 100%;
padding-left: 8px;
padding-right: 2px;
position: relative;
width: 100%;
&::before {
display: inline-block;
line-height: 22px;
}
}
.ql-picker-options {
background-color: var(--ql-background-color);
display: none;
min-width: 100%;
padding: 4px 8px;
position: absolute;
white-space: nowrap;
.ql-picker-item {
cursor: pointer;
display: block;
padding-bottom: 5px;
padding-top: 5px;
}
}
.ql-color-picker,
.ql-icon-picker {
width: $controlHeight + 4;
}
.ql-color-picker .ql-picker-label,
.ql-icon-picker .ql-picker-label {
padding: 2px 4px;
}
.ql-color-picker .ql-picker-label svg,
.ql-icon-picker .ql-picker-label svg {
right: 4px;
}
.ql-icon-picker {
.ql-picker-options {
padding: 4px 0px;
}
.ql-picker-item {
height: $controlHeight;
width: $controlHeight;
padding: 2px 4px;
}
}
.ql-color-picker {
.ql-picker-options {
padding: $inputPaddingHeight $inputPaddingWidth;
width:
($colorItemSize + 2 * $colorItemMargin) *
$colorItemsPerRow + 2 * $inputPaddingWidth + 2
;
}
.ql-picker-item {
border: 1px solid transparent;
float: left;
height: $colorItemSize;
margin: $colorItemMargin;
padding: 0px;
width: $colorItemSize;
}
}
.ql-picker {
&:not(.ql-color-picker):not(.ql-icon-picker) {
svg {
position: absolute;
margin-top: -9px;
right: 0;
top: 50%;
width: 18px;
}
}
}
.ql-picker.ql-header,
.ql-picker.ql-font,
.ql-picker.ql-size {
.ql-picker-label[data-label]:not([data-label='']),
.ql-picker-item[data-label]:not([data-label='']) {
&::before {
content: attr(data-label);
}
}
}
.ql-picker.ql-header {
width: 98px;
.ql-picker-label::before,
.ql-picker-item::before {
content: 'Normal';
}
@for $num from 1 through 3 {
.ql-picker-label[data-value="#{$num}"]::before,
.ql-picker-item[data-value="#{$num}"]::before {
content: 'Heading #{$num}';
}
}
.ql-picker-item[data-value='1']::before {
font-size: 2em;
}
.ql-picker-item[data-value='2']::before {
font-size: 1.5em;
}
.ql-picker-item[data-value='3']::before {
font-size: 1.17em;
}
.ql-picker-item[data-value='4']::before {
font-size: 1em;
}
.ql-picker-item[data-value='5']::before {
font-size: 0.83em;
}
.ql-picker-item[data-value='6']::before {
font-size: 0.67em;
}
}
.ql-picker.ql-font {
width: 108px;
.ql-picker-label::before,
.ql-picker-item::before {
content: 'Sans Serif';
}
.ql-picker-label[data-value='serif']::before,
.ql-picker-item[data-value='serif']::before {
content: 'Serif';
}
.ql-picker-label[data-value='monospace']::before,
.ql-picker-item[data-value='monospace']::before {
content: 'Monospace';
}
.ql-picker-item[data-value='serif']::before {
font-family: Georgia, Times New Roman, serif;
}
.ql-picker-item[data-value='monospace']::before {
font-family: Monaco, Courier New, monospace;
}
}
.ql-picker.ql-size {
width: 98px;
.ql-picker-label::before,
.ql-picker-item::before {
content: 'Normal';
}
.ql-picker-label[data-value='small']::before,
.ql-picker-item[data-value='small']::before {
content: 'Small';
}
.ql-picker-label[data-value='large']::before,
.ql-picker-item[data-value='large']::before {
content: 'Large';
}
.ql-picker-label[data-value='huge']::before,
.ql-picker-item[data-value='huge']::before {
content: 'Huge';
}
.ql-picker-item[data-value='small']::before {
font-size: 10px;
}
.ql-picker-item[data-value='large']::before {
font-size: 18px;
}
.ql-picker-item[data-value='huge']::before {
font-size: 32px;
}
}
.ql-color-picker.ql-background {
.ql-picker-item {
background-color: #fff;
}
}
.ql-color-picker.ql-color {
.ql-picker-item {
background-color: #000;
}
}
}
.ql-code-block-container {
position: relative;
.ql-ui {
right: 5px;
top: 5px;
}
}
.ql-editor, .quill-render {
// h1 {
// font-size: 2em;
// }
// h2 {
// font-size: 1.5em;
// }
// h3 {
// font-size: 1.17em;
// }
// h4 {
// font-size: 1em;
// }
// h5 {
// font-size: 0.83em;
// }
// h6 {
// font-size: 0.67em;
// }
// a {
// text-decoration: underline;
// }
blockquote {
border-left: 4px solid #ccc;
margin-bottom: 5px;
margin-top: 5px;
padding-left: 16px;
}
code,
.ql-code-block-container {
background-color: #f0f0f0;
border-radius: 3px;
}
.ql-code-block-container {
margin-bottom: 5px;
margin-top: 5px;
padding: 5px 10px;
background-color: #23241f;
color: #f8f8f2;
overflow: visible;
}
code {
font-size: 85%;
padding: 2px 4px;
}
img {
max-width: 100%;
}
}

View File

@ -0,0 +1,312 @@
// Styles necessary for Quill
$LIST_STYLE: (decimal, lower-alpha, lower-roman) !default;
$LIST_STYLE_WIDTH: 1.2em !default;
$LIST_STYLE_MARGIN: 0.3em !default;
$LIST_STYLE_OUTER_WIDTH: $LIST_STYLE_MARGIN + $LIST_STYLE_WIDTH !default;
$MAX_INDENT: 9 !default;
@function resets($from, $to) {
$list: (unquote('')); // sass wont allow empty "()" property list
@for $num from $from through $to {
$list: append($list, unquote('list-' + $num), space);
}
@return $list;
}
.ql-container {
box-sizing: border-box;
// font-family: Helvetica, Arial, sans-serif;
// font-size: 13px;
height: 100%;
margin: 0px;
position: relative;
}
.ql-container.ql-disabled {
.ql-tooltip {
visibility: hidden;
}
}
.ql-container:not(.ql-disabled) {
li[data-list='checked'],
li[data-list='unchecked'] {
> .ql-ui {
cursor: pointer;
}
}
}
.ql-clipboard {
left: -100000px;
height: 1px;
overflow-y: hidden;
position: absolute;
top: 50%;
p {
margin: 0;
padding: 0;
}
}
.ql-editor {
box-sizing: border-box;
counter-reset: resets(0, $MAX_INDENT);
line-height: 1.42;
height: 100%;
outline: none;
overflow-y: auto;
padding: 12px 15px;
tab-size: 4;
-moz-tab-size: 4;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
.ql-editor, .quill-render {
> * {
cursor: text;
}
// p,
// ol,
// pre,
// blockquote,
// h1,
// h2,
// h3,
// h4,
// h5,
// h6 {
// margin: 0;
// padding: 0;
// }
p,
h1,
h2,
h3,
h4,
h5,
h6 {
@supports (counter-set: none) {
counter-set: resets(0, $MAX_INDENT);
}
@supports not (counter-set: none) {
counter-reset: resets(0, $MAX_INDENT);
}
}
table {
@extend .table;
@extend .table-bordered;
@extend .table-sm;
}
ol {
padding-left: 1.5em;
}
li {
list-style-type: none;
padding-left: $LIST_STYLE_OUTER_WIDTH;
position: relative;
> .ql-ui:before {
display: inline-block;
margin-left: -1 * $LIST_STYLE_OUTER_WIDTH;
margin-right: $LIST_STYLE_MARGIN;
text-align: right;
white-space: nowrap;
width: $LIST_STYLE_WIDTH;
}
}
li[data-list='checked'],
li[data-list='unchecked'] {
> .ql-ui {
color: #777;
}
}
li[data-list='bullet'] > .ql-ui:before {
content: '\2022';
}
li[data-list='checked'] > .ql-ui:before {
content: '\2611';
}
li[data-list='unchecked'] > .ql-ui:before {
content: '\2610';
}
li[data-list] {
@supports (counter-set: none) {
counter-set: resets(1, $MAX_INDENT);
}
@supports not (counter-set: none) {
counter-reset: resets(1, $MAX_INDENT);
}
}
li[data-list='ordered'] {
counter-increment: list-0;
> .ql-ui:before {
content: unquote('counter(list-0, ' + nth($LIST_STYLE, 1) + ')') '. '; // indexs stars with 1
}
}
@for $num from 1 through $MAX_INDENT {
li[data-list='ordered'].ql-indent-#{$num} {
counter-increment: unquote('list-' + $num);
> .ql-ui:before {
content: unquote(
'counter(list-' + $num + ', ' + nth($LIST_STYLE, 1 + ($num % 3)) + ')'
)
'. ';
}
}
@if $num < $MAX_INDENT {
li[data-list].ql-indent-#{$num} {
@supports (counter-set: none) {
counter-set: resets(($num + 1), $MAX_INDENT);
}
@supports not (counter-set: none) {
counter-reset: resets(($num + 1), $MAX_INDENT);
}
}
}
}
@for $num from 1 through $MAX_INDENT {
.ql-indent-#{$num}:not(.ql-direction-rtl) {
padding-left: #{3 * $num}em;
}
li.ql-indent-#{$num}:not(.ql-direction-rtl) {
padding-left: #{(3 * $num + $LIST_STYLE_OUTER_WIDTH)};
}
.ql-indent-#{$num}.ql-direction-rtl.ql-align-right {
padding-right: #{(3 * $num)}em;
}
li.ql-indent-#{$num}.ql-direction-rtl.ql-align-right {
padding-right: #{(3 * $num + $LIST_STYLE_OUTER_WIDTH)};
}
}
li.ql-direction-rtl {
padding-right: $LIST_STYLE_OUTER_WIDTH;
> .ql-ui:before {
margin-left: $LIST_STYLE_MARGIN;
margin-right: -1 * $LIST_STYLE_OUTER_WIDTH;
text-align: left;
}
}
table {
table-layout: fixed;
width: 100%;
td {
outline: none;
}
}
.ql-code-block-container {
font-family: monospace;
}
.ql-video {
display: block;
max-width: 100%;
}
.ql-video.ql-align-center {
margin: 0 auto;
}
.ql-video.ql-align-right {
margin: 0 0 0 auto;
}
.ql-bg-black {
background-color: rgb(0, 0, 0);
}
.ql-bg-red {
background-color: rgb(230, 0, 0);
}
.ql-bg-orange {
background-color: rgb(255, 153, 0);
}
.ql-bg-yellow {
background-color: rgb(255, 255, 0);
}
.ql-bg-green {
background-color: rgb(0, 138, 0);
}
.ql-bg-blue {
background-color: rgb(0, 102, 204);
}
.ql-bg-purple {
background-color: rgb(153, 51, 255);
}
.ql-color-white {
color: rgb(255, 255, 255);
}
.ql-color-red {
color: rgb(230, 0, 0);
}
.ql-color-orange {
color: rgb(255, 153, 0);
}
.ql-color-yellow {
color: rgb(255, 255, 0);
}
.ql-color-green {
color: rgb(0, 138, 0);
}
.ql-color-blue {
color: rgb(0, 102, 204);
}
.ql-color-purple {
color: rgb(153, 51, 255);
}
.ql-font-serif {
font-family: Georgia, Times New Roman, serif;
}
.ql-font-monospace {
font-family: Monaco, Courier New, monospace;
}
.ql-size-small {
font-size: 0.75em;
}
.ql-size-large {
font-size: 1.5em;
}
.ql-size-huge {
font-size: 2.5em;
}
.ql-direction-rtl {
direction: rtl;
text-align: inherit;
}
.ql-align-center {
text-align: center;
}
.ql-align-justify {
text-align: justify;
}
.ql-align-right {
text-align: right;
}
.ql-ui {
position: absolute;
}
}

View File

@ -0,0 +1,27 @@
$themeName: 'snow';
$tooltipMargin: 8px;
:root {
--ql-active-color: var(--bs-body-color);
--ql-border-color: var(--bs-border-color);
--ql-background-color: var(--bs-body-bg);
--ql-inactive-color: var(--bs-secondary);
--ql-shadow-color: #ddd;
--ql-text-color: var(--bs-body-color);
--ql-tooltip-background: var(--bs-body-bg);
}
@import 'core.scss';
@import 'base';
@import 'snow/toolbar';
@import 'snow/tooltip';
// .ql-snow {
// a {
// color: var(--ql-active-color);
// }
// }
.ql-container.ql-snow {
border: 1px solid var(--ql-border-color);
}

View File

@ -0,0 +1,39 @@
.ql-toolbar.ql-snow {
border: 1px solid var(--ql-border-color);
box-sizing: border-box;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
padding: 8px;
.ql-formats {
margin-right: 15px;
}
.ql-picker-label {
border: 1px solid transparent;
}
.ql-picker-options {
border: 1px solid transparent;
box-shadow: rgba(0, 0, 0, 0.2) 0 2px 8px;
}
.ql-picker.ql-expanded {
.ql-picker-label {
border-color: var(--ql-border-color);
}
.ql-picker-options {
border-color: var(--ql-border-color);
}
}
.ql-color-picker {
.ql-picker-item.ql-selected,
.ql-picker-item:hover {
border-color: var(--ql-border-color-active);
}
}
}
.ql-toolbar.ql-snow + .ql-container.ql-snow {
border-top: 0px;
}

View File

@ -0,0 +1,79 @@
.ql-snow {
.ql-tooltip {
background-color: var(--ql-tooltip-background);
border: 1px solid var(--ql-border-color);
box-shadow: 0px 0px 5px var(--ql-shadow-color);
color: var(--ql-text-color);
padding: 5px 12px;
white-space: nowrap;
&::before {
content: 'Visit URL:';
line-height: 26px;
margin-right: $tooltipMargin;
}
input[type='text'] {
display: none;
border: 1px solid var(--ql-border-color);
font-size: 13px;
height: 26px;
margin: 0px;
padding: 3px 5px;
width: 170px;
}
a {
line-height: 26px;
}
a.ql-preview {
display: inline-block;
max-width: $tooltipMargin * 2;
overflow-x: hidden;
text-overflow: ellipsis;
vertical-align: top;
}
a.ql-action::after {
border-right: 1px solid var(--ql-border-color);
content: 'Edit';
margin-left: $tooltipMargin * 2;
padding-right: $tooltipMargin;
}
a.ql-remove::before {
content: 'Remove';
margin-left: $tooltipMargin;
}
}
.ql-tooltip.ql-editing {
a.ql-preview,
a.ql-remove {
display: none;
}
input[type='text'] {
display: inline-block;
}
a.ql-action::after {
border-right: 0px;
content: 'Save';
padding-right: 0px;
}
}
.ql-tooltip[data-mode='link']::before {
content: 'Enter link:';
}
.ql-tooltip[data-mode='formula']::before {
content: 'Enter formula:';
}
.ql-tooltip[data-mode='video']::before {
content: 'Enter video:';
}
}

View File

@ -0,0 +1,263 @@
$tw-slate-050: #f8fafc;
$tw-slate-100: #f1f5f9;
$tw-slate-200: #e2e8f0;
$tw-slate-300: #cbd5e1;
$tw-slate-400: #94a3b8;
$tw-slate-500: #64748b;
$tw-slate-600: #475569;
$tw-slate-700: #334155;
$tw-slate-800: #1e293b;
$tw-slate-900: #0f172a;
$tw-slate-950: #020617;
$tw-gray-050: #f9fafb;
$tw-gray-100: #f3f4f6;
$tw-gray-200: #e5e7eb;
$tw-gray-300: #d1d5db;
$tw-gray-400: #9ca3af;
$tw-gray-500: #6b7280;
$tw-gray-600: #4b5563;
$tw-gray-700: #374151;
$tw-gray-800: #1f2937;
$tw-gray-900: #111827;
$tw-gray-950: #030712;
$tw-zinc-050: #fafafa;
$tw-zinc-100: #f4f4f5;
$tw-zinc-200: #e4e4e7;
$tw-zinc-300: #d4d4d8;
$tw-zinc-400: #a1a1aa;
$tw-zinc-500: #71717a;
$tw-zinc-600: #52525b;
$tw-zinc-700: #3f3f46;
$tw-zinc-800: #27272a;
$tw-zinc-900: #18181b;
$tw-zinc-950: #09090b;
$tw-neutral-050: #fafafa;
$tw-neutral-100: #f5f5f5;
$tw-neutral-200: #e5e5e5;
$tw-neutral-300: #d4d4d4;
$tw-neutral-400: #a3a3a3;
$tw-neutral-500: #737373;
$tw-neutral-600: #525252;
$tw-neutral-700: #404040;
$tw-neutral-800: #262626;
$tw-neutral-900: #171717;
$tw-neutral-950: #0a0a0a;
$tw-stone-050: #fafaf9;
$tw-stone-100: #f5f5f4;
$tw-stone-200: #e7e5e4;
$tw-stone-300: #d6d3d1;
$tw-stone-400: #a8a29e;
$tw-stone-500: #78716c;
$tw-stone-600: #57534e;
$tw-stone-700: #44403c;
$tw-stone-800: #292524;
$tw-stone-900: #1c1917;
$tw-stone-950: #0c0a09;
$tw-red-050: #fef2f2;
$tw-red-100: #fee2e2;
$tw-red-200: #fecaca;
$tw-red-300: #fca5a5;
$tw-red-400: #f87171;
$tw-red-500: #ef4444;
$tw-red-600: #dc2626;
$tw-red-700: #b91c1c;
$tw-red-800: #991b1b;
$tw-red-900: #7f1d1d;
$tw-red-950: #450a0a;
$tw-orange-050: #fff7ed;
$tw-orange-100: #ffedd5;
$tw-orange-200: #fed7aa;
$tw-orange-300: #fdba74;
$tw-orange-400: #fb923c;
$tw-orange-500: #f97316;
$tw-orange-600: #ea580c;
$tw-orange-700: #c2410c;
$tw-orange-800: #9a3412;
$tw-orange-900: #7c2d12;
$tw-orange-950: #431407;
$tw-amber-050: #fffbeb;
$tw-amber-100: #fef3c7;
$tw-amber-200: #fde68a;
$tw-amber-300: #fcd34d;
$tw-amber-400: #fbbf24;
$tw-amber-500: #f59e0b;
$tw-amber-600: #d97706;
$tw-amber-700: #b45309;
$tw-amber-800: #92400e;
$tw-amber-900: #78350f;
$tw-amber-950: #451a03;
$tw-yellow-050: #fefce8;
$tw-yellow-100: #fef9c3;
$tw-yellow-200: #fef08a;
$tw-yellow-300: #fde047;
$tw-yellow-400: #facc15;
$tw-yellow-500: #eab308;
$tw-yellow-600: #ca8a04;
$tw-yellow-700: #a16207;
$tw-yellow-800: #854d0e;
$tw-yellow-900: #713f12;
$tw-yellow-950: #422006;
$tw-lime-050: #f7fee7;
$tw-lime-100: #ecfccb;
$tw-lime-200: #d9f99d;
$tw-lime-300: #bef264;
$tw-lime-400: #a3e635;
$tw-lime-500: #84cc16;
$tw-lime-600: #65a30d;
$tw-lime-700: #4d7c0f;
$tw-lime-800: #3f6212;
$tw-lime-900: #365314;
$tw-lime-950: #1a2e05;
$tw-green-050: #f0fdf4;
$tw-green-100: #dcfce7;
$tw-green-200: #bbf7d0;
$tw-green-300: #86efac;
$tw-green-400: #4ade80;
$tw-green-500: #22c55e;
$tw-green-600: #16a34a;
$tw-green-700: #15803d;
$tw-green-800: #166534;
$tw-green-900: #14532d;
$tw-green-950: #052e16;
$tw-emerald-050: #ecfdf5;
$tw-emerald-100: #d1fae5;
$tw-emerald-200: #a7f3d0;
$tw-emerald-300: #6ee7b7;
$tw-emerald-400: #34d399;
$tw-emerald-500: #10b981;
$tw-emerald-600: #059669;
$tw-emerald-700: #047857;
$tw-emerald-800: #065f46;
$tw-emerald-900: #064e3b;
$tw-emerald-950: #022c22;
$tw-teal-050: #f0fdfa;
$tw-teal-100: #ccfbf1;
$tw-teal-200: #99f6e4;
$tw-teal-300: #5eead4;
$tw-teal-400: #2dd4bf;
$tw-teal-500: #14b8a6;
$tw-teal-600: #0d9488;
$tw-teal-700: #0f766e;
$tw-teal-800: #115e59;
$tw-teal-900: #134e4a;
$tw-teal-950: #042f2e;
$tw-cyan-050: #ecfeff;
$tw-cyan-100: #cffafe;
$tw-cyan-200: #a5f3fc;
$tw-cyan-300: #67e8f9;
$tw-cyan-400: #22d3ee;
$tw-cyan-500: #06b6d4;
$tw-cyan-600: #0891b2;
$tw-cyan-700: #0e7490;
$tw-cyan-800: #155e75;
$tw-cyan-900: #164e63;
$tw-cyan-950: #083344;
$tw-sky-050: #f0f9ff;
$tw-sky-100: #e0f2fe;
$tw-sky-200: #bae6fd;
$tw-sky-300: #7dd3fc;
$tw-sky-400: #38bdf8;
$tw-sky-500: #0ea5e9;
$tw-sky-600: #0284c7;
$tw-sky-700: #0369a1;
$tw-sky-800: #075985;
$tw-sky-900: #0c4a6e;
$tw-sky-950: #083344;
$tw-blue-050: #eff6ff;
$tw-blue-100: #dbeafe;
$tw-blue-200: #bfdbfe;
$tw-blue-300: #93c5fd;
$tw-blue-400: #60a5fa;
$tw-blue-500: #3b82f6;
$tw-blue-600: #2563eb;
$tw-blue-700: #1d4ed8;
$tw-blue-800: #1e40af;
$tw-blue-900: #1e3a8a;
$tw-blue-950: #172554;
$tw-indigo-050: #eef2ff;
$tw-indigo-100: #e0e7ff;
$tw-indigo-200: #c7d2fe;
$tw-indigo-300: #a5b4fc;
$tw-indigo-400: #818cf8;
$tw-indigo-500: #6366f1;
$tw-indigo-600: #4f46e5;
$tw-indigo-700: #4338ca;
$tw-indigo-800: #3730a3;
$tw-indigo-900: #312e81;
$tw-indigo-950: #1e1b4b;
$tw-violet-050: #f5f3ff;
$tw-violet-100: #ede9fe;
$tw-violet-200: #ddd6fe;
$tw-violet-300: #c4b5fd;
$tw-violet-400: #a78bfa;
$tw-violet-500: #8b5cf6;
$tw-violet-600: #7c3aed;
$tw-violet-700: #6d28d9;
$tw-violet-800: #5b21b6;
$tw-violet-900: #4c1d95;
$tw-violet-950: #2e1065;
$tw-purple-050: #faf5ff;
$tw-purple-100: #f3e8ff;
$tw-purple-200: #e9d5ff;
$tw-purple-300: #d8b4fe;
$tw-purple-400: #c084fc;
$tw-purple-500: #a855f7;
$tw-purple-600: #9333ea;
$tw-purple-700: #7e22ce;
$tw-purple-800: #6b21a8;
$tw-purple-900: #581c87;
$tw-purple-950: #3b0764;
$tw-fuchsia-050: #fdf4ff;
$tw-fuchsia-100: #fae8ff;
$tw-fuchsia-200: #f5d0fe;
$tw-fuchsia-300: #f0abfc;
$tw-fuchsia-400: #e879f9;
$tw-fuchsia-500: #d946ef;
$tw-fuchsia-600: #c026d3;
$tw-fuchsia-700: #a21caf;
$tw-fuchsia-800: #86198f;
$tw-fuchsia-900: #701a75;
$tw-fuchsia-950: #4a044e;
$tw-pink-050: #fdf2f8;
$tw-pink-100: #fce7f3;
$tw-pink-200: #fbcfe8;
$tw-pink-300: #f9a8d4;
$tw-pink-400: #f472b6;
$tw-pink-500: #ec4899;
$tw-pink-600: #db2777;
$tw-pink-700: #be185d;
$tw-pink-800: #9d174d;
$tw-pink-900: #831843;
$tw-pink-950: #500724;
$tw-rose-050: #fff1f2;
$tw-rose-100: #ffe4e6;
$tw-rose-200: #fecdd3;
$tw-rose-300: #fda4af;
$tw-rose-400: #fb7185;
$tw-rose-500: #f43f5e;
$tw-rose-600: #e11d48;
$tw-rose-700: #be123c;
$tw-rose-800: #9f1239;
$tw-rose-900: #881337;
$tw-rose-950: #4c0519;

View File

@ -1,34 +1,21 @@
<form method="POST"action="{{ route('login.submit') }}">
@csrf
<x-layout-auth>
<x-form::form method="POST" action="{{ route('login.submit') }}">
<x-form::input class="mb-3" type="email" name="email" id="email" label="{{ __('boilerplate::auth.email') }}:" />
<x-form::input class="mb-3" type="password" name="password" id="password" label="{{ __('boilerplate::auth.password') }}:" />
<x-form::checkbox name="remmeber" id="remmeber" label="{{ __('boilerplate::auth.remember') }}:" />
<label for="email">{{ __('Email') }}:</label><br>
<input type="email" id="email" name="email" placeholder="email@post.xx"><br>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<label for="password">{{ __('Password') }}:</label><br>
<input type="password" id="password" name="password"><br>
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<input type="checkbox" id="remember" name="remember"><br>
@if (Route::has('password'))
<a href="{{ route('password') }}">
{{ __('Password Reset') }} ?
</a><br>
@endif
<input type="submit" value="{{ __('Login') }}">
@if (Route::has('register'))
<a href="{{ route('register') }}">
{{ __('Register') }} ?
</a>
@endif
</form>
<div class="d-flex">
<x-form::button class="btn-primary" type="submit">{{ __('boilerplate::auth.login') }}</x-form::button>
@if (Route::has('register'))
<a class="ms-2 btn btn-primary" href="{{ route('register') }}">
{{ __('boilerplate::auth.register') }}
</a>
@endif
@if (Route::has('password'))
<a class="ms-auto text-nowrap" href="{{ route('password') }}">
{{ __('boilerplate::auth.password_reset') }}
</a>
@endif
</div>
</x-form::form>
</x-layout-auth>

View File

@ -0,0 +1,61 @@
<x-layout-app>
<div class="container-xl">
<div class="page-header">
<h1>{{ __('boilerplate::ui.profile') }}</h1>
</div>
<div class="form-group">
<label class="form-label"><b>{{ __('boilerplate::ui.name') }}:</b></label>
<p>{{ $user->name ?? '' }}</p>
</div>
<div class="form-group">
<label class="form-label"><b>{{ __('boilerplate::ui.email') }}:</b></label>
<p>{{ $user->email ?? '' }}</p>
</div>
@if (config('session.driver') == 'database')
<div>
<h4>{{ __('boilerplate::ui.sessions') }}</h4>
@livewire('session.data-table', [], key('data-table'))
</div>
@endif
<div>
<h4>{{ __('boilerplate::ui.change_password') }}</h4>
</div>
<form action="{{ route('profile.update', ['user' => $user]) }}" method="POST">
@csrf
@method('put')
<div class="form-group">
<label class="form-label" for="password">{{ __('boilerplate::ui.old.password') }}</label>
<input class="form-control @error('password') is-invalid @enderror" id="password" name="password" type="password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<div class="form-group">
<label class="form-label" for="newPassword">{{ __('boilerplate::ui.new.password') }}</label>
<input class="form-control @error('newPassword') is-invalid @enderror" id="newPassword" name="newPassword" type="password">
@error('newPassword')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<div class="form-group mb-3">
<label class="form-label" for="newPassword-confirm">{{ __('boilerplate::ui.confirm.password') }}</label>
<input class="form-control" id="newPassword-confirm" name="newPassword_confirmation" type="password">
</div>
<button class="btn btn-primary" type="submit">{{ __('boilerplate::ui.update') }}</button>
</form>
</div>
</x-layout-app>

View File

@ -0,0 +1,69 @@
<x-layout-app>
<div class="container-xl">
<div class="page-header">
<h1>{{ __('boilerplate::ui.api_tokens') }}</h1>
<div>
@if (session()->has('secret'))
<code>{{ session()->get('secret') }}</code>
@else
<form action="{{ route('profile.api.create') }}" class="row row-cols-lg-auto g-3 align-items-center mb-3" method="post">
@csrf
<div class="col-12">
<label class="visually-hidden" for="token-name">{{ __('boilerplate::ui.name') }}</label>
<div class="input-group">
<input class="form-control" id="token-name" name="token_name" placeholder="{{ __('boilerplate::ui.name') }}" type="text">
</div>
</div>
<x-form::input class="form-control" type="date" name="expire_at" id="expire_at" min="{{ now()->toDateString('Y-m-d') }}" placeholder="{{ __('boilerplate::ui.expire_at') }}" />
<div class="col-12">
<button class="btn btn-primary" type="submit">{{ __('boilerplate::ui.create') }}</button>
</div>
<div class="col-12">
@error('token_name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<div class="col-12">
@error('expire_at')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</form>
@endif
</div>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th scope="col">{{ __('boilerplate::ui.name') }}</th>
<th scope="col">{{ __('boilerplate::ui.last_used_at') }}</th>
<th scope="col">{{ __('boilerplate::ui.expire_at') }}</th>
<th scope="col">{{ __('boilerplate::ui.actions') }}</th>
</tr>
</thead>
<tbody>
@foreach ($tokens as $token)
<tr>
<th scope="row">{{ $token->name }}</th>
<td>{{ $token->last_used_at ?? __('boilerplate::ui.never') }}</td>
<td>{{ $token->expire_at ?? __('boilerplate::ui.never') }}</td>
<td>
<form action="{{ route('profile.api.remove', ['token_id' => $token->id]) }}" method="post">
@method('DELETE')
@csrf
<input class="btn btn-danger" type="submit" value="{{ __('boilerplate::ui.remove') }}" />
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</x-layout-app>

View File

@ -1,37 +1,17 @@
<form method="POST"action="{{ route('register.submit') }}">
@csrf
<x-layout-auth>
<x-form::form method="POST" action="{{ route('register.submit') }}">
<x-form::input class="mb-3" type="text" name="name" id="name" label="{{ __('boilerplate::auth.name') }}:" placeholder="JohnDoe" />
<x-form::input class="mb-3" type="email" name="email" id="email" label="{{ __('boilerplate::auth.email') }}:" placeholder="email@post.xx" />
<x-form::input class="mb-3" type="password" name="password" id="password" label="{{ __('boilerplate::auth.password') }}:" />
<x-form::input class="mb-3" type="password" name="password_confirmation" id="password_confirmation" label="{{ __('boilerplate::auth.password_confirm') }}:" />
<label for="name">{{ __('Name') }}:</label><br>
<input type="text" id="name" name="name" placeholder="JohnDoe"><br>
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<label for="email">{{ __('Email') }}:</label><br>
<input type="email" id="email" name="email" placeholder="email@post.xx"><br>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<label for="password">{{ __('Password') }}:</label><br>
<input type="password" id="password" name="password"><br>
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<label for="password_confirmation">{{ __('Confirm Password') }}:</label><br>
<input type="password" id="password_confirmation" name="password_confirmation"><br>
@error('password_confirmation')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<input type="submit" value="{{ __('Register') }}">
</form>
<div class="d-flex">
<x-form::button class="p-2 btn-primary" type="submit">{{ __('boilerplate::auth.register') }}</x-form::button>
@if (Route::has('login'))
<a class="ms-auto text-nowrap" href="{{ route('login') }}">
{{ __('boilerplate::auth.login') }}
</a>
@endif
</div>
</x-form::form>
</x-layout-auth>

View File

@ -1,42 +1,21 @@
<form method="POST"action="{{ (isset($token) ? route('password.update') : route('password.email')) }}">
@csrf
<x-layout-auth>
<x-form::form method="POST" action="{{ isset($token) ? route('password.update') : route('password.email') }}">
@if (isset($token))
<x-form::input type="hidden" id="token" name="token" value="{{ $token }}" />
<x-form::input class="mb-3" type="email" id="email" name="email" label="{{ __('boilerplate::auth.email') }}" value="{{ $email }}" />
<x-form::input class="mb-3" type="password" id="password" name="password" label="{{ __('Password') }}" />
<x-form::input class="mb-3" type="password" id="password_confirmation" name="password_confirmation" label="{{ __('boilerplate::auth.password_confirm') }}" />
@else
<x-form::input class="mb-3" type="email" id="email" name="email" label="{{ __('boilerplate::auth.email') }}" placeholder="email@post.xx" required/>
@endif
@if (isset($token))
<input type="hidden" name="token" value="{{ $token }}">
<label for="email">{{ __('Email') }}:</label><br>
<input type="email" id="email" name="email" placeholder="email@post.xx" value="{{ $email }}"><br>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<label for="password">{{ __('Password') }}:</label><br>
<input type="password" id="password" name="password" value="{{ $password ?? old('password') }}"><br>
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
<label for="password_confirmation">{{ __('Confirm Password') }}:</label><br>
<input type="password" id="password_confirmation" name="password_confirmation"
value="{{ $password_confirmation ?? old('password_confirmation') }}"><br>
@error('password_confirmation')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
@else
<label for="email">{{ __('Email') }}:</label><br>
<input type="email" id="email" name="email" placeholder="email@post.xx" value="{{ old('email') }}"><br>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong><br>
</span>
@enderror
@endif
<input type="submit" value="{{ __('Send Password Reset Link') }}">
</form>
<div class="d-flex">
<x-form::button class="btn-primary" type="submit">{{ __('boilerplate::auth.send_password_reset') }}</x-form::button>
@if (Route::has('login'))
<a class="ms-auto text-nowrap" href="{{ route('login') }}">
{{ __('boilerplate::auth.login') }} ?
</a>
@endif
</div>
</x-form::form>
</x-layout-auth>

View File

@ -0,0 +1,26 @@
<x-layout-auth>
<div class="container-xl">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('auth.Verify') }}</div>
<div class="card-body">
@if (session('resent'))
<div class="alert alert-success" role="alert">
{{ __('auth.sendEmail') }}
</div>
@endif
{{ __('auth.checkEmail') }}
{{ __('auth.notReceiveEmail') }},
<form class="d-inline" method="POST" action="{{ route('verification.resend') }}">
@csrf
<button type="submit" class="btn btn-link p-0 m-0 align-baseline">{{ __('auth.requestNew') }}</button>.
</form>
</div>
</div>
</div>
</div>
</div>
</x-layout-auth>

View File

@ -0,0 +1,26 @@
<div class="snackbar-container">
@foreach ($alerts as $alert)
<div class="snackbar alert">
<button type="button" class="btn-close close" data-bs-dismiss="alert"></button>
<div class="alert-content">
@switch($alert['type'])
@case('success')
<i class="alert-ico far fa-check-circle text-success"></i>
@break
@case('error')
<i class="alert-ico fas fa-times-circle text-danger"></i>
@break
@case('warning')
<i class="alert-ico fas fa-exclamation-triangle text-warning"></i>
@break
@default
<i class="alert-ico fas fa-info-circle text-info"></i>
@endswitch
<div>
<div class="alert-title">{{ $alert['message'] }}</div>
</div>
</div>
</div>
@endforeach
</div>

View File

@ -0,0 +1,133 @@
<!doctype html>
<html data-bs-theme="{{ Cookie::get('theme') }}" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<!-- CSRF Token -->
<meta content="{{ csrf_token() }}" name="csrf-token">
<title>{{ config('app.name', 'Laravel') }}</title>
{{-- <link href="{{ asset('/manifest.json') }}" rel="manifest"> --}}
<link href="{{ asset('/favicon.ico') }}" rel="shortcut icon" type="image/x-icon">
<!-- Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect">
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.20/dist/summernote-lite.min.css" rel="stylesheet">
<!-- Quill -->
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.0-rc.2/dist/quill.js"></script>
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.0-rc.2/dist/quill.snow.css" rel="stylesheet">
<style>
.quill-editor-wrap {
position: relative;
min-height: 9rem;
display: flex;
flex-direction: column;
}
.quill-editor-wrap textarea {
display: none;
}
.quill-editor {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.quill-editor .ql-editor {
flex-grow: 1;
}
.quill-loading {
position: absolute;
z-index: 10;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
display: flex;
border-radius: var(--bs-border-radius);
border: var(--bs-border-width) solid var(--bs-border-color);
}
.quill-editor.ready+.quill-loading {
display: none;
}
</style>
<script>
window.loadQuill = function() {
document.querySelectorAll('.quill-editor:not(.ready)').forEach(function(element) {
let textarea = element.closest('.quill-container').querySelector('.quill-textarea');
let quill = new Quill(element, {
theme: 'snow'
});
quill.root.innerHTML = textarea.value;
quill.on('text-change', function() {
let value = quill.root.innerHTML;
textarea.value = value;
textarea.dispatchEvent(new Event('input'));
});
element.classList.add('ready');
element.closest('.quill-container').querySelector('.quill-loading').remove();
});
}
window.loadQuill();
</script>
<!-- Scripts -->
@livewireStyles
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
{{-- <script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('{{ asset('/service-worker.js') }}');
});
}
</script> --}}
</head>
<body>
<div id="app">
@auth
@include('partials.navbar')
@endauth
<div class="layout">
<x-navigation />
@include('partials.navigation-mobile')
<div class="layout-content">
<div class="content">
{{ $slot }}
</div>
</div>
</div>
</div>
<x-alerts/>
@livewireScripts
@livewire('modal-basic', key('modal'))
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,58 @@
<!doctype html>
<html data-bs-theme="{{ Cookie::get('theme') }}" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<!-- CSRF Token -->
<meta content="{{ csrf_token() }}" name="csrf-token">
<title>{{ config('app.name', 'Laravel') }}</title>
<link href="{{ asset('/manifest.json') }}" rel="manifest">
<link href="{{ asset('/favicon.ico') }}" rel="shortcut icon" type="image/x-icon">
<!-- Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect">
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Scripts -->
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
<div id="app">
<div class="row g-0 h-100">
<div class="align-content-center col-md-6 d-none d-md-flex flex-column justify-content-center bg-body-tertiary">
<div class="container py-4 text-center">
<a href="{{ url('/') }}">
<img class="mb-4" src="{{ asset('storage/images/logo.png') }}">
</a>
<h1>{{ config('app.name', 'Laravel') }}</h1>
<p class="text-black-50">
{{ __('general.MetaDescription') }}
</p>
</div>
</div>
<div class="col-md-6 d-flex flex-column grid justify-content-center align-content-center">
<div class="container py-4 px-4">
<div class="d-md-none text-center mb-4">
<img src="{{ asset('storage/images/logo.png') }}" class="mb-4" width="64px" height="64px">
</div>
<x-alerts/>
<div class="row justify-content-center">
<div class="col-md-10 col-xl-6">
{{ $slot }}
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,128 @@
<div class="layout-nav">
<div class="dropdown mb-2">
<a class="app-nav-header mb-2" data-bs-toggle="dropdown">
<div class="app-nav-logo">
<img src="{{ asset('storage/images/logo.png') }}" width="32px" height="32px">
</div>
{{-- <div class="app-nav-logo random-bg-2">
SA
</div> --}}
<div class="app-nav-header-content">
<div class="fw-semibold">{{ config('app.name', 'Laravel') }}</div>
{{-- <div class="fw-semibold">Steelants</div> --}}
{{-- <small class="text-body-secondary">steelants.cz</small> --}}
</div>
<div class="ms-auto">
<svg class="bi bi-chevron-expand" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708m0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708" />
</svg>
</div>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#">
<div class="dropdown-img random-bg-2">
SA
</div>
{{-- <div class="dropdown-img">
<img src="{{ asset('storage/images/logo.png') }}" width="2rem" height="2rem">
</div> --}}
<div class="lh-1">
<div class="fw-semibold">Steelants</div>
<small class="text-body-secondary">1 member</small>
</div>
</a>
<a class="dropdown-item" href="#">
{{-- <div class="dropdown-img random-bg-3">
A
</div> --}}
<div class="dropdown-img">
<img src="{{ asset('storage/images/logo.png') }}" width="2rem" height="2rem">
</div>
<div class="lh-1">
<div class="fw-semibold">Anthill</div>
<small class="text-body-secondary">42 members</small>
</div>
</a>
</div>
</div>
<ul class="app-nav nav flex-column">
@foreach ($mainMenuItems as $item)
<li class="nav-item {{ ($item->isActive() || $item->isUse()) ? 'is-active' : '' }}">
<a class="nav-link" href="{{ route($item->route, $item->parameters) }}">
<i class="nav-link-ico {{ $item->icon }}"></i>
{{ __($item->title) }}
</a>
</li>
@endforeach
{{-- MAIN NAVIGATION ALL --}}
@auth
<li class="mt-4 text-body-secondary"><small>{{ __('boilerplate::ui.system') }}</small></li>
@foreach ($systemMenuItems as $item)
<li class="nav-item {{ ($item->isActive() || $item->isUse()) ? 'is-active' : '' }}">
<a class="nav-link" href="{{ route($item->route, $item->parameters) }}">
<i class="nav-link-ico {{ $item->icon }}"></i>
{{ __($item->title) }}
</a>
</li>
@endforeach
{{-- MAIN NAVIGATION SYSTEM --}}
@endauth
</ul>
<div class="mt-auto">
<div class="dropup">
<a class="app-nav-user" data-bs-toggle="dropdown">
<div class="app-nav-profile random-bg-1">
PS
</div>
<div class="app-nav-header-content">
<div class="fw-semibold">{{ Auth::user()->name }}</div>
<small class="text-body-secondary">{{ Auth::user()->email }}</small>
</div>
<div class="ms-auto">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots-vertical" viewBox="0 0 16 16">
<path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0" />
</svg>
</div>
</a>
<div class="dropdown-menu">
<div class="dropdown-header d-flex align-items-center">
<div class="fw-semibold me-auto">{{ Auth::user()->name }}</div>
<small class="text-body-tertiary">v1.23</small>
</div>
<a class="dropdown-item" href="{{ route('profile.index') }}">
<i class="dropdown-ico fas fa-user"></i>
{{ __('boilerplate::ui.profile') }}
</a>
<label class="dropdown-item">
<i class="dropdown-ico fas fa-moon"></i>
<div class="me-auto">{{ __('boilerplate::ui.dark_mode') }}</div>
<div class="form-switch ms-4">
<input {{ Cookie::get('theme') == 'dark' ? 'checked' : '' }} class="form-check-input me-0" id="datk-theme" onchange="toggleDatkTheme()" type="checkbox">
</div>
</label>
<a class="dropdown-item" href="{{ route('profile.api') }}">
<i class="dropdown-ico fas fa-server"></i>
{{ __('boilerplate::ui.api_tokens') }}
</a>
<hr class="dropdown-divider">
<a class="dropdown-item" href="{{ route('logout') }}" onclick="event.preventDefault();document.getElementById('logout-form').submit();">
<i class="dropdown-ico fas fa-sign-out-alt text-danger"></i>
{{ __('boilerplate::ui.logout') }}
</a>
<form action="{{ route('logout') }}" class="d-none" id="logout-form" method="POST">
@csrf
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,5 @@
<x-layout-auth>
<h1>404</h1>
<p>{{ __('boilerplate::ui.not_found') }}</p>
<a class="btn btn-primary" href="{{ url('/') }}">{{ __('boilerplate::ui.home') }}</a>
</x-layout-auth>

View File

@ -0,0 +1,5 @@
<x-layout-auth>
<h1>{{ $exception->getStatusCode() }}</h1>
<p>{{ $exception->getMessage() }}</p>
<a class="btn btn-primary" href="{{ url('/') }}">{{ __('boilerplate::ui.home') }}</a>
</x-layout-auth>

View File

@ -0,0 +1,5 @@
<x-layout-auth>
<h1>503</h1>
<p>{{ __('boilerplate::ui.maintenance') }}</p>
<a class="btn btn-primary" href="{{ url('/') }}">{{ __('boilerplate::ui.home') }}</a>
</x-layout-auth>

View File

@ -0,0 +1,5 @@
<x-layout-auth>
<h1>{{ $exception->getStatusCode() }}</h1>
<p>{{ $exception->getMessage() }}</p>
<a class="btn btn-primary" href="{{ url('/') }}">{{ __('boilerplate::ui.home') }}</a>
</x-layout-auth>

View File

@ -0,0 +1,189 @@
<x-layout-app>
<div class="container-xl">
<div class="page-header">
<h1>Welcolm Back !</h1>
<a class="btn btn-primary" href="{{ url('home') }}"><i class="fa fa-plus me-2"></i> Page Action</a>
</div>
</div>
<div class="container-xl">
<h1>head 1</h1>
<h2>head 2</h2>
<h3>head 3</h3>
<h4>head 4</h4>
<h5>head 5</h5>
<h6>head 6</h6>
<div class="my-4">
<h4>Tabs</h4>
<ul class="nav nav-tabs mb-4">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
</div>
<div class="my-4 d-flex">
<div class="app-nav-profile random-bg-1">
PS
</div>
<div class="app-nav-profile random-bg-2">
PS
</div>
<div class="app-nav-profile random-bg-3">
PS
</div>
<div class="app-nav-profile random-bg-4">
PS
</div>
<div class="app-nav-profile random-bg-5">
PS
</div>
<div class="app-nav-profile random-bg-6">
PS
</div>
</div>
<div class="my-4">
<h4>Buttons</h4>
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-light">Light</button>
<button type="button" class="btn btn-dark">Dark</button>
<button type="button" class="btn">Button</button>
<button type="button" class="btn btn-link">Link</button>
</div>
<div class="my-4">
<h4>Badges</h4>
<span class="badge text-bg-primary">Primary</span>
<span class="badge text-bg-secondary">Secondary</span>
<span class="badge text-bg-success">Success</span>
<span class="badge text-bg-danger">Danger</span>
<span class="badge text-bg-warning">Warning</span>
<span class="badge text-bg-info">Info</span>
<span class="badge text-bg-light">Light</span>
<span class="badge text-bg-dark">Dark</span>
<br>
<span class="badge rounded-pill text-bg-primary">Primary</span>
<span class="badge rounded-pill text-bg-secondary">Secondary</span>
<span class="badge rounded-pill text-bg-success">Success</span>
<span class="badge rounded-pill text-bg-danger">Danger</span>
<span class="badge rounded-pill text-bg-warning">Warning</span>
<span class="badge rounded-pill text-bg-info">Info</span>
<span class="badge rounded-pill text-bg-light">Light</span>
<span class="badge rounded-pill text-bg-dark">Dark</span>
<br>
</div>
<div class="my-4">
<h4>Breadcrumbs</h4>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item active" aria-current="page">Home</li>
</ol>
</nav>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">Library</li>
</ol>
</nav>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item"><a href="#">Library</a></li>
<li class="breadcrumb-item active" aria-current="page">Data</li>
</ol>
</nav>
</div>
<div class="my-4">
<h4>Alerts</h4>
<div class="alert alert-primary" role="alert">
A simple primary alert—check it out!
</div>
<div class="alert alert-secondary" role="alert">
A simple secondary alert—check it out!
</div>
<div class="alert alert-success" role="alert">
A simple success alert—check it out!
</div>
<div class="alert alert-danger" role="alert">
A simple danger alert—check it out!
</div>
<div class="alert alert-warning" role="alert">
A simple warning alert—check it out!
</div>
<div class="alert alert-info" role="alert">
A simple info alert—check it out!
</div>
<div class="alert alert-light" role="alert">
A simple light alert—check it out!
</div>
<div class="alert alert-dark" role="alert">
A simple dark alert—check it out!
</div>
</div>
<div class="my-4">
<h4>Pagination</h4>
<nav aria-label="...">
<ul class="pagination">
<li class="page-item disabled">
<a class="page-link">Previous</a>
</li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item active" aria-current="page">
<a class="page-link" href="#">2</a>
</li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
<div class="my-4">
<h4>Colors</h4>
<div class="text-bg-primary p-3">Primary with contrasting color</div>
<div class="text-bg-secondary p-3">Secondary with contrasting color</div>
<div class="text-bg-success p-3">Success with contrasting color</div>
<div class="text-bg-danger p-3">Danger with contrasting color</div>
<div class="text-bg-warning p-3">Warning with contrasting color</div>
<div class="text-bg-info p-3">Info with contrasting color</div>
<div class="text-bg-light p-3">Light with contrasting color</div>
<div class="text-bg-dark p-3">Dark with contrasting color</div>
</div>
</div>
</x-layout-app>

View File

@ -0,0 +1,13 @@
<x-layout-app>
<div class="container-xl">
<div class="page-header">
<h1>{{ __('boilerplate::hosts.title') }}</h1>
<button class="btn btn-primary" onclick="Livewire.dispatch('openModal', {livewireComponents: 'host.form', title: '{{ __('boilerplate::host.create') }}'})">
<i class="me-2 fas fa-plus"></i><span>{{ __('boilerplate::ui.add') }}</span>
</button>
</div>
@livewire('host.data-table', [], key('data-table'))
</div>
</x-layout-app>

View File

@ -0,0 +1,6 @@
<div>
<x-form::form wire:submit.prevent="{{$action}}">
<x-form::input group-class="mb-3" type="text" wire:model="hostname" id="hostname" label="hostname"/>
<x-form::button class="btn-primary" type="submit">Create</x-form::button>
</x-form::form>
</div>

Some files were not shown because too many files have changed in this diff Show More