feat: letní vzhled
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 37s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 25s
CI / Build client (push) Successful in 37s
CI / Playwright E2E tests (push) Successful in 1m20s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<g stroke="#1B3F73" stroke-width="1.5" stroke-linejoin="round">
|
||||
<path fill="#4A8FE7" d="M48,32 C30,10 8,14 8,34 C8,50 30,52 48,50 Z"/>
|
||||
<path fill="#4A8FE7" d="M52,32 C70,10 92,14 92,34 C92,50 70,52 52,50 Z"/>
|
||||
<path fill="#4A8FE7" d="M47,50 C34,54 16,58 18,74 C20,86 40,84 47,66 Z"/>
|
||||
<path fill="#4A8FE7" d="M53,50 C66,54 84,58 82,74 C80,86 60,84 53,66 Z"/>
|
||||
</g>
|
||||
<g fill="#BBD7FF" stroke="none">
|
||||
<circle cx="20" cy="30" r="4"/>
|
||||
<circle cx="80" cy="30" r="4"/>
|
||||
<circle cx="30" cy="70" r="3"/>
|
||||
<circle cx="70" cy="70" r="3"/>
|
||||
</g>
|
||||
<g stroke="#17233B" stroke-width="1.6" fill="none" stroke-linecap="round">
|
||||
<path d="M49,28 C44,18 40,14 36,11"/>
|
||||
<path d="M51,28 C56,18 60,14 64,11"/>
|
||||
</g>
|
||||
<g fill="#17233B" stroke="none">
|
||||
<circle cx="36" cy="11" r="1.8"/>
|
||||
<circle cx="64" cy="11" r="1.8"/>
|
||||
<rect x="47.5" y="24" width="5" height="52" rx="2.5"/>
|
||||
<circle cx="50" cy="24" r="4.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<g stroke="#7A3B00" stroke-width="1.5" stroke-linejoin="round">
|
||||
<path fill="#F59E1E" d="M48,32 C30,10 8,14 8,34 C8,50 30,52 48,50 Z"/>
|
||||
<path fill="#F59E1E" d="M52,32 C70,10 92,14 92,34 C92,50 70,52 52,50 Z"/>
|
||||
<path fill="#F59E1E" d="M47,50 C34,54 16,58 18,74 C20,86 40,84 47,66 Z"/>
|
||||
<path fill="#F59E1E" d="M53,50 C66,54 84,58 82,74 C80,86 60,84 53,66 Z"/>
|
||||
</g>
|
||||
<g fill="#FFE0A3" stroke="none">
|
||||
<circle cx="20" cy="30" r="4"/>
|
||||
<circle cx="80" cy="30" r="4"/>
|
||||
<circle cx="30" cy="70" r="3"/>
|
||||
<circle cx="70" cy="70" r="3"/>
|
||||
</g>
|
||||
<g stroke="#2B1A0B" stroke-width="1.6" fill="none" stroke-linecap="round">
|
||||
<path d="M49,28 C44,18 40,14 36,11"/>
|
||||
<path d="M51,28 C56,18 60,14 64,11"/>
|
||||
</g>
|
||||
<g fill="#2B1A0B" stroke="none">
|
||||
<circle cx="36" cy="11" r="1.8"/>
|
||||
<circle cx="64" cy="11" r="1.8"/>
|
||||
<rect x="47.5" y="24" width="5" height="52" rx="2.5"/>
|
||||
<circle cx="50" cy="24" r="4.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<g stroke="#8A2255" stroke-width="1.5" stroke-linejoin="round">
|
||||
<path fill="#F06B9B" d="M48,32 C30,10 8,14 8,34 C8,50 30,52 48,50 Z"/>
|
||||
<path fill="#F06B9B" d="M52,32 C70,10 92,14 92,34 C92,50 70,52 52,50 Z"/>
|
||||
<path fill="#F06B9B" d="M47,50 C34,54 16,58 18,74 C20,86 40,84 47,66 Z"/>
|
||||
<path fill="#F06B9B" d="M53,50 C66,54 84,58 82,74 C80,86 60,84 53,66 Z"/>
|
||||
</g>
|
||||
<g fill="#FFD1E3" stroke="none">
|
||||
<circle cx="20" cy="30" r="4"/>
|
||||
<circle cx="80" cy="30" r="4"/>
|
||||
<circle cx="30" cy="70" r="3"/>
|
||||
<circle cx="70" cy="70" r="3"/>
|
||||
</g>
|
||||
<g stroke="#3B0E24" stroke-width="1.6" fill="none" stroke-linecap="round">
|
||||
<path d="M49,28 C44,18 40,14 36,11"/>
|
||||
<path d="M51,28 C56,18 60,14 64,11"/>
|
||||
</g>
|
||||
<g fill="#3B0E24" stroke="none">
|
||||
<circle cx="36" cy="11" r="1.8"/>
|
||||
<circle cx="64" cy="11" r="1.8"/>
|
||||
<rect x="47.5" y="24" width="5" height="52" rx="2.5"/>
|
||||
<circle cx="50" cy="24" r="4.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<g stroke="#8A6D00" stroke-width="1.5" stroke-linejoin="round">
|
||||
<path fill="#FFD23F" d="M48,32 C30,10 8,14 8,34 C8,50 30,52 48,50 Z"/>
|
||||
<path fill="#FFD23F" d="M52,32 C70,10 92,14 92,34 C92,50 70,52 52,50 Z"/>
|
||||
<path fill="#FFD23F" d="M47,50 C34,54 16,58 18,74 C20,86 40,84 47,66 Z"/>
|
||||
<path fill="#FFD23F" d="M53,50 C66,54 84,58 82,74 C80,86 60,84 53,66 Z"/>
|
||||
</g>
|
||||
<g fill="#FFF3C4" stroke="none">
|
||||
<circle cx="20" cy="30" r="4"/>
|
||||
<circle cx="80" cy="30" r="4"/>
|
||||
<circle cx="30" cy="70" r="3"/>
|
||||
<circle cx="70" cy="70" r="3"/>
|
||||
</g>
|
||||
<g stroke="#3A2E00" stroke-width="1.6" fill="none" stroke-linecap="round">
|
||||
<path d="M49,28 C44,18 40,14 36,11"/>
|
||||
<path d="M51,28 C56,18 60,14 64,11"/>
|
||||
</g>
|
||||
<g fill="#3A2E00" stroke="none">
|
||||
<circle cx="36" cy="11" r="1.8"/>
|
||||
<circle cx="64" cy="11" r="1.8"/>
|
||||
<rect x="47.5" y="24" width="5" height="52" rx="2.5"/>
|
||||
<circle cx="50" cy="24" r="4.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,7 +1,7 @@
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { ProvideSettings } from "./context/settings";
|
||||
// import Snowfall from "react-snowfall";
|
||||
import { SnowOverlay } from 'react-snow-overlay';
|
||||
import FlyingButterflies, { BUTTERFLY_PRESETS } from "./FlyingButterflies";
|
||||
import "./FlyingButterflies.scss";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { SocketContext, socket } from "./context/socket";
|
||||
import StatsPage from "./pages/StatsPage";
|
||||
@@ -34,13 +34,7 @@ export default function AppRoutes() {
|
||||
<ProvideSettings>
|
||||
<SocketContext.Provider value={socket}>
|
||||
<>
|
||||
{/* <Snowfall style={{
|
||||
zIndex: 2,
|
||||
position: 'fixed',
|
||||
width: '100vw',
|
||||
height: '100vh'
|
||||
}} /> */}
|
||||
<SnowOverlay color={'rgba(240, 240, 240, 0.9)'} disabledOnSingleCpuDevices={true} />
|
||||
<FlyingButterflies numButterflies={BUTTERFLY_PRESETS.NORMAL} />
|
||||
<App />
|
||||
</>
|
||||
<ToastContainer />
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
.flying-butterflies {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.butterfly-scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
// Vnější element – pozici a natočení nastavuje JS přes transform
|
||||
> div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
will-change: transform;
|
||||
}
|
||||
}
|
||||
|
||||
// Vnitřní element se sprite motýla – máchá křídly (scaleX kolem svislé osy těla)
|
||||
.butterfly-sprite {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
animation: butterfly-flap 0.45s ease-in-out infinite;
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
@keyframes butterfly-flap {
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
50% {
|
||||
transform: scaleX(0.35);
|
||||
}
|
||||
}
|
||||
|
||||
// Ohleduplnost k uživatelům, kteří nechtějí pohyb
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.butterfly-sprite {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
// Různé barevné varianty motýlů
|
||||
const BUTTERFLY_VARIANTS = [
|
||||
'butterfly-orange.svg', // Oranžová (monarcha)
|
||||
'butterfly-blue.svg', // Modrá
|
||||
'butterfly-yellow.svg', // Žlutá (otakárek)
|
||||
'butterfly-pink.svg', // Růžová
|
||||
] as const;
|
||||
|
||||
interface ButterflyData {
|
||||
/** Vnější element – nese pozici, natočení a velikost */
|
||||
el: HTMLDivElement;
|
||||
/** Vnitřní element – nese CSS animaci mávání křídly */
|
||||
sprite: HTMLDivElement;
|
||||
x: number;
|
||||
y: number;
|
||||
/** Směr letu vodorovně: +1 doprava, -1 doleva */
|
||||
dir: number;
|
||||
/** Vodorovná rychlost (px/snímek) */
|
||||
speed: number;
|
||||
/** Velikost (měřítko sprite) */
|
||||
size: number;
|
||||
// Dvě sinusovky pro přirozené zvlněné stoupání a klesání
|
||||
bobFreq1: number;
|
||||
bobPhase1: number;
|
||||
bobAmp1: number;
|
||||
bobFreq2: number;
|
||||
bobPhase2: number;
|
||||
bobAmp2: number;
|
||||
}
|
||||
|
||||
interface FlyingButterfliesProps {
|
||||
/** Počet poletujících motýlů (výchozí: 9) */
|
||||
numButterflies?: number;
|
||||
/** CSS třída pro kontejner (výchozí: 'flying-butterflies') */
|
||||
className?: string;
|
||||
/** Barevné varianty motýlů k použití (výchozí: všechny) */
|
||||
butterflyVariants?: readonly string[];
|
||||
}
|
||||
|
||||
class ButterflyScene {
|
||||
private viewport: HTMLElement;
|
||||
private world: HTMLDivElement;
|
||||
private butterflies: ButterflyData[] = [];
|
||||
private numButterflies: number;
|
||||
private variants: readonly string[];
|
||||
private width: number;
|
||||
private height: number;
|
||||
private timer: number = 0;
|
||||
private animationId: number | null = null;
|
||||
private handleResize: () => void;
|
||||
|
||||
// Základní velikost sprite v px (dále se násobí náhodným měřítkem)
|
||||
private static readonly BASE_SIZE = 34;
|
||||
|
||||
constructor(el: HTMLElement, numButterflies: number = 9, variants: readonly string[] = BUTTERFLY_VARIANTS) {
|
||||
this.viewport = el;
|
||||
this.world = document.createElement('div');
|
||||
this.numButterflies = numButterflies;
|
||||
this.variants = variants;
|
||||
this.width = this.viewport.offsetWidth;
|
||||
this.height = this.viewport.offsetHeight;
|
||||
|
||||
this.handleResize = () => {
|
||||
this.width = this.viewport.offsetWidth;
|
||||
this.height = this.viewport.offsetHeight;
|
||||
};
|
||||
}
|
||||
|
||||
// Nastaví motýlovi nové náhodné parametry a umístí ho na okraj obrazovky
|
||||
private resetButterfly = (b: ButterflyData): void => {
|
||||
// Vletí zleva nebo zprava
|
||||
b.dir = Math.random() > 0.5 ? 1 : -1;
|
||||
b.speed = Math.random() * 1.1 + 0.7; // 0.7 – 1.8 px/snímek
|
||||
|
||||
// Na začátku (timer 0) rozprostřeme motýly po celé šířce, ať není obrazovka prázdná
|
||||
if (this.timer === 0) {
|
||||
b.x = Math.random() * this.width;
|
||||
} else {
|
||||
b.x = b.dir === 1 ? -40 : this.width + 40;
|
||||
}
|
||||
b.y = Math.random() * (this.height - 60) + 30;
|
||||
|
||||
b.size = Math.random() * 0.6 + 0.6; // 0.6 – 1.2
|
||||
|
||||
// Dvě sinusovky s různou frekvencí → nepravidelné, měkké křivky letu
|
||||
b.bobFreq1 = Math.random() * 0.02 + 0.03;
|
||||
b.bobPhase1 = Math.random() * Math.PI * 2;
|
||||
b.bobAmp1 = Math.random() * 0.8 + 0.6;
|
||||
b.bobFreq2 = Math.random() * 0.015 + 0.01;
|
||||
b.bobPhase2 = Math.random() * Math.PI * 2;
|
||||
b.bobAmp2 = Math.random() * 0.6 + 0.4;
|
||||
|
||||
// Náhodná varianta a rychlost mávání křídly
|
||||
const variant = this.variants[Math.floor(Math.random() * this.variants.length)];
|
||||
b.sprite.style.backgroundImage = `url(${variant})`;
|
||||
b.sprite.style.animationDuration = `${Math.random() * 0.25 + 0.3}s`; // 0.3 – 0.55 s
|
||||
};
|
||||
|
||||
private updateButterfly = (b: ButterflyData): void => {
|
||||
const vx = b.dir * b.speed;
|
||||
// Svislá rychlost jako součet dvou sinusovek – přirozené plachtění
|
||||
const vy =
|
||||
Math.sin(this.timer * b.bobFreq1 + b.bobPhase1) * b.bobAmp1 +
|
||||
Math.sin(this.timer * b.bobFreq2 + b.bobPhase2) * b.bobAmp2;
|
||||
|
||||
b.x += vx;
|
||||
b.y += vy;
|
||||
|
||||
// Sprite natočíme po směru letu (motýl je nakreslený hlavou nahoru)
|
||||
const angle = Math.atan2(vy, vx) * (180 / Math.PI) + 90;
|
||||
|
||||
b.el.style.transform = `translate(${b.x}px, ${b.y}px) rotate(${angle}deg) scale(${b.size})`;
|
||||
|
||||
// Reset po opuštění obrazovky (s rezervou na natočení)
|
||||
if (
|
||||
(b.dir === 1 && b.x > this.width + 50) ||
|
||||
(b.dir === -1 && b.x < -50) ||
|
||||
b.y < -60 ||
|
||||
b.y > this.height + 60
|
||||
) {
|
||||
this.resetButterfly(b);
|
||||
}
|
||||
};
|
||||
|
||||
public init = (): void => {
|
||||
this.butterflies = [];
|
||||
this.world.innerHTML = '';
|
||||
this.world.className = 'butterfly-scene';
|
||||
|
||||
for (let i = 0; i < this.numButterflies; i++) {
|
||||
const el = document.createElement('div');
|
||||
const sprite = document.createElement('div');
|
||||
sprite.className = 'butterfly-sprite';
|
||||
el.appendChild(sprite);
|
||||
|
||||
const b: ButterflyData = {
|
||||
el,
|
||||
sprite,
|
||||
x: 0,
|
||||
y: 0,
|
||||
dir: 1,
|
||||
speed: 1,
|
||||
size: 1,
|
||||
bobFreq1: 0,
|
||||
bobPhase1: 0,
|
||||
bobAmp1: 0,
|
||||
bobFreq2: 0,
|
||||
bobPhase2: 0,
|
||||
bobAmp2: 0,
|
||||
};
|
||||
|
||||
this.resetButterfly(b);
|
||||
this.butterflies.push(b);
|
||||
this.world.appendChild(el);
|
||||
}
|
||||
|
||||
this.viewport.appendChild(this.world);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
};
|
||||
|
||||
public render = (): void => {
|
||||
for (let i = 0; i < this.butterflies.length; i++) {
|
||||
this.updateButterfly(this.butterflies[i]);
|
||||
}
|
||||
|
||||
this.timer++;
|
||||
this.animationId = requestAnimationFrame(this.render);
|
||||
};
|
||||
|
||||
public destroy = (): void => {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
|
||||
if (this.world && this.world.parentNode) {
|
||||
this.world.parentNode.removeChild(this.world);
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponenta pro zobrazení poletujících motýlů na pozadí stránky.
|
||||
* Motýli plachtí vodorovně po měkkých zvlněných křivkách a mávají křídly.
|
||||
*
|
||||
* @param numButterflies - Počet motýlů (výchozí: 9)
|
||||
* @param className - CSS třída pro kontejner (výchozí: 'flying-butterflies')
|
||||
* @param butterflyVariants - Barevné varianty motýlů (výchozí: všechny)
|
||||
*/
|
||||
const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
numButterflies = 9,
|
||||
className = 'flying-butterflies',
|
||||
butterflyVariants = BUTTERFLY_VARIANTS,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const sceneRef = useRef<ButterflyScene | null>(null);
|
||||
|
||||
const initialize = useCallback(() => {
|
||||
if (containerRef.current) {
|
||||
sceneRef.current = new ButterflyScene(containerRef.current, numButterflies, butterflyVariants);
|
||||
sceneRef.current.init();
|
||||
sceneRef.current.render();
|
||||
}
|
||||
}, [numButterflies, butterflyVariants]);
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
|
||||
return () => {
|
||||
if (sceneRef.current) {
|
||||
sceneRef.current.destroy();
|
||||
sceneRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [initialize]);
|
||||
|
||||
return <div ref={containerRef} className={className} />;
|
||||
};
|
||||
|
||||
// Přednastavení množství motýlů pro různé účely
|
||||
export const BUTTERFLY_PRESETS = {
|
||||
LIGHT: 5, // Pár motýlů
|
||||
NORMAL: 9, // Standardní množství
|
||||
HEAVY: 16, // Rušná letní louka
|
||||
} as const;
|
||||
|
||||
// Přednastavené barevné kombinace
|
||||
export const BUTTERFLY_COLOR_THEMES = {
|
||||
ALL: BUTTERFLY_VARIANTS,
|
||||
WARM: ['butterfly-orange.svg', 'butterfly-yellow.svg', 'butterfly-pink.svg'] as const,
|
||||
COOL: ['butterfly-blue.svg'] as const,
|
||||
} as const;
|
||||
|
||||
export default FlyingButterflies;
|
||||
@@ -1,5 +1,6 @@
|
||||
[
|
||||
"Oprava: uložení preferovaného času odchodu i u podniku bez načteného menu",
|
||||
"Oprava: načítání denního menu podniku TechTower po změně jejich webu",
|
||||
"Oprava: rychlé klikání na šipky přepínání dní už nepřeskočí mimo pracovní týden"
|
||||
"Oprava: rychlé klikání na šipky přepínání dní už nepřeskočí mimo pracovní týden",
|
||||
"Letní vzhled: místo padajícího sněhu teď po stránce poletují motýli"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user