}
/*============================================================*/
+
+.animate-shake-horizontal {
+ animation-name: animate-shake-horizontal;
+ animation-timing-function: cubic-bezier(0.455, 0.030, 0.515, 0.955);
+}
+
+@keyframes animate-shake-horizontal {
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+ 10%,
+ 30%,
+ 50%,
+ 70% {
+ transform: translateX(-10px);
+ }
+ 20%,
+ 40%,
+ 60% {
+ transform: translateX(10px);
+ }
+ 80% {
+ transform: translateX(8px);
+ }
+ 90% {
+ transform: translateX(-8px);
+ }
+}
+
+/*============================================================*/
--- /dev/null
+{{-- PIN Code Component --}}
+@php
+ $pin = $pin ?? '1234';
+@endphp
+
+<div x-data="lockscreen()"
+ @keydown.window="keyboard(event)"
+ class="absolute top-0 left-0
+ w-full h-screen
+ flex items-center justify-center
+ text-3xl select-none"
+ x-show="!unlocked"
+ x-transition.opacity
+ x-cloak>
+
+ <div class="relative flex flex-col items-center">
+ <h3 class="font-semibold">Saisir le code d’accès</h3>
+
+ <div class="absolute -top-10 font-semibold text-red text-lg" x-show="failed" x-transition.opacity>
+ Ce code n’est pas valide
+ </div>
+
+ {{-- PIN DIGITS --}}
+ <div class="mt-6 space-x-3" :class="{ 'animate animate-shake-horizontal': failed }" style="--animation-duration: 0.7s">
+ <template x-for="i in pin.length">
+ <div :class="{
+ 'border-[#d5d5d5]': !success && !failed && !input[i - 1] && (i !== input.length + 1),
+ 'bg-blue border-blue': !success && !failed && input[i - 1],
+ 'bg-red border-red': failed,
+ 'bg-green border-green': success,
+ 'pin-input-focused border-white': (i === input.length + 1)
+ }"
+ class="relative w-16 h-16
+ inline-flex items-center justify-center
+ text-white
+ border border-2 rounded-lg
+ transition">
+ <span class="inline-block text-3xl mt-2">*</span>
+ </div>
+ </template>
+ </div>
+
+ {{-- KEYPAD BUTTONS --}}
+ <div class="grid gap-3 mt-8" style="grid-template-columns: repeat(3, min-content)">
+ <?php
+ $digits = [
+ '1', '2', '3',
+ '4', '5', '6',
+ '7', '8', '9',
+ '-', '0', 'x',
+ ];
+
+ foreach($digits as $digit) {
+
+ $click = "@click=\"type('$digit')\"";
+
+ $class = 'inline-flex items-center justify-center
+ w-22 h-22
+ bg-[#fafafa] hover:bg-[#d5d5d5]
+ rounded-full
+ cursor-pointer';
+
+ if ($digit === '-') {
+ $digit = $click = $class = '';
+ } elseif ($digit === 'x') {
+ $digit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.9 17.9" xml:space="preserve" class="fill-current w-[1em]"><path d="M19.69 17.9H9.93c-1.15 0-2.25-.44-3.08-1.24l-6.2-5.94C-.19 9.9-.21 8.57.59 7.75l6.22-6.39C7.64.5 8.81 0 10 0h9.68c2.32 0 4.21 1.89 4.21 4.21v9.48a4.202 4.202 0 0 1-4.2 4.21zM10.01 1.28c-.85 0-1.69.35-2.28.97L1.51 8.63a.82.82 0 0 0 .01 1.16l6.2 5.94c.59.57 1.38.88 2.2.89h9.76c1.62 0 2.93-1.32 2.93-2.94V4.21c0-1.62-1.32-2.93-2.93-2.93h-9.67z"/><path d="m15.35 8.96 2.67-2.67c.25-.25.25-.65 0-.9a.634.634 0 0 0-.9 0l-2.67 2.67-2.67-2.67c-.25-.25-.65-.25-.9 0s-.25.65 0 .9l2.67 2.67-2.67 2.67c-.25.25-.25.65 0 .9.12.12.29.19.45.19s.33-.06.45-.19l2.67-2.67 2.67 2.67c.12.12.29.19.45.19s.33-.06.45-.19c.25-.25.25-.65 0-.9l-2.67-2.67z"/></svg>';
+ }
+ ?>
+ <span class="{{ $class }}" {!! $click !!}>
+ {!! $digit !!}
+ </span>
+ <?php } ?>
+ </div>
+
+ </div>
+
+</div>
+
+@push('after_css')
+ <style>
+ .pin-input-focused {
+ position: relative;
+ box-shadow: 0 5px 25px rgb(0 0 0 / 15%);
+ }
+ .pin-input-focused:after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 3px;
+ height: 1em;
+ background-color: black;
+ animation: 1.25s cursor-blink step-end infinite;
+ }
+
+ @keyframes cursor-blink {
+ from, to {
+ background-color: transparent;
+ }
+ 50% {
+ background-color: black;
+ }
+ }
+ </style>
+@endpush
+
+@push('before_scripts')
+ <script>
+ function lockscreen() {
+ return {
+ {{-- Some very light protection so PIN isn't directly visible in source --}}
+ secret: '{{ base64_encode($pin) }}',
+ failed: false,
+ success: false,
+ input: [],
+
+ get pin() {
+ return atob(this.secret); {{-- JS equivalent of base64_decode --}}
+ },
+
+ type(key) {
+ if (key === 'x') { // Delete key
+ this.input.pop();
+ return true;
+ }
+
+ // Don't accept more digits if all are entered already
+ if (this.input.length >= this.pin.length) {
+ return false;
+ }
+
+ this.input.push(key);
+ this.verify();
+ },
+
+ keyboard(event) {
+ // Handle physical keyboard entry but only allow numbers + backspace key
+ let key = event.key.replace(/\D/g, "");
+ if (key !== '') {
+ this.type(key);
+ } else if (event.key === 'Backspace') {
+ this.type('x'); // Special case
+ }
+ },
+
+ verify() {
+ if (this.input.length < this.pin.length) return false;
+
+ this.success = this.input.join('') === this.pin;
+
+ if (this.success) {
+ setTimeout(() => this.unlocked = true, 500);
+ } else {
+ this.failed = true;
+
+ // Reset input and errors after a delay
+ setTimeout(() => {
+ this.input = [];
+ this.failed = false;
+ }, 2500);
+ }
+ },
+ }
+ }
+ </script>
+@endpush
@section('body_tag')
{{-- Make sure no scrollbars are present because they affect the background scaling --}}
- <body class="overflow-hidden">
+ <body class="overflow-hidden font-primary">
@endsection
@section('main')
{{-- Title + Illustration --}}
- <div x-data="{ shown: false }" x-intersect="shown = true"
+ <div x-data="splash()"
class="h-screen flex items-center z-10"
x-cloak>
<video playsinline
muted
preload="auto"
- data-delay="1000" {{-- How long in milliseconds to wait before starting the video --}}
+ data-delay="0" {{-- How long in milliseconds to wait before starting the video --}}
id="background_video"
class="w-full absolute left-0 bottom-0">
<source src="{{ asset('images/splash.mp4') }}" type="video/mp4">
<x-link href="home"
id="home_link"
+ data-delay="4000" {{-- How long in ms to wait before triggering click once unlocked --}}
class="w-full max-h-[90vh] flex flex-col items-center justify-around">
- <img x-show="shown"
+ <img x-show="unlocked"
x-transition.opacity.scale.75.origin.bottom.duration.1000ms
class="max-w-[488px] mb-16"
src="{{ asset('images/splash-text.svg') }}"
alt="Source d'avenir">
- <img x-show="shown"
+ <img x-show="unlocked"
x-transition.opacity.scale.95.origin.center.duration.1200ms.delay.800ms
class="max-w-[520px]"
style="backface-visibility: hidden"
src="{{ asset('images/splash-illustration.png') }}">
</x-link>
+ <x-pin-code :pin="$settings->get('pin')" />
+
</div>
@endsection
-@push('after_scripts')
+@push('before_scripts')
<script>
- // Delay start of video
- const startVideo = async (video) => {
- try {
- await video.play();
- video.setAttribute('autoplay', true);
- } catch (err) {
- console.warn(err, 'Error playing video');
- }
- }
+ function splash() {
+ return {
+ unlocked: false,
+
+ init() {
+ this.startVideo();
+ this.handleUnlock();
+ },
- //========================================================
+ startVideo() {
+ // Delay start of video
+ const startVideo = async (video) => {
+ try {
+ await video.play();
+ video.setAttribute('autoplay', true);
+ } catch (err) {
+ console.warn(err, 'Error playing video');
+ }
+ }
- const video = document.querySelector('#background_video');
+ //========================================================
- // Trigger click on main link when video ends
- video.addEventListener('ended', () => document.querySelector('#home_link').click());
+ const video = document.querySelector('#background_video');
- // Start video after a certain delay
- setTimeout(() => startVideo(video), video.dataset?.delay);
+ // Start video after a certain delay
+ setTimeout(() => startVideo(video), video.dataset?.delay);
+ },
+
+ handleUnlock() {
+ this.homeLink = document.querySelector('#home_link');
+ this.redirectDelay = this.homeLink.dataset.delay || 2000;
+
+ // Once the interface is unlocked, trigger click on main link after a specified delay
+ this.$watch('unlocked', (isUnlocked) => {
+ if (isUnlocked) {
+ let $this = this;
+ setTimeout(() => $this.homeLink.click(), parseInt(this.redirectDelay));
+ }
+ });
+ },
+ }
+ }
</script>
@endpush