420 lines
20 KiB
PHP
420 lines
20 KiB
PHP
<x-guest-layout title="SoftSPM 2 - Logowanie">
|
|
|
|
<style>
|
|
.login-screen { min-height: 100vh; display: flex; }
|
|
|
|
/* ===== Lewa kolumna - panel wizualny ===== */
|
|
.login-visual {
|
|
position: relative;
|
|
width: 50%;
|
|
overflow: hidden;
|
|
/* Gradient turkus -> czerwień (jak hero na stronie głównej SoftSPM) */
|
|
background:
|
|
radial-gradient(circle at 86% 10%, rgba(255,255,255,.10), transparent 38%),
|
|
radial-gradient(circle at 8% 92%, rgba(255,255,255,.07), transparent 44%),
|
|
radial-gradient(120% 95% at 102% 102%, rgba(140,22,38,.50), transparent 58%),
|
|
linear-gradient(118deg, #0a5366 0%, #094a5a 33%, #33263a 64%, #5e1f2c 88%, #6e1a26 100%);
|
|
}
|
|
.login-dots {
|
|
position: absolute; inset: 0; z-index: 0;
|
|
background-image: radial-gradient(rgba(255,255,255,.06) 1px, transparent 1.4px);
|
|
background-size: 24px 24px;
|
|
-webkit-mask-image: linear-gradient(to bottom, #000 28%, transparent 92%);
|
|
mask-image: linear-gradient(to bottom, #000 28%, transparent 92%);
|
|
}
|
|
/* Okręgi w tle - bardziej wyraziste */
|
|
.login-ring, .login-ring2 { position: absolute; z-index: 0; border-radius: 50%; }
|
|
.login-ring { top: -180px; right: -150px; width: 470px; height: 470px; border: 1.5px solid rgba(255,255,255,.16); }
|
|
.login-ring2 { top: -90px; right: -60px; width: 310px; height: 310px; border: 1px solid rgba(255,255,255,.11); }
|
|
.login-ring3 { position: absolute; z-index: 0; bottom: -150px; left: -120px; width: 360px; height: 360px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,.10); }
|
|
.login-mesh { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
|
|
.login-brand { position: relative; z-index: 2; padding: 2.75rem 3rem; max-width: min(1200px, 90%); }
|
|
.login-brand .over {
|
|
color: #4fc3e8; font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
|
letter-spacing: .18em; text-transform: uppercase; font-weight: 600;
|
|
}
|
|
.login-brand h1 {
|
|
color: #fff; font-weight: 700; line-height: 1.18; margin-top: .9rem;
|
|
font-size: clamp(26px, 3.2vw, 44px);
|
|
text-shadow: 0 2px 24px rgba(0,0,0,.25);
|
|
}
|
|
|
|
/* ===== Prawa kolumna - formularz ===== */
|
|
.login-form-panel {
|
|
width: 50%; display: flex; align-items: center; justify-content: center;
|
|
padding: 2rem; background: #f7fafb;
|
|
}
|
|
|
|
/* ===== Responsywnie: < 860px pionowo ===== */
|
|
@media (max-width: 860px) {
|
|
.login-screen { flex-direction: column; }
|
|
.login-visual { width: 100%; height: 200px; }
|
|
.login-brand { padding: 1.25rem 1.5rem; }
|
|
.login-form-panel { width: 100%; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.login-mesh { opacity: .9; }
|
|
}
|
|
</style>
|
|
|
|
<div class="login-screen">
|
|
|
|
{{-- ===== LEWA: wizual + animacja ===== --}}
|
|
<div class="login-visual">
|
|
<div class="login-dots"></div>
|
|
<div class="login-ring"></div>
|
|
<div class="login-ring2"></div>
|
|
<div class="login-ring3"></div>
|
|
<canvas id="login-mesh" class="login-mesh" aria-hidden="true" data-mode="text" data-text="SoftSPM"></canvas>
|
|
|
|
<div class="login-brand">
|
|
<p class="over">Submersible Pumps Management</p>
|
|
<h1>Inteligentny Nadzór Eksploatacji Pomp Głębinowych i Raportowanie</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ===== PRAWA: formularz ===== --}}
|
|
<div class="login-form-panel">
|
|
<div class="w-full" style="max-width: 360px;">
|
|
|
|
{{-- Logo - link do softspm.com (ta sama zakładka) --}}
|
|
<div class="mb-7">
|
|
<a href="https://softspm.com" class="inline-block">
|
|
<img src="{{ asset('images/logo.png') }}" alt="SoftSPM" style="width: 220px; max-width: 100%;" class="h-auto">
|
|
</a>
|
|
</div>
|
|
|
|
<h2 class="font-headline-md text-headline-md text-on-surface">Logowanie</h2>
|
|
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1 mb-6">Zaloguj się do systemu SoftSPM</p>
|
|
|
|
@if ($errors->any())
|
|
<div class="status-chip-alert px-3 py-2 rounded-lg font-body-sm text-body-sm mb-4">{{ $errors->first() }}</div>
|
|
@endif
|
|
|
|
<form method="POST" action="{{ route('login.attempt') }}" class="space-y-4" style="padding: 0;">
|
|
@csrf
|
|
|
|
{{-- Nazwa użytkownika --}}
|
|
<div class="space-y-1">
|
|
<label for="username" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Nazwa użytkownika</label>
|
|
<div class="relative">
|
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-on-surface-variant/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
|
</svg>
|
|
<input id="username" name="username" type="text" required autofocus autocomplete="username"
|
|
value="{{ old('username') }}" placeholder="Podaj nazwę użytkownika"
|
|
class="w-full bg-white border border-outline-variant pl-10 pr-3 py-3 rounded-lg focus:ring-1 focus:ring-secondary focus:border-secondary font-body-md text-on-surface transition-all">
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Hasło --}}
|
|
<div class="space-y-1">
|
|
<label for="password" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Hasło</label>
|
|
<div class="relative">
|
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-on-surface-variant/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
</svg>
|
|
<input id="password" name="password" type="password" required autocomplete="current-password"
|
|
placeholder="••••••••"
|
|
class="w-full bg-white border border-outline-variant pl-10 pr-3 py-3 rounded-lg focus:ring-1 focus:ring-secondary focus:border-secondary font-body-md text-on-surface transition-all">
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Oddział - ukryty na czas testów (config softspm.login_department); odwracalne (ustaw null) --}}
|
|
@unless (config('softspm.login_department'))
|
|
<div class="space-y-1">
|
|
<label for="department" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Oddział</label>
|
|
<div class="relative">
|
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-on-surface-variant/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-4"/><path d="M9 9v.01M9 12v.01M9 15v.01M9 18v.01"/>
|
|
</svg>
|
|
<select id="department" name="department" required
|
|
class="w-full bg-white border border-outline-variant pl-10 pr-3 py-3 rounded-lg focus:ring-1 focus:ring-secondary focus:border-secondary font-body-md text-on-surface transition-all">
|
|
@foreach (config('softspm.departments') as $dep)
|
|
<option value="{{ $dep }}" {{ old('department') === $dep ? 'selected' : '' }}>{{ $dep }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
@endunless
|
|
|
|
<x-btn type="submit" variant="primary" class="w-full mt-2">Zaloguj</x-btn>
|
|
</form>
|
|
|
|
<p class="mt-8 text-center font-body-sm text-body-sm text-on-surface-variant/70">
|
|
© SoftSPM {{ date('Y') }} · v.2.0
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ===== Animacja "flowing wave mesh" (Canvas 2D, bez bibliotek) ===== --}}
|
|
<script>
|
|
(function () {
|
|
const canvas = document.getElementById('login-mesh');
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d', { alpha: true });
|
|
|
|
// ===== CONFIG (wartości referencyjne; data-mode/data-text nadpisują) =====
|
|
const CONFIG = {
|
|
mode: canvas.dataset.mode || 'text',
|
|
text: canvas.dataset.text || 'SoftSPM',
|
|
grid: 7,
|
|
alphaThreshold: 120,
|
|
fontWeight: 700,
|
|
textHeightRatio: 0.17,
|
|
textAnchor: { x: 0.07, y: 0.90 },
|
|
amplitude: 1.7,
|
|
speed: 0.0016,
|
|
color: { r: 41, g: 171, b: 226 },
|
|
opacity: 0.85,
|
|
glow: 8,
|
|
lineWidth: 1,
|
|
nodeRadius: 1.15,
|
|
drawNodes: true,
|
|
edgeFalloff: 0.55,
|
|
buckets: 6,
|
|
// tryb wave
|
|
bandCenter: 0.72,
|
|
bandWidth: 0.40,
|
|
waveCols: 90,
|
|
waveRows: 13,
|
|
connectorEvery: 3,
|
|
waves: [
|
|
{ amp: 14, freq: 1.1, speed: 0.55, phase: 0.0 },
|
|
{ amp: 8, freq: 2.3, speed: -0.9, phase: 1.7 },
|
|
{ amp: 5, freq: 3.7, speed: 1.35, phase: 3.1 },
|
|
],
|
|
};
|
|
|
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
const rgba = (a) => 'rgba(' + CONFIG.color.r + ',' + CONFIG.color.g + ',' + CONFIG.color.b + ',' + a + ')';
|
|
|
|
let dpr = 1, cssW = 0, cssH = 0;
|
|
let nodes = [], edges = [], spotX = 0;
|
|
let particles = [];
|
|
let rafId = null, resizeTimer = null, start = 0;
|
|
|
|
function setSize() {
|
|
const rect = canvas.getBoundingClientRect();
|
|
cssW = Math.max(1, Math.round(rect.width));
|
|
cssH = Math.max(1, Math.round(rect.height));
|
|
dpr = Math.min(window.devicePixelRatio || 1, 2); // ostre linie na HiDPI
|
|
canvas.width = Math.round(cssW * dpr);
|
|
canvas.height = Math.round(cssH * dpr);
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
}
|
|
|
|
// ===== Konstelacja tła (dryfujące punkty + linie - jak hero strony głównej) =====
|
|
function makeParticles() {
|
|
const count = Math.round(cssW * cssH / 15000);
|
|
particles = [];
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push({
|
|
x: Math.random() * cssW, y: Math.random() * cssH,
|
|
vx: (Math.random() - 0.5) * 0.22, vy: (Math.random() - 0.5) * 0.22,
|
|
});
|
|
}
|
|
}
|
|
function updateParticles() {
|
|
for (const p of particles) {
|
|
p.x += p.vx; p.y += p.vy;
|
|
if (p.x < 0 || p.x > cssW) p.vx = -p.vx;
|
|
if (p.y < 0 || p.y > cssH) p.vy = -p.vy;
|
|
}
|
|
}
|
|
function drawConstellation() {
|
|
const maxD = 155, maxD2 = maxD * maxD;
|
|
ctx.lineWidth = 1;
|
|
for (let i = 0; i < particles.length; i++) {
|
|
const a = particles[i];
|
|
for (let j = i + 1; j < particles.length; j++) {
|
|
const b = particles[j];
|
|
const dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy;
|
|
if (d2 < maxD2) {
|
|
ctx.strokeStyle = 'rgba(180,214,228,' + ((1 - Math.sqrt(d2) / maxD) * 0.16) + ')';
|
|
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
ctx.fillStyle = 'rgba(205,230,240,.32)';
|
|
ctx.beginPath();
|
|
for (const p of particles) { ctx.moveTo(p.x + 1.3, p.y); ctx.arc(p.x, p.y, 1.3, 0, Math.PI * 2); }
|
|
ctx.fill();
|
|
}
|
|
|
|
// ===== Budowa modelu z napisu =====
|
|
function buildText() {
|
|
const off = document.createElement('canvas');
|
|
off.width = cssW; off.height = cssH;
|
|
const o = off.getContext('2d');
|
|
const fontSize = Math.max(12, Math.round(cssH * CONFIG.textHeightRatio));
|
|
o.clearRect(0, 0, cssW, cssH);
|
|
o.fillStyle = '#fff';
|
|
o.textBaseline = 'alphabetic';
|
|
o.textAlign = 'left';
|
|
o.font = CONFIG.fontWeight + ' ' + fontSize + "px 'Inter', system-ui, sans-serif";
|
|
o.fillText(CONFIG.text, cssW * CONFIG.textAnchor.x, cssH * CONFIG.textAnchor.y);
|
|
const data = o.getImageData(0, 0, cssW, cssH).data;
|
|
|
|
nodes = []; edges = [];
|
|
const map = new Map();
|
|
const g = CONFIG.grid;
|
|
let minX = 1e9, maxX = -1e9;
|
|
for (let y = 0; y < cssH; y += g) {
|
|
const row = (y / g) | 0;
|
|
for (let x = 0; x < cssW; x += g) {
|
|
if (data[(y * cssW + x) * 4 + 3] >= CONFIG.alphaThreshold) {
|
|
const col = (x / g) | 0;
|
|
map.set(col + '_' + row, nodes.length);
|
|
nodes.push({ bx: x, by: y, x: x, y: y, col: col, row: row, ph: col * 0.7 + row * 0.9 });
|
|
if (x < minX) minX = x;
|
|
if (x > maxX) maxX = x;
|
|
}
|
|
}
|
|
}
|
|
buildEdges(map, true);
|
|
spotX = nodes.length ? (minX + maxX) / 2 : cssW * 0.3;
|
|
}
|
|
|
|
// ===== Budowa modelu pasma fal =====
|
|
function buildWave() {
|
|
nodes = []; edges = [];
|
|
const map = new Map();
|
|
const cols = CONFIG.waveCols, rows = CONFIG.waveRows;
|
|
const bandTop = cssH * (CONFIG.bandCenter - CONFIG.bandWidth / 2);
|
|
const bandH = cssH * CONFIG.bandWidth;
|
|
for (let r = 0; r < rows; r++) {
|
|
for (let c = 0; c < cols; c++) {
|
|
const x = cols > 1 ? (c / (cols - 1)) * cssW : 0;
|
|
const y = rows > 1 ? bandTop + (r / (rows - 1)) * bandH : bandTop;
|
|
map.set(c + '_' + r, nodes.length);
|
|
nodes.push({ bx: x, by: y, x: x, y: y, col: c, row: r, ph: c * 0.3 + r * 0.5 });
|
|
}
|
|
}
|
|
buildEdges(map, false);
|
|
spotX = cssW * 0.5;
|
|
}
|
|
|
|
function buildEdges(map, triangular) {
|
|
for (const n of nodes) {
|
|
const i = map.get(n.col + '_' + n.row);
|
|
const right = map.get((n.col + 1) + '_' + n.row);
|
|
if (right !== undefined) edges.push([i, right]);
|
|
if (triangular) {
|
|
const down = map.get(n.col + '_' + (n.row + 1));
|
|
const diag = map.get((n.col + 1) + '_' + (n.row + 1));
|
|
if (down !== undefined) edges.push([i, down]);
|
|
if (diag !== undefined) edges.push([i, diag]);
|
|
} else if (n.col % CONFIG.connectorEvery === 0) {
|
|
const down = map.get(n.col + '_' + (n.row + 1));
|
|
if (down !== undefined) edges.push([i, down]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function build() {
|
|
if (CONFIG.mode === 'wave') buildWave(); else buildText();
|
|
}
|
|
|
|
// ===== Ruch węzłów =====
|
|
function updateNodes(t) {
|
|
if (CONFIG.mode === 'wave') {
|
|
for (const n of nodes) {
|
|
let dy = 0;
|
|
for (const w of CONFIG.waves) {
|
|
dy += w.amp * Math.sin((n.col / CONFIG.waveCols) * Math.PI * 2 * w.freq + t * w.speed + w.phase + n.row * 0.1);
|
|
}
|
|
n.x = n.bx;
|
|
n.y = n.by + dy;
|
|
}
|
|
} else {
|
|
const A = CONFIG.amplitude;
|
|
for (const n of nodes) {
|
|
n.x = n.bx + A * Math.sin(t + n.ph);
|
|
n.y = n.by + A * Math.cos(t * 1.1 + n.ph * 0.8);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== Rysowanie (kwantyzacja jasności do kubełków - 1 stroke/kubełek) =====
|
|
function draw() {
|
|
ctx.clearRect(0, 0, cssW, cssH);
|
|
ctx.shadowBlur = 0;
|
|
drawConstellation(); // tło konstelacji (bez poświaty)
|
|
const B = CONFIG.buckets;
|
|
const bins = [];
|
|
for (let i = 0; i < B; i++) bins.push([]);
|
|
|
|
for (const e of edges) {
|
|
const a = nodes[e[0]], b = nodes[e[1]];
|
|
const nx = Math.abs((a.x + b.x) / 2 - spotX) / cssW; // odległość poziomą od reflektora
|
|
let f = 1 - CONFIG.edgeFalloff * (2 * nx) * (2 * nx); // reflektor
|
|
if (f < 0.05) f = 0.05; else if (f > 1) f = 1;
|
|
bins[Math.min(B - 1, Math.floor((1 - f) * B))].push(e);
|
|
}
|
|
|
|
ctx.lineWidth = CONFIG.lineWidth;
|
|
ctx.lineCap = 'round';
|
|
ctx.shadowColor = rgba(CONFIG.opacity);
|
|
ctx.shadowBlur = CONFIG.glow;
|
|
|
|
for (let bi = 0; bi < B; bi++) {
|
|
const list = bins[bi];
|
|
if (!list.length) continue;
|
|
ctx.strokeStyle = rgba(CONFIG.opacity * (1 - (bi + 0.5) / B));
|
|
ctx.beginPath();
|
|
for (const e of list) {
|
|
const a = nodes[e[0]], b = nodes[e[1]];
|
|
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
|
|
if (CONFIG.drawNodes) {
|
|
ctx.shadowBlur = CONFIG.glow * 0.6;
|
|
ctx.fillStyle = rgba(CONFIG.opacity);
|
|
ctx.beginPath();
|
|
const r = CONFIG.nodeRadius;
|
|
for (const n of nodes) {
|
|
ctx.moveTo(n.x + r, n.y);
|
|
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
|
}
|
|
ctx.fill();
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
function loop(now) {
|
|
updateNodes((now - start) * CONFIG.speed);
|
|
updateParticles();
|
|
draw();
|
|
rafId = requestAnimationFrame(loop);
|
|
}
|
|
|
|
function init() {
|
|
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
|
|
setSize();
|
|
makeParticles();
|
|
build();
|
|
if (reduce) { updateNodes(0); draw(); return; } // statyczny render
|
|
start = performance.now();
|
|
rafId = requestAnimationFrame(loop);
|
|
}
|
|
|
|
function onResize() {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(init, 150);
|
|
}
|
|
window.addEventListener('resize', onResize);
|
|
|
|
// Czekaj na fonty (czytelny napis), potem buduj model.
|
|
if (document.fonts && document.fonts.ready) {
|
|
document.fonts.ready.then(init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|
|
</script>
|
|
</x-guest-layout>
|