Compare commits

..

55 Commits

Author SHA1 Message Date
ac751f5f28 adding prefers reduced motion and clamp width to percent between values on body 2024-08-12 12:44:35 -06:00
a1aecdf3e2 adding AbuseIPDB to composer list 2024-08-12 12:43:44 -06:00
a747ef0129 adding a security.txt file 2024-03-18 11:18:25 -06:00
80aa487e7e updating some status codes 2024-02-28 14:52:37 -07:00
5048abc3e6 adding some text gradient CSS 2023-12-12 14:00:23 -07:00
afd34e9b4e adding profile photo trait 2023-12-12 13:59:55 -07:00
a81ce8a922 adding glossy button CSS 2023-12-12 13:59:44 -07:00
9d25befeba adding a method to also generate an avatar for a user 2023-12-12 13:58:31 -07:00
5d9f7e3997 adding package to convert heic/heif to jpeg, also paperclip 2023-12-12 13:58:10 -07:00
ecfae3e01a using new style of attribute casting 2023-12-12 13:57:42 -07:00
9e6c93ba95 adding some defaults to image tags 2023-12-12 13:57:12 -07:00
231dd959af adding some gradient border CSS 2023-12-12 13:56:57 -07:00
5607425bde adding a security template file 2023-12-12 13:56:38 -07:00
ac99d29109 removing unnecessary code to properly extend Jetstream user profile controller 2023-03-27 10:24:09 -06:00
d4f0044656 moving to mailpit from mailhog, adding paths to SSLs 2023-03-27 10:23:28 -06:00
9d455bdcf7 moving around some stuff with PHPCS config file 2023-03-27 10:22:57 -06:00
3b39ae0470 adding require pin package 2023-03-27 10:22:07 -06:00
9551f661c1 adding more colors 2023-03-27 10:21:52 -06:00
c02b40071f adding some colors as a markdown file 2023-03-27 10:21:34 -06:00
a0dc0e01f3 adding an action for genering profile photos 2023-03-27 10:21:08 -06:00
8876f07ac6 fixing some text and height stuff for the body tag 2023-03-27 10:20:56 -06:00
2ac78ef539 adding some variables for colors 2023-03-27 10:20:41 -06:00
56cef79880 fixing path to SSLs 2023-03-27 10:20:09 -06:00
aa5753cc82 adding a Vite SSR file 2023-03-27 10:19:55 -06:00
4dcb6b6ebe adding a clamp function 2023-03-27 10:19:42 -06:00
22946bc031 Adding some fonts 2023-03-27 10:19:09 -06:00
e2ce420764 wip: a wave/ripple effect 2022-12-27 14:19:05 -07:00
7583883d64 adding some css resets 2022-12-27 14:18:49 -07:00
fb5bde70ef checking for a user on the request before checking admin email 2022-12-16 10:48:18 -07:00
5e2a386b15 changing some URLs in the app view 2022-12-16 10:47:58 -07:00
75d052af88 updating Lucide icons to inherit height and width 2022-12-16 10:47:39 -07:00
87cd634613 adding a defines comment to input error vue component 2022-12-07 11:45:25 -07:00
48a34d5acf tweaking form label color 2022-12-07 11:44:55 -07:00
05ca3574b0 moving a transition property that was in it's own selector without reason 2022-12-07 11:44:29 -07:00
739c2b79e9 adding universally unique lexicographically sortable identifier (ULID) 2022-12-07 11:43:52 -07:00
6e7ded098b updating docblock 2022-12-07 11:43:09 -07:00
64d4d326b7 updating user and language models 2022-12-07 11:42:36 -07:00
fdba9f1ae7 updating create user and reset password with new return constants 2022-12-07 11:42:12 -07:00
27032f433a updating pre-commit 2022-12-07 11:41:52 -07:00
0c8a6c410d adding money castable 2022-12-07 11:41:43 -07:00
3f340d57fc adding a bunch of wip: i18n stuff 2022-11-28 12:32:48 -07:00
995cc32578 adding a middleware to handle super basic admin role check 2022-11-28 12:32:29 -07:00
5f5b443df7 wip: adding a team scope 2022-11-28 12:31:57 -07:00
e5366171fd adding some stuff for vite 2022-11-28 12:31:45 -07:00
1c650fbb64 removing check custom session data middleware 2022-11-28 12:30:56 -07:00
a784d44d16 adding a bubbles in background animation 2022-11-28 12:30:12 -07:00
e27aa8969f adding minio, admin email, session stuff, and clockwork env vars 2022-11-28 12:29:29 -07:00
433ad39a08 adding some form vue components 2022-11-28 12:28:41 -07:00
e78324e92a wip: adding user observer 2022-11-10 12:16:20 -07:00
5ceb315b6c adding some geo-spatial mysql stuff 2022-11-10 12:16:09 -07:00
58a4701ba3 fixing syntax error in global functions 2022-11-10 12:15:55 -07:00
a1f677ff9b adding Lucide icon set 2022-11-10 12:15:39 -07:00
ebb6a4721a moving Icons to FeatherIcons 2022-11-10 12:15:00 -07:00
625dfeb79f lots of updates 2022-10-19 14:33:03 -06:00
427d4e2500 finishing up adding all the icons from Feather icons 2022-09-28 14:30:10 -06:00
1249 changed files with 48976 additions and 906 deletions

60
colors.md Normal file
View File

@ -0,0 +1,60 @@
Colors
### Sunbaked Mint
HEX: #80e8d4
RGB: 128, 232, 212
CMYK: 43, 0, 25, 0
### Honey Dijon
HEX: #e2ae61
RGB: 226, 174, 97
CMYK: 1, 32, 72, 0
### Ultimate Gray
HEX: #97999b
RGB: 151, 153, 155
CMYK (approximation): 44, 35, 34, 1
### Frosty Blue
HEX: #bcddfc
RGB: 188, 221, 252
CMYK: 23, 5, 0, 0
### Electric Tangerine
HEX: #ff825c
RGB: 255, 130, 92
CMYK: 0, 61, 64, 0
### Holo Lilac
HEX: #c5d2fe
RGB: 197, 210, 254
CMYK: 22, 17, 0, 0
### Poppy Sunset
HEX: #ee645b
RGB: 238, 100, 91
CMYK: 2, 76, 62, 0
### Lime Nouveau
HEX: #cddf8b
RGB: 205, 223, 139
CMYK: 22, 1, 58, 5
### Very Peri
HEX: #6667ab
RGB: 102, 103, 171
CMYK (approximation): 40, 40, 0, 33
### Classic Blue
HEX: #0f4c81
RGB: 15, 76, 129
CMYK (approximation): 99, 76, 24, 8

View File

@ -27,6 +27,6 @@ modified="git diff --diff-filter=M --name-only --cached | grep \".php$\""
ignore="resources/lang,resources/views,bootstrap/helpers,database/migrations,bin" ignore="resources/lang,resources/views,bootstrap/helpers,database/migrations,bin"
phpcs="vendor/bin/phpcs --report=code --colors --report-width=80 --ignore=${ignore}" phpcs="vendor/bin/phpcs --report=code --colors --report-width=80 --ignore=${ignore}"
__run "1/1" "code sniffer" "${modified} | xargs -r ${phpcs}" __run "1/2" "php lint" "${modified} | xargs -r php -l"
# __run "2/3" "php lint" "${modified} | xargs -r php -l" __run "2/2" "code sniffer" "${modified} | xargs -r ${phpcs}"
# __run "3/3" "phpstan" "${modified} | xargs -r vendor/bin/phpstan analyse" # __run "3/3" "phpstan" "${modified} | xargs -r vendor/bin/phpstan analyse"

View File

@ -4,7 +4,7 @@ APP_KEY=base64:hSCTwZ507IdKQ5QJHJ+mQw0DSMgDdAspasjwHCdiB8Y=
APP_DEBUG=true APP_DEBUG=true
APP_DOMAIN=localhost APP_DOMAIN=localhost
APP_URL="https://${APP_DOMAIN}" APP_URL="https://${APP_DOMAIN}"
APP_UID_BYTES=8 ADMIN_EMAIL=""
GIT_HASH="00000000" GIT_HASH="00000000"
GIT_TAG="x.x.x" GIT_TAG="x.x.x"
@ -28,6 +28,9 @@ FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=sync QUEUE_CONNECTION=sync
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
#SESSION_STORE=redis
#SESSION_DOMAIN="${APP_DOMAIN}"
#SESSION_SECURE_COOKIE=true
MEMCACHED_HOST=memcache MEMCACHED_HOST=memcache
@ -35,6 +38,15 @@ REDIS_HOST=redis
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@${APP_DOMAIN}"
MAIL_FROM_NAME="${APP_NAME}"
SCOUT_DRIVER=meilisearch SCOUT_DRIVER=meilisearch
SCOUT_PREFIX= SCOUT_PREFIX=
SCOUT_QUEUE=false SCOUT_QUEUE=false
@ -43,14 +55,15 @@ MEILISEARCH_KEY=
MEILISEARCH_PRIVATE_KEY= MEILISEARCH_PRIVATE_KEY=
MEILISEARCH_PUBLIC_KEY= MEILISEARCH_PUBLIC_KEY=
MAIL_MAILER=smtp GOOGLE_GEOCODE_API_KEY=
MAIL_HOST=mailhog
MAIL_PORT=1125 MINIO_USERNAME=
MAIL_USERNAME=null MINIO_PASSWORD=
MAIL_PASSWORD=null MINIO_DEFAULT_REGION=us-west-1
MAIL_ENCRYPTION=null MINIO_BUCKET=
MAIL_FROM_ADDRESS="no-reply@${APP_DOMAIN}" MINIO_URL=
MAIL_FROM_NAME="${APP_NAME}" MINIO_ENDPOINT=
MINIO_USE_PATH_STYLE_ENDPOINT=false
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
@ -61,8 +74,14 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1 PUSHER_APP_CLUSTER=mt1
VITE_SSL_KEY_FILE_PATH="/code/docker/configs/nginx/ssls/${APP_DOMAIN}/${APP_DOMAIN}.key"
VITE_SSL_CERT_FILE_PATH="/code/docker/configs/nginx/ssls/${APP_DOMAIN}/${APP_DOMAIN}.crt"
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_PUSHER_HOST="${PUSHER_HOST}" MIX_PUSHER_HOST="${PUSHER_HOST}"
@ -84,3 +103,13 @@ INTEGRITY_HASH_WEBMANIFEST_JSON=""
INTEGRITY_HASH_MIX_MANIFEST_JSON="" INTEGRITY_HASH_MIX_MANIFEST_JSON=""
INTEGRITY_HASH_APP_CSS="" INTEGRITY_HASH_APP_CSS=""
INTEGRITY_HASH_APP_JS="" INTEGRITY_HASH_APP_JS=""
## Clockwork debug helpers
## default values are set, except for the main enable switch
CLOCKWORK_ENABLE=true
CLOCKWORK_WEB=true
CLOCKWORK_AUTHENTICATION=false
#CLOCKWORK_AUTHENTICATION_PASSWORD=VerySecretPassword
CLOCKWORK_CACHE_COLLECT_VALUES=true
#CLOCKWORK_DATABASE_SLOW_THRESHOLD= # time in miliseconds
#CLOCKWORK_REQUESTS_SLOW_THRESHOLD= # time in miliseconds

View File

