Compare commits

...

13 Commits

25 changed files with 570 additions and 80 deletions

View File

@ -34,6 +34,12 @@ REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
SCOUT_DRIVER=meilisearch
SCOUT_PREFIX=
SCOUT_QUEUE=false
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1125
@ -58,8 +64,17 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
INTEGRITY_HASH_HUMANS_TXT=""
INTEGRITY_HASH_ROBOTS_TXT=""
INTEGRITY_HASH_COPYRIGHT_HTML=""
INTEGRITY_HASH_COPYRIGHT_MD=""
INTEGRITY_HASH_RSS_FEED=""
INTEGRITY_HASH_ATOM_FEED=""
INTEGRITY_HASH_FAVICON_ICO=""
INTEGRITY_HASH_FAVICON_PNG=""
INTEGRITY_HASH_FAVICON_SVG=""
INTEGRITY_HASH_NUNITO_REGULAR_WOFF2_FONT=""
INTEGRITY_HASH_POIRETONE_REGULAR_WOFF2_FONT=""
INTEGRITY_HASH_WEBMANIFEST_JSON=""
INTEGRITY_HASH_MIX_MANIFEST_JSON=""
INTEGRITY_HASH_APP_CSS=""
INTEGRITY_HASH_APP_JS=""

View File

@ -9,10 +9,14 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Inertia\Response;
use Jenssegers\Agent\Agent;
use Laravel\Fortify\Features;
use Laravel\Jetstream\Http\Controllers\Inertia\Concerns\ConfirmsTwoFactorAuthentication;
use Laravel\Jetstream\Jetstream;
class UserProfileController extends Controller
{
use ConfirmsTwoFactorAuthentication;
/**
* Show the general profile settings screen.
*
@ -27,6 +31,7 @@ class UserProfileController extends Controller
$this->validateTwoFactorAuthenticationState($request);
return Jetstream::inertia()->render($request, 'Profile/Show', [
'confirmsTwoFactorAuthentication' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
'sessions' => $this->sessions($request)->all(),
'timezones' => timezone_identifiers_list(),
]);

View File

@ -19,8 +19,8 @@ class CheckCustomSessionData
*/
public function handle(Request $request, Closure $next)
{
if ((! session()->has('timezone_name') || empty(session('timezone_name'))) && $request->user()) {
session()->put('timezone_name', $request->user()->timezone_name);
if ((! session()->has('thing') || empty(session('thing'))) && $request->user()) {
session()->put('thing', $request->user()->thing);
}
return $next($request);

View File

@ -0,0 +1,33 @@
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Session;
class SuccessfulLogin
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \Illuminate\Auth\Events\Login $event
*
* @return void
*/
public function handle(Login $event): void
{
Session::put('timezone_name', $event->user->timezone_name);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Logout;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Session;
class SuccessfulLogout
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \Illuminate\Auth\Events\Logout $event
* @return void
*/
public function handle(Logout $event)
{
Session::forget('timezone_name');
}
}

View File

@ -64,7 +64,7 @@ class User extends Authenticatable
/** @var array */
protected $appends = [
'full_name',
'name_full',
'surname_full',
'profile_photo_url',
];
@ -142,7 +142,7 @@ class User extends Authenticatable
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function fullNameReversed(): Attribute
public function surnameFull(): Attribute
{
return Attribute::make(
get: fn ($value, $attributes) => "{$attributes['surname']}, {$attributes['name']}",

View File

@ -0,0 +1,76 @@
<?php
namespace App\Providers;
use Carbon\Carbon;
use Illuminate\Support\ServiceProvider;
class CarbonServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
Carbon::macro('getHumanInterval', function ($other) {
$isNow = $other === null;
if ($isNow) {
$other = static::now($this->tz);
}
$diffInMinutes = $this->diffInMinutes($other);
$minutes = $diffInMinutes % 60;
$diffInHours = $diffInMinutes / 60;
$hours = $diffInHours % 60;
$diffInDays = $diffInHours / 24;
$days = $diffInDays % 24;
$diffInWeeks = $diffInDays / 24;
$weeks = $diffInWeeks % 24;
$diffInMonths = $diffInWeeks / 7;
$months = $diffInMonths % 7;
$diffInYears = $diffInMonths / 12;
$years = $diffInYears % 12;
$outputArray = [];
if ($years > 0) {
$outputArray['years'] = "$years years";
}
if ($months > 0) {
$outputArray['months'] = "$months months";
}
if ($weeks > 0 && $days === 0) {
$outputArray['weeks'] = "$weeks weeks";
}
if ($days > 0) {
$outputArray['days'] = "$days days";
}
if ($hours > 0) {
$outputArray['hours'] = "$hours hours";
}
if ($minutes > 0) {
$outputArray['minutes'] = "$minutes minutes";
}
return implode(', ', $outputArray);
});
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Providers;
use App\Listeners\SuccessfulLogin;
//use App\Listeners\SuccessfulLogout;
use Illuminate\Auth\Events\Login;
//use Illuminate\Auth\Events\Logout;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
Login::class => [
SuccessfulLogin::class,
],
/*Logout::class => [
SuccessfulLogout::class,
],*/
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return false;
}
}

22
src/config/app.php Normal file
View File

@ -0,0 +1,22 @@
<?php
...
return [
...
'providers' => [
...
/*
* Application Service Providers...
*/
...
App\Providers\CarbonServiceProvider::class,
],
...
];

View File

@ -5,6 +5,8 @@ if (! function_exists('snake2Title')) {
* Convert a snake case string to a title with spaces
* and every word capitalized.
*
* @since 1.0.0
*
* @param string $stakeSlug A snake case string, commonly a slug
*
* @return string
@ -22,6 +24,8 @@ if (! function_exists('carbon')) {
* It will attempt to find a timezone in the current
* session but default to UTC.
*
* @since 1.0.0
*
* @param string|null $timestring
*
* @return \Carbon\Carbon
@ -40,6 +44,8 @@ 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
*
@ -59,10 +65,106 @@ if (! function_exists('jddayofweek')) {
}
}
if (! function_exists('is_serialized')) {
/**
* Check a value to find if it was serialized.
*
* @since 1.0.0
*
* @param mixed $data
* @param bool $strict
*
* @return bool
*/
function is_serialized($data, bool $strict = true): bool
{
// If it isn't a string, it isn't serialized.
if (! is_string($data)) {
return false;
}
$data = trim($data);
if ('N;' === $data) {
return true;
}
if (strlen($data) < 4) {
return false;
}
if (':' !== $data[1]) {
return false;
}
if ($strict) {
$lastc = substr($data, -1);
if (';' !== $lastc && '}' !== $lastc) {
return false;
}
} else {
$semicolon = strpos($data, ';');
$brace = strpos($data, '}');
// Either ; or } must exist.
if (!$semicolon && !$brace) {
return false;
}
// But neither must be in the first X characters.
if ($semicolon && $semicolon < 3) {
return false;
}
if ($brace && $brace < 4) {
return false;
}
}
$token = $data[0];
switch ($token) {
case 's':
if ($strict) {
if ('"' !== substr($data, -2, 1)) {
return false;
}
} elseif (!strpos($data, '"')) {
return false;
}
// Or else fall through.
case 'a':
case 'O':
return (bool) preg_match("/^{$token}:[0-9]+:/s", $data);
case 'b':
case 'i':
case 'd':
$end = $strict ? '$' : '';
return (bool) preg_match("/^{$token}:[0-9.E+-]+;$end/", $data);
}
return false;
}
}
if (! function_exists('maybe_unserialize')) {
/**
* Unserialize data only if it was serialized. Will return
* an array if it was a serialized string, otherwise it
* will return whatever was passed into the function
* leaving it untouched.
*
* @since 1.0.0
*
* @param mixed $data
*
* @return mixed
*/
function maybe_unserialize($data)
{
if (is_serialized($data)) { // Don't attempt to unserialize data that wasn't serialized going in.
return @unserialize(trim($data));
}
return $data;
}
}
if (! function_exists('cel2Fah')) {
/**
* Convert from celsius to fahrenheit.
*
* @since 1.0.0
*
* @param float|int|string $celsius
* @param int $precision
*
@ -78,6 +180,8 @@ if (! function_exists('fah2Cel')) {
/**
* Convert from fahrenheit to celsius.
*
* @since 1.0.0
*
* @param float|int|string $fahrenheit
* @param int $precision
*
@ -93,6 +197,8 @@ if (! function_exists('meters2Miles')) {
/**
* Convert from meters to miles.
*
* @since 1.0.0
*
* @param float|int|string $meters
* @param int $precision
*
@ -108,6 +214,8 @@ if (! function_exists('kilometers2Miles')) {
/**
* Convert from kilometers to meters.
*
* @since 1.0.0
*
* @param float|int|string $kilometers
* @param int $precision
*
@ -123,6 +231,8 @@ if (! function_exists('m2Km')) {
/**
* Convert from meters to kilometers.
*
* @since 1.0.0
*
* @param float|int|string $meters
* @param int $precision
*
@ -138,6 +248,8 @@ if (! function_exists('mm2Inches')) {
/**
* Convert from milimeters to inches.
*
* @since 1.0.0
*
* @param float|int|string $milimeters
* @param int $precision
*
@ -153,6 +265,8 @@ if (! function_exists('pa2Mbar')) {
/**
* Convert from pascals to milibars.
*
* @since 1.0.0
*
* @param float|int|string $pascals
* @param int $precision
*

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,11 @@
.spin {
animation: spin 1.5s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/** https://thinkdobecreate.com/articles/css-animating-newly-added-element/ **/
.anim-show {

View File

@ -3,7 +3,7 @@
@import 'tailwindcss/utilities';
@import 'animations.css';
@import 'typography.css';
@import 'fontfaces.css';
@import 'components/buttons.css';
@import 'components/cards.css';

View File

@ -2,6 +2,58 @@
/** | Sans fonts | **/
/** +--------------------------------+ **/
@font-face {
font-family: "OpenSans";
src: url('/fonts/OpenSans/OpenSans-Regular.woff2') format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url('/fonts/OpenSans/OpenSans-Italic.woff2') format("woff2");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url('/fonts/OpenSans/OpenSans-SemiBold.woff2') format("woff2");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url('/fonts/OpenSans/OpenSans-SemiBoldItalic.woff2') format("woff2");
font-weight: 600;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url('/fonts/OpenSans/OpenSans-Bold.woff2') format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url('/fonts/OpenSans/OpenSans-BoldItalic.woff2') format("woff2");
font-weight: 700;
font-style: italic;
font-display: swap;
}
/** +--------------------------------+ **/
/** | Serif fonts | **/
/** +--------------------------------+ **/
@font-face {
font-family: "Nunito";
src: url('/fonts/Nunito/Nunito-Regular.woff2') format("woff2");
@ -50,12 +102,6 @@
font-display: swap;
}
/** +--------------------------------+ **/
/** | Serif fonts | **/
/** +--------------------------------+ **/
/**/
/** +--------------------------------+ **/
/** | Monospace fonts | **/
/** +--------------------------------+ **/

View File

@ -0,0 +1,81 @@
<script setup>
import { reactive, computed, onBeforeMount, provide } from 'vue'
const emit = defineEmits(['themeUpdate'])
let settings = reactive({
theme: 'light',
})
const htmlNode = document.documentElement
let mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
// computed properties
const isDarkMode = computed(() => {
return settings.theme === 'dark'
})
const isLightMode = computed(() => {
return settings.theme === 'light'
})
// lifecycle hooks
onBeforeMount(() => {
window.addEventListener('storage', update)
if (mediaQuery?.addEventListener) {
mediaQuery.addEventListener('change', update)
}
update()
})
onMounted(() => {
provide('darkMode', isDarkMode)
})
// methods
const update = () => {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
setModeDark()
} else {
setModeLight()
}
emit('themeUpdate')
}
const setModeDark = () => {
settings.theme = 'dark'
localStorage.theme = 'dark'
htmlNode.classList.remove('light')
htmlNode.classList.add('dark')
}
const setModeLight = () => {
settings.theme = 'light'
localStorage.theme = 'light'
htmlNode.classList.remove('dark')
htmlNode.classList.add('light')
}
</script>
<template>
<div class="flex items-center cursor-pointer">
<svg v-show="isDarkMode" @click="setModeLight" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg v-show="isLightMode" @click="setModeDark" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</div>
</template>

View File

@ -1,5 +1,62 @@
<script setup>
import { computed } from 'vue'
import { Link } from '@inertiajs/inertia-vue3'
const props = defineProps({
paginationData: Object,
})
// computed properties
const onFirstPage = computed(() => {
return props.paginationData.current_page === 1
})
const hasMorePages = computed(() => {
return props.paginationData.current_page < props.paginationData.last_page
})
const nextPageUrl = computed(() => {
return props.paginationData.next_page_url
})
const previousPageUrl = computed(() => {
return props.paginationData.prev_page_url
})
const firstItem = computed(() => {
if (props.paginationData.from == null) {
return '-'
}
return props.paginationData.from
})
const lastItem = computed(() => {
if (props.paginationData.to == null) {
return '-'
}
return props.paginationData.to
})
const total = computed(() => {
if (isNaN(props.paginationData.total)) {
return '0'
}
return props.paginationData.total
})
// watchers
// lifecycle hooks
// methods
const isFirstOrLastOrDots = (index, linksLength, label) => {
return index === 0 || index === linksLength - 1 || label.includes('...')
}
</script>
<template>
<nav v-if="pagi !== undefined" class="flex items-center justify-between" role="navigation">
<nav v-if="paginationData !== undefined" class="flex items-center justify-between" role="navigation">
<div class="flex justify-between flex-1 sm:hidden">
<span v-if="onFirstPage" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-gray-100 border border-gray-300 cursor-default leading-5 rounded-md">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
@ -45,8 +102,8 @@
</svg>
</Link>
<div v-for="(link, index) in pagi.links">
<Link v-if="!isFirstOrLastOrDots(index, pagi.links.length, link.label)" :class="{ 'bg-blue-200' : link.active }" :href="link.url" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" v-html="link.label"></Link>
<div v-for="(link, index) in paginationData.links">
<Link v-if="!isFirstOrLastOrDots(index, paginationData.links.length, link.label)" :class="{ 'bg-blue-200' : link.active }" :href="link.url" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" v-html="link.label"></Link>
<span v-else-if="link.label === '...'" aria-disabled="true" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5" v-html="link.label"></span>
</div>
@ -65,63 +122,3 @@
</div>
</nav>
</template>
<script>
import { defineComponent } from "vue"
import { Link } from "@inertiajs/inertia-vue3"
export default defineComponent({
props: {
pagi: Object,
},
components: {
Link,
},
computed: {
onFirstPage() {
return this.pagi.current_page === 1
},
hasMorePages() {
return this.pagi.current_page < this.pagi.last_page
},
nextPageUrl() {
return this.pagi.next_page_url
},
previousPageUrl() {
return this.pagi.prev_page_url
},
firstItem() {
if (this.pagi.from == null) {
return '-'
}
return this.pagi.from
},
lastItem() {
if (this.pagi.to == null) {
return '-'
}
return this.pagi.to
},
total() {
if (isNaN(this.pagi.total)) {
return '0'
}
return this.pagi.total
},
},
methods: {
isFirstOrLastOrDots(index, linksLength, label) {
return index === 0 || index === linksLength - 1 || label.includes('...')
},
},
})
</script>

View File

@ -48,7 +48,7 @@
{{-- <link href="https://de.example.com/2010/06/title-of-my-article" rel="alternate" hreflang="de"> --}}
<!-- Android web manifest file -->
{{-- <link href="{{ url('/.webmanifest') }}" rel="manifest"> --}}
{{-- <link href="{{ url('/site.webmanifest') }}" rel="manifest" integrity="{{ env('INTEGRITY_HASH_WEBMANIFEST_JSON') }}"> --}}
<!-- Files listing who was involved in this site and copyrights -->
<link href="{{ url('/humans.txt') }}" rel="author" integrity="{{ env('INTEGRITY_HASH_HUMANS_TXT') }}">
@ -63,9 +63,9 @@
<link rel="alternate" type="text/xml+oembed" href="https://example.com/services/oembed?url=http%3A%2F%2Fexample.com%2Ffoo%2F&amp;format=xml" title="oEmbed Profile: XML">
<!-- Favicon -->
{{-- <link href="{{ asset('/favicon.ico') }}" rel="icon" sizes="16x16" type="image/icon"> --}}
{{-- <link href="{{ asset('/favicon.svg') }}" rel="icon" type="image/svg+xml"> --}}
{{-- <link href="{{ asset('/favicon.png') }}" rel="icon" sizes="192x192"> --}}
{{-- <link href="{{ asset('/favicon.ico') }}" rel="icon" sizes="16x16" type="image/icon" integrity="{{ env('INTEGRITY_HASH_FAVICON_ICO') }}"> --}}
{{-- <link href="{{ asset('/favicon.svg') }}" rel="icon" type="image/svg+xml" integrity="{{ env('INTEGRITY_HASH_FAVICON_SVG') }}"> --}}
{{-- <link href="{{ asset('/favicon.png') }}" rel="icon" sizes="192x192" integrity="{{ env('INTEGRITY_HASH_FAVICON_PNG') }}"> --}}
<!-- Font preloads (should be done for each font file) -->
<link href="{{ asset('/fonts/Nunito/Nunito-Regular.woff2') }}" rel="preload" as="font" type="font/woff2" integrity="{{ env('INTEGRITY_HASH_NUNITO_REGULAR_WOFF2_FONT') }}" crossorigin="anonymous">

View File

@ -3,9 +3,18 @@ const { exec } = require('child_process');
const resIntegrityFiles = [
{ "envKey": "HUMANS_TXT", "path": "public/humans.txt" },
{ "envKey": "ROBOTS_TXT", "path": "public/robots.txt" },
{ "envKey": "COPYRIGHT_HTML", "path": "public/copyright.html" },
{ "envKey": "COPYRIGHT_MD", "path": "public/copyright.md" },
{ "envKey": "RSS_FEED", "path": "public/rss.xml" },
{ "envKey": "ATOM_FEED", "path": "public/feed.atom" },
{ "envKey": "FAVICON_ICO", "path": "public/favicon.ico" },
{ "envKey": "FAVICON_SVG", "path": "public/favicon.svg" },
{ "envKey": "FAVICON_PNG", "path": "public/favicon.png" },
{ "envKey": "NUNITO_REGULAR_WOFF2_FONT", "path": "public/fonts/Nunito/Nunito-Regular.woff2" },
{ "envKey": "POIRETONE_REGULAR_WOFF2_FONT", "path": "public/fonts/PoiretOne/PoiretOne-Regular.woff2" },
{ "envKey": "WEBMANIFEST_JSON", "path": "public/site.webmanifest" },
{ "envKey": "MIX_MANIFEST_JSON", "path": "public/mix-manifest.json" },
{ "envKey": "APP_CSS", "path": "public/css/app.css" },
{ "envKey": "APP_JS", "path": "public/js/app.js" },
];