diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..d9fc97f --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,37 @@ + + */ + 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(), + ]); + } +} diff --git a/app/Helpers/AbstractHelper.php b/app/Helpers/AbstractHelper.php new file mode 100644 index 0000000..716c936 --- /dev/null +++ b/app/Helpers/AbstractHelper.php @@ -0,0 +1,41 @@ + $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')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php new file mode 100644 index 0000000..9cfe4bc --- /dev/null +++ b/app/Http/Controllers/BaseController.php @@ -0,0 +1,13 @@ +middleware('auth'); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..77ec359 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -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; } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php new file mode 100644 index 0000000..b0eb37c --- /dev/null +++ b/app/Http/Controllers/HomeController.php @@ -0,0 +1,11 @@ +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); + } +} diff --git a/app/Http/Controllers/System/AuditController.php b/app/Http/Controllers/System/AuditController.php new file mode 100644 index 0000000..ff1e191 --- /dev/null +++ b/app/Http/Controllers/System/AuditController.php @@ -0,0 +1,32 @@ +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, + ]); + } +} diff --git a/app/Http/Controllers/System/BackupController.php b/app/Http/Controllers/System/BackupController.php new file mode 100644 index 0000000..9943212 --- /dev/null +++ b/app/Http/Controllers/System/BackupController.php @@ -0,0 +1,77 @@ +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]; + } +} diff --git a/app/Http/Controllers/System/CacheController.php b/app/Http/Controllers/System/CacheController.php new file mode 100644 index 0000000..a373dbf --- /dev/null +++ b/app/Http/Controllers/System/CacheController.php @@ -0,0 +1,50 @@ +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')); + } +} diff --git a/app/Http/Controllers/System/JobsController.php b/app/Http/Controllers/System/JobsController.php new file mode 100644 index 0000000..b2f24c8 --- /dev/null +++ b/app/Http/Controllers/System/JobsController.php @@ -0,0 +1,40 @@ +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')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/System/LogController.php b/app/Http/Controllers/System/LogController.php new file mode 100644 index 0000000..5253d35 --- /dev/null +++ b/app/Http/Controllers/System/LogController.php @@ -0,0 +1,111 @@ + $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')); + } +} diff --git a/app/Http/Controllers/System/SubscriptionController.php b/app/Http/Controllers/System/SubscriptionController.php new file mode 100644 index 0000000..c4cd31c --- /dev/null +++ b/app/Http/Controllers/System/SubscriptionController.php @@ -0,0 +1,13 @@ + + */ + protected $except = [ + 'theme', + ]; +} diff --git a/app/Http/Middleware/GenerateMenus.php b/app/Http/Middleware/GenerateMenus.php new file mode 100644 index 0000000..6de8cc5 --- /dev/null +++ b/app/Http/Middleware/GenerateMenus.php @@ -0,0 +1,72 @@ +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); + } +} diff --git a/app/Http/Requests/Auth/CreateApiTokenRequest.php b/app/Http/Requests/Auth/CreateApiTokenRequest.php new file mode 100644 index 0000000..ce794cd --- /dev/null +++ b/app/Http/Requests/Auth/CreateApiTokenRequest.php @@ -0,0 +1,29 @@ + + */ + public function rules(): array + { + return [ + 'token_name' => ['required', 'string'], + 'expire_at' => ['nullable', 'date', 'after_or_equal:' . date('Y-m-d')], + ]; + } +} diff --git a/app/Http/Requests/Auth/RemoveApiTokenRequest.php b/app/Http/Requests/Auth/RemoveApiTokenRequest.php new file mode 100644 index 0000000..5b90a26 --- /dev/null +++ b/app/Http/Requests/Auth/RemoveApiTokenRequest.php @@ -0,0 +1,28 @@ + + */ + public function rules(): array + { + return [ + 'token_id' => ['required', 'exists:personal_access_tokens,id'], + ]; + } +} diff --git a/app/Http/Requests/Auth/UpdateUserRequest.php b/app/Http/Requests/Auth/UpdateUserRequest.php new file mode 100644 index 0000000..4881829 --- /dev/null +++ b/app/Http/Requests/Auth/UpdateUserRequest.php @@ -0,0 +1,29 @@ + + */ + public function rules(): array + { + return [ + 'email' => ['sometimes', 'string', 'email', 'max:255', 'unique:users'], + 'newPassword' => ['sometimes', 'nullable', 'string', 'min:8', 'confirmed'], + ]; + } +} diff --git a/app/Jobs/Backup.php b/app/Jobs/Backup.php new file mode 100644 index 0000000..94b3650 --- /dev/null +++ b/app/Jobs/Backup.php @@ -0,0 +1,137 @@ +&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); + } +} diff --git a/app/Livewire/Audit/DataTable.php b/app/Livewire/Audit/DataTable.php new file mode 100644 index 0000000..ea54ed8 --- /dev/null +++ b/app/Livewire/Audit/DataTable.php @@ -0,0 +1,45 @@ +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" + ]; + } +} diff --git a/app/Livewire/Host/DataTable.php b/app/Livewire/Host/DataTable.php new file mode 100644 index 0000000..21cfdca --- /dev/null +++ b/app/Livewire/Host/DataTable.php @@ -0,0 +1,53 @@ + '$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]); + } +} diff --git a/app/Livewire/Host/Form.php b/app/Livewire/Host/Form.php new file mode 100644 index 0000000..6e3cd40 --- /dev/null +++ b/app/Livewire/Host/Form.php @@ -0,0 +1,54 @@ + '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'); + } +} diff --git a/app/Livewire/Maintenance/DataTable.php b/app/Livewire/Maintenance/DataTable.php new file mode 100644 index 0000000..1311873 --- /dev/null +++ b/app/Livewire/Maintenance/DataTable.php @@ -0,0 +1,55 @@ + '$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]); + } +} diff --git a/app/Livewire/Maintenance/Form.php b/app/Livewire/Maintenance/Form.php new file mode 100644 index 0000000..543c6e9 --- /dev/null +++ b/app/Livewire/Maintenance/Form.php @@ -0,0 +1,60 @@ + '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'); + } +} diff --git a/app/Livewire/Session/DataTable.php b/app/Livewire/Session/DataTable.php new file mode 100644 index 0000000..a7cd903 --- /dev/null +++ b/app/Livewire/Session/DataTable.php @@ -0,0 +1,62 @@ +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')); + } +} diff --git a/app/Livewire/Subscription/DataTable.php b/app/Livewire/Subscription/DataTable.php new file mode 100644 index 0000000..b501457 --- /dev/null +++ b/app/Livewire/Subscription/DataTable.php @@ -0,0 +1,52 @@ + '$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); + } +} diff --git a/app/Livewire/Subscription/Form.php b/app/Livewire/Subscription/Form.php new file mode 100644 index 0000000..3f7d0a8 --- /dev/null +++ b/app/Livewire/Subscription/Form.php @@ -0,0 +1,80 @@ + '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'); + } +} diff --git a/app/Livewire/User/DataTable.php b/app/Livewire/User/DataTable.php new file mode 100644 index 0000000..26ea34a --- /dev/null +++ b/app/Livewire/User/DataTable.php @@ -0,0 +1,51 @@ + '$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(); + } +} diff --git a/app/Livewire/User/Form.php b/app/Livewire/User/Form.php new file mode 100644 index 0000000..b21ca86 --- /dev/null +++ b/app/Livewire/User/Form.php @@ -0,0 +1,45 @@ + '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'); + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php new file mode 100644 index 0000000..d8cbc0a --- /dev/null +++ b/app/Models/Activity.php @@ -0,0 +1,46 @@ + 'array', + ]; + const UPDATED_AT = null; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + 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'); + } +} diff --git a/app/Models/Host.php b/app/Models/Host.php index 6108d13..87b6a8a 100644 --- a/app/Models/Host.php +++ b/app/Models/Host.php @@ -8,4 +8,8 @@ class Host extends Model { use HasFactory; + + protected $fillable = [ + 'hostname', + ]; } diff --git a/app/Models/Maintenance.php b/app/Models/Maintenance.php index a32fd66..aa90bf8 100644 --- a/app/Models/Maintenance.php +++ b/app/Models/Maintenance.php @@ -8,4 +8,10 @@ class Maintenance extends Model { use HasFactory; + + protected $fillable = [ + 'name', + 'description', + 'schedule', + ]; } diff --git a/app/Models/Session.php b/app/Models/Session.php new file mode 100644 index 0000000..af0e9b9 --- /dev/null +++ b/app/Models/Session.php @@ -0,0 +1,63 @@ + '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'; + }, + ); + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..62ff3ec --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,17 @@ +morphTo(); + } +} diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php new file mode 100644 index 0000000..7dc2c08 --- /dev/null +++ b/app/Models/Subscription.php @@ -0,0 +1,20 @@ + 'datetime', + ]; +} diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php new file mode 100644 index 0000000..c627a6e --- /dev/null +++ b/app/Models/Tenant.php @@ -0,0 +1,21 @@ +attributes['domain'] = strtolower($value); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index def621f..2af6a59 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,10 +6,12 @@ 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 @@ protected function casts(): array 'password' => 'hashed', ]; } + + public function sessions() + { + return $this->hasMany(Session::class); + } } diff --git a/app/Observers/ActivityObserver.php b/app/Observers/ActivityObserver.php new file mode 100644 index 0000000..6dd3fc2 --- /dev/null +++ b/app/Observers/ActivityObserver.php @@ -0,0 +1,37 @@ +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 + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..d0ea538 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -19,6 +19,8 @@ public function register(): void */ public function boot(): void { - // + if (config('app.env') != 'local'){ + \URL::forceScheme('https'); + } } } diff --git a/app/Providers/TenantServiceProvider.php b/app/Providers/TenantServiceProvider.php new file mode 100644 index 0000000..152d752 --- /dev/null +++ b/app/Providers/TenantServiceProvider.php @@ -0,0 +1,43 @@ +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); + }); + } +} diff --git a/app/Traits/Auditable.php b/app/Traits/Auditable.php new file mode 100644 index 0000000..f750a9c --- /dev/null +++ b/app/Traits/Auditable.php @@ -0,0 +1,35 @@ +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(); + }); + } +} diff --git a/app/Traits/HasSettings.php b/app/Traits/HasSettings.php new file mode 100644 index 0000000..801f24e --- /dev/null +++ b/app/Traits/HasSettings.php @@ -0,0 +1,15 @@ +morphMany(Setting::class, 'settable'); + } +} diff --git a/app/Types/SubscriptionTier.php b/app/Types/SubscriptionTier.php new file mode 100644 index 0000000..615fe3e --- /dev/null +++ b/app/Types/SubscriptionTier.php @@ -0,0 +1,41 @@ + [ + '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'; + } +} diff --git a/app/View/Components/Alerts.php b/app/View/Components/Alerts.php new file mode 100644 index 0000000..8ba9572 --- /dev/null +++ b/app/View/Components/Alerts.php @@ -0,0 +1,58 @@ +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'); + } +} diff --git a/app/View/Components/Navigation.php b/app/View/Components/Navigation.php new file mode 100644 index 0000000..90126ea --- /dev/null +++ b/app/View/Components/Navigation.php @@ -0,0 +1,30 @@ + Menu::get('main-menu')->items() ?? [], + 'systemMenuItems' => Menu::get('system-menu')->items() ?? [], + ]); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 7b162da..d654276 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/composer.json b/composer.json index 8303bde..357e0a0 100644 --- a/composer.json +++ b/composer.json @@ -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" }, diff --git a/composer.lock b/composer.lock index d233eca..04c6632 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/livewire.php b/config/livewire.php new file mode 100644 index 0000000..4e22072 --- /dev/null +++ b/config/livewire.php @@ -0,0 +1,158 @@ + '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 + | "/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, + +]; diff --git a/config/logging.php b/config/logging.php index 8d94292..4712aeb 100644 --- a/config/logging.php +++ b/config/logging.php @@ -12,13 +12,13 @@ | 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 @@ 'deprecations' => [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), - 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + 'trace' => false, ], /* @@ -41,20 +41,20 @@ | 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 @@ '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 @@ '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 @@ 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], - ], ]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..764a82f --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,83 @@ + 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, + ], + +]; diff --git a/database/migrations/2023_08_23_145233_create_activities_table.php b/database/migrations/2023_08_23_145233_create_activities_table.php new file mode 100644 index 0000000..5b88ec5 --- /dev/null +++ b/database/migrations/2023_08_23_145233_create_activities_table.php @@ -0,0 +1,34 @@ +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'); + } +}; diff --git a/database/migrations/2023_11_03_164300_create_subscriptions_table.php b/database/migrations/2023_11_03_164300_create_subscriptions_table.php new file mode 100644 index 0000000..b17a7b9 --- /dev/null +++ b/database/migrations/2023_11_03_164300_create_subscriptions_table.php @@ -0,0 +1,29 @@ +id(); + $table->integer('tier'); + $table->date('valid_to'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscriptions'); + } +}; diff --git a/database/migrations/2024_04_02_095547_create_settings_table.php b/database/migrations/2024_04_02_095547_create_settings_table.php new file mode 100644 index 0000000..322e9b5 --- /dev/null +++ b/database/migrations/2024_04_02_095547_create_settings_table.php @@ -0,0 +1,32 @@ +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'); + } +}; diff --git a/database/migrations/2024_07_30_083546_create_maintenance_task_histories_table.php b/database/migrations/2024_07_30_083546_create_maintenance_task_histories_table.php index b351a99..7828311 100644 --- a/database/migrations/2024_07_30_083546_create_maintenance_task_histories_table.php +++ b/database/migrations/2024_07_30_083546_create_maintenance_task_histories_table.php @@ -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 @@ public function up(): void { 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'); diff --git a/database/migrations/2024_07_30_145620_create_personal_access_tokens_table.php b/database/migrations/2024_07_30_145620_create_personal_access_tokens_table.php new file mode 100644 index 0000000..e828ad8 --- /dev/null +++ b/database/migrations/2024_07_30_145620_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +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'); + } +}; diff --git a/package-lock.json b/package-lock.json index 8e45843..e8ed735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4e934ca..aced9a5 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0..de557d2 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,3 @@ import './bootstrap'; +import './functions'; +import './quill'; \ No newline at end of file diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 5f1390b..ae70ea1 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -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'], +// }); diff --git a/resources/js/functions.js b/resources/js/functions.js new file mode 100644 index 0000000..35bc768 --- /dev/null +++ b/resources/js/functions.js @@ -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 = ` +
+ + +
`; + + if(icon){ + template += ``; + } + + template += ` +
+
${message}
` + if(details){ + template += `
${details}
` + } + + template += ` +
+
+
`; + + 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'); +} \ No newline at end of file diff --git a/resources/js/quill.js b/resources/js/quill.js new file mode 100644 index 0000000..9ecca8f --- /dev/null +++ b/resources/js/quill.js @@ -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(); \ No newline at end of file diff --git a/resources/sass/_quill.scss b/resources/sass/_quill.scss new file mode 100644 index 0000000..39c5c18 --- /dev/null +++ b/resources/sass/_quill.scss @@ -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); +// } diff --git a/resources/sass/admin/_misc.scss b/resources/sass/admin/_misc.scss new file mode 100644 index 0000000..1174495 --- /dev/null +++ b/resources/sass/admin/_misc.scss @@ -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; +} \ No newline at end of file diff --git a/resources/sass/admin/_variables-dark.scss b/resources/sass/admin/_variables-dark.scss new file mode 100644 index 0000000..7fe5c5b --- /dev/null +++ b/resources/sass/admin/_variables-dark.scss @@ -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; diff --git a/resources/sass/admin/_variables.scss b/resources/sass/admin/_variables.scss new file mode 100644 index 0000000..68d3e93 --- /dev/null +++ b/resources/sass/admin/_variables.scss @@ -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; \ No newline at end of file diff --git a/resources/sass/admin/components/_nav.scss b/resources/sass/admin/components/_nav.scss new file mode 100644 index 0000000..b7734a0 --- /dev/null +++ b/resources/sass/admin/components/_nav.scss @@ -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); + } +} diff --git a/resources/sass/admin/components/_snackbar.scss b/resources/sass/admin/components/_snackbar.scss new file mode 100644 index 0000000..d4c4ee4 --- /dev/null +++ b/resources/sass/admin/components/_snackbar.scss @@ -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; +} diff --git a/resources/sass/admin/components/_tables.scss b/resources/sass/admin/components/_tables.scss new file mode 100644 index 0000000..76138a1 --- /dev/null +++ b/resources/sass/admin/components/_tables.scss @@ -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); + } +} \ No newline at end of file diff --git a/resources/sass/admin/layout/_containers.scss b/resources/sass/admin/layout/_containers.scss new file mode 100644 index 0000000..b0fc51a --- /dev/null +++ b/resources/sass/admin/layout/_containers.scss @@ -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; + } +} diff --git a/resources/sass/admin/layout/_layout-nav-default.scss b/resources/sass/admin/layout/_layout-nav-default.scss new file mode 100644 index 0000000..4d47fb1 --- /dev/null +++ b/resources/sass/admin/layout/_layout-nav-default.scss @@ -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; + } + } +} diff --git a/resources/sass/admin/layout/_layout-nav-mobile.scss b/resources/sass/admin/layout/_layout-nav-mobile.scss new file mode 100644 index 0000000..de64657 --- /dev/null +++ b/resources/sass/admin/layout/_layout-nav-mobile.scss @@ -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; + } + } + +} diff --git a/resources/sass/admin/layout/_layout.scss b/resources/sass/admin/layout/_layout.scss new file mode 100644 index 0000000..b1cb1e7 --- /dev/null +++ b/resources/sass/admin/layout/_layout.scss @@ -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; + } +} diff --git a/resources/sass/app.scss b/resources/sass/app.scss new file mode 100644 index 0000000..9bdc764 --- /dev/null +++ b/resources/sass/app.scss @@ -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"; diff --git a/resources/sass/quill/_base.scss b/resources/sass/quill/_base.scss new file mode 100644 index 0000000..4ae8baf --- /dev/null +++ b/resources/sass/quill/_base.scss @@ -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%; + } +} \ No newline at end of file diff --git a/resources/sass/quill/core.scss b/resources/sass/quill/core.scss new file mode 100644 index 0000000..72afc9b --- /dev/null +++ b/resources/sass/quill/core.scss @@ -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; + } +} diff --git a/resources/sass/quill/snow.scss b/resources/sass/quill/snow.scss new file mode 100644 index 0000000..d6147aa --- /dev/null +++ b/resources/sass/quill/snow.scss @@ -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); +} diff --git a/resources/sass/quill/snow/_toolbar.scss b/resources/sass/quill/snow/_toolbar.scss new file mode 100644 index 0000000..dc9ea04 --- /dev/null +++ b/resources/sass/quill/snow/_toolbar.scss @@ -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; +} diff --git a/resources/sass/quill/snow/_tooltip.scss b/resources/sass/quill/snow/_tooltip.scss new file mode 100644 index 0000000..940983f --- /dev/null +++ b/resources/sass/quill/snow/_tooltip.scss @@ -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:'; + } +} diff --git a/resources/sass/tailwind/variables/_colors.scss b/resources/sass/tailwind/variables/_colors.scss new file mode 100644 index 0000000..f6d9425 --- /dev/null +++ b/resources/sass/tailwind/variables/_colors.scss @@ -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; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 9f56e6b..9496936 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,34 +1,21 @@ -
- @csrf + + + + + -
-
- @error('email') - - {{ $message }}
-
- @enderror - -
-
- @error('password') - - {{ $message }}
-
- @enderror - -
- - @if (Route::has('password')) - - {{ __('Password Reset') }} ? -
- @endif - - - @if (Route::has('register')) - - {{ __('Register') }} ? - - @endif - +
+ {{ __('boilerplate::auth.login') }} + @if (Route::has('register')) + + {{ __('boilerplate::auth.register') }} + + @endif + @if (Route::has('password')) + + {{ __('boilerplate::auth.password_reset') }} + + @endif +
+
+
diff --git a/resources/views/auth/profile.blade.php b/resources/views/auth/profile.blade.php new file mode 100644 index 0000000..bfbf19a --- /dev/null +++ b/resources/views/auth/profile.blade.php @@ -0,0 +1,61 @@ + +
+ + +
+ +

{{ $user->name ?? '' }}

+
+ +
+ +

{{ $user->email ?? '' }}

+
+ + @if (config('session.driver') == 'database') +
+

{{ __('boilerplate::ui.sessions') }}

+ @livewire('session.data-table', [], key('data-table')) +
+ @endif + +
+

{{ __('boilerplate::ui.change_password') }}

+
+ +
+ @csrf + @method('put') +
+ + + + @error('password') + + {{ $message }} + + @enderror +
+ +
+ + + + @error('newPassword') + + {{ $message }} + + @enderror +
+ +
+ + +
+ + +
+
+
\ No newline at end of file diff --git a/resources/views/auth/profile_api.blade.php b/resources/views/auth/profile_api.blade.php new file mode 100644 index 0000000..f37e731 --- /dev/null +++ b/resources/views/auth/profile_api.blade.php @@ -0,0 +1,69 @@ + +
+ + +
+ + + + + + + + + + + @foreach ($tokens as $token) + + + + + + + @endforeach + +
{{ __('boilerplate::ui.name') }}{{ __('boilerplate::ui.last_used_at') }}{{ __('boilerplate::ui.expire_at') }}{{ __('boilerplate::ui.actions') }}
{{ $token->name }}{{ $token->last_used_at ?? __('boilerplate::ui.never') }}{{ $token->expire_at ?? __('boilerplate::ui.never') }} +
+ @method('DELETE') + @csrf + +
+
+
+
+
\ No newline at end of file diff --git a/resources/views/auth/registration.blade.php b/resources/views/auth/registration.blade.php index 85344b5..6e58619 100644 --- a/resources/views/auth/registration.blade.php +++ b/resources/views/auth/registration.blade.php @@ -1,37 +1,17 @@ -
- @csrf + + + + + + -
-
- @error('name') - - {{ $message }}
-
- @enderror - -
-
- @error('email') - - {{ $message }}
-
- @enderror - -
-
- @error('password') - - {{ $message }}
-
- @enderror - -
-
- @error('password_confirmation') - - {{ $message }}
-
- @enderror - - - +
+ {{ __('boilerplate::auth.register') }} + @if (Route::has('login')) + + {{ __('boilerplate::auth.login') }} + + @endif +
+
+
diff --git a/resources/views/auth/reset.blade.php b/resources/views/auth/reset.blade.php index be48efa..c2fc37b 100644 --- a/resources/views/auth/reset.blade.php +++ b/resources/views/auth/reset.blade.php @@ -1,42 +1,21 @@ -
- @csrf + + + @if (isset($token)) + + + + + @else + + @endif - @if (isset($token)) - - -
-
- @error('email') - - {{ $message }}
-
- @enderror - -
-
- @error('password') - - {{ $message }}
-
- @enderror - -
-
- @error('password_confirmation') - - {{ $message }}
-
- @enderror - @else -
-
- @error('email') - - {{ $message }}
-
- @enderror - @endif - - - +
+ {{ __('boilerplate::auth.send_password_reset') }} + @if (Route::has('login')) + + {{ __('boilerplate::auth.login') }} ? + + @endif +
+
+
diff --git a/resources/views/auth/verify.blade.php b/resources/views/auth/verify.blade.php new file mode 100644 index 0000000..7ca354f --- /dev/null +++ b/resources/views/auth/verify.blade.php @@ -0,0 +1,26 @@ + +
+
+
+
+
{{ __('auth.Verify') }}
+ +
+ @if (session('resent')) + + @endif + + {{ __('auth.checkEmail') }} + {{ __('auth.notReceiveEmail') }}, +
+ @csrf + . +
+
+
+
+
+
+
diff --git a/resources/views/components/alerts.blade.php b/resources/views/components/alerts.blade.php new file mode 100644 index 0000000..c94e86c --- /dev/null +++ b/resources/views/components/alerts.blade.php @@ -0,0 +1,26 @@ +
+ @foreach ($alerts as $alert) +
+ + +
+ @switch($alert['type']) + @case('success') + + @break + @case('error') + + @break + @case('warning') + + @break + @default + + @endswitch +
+
{{ $alert['message'] }}
+
+
+
+ @endforeach +
diff --git a/resources/views/components/layout-app.blade.php b/resources/views/components/layout-app.blade.php new file mode 100644 index 0000000..bf7aaab --- /dev/null +++ b/resources/views/components/layout-app.blade.php @@ -0,0 +1,133 @@ + + + + + + + + + + + {{ config('app.name', 'Laravel') }} + + {{-- --}} + + + + + + + + + + + + + + + + + @livewireStyles + @vite(['resources/sass/app.scss', 'resources/js/app.js']) + + {{-- --}} + + + + +
+ @auth + @include('partials.navbar') + @endauth + +
+ + + + @include('partials.navigation-mobile') + +
+
+ {{ $slot }} +
+
+
+
+ + + + @livewireScripts + @livewire('modal-basic', key('modal')) + @stack('scripts') + + + diff --git a/resources/views/components/layout-auth.blade.php b/resources/views/components/layout-auth.blade.php new file mode 100644 index 0000000..9fd97e4 --- /dev/null +++ b/resources/views/components/layout-auth.blade.php @@ -0,0 +1,58 @@ + + + + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + + + + + @vite(['resources/sass/app.scss', 'resources/js/app.js']) + + + +
+
+
+
+ + + +

{{ config('app.name', 'Laravel') }}

+

+ {{ __('general.MetaDescription') }} +

+
+
+
+
+
+ +
+ + + +
+
+ {{ $slot }} +
+
+
+
+
+
+ + + diff --git a/resources/views/components/navigation.blade.php b/resources/views/components/navigation.blade.php new file mode 100644 index 0000000..7fd2ee6 --- /dev/null +++ b/resources/views/components/navigation.blade.php @@ -0,0 +1,128 @@ +
+ + + + + +
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php new file mode 100644 index 0000000..4a4b99f --- /dev/null +++ b/resources/views/errors/404.blade.php @@ -0,0 +1,5 @@ + +

404

+

{{ __('boilerplate::ui.not_found') }}

+ {{ __('boilerplate::ui.home') }} +
\ No newline at end of file diff --git a/resources/views/errors/4xx.blade.php b/resources/views/errors/4xx.blade.php new file mode 100644 index 0000000..4262e48 --- /dev/null +++ b/resources/views/errors/4xx.blade.php @@ -0,0 +1,5 @@ + +

{{ $exception->getStatusCode() }}

+

{{ $exception->getMessage() }}

+ {{ __('boilerplate::ui.home') }} +
\ No newline at end of file diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php new file mode 100644 index 0000000..27eb908 --- /dev/null +++ b/resources/views/errors/503.blade.php @@ -0,0 +1,5 @@ + +

503

+

{{ __('boilerplate::ui.maintenance') }}

+ {{ __('boilerplate::ui.home') }} +
\ No newline at end of file diff --git a/resources/views/errors/5xx.blade.php b/resources/views/errors/5xx.blade.php new file mode 100644 index 0000000..4262e48 --- /dev/null +++ b/resources/views/errors/5xx.blade.php @@ -0,0 +1,5 @@ + +

{{ $exception->getStatusCode() }}

+

{{ $exception->getMessage() }}

+ {{ __('boilerplate::ui.home') }} +
\ No newline at end of file diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php new file mode 100644 index 0000000..dc4921b --- /dev/null +++ b/resources/views/home.blade.php @@ -0,0 +1,189 @@ + +
+ +
+
+ +

head 1

+

head 2

+

head 3

+

head 4

+
head 5
+
head 6
+ +
+

Tabs

+ + +
+ +
+
+ PS +
+
+ PS +
+
+ PS +
+
+ PS +
+
+ PS +
+
+ PS +
+
+ +
+

Buttons

+ + + + + + + + + + +
+ +
+

Badges

+ Primary + Secondary + Success + Danger + Warning + Info + Light + Dark +
+ Primary + Secondary + Success + Danger + Warning + Info + Light + Dark +
+
+ +
+

Breadcrumbs

+ + + + + +
+ +
+

Alerts

+ + + + + + + + +
+ +
+

Pagination

+ +
+ +
+

Colors

+
Primary with contrasting color
+
Secondary with contrasting color
+
Success with contrasting color
+
Danger with contrasting color
+
Warning with contrasting color
+
Info with contrasting color
+
Light with contrasting color
+
Dark with contrasting color
+
+
+
diff --git a/resources/views/hosts/index.blade.php b/resources/views/hosts/index.blade.php new file mode 100644 index 0000000..99697d7 --- /dev/null +++ b/resources/views/hosts/index.blade.php @@ -0,0 +1,13 @@ + +
+ + + @livewire('host.data-table', [], key('data-table')) +
+
diff --git a/resources/views/livewire/host/form.blade.php b/resources/views/livewire/host/form.blade.php new file mode 100644 index 0000000..b992a01 --- /dev/null +++ b/resources/views/livewire/host/form.blade.php @@ -0,0 +1,6 @@ +
+ + + Create + +
\ No newline at end of file diff --git a/resources/views/livewire/maintenance/form.blade.php b/resources/views/livewire/maintenance/form.blade.php new file mode 100644 index 0000000..36df09a --- /dev/null +++ b/resources/views/livewire/maintenance/form.blade.php @@ -0,0 +1,8 @@ +
+ + + + + Create + +
diff --git a/resources/views/livewire/subscription/form.blade.php b/resources/views/livewire/subscription/form.blade.php new file mode 100644 index 0000000..e235aa2 --- /dev/null +++ b/resources/views/livewire/subscription/form.blade.php @@ -0,0 +1,14 @@ +
+ + + + + + @if ($action == 'update') + {{ __('boilerplate::ui.add') }} + @else + {{ __('boilerplate::ui.add') }} + @endif + +
diff --git a/resources/views/livewire/user/form.blade.php b/resources/views/livewire/user/form.blade.php new file mode 100644 index 0000000..f87e6ce --- /dev/null +++ b/resources/views/livewire/user/form.blade.php @@ -0,0 +1,9 @@ +
+ + + + + + {{ __('boilerplate::ui.create') }} + +
\ No newline at end of file diff --git a/resources/views/maintenance/index.blade.php b/resources/views/maintenance/index.blade.php new file mode 100644 index 0000000..27a0500 --- /dev/null +++ b/resources/views/maintenance/index.blade.php @@ -0,0 +1,13 @@ + +
+ + + @livewire('maintenance.data-table', [], key('data-table')) +
+
diff --git a/resources/views/partials/navbar.blade.php b/resources/views/partials/navbar.blade.php new file mode 100644 index 0000000..ba50302 --- /dev/null +++ b/resources/views/partials/navbar.blade.php @@ -0,0 +1,21 @@ + diff --git a/resources/views/partials/navigation-mobile.blade.php b/resources/views/partials/navigation-mobile.blade.php new file mode 100644 index 0000000..f9eb45c --- /dev/null +++ b/resources/views/partials/navigation-mobile.blade.php @@ -0,0 +1,36 @@ + diff --git a/resources/views/system/api/index.blade.php b/resources/views/system/api/index.blade.php new file mode 100644 index 0000000..d4342a9 --- /dev/null +++ b/resources/views/system/api/index.blade.php @@ -0,0 +1,71 @@ + +
+ +
+ @foreach ($routes as $id => $route) + @php + $color = 'info'; + switch ($route['Method']) { + case 'GET': + $color = 'info'; + break; + case 'POST': + $color = 'success'; + break; + case 'PUT': + $color = 'warning'; + break; + case 'DELETE': + $color = 'danger'; + break; + } + @endphp + +
+
+ {{ $route['Method'] }} + {{ $route['Uri'] }} +
+
+
+
Description
+
{{ $route['Description'] }}
+ +
Parameters
+ + + + + + + @foreach ($route['Parameters'] as $parameter) + + + + + + @endforeach +
NameTypeComment
+ {{ $parameter['name'] }} + + {{ $parameter['type'] }} + + {{ $parameter['comment'] }} +
+ +
Returns
+
{{ $route['Returns'] ?? 'NULL' }}
+
+
+
+ @endforeach +
+
+
diff --git a/resources/views/system/audit/index.blade.php b/resources/views/system/audit/index.blade.php new file mode 100644 index 0000000..1d2998d --- /dev/null +++ b/resources/views/system/audit/index.blade.php @@ -0,0 +1,8 @@ + +
+ + @livewire('audit.data-table', [], key('data-table')) +
+
diff --git a/resources/views/system/backup/index.blade.php b/resources/views/system/backup/index.blade.php new file mode 100644 index 0000000..8426802 --- /dev/null +++ b/resources/views/system/backup/index.blade.php @@ -0,0 +1,46 @@ + +
+ + + + + + + + + + + @if(!empty($backups)) + @foreach ($backups as $backup) + + + + + + @endforeach + @endif + +
{{ __('boilerplate::ui.name') }}{{ __('boilerplate::ui.size') }}{{ __('boilerplate::ui.actions') }}
+ @foreach ($backup['fileName'] as $key => $fileName) + {{ ($key != 0 ? ", " : "") }}{{ $fileName }} + @endforeach + + {{ $backup['fileSize'] }} + + @php($backups_slug = explode('_', $backup['fileName'][0])[0]) + +
+ {{ __('web.delete') }} +
+
+ +
+
+
+
+
diff --git a/resources/views/system/cache/index.blade.php b/resources/views/system/cache/index.blade.php new file mode 100644 index 0000000..d63dc57 --- /dev/null +++ b/resources/views/system/cache/index.blade.php @@ -0,0 +1,30 @@ + +
+ + +
+ + + + + + + + + @foreach ($cache_items as $item) + + + + @endforeach + +
key
+ {{ var_dump($item) }} +
+
+
+ + +
diff --git a/resources/views/system/jobs copy/index.blade.php b/resources/views/system/jobs copy/index.blade.php new file mode 100644 index 0000000..3850fc0 --- /dev/null +++ b/resources/views/system/jobs copy/index.blade.php @@ -0,0 +1,74 @@ + +
+ + +
+ + + + + + + + + + + + @foreach ($failed_jobs as $item) + + + + + + + + @endforeach + +
uuidqueuepayloadexceptionfailed_at
+ {{ $item->uuid }} + + {{ $item->queue }} + +
{{ Str::substr($item->payload, 0, 150) }}
+
+
{{ Str::substr($item->exception, 0, 300) }}
+
+ {{ $item->failed_at }} +
+
+ +
+ + + + + + + + + + + @foreach ($jobs as $item) + + + + + + + @endforeach + +
idqueuepayloadavailable_at
+ {{ $item->id }} + + {{ $item->queue }} + +
{{ Str::substr($item->payload, 0, 150) }}
+
+ {{ $item->available_at }} +
+
+
+
diff --git a/resources/views/system/jobs/index.blade.php b/resources/views/system/jobs/index.blade.php new file mode 100644 index 0000000..e0e34e0 --- /dev/null +++ b/resources/views/system/jobs/index.blade.php @@ -0,0 +1,83 @@ + +
+ + +
{{ __('boilerplate::ui.jobs-start') }}
+
+ @foreach ($job_actions as $job) + {{$job}} + @endforeach +
+ +
{{ __('boilerplate::ui.jobs-waiting') }}
+
+ + + + + + + + + + + @foreach ($jobs as $item) + + + + + + + @endforeach + +
idqueuepayloadavailable_at
+ {{ $item->id }} + + {{ $item->queue }} + +
{{ Str::substr($item->payload, 0, 150) }}
+
+ {{ $item->available_at }} +
+
+ +
{{ __('boilerplate::ui.jobs-failed') }}
+
+ + + + + + + + + + + + @foreach ($failed_jobs as $item) + + + + + + + + @endforeach + +
uuidqueuepayloadexceptionfailed_at
+ {{ $item->uuid }} + + {{ $item->queue }} + +
{{ Str::substr($item->payload, 0, 150) }}
+
+
{{ Str::substr($item->exception, 0, 300) }}
+
+ {{ $item->failed_at }} +
+
+
+
\ No newline at end of file diff --git a/resources/views/system/log/detail.blade.php b/resources/views/system/log/detail.blade.php new file mode 100644 index 0000000..e654573 --- /dev/null +++ b/resources/views/system/log/detail.blade.php @@ -0,0 +1,12 @@ + +
+ + +
{!! $content !!}
+ +
+
+
diff --git a/resources/views/system/log/index.blade.php b/resources/views/system/log/index.blade.php new file mode 100644 index 0000000..76faee8 --- /dev/null +++ b/resources/views/system/log/index.blade.php @@ -0,0 +1,49 @@ + +
+ + +
+ @foreach ($todayStats as $stat => $value) +
+
+
+
{{ $stat }}
+
{{ $value }}
+ today +
+
+
+ @endforeach +
+ +
+ + + + + + + + + + @foreach ($items as $item) + + + + + + @endforeach + +
{{ __('boilerplate::ui.name')}}{{ __('boilerplate::ui.size')}}{{ __('boilerplate::ui.actions') }}
+ {{ $item['fileName'] }} + + {{ $item['humanReadableSize'] }} + + $item['fileName']]) }}' class="btn btn-secondary">{{ __('boilerplate::ui.logs-download') }} +
+
+
+
diff --git a/resources/views/system/subscription/index.blade.php b/resources/views/system/subscription/index.blade.php new file mode 100644 index 0000000..e36720b --- /dev/null +++ b/resources/views/system/subscription/index.blade.php @@ -0,0 +1,13 @@ + +
+ + + @livewire('subscription.data-table', [], key('data-table')) +
+
diff --git a/resources/views/system/user/index.blade.php b/resources/views/system/user/index.blade.php new file mode 100644 index 0000000..5481bcd --- /dev/null +++ b/resources/views/system/user/index.blade.php @@ -0,0 +1,11 @@ + +
+ + @livewire('user.data-table', [], key('data-table')) +
+
diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..ccc387f --- /dev/null +++ b/routes/api.php @@ -0,0 +1,8 @@ +user(); +})->middleware('auth:sanctum'); diff --git a/routes/web.php b/routes/web.php index f914da2..ea45966 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,7 +2,57 @@ use Illuminate\Support\Facades\Route; +Route::auth(); Route::get('/', function () { return view('welcome'); }); -Route::auth(); \ No newline at end of file + +Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); +Route::get('/host', [App\Http\Controllers\HostController::class, 'index'])->name('host'); +Route::get('/maintenance', [App\Http\Controllers\MaintenanceController::class, 'index'])->name('maintenance'); + + + +Route::prefix('profile')->name('profile.')->middleware(['auth'])->group(function () { + Route::get('/', [App\Http\Controllers\Auth\ProfileController::class, 'index'])->middleware('auth')->name('index'); + Route::put('/update', [App\Http\Controllers\Auth\ProfileController::class, 'update'])->middleware('auth')->name('update'); + Route::get('/api', [App\Http\Controllers\Auth\ProfileController::class, 'api'])->middleware('auth')->name('api'); + Route::post('/api/create', [App\Http\Controllers\Auth\ProfileController::class, 'createApiToken'])->middleware('auth')->name('api.create'); + Route::delete('/api/remove', [App\Http\Controllers\Auth\ProfileController::class, 'removeApiToken'])->middleware('auth')->name('api.remove'); +}); + +Route::prefix('system')->name('system.')->middleware(['auth'])->group(function () { + Route::get('/audit', [App\Http\Controllers\System\AuditController::class, 'index'])->name('audit.index'); + + Route::get('/user', [App\Http\Controllers\System\UserController::class, 'index'])->name('user.index'); + + Route::get('/subscription', [App\Http\Controllers\System\SubscriptionController::class, 'index'])->name('subscription.index'); + + Route::get('/api', [App\Http\Controllers\System\ApiController::class, 'index'])->name('api.index'); + + Route::prefix('jobs')->name('jobs.')->group(function () { + Route::get('/', [App\Http\Controllers\System\JobsController::class, 'index'])->name('index'); + Route::get('/clear', [App\Http\Controllers\System\JobsController::class, 'clear'])->name('clear'); + Route::get('/call/{job}', [App\Http\Controllers\System\JobsController::class, 'call'])->name('call'); + }); + + Route::prefix('cache')->name('cache.')->group(function () { + Route::get('/', [App\Http\Controllers\System\CacheController::class, 'index'])->name('index'); + Route::get('/clear', [App\Http\Controllers\System\CacheController::class, 'clear'])->name('clear'); + }); + + Route::prefix('log')->name('log.')->group(function () { + Route::get('/', [App\Http\Controllers\System\LogController::class, 'index'])->name('index'); + Route::get('/detail/{file}', [App\Http\Controllers\System\LogController::class, 'detail'])->name('detail'); + Route::get('/download/{file}', [App\Http\Controllers\System\LogController::class, 'download'])->name('download'); + Route::get('/clear', [App\Http\Controllers\System\LogController::class, 'clear'])->name('clear'); + }); + + Route::prefix('backup')->name('backup.')->group(function () { + Route::get('/', [App\Http\Controllers\System\BackupController::class, 'index'])->name('index'); + Route::get('/run', [App\Http\Controllers\System\BackupController::class, 'run'])->name('run'); + Route::get('/delete/{backup_date}', [App\Http\Controllers\System\BackupController::class, 'delete'])->name('delete'); + Route::get('/download/{file_name}', [App\Http\Controllers\System\BackupController::class, 'download'])->name('download'); + Route::get('/download', [App\Http\Controllers\System\BackupController::class, 'download'])->name('download.latest'); + }); +}); diff --git a/vite.config.js b/vite.config.js index 421b569..03ad23e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,11 +1,21 @@ import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; +import path from 'path'; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], + input: [ + 'resources/sass/app.scss', + //'resources/css/app.css', + 'resources/js/app.js', + ], refresh: true, }), ], -}); + // resolve: { + // alias: { + // '~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap') + // } + // } +}); \ No newline at end of file