Compare commits
16 Commits
3b39ae0470
...
master
Author | SHA1 | Date | |
---|---|---|---|
ac751f5f28
|
|||
a1aecdf3e2
|
|||
a747ef0129
|
|||
80aa487e7e
|
|||
5048abc3e6
|
|||
afd34e9b4e
|
|||
a81ce8a922
|
|||
9d25befeba
|
|||
5d9f7e3997
|
|||
ecfae3e01a
|
|||
9e6c93ba95
|
|||
231dd959af
|
|||
5607425bde
|
|||
ac99d29109
|
|||
d4f0044656
|
|||
9d455bdcf7
|
@ -39,8 +39,8 @@ REDIS_PASSWORD=null
|
|||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
MAIL_MAILER=smtp
|
||||||
MAIL_HOST=mailhog
|
MAIL_HOST=mailpit
|
||||||
MAIL_PORT=1125
|
MAIL_PORT=1025
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
@ -74,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}"
|
||||||
|
@ -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>
|
||||||
|
6
src/.well-known/security.txt
Normal file
6
src/.well-known/security.txt
Normal 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
30
src/SECURITY.md
Normal 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.
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,24 @@
|
|||||||
|
|
||||||
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.
|
||||||
*
|
*
|
||||||
|
* @package Namespace\App\Contact
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*
|
*
|
||||||
* @return string
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function getPhoneNumberAttribute(): string
|
protected function phoneNumber(): Attribute
|
||||||
{
|
{
|
||||||
$phoneLength = strlen($this->phone);
|
return Attribute::make(
|
||||||
$phoneNumber = preg_replace('//', '', $this->phone);
|
get: function (mixed $value, array $attributes) {
|
||||||
|
$phoneLength = strlen($attributes['phone_number']);
|
||||||
|
$phoneNumber = preg_replace('/[^0-9]/', '', $attributes['phone_number']);
|
||||||
|
|
||||||
if ($phoneLength > 10) {
|
if ($phoneLength > 10) {
|
||||||
$countryCode = substr($phoneNumber, 0, $phoneLength - 10);
|
$countryCode = substr($phoneNumber, 0, $phoneLength - 10);
|
||||||
@ -37,19 +42,10 @@ trait FormattedPhoneTrait
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $phoneNumber;
|
return $phoneNumber;
|
||||||
}
|
},
|
||||||
|
set: function (mixed $value) {
|
||||||
/**
|
return preg_replace('/[^0-9]/', '', $value);
|
||||||
* Remove all non-numeric characters from the phone number.
|
},
|
||||||
*
|
);
|
||||||
* @since 1.0.0
|
|
||||||
*
|
|
||||||
* @param string $value
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setPhoneNumberAttribute($value): void
|
|
||||||
{
|
|
||||||
$this->attributes['phone'] = preg_replace('/[^0-9]/', '', $value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
src/app/Models/Traits/HasProfilePhoto.php
Normal file
88
src/app/Models/Traits/HasProfilePhoto.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,9 @@
|
|||||||
"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
|
// Options/Flags
|
||||||
"spatie/laravel-model-flags": "", // add a simple true/false flag to any model
|
"spatie/laravel-model-flags": "", // add a simple true/false flag to any model
|
||||||
|
|
||||||
@ -30,6 +33,9 @@
|
|||||||
"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
|
||||||
@ -37,6 +43,7 @@
|
|||||||
"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
|
||||||
|
@ -6,6 +6,7 @@ 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;
|
||||||
|
|
||||||
@ -71,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}"
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -26,7 +26,40 @@
|
|||||||
@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 {
|
||||||
|
background-image: linear-gradient(100deg, hsl(0, 0%, 98%), hsl(240, 5.9%, 90%));
|
||||||
color: hsl(240, 5.9%, 10%);
|
color: hsl(240, 5.9%, 10%);
|
||||||
/*color: lch(8.35%, 2.25, 285.92);*/
|
/*color: lch(8.35%, 2.25, 285.92);*/
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@ -34,6 +67,13 @@ body {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
text-rendering: optimizeLegibility;
|
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 {
|
||||||
@ -50,6 +90,16 @@ nav {
|
|||||||
user-select: none;
|
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;
|
||||||
|
106
src/resources/css/elements/glossy-buttons.css
Normal file
106
src/resources/css/elements/glossy-buttons.css
Normal 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;
|
||||||
|
}*/
|
45
src/resources/css/typography/text-gradient.css
Normal file
45
src/resources/css/typography/text-gradient.css
Normal 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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user