@ -1,17 +1,7 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PSR2" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/src/Standards/PSR2/ruleset.xml"> <ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PSR2" xsi:noNamespaceSchemaLocation="vendor/slevomat/coding-standard">
<description>PHP Codesniffer ruleset for this project</description> <description>PHP Codesniffer ruleset for this project</description>
<file>./app/Models</file>
<file>./config</file>
<file>./database</file>
<file>./helpers</file>
<file>./lang</file>
<file>./resources/views</file>
<file>./routes</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<arg name="basepath" value="."/> <arg name="basepath" value="."/>
<arg name="parallel" value="4"/> <arg name="parallel" value="4"/>
<arg name="report" value="full"/> <arg name="report" value="full"/>
@ -24,6 +14,16 @@
<autoload>./vendor/autoload.php</autoload> <autoload>./vendor/autoload.php</autoload>
<file>./app/Models</file>
<file>./config</file>
<file>./database</file>
<file>./helpers</file>
<file>./lang</file>
<file>./resources/views</file>
<file>./routes</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<!-- Don't hide tokenizer exceptions --> <!-- Don't hide tokenizer exceptions -->
<rule ref="Internal.Tokenizer.Exception"> <rule ref="Internal.Tokenizer.Exception">
<type>error</type> <type>error</type>

View File

@ -0,0 +1,6 @@
Contact: mailto:person@domain.com
Contact: https://twitter.com/person
Expires: 2024-09-14T14:00:00.000Z
Encryption: https://keybase.io/person
Encryption: https://domain.com/pgp-key.txt
Preferred-Languages: en

30
src/SECURITY.md Normal file
View File

@ -0,0 +1,30 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/SECURITY.md -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Security Policy](#security-policy)
- [Supported Versions](#supported-versions)
- [Reporting a Vulnerability](#reporting-a-vulnerability)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Security Policy
## Supported Versions
We release patches for security vulnerabilities. Which versions are eligible for
receiving such patches depends on the CVSS v3.0 Rating:
| CVSS v3.0 | Supported Versions |
| --------- | ----------------------------------------- |
| 9.0-10.0 | Releases within the previous three months |
| 4.0-8.9 | Most recent release |
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities to
**[security@ory.sh](mailto:security@ory.sh)**. You will receive a response from
us within 48 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity but historically within a few days.

View File

@ -0,0 +1,28 @@
<?php
namespace App\Actions\Chained;
use App\Models\User;
use Closure;
class GenerateProfilePhoto
{
/**
* Generates a profile photo for a user and saves it
* to disk.
*
* @package App\Actions\Chained\GenerateProfilePhoto
* @since 1.0.0
*
* @param \App\Models\User $user
* @param \Closure $next
*
* @return \App\Models\User
*/
public function __invoke(User $user, Closure $next): User
{
//
return $next($user);
}
}

View File

@ -52,7 +52,7 @@ class CreateUser extends Command
]); ]);
$this->info("$email created successfully."); $this->info("$email created successfully.");
return 0; return Command::SUCCESS;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->error('Unable to create your user.'); $this->error('Unable to create your user.');
$this->line($e->getMessage()); $this->line($e->getMessage());

View File

@ -52,7 +52,7 @@ class ResetPassword extends Command
$column = 'email'; $column = 'email';
$value = $email; $value = $email;
} else { } else {
$column = strtolower($this->choice('What column would you like to search by?', ['ID', 'Email'])); $column = strtolower($this->choice('What database column would you like to search by?', ['ID', 'Email']));
$value = $this->ask("Please provide an $column to search for"); $value = $this->ask("Please provide an $column to search for");
} }
@ -68,7 +68,7 @@ class ResetPassword extends Command
try { try {
$user->update(['password' => Hash::make($password)]); $user->update(['password' => Hash::make($password)]);
$this->info("User {$user->id} ({$user->email}) password update successful!"); $this->info("User {$user->id} ({$user->email}) password update successful!");
return 0; return Command::SUCCESS;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->error('Unable to set the password!'); $this->error('Unable to set the password!');
$this->line($e->getMessage()); $this->line($e->getMessage());

View File

@ -0,0 +1,83 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class TranslationCheckerCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'i18n:check-json {--l|locale= : The source of truth locale}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Uses one JSON file as the source of truth to check other lang files against.';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$masterLocale = 'en';
if (!empty($this->option('locale'))) {
$masterLocale = trim($this->option('locale'));
}
$langDir = base_path('lang');
$langFiles = scandir($langDir);
$bigCount = count($langFiles);
for ($i = 0; $i < $bigCount; $i++) {
if (! preg_match('/.*\.json$/', $langFiles[$i]) || "{$masterLocale}.json" === $langFiles[$i]) {
unset($langFiles[$i]);
}
}
$langFilesCount = count($langFiles);
$this->info("Checking {$langFilesCount} file(s) against {$masterLocale}...");
$masterLocaleTranslations = translations(base_path("lang/{$masterLocale}.json"));
foreach ($langFiles as $localeFile) {
$otherLocaleTranslations = translations(base_path("lang/{$localeFile}"));
$counts = 0;
$mergedTranslations = $this->recursiveMergeArray($masterLocaleTranslations, $otherLocaleTranslations, $counts);
file_put_contents(base_path("lang/{$localeFile}"), json_encode($mergedTranslations, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));
$this->info("{$localeFile} had {$counts} missing translations.");
}
return Command::SUCCESS;
}
public function recursiveMergeArray(array $firstArray, array $secondArray, int &$counts)
{
$mergedArray = $secondArray;
foreach ($firstArray as $key => $value) {
if (is_array($value)) {
if (!array_key_exists($key, $secondArray)) {
$mergedArray[$key] = $value;
} else {
$subDiff = $this->recursiveMergeArray($value, $secondArray[$key], $counts);
if (!empty($subDiff)) {
$mergedArray[$key] = $subDiff;
}
}
} else {
if (!array_key_exists($key, $secondArray)) {
$counts++;
$mergedArray[$key] = $value;
}
}
}
return $mergedArray;
}
}

View File

@ -2,21 +2,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Inertia\Response; use Inertia\Response;
use Jenssegers\Agent\Agent;
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
use Laravel\Jetstream\Http\Controllers\Inertia\Concerns\ConfirmsTwoFactorAuthentication; use Laravel\Jetstream\Http\Controllers\Inertia\UserProfileController as JetstreamUserProfileController;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
class UserProfileController extends Controller class UserProfileController extends JetstreamUserProfileController
{ {
use ConfirmsTwoFactorAuthentication;
/** /**
* Show the general profile settings screen. * Show the general profile settings screen.
* *
@ -36,56 +29,4 @@ class UserProfileController extends Controller
'timezones' => timezone_identifiers_list(), 'timezones' => timezone_identifiers_list(),
]); ]);
} }
/**
* Get the current sessions.
*
* @since 1.0.0
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Support\Collection
*/
public function sessions(Request $request): Collection
{
if (config('session.driver') !== 'database') {
return collect();
}
return collect(
DB::connection(config('session.connection'))->table(config('session.table', 'sessions'))
->where('user_id', $request->user()->getAuthIdentifier())
->orderBy('last_activity', 'desc')
->get()
)->map(function ($session) use ($request) {
$agent = $this->createAgent($session);
return (object) [
'agent' => [
'is_desktop' => $agent->isDesktop(),
'platform' => $agent->platform(),
'browser' => $agent->browser(),
],
'ip_address' => $session->ip_address,
'is_current_device' => $session->id === $request->session()->getId(),
'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans(),
];
});
}
/**
* Create a new agent instance from the given session.
*
* @since 1.0.0
*
* @param mixed $session
*
* @return \Jenssegers\Agent\Agent
*/
protected function createAgent($session): Agent
{
return tap(new Agent, function ($agent) use ($session) {
$agent->setUserAgent($session->user_agent);
});
}
} }

33
src/app/Http/Kernel.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
//...
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
//...
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\SetLocale::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
//...
],
'admin' => [
//...
\Laravel\Jetstream\Http\Middleware\ShareInertiaData::class,
\App\Http\Middleware\SetLocale::class,
\App\Http\Middleware\AuthorizeAdmin::class,
//...
],
];
}

View File

@ -5,22 +5,20 @@ namespace App\Http\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class CheckCustomSessionData class AuthorizeAdmin
{ {
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @since 1.0.0
*
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* *
* @return mixed * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if ((! session()->has('thing') || empty(session('thing'))) && $request->user()) { if ($request->user()->email !== env('ADMIN_EMAIL')) {
session()->put('thing', $request->user()->thing); abort(HTTP_NOT_FOUND);
} }
return $next($request); return $next($request);

View File

@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Models\Language;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Middleware; use Inertia\Middleware;
@ -41,19 +42,58 @@ class HandleInertiaRequests extends Middleware
*/ */
public function share(Request $request): array public function share(Request $request): array
{ {
$localeFields = ['locale_name', 'iso_code', 'name', 'localized_name'];
$currentLocale = $request->session()->get('locale', null);
if (is_null($currentLocale)) {
$currentLocale = Language::where(['locale_name' => 'en', 'iso_code' => 'en_US'])->get($localeFields)[0]->toArray();
$request->session()->put('locale', [
'locale_name' => $currentLocale['locale_name'],
'iso_code' => $currentLocale['iso_code'],
'name' => $currentLocale['name'],
'localized_name' => $currentLocale['localized_name'],
]);
}
$localeFilePath = base_path("lang/{$currentLocale['locale_name']}.json");
$notifications = []; $notifications = [];
$notificationsCount = count($notifications);
$unreadNotifications = false; $unreadNotifications = false;
if (! is_null($request->user())) { if (! is_null($request->user())) {
$notifications = $request->user()->notifications; $notifications = $request->user()->notifications;
$notificationsCount = count($notifications);
for ($i = 0; $i < $notificationsCount; $i++) {
$newData = $notifications[$i]->data;
$createdAt = carbon($notifications[$i]->created_at);
$dateFormat = 'F j';
if (!$createdAt->is(gmdate('Y'))) {
$dateFormat = 'F j, Y';
}
$newData['created_at_date'] = $createdAt->copy()->format($dateFormat);
$newData['created_at_time'] = $createdAt->copy()->format('H:i');
$notifications[$i]->data = $newData;
}
if (count($request->user()->unreadNotifications) > 0) { if (count($request->user()->unreadNotifications) > 0) {
$unreadNotifications = true; $unreadNotifications = true;
} }
} }
return array_merge(parent::share($request), [ $additionalData = [
'appName' => config('app.name'), 'appName' => config('app.name'),
'availableLocales' => Language::get($localeFields),
'currentLocale' => $currentLocale,
'language' => translations($localeFilePath),
'notifications' => $notifications, 'notifications' => $notifications,
'unreadNotifications' => $unreadNotifications, 'unreadNotifications' => $unreadNotifications,
]); ];
if (! is_null($request->user())) {
if ($request->user()->email === env('ADMIN_EMAIL')) {
$additionalData['is_admin_user'] = true;
}
}
return array_merge(parent::share($request), $additionalData);
} }
} }

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SetLocale
{
/**
* Handle an incoming request.
*
* @package App\Http\Middleware\SetLocale
* @since 1.0.0
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
*
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if (session()->has('locale')) {
app()->setLocale(session('locale')['locale']);
}
return $next($request);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Models\Casts;
use Brick\Money\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class MoneyCastable implements CastsAttributes
{
/**
* Get the value from the database and modify it to casted type
* before returning.
*
* @package App\Models\Casts\MoneyCastable
* @since 1.0.0
*
* @param \Illuminate\Database\Eloquent\Model $model The Model that is being used
* @param string $key The attribute key
* @param mixed $value The value stored in the database
* @param array $attributes The array of model attributes
*
* @return \Brick\Money\Money
*/
public function get(Model $model, string $key, $value, array $attributes): Money
{
return Money::ofMinor($attributes['amount'], $attributes['currency']);
}
/**
* Store the data .
*
* @package App\Models\Casts\MoneyCastable
* @since 1.0.0
*
* @param \Illuminate\Database\Eloquent\Model $model The Model that is being used
* @param string $key The attribute key
* @param mixed $value The value stored in the database
* @param array $attributes The array of model attributes
*
* @return mixed
*/
public function set(Model $model, string $key, $value, array $attributes)
{
if (! $value instanceof Money) {
return $value;
}
return $value->getMinorAmount()->toInt();
}
}

117
src/app/Models/Language.php Normal file
View File

@ -0,0 +1,117 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Language extends Model
{
use HasFactory;
use Prunable;
use SoftDeletes;
/** @var string */
protected $table = 'languages';
/** @var array<int,string> */
protected $fillable = [
'locale_name',
'iso_code',
'name',
'localized_name',
];
/**
|--------------------------------------------------------------------------
| Class Constants
|--------------------------------------------------------------------------
|
*/
//
/**
|--------------------------------------------------------------------------
| Custom/Private Methods
|--------------------------------------------------------------------------
|
*/
/**
* Get the prunable model query.
*
* @package App\Models\Language
* @since 1.0.0
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function prunable(): Builder
{
//return static::where('deleted_at', '<=', now()->subMonth());
}
/**
* Prepare the model for pruning.
*
* @package App\Models\Language
* @since 1.0.0
*
* @return void
*/
protected function pruning(): void
{
//
}
/**
|--------------------------------------------------------------------------
| Accessors
|--------------------------------------------------------------------------
|
*/
//
/**
|--------------------------------------------------------------------------
| Mutators
|--------------------------------------------------------------------------
|
*/
//
/**
|--------------------------------------------------------------------------
| Scopes
|--------------------------------------------------------------------------
|
*/
//
/**
|--------------------------------------------------------------------------
| Relationships
|--------------------------------------------------------------------------
|
*/
/**
* User relationship.
*
* @package App\Models\Language
* @since 1.0.0
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class OnTeamScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
*
* @return void
*/
public function apply(Builder $builder, Model $model): void
{
$builder->where('current_team_id', );
}
}

View File

@ -2,54 +2,50 @@
namespace App\Models\Traits; namespace App\Models\Traits;
use Illuminate\Database\Eloquent\Casts\Attribute;
trait FormattedPhoneTrait trait FormattedPhoneTrait
{ {
/** /**
* Format a phone number to be human readable. * Format a phone number to be human readable.
* *
* @since 1.0.0 * @package Namespace\App\Contact
* * @since 1.0.0
* @return string
*/
public function getPhoneNumberAttribute(): string
{
$phoneLength = strlen($this->phone);
$phoneNumber = preg_replace('//', '', $this->phone);
if ($phoneLength > 10) {
$countryCode = substr($phoneNumber, 0, $phoneLength - 10);
$areaCode = substr($phoneNumber, -10, 3);
$nextThree = substr($phoneNumber, -7, 3);
$lastFour = substr($phoneNumber, -4, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 10) {
$areaCode = substr($phoneNumber, 0, 3);
$nextThree = substr($phoneNumber, 3, 3);
$lastFour = substr($phoneNumber, 6, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 7) {
$nextThree = substr($phoneNumber, 0, 3);
$lastFour = substr($phoneNumber, 3, 4);
$phoneNumber = "{$nextThree}-{$lastFour}";
}
return $phoneNumber;
}
/**
* Remove all non-numeric characters from the phone number.
*
* @since 1.0.0
*
* @param string $value
* *
* @return void * @return void
*/ */
public function setPhoneNumberAttribute($value): void protected function phoneNumber(): Attribute
{ {
$this->attributes['phone'] = preg_replace('/[^0-9]/', '', $value); return Attribute::make(
get: function (mixed $value, array $attributes) {
$phoneLength = strlen($attributes['phone_number']);
$phoneNumber = preg_replace('/[^0-9]/', '', $attributes['phone_number']);
if ($phoneLength > 10) {
$countryCode = substr($phoneNumber, 0, $phoneLength - 10);
$areaCode = substr($phoneNumber, -10, 3);
$nextThree = substr($phoneNumber, -7, 3);
$lastFour = substr($phoneNumber, -4, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 10) {
$areaCode = substr($phoneNumber, 0, 3);
$nextThree = substr($phoneNumber, 3, 3);
$lastFour = substr($phoneNumber, 6, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 7) {
$nextThree = substr($phoneNumber, 0, 3);
$lastFour = substr($phoneNumber, 3, 4);
$phoneNumber = "{$nextThree}-{$lastFour}";
}
return $phoneNumber;
},
set: function (mixed $value) {
return preg_replace('/[^0-9]/', '', $value);
},
);
} }
} }

View File

@ -0,0 +1,88 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
trait HasProfilePhoto
{
/**
* Update the user's profile photo.
*
* @param \Illuminate\Http\UploadedFile $photo
* @param string $storagePath
* @return void
*/
public function updateProfilePhoto(UploadedFile $photo, $storagePath = 'profile-photos')
{
tap($this->profile_photo_path, function ($previous) use ($photo, $storagePath) {
$this->forceFill([
'profile_photo_path' => $photo->storePublicly(
$storagePath, ['disk' => $this->profilePhotoDisk()]
),
])->save();
if ($previous) {
Storage::disk($this->profilePhotoDisk())->delete($previous);
}
});
}
/**
* Delete the user's profile photo.
*
* @return void
*/
public function deleteProfilePhoto()
{
if (! Features::managesProfilePhotos()) {
return;
}
if (is_null($this->profile_photo_path)) {
return;
}
Storage::disk($this->profilePhotoDisk())->delete($this->profile_photo_path);
$this->forceFill([
'profile_photo_path' => null,
])->save();
}
/**
* Get the URL to the user's profile photo.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function profilePhotoUrl(): Attribute
{
return Attribute::get(function () {
return $this->profile_photo_path
? Storage::disk($this->profilePhotoDisk())->url($this->profile_photo_path)
: $this->defaultProfilePhotoUrl();
});
}
/**
* Get the default profile photo URL if no profile photo has been uploaded.
*
* @return string
*/
protected function defaultProfilePhotoUrl()
{
return 'https://avatars.test/avatar?size=256';
}
/**
* Get the disk that profile photos should be stored on.
*
* @return string
*/
protected function profilePhotoDisk()
{
return isset($_ENV['VAPOR_ARTIFACT_NAME']) ? 's3' : config('jetstream.profile_photo_disk', 'public');
}
}

View File

@ -37,6 +37,7 @@ class User extends Authenticatable
'name', 'name',
'surname', 'surname',
'timezone_name', 'timezone_name',
'language_id',
'current_team_id', 'current_team_id',
'profile_photo_path', 'profile_photo_path',
'email', 'email',
@ -183,4 +184,17 @@ class User extends Authenticatable
{ {
return $this->morphOne(Address::class, 'addressable'); return $this->morphOne(Address::class, 'addressable');
} }
/**
* Language relationship.
*
* @package App\Models\User
* @since 1.0.0
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function language(): BelongsTo
{
return $this->belongsTo(Language::class);
}
} }

View File

@ -0,0 +1,125 @@
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* Handle events after all transactions are committed.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @var bool
*/
public $afterCommit = true;
/**
* Handle the User "retrieve" event.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @param \App\Models\User $user
*
* @return void
*/
public function retrieved(User $user)
{
//
}
/**
* Handle the User "saving" event.
* This may apply as either a create
* or an update event.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @param \App\Models\User $user
*
* @return void
*/
public function saving(User $user)
{
//
}
/**
* Handle the User "created" event.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @param \App\Models\User $user
*
* @return void
*/
public function created(User $user)
{
//
}
/**
* Handle the User "updated" event.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @param \App\Models\User $user
*
* @return void
*/
public function updated(User $user)
{
//
}
/**
* Handle the User "deleted" event.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @param \App\Models\User $user
*
* @return void
*/
public function deleted(User $user)
{
//
}
/**
* Handle the User "restored" event.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @param \App\Models\User $user
*
* @return void
*/
public function restored(User $user)
{
//
}
/**
* Handle the User "forceDeleted" event.
*
* @package App\Observers\UserObserver
* @since 1.0.0
*
* @param \App\Models\User $user
*
* @return void
*/
public function forceDeleted(User $user)
{
//
}
}

View File

@ -17,15 +17,25 @@
"spatie/laravel-permission": "", "spatie/laravel-permission": "",
"zizaco/entrust": "", "zizaco/entrust": "",
// Security stuff
"abuseipdb/laravel": "", // API to check an IP for abusive behavior with option to report IP for abusive behavior
// Options/Flags
"spatie/laravel-model-flags": "", // add a simple true/false flag to any model
// SDKs // SDKs
"aws/aws-sdk-php-laravel": "", // interact with S3 or S3-compatible storage "aws/aws-sdk-php-laravel": "", // interact with S3 or S3-compatible storage
"propaganistas/laravel-phone": "", // uses Google's libphonenumber API to (try to) sanely handle phone numbers "propaganistas/laravel-phone": "", // uses Google's libphonenumber API to (try to) sanely handle phone numbers
"vladimir-yuldashev/laravel-queue-rabbitmq": "", // Interface with RabbitMQ servers "vladimir-yuldashev/laravel-queue-rabbitmq": "", // Interface with RabbitMQ servers
"grimzy/laravel-mysql-spatial": "", // Adds Geo-spatial column support for MySQL "grimzy/laravel-mysql-spatial": "", // Adds Geo-spatial column support for MySQL (WARNING: only works with Laravel v8 or lower)
"matanyadaev/laravel-eloquent-spatial": "", // Adds Geo-spatial column support for MySQL
"irazasyed/telegram-bot-sdk": "", // unofficial Telegram Bot API/SDK "irazasyed/telegram-bot-sdk": "", // unofficial Telegram Bot API/SDK
"torann/geoip": "", // support for multiple GeoIP services "torann/geoip": "", // support for multiple GeoIP services
"coderjerk/nasa-php": "", // NASA API integrations "coderjerk/nasa-php": "", // NASA API integrations
// File management
"czim/laravel-paperclip": "", // attach files to an Eloquent model
// Media (image/video/audio) Management // Media (image/video/audio) Management
"intervention/image": "", // image manipulation "intervention/image": "", // image manipulation
"intervention/imagecache": "", // caching for intervention/image "intervention/imagecache": "", // caching for intervention/image
@ -33,11 +43,15 @@
"spatie/laravel-image-optimizer": "", // optimize png, jpg, svg, and gif "spatie/laravel-image-optimizer": "", // optimize png, jpg, svg, and gif
"unisharp/laravel-filemanager": "", // File manager (including uploads) "unisharp/laravel-filemanager": "", // File manager (including uploads)
"pbmedia/laravel-ffmpeg": "", // Integration with FFMpeg "pbmedia/laravel-ffmpeg": "", // Integration with FFMpeg
"maestroerror/php-heic-to-jpg": "", // Use Go(?) to convert HEIC/HEIF to JPEG
// Localization // Localization
"mcamara/laravel-localization": "", // localization that also handles route translations "mcamara/laravel-localization": "", // localization that also handles route translations
"barryvdh/laravel-translation-manager": "", // self-evident "barryvdh/laravel-translation-manager": "", // self-evident
// PDF stuff
"creagia/laravel-sign-pad": "", // allows signatures on documents
// Geo-spatial // Geo-spatial
"brick/geo": "", // GIS geometry library "brick/geo": "", // GIS geometry library
"stevebauman/location": "", // retrieve a user's location by their IP address "stevebauman/location": "", // retrieve a user's location by their IP address
@ -52,12 +66,18 @@
"spatie/laravel-sitemap": "", // generate site maps "spatie/laravel-sitemap": "", // generate site maps
"artesaos/seotools": "", // automatic SEO tools "artesaos/seotools": "", // automatic SEO tools
"eumanito/php-capitalize-names": "", // Capitalize names with funky rules "eumanito/php-capitalize-names": "", // Capitalize names with funky rules
"robinvdvleuten/ulid": "", // Universally Unique Lexicographically Sortable Identifier (ULID)
// Others/uncategorized // Others/uncategorized
"appstract/laravel-opcache": "", // provides artisan commands to interact with opcache (not sure if site-specific) "appstract/laravel-opcache": "", // provides artisan commands to interact with opcache (not sure if site-specific)
"brick/math": "", // arbitrary-precision arithmetic library "brick/math": "", // arbitrary-precision arithmetic library
"brick/money": "", // money and currency library "brick/money": "", // money and currency library
"protonemedia/laravel-verify-new-email": "", // must verify new email address before email update will be completed
"protonemedia/inertiajs-tables-laravel-query-builder": "", // datatables for InertiaJS/Vue and Laravel
"ikechukwukalu/requirepin": "", // use password/pin protection for routes
"doctrine/dbal": "", // useful for artisan db:show "doctrine/dbal": "", // useful for artisan db:show
}, },
"require-dev": { "require-dev": {

View File

@ -2,9 +2,11 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\Language;
use App\Models\Team; use App\Models\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Jetstream\Features; use Laravel\Jetstream\Features;
@ -31,6 +33,7 @@ class UserFactory extends Factory
'email' => $this->faker->unique()->safeEmail(), 'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(), 'email_verified_at' => now(),
'timezone_name' => $this->faker->timezone(), 'timezone_name' => $this->faker->timezone(),
'language_id' => Language::all()->random()->id,
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10), 'remember_token' => Str::random(10),
]; ];
@ -69,4 +72,25 @@ class UserFactory extends Factory
'ownedTeams' 'ownedTeams'
); );
} }
/**
* Indicate that the user should have an avatar image.
*
* @return $this
*/
public function withAvatar()
{
if (! Features::enabled(Features::profilePhotos())) {
return $this->state([]);
}
$imageUrl = 'https://avatars.test/';
$imageContents = file_get_contents($imageUrl);
$imageName = md5($imageContents) . '.jpg';
Storage::put("public/avatars/{$imageName}", $imageContents);
return $this->state([
'profile_photo_path' => "avatars/{$imageName}"
]);
}
} }

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('languages', function (Blueprint $table) {
$table->id();
$table->string('locale_name')->index();
$table->string('iso_code')->unique();
$table->string('name');
$table->string('localized_name');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('languages');
}
};

View File

@ -20,7 +20,8 @@ return new class extends Migration
$table->string('email')->unique(); $table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable(); $table->timestamp('email_verified_at')->nullable();
$table->string('timezone_name')->default('UTC'); $table->string('timezone_name')->default('UTC');
$table->string('current_team_id', 64)->nullable(); $table->foreignId('language_id');
$table->foreignId('current_team_id')->nullable();
$table->string('profile_photo_path', 2048)->nullable(); $table->string('profile_photo_path', 2048)->nullable();
$table->string('password'); $table->string('password');
$table->text('two_factor_secret')->nullable(); $table->text('two_factor_secret')->nullable();

View File

@ -16,55 +16,18 @@ class LanguageSeeder extends Seeder
*/ */
public function run() public function run()
{ {
$languages = [ $languagesData = [
[ ['locale' => 'en', 'iso_code' => 'en_US', 'name' => 'English', 'localized_name' => 'English (US)'],
'iso_code' => 'en_US', ['locale' => 'de', 'iso_code' => 'de_DE', 'name' => 'German', 'localized_name' => 'Deutsch'],
'locale' => 'en', ['locale' => 'fr', 'iso_code' => 'fr_FR', 'name' => 'French', 'localized_name' => 'Français'],
'country_code' => 'US', ['locale' => 'jp', 'iso_code' => 'jp_JP', 'name' => 'Japanese', 'localized_name' => '日本'],
'title' => 'English', ['locale' => 'mx', 'iso_code' => 'es_MX', 'name' => 'Spanish', 'localized_name' => 'Español'],
'title_localized' => 'English',
],
[
'iso_code' => 'de_DE',
'locale' => 'de',
'country_code' => 'DE',
'title' => 'German',
'title_localized' => 'Deutsch',
],
[
'iso_code' => 'fr_FR',
'locale' => 'fr',
'country_code' => 'FR',
'title' => 'French',
'title_localized' => 'Français',
],
[
'iso_code' => 'es_SP',
'locale' => 'es',
'country_code' => 'SP',
'title' => 'Spanish',
'title_localized' => 'Español',
],
[
'iso_code' => 'jp_JP',
'locale' => 'jp',
'country_code' => 'JP',
'title' => 'Japanese',
'title_localized' => '日本',
],
[
'iso_code' => 'zh_TW',
'locale' => 'zh',
'country_code' => 'TW',
'title' => 'Taiwanese',
'title_localized' => '台湾',
],
]; ];
$startTime = Carbon::now(); $startTime = Carbon::now();
$offset = 0; $offset = 0;
foreach ($languages as $language) { foreach ($languagesData as $language) {
$datetime = $startTime->copy()->addMinute($offset)->toDateTimeString(); $datetime = $startTime->copy()->addMinute($offset)->toDateTimeString();
$offset++; $offset++;
$language['created_at'] = $datetime; $language['created_at'] = $datetime;

View File

@ -24,6 +24,7 @@ $httpCodes = [
204 => 'HTTP_NO_CONTENT', 204 => 'HTTP_NO_CONTENT',
205 => 'HTTP_RESET_CONTENT', 205 => 'HTTP_RESET_CONTENT',
206 => 'HTTP_PARTIAL_CONTENT', 206 => 'HTTP_PARTIAL_CONTENT',
218 => 'HTTP_THIS_IS_FINE', // Apache specific
// 300's (redirections / "go away") // 300's (redirections / "go away")
300 => 'HTTP_MULTIPLE_CHOICE', 300 => 'HTTP_MULTIPLE_CHOICE',
@ -57,12 +58,14 @@ $httpCodes = [
236 => 'HTTP_UPGRADE_REQUIRED', 236 => 'HTTP_UPGRADE_REQUIRED',
428 => 'HTTP_PRECONDITION_REQUIRED', 428 => 'HTTP_PRECONDITION_REQUIRED',
429 => 'HTTP_TOO_MANY_REQUESTS', 429 => 'HTTP_TOO_MANY_REQUESTS',
444 => 'HTTP_NO_RESPONSE', // Nginx specific, used internally to return no info and close connection immediately
451 => 'HTTP_GAG_ORDER', 451 => 'HTTP_GAG_ORDER',
// 500's (server-level problem, process died or configuration is incorrect / "server screwed up") // 500's (server-level problem, process died or configuration is incorrect / "server screwed up")
500 => 'HTTP_SERVER_ERROR', 500 => 'HTTP_SERVER_ERROR',
501 => 'HTTP_NOT_IMPLEMENTED', 501 => 'HTTP_NOT_IMPLEMENTED',
503 => 'HTTP_UNAVAILABLE', 503 => 'HTTP_UNAVAILABLE',
529 => 'HTTP_SITE_OVERLOADED', // Qualys specific, API response to indicate server resources are over capacity
530 => 'HTTP_SUSPENDED', 530 => 'HTTP_SUSPENDED',
]; ];

View File

@ -0,0 +1,76 @@
<?php
/**
|--------------------------------------------------------------------------
| Global Date/Time Functions
|--------------------------------------------------------------------------
|
| This is a home for functions that don't belong to any one class and
| that should be available anywhere in the application.
|
*/
if (! function_exists('secondsToTime')) {
/**
* Convert seconds to hour:minute:second with hour being omitted
* if the amount of seconds is less than an hour.
*
* @since 1.0.0
*
* @param datatype $seconds
*
* @return string
*/
function secondsToTime($seconds): string
{
$seconds = intval($seconds);
if ($seconds >= 3600) {
return sprintf('%02d:%02d:%02d', ($seconds / 3600), (($seconds / 60) % 60), ($seconds % 60));
}
return sprintf('%02d:%02d', (($seconds / 60) % 60), ($seconds % 60));
}
}
if (! function_exists('minutesToTime')) {
/**
* Convert minutes to HH:MM
*
* @since 1.0.0
*
* @param datatype $minutes
*
* @return string
*/
function minutesToTime($minutes): string
{
$minutes = intval($minutes);
return sprintf('%d:%02d', floor($minutes / 60), ($minutes % 60));
}
}
if (! function_exists('jddayofweek')) {
/**
* Returns the day of the week. Can return a string or an integer depending on the mode.
*
* @since 1.0.0
*
* @param int|null $intDay
* @param int $mode
*
* @return string
*/
function jddayofweek(?int $intDay = null, int $mode = 0): string
{
if (is_null($intDay)) {
$intDay = date('l');
}
if ($mode === 0) {
return $intDay;
}
return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][$intDay];
}
}

View File

@ -0,0 +1,233 @@
<?php
/**
|--------------------------------------------------------------------------
| Global Distance Functions
|--------------------------------------------------------------------------
|
| This is a home for functions that don't belong to any one class and
| that should be available anywhere in the application.
|
*/
/**
|--------------------------------------------------------------------------
| Metric functions
|--------------------------------------------------------------------------
| Functions that convert from metric to imperial or from a smaller
| metric unit to a larger one.
|
*/
if (! function_exists('centimeters2Inches')) {
/**
* Convert from centimeters to inches.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Inches($centimeters, int $preceision = 1): float
{
return round(($centimeters / 2.54), $preceision);
}
}
if (! function_exists('centimeters2Feet')) {
/**
* Convert from centimeters to inches.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Feet($centimeters, int $preceision = 1): float
{
return round((($centimeters / 2.54) * 12), $preceision);
}
}
if (! function_exists('centimeters2Meters')) {
/**
* Convert from centimeters to meters.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Meters($centimeters, int $preceision = 1): float
{
return round(($centimeters / 100), $preceision);
}
}
if (! function_exists('centimeters2Yards')) {
/**
* Convert from centimeters to yards.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Yards($centimeters, int $preceision = 1): float
{
return round(($centimeters / 91.44), $preceision);
}
}
if (! function_exists('meters2Miles')) {
/**
* Convert from meters to miles.
*
* @since 1.0.0
*
* @param float|int|string $meters
* @param int $precision
*
* @return float
*/
function meters2Miles($meters, int $preceision = 1): float
{
return round(($meters * 0.00062137), $preceision);
}
}
if (! function_exists('kilometers2Miles')) {
/**
* Convert from kilometers to meters.
*
* @since 1.0.0
*
* @param float|int|string $kilometers
* @param int $precision
*
* @return float
*/
function kilometers2Miles($kilometers, int $preceision = 1): float
{
return round(($kilometers * 1.609), $preceision);
}
}
if (! function_exists('meters2Kilometers')) {
/**
* Convert from meters to kilometers.
*
* @since 1.0.0
*
* @param float|int|string $meters
* @param int $precision
*
* @return float
*/
function meters2Kilometers($meters, int $preceision = 1): float
{
return round(($meters / 1000), $preceision);
}
}
if (! function_exists('millimeters2Inches')) {
/**
* Convert from millimeters to inches.
*
* @since 1.0.0
*
* @param float|int|string $millimeters
* @param int $precision
*
* @return float
*/
function millimeters2Inches($millimeters, int $preceision = 1): float
{
return round(($millimeters / 25.4), $preceision);
}
}
/**
|--------------------------------------------------------------------------
| Imperial functions
|--------------------------------------------------------------------------
| Functions that convert from imperial to metric or from a smaller
| imperial unit to a larger one.
|
*/
if (! function_exists('inches2Millimeters')) {
/**
* Convert from inches to millimeters.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Millimeters($inches, int $preceision = 1): float
{
return round(($inches * 2.54), $preceision);
}
}
if (! function_exists('inches2Meters')) {
/**
* Convert from inches to meters.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Meters($inches, int $preceision = 1): float
{
return round(($inches / 39.37), $preceision);
}
}
if (! function_exists('inches2Yards')) {
/**
* Convert from inches to yards.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Yards($inches, int $preceision = 1): float
{
return round(($inches / 36), $preceision);
}
}
if (! function_exists('inches2Feet')) {
/**
* Convert from inches to feet.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Feet($inches, int $preceision = 1): float
{
return round(($inches * 12), $preceision);
}
}

View File

@ -0,0 +1,29 @@
<?php
// phpcs:ignore
/**
|--------------------------------------------------------------------------
| Global String Functions
|--------------------------------------------------------------------------
|
| This is a home for functions that don't belong to any one class and
| that should be available anywhere in the application.
|
*/
if (! function_exists('snake2Title')) {
/**
* Convert a snake case string to a title with spaces
* and every word capitalized.
*
* @param string $snakeSlug A snake case string, commonly a slug
*
* @since 1.0.0
*
* @return string
*/
function snake2Title(string $snakeSlug): string
{
$output = preg_replace('/\_/', ' ', $snakeSlug);
return ucwords($output);
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
|--------------------------------------------------------------------------
| Global Temperature Functions
|--------------------------------------------------------------------------
|
| This is a home for functions that don't belong to any one class and
| that should be available anywhere in the application.
|
*/
if (! function_exists('celsius2Fahrenheit')) {
/**
* Convert from celsius to fahrenheit.
*
* @since 1.0.0
*
* @param float|int|string $celsius
* @param int $precision
*
* @return float
*/
function celsius2Fahrenheit($celsius, int $preceision = 0): float
{
return round((($celsius * (9/5)) + 32), $preceision);
}
}
if (! function_exists('fahrenheit2Celsius')) {
/**
* Convert from fahrenheit to celsius.
*
* @since 1.0.0
*
* @param float|int|string $fahrenheit
* @param int $precision
*
* @return float
*/
function fahrenheit2Celsius($fahrenheit, int $preceision = 1): float
{
return round(($fahrenheit - 32 * (5/9)), $preceision);
}
}

View File

@ -1,5 +1,10 @@
<?php <?php
require_once "functions/strings.php";
require_once "functions/date_and_time.php";
require_once "functions/distances.php";
require_once "functions/temperatures.php";
/** /**
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Global Functions | Global Functions
@ -10,21 +15,72 @@
| |
*/ */
if (! function_exists('snake2Title')) { if (! function_exists('clamp')) {
/** /**
* Convert a snake case string to a title with spaces * Ensure a numerical value is between two bounds.
* and every word capitalized.
* *
* @since 1.0.0 * @since 1.0.0
* *
* @param string $stakeSlug A snake case string, commonly a slug * @param int|float|string $number The value to be clamped between two other values.
* @param int|float|string $minNumber The miminum value for clamping bounds.
* @param int|float|string $maxNumber The maximum value for clamping bounds.
*
* @throws \Exception
*
* @return int|float
*/
function clamp($number, $minNumber, $maxNumber)
{
if (! is_numeric($number)) {
throw new Exception('Clamp number must be numeric in value.');
}
if (! is_numeric($minNumber)) {
throw new Exception('Clamped minimum number must be numeric in value.');
}
if (! is_numeric($maxNumber)) {
throw new Exception('Clamped maximum number must be numeric in value.');
}
$returnValue = $number;
if ($minNumber >= $number) {
$returnValue = $minNumber;
}
if ($maxNumber <= $number) {
$returnValue = $maxNumber;
}
return $returnValue;
}
}
if (! function_exists('humanBytes')) {
/**
* Convert bytes to a human-friendly format.
*
* @since 1.0.0
*
* @param int|string $number
* @param int|string|null $precision Decimal places to show (optional)
* *
* @return string * @return string
*/ */
function snake2Title(string $snakeSlug): string function humanBytes($number, $precision = null): string
{ {
$output = preg_replace('/\_/', ' ', $snakeSlug); if (empty($number)) {
return ucwords($output); return '0 B';
}
static $units = ['B', 'KB', 'MB', 'GB'];
static $bytePreceision = [0, 0, 1, 2];
$divisor = 1024;
for ($i = 0; $number / $divisor >= 0.9 && $i < 4; $i++) {
$number /= $divisor;
}
return round($number, is_null($precision) ? $bytePreceision[$i] : $precision) . $units[$i];
} }
} }
@ -50,28 +106,50 @@ if (! function_exists('carbon')) {
} }
} }
if (! function_exists('jddayofweek')) { if (! function_exists('translations')) {
/** /**
* Returns the day of the week. Can return a string or an integer depending on the mode. * Loads translations from a JSON file.
* *
* @since 1.0.0 * @since 1.0.0
* *
* @param int|null $intDay * @param string $jsonFilePath
* @param int $mode
* *
* @return string * @return array
*/ */
function jddayofweek(?int $intDay = null, int $mode = 0): string function translations(string $jsonFilePath): array
{ {
if (is_null($intDay)) { if (! file_exists($jsonFilePath)) {
$intDay = date('l'); return [];
} }
if ($mode === 0) { $payload = [];
return $intDay; $jsonData = json_decode(file_get_contents($jsonFilePath), true);
switch (json_last_error()) {
case JSON_ERROR_NONE:
$payload = $jsonData;
break;
case JSON_ERROR_DEPTH:
throw new \Exception('Maximum stack depth exceeded');
break;
case JSON_ERROR_STATE_MISMATCH:
throw new \Exception('Underflow or the modes mismatch');
break;
case JSON_ERROR_CTRL_CHAR:
throw new \Exception('Unexpected control character found');
break;
case JSON_ERROR_SYNTAX:
throw new \Exception('Syntax error, malformed JSON');
break;
case JSON_ERROR_UTF8:
throw new \Exception('Malformed UTF-8 characters, possibly incorrectly encoded');
break;
default:
throw new \Exception('Generic or unknown JSON error while decoding');
break;
} }
return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][$intDay]; return $payload;
} }
} }
@ -158,7 +236,7 @@ if (! function_exists('maybe_unserialize')) {
* *
* @param mixed $data * @param mixed $data
* *
* @return mixed * @return array|string|bool
*/ */
function maybe_unserialize($data) function maybe_unserialize($data)
{ {
@ -174,284 +252,3 @@ if (! function_exists('maybe_unserialize')) {
return $data; return $data;
} }
} }
/**
|--------------------------------------------------------------------------
| Metric functions
|--------------------------------------------------------------------------
| Functions that convert from metric to imperial or from a smaller
| metric unit to a larger one.
|
*/
if (! function_exists('centimeters2Inches')) {
/**
* Convert from centimeters to inches.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Inches($centimeters, int $preceision = 1): float
{
return round(($centimeters / 2.54), $preceision);
}
}
if (! function_exists('centimeters2Feet')) {
/**
* Convert from centimeters to inches.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Feet($centimeters, int $preceision = 1): float
{
return round((($centimeters / 2.54) * 12), $preceision);
}
}
if (! function_exists('centimeters2Meters')) {
/**
* Convert from centimeters to meters.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Meters($centimeters, int $preceision = 1): float
{
return round(($centimeters / 100), $preceision);
}
}
if (! function_exists('centimeters2Yards')) {
/**
* Convert from centimeters to yards.
*
* @since 1.0.0
*
* @param float|int|string $centimeters
* @param int $precision
*
* @return float
*/
function centimeters2Yards($centimeters, int $preceision = 1): float
{
return round(($centimeters / 91.44), $preceision);
}
}
if (! function_exists('meters2Miles')) {
/**
* Convert from meters to miles.
*
* @since 1.0.0
*
* @param float|int|string $meters
* @param int $precision
*
* @return float
*/
function meters2Miles($meters, int $preceision = 1): float
{
return round(($meters * 0.00062137), $preceision);
}
}
if (! function_exists('kilometers2Miles')) {
/**
* Convert from kilometers to meters.
*
* @since 1.0.0
*
* @param float|int|string $kilometers
* @param int $precision
*
* @return float
*/
function kilometers2Miles($kilometers, int $preceision = 1): float
{
return round(($kilometers * 1.609), $preceision);
}
}
if (! function_exists('meters2Kilometers')) {
/**
* Convert from meters to kilometers.
*
* @since 1.0.0
*
* @param float|int|string $meters
* @param int $precision
*
* @return float
*/
function meters2Kilometers($meters, int $preceision = 1): float
{
return round(($meters / 1000), $preceision);
}
}
if (! function_exists('celsius2Fahrenheit')) {
/**
* Convert from celsius to fahrenheit.
*
* @since 1.0.0
*
* @param float|int|string $celsius
* @param int $precision
*
* @return float
*/
function celsius2Fahrenheit($celsius, int $preceision = 0): float
{
return round((($celsius * (9/5)) + 32), $preceision);
}
}
if (! function_exists('millimeters2Inches')) {
/**
* Convert from millimeters to inches.
*
* @since 1.0.0
*
* @param float|int|string $millimeters
* @param int $precision
*
* @return float
*/
function millimeters2Inches($millimeters, int $preceision = 1): float
{
return round(($millimeters / 25.4), $preceision);
}
}
/**
|--------------------------------------------------------------------------
| Imperial functions
|--------------------------------------------------------------------------
| Functions that convert from imperial to metric or from a smaller
| imperial unit to a larger one.
|
*/
if (! function_exists('inches2Millimeters')) {
/**
* Convert from inches to millimeters.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Millimeters($inches, int $preceision = 1): float
{
return round(($inches * 2.54), $preceision);
}
}
if (! function_exists('inches2Meters')) {
/**
* Convert from inches to meters.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Meters($inches, int $preceision = 1): float
{
return round(($inches / 39.37), $preceision);
}
}
if (! function_exists('inches2Yards')) {
/**
* Convert from inches to yards.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Yards($inches, int $preceision = 1): float
{
return round(($inches / 36), $preceision);
}
}
if (! function_exists('inches2Feet')) {
/**
* Convert from inches to feet.
*
* @since 1.0.0
*
* @param float|int|string $inches
* @param int $precision
*
* @return float
*/
function inches2Feet($inches, int $preceision = 1): float
{
return round(($inches * 12), $preceision);
}
}
if (! function_exists('fahrenheit2Celsius')) {
/**
* Convert from fahrenheit to celsius.
*
* @since 1.0.0
*
* @param float|int|string $fahrenheit
* @param int $precision
*
* @return float
*/
function fahrenheit2Celsius($fahrenheit, int $preceision = 1): float
{
return round(($fahrenheit - 32 * (5/9)), $preceision);
}
}
/**
|--------------------------------------------------------------------------
| Miscellaneous functions
|--------------------------------------------------------------------------
| Functions that don't really belong to the other two categories.
|
*/
if (! function_exists('pa2Mbar')) {
/**
* Convert from pascals to milibars.
*
* @since 1.0.0
*
* @param float|int|string $pascals
* @param int $precision
*
* @return float
*/
function pa2Mbar($pascals, int $preceision = 1): float
{
return round(($pascals / 100), $preceision);
}
}

View File

@ -1,3 +1,88 @@
{ {
"key": "translation that has :attribute in the string." "titles": {
"welcome": "Welcome",
"dashboard": "Dashboard",
"secure_area": "Secure Area",
"forgot_password": "Forgot Password",
"my_profile": "My Profile",
"edit_profile": "Edit Profile",
"log_in": "Log In To Your Account",
"register": "Register A New Account",
"reset_password": "Reset Password",
"two_factor_confirmation": "Two-factor Confirmation",
"verify_email": "Email Verification"
},
"labels": {
"type": "Type",
"none": "None",
"title": "Title",
"name": "Name",
"created_at": "Created At",
"updated_at": "Updated At",
"actions": "Actions",
"email": "Email",
"timezone_name": "Timezone",
"language": "Language",
"password": "Password",
"confirm_password": "Confirm Password",
"current_password": "Current Password",
"remember_me": "Remember me",
"first_name": "First Name",
"last_name": "Surname",
"forgot_password": "Forgot your password?",
"already_registered": "Already registered?",
"code": "Code",
"recovery_code": "Recovery Code",
"use_recovery_code": "Use a recovery code",
"use_authentication_code": "Use an authentication code",
"email_verification": "Email Verification",
"user": "User",
"optional": "optional"
},
"actions": {
"save": "Save",
"update": "Update",
"edit": "Edit",
"delete": "Delete",
"confirm": "Confirm",
"log_in": "Log in",
"log_out": "Log Out",
"register": "Register",
"edit_profile": "Edit Profile",
"confirm_password": "Confirm Password",
"reset_password": "Reset Password",
"email_password_reset_link": "Email Password Reset Link",
"resend_verification_email": "Resend Verification Email"
},
"nav": {
"manage_team": "Manage Team",
"team_settings": "Team Settings",
"create_new_team": "Create New Team",
"switch_teams": "Switch Teams",
"manage_account": "Manage Account",
"profile": "My Profile",
"admin_settings": "Admin Settings",
"api_tokens": "API Tokens"
},
"pagination": {
"show_from_to_count": "Showing :from to :to of :count results"
},
"notifications": {
"saved": "Saved.",
"verified": "Verified!",
"unverified": "Unverified.",
"new_email_verification_sent": "A new verification link has been sent to the email address you provided in your profile settings."
},
"pages": {
"profile": {
"titles": {
"update_password": "Update Your Password"
},
"text_blocks": {
"unverified_message": "Please verify your account.",
"verified_message": "Thanks for being amazing.",
"secure_password": "Ensure your account is using a long, random password to stay secure."
}
}
}
} }

View File

@ -1,28 +1,24 @@
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run development", "dev": "vite",
"development": "mix", "build": "vite build"
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
}, },
"devDependencies": { "devDependencies": {
"laravel-mix": "^6.0.49", "@vitejs/plugin-vue": "^3.0.0",
"laravel-vite-plugin": "^0.6.0",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"postcss-import": "^14.1.0", "vite": "^3.0.0"
"vue-loader": "^17.0.0"
}, },
"dependencies": { "dependencies": {
"@inertiajs/inertia": "^0.11.0", "@inertiajs/inertia": "^0.11.0",
"@inertiajs/inertia-vue3": "^0.6.0", "@inertiajs/inertia-vue3": "^0.6.0",
"@inertiajs/progress": "^0.2.7", "@inertiajs/progress": "^0.2.7",
"@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/aspect-ratio": "^0.4.0",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.2", "@tailwindcss/typography": "^0.5.2",
"@vueuse/core": "^8.7.5", "@vueuse/core": "^8.7.5",
"apexcharts": "^3.35.5",
"axios": "^0.27.2", "axios": "^0.27.2",
"daisyui": "^2.29.0", "daisyui": "^2.29.0",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",

6
src/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,59 @@
/**
<div class="bubbles-container">
<div class="bubbles">
<span class="animate-bubble" style="style="--i:(random value between 1 and 15);"></span>
<span class="animate-bubble" style="style="--i:(random value between 1 and 15);"></span>
<span class="animate-bubble" style="style="--i:(random value between 1 and 15);"></span>
<span class="animate-bubble" style="style="--i:(random value between 1 and 15);"></span>
<span class="animate-bubble" style="style="--i:(random value between 1 and 15);"></span>
</div>
</div>
**/
.bubbles-container {
background: hsl(216, 57.1%, 11%);
min-height: 100%;
overflow: hidden;
position: relative;
width: 100%;
}
.bubbles {
position: relative;
display: flex;
}
.bubbles span {
border-radius: 50%;
height: 30px;
margin: 0 4px;
position: relative;
width: 30px;
/*animation: bubble 15s linear infinite;*/
/*animation-name: bubbleup;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-duration: calc(300s / calc(var(--i) * 5));*/
}
.bubbles span:nth-child(odd) {
background: hsl(191, 66.8%, 58.6%);
box-shadow: 0 0 0 10px hsla(191, 66.8%, 58.6%, 0.3), 0 0 50px hsl(191, 66.8%, 58.6%), 0 0 100px hsl(191, 66.8%, 58.6%);
}
.bubbles span:nth-child(even) {
background: hsl(339, 100%, 58.8%);
box-shadow: 0 0 0 10px hsla(339, 100%, 58.8%, 0.3), 0 0 50px hsl(339, 100%, 58.8%), 0 0 100px hsl(339, 100%, 58.8%);
}
.animate-bubble {
animation-name: bubbleup;
animation-duration: calc(300s / calc(var(--i) * 5));
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@keyframes bubbleup {
0% { transform: translateY(100px) scale(0); }
100% { transform: translateY(-100px) scale(1); }
}

View File

@ -0,0 +1,30 @@
:root {
--ripple-color: #fff;
--ripple-radius: 9999px;
--ripple-duration: 600ms;
--ripple-timing-function: linear;
}
.ripple {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.ripple span {
position: absolute;
border-radius: var(--ripple-radius);
opacity: 0.5;
background: var(--ripple-color);
transform: scale(0);
animation: ripple var(--ripple-duration) var(--ripple-timing-function);
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}

View File

@ -26,25 +26,80 @@
@import 'components/navbars.css'; @import 'components/navbars.css';
@import 'components/pagination.css'; @import 'components/pagination.css';
:root {
--primary-text-color: hsl(0, 90.4%, 59.2%);
--primary-text-color-gradient-start: hsl(270, 66.9%, 47.5%);
--primary-text-color-gradient-end: hsl(330, 100%, 50%);
--text-info-color: hsl(191, 82%, 50%);
--text-info-color-gradient-start: hsl(227, 100%, 56.5%);
--text-info-color-gradient-end: hsl(191, 98.2%, 56.1%);
--text-success-color: hsl(86, 81.4%, 46.3%);
--text-success-color-gradient-start: hsl(133, 76.5%, 38.4%);
--text-success-color-gradient-end: hsl(72, 81.1%, 52.4%);
--text-warning-color: hsl(47, 96.2%, 59.2%);
--text-warning-color-gradient-start: hsl(0, 90.4%, 59.2%);
--text-warning-color-gradient-end: hsl(47, 96.2%, 59.2%);
--text-danger-color: hsl(0, 95%, 47.1%);
--text-danger-color-gradient-start: hsl(0, 92.8%, 43.5%);
--text-danger-color-gradient-end: hsl(344, 100%, 70%);
}
@media (prefers-reduced-motion) {
*, *::before, *::after {
animation-duration: 0s !important;
/* additional recommendation */
transition: none !important;
scroll-behavior: auto !important;
}
}
body { body {
min-height: 100vh; background-image: linear-gradient(100deg, hsl(0, 0%, 98%), hsl(240, 5.9%, 90%));
color: hsl(240, 5.9%, 10%);
/*color: lch(8.35%, 2.25, 285.92);*/
-webkit-font-smoothing: antialiased;
min-height: 100dvh;
overflow: auto; overflow: auto;
scrollbar-gutter: stable both-edges; scrollbar-gutter: stable both-edges;
text-rendering: optimizeLegibility;
font-size: 1.25rem;
line-height: 1.1;
/* these will clamp the width between the two values
and keep it at 90% of the max when between them */
max-width: clamp(320px, 90%, 1280px);
margin: auto;
} }
html, body { html, body {
transition-duration: 0.05s; transition-duration: 0.05s;
transition-timing-function: ease-in-out;
}
html, body {
transition-property: background, color; transition-property: background, color;
transition-timing-function: ease-in-out;
} }
button, a { button, a {
transition-property: all; transition-property: all;
} }
nav {
user-select: none;
}
img {
max-width: 100%;
height: auto;
vertical-align: middle;
font-style: italic;
background-repeat: no-repeat;
background-size: cover;
shape-margin: 0.75rem;
}
@media (max-width: 1023px) { @media (max-width: 1023px) {
.grid-container { .grid-container {
@apply grid-cols-1; @apply grid-cols-1;

View File

@ -3,7 +3,7 @@
} }
.form-label { .form-label {
@apply uppercase font-semibold text-xs text-zinc-600 dark:text-zinc-300; @apply uppercase font-semibold text-xs text-zinc-600 dark:text-zinc-200;
} }
input.form-input[type="date"], input.form-input[type="date"],
@ -22,6 +22,7 @@ input.form-input[type="week"],
textarea.form-input, textarea.form-input,
select.form-input { select.form-input {
@apply relative px-3 py-1 border rounded focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 bg-white dark:bg-indigo-900 text-zinc-900 dark:text-zinc-100 border-stone-300 dark:border-stone-600 shadow dark:shadow-white; @apply relative px-3 py-1 border rounded focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 bg-white dark:bg-indigo-900 text-zinc-900 dark:text-zinc-100 border-stone-300 dark:border-stone-600 shadow dark:shadow-white;
transition: box-shadow 300, background 300;
} }
.form-checkbox-label { .form-checkbox-label {
@ -31,3 +32,40 @@ select.form-input {
.form-checkbox-label .form-checkbox { .form-checkbox-label .form-checkbox {
@apply rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50; @apply rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50;
} }
input.form-input[type="date"]:active,
input.form-input[type="datetime-local"]:active,
input.form-input[type="email"]:active,
input.form-input[type="month"]:active,
input.form-input[type="number"]:active,
input.form-input[type="password"]:active,
input.form-input[type="search"]:active,
input.form-input[type="tel"]:active,
input.form-input[type="text"]:active,
input.form-input[type="textarea"]:active,
input.form-input[type="time"]:active,
input.form-input[type="url"]:active,
input.form-input[type="week"]:active,
textarea.form-input:active,
select.form-input:active {
@apply outline-none shadow;
}
input.form-input[type="date"]:disabled,
input.form-input[type="datetime-local"]:disabled,
input.form-input[type="email"]:disabled,
input.form-input[type="month"]:disabled,
input.form-input[type="number"]:disabled,
input.form-input[type="password"]:disabled,
input.form-input[type="search"]:disabled,
input.form-input[type="tel"]:disabled,
input.form-input[type="text"]:disabled,
input.form-input[type="textarea"]:disabled,
input.form-input[type="time"]:disabled,
input.form-input[type="url"]:disabled,
input.form-input[type="week"]:disabled,
textarea.form-input:disabled,
select.form-input:disabled {
@apply bg-neutral-400 opacity-75;
}

View File

@ -0,0 +1,106 @@
.container {
display: grid;
gap: 4em 1em;
}
.primary-button {
--h: 196;
--s: 100%;
--l: 41%;
}
.secondary-button {
--h: 28;
--s: 100%;
--l: 41%;
}
.glass-button {
font-size: clamp(1.2rem, 5vw + 1rem, 2.5rem);
width: 10em;
height: 4em;
border-radius: 0.5em;
background-image: linear-gradient(#0003,#0000);
box-shadow:
0 -0.125em 0.25em #0002,
0 0.25em 0.25em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0 0 0.1em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0.175em 0.3em hsl(var(--h) var(--s) var(--l) / 0.5) inset,
0 -0.025em 0.175em hsl(var(--h) var(--s) var(--l) / 0.4) inset,
0 -0.25em 1em 0.3em hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 0.6em 0 hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 2em 1em #0004;
backdrop-filter: blur(0.15em);
position: relative;
display: grid;
place-content: center;
color: hsl(var(--h) var(--s) var(--l) / .7);
text-shadow:
0.03em 0.03em #fff5,
-0.03em -0.03em #0005;
cursor: pointer;
transition: 0.1s ease;
padding-top: 0.2em;
border: none;
background: transparent;
}
.glass-button:before {
content: '';
height: 1.5em;
left: 10%;
top: 100%;
position: absolute;
width: 80%;
}
.glass-button:after {
content: '';
inset: 0;
top: 0.5em;
position: absolute;
background-image:
linear-gradient(
105deg,
transparent 20%,
hsl(var(--h) var(--s) var(--l) / .2) 20%,
hsl(var(--h) var(--s) var(--l) / .2) 30%,
transparent 30%,
transparent 32%,
hsl(var(--h) var(--s) var(--l) / .2) 5%,
hsl(var(--h) var(--s) var(--l) / .2) 40%,
transparent 0%
);
background-size: 400% 100%;
background-position: 100% 0%;
transition: .3s ease;
}
.glass-button:active:after {
background-position: -50% 0%;
}
.glass-button:active {
backdrop-filter: blur(0.08em);
box-shadow:
0 -0.125em 0.25em #0002,
0 0.25em 0.25em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0 0 0.1em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0.175em 0.3em hsl(var(--h) var(--s) var(--l) / 0.8) inset,
0 -0.025em 0.175em hsl(var(--h) var(--s) var(--l) / 0.4) inset,
0 -0.25em 1em 0.3em hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 0.6em 0 hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 1em 0.5em #0004;
translate: .01em .25em;
}
.glass-button:active:before {
height: 1em;
}
/*body {
background-color: #3d3d3d;
display: grid;
margin: 0;
min-height: 100vh;
place-content: center;
}*/

View File

@ -0,0 +1,74 @@
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
margin: 0;
}
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role='list'],
ol[role='list'] {
list-style: none;
}
/* Set core root defaults */
html:focus-within {
scroll-behavior: smooth;
}
/* Set core body defaults */
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}
/* Make images easier to work with */
img,
picture {
max-width: 100%;
display: block;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
html:focus-within {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@ -0,0 +1,48 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@ -3,6 +3,70 @@
/** +--------------------------------+ **/ /** +--------------------------------+ **/
@font-face { @font-face {
font-family: "BarlowCondensed";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-Regular.woff2") format("woff2");
}
@font-face {
font-family: "BarlowCondensed";
font-weight: 400;
font-style: italic;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-Italic.woff2") format("woff2");
}
@font-face {
font-family: "BarlowCondensed";
font-weight: 500;
font-style: normal;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-Medium.woff2") format("woff2");
}
@font-face {
font-family: "BarlowCondensed";
font-weight: 500;
font-style: italic;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-MediumItalic.woff2") format("woff2");
}
@font-face {
font-family: "BarlowCondensed";
font-weight: 600;
font-style: normal;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "BarlowCondensed";
font-weight: 600;
font-style: italic;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-SemiBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "BarlowCondensed";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-Bold.woff2") format("woff2");
}
@font-face {
font-family: "BarlowCondensed";
font-weight: 700;
font-style: italic;
font-display: swap;
src: url("/fonts/BarlowCondensed/BarlowCondensed-BoldItalic.woff2") format("woff2");
}
/*@font-face {
font-family: "Lato"; font-family: "Lato";
src: url('/fonts/Lato/Lato-Regular.woff2') format("woff2"); src: url('/fonts/Lato/Lato-Regular.woff2') format("woff2");
font-weight: 400; font-weight: 400;
@ -48,13 +112,77 @@
font-weight: 700; font-weight: 700;
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }*/
/** +--------------------------------+ **/ /** +--------------------------------+ **/
/** | Serif fonts | **/ /** | Serif fonts | **/
/** +--------------------------------+ **/ /** +--------------------------------+ **/
@font-face { @font-face {
font-family: "Montserrat";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Montserrat";
font-weight: 400;
font-style: italic;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-Italic.woff2") format("woff2");
}
@font-face {
font-family: "Montserrat";
font-weight: 500;
font-style: normal;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Montserrat";
font-weight: 500;
font-style: italic;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-MediumItalic.woff2") format("woff2");
}
@font-face {
font-family: "Montserrat";
font-weight: 600;
font-style: normal;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "Montserrat";
font-weight: 600;
font-style: italic;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-SemiBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Montserrat";
font-weight: 700;
font-style: normal;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-Bold.woff2") format("woff2");
}
@font-face {
font-family: "Montserrat";
font-weight: 700;
font-style: italic;
font-display: swap;
src: url("/fonts/Montserrat/Montserrat-BoldItalic.woff2") format("woff2");
}
/*@font-face {
font-family: "Nunito"; font-family: "Nunito";
src: url('/fonts/Nunito/Nunito-Regular.woff2') format("woff2"); src: url('/fonts/Nunito/Nunito-Regular.woff2') format("woff2");
font-weight: 400; font-weight: 400;
@ -118,7 +246,7 @@
font-stretch: 75% 125%; font-stretch: 75% 125%;
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }*/
/*@font-face { /*@font-face {
font-family: "Raleway"; font-family: "Raleway";

View File

@ -0,0 +1,45 @@
.text-gradient {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
position: relative;
z-index: 1;
}
.text-gradient.text-primary {
background-image: linear-gradient(310deg, var(--primary-text-color-gradient-start), var(--primary-text-color-gradient-end));
}
.text-primary {
color: var(--primary-text-color);
}
.text-gradient.text-info {
background-image: linear-gradient(310deg, var(--text-info-color-gradient-start), var(--text-info-color-gradient-end));
}
.text-info {
color: var(--text-info-color);
}
.text-gradient.text-success {
background-image: linear-gradient(310deg, var(--text-success-color-gradient-start), var(--text-success-color-gradient-end));
color: hsl(86, 81.4%, 46.3%);
}
.text-success {
color: var(--text-success-color);
}
.text-gradient.text-warning {
background-image: linear-gradient(310deg, var(--text-warning-color-gradient-start), var(--text-warning-color-gradient-end));
color: hsl(47, 96.2%, 59.2%);
}
.text-warning {
color: var(--text-warning-color);
}
.text-gradient.text-danger {
background-image: linear-gradient(310deg, var(--text-danger-color-gradient-start), var(--text-danger-color-gradient-end));
color: hsl(0, 95%, 47.1%);
}
.text-danger {
color: var(--text-danger-color);
}

View File

@ -1,3 +1,30 @@
.button-row { .button-row {
@apply inline-grid grid-flow-col auto-cols-max items-center; /* intentionally leaving gap out so that it can be set on per-case basis */ @apply inline-grid grid-flow-col auto-cols-max items-center; /* intentionally leaving gap out so that it can be set on per-case basis */
} }
.gradBorder i {
content: '';
position: absolute;
z-index: -1;
inset: 0;
border-radius: 30px;
padding: 1.5px; /* the thickness of the border */
/* the below will do the magic */
mask:
linear-gradient(#fff 0 0) content-box,
/* this will cover only the content area (no padding) */
linear-gradient(#fff 0 0); /* this will cover all the area */
-webkit-mask-composite: xor; /* needed for old browsers until the below is more supported */
mask-composite: exclude; /* this will exclude the first layer from the second so only the padding area will be kept visible */
}
.gradBorder i::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, #ffffff73 0%, #ffffff10 50%);
transition: transform 0.7s linear;
}

View File

@ -1,3 +1,108 @@
:root { :root {
/**/
/** ------------------------------
* START -- https://coolors.co/022f40-38aecc-0090c1-183446-046e8f
* -------------------------------**/
/* CSS HEX */
--prussian-blue: #022f40ff;
--pacific-cyan: #38aeccff;
--blue-ncs: #0090c1ff;
--prussian-blue-2: #183446ff;
--cerulean: #046e8fff;
/* CSS HSL */
--prussian-blue: hsla(196, 94%, 13%, 1);
--pacific-cyan: hsla(192, 59%, 51%, 1);
--blue-ncs: hsla(195, 100%, 38%, 1);
--prussian-blue-2: hsla(203, 49%, 18%, 1);
--cerulean: hsla(194, 95%, 29%, 1);
/* SCSS HEX */
$prussian-blue: #022f40ff;
$pacific-cyan: #38aeccff;
$blue-ncs: #0090c1ff;
$prussian-blue-2: #183446ff;
$cerulean: #046e8fff;
/* SCSS HSL */
$prussian-blue: hsla(196, 94%, 13%, 1);
$pacific-cyan: hsla(192, 59%, 51%, 1);
$blue-ncs: hsla(195, 100%, 38%, 1);
$prussian-blue-2: hsla(203, 49%, 18%, 1);
$cerulean: hsla(194, 95%, 29%, 1);
/* SCSS RGB */
$prussian-blue: rgba(2, 47, 64, 1);
$pacific-cyan: rgba(56, 174, 204, 1);
$blue-ncs: rgba(0, 144, 193, 1);
$prussian-blue-2: rgba(24, 52, 70, 1);
$cerulean: rgba(4, 110, 143, 1);
/* SCSS Gradient */
/*$gradient-top: linear-gradient(0deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-right: linear-gradient(90deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-bottom: linear-gradient(180deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-left: linear-gradient(270deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-top-right: linear-gradient(45deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-bottom-right: linear-gradient(135deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-top-left: linear-gradient(225deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-bottom-left: linear-gradient(315deg, #022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/*$gradient-radial: radial-gradient(#022f40ff, #38aeccff, #0090c1ff, #183446ff, #046e8fff);*/
/** ------------------------------
* END -- https://coolors.co/022f40-38aecc-0090c1-183446-046e8f
* -------------------------------**/
/** ------------------------------
* START -- https://coolors.co/8ac482-47783f-ffbe86-ffb5c2-3777ff
* -------------------------------**/
/* CSS HEX */
--pistachio: #8ac482ff;
--fern-green: #47783fff;
--peach: #ffbe86ff;
--cherry-blossom-pink: #ffb5c2ff;
--blue-crayola: #3777ffff;
/* CSS HSL */
--pistachio: hsla(113, 36%, 64%, 1);
--fern-green: hsla(112, 31%, 36%, 1);
--peach: hsla(28, 100%, 76%, 1);
--cherry-blossom-pink: hsla(349, 100%, 85%, 1);
--blue-crayola: hsla(221, 100%, 61%, 1);
/* SCSS HEX */
$pistachio: #8ac482ff;
$fern-green: #47783fff;
$peach: #ffbe86ff;
$cherry-blossom-pink: #ffb5c2ff;
$blue-crayola: #3777ffff;
/* SCSS HSL */
$pistachio: hsla(113, 36%, 64%, 1);
$fern-green: hsla(112, 31%, 36%, 1);
$peach: hsla(28, 100%, 76%, 1);
$cherry-blossom-pink: hsla(349, 100%, 85%, 1);
$blue-crayola: hsla(221, 100%, 61%, 1);
/* SCSS RGB */
$pistachio: rgba(138, 196, 130, 1);
$fern-green: rgba(71, 120, 63, 1);
$peach: rgba(255, 190, 134, 1);
$cherry-blossom-pink: rgba(255, 181, 194, 1);
$blue-crayola: rgba(55, 119, 255, 1);
/* SCSS Gradient */
/*$gradient-top: linear-gradient(0deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-right: linear-gradient(90deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-bottom: linear-gradient(180deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-left: linear-gradient(270deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-top-right: linear-gradient(45deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-bottom-right: linear-gradient(135deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-top-left: linear-gradient(225deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-bottom-left: linear-gradient(315deg, #8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/*$gradient-radial: radial-gradient(#8ac482ff, #47783fff, #ffbe86ff, #ffb5c2ff, #3777ffff);*/
/** ------------------------------
* END -- https://coolors.co/8ac482-47783f-ffbe86-ffb5c2-3777ff
* -------------------------------**/
} }

View File

@ -0,0 +1,93 @@
<script setup>
import { ref } from 'vue'
import { usePage } from '@inertiajs/inertia-vue3'
import Dropdown from './Dropdown.vue'
import GhostButton from '@/Components/Buttons/GhostButton.vue'
import IconBell from '@/Icons/Bell.vue'
import Modal from '@/Components/Modals/Modal.vue'
// variables
const notifications = ref(usePage().props.value.notifications)
const hasUnreadNotifications = ref(usePage().props.value.unreadNotifications)
const showNotification = ref(false)
const activeNotification = ref({})
// methods
const openNotification = (notification) => {
activeNotification.value = notification
showNotification.value = true
markActiveAsRead()
}
const closeModal = () => {
showNotification.value = false
activeNotification.value = {}
}
const markActiveAsRead = () => {
axios.post(route('api.notifications.mark-read', activeNotification.value.id))
.then(resp => {
activeNotification.value.read_at = resp.data.read_at
})
.catch(erro => {
//console.error(erro)
})
}
const markAllRead = () => {
axios.post(route('api.notifications.mark-all-read'))
.then(resp => {
let nowDt = new Date()
notifications.value.forEach(notification => {
if (notification.read_at === null) {
notification.read_at = nowDt
}
})
hasUnreadNotifications.value = false
})
.catch(erro => {
//console.error(erro)
})
}
</script>
<template>
<Dropdown align="right" width="80">
<template #trigger>
<GhostButton>
<IconBell></IconBell>
<div v-show="hasUnreadNotifications" class="absolute top-2 right-4 w-3 h-3 z-10 border-2 border-stone-100 bg-red-800 rounded-full">&nbsp;</div>
</GhostButton>
</template>
<template #content>
<div class="w-full">
<div class="grid auto-rows-max gap-y-px bg-stone-400">
<template v-for="notification in notifications" :key="notification.id">
<div class="px-4 py-2 grid auto-rows-max gap-3 cursor-pointer" :class="{'bg-stone-200': notification.read_at !== null, 'bg-white': notification.read_at === null}" @click="openNotification(notification)">
<div>{{ notification.data.title }}</div>
<div class="text-xs text-zinc-600">{{ notification.data.created_at_date }}</div>
</div>
</template>
</div>
<div class="border-t border-gray-100"></div>
<div class="px-2">
<button class="btn btn-ghost text-xs" @click="markAllRead">Mark All Read</button>
</div>
</div>
</template>
</Dropdown>
<Modal :show="showNotification" @close="closeModal">
<div class="grid auto-rows-max">
<header class="bg-sky-800 text-white p-4">{{ activeNotification.data.title }}</header>
<div class="p-4 bg-white text-zinc-900">
<p class="mb-4">{{ activeNotification.data.body }}</p>
<p class="text-sm">{{ activeNotification.data.type }} | {{ activeNotification.data.created_at_date }} at {{ activeNotification.data.created_at_time }}</p>
</div>
</div>
</Modal>
</template>

View File

@ -1,7 +1,3 @@
<script setup>
//
</script>
<template> <template>
<div class="border-t border-gray-100"></div> <div class="border-t border-gray-100"></div>
</template> </template>

View File

@ -1,9 +1,5 @@
<script setup>
//
</script>
<template> <template>
<div class="block px-4 py-2 text-xs text-gray-400"> <div class="block px-4 py-2 text-xs text-gray-400">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>

View File

@ -0,0 +1,32 @@
<script setup>
import { computed } from 'vue'
// variables
const emit = defineEmits(['update:checked'])
const props = defineProps({
checked: {
type: [Array, Boolean],
default: false,
},
value: {
type: String,
default: null,
},
})
// computed properties
const proxyChecked = computed({
get() {
return props.checked
},
set(val) {
emit('update:checked', val)
},
})
</script>
<template>
<input type="checkbox" class="checkbox" :value="value" v-model="proxyChecked">
</template>

View File

@ -0,0 +1,10 @@
<script setup>
// defines
defineProps({
message: String,
})
</script>
<template>
<p v-show="message" class="px-2 py-1 text-sm text-red-600">{{ message }}</p>
</template>

View File

@ -0,0 +1,20 @@
<script setup>
import { useSlots, computed } from 'vue'
// defines
defineProps({
value: String,
})
// computed properties
const hasActions = computed(() => !! useSlots().actions)
</script>
<template>
<label class="label">
<span class="label-text"><slot></slot></span>
<span v-show="hasActions" class="flex items-center">
<slot name="actions"></slot>
</span>
</label>
</template>

View File

@ -0,0 +1,28 @@
<script setup>
import { computed } from 'vue'
// variables
const emit = defineEmits(['update:checked'])
const props = defineProps({
checked: {
type: [Array, Boolean],
default: false,
},
})
// computed properties
const proxyChecked = computed({
get() {
return props.checked
},
set(val) {
emit('update:checked', val)
},
})
</script>
<template>
<input type="radio" class="radio" v-model="proxyChecked">
</template>

View File

@ -0,0 +1,28 @@
<script setup>
import { ref, onMounted } from 'vue'
// defines
defineEmits(['update:modelValue'])
defineProps({
modelValue: String,
})
defineExpose({
focus: () => input.value.focus()
})
// variables
const input = ref(null)
// lifecycle hooks
onMounted(() => {
if (input.value.hasAttribute('autofocus')) {
input.value.focus()
}
})
</script>
<template>
<input ref="input" class="input" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

View File

@ -0,0 +1,30 @@
<script setup>
import { ref, onMounted } from 'vue'
// defines
defineEmits(['update:modelValue'])
defineProps({
modelValue: String,
})
defineExpose({
focus: () => input.value.focus()
})
// variables
const input = ref(null)
// lifecycle hooks
onMounted(() => {
if (input.value.hasAttribute('autofocus')) {
input.value.focus()
}
})
</script>
<template>
<textarea ref="input" class="textarea" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
<slot></slot>
</textarea>
</template>

View File

@ -0,0 +1,52 @@
<script setup>
import { ref } from 'vue'
import { Inertia } from '@inertiajs/inertia'
import { usePage } from '@inertiajs/inertia-vue3'
import Dropdown from './DropdownMenu.vue'
// variables
const currentLocale = ref(usePage().props.value.currentLocale)
// methods
const isCurrent = (language) => {
return language.iso_code === currentLocale.value.iso_code
}
const changeLocale = (language) => {
if (language.iso_code !== currentLocale.value.iso_code) {
Inertia.post(route('locale.set'), language, {
replace: true,
onSuccess: page => {
currentLocale.value = page.props.currentLocale
},
onError: errors => {
console.log(errors)
}
})
}
}
</script>
<template>
<Dropdown align="right" width="60">
<template #trigger>
<div class="py-2 px-4 inline-grid grid-flow-col auto-cols-max gap-x-2 items-center bg-white hover:bg-gray-50 text-sm leading-4 font-medium rounded-md cursor-pointer">
<svg class="h-4 w-8">
<use :href="`#flag-${currentLocale.iso_code}`"></use>
</svg>
<span class="ml-2">{{ currentLocale.localized_name }}</span>
</div>
</template>
<template #content>
<div class="w-60">
<button type="button" v-for="(lang, langIdx) in $page.props.availableLocales" :key="langIdx" @click="changeLocale(lang)" class="py-2 px-4 inline-grid grid-flow-col auto-cols-max gap-x-2 items-center w-full" :class="{ 'text-white bg-sky-600 cursor-default': isCurrent(lang), 'hover:bg-sky-600/50': !isCurrent(lang) }">
<svg class="h-4 w-8">
<use :href="`#flag-${lang.iso_code}`"></use>
</svg>
<span class="ml-2">{{ lang.localized_name }}</span>
</button>
</div>
</template>
</Dropdown>
</template>

View File

@ -35,6 +35,7 @@ const titleString = computed(() => {
<template> <template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet"> <svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title> <title v-if="titleString.length > 0" v-html="title"></title>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line> <path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path>
<polygon points="12 15 17 21 7 21 12 15"></polygon>
</svg> </svg>
</template> </template>

View File

@ -0,0 +1,42 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</template>

View File

@ -0,0 +1,43 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="18" y1="10" x2="6" y2="10"></line>
<line x1="21" y1="6" x2="3" y2="6"></line>
<line x1="21" y1="14" x2="3" y2="14"></line>
<line x1="18" y1="18" x2="6" y2="18"></line>
</svg>
</template>

View File

@ -0,0 +1,43 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="21" y1="10" x2="3" y2="10"></line>
<line x1="21" y1="6" x2="3" y2="6"></line>
<line x1="21" y1="14" x2="3" y2="14"></line>
<line x1="21" y1="18" x2="3" y2="18"></line>
</svg>
</template>

View File

@ -0,0 +1,43 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="17" y1="10" x2="3" y2="10"></line>
<line x1="21" y1="6" x2="3" y2="6"></line>
<line x1="21" y1="14" x2="3" y2="14"></line>
<line x1="17" y1="18" x2="3" y2="18"></line>
</svg>
</template>

View File

@ -0,0 +1,43 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="21" y1="10" x2="7" y2="10"></line>
<line x1="21" y1="6" x2="3" y2="6"></line>
<line x1="21" y1="14" x2="3" y2="14"></line>
<line x1="21" y1="18" x2="7" y2="18"></line>
</svg>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="5" r="3"></circle>
<line x1="12" y1="22" x2="12" y2="8"></line>
<path d="M5 12H2a10 10 0 0 0 20 0h-3"></path>
</svg>
</template>

View File

@ -0,0 +1,46 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="12" r="10"></circle>
<line x1="14.31" y1="8" x2="20.05" y2="17.94"></line>
<line x1="9.69" y1="8" x2="21.17" y2="8"></line>
<line x1="7.38" y1="12" x2="13.12" y2="2.06"></line>
<line x1="9.69" y1="16" x2="3.95" y2="6.06"></line>
<line x1="14.31" y1="16" x2="2.83" y2="16"></line>
<line x1="16.62" y1="12" x2="10.88" y2="21.94"></line>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="12" y1="5" x2="12" y2="19"></line>
<polyline points="19 12 12 19 5 12"></polyline>
</svg>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="8 12 12 16 16 12"></polyline>
<line x1="12" y1="8" x2="12" y2="16"></line>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="17" y1="7" x2="7" y2="17"></line>
<polyline points="17 17 7 17 7 7"></polyline>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="7" y1="7" x2="17" y2="17"></line>
<polyline points="17 7 17 17 7 17"></polyline>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 8 8 12 12 16"></polyline>
<line x1="16" y1="12" x2="8" y2="12"></line>
</svg>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 16 16 12 12 8"></polyline>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="16 12 12 8 8 12"></polyline>
<line x1="12" y1="16" x2="12" y2="8"></line>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="17" y1="17" x2="7" y2="7"></line>
<polyline points="7 17 7 7 17 7"></polyline>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<line x1="7" y1="17" x2="17" y2="7"></line>
<polyline points="7 7 17 7 17 17"></polyline>
</svg>
</template>

View File

@ -0,0 +1,41 @@
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String,
height: Number,
width: Number,
})
// variables
const heightNumber = ref(24)
const widthNumber = ref(24)
if (!Number.isNaN(props.height) && typeof props.height !== "undefined") {
heightNumber.value = Number.parseInt(props.height)
} else if (typeof props.height === "undefined" && (!Number.isNaN(props.width) && typeof props.width !== "undefined")) {
heightNumber.value = Number.parseInt(props.width)
}
if (!Number.isNaN(props.width) && typeof props.width !== "undefined") {
widthNumber.value = Number.parseInt(props.width)
} else if (typeof props.width === "undefined" && (!Number.isNaN(props.height) && typeof props.height !== "undefined")) {
widthNumber.value = Number.parseInt(props.height)
}
// computed
const titleString = computed(() => {
if (typeof props.title === "string" && props.title.trim().length > 0) {
return props.title.trim()
}
return ""
})
</script>
<template>
<svg viewBox="0 0 24 24" :height="heightNumber" :width="widthNumber" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<title v-if="titleString.length > 0" v-html="title"></title>
<circle cx="12" cy="12" r="4"></circle>
<path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"></path>
</svg>
</template>

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