booty #93

Merged
lubiana merged 3 commits from booty into main 2025-06-18 18:16:34 +00:00
47 changed files with 1745 additions and 1291 deletions

5
.gitignore vendored
View file

@ -26,3 +26,8 @@
###> phpstan/phpstan ### ###> phpstan/phpstan ###
phpstan.neon phpstan.neon
###< phpstan/phpstan ### ###< phpstan/phpstan ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###

22
assets/app.js Normal file
View file

@ -0,0 +1,22 @@
/*
* Welcome to your app's main JavaScript file!
*
* This file will be included onto the page via the importmap() Twig function,
* which should already be in your base.html.twig.
*/
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles/app.css';
import './styles/modes.css';
import './styles/emoji-footprint.css';
// Import modules
import './javascript/theme.js';
import './javascript/emoji-footprint.js';
import './javascript/modes.js';
import './javascript/htmx.js';
import 'bootstrap';
import { initRadioState } from './javascript/radioState.js';
document.addEventListener('DOMContentLoaded', () => {
initRadioState();
});

View file

@ -0,0 +1,19 @@
// Sparkle effect on mouse move
document.addEventListener('mousemove', function (e) {
const emojis = ['✨', '💖', '🌟', '💅', '🦄', '🎉', '🌈'];
const sparkle = document.createElement('div');
sparkle.className = 'emoji-footprint';
sparkle.textContent = emojis[Math.floor(Math.random() * emojis.length)];
sparkle.style.left = e.pageX + 'px';
sparkle.style.top = e.pageY + 'px';
document.body.appendChild(sparkle);
setTimeout(() => {
sparkle.remove();
}, 1000);
});
export function initEmojiFootprint() {
// The sparkle effect is already initialized when this module is imported
// This function can be used if we need to control when the effect starts
}

View file

@ -0,0 +1,3 @@
import htmx from 'htmx.org';
window.htmx = htmx;

136
assets/javascript/modes.js Normal file
View file

@ -0,0 +1,136 @@
// Bonkers mode functionality
function setEmojiLevelClass(mode) {
document.body.classList.remove('emoji-normal', 'emoji-enhanced', 'emoji-bonkers');
if (mode === 'bonkers') {
document.body.classList.add('emoji-bonkers');
} else if (mode === 'enhanced') {
document.body.classList.add('emoji-enhanced');
} else {
document.body.classList.add('emoji-normal');
}
}
function initBonkersMode() {
// Check if we're in bonkers mode
const currentMode = document.documentElement.getAttribute('data-website-mode');
setEmojiLevelClass(currentMode);
if (currentMode === 'bonkers') {
// Apply bonkers mode immediately
document.body.classList.add('bonkers-mode');
// Start the fabulous effects
createExtraSparkles();
createSlayEffects();
console.log('🌈✨ Bonkers mode activated! ✨🌈');
} else {
// Remove bonkers mode if it was active
document.body.classList.remove('bonkers-mode');
}
}
// Function to create extra sparkles during bonkers mode
function createExtraSparkles() {
const currentMode = document.documentElement.getAttribute('data-website-mode');
if (currentMode !== 'bonkers') return;
const extraEmojis = [
'💃', '🕺',
'🍑', '💦', '😏', '😈', '👅', '💋', '🥵', '😳', '🤤', '😍', '🥴',
'💕', '💖', '💗', '💘', '💝', '💞', '💟', '💌', '💏', '💑',
'🍆', '🥒', '🍌', '💦', '👀', '😉', '😌', '😍', '🥰', '😘',
'😚', '😋', '😏', '😫', '😩', '🥺', '🥵', '🥴',
'💖', '💗', '💕', '💞', '💓', '💗', '💖', '💘', '💝',
'💋', '💏', '💑'
];
const sparkle = document.createElement('div');
sparkle.className = 'emoji-footprint';
sparkle.textContent = extraEmojis[Math.floor(Math.random() * extraEmojis.length)];
sparkle.style.left = Math.random() * window.innerWidth + 'px';
sparkle.style.top = Math.random() * window.innerHeight + 'px';
document.body.appendChild(sparkle);
setTimeout(() => {
if (sparkle.parentNode) {
sparkle.remove();
}
}, 3000);
// Continue creating extra sparkles while in bonkers mode
const newMode = document.documentElement.getAttribute('data-website-mode');
if (newMode === 'bonkers') {
setTimeout(() => createExtraSparkles(), 150);
}
}
// Function to create slay effects
function createSlayEffects() {
const currentMode = document.documentElement.getAttribute('data-website-mode');
if (currentMode !== 'bonkers') return;
// Create floating "SLAY" text effects
const slayWords = [
'SLAY', 'QUEEN', 'FABULOUS', 'ICONIC', 'LEGENDARY', 'STUNNING', 'GORGEOUS', 'FLAWLESS',
'DAZZLING', 'RADIANT', 'BREATHTAKING', 'EXQUISITE', 'DIVINE'
];
const slayElement = document.createElement('div');
slayElement.className = 'slay-text';
slayElement.textContent = slayWords[Math.floor(Math.random() * slayWords.length)];
slayElement.style.left = Math.random() * window.innerWidth + 'px';
slayElement.style.top = Math.random() * window.innerHeight + 'px';
document.body.appendChild(slayElement);
setTimeout(() => {
if (slayElement.parentNode) {
slayElement.remove();
}
}, 3000);
// Continue creating slay effects while in bonkers mode
const newMode = document.documentElement.getAttribute('data-website-mode');
if (newMode === 'bonkers') {
setTimeout(() => createSlayEffects(), 800);
}
}
// Watch for mode changes
function watchModeChanges() {
// Create a MutationObserver to watch for changes to the data-website-mode attribute
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-website-mode') {
const newMode = document.documentElement.getAttribute('data-website-mode');
if (newMode === 'bonkers') {
document.body.classList.add('bonkers-mode');
setEmojiLevelClass(newMode);
// Start the fabulous effects
createExtraSparkles();
createSlayEffects();
console.log('🌈✨ Switched to bonkers mode! ✨🌈');
} else {
document.body.classList.remove('bonkers-mode');
setEmojiLevelClass(newMode);
console.log(`😴 Switched to ${newMode} mode`);
}
}
});
});
// Start observing
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-website-mode']
});
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
initBonkersMode();
watchModeChanges();
});
export { initBonkersMode, watchModeChanges };

View file

@ -0,0 +1,55 @@
// Function to initialize number input buttons
function initNumberInputs(container = document) {
container.querySelectorAll('.number-input-wrapper').forEach(function(wrapper) {
const input = wrapper.querySelector('input[type="number"]');
const decreaseBtn = wrapper.querySelector('[data-action="decrease"]');
const increaseBtn = wrapper.querySelector('[data-action="increase"]');
if (!input || !decreaseBtn || !increaseBtn) return;
// Skip if already initialized
if (decreaseBtn.hasAttribute('data-initialized')) return;
const step = parseFloat(input.getAttribute('step')) || 1;
const min = 0;
const max = input.getAttribute('max') ? parseFloat(input.getAttribute('max')) : null;
decreaseBtn.addEventListener('click', function() {
const currentValue = parseFloat(input.value) || 0;
const newValue = currentValue - step;
if (min === null || newValue >= min) {
input.value = newValue;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
});
increaseBtn.addEventListener('click', function() {
const currentValue = parseFloat(input.value) || 0;
const newValue = currentValue + step;
if (max === null || newValue <= max) {
input.value = newValue;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
});
// Validate input on change
input.addEventListener('input', function() {
const value = parseFloat(this.value);
if (min !== null && value < min) {
this.value = min;
}
if (max !== null && value > max) {
this.value = max;
}
});
// Mark as initialized
decreaseBtn.setAttribute('data-initialized', 'true');
increaseBtn.setAttribute('data-initialized', 'true');
});
}
export { initNumberInputs };

View file

@ -0,0 +1,35 @@
// Radio button state management with localStorage
function initRadioState() {
// Store and retrieve radio button state
const radioButtons = document.querySelectorAll('input[name="mode"]');
// Load saved state on page load
const savedMode = localStorage.getItem('selectedMode');
if (savedMode) {
const radioToCheck = document.getElementById(savedMode);
if (radioToCheck) {
radioToCheck.checked = true;
// Set the data attribute to match the saved mode
document.documentElement.setAttribute('data-website-mode', savedMode);
}
} else {
// If no saved state, set to the currently checked radio button
const checkedRadio = document.querySelector('input[name="mode"]:checked');
if (checkedRadio) {
document.documentElement.setAttribute('data-website-mode', checkedRadio.id);
}
}
// Save state when radio button changes
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
if (this.checked) {
localStorage.setItem('selectedMode', this.id);
// Update the data attribute when mode changes
document.documentElement.setAttribute('data-website-mode', this.id);
}
});
});
}
export { initRadioState };

View file

@ -0,0 +1,18 @@
// Theme detection and switching
const getPreferredTheme = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
document.documentElement.setAttribute('data-bs-theme', theme)
}
// Set initial theme
setTheme(getPreferredTheme())
// Listen for changes in user's preferred color scheme
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
setTheme(getPreferredTheme())
})
export { getPreferredTheme, setTheme };

179
assets/styles/app.css Normal file
View file

@ -0,0 +1,179 @@
/*
* =================================================================================================
* 💖 BUBBLEGUM PUNK THEME (LIGHT) 💖
*
* This isn't just a theme. It's a statement.
* Unapologetically loud, pink, and quirky.
* =================================================================================================
*/
:root,
[data-bs-theme=light] {
/* --- CORE VIBE --- */
--bs-pink: #FF007A; /* 💖 Hyper Pink (Our Queen) */
--bs-green: #CFFF50; /* 🧪 Toxic Slime */
--bs-purple: #A328D6; /* 👾 Graffiti Purple */
--bs-yellow: #F9F871; /* ⚡ Neon Lemon */
--bs-cyan: #00F5D4; /* 💎 Glitchy Teal */
--bs-blue: #00A9E0; /* 💦 Splash Zone */
/* Let's redefine ALL the core colors to match the new energy */
--bs-primary: var(--bs-pink);
--bs-secondary: var(--bs-green);
--bs-success: var(--bs-cyan);
--bs-info: var(--bs-blue);
--bs-warning: var(--bs-yellow);
--bs-danger: #FF3D3D; /* 🚨 Code Red Rave */
/* --- BACKGROUNDS & TEXT --- */
/* No more boring white! */
--bs-body-bg: #FFF5FD; /* A soft, dreamy pink canvas */
--bs-body-color: #4A003D; /* Dark Plum (instead of black) for text */
--bs-heading-color: var(--bs-purple); /* Make headings POP */
--bs-secondary-color: rgba(74, 0, 61, 0.75); /* Plum, but softer */
--bs-tertiary-color: rgba(74, 0, 61, 0.5);
/* Make cards and containers pure white to contrast the pink background */
--bs-tertiary-bg: #FFFFFF;
--bs-secondary-bg: #FEF9FE;
/* --- LINKS & CODE --- */
--bs-link-color: var(--bs-pink);
--bs-link-hover-color: var(--bs-purple);
--bs-code-color: var(--bs-purple);
/* --- BORDERS & SHADOWS: LET'S GET QUIRKY --- */
--bs-border-width: 2px; /* Chunky borders! */
--bs-border-color: #FFD6F5; /* Pink-tinted border color */
--bs-border-color-translucent: rgba(74, 0, 61, 0.2);
--bs-border-radius: 1rem; /* Super bubbly and round */
--bs-border-radius-sm: 0.5rem;
--bs-border-radius-lg: 1.5rem;
--bs-border-radius-pill: 50rem;
/* Say goodbye to black shadows, hello to colored glows! */
--bs-box-shadow: 0 4px 12px rgba(255, 0, 122, 0.2);
--bs-box-shadow-sm: 0 2px 4px rgba(255, 0, 122, 0.15);
--bs-box-shadow-lg: 0 8px 30px rgba(255, 0, 122, 0.25);
--bs-box-shadow-inset: inset 0 1px 4px rgba(74, 0, 61, 0.2);
/* --- THE GRADIENT: THE SOUL OF THE THEME --- */
--bs-gradient: linear-gradient(75deg, var(--bs-primary), var(--bs-secondary));
/* --- Don't forget the RGB values for Bootstrap components! --- */
--bs-primary-rgb: 255, 0, 122;
--bs-secondary-rgb: 207, 255, 80;
--bs-body-color-rgb: 74, 0, 61;
--bs-body-bg-rgb: 255, 245, 253;
}
/*
* =================================================================================================
* 🌙🦇 CYBER GOTH THEME (DARK) 🦇🌙
*
* The lights are out, the neon is ON.
* A dark, moody theme with vibrant, glowing accents.
* =================================================================================================
*/
[data-bs-theme=dark] {
color-scheme: dark;
/* --- BACKGROUNDS & TEXT --- */
--bs-body-bg: #1D001A; /* Deep, dark space purple */
--bs-body-color: #FFE9FA; /* Light pink text for high contrast */
--bs-heading-color: var(--bs-cyan); /* Glowing cyan headings */
--bs-tertiary-bg: #2E0028; /* A slightly lighter container background */
--bs-secondary-bg: #3A0033;
--bs-secondary-color: rgba(255, 233, 250, 0.75);
--bs-tertiary-color: rgba(255, 233, 250, 0.5);
/* --- LINKS & CODE --- */
/* Using the Toxic Slime for links gives it that cyber look */
--bs-link-color: var(--bs-green);
--bs-link-hover-color: var(--bs-cyan);
--bs-code-color: var(--bs-pink);
/* --- BORDERS & SHADOWS: NEON GLOWS --- */
--bs-border-color: #5C004F;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
/* Redefine shadows to be neon glows */
--bs-box-shadow: 0 0 15px rgba(var(--bs-primary-rgb), 0.4);
--bs-box-shadow-lg: 0 0 30px rgba(var(--bs-primary-rgb), 0.5);
/* --- EMPHASIS & SUBTLE BACKGROUNDS --- */
/* These are for alerts, badges, etc. They'll be dark with glowing text. */
--bs-primary-text-emphasis: #FF8AD1;
--bs-secondary-text-emphasis: #E2FF8A;
--bs-success-text-emphasis: #8AFFEB;
--bs-info-text-emphasis: #7ADCF5;
--bs-warning-text-emphasis: #FAF8A8;
--bs-danger-text-emphasis: #FF8A8A;
--bs-primary-bg-subtle: #3D002B;
--bs-secondary-bg-subtle: #415215;
--bs-success-bg-subtle: #00332B;
--bs-info-bg-subtle: #00313D;
--bs-warning-bg-subtle: #3E3D1C;
--bs-danger-bg-subtle: #520E0E;
}
/* === EMOJI LEVELS === */
.emoji-normal .emoji-normal { display: inline; }
.emoji-normal .emoji-enhanced,
.emoji-normal .emoji-bonkers { display: none; }
.emoji-enhanced .emoji-enhanced { display: inline; }
.emoji-enhanced .emoji-normal,
.emoji-enhanced .emoji-bonkers { display: none; }
.emoji-bonkers .emoji-bonkers { display: inline; }
.emoji-bonkers .emoji-normal,
.emoji-bonkers .emoji-enhanced { display: none; }
/*
* =================================================================================================
* 🌈 RAINBOW PRIDE ELEMENTS 🌈
*
* Fabulous rainbow-themed elements to celebrate diversity and pride!
* =================================================================================================
*/
.bg-rainbow {
background: linear-gradient(
to right,
#FF5757, /* Red */
#FFBD59, /* Orange */
#F9F871, /* Yellow */
#CFFF50, /* Green */
#00F5D4, /* Teal */
#00A9E0, /* Blue */
#A328D6 /* Purple */
);
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
font-weight: bold;
border: none;
}
.fun-fact {
font-size: 1.1rem;
line-height: 1.6;
font-style: italic;
}
/* Add a subtle rainbow border to the fun facts card */
.card:has(.fun-fact) {
border-width: 2px;
border-style: solid;
border-image: linear-gradient(
to right,
#FF5757, /* Red */
#FFBD59, /* Orange */
#F9F871, /* Yellow */
#CFFF50, /* Green */
#00F5D4, /* Teal */
#00A9E0, /* Blue */
#A328D6 /* Purple */
) 1;
box-shadow: 0 4px 15px rgba(163, 40, 214, 0.2);
}

View file

@ -0,0 +1,30 @@
/* Emoji Footprint Animation */
.emoji-footprint {
position: absolute;
font-size: 1.6rem;
pointer-events: none;
animation: emojiFade 1s ease-out forwards;
transform: translate(-50%, -50%) scale(1);
opacity: 1;
z-index: 9999;
text-shadow:
0 0 4px #ff00bf,
0 0 8px #ff80df,
0 0 12px #ffccff;
}
@keyframes emojiFade {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
50% {
transform: translate(-50%, -50%) scale(1.5);
opacity: 0.7;
}
100% {
transform: translate(-50%, -50%) scale(2);
opacity: 0;
}
}

565
assets/styles/modes.css Normal file
View file

@ -0,0 +1,565 @@
/* 🌈✨ BONKERS MODE ANIMATIONS ✨🌈 */
@keyframes rainbowGradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes discoFlash {
0%, 100% {
background-color: var(--bs-pink);
box-shadow: 0 0 20px var(--bs-pink), 0 0 40px var(--bs-pink);
}
16.66% {
background-color: var(--bs-purple);
box-shadow: 0 0 20px var(--bs-purple), 0 0 40px var(--bs-purple);
}
33.33% {
background-color: var(--bs-cyan);
box-shadow: 0 0 20px var(--bs-cyan), 0 0 40px var(--bs-cyan);
}
50% {
background-color: var(--bs-yellow);
box-shadow: 0 0 20px var(--bs-yellow), 0 0 40px var(--bs-yellow);
}
66.66% {
background-color: var(--bs-green);
box-shadow: 0 0 20px var(--bs-green), 0 0 40px var(--bs-green);
}
83.33% {
background-color: var(--bs-orange);
box-shadow: 0 0 20px var(--bs-orange), 0 0 40px var(--bs-orange);
}
}
@keyframes wiggle {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-2deg); }
75% { transform: rotate(2deg); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes rainbowText {
0% { color: var(--bs-red); }
14.28% { color: var(--bs-orange); }
28.57% { color: var(--bs-yellow); }
42.85% { color: var(--bs-green); }
57.14% { color: var(--bs-cyan); }
71.42% { color: var(--bs-purple); }
85.71% { color: var(--bs-pink); }
100% { color: var(--bs-red); }
}
@keyframes shine {
0% { left: -100%; }
50% { left: 100%; }
100% { left: 100%; }
}
@keyframes slayFloat {
0% {
transform: translateY(0) scale(0.5);
opacity: 0;
}
20% {
transform: translateY(-20px) scale(1);
opacity: 1;
}
80% {
transform: translateY(-60px) scale(1.2);
opacity: 0.8;
}
100% {
transform: translateY(-100px) scale(1.5);
opacity: 0;
}
}
/* 🎭 BONKERS MODE CLASSES 🎭 */
.bonkers-mode {
transition: all 0.3s ease-in-out;
}
.bonkers-mode .btn {
animation: discoFlash 0.3s infinite, wiggle 0.2s infinite;
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red));
background-size: 400% 400%;
animation: discoFlash 0.3s infinite, wiggle 0.2s infinite, rainbowGradient 1s ease infinite;
border: 4px solid var(--bs-white);
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
position: relative;
overflow: hidden;
transition: all 0.2s ease;
}
.bonkers-mode .btn:hover {
animation: discoFlash 0.2s infinite, wiggle 0.1s infinite, rainbowGradient 0.5s ease infinite;
box-shadow: 0 0 30px var(--bs-pink), 0 0 60px var(--bs-purple);
}
.bonkers-mode .btn::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.5), transparent);
transform: rotate(45deg);
animation: spin 0.5s linear infinite;
}
.bonkers-mode .navbar {
background: linear-gradient(90deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red));
background-size: 200% 200%;
animation: rainbowGradient 2s ease infinite;
box-shadow: 0 0 50px rgba(255, 105, 180, 0.9);
height: auto !important;
min-height: 56px;
}
.bonkers-mode .navbar-brand {
animation: rainbowText 0.8s infinite, wiggle 0.4s infinite;
font-size: 1.8em;
text-shadow: 3px 3px 6px rgba(0,0,0,0.5);
position: relative;
overflow: hidden;
}
.bonkers-mode .navbar-brand::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.3), transparent);
transform: rotate(45deg);
animation: spin 2s linear infinite;
}
.bonkers-mode .navbar-nav .nav-link {
animation: rainbowText 1.2s infinite, wiggle 0.3s infinite;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
border: 2px solid transparent;
border-radius: 8px;
padding: 8px 16px;
margin: 0 4px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.bonkers-mode .navbar-nav .nav-link::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shine 1.5s ease-in-out infinite;
}
.bonkers-mode .navbar-nav .nav-link:hover {
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple));
border-color: var(--bs-white);
box-shadow: 0 0 20px var(--bs-pink);
animation: discoFlash 0.5s infinite, wiggle 0.2s infinite;
}
.bonkers-mode .navbar-nav .nav-link.active {
background: linear-gradient(45deg, var(--bs-yellow), var(--bs-orange));
border-color: var(--bs-white);
box-shadow: 0 0 25px var(--bs-yellow);
animation: discoFlash 0.8s infinite, wiggle 0.3s infinite;
}
.bonkers-mode .navbar-text {
animation: rainbowText 1.5s infinite, wiggle 0.5s infinite;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
border: 2px solid var(--bs-white);
border-radius: 8px;
padding: 6px 12px;
background: linear-gradient(45deg, var(--bs-cyan), var(--bs-blue));
box-shadow: 0 0 15px var(--bs-cyan);
}
.bonkers-mode .navbar-toggler {
border: 3px solid var(--bs-white);
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple));
animation: discoFlash 0.6s infinite, wiggle 0.4s infinite;
box-shadow: 0 0 20px var(--bs-pink);
}
.bonkers-mode .navbar-toggler:focus {
box-shadow: 0 0 30px var(--bs-pink), 0 0 0 0.2rem rgba(255, 105, 180, 0.5);
}
.bonkers-mode .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
animation: spin 1s linear infinite;
}
.bonkers-mode .dropdown-menu {
background: linear-gradient(135deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan));
border: 3px solid var(--bs-white);
box-shadow: 0 0 30px rgba(255,105,180,0.8);
animation: rainbowGradient 2s ease infinite;
}
.bonkers-mode .dropdown-item {
animation: rainbowText 1.8s infinite, wiggle 0.6s infinite;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
border-bottom: 1px solid rgba(255,255,255,0.3);
transition: all 0.3s ease;
}
.bonkers-mode .dropdown-item:hover {
background: linear-gradient(45deg, var(--bs-yellow), var(--bs-orange));
color: var(--bs-white);
box-shadow: 0 0 15px var(--bs-yellow);
animation: discoFlash 0.5s infinite, wiggle 0.3s infinite;
}
.bonkers-mode .navbar-collapse {
background: linear-gradient(135deg, rgba(255,105,180,0.1), rgba(138,43,226,0.1));
border-radius: 8px;
margin-top: 8px;
padding: 8px;
border: 2px solid var(--bs-pink);
}
.bonkers-mode h1, .bonkers-mode h2, .bonkers-mode h3 {
animation: rainbowText 1.5s infinite;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.bonkers-mode .table {
background: linear-gradient(135deg, rgba(255,105,180,0.2), rgba(138,43,226,0.2), rgba(0,255,255,0.2));
animation: rainbowGradient 3s ease infinite;
border: 3px solid var(--bs-pink);
box-shadow: 0 0 30px rgba(255,105,180,0.5);
}
.bonkers-mode .table th {
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple));
color: var(--bs-white);
animation: discoFlash 0.8s infinite;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
font-size: 1.1em;
}
.bonkers-mode .form-control {
border: 3px solid var(--bs-pink);
box-shadow: 0 0 15px var(--bs-pink);
animation: pulse 0.6s infinite;
}
.bonkers-mode .alert {
animation: discoFlash 0.6s infinite, wiggle 0.3s infinite;
border: 4px solid var(--bs-white);
font-weight: bold;
font-size: 1.1em;
}
.bonkers-mode .card {
background: linear-gradient(45deg, rgba(255,105,180,0.2), rgba(138,43,226,0.2));
border: 3px solid var(--bs-purple);
box-shadow: 0 0 35px rgba(138,43,226,0.6);
animation: pulse 1s infinite;
}
.bonkers-mode .modal-content {
background: linear-gradient(135deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan));
border: 4px solid var(--bs-white);
box-shadow: 0 0 50px rgba(255,105,180,0.8);
animation: rainbowGradient 2s ease infinite;
}
.bonkers-mode .modal-header {
background: linear-gradient(90deg, var(--bs-yellow), var(--bs-orange));
animation: discoFlash 0.8s infinite;
font-size: 1.2em;
}
.bonkers-mode .number-input-wrapper {
animation: wiggle 0.4s infinite;
}
.bonkers-mode .number-input-wrapper .btn {
animation: discoFlash 0.3s infinite, wiggle 0.2s infinite;
}
/* Enhanced mode styles (for future use) */
[data-website-mode="enhanced"] .btn {
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red));
background-size: 400% 400%;
animation: rainbowGradient 1s ease infinite;
border: 4px solid var(--bs-white);
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
position: relative;
overflow: hidden;
transition: all 0.2s ease;
}
[data-website-mode="enhanced"] .btn:hover {
animation: rainbowGradient 0.5s ease infinite;
box-shadow: 0 0 30px var(--bs-pink), 0 0 60px var(--bs-purple);
}
[data-website-mode="enhanced"] .btn::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.5), transparent);
transform: rotate(45deg);
animation: spin 0.5s linear infinite;
}
[data-website-mode="enhanced"] .navbar {
background: linear-gradient(90deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red));
background-size: 200% 200%;
animation: rainbowGradient 2s ease infinite;
box-shadow: 0 0 50px rgba(255, 105, 180, 0.9);
height: auto !important;
min-height: 56px;
}
[data-website-mode="enhanced"] .navbar-brand {
animation: rainbowText 0.8s infinite;
font-size: 1.8em;
text-shadow: 3px 3px 6px rgba(0,0,0,0.5);
position: relative;
overflow: hidden;
}
[data-website-mode="enhanced"] .navbar-brand::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.3), transparent);
transform: rotate(45deg);
animation: spin 2s linear infinite;
}
[data-website-mode="enhanced"] .navbar-nav .nav-link {
animation: rainbowText 1.2s infinite;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
border: 2px solid transparent;
border-radius: 8px;
padding: 8px 16px;
margin: 0 4px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
[data-website-mode="enhanced"] .navbar-nav .nav-link::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shine 1.5s ease-in-out infinite;
}
[data-website-mode="enhanced"] .navbar-nav .nav-link:hover {
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple));
border-color: var(--bs-white);
box-shadow: 0 0 20px var(--bs-pink);
}
[data-website-mode="enhanced"] .navbar-nav .nav-link.active {
background: linear-gradient(45deg, var(--bs-yellow), var(--bs-orange));
border-color: var(--bs-white);
box-shadow: 0 0 25px var(--bs-yellow);
}
[data-website-mode="enhanced"] .navbar-text {
animation: rainbowText 1.5s infinite;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
border: 2px solid var(--bs-white);
border-radius: 8px;
padding: 6px 12px;
background: linear-gradient(45deg, var(--bs-cyan), var(--bs-blue));
box-shadow: 0 0 15px var(--bs-cyan);
}
[data-website-mode="enhanced"] .navbar-toggler {
border: 3px solid var(--bs-white);
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple));
animation: rainbowGradient 0.6s ease infinite;
box-shadow: 0 0 20px var(--bs-pink);
}
[data-website-mode="enhanced"] .navbar-toggler:focus {
box-shadow: 0 0 30px var(--bs-pink), 0 0 0 0.2rem rgba(255, 105, 180, 0.5);
}
[data-website-mode="enhanced"] .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
animation: spin 1s linear infinite;
}
[data-website-mode="enhanced"] .dropdown-menu {
background: linear-gradient(135deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan));
border: 3px solid var(--bs-white);
box-shadow: 0 0 30px rgba(255,105,180,0.8);
animation: rainbowGradient 2s ease infinite;
}
[data-website-mode="enhanced"] .dropdown-item {
animation: rainbowText 1.8s infinite;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
border-bottom: 1px solid rgba(255,255,255,0.3);
transition: all 0.3s ease;
}
[data-website-mode="enhanced"] .dropdown-item:hover {
background: linear-gradient(45deg, var(--bs-yellow), var(--bs-orange));
color: var(--bs-white);
box-shadow: 0 0 15px var(--bs-yellow);
}
[data-website-mode="enhanced"] .navbar-collapse {
background: linear-gradient(135deg, rgba(255,105,180,0.1), rgba(138,43,226,0.1));
border-radius: 8px;
margin-top: 8px;
padding: 8px;
border: 2px solid var(--bs-pink);
}
[data-website-mode="enhanced"] h1, [data-website-mode="enhanced"] h2, [data-website-mode="enhanced"] h3 {
animation: rainbowText 1.5s infinite;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
[data-website-mode="enhanced"] .table {
background: linear-gradient(135deg, rgba(255,105,180,0.2), rgba(138,43,226,0.2), rgba(0,255,255,0.2));
animation: rainbowGradient 3s ease infinite;
border: 3px solid var(--bs-pink);
box-shadow: 0 0 30px rgba(255,105,180,0.5);
}
[data-website-mode="enhanced"] .table th {
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple));
color: var(--bs-white);
animation: rainbowGradient 0.8s ease infinite;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
font-size: 1.1em;
}
[data-website-mode="enhanced"] .form-control {
border: 3px solid var(--bs-pink);
box-shadow: 0 0 15px var(--bs-pink);
}
[data-website-mode="enhanced"] .alert {
animation: rainbowGradient 0.6s ease infinite;
border: 4px solid var(--bs-white);
font-weight: bold;
font-size: 1.1em;
}
[data-website-mode="enhanced"] .card {
background: linear-gradient(45deg, rgba(255,105,180,0.2), rgba(138,43,226,0.2));
border: 3px solid var(--bs-purple);
box-shadow: 0 0 35px rgba(138,43,226,0.6);
}
[data-website-mode="enhanced"] .modal-content {
background: linear-gradient(135deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan));
border: 4px solid var(--bs-white);
box-shadow: 0 0 50px rgba(255,105,180,0.8);
animation: rainbowGradient 2s ease infinite;
}
[data-website-mode="enhanced"] .modal-header {
background: linear-gradient(90deg, var(--bs-yellow), var(--bs-orange));
animation: rainbowGradient 0.8s ease infinite;
font-size: 1.2em;
}
[data-website-mode="enhanced"] .number-input-wrapper {
}
[data-website-mode="enhanced"] .number-input-wrapper .btn {
animation: rainbowGradient 0.3s ease infinite;
}
/* Emoji Footprint Animation */
.emoji-footprint {
position: absolute;
font-size: 1.6rem;
pointer-events: none;
animation: emojiFade 1s ease-out forwards;
transform: translate(-50%, -50%) scale(1);
opacity: 1;
z-index: 9999;
text-shadow:
0 0 4px #ff00bf,
0 0 8px #ff80df,
0 0 12px #ffccff;
}
@keyframes emojiFade {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
50% {
transform: translate(-50%, -50%) scale(1.5);
opacity: 0.7;
}
100% {
transform: translate(-50%, -50%) scale(2);
opacity: 0;
}
}
/* 💅 SLAY TEXT EFFECTS 💅 */
.slay-text {
position: fixed;
font-size: 2rem;
font-weight: bold;
pointer-events: none;
z-index: 10000;
animation: slayFloat 3s ease-out forwards;
text-shadow:
0 0 10px #ff00bf,
0 0 20px #ff80df,
0 0 30px #ffccff,
2px 2px 4px rgba(0,0,0,0.5);
background: linear-gradient(45deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow));
background-size: 400% 400%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: slayFloat 3s ease-out forwards, rainbowGradient 1s ease infinite;
}

View file

@ -18,6 +18,7 @@
"phpstan/phpdoc-parser": "^1.33", "phpstan/phpdoc-parser": "^1.33",
"psr/clock": "^1.0", "psr/clock": "^1.0",
"symfony/asset": "7.3.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*", "symfony/console": "7.3.*",
"symfony/dotenv": "7.3.*", "symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.3.*", "symfony/expression-language": "7.3.*",
@ -33,7 +34,9 @@
"symfony/twig-bundle": "7.3.*", "symfony/twig-bundle": "7.3.*",
"symfony/uid": "7.3.*", "symfony/uid": "7.3.*",
"symfony/validator": "7.3.*", "symfony/validator": "7.3.*",
"symfony/yaml": "7.3.*" "symfony/yaml": "7.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
}, },
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.1", "doctrine/doctrine-fixtures-bundle": "^4.1",
@ -86,7 +89,8 @@
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
"cache:clear": "symfony-cmd", "cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd" "assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
}, },
"post-install-cmd": [ "post-install-cmd": [
"@auto-scripts" "@auto-scripts"

583
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c21233664fb5c4d25afb8a8cdc29213a", "content-hash": "f7f3513731749141a755b5e676088c7d",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
@ -222,6 +222,87 @@
}, },
"time": "2025-06-13T13:00:13+00:00" "time": "2025-06-13T13:00:13+00:00"
}, },
{
"name": "composer/semver",
"version": "3.4.3",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.3"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-09-19T14:15:21+00:00"
},
{ {
"name": "doctrine/collections", "name": "doctrine/collections",
"version": "2.3.0", "version": "2.3.0",
@ -2000,6 +2081,86 @@
], ],
"time": "2025-03-05T10:15:41+00:00" "time": "2025-03-05T10:15:41+00:00"
}, },
{
"name": "symfony/asset-mapper",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset-mapper.git",
"reference": "6516f38868b75c4902ea72a9fa44967628375ae7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset-mapper/zipball/6516f38868b75c4902ea72a9fa44967628375ae7",
"reference": "6516f38868b75c4902ea72a9fa44967628375ae7",
"shasum": ""
},
"require": {
"composer/semver": "^3.0",
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/filesystem": "^7.1",
"symfony/http-client": "^6.4|^7.0"
},
"conflict": {
"symfony/framework-bundle": "<6.4"
},
"require-dev": {
"symfony/asset": "^6.4|^7.0",
"symfony/browser-kit": "^6.4|^7.0",
"symfony/console": "^6.4|^7.0",
"symfony/event-dispatcher-contracts": "^3.0",
"symfony/finder": "^6.4|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/web-link": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\AssetMapper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Maps directories of assets & makes them available in a public directory with versioned filenames.",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/asset-mapper/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-05-24T14:05:12+00:00"
},
{ {
"name": "symfony/cache", "name": "symfony/cache",
"version": "v7.3.0", "version": "v7.3.0",
@ -3493,6 +3654,179 @@
], ],
"time": "2025-05-28T06:56:42+00:00" "time": "2025-05-28T06:56:42+00:00"
}, },
{
"name": "symfony/http-client",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"amphp/amp": "<2.5",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/amphp-http-client-meta": "^1.0|^2.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/rate-limiter": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-05-02T08:23:16+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-29T11:18:49+00:00"
},
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v7.3.0", "version": "v7.3.0",
@ -6071,6 +6405,80 @@
], ],
"time": "2025-04-04T10:10:33+00:00" "time": "2025-04-04T10:10:33+00:00"
}, },
{
"name": "twig/extra-bundle",
"version": "v3.21.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
"reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/62d1cf47a1aa009cbd07b21045b97d3d5cb79896",
"reference": "62d1cf47a1aa009cbd07b21045b97d3d5cb79896",
"shasum": ""
},
"require": {
"php": ">=8.1.0",
"symfony/framework-bundle": "^5.4|^6.4|^7.0",
"symfony/twig-bundle": "^5.4|^6.4|^7.0",
"twig/twig": "^3.2|^4.0"
},
"require-dev": {
"league/commonmark": "^1.0|^2.0",
"symfony/phpunit-bridge": "^6.4|^7.0",
"twig/cache-extra": "^3.0",
"twig/cssinliner-extra": "^3.0",
"twig/html-extra": "^3.0",
"twig/inky-extra": "^3.0",
"twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.0",
"twig/string-extra": "^3.0"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Twig\\Extra\\TwigExtraBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
}
],
"description": "A Symfony bundle for extra Twig extensions",
"homepage": "https://twig.symfony.com",
"keywords": [
"bundle",
"extra",
"twig"
],
"support": {
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.21.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2025-02-19T14:29:33+00:00"
},
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v3.21.1", "version": "v3.21.1",
@ -9658,179 +10066,6 @@
], ],
"time": "2025-03-05T10:15:41+00:00" "time": "2025-03-05T10:15:41+00:00"
}, },
{
"name": "symfony/http-client",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"amphp/amp": "<2.5",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/amphp-http-client-meta": "^1.0|^2.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/rate-limiter": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-05-02T08:23:16+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-29T11:18:49+00:00"
},
{ {
"name": "symfony/maker-bundle", "name": "symfony/maker-bundle",
"version": "v1.63.0", "version": "v1.63.0",

View file

@ -11,6 +11,7 @@ use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
return [ return [
FrameworkBundle::class => [ FrameworkBundle::class => [
@ -49,4 +50,7 @@ return [
ApiPlatformBundle::class => [ ApiPlatformBundle::class => [
'all' => true, 'all' => true,
], ],
TwigExtraBundle::class => [
'all' => true,
],
]; ];

View file

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('framework', [
'asset_mapper' => [
'paths' => [
'assets/',
],
'missing_import_mode' => 'strict',
],
]);
if ($containerConfigurator->env() === 'prod') {
$containerConfigurator->extension('framework', [
'asset_mapper' => [
'missing_import_mode' => 'warn',
],
]);
}
};

View file

@ -1,17 +1,16 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Config\TwigConfig;
return static function (ContainerConfigurator $containerConfigurator): void { return static function (
$containerConfigurator->extension('twig', [ ContainerConfigurator $containerConfigurator,
'file_name_pattern' => '*.twig', TwigConfig $twig,
'globals' => [ ): void {
'favicon' => '@App\Service\Favicon',
],
]);
if ($containerConfigurator->env() === 'test') { if ($containerConfigurator->env() === 'test') {
$containerConfigurator->extension('twig', [ $twig->strictVariables(true);
'strict_variables' => true,
]);
} }
$twig->formThemes(['bootstrap_5_layout.html.twig']);
$twig->fileNamePattern('*.twig');
$twig->global('favicon', '@App\Service\Favicon');
}; };

32
importmap.php Normal file
View file

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'bootstrap' => [
'version' => '5.3.7',
],
'@popperjs/core' => [
'version' => '2.11.8',
],
'bootstrap/dist/css/bootstrap.min.css' => [
'version' => '5.3.7',
'type' => 'css',
],
'htmx.org' => [
'version' => '1.9.12',
],
];

View file

@ -1,965 +0,0 @@
/* SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: Copyright (c) 2022-2025 zichy
*/
/* Custom properties
========================================
*/
:root {
--f-sans: ui-sans-serif, sans-serif;
--f-body: ui-serif;
--f-heading: var(--f-sans);
--f-form: var(--f-sans);
--f-code: ui-monospace;
--f-size: clamp(1.6rem, 1.75vw, 2rem);
--f-size-small: 0.85em;
--f-size-large: 1.25em;
--f-line: 1.5;
--c-gray: #666;
--c-red: #b30;
--c-yellow: #fe9;
--i-triangle: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"%3E%3Cpolygon fill="black" points="5 10 10 0 0 0"/%3E%3C/svg%3E');
--w-body: 80ch;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--c-gray: #999;
--c-red: #f99;
--c-yellow: #ff9;
--i-triangle: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"%3E%3Cpolygon fill="white" points="5 10 10 0 0 0"/%3E%3C/svg%3E');
}
}
/* Globals
========================================
*/
/* Box sizing */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Text rendering */
* {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/* Interaction */
::selection {
background: Highlight;
color: HighlightText;
text-shadow: none;
}
*:focus {
outline: 2px solid LinkText;
outline-offset: 0.25rem;
}
/* Font size & Scrolling */
html {
font-size: 62.5%;
scroll-behavior: smooth;
scroll-padding-top: 2rem;
}
/* Backdrop */
::backdrop {
background-color: rgba(255, 255, 255, 0.6);
}
@media (prefers-color-scheme: dark) {
::backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
}
/* Hidden elements */
[hidden] {
display: none;
}
/* Print spacing */
@page {
margin: 15mm 20mm;
}
/* Body
========================================
*/
/* Colors & Typography */
body {
background-color: Canvas;
color: CanvasText;
font-size: var(--f-size);
font-family: var(--f-body);
line-height: var(--f-line);
}
/* Body sizing */
@media screen {
body {
max-width: var(--w-body, 100%);
min-width: 320px;
padding: 2rem;
margin: 0 auto;
overflow-x: hidden;
overflow-y: scroll;
}
}
/* Print colors */
@media print {
body {
background-color: white;
color: black;
}
}
/* Links
========================================
*/
a:any-link {
color: LinkText;
text-decoration: underline;
text-decoration-thickness: 0.125em;
}
a:any-link:hover {
background-color: LinkText;
color: Canvas;
text-decoration-line: none;
}
@media print {
a[href^="http"]::after {
content: ' ('attr(href)')';
font-size: var(--f-size-small);
word-break: break-all;
}
}
/* Media
========================================
*/
/* Reset */
:where(iframe, img, svg, canvas, audio, video) {
display: block;
max-width: 100%;
}
@media print {
:where(audio, video) {
display: none;
}
}
figure {
margin-inline: 0;
break-inside: avoid;
}
/* Image */
img {
height: auto;
position: relative;
}
img::before {
content: '';
background-color: Highlight;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
@media screen {
picture img {
width: 100%;
}
}
/* Video */
video {
width: 100%;
height: auto;
}
/* Iframe */
iframe {
border-style: none;
}
/* Headings
========================================
*/
:where(h1, h2, h3, h4, h5, h6) {
font-family: var(--f-heading);
line-height: calc(var(--f-line) / 1.25);
hyphens: auto;
}
:where(h3, h5) {
color: var(--c-gray);
}
:where(h4, h5, h6) {
text-transform: uppercase;
}
:where(h2, h3, h4, h5, h6):target {
background-color: var(--c-yellow);
color: MarkText;
}
/* Lists
========================================
*/
:where(ul, ol) {
padding-inline-start: 1em;
}
ul {
list-style-type: disc;
}
li::marker {
color: var(--c-gray);
}
li p {
margin: 0;
}
/* Description */
dt {
font-style: italic;
}
/* Navigation */
nav ul {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 2rem;
list-style-type: none;
padding: 0;
}
@media print {
nav {
display: none;
}
}
/* Inline elements
========================================
*/
/* Bold text */
:where(b, strong) {
font-weight: bolder;
}
/* Small text */
small {
font-size: var(--f-size-small);
}
/* Mark */
mark {
background-color: var(--c-yellow);
}
/* Abbreviation */
abbr[title] {
text-decoration-line: underline;
text-decoration-style: dotted;
cursor: help;
}
a abbr[title] {
text-decoration: none;
}
/* Subscript & Superscript */
:where(sub, sup) {
line-height: 0;
}
/* Quote */
q {
font-style: italic;
quotes: none;
}
/* Keyboard input */
kbd {
background: linear-gradient(0deg, Canvas 0%, ButtonFace 100%);
font-size: var(--f-size-small);
font-family: var(--f-sans);
font-weight: bold;
padding: 0.2em 0.4em;
border-radius: 0.5rem;
box-shadow: 1px 1px 1px 0px var(--c-gray);
}
/* Ruby annotation
========================================
*/
rt {
color: var(--c-gray);
font-family: var(--f-sans);
letter-spacing: -0.05em;
padding: 0 0.25em;
}
/* Horizontal rule
========================================
*/
hr {
height: 0;
margin: 2em 0;
border: 0;
border-top: 2px solid var(--c-gray);
}
/* Blockquote
========================================
*/
blockquote {
font-size: var(--f-size-large);
font-style: italic;
line-height: calc(var(--f-line) / 1.25);
margin: 0;
}
blockquote > *:first-child {
margin-block-start: 0;
}
blockquote > *:last-child {
margin-block-end: 0;
}
/* Captions
========================================
*/
:where(caption, figcaption) {
color: var(--c-gray);
font-family: var(--f-heading);
font-size: var(--f-size-small);
font-style: italic;
margin-block-start: 0.5rem;
}
caption {
text-align: left;
caption-side: bottom;
}
[dir='rtl' i] caption {
text-align: right;
}
/* Code
========================================
*/
:where(pre, code, samp, var) {
background-color: ButtonFace;
}
:where(code, samp, var) {
font-size: var(--f-size-small);
font-family: var(--f-code);
padding: 0.2em 0.4em;
}
pre {
font-size: var(--f-size-small);
padding: 2rem;
}
@media screen {
pre {
overflow-x: scroll;
}
}
pre code {
background-color: transparent;
display: block;
white-space: pre-wrap;
padding: 0;
}
/* Details
========================================
*/
details {
background-color: ButtonFace;
padding: 2rem;
margin: 1em 0;
border-radius: 0.5rem;
}
details > *:nth-child(2) {
margin-block-start: 0;
}
details > *:last-child {
margin-block-end: 0;
}
summary {
color: LinkText;
font-family: var(--f-heading);
font-weight: bold;
cursor: pointer;
}
summary:hover {
text-decoration: underline;
}
details[open] summary {
margin-block-end: 2rem;
}
/* Aside
========================================
*/
aside {
color: var(--c-gray);
}
@media (min-width: 769px) {
aside {
font-size: var(--f-size-small);
float: right;
width: calc(var(--w-body) / 2.5);
padding-block-end: 2rem;
padding-inline-start: 4rem;
}
aside > *:first-child {
margin-block-start: 0;
}
aside > *:last-child {
margin-block-end: 0;
}
}
/* Table
========================================
*/
table {
width: 100%;
margin: 1em 0;
border-collapse: collapse;
border-spacing: 0;
break-inside: avoid;
}
@media screen and (max-width: 768px) {
table {
display: block;
overflow-x: auto;
overflow-y: hidden;
}
}
thead {
border-bottom: 2px solid var(--c-gray);
}
tbody tr:nth-child(odd) {
background-color: ButtonFace;
}
tfoot {
border-top: 2px solid var(--c-gray);
}
:where(th, td) {
padding: 0.5rem 1rem;
}
@media (max-width: 768px) {
:where(th, td) {
min-width: 10rem;
}
}
th {
font-family: var(--f-heading);
text-align: left;
vertical-align: bottom;
}
[dir='rtl' i] th {
text-align: right;
}
/* Forms & Inputs
========================================
*/
/* Reset */
:where(input, textarea, select, button, progress) {
-webkit-appearance: none;
background-color: transparent;
break-inside: avoid;
}
:where(input, textarea, select, button) {
font-family: var(--f-form);
font-size: 1em;
border-radius: 0.5rem;
}
:where(input:not([type='button' i]):not([type='submit' i]):not([type='reset' i]):not([type='checkbox' i]):not([type='radio' i]):not([type='image' i]), textarea, select) {
color: CanvasText;
font-size: var(--f-size-small);
display: block;
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid LinkText;
}
/* Placeholder */
::placeholder {
color: var(--c-gray);
}
/* Fieldset */
fieldset {
padding: 2rem;
border: 2px solid LinkText;
border-radius: 0.5rem;
break-inside: avoid;
}
/* Label & Legend */
:where(legend, label) {
font-family: var(--f-form);
font-weight: bold;
display: block;
}
legend {
padding: 0 1rem;
}
:where(legend, label) small {
color: var(--c-gray);
font-weight: normal;
}
/* Textarea */
textarea {
resize: vertical;
}
/* Checkbox & Radio input */
label:has([type='checkbox' i], [type='radio' i]) {
font-family: var(--f-form);
font-size: var(--f-size-small);
font-weight: normal;
display: grid;
grid-template-columns: 1.25em 1fr;
column-gap: 0.5em;
padding-block-end: 0;
}
label:has([type='checkbox' i][disabled], [type='radio' i][disabled]) {
color: var(--c-gray);
}
:where([type='checkbox' i], [type='radio' i]) {
width: 1.25em;
height: 1.25em;
position: relative;
margin: 0.2rem 0 0;
border: 2px solid LinkText;
cursor: pointer;
}
[type='radio' i] {
border-radius: 50%;
}
:where([type='checkbox' i], [type='radio' i]):checked {
background-color: LinkText;
}
[type='checkbox' i]:checked::after {
content: '\2713';
color: Canvas;
font-family: var(--f-form);
font-weight: bold;
line-height: 1;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Color input */
[type='color' i] {
height: 4rem;
padding: 0.5rem;
cursor: pointer;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-color-swatch {
border: 0;
}
::-moz-color-swatch {
border: 0;
}
/* Range input */
[type='range' i] {
margin: 1.25rem 0 0;
padding: 0;
border: 0;
}
[type='range' i]:focus {
outline: none;
}
::-webkit-slider-runnable-track {
background-color: LinkText;
height: 4px;
border-radius: 0.5rem;
}
[disabled]::-webkit-slider-runnable-track {
background-color: var(--c-gray);
}
::-moz-range-track {
background-color: LinkText;
height: 4px;
border-radius: 0.5rem;
}
[disabled]::-moz-range-track {
background-color: var(--c-gray);
}
::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
background-color: Canvas;
height: 2rem;
width: 2rem;
margin-block-start: calc(-1rem + 2px);
border: 2px solid LinkText;
border-radius: 50%;
cursor: ew-resize;
}
[disabled]::-webkit-slider-thumb {
border-color: var(--c-gray);
}
[type='range' i]:focus::-webkit-slider-thumb {
outline: 2px solid LinkText;
outline-offset: 0.25rem;
}
::-moz-range-thumb {
appearance: none;
background-color: Canvas;
height: 2rem;
width: 2rem;
margin-block-start: calc(-1rem + 2px);
border: 2px solid LinkText;
border-radius: 50%;
cursor: ew-resize;
}
[disabled]::-moz-range-thumb {
border-color: var(--c-gray);
}
[type='range' i]:focus::-moz-range-thumb {
outline: 2px solid LinkText;
outline-offset: 0.25rem;
}
/* Select */
select {
background: Canvas var(--i-triangle) no-repeat calc(100% - 1rem) center / 1.5rem;
text-overflow: ellipsis;
white-space: nowrap;
padding-inline-end: 3.5rem;
overflow: hidden;
cursor: pointer;
}
[dir='rtl' i] select {
background-position: 1rem center;
padding-inline: 3.5rem 1rem;
}
select[multiple] {
background-image: none;
padding-inline-end: 1rem;
}
/* Buttons */
:where(button, [type='button' i], [type='submit' i], [type='reset' i]) {
font-size: var(--f-size-small);
font-weight: bold;
text-align: center;
text-decoration: none;
line-height: 1;
display: inline-block;
min-width: 5rem;
padding: 0.2em 0.4em;
border: 2px solid LinkText;
-webkit-user-select: text;
user-select: text;
cursor: pointer;
touch-action: manipulation;
}
:where(button:not([disabled]), [type='button' i]:not([disabled]), [type='submit' i]:not([disabled]), [type='reset' i]:not([disabled])):hover {
text-decoration: underline;
}
@media screen {
:where(button, [type='button' i], [type='submit' i], [type='reset' i]) {
background-color: LinkText;
color: Canvas;
}
:where(button[disabled], [type='button' i][disabled], [type='submit' i][disabled], [type='reset' i][disabled]) {
background-color: var(--c-gray);
color: currentColor;
}
}
form :where(button, [type='button' i], [type='submit' i], [type='reset' i]) {
padding: 1rem 1.5rem;
}
/* Meter & Progress */
:where(meter, progress) {
width: 100%;
height: 3rem;
border: 2px solid var(--c-gray);
}
label + :where(meter, progress) {
margin-block-start: 0.5rem;
}
meter {
background: transparent;
display: block;
margin-block-end: 1em;
border: 2px solid var(--c-gray);
}
::-webkit-meter-bar {
background: Canvas;
height: 3rem;
border: 2px solid var(--c-gray);
border-radius: 0;
}
::-webkit-progress-bar {
background-color: Canvas;
}
::-moz-progress-bar {
background-color: var(--c-gray);
}
::-webkit-progress-value {
background-color: var(--c-gray);
}
/* Disabled state */
[disabled] {
border-color: var(--c-gray);
cursor: not-allowed;
}
/* Error state */
[aria-invalid] {
border-color: var(--c-red) !important;
}
[aria-invalid]:focus {
outline-color: var(--c-red);
}
[aria-invalid] + p[id] {
color: var(--c-red);
}
/* Form spacing */
form label:not(:first-of-type) {
margin-block-start: 3rem;
}
form label + :where(input, textarea, select) {
margin-block-start: 0.5rem;
}
form fieldset {
margin: 3rem 0;
}
fieldset label:not(:first-of-type) {
margin-block-start: 2rem;
}
form p[id] {
margin-block-start: 0.5rem;
}
/* Dialog
========================================
*/
dialog[open] {
background-color: Canvas;
color: currentColor;
display: block;
max-width: var(--w-body, 100%);
min-width: calc(var(--w-body) / 2);
padding: 2rem;
border: 2px solid var(--c-gray);
border-radius: 0.5rem;
}
body:has(dialog[open]) {
overflow: hidden;
}
dialog:not([open]) {
display: none;
}
dialog > *:first-child {
margin-block-start: 0;
}
dialog > *:last-child {
margin-block-end: 0;
}
/* Opinionated layout
========================================
*/
@media screen {
body > header {
margin-block-end: 4em;
}
main > :where(section, article),
body > footer {
margin-block-start: 4em;
clear: both;
}
body > footer {
margin-block-start: 4em;
}
}

View file

@ -1 +0,0 @@
blockquote,header{background:var(--nc-bg-2)}dt,summary,table caption{font-weight:700}img,pre,textarea{max-width:100%}:root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000000;--nc-tx-2:#1A1A1A;--nc-bg-1:#FFFFFF;--nc-bg-2:#F6F8FA;--nc-bg-3:#E5E7EB;--nc-lk-1:#0070F3;--nc-lk-2:#0366D6;--nc-lk-tx:#FFFFFF;--nc-ac-1:#79FFE1;--nc-ac-tx:#0C4047;--nc-d-tx-1:#ffffff;--nc-d-tx-2:#eeeeee;--nc-d-bg-1:#000000;--nc-d-bg-2:#111111;--nc-d-bg-3:#222222;--nc-d-lk-1:#3291FF;--nc-d-lk-2:#0070F3;--nc-d-lk-tx:#FFFFFF;--nc-d-ac-1:#7928CA;--nc-d-ac-tx:#FFFFFF}@media (prefers-color-scheme:dark){:root{--nc-tx-1:var(--nc-d-tx-1);--nc-tx-2:var(--nc-d-tx-2);--nc-bg-1:var(--nc-d-bg-1);--nc-bg-2:var(--nc-d-bg-2);--nc-bg-3:var(--nc-d-bg-3);--nc-lk-1:var(--nc-d-lk-1);--nc-lk-2:var(--nc-d-lk-2);--nc-lk-tx:var(--nc--dlk-tx);--nc-ac-1:var(--nc-d-ac-1);--nc-ac-tx:var(--nc--dac-tx)}}*{margin:0;padding:0}address,area,article,aside,audio,blockquote,datalist,details,dl,fieldset,figure,form,iframe,img,input,meter,nav,ol,optgroup,option,output,p,pre,progress,ruby,section,table,textarea,ul,video{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:break-word;overflow-wrap:break-word;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2)}abbr,abbr:hover{cursor:help}blockquote{padding:1.5rem;border-left:5px solid var(--nc-bg-3)}blockquote :last-child{padding-bottom:0;margin-bottom:0}header{border-bottom:1px solid var(--nc-bg-3);padding:2rem 1.5rem;margin:-2rem calc(50% - 50vw) 2rem;padding-left:calc(50vw - 50%);padding-right:calc(50vw - 50%)}header h1,header h2,header h3{padding-bottom:0;border-bottom:0}header>:first-child{margin-top:0;padding-top:0}a img,details[open]>:last-child,header>:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{opacity:.5;cursor:not-allowed}.button:enabled:hover,.button:focus,button:enabled:hover,button:focus,input[type=button]:enabled:hover,input[type=button]:focus,input[type=reset]:enabled:hover,input[type=reset]:focus,input[type=submit]:enabled:hover,input[type=submit]:focus{background:var(--nc-lk-2)}code,details,input,kbd,pre,samp,select,textarea,th,tr:nth-child(2n){background:var(--nc-bg-2)}code,kbd,pre,samp{font-family:var(--nc-font-mono);border:1px solid var(--nc-bg-3);border-radius:4px;padding:3px 6px;font-size:.9em}code pre,pre code{background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}details,fieldset{border:1px solid var(--nc-bg-3)}kbd{border-bottom:3px solid var(--nc-bg-3)}pre{padding:1rem 1.4rem;overflow:auto}code pre{display:inline}details{padding:.6rem 1rem;border-radius:4px}summary{cursor:pointer}details[open]{padding-bottom:.75rem}details[open] summary{margin-bottom:6px}dd::before{content:'→ '}hr{border:0;border-bottom:1px solid var(--nc-bg-3);margin:1rem auto}fieldset{margin-top:1rem;padding:2rem;border-radius:4px}input,select,td,textarea,th{border:1px solid var(--nc-bg-3)}legend{padding:auto .5rem}table{border-collapse:collapse;width:100%}td,th{text-align:left;padding:.5rem}table caption{margin-bottom:.5rem}ol,ul{padding-left:2rem}li{margin-top:.4rem}mark{padding:3px 6px;background:var(--nc-ac-1);color:var(--nc-ac-tx)}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;color:var(--nc-tx-2);border-radius:4px;box-shadow:none;box-sizing:border-box}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -115,6 +115,21 @@
"phpcs.xml.dist" "phpcs.xml.dist"
] ]
}, },
"symfony/asset-mapper": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/asset_mapper.yaml",
"importmap.php"
]
},
"symfony/console": { "symfony/console": {
"version": "7.3", "version": "7.3",
"recipe": { "recipe": {
@ -265,5 +280,8 @@
"config/packages/web_profiler.yaml", "config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml" "config/routes/web_profiler.yaml"
] ]
},
"twig/extra-bundle": {
"version": "v3.21.0"
} }
} }

View file

@ -1,4 +1,4 @@
{{ form_start(form) }} {{ form_start(form, {'attr': {'class': 'mb-3'}}) }}
{{ form_widget(form) }} {{ form_widget(form, {'attr': {'class': 'form-control'}}) }}
<button class="btn">{{ button_label|default('Save') }}</button> <button class="btn btn-primary mt-2">{{ button_label|default('Save') }}</button>
{{ form_end(form) }} {{ form_end(form) }}

View file

@ -3,42 +3,61 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark light"> <meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0000ff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#222222" media="(prefers-color-scheme: dark)">
<title>{% block title %}Welcome!{% endblock %}</title> <title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" type="image/svg+xml" <link rel="icon" type="image/svg+xml"
href="{{ favicon }}" /> href="{{ favicon }}" />
{% set currentDate = "now"|date("d") %} {% block javascripts %}
{% if currentDate % 4 == 0 %} {% block importmap %}{{ importmap('app') }}{% endblock %}
<link rel="stylesheet" href="/static/css/new.min.css"> {% endblock %}
{% elseif currentDate % 4 == 1 %}
<link rel="stylesheet" href="/static/css/simple.min.css">
{% elseif currentDate % 4 == 2 %}
<link rel="stylesheet" href="/static/css/water.min.css">
{% else %}
<link rel="stylesheet" href="/static/css/fieber.css">
{% endif %}
<style>
label{
display: block;
}
</style>
<script src="/static/js/htmx.min.js"></script>
</head> </head>
<body> <body>
<header> <header class="mb-4">
<p>Hello {{ app.request.cookies.get('username', 'nobody') }} - <a href="{{ path('username') }}">change name</a></p> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<nav> <div class="container-fluid">
<a href="{{ path('app_food_order_index') }}">Orders</a> / <span class="navbar-brand">Futtern</span>
<a href="{{ path('app_food_vendor_index') }}">Vendors</a> / <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<a <span class="navbar-toggler-icon"></span>
href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" </button>
target="_blank" <div class="collapse navbar-collapse" id="navbarNav">
>Create Issue</a> / <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<a href="/api">API</a> <li class="nav-item"><a class="nav-link" href="{{ path('app_food_order_index') }}">Orders</a></li>
<li class="nav-item"><a class="nav-link" href="{{ path('app_food_vendor_index') }}">Vendors</a></li>
<li class="nav-item"><a class="nav-link" href="/api">API</a></li>
<li class="nav-item"><a class="nav-link" href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank">Create Issue</a></li>
</ul>
<div class="btn-group ms-auto" role="group" aria-label="Mode selection">
<input type="radio" class="btn-check" name="mode" id="normal" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="normal">
Normal
<span class="emoji-normal">😌</span>
<span class="emoji-enhanced">😌🍑</span>
<span class="emoji-bonkers">😌🍑🍆💦👅💋😈🏳️‍🌈✨</span>
</label>
<input type="radio" class="btn-check" name="mode" id="enhanced" autocomplete="off">
<label class="btn btn-outline-primary" for="enhanced">
Enhanced
<span class="emoji-normal">✨</span>
<span class="emoji-enhanced">✨🍑🍆</span>
<span class="emoji-bonkers">✨🍑🍆💦👅💋😈🏳️‍🌈</span>
</label>
<input type="radio" class="btn-check" name="mode" id="bonkers" autocomplete="off">
<label class="btn btn-outline-primary" for="bonkers">
Bonkers
<span class="emoji-normal">💦</span>
<span class="emoji-enhanced">💦🍑🍆</span>
<span class="emoji-bonkers">💦🍑🍆👅💋😈🏳️‍🌈✨🥵😳🤤😍🥴💕💗💘💝💞💟💌💏💑</span>
</label>
</div>
<span class="navbar-text">
Hello {{ app.request.cookies.get('username', 'nobody') }} - <a class="text-light" href="{{ path('username') }}">change name</a>
</span>
</div>
</div>
</nav> </nav>
</header> </header>
<main> <main class="container pb-5">
{% block body %}{% endblock %} {% block body %}{% endblock %}
</main> </main>
</body> </body>

View file

@ -3,10 +3,12 @@
{% block title %}Edit FoodOrder{% endblock %} {% block title %}Edit FoodOrder{% endblock %}
{% block body %} {% block body %}
<h1>Edit FoodOrder</h1> <h1 class="mb-4">Edit FoodOrder</h1>
{{ include('_form.html.twig', {'button_label': 'Update'}) }} <div class="mb-4">
{{ include('_form.html.twig', {'button_label': 'Update'}) }}
</div>
<a href="{{ path('app_food_order_index') }}">back to list</a> <a class="btn btn-secondary" href="{{ path('app_food_order_index') }}">back to list</a>
{% endblock %} {% endblock %}

View file

@ -3,17 +3,18 @@
{% block title %}FoodOrder index{% endblock %} {% block title %}FoodOrder index{% endblock %}
{% block body %} {% block body %}
<h1>FoodOrder index</h1> <h1 class="mb-4">FoodOrder index</h1>
<div> <div class="mb-3">
<button <button
class="btn btn-primary"
hx-get="{{ path('app_food_order_new') }}" hx-get="{{ path('app_food_order_new') }}"
hx-trigger="click" hx-trigger="click"
hx-target="closest div" hx-target="closest div"
>Create new</button> >Create new</button>
</div> </div>
<hr> <hr>
<table class="table"> <table class="table table-striped table-hover">
<thead> <thead class="table-light">
<tr> <tr>
<th>CreatedBy</th> <th>CreatedBy</th>
<th>Vendor</th> <th>Vendor</th>
@ -28,7 +29,7 @@
{% endfor %} {% endfor %}
{% if food_orders|length < 10 %} {% if food_orders|length < 10 %}
<tr> <tr>
<td colspan="5"> <td colspan="5" class="text-center text-muted">
check the <a href="{{ path('app_food_order_archive') }}">archive</a> check the <a href="{{ path('app_food_order_archive') }}">archive</a>
for older orders for older orders
</td> </td>
@ -36,10 +37,12 @@
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
{% if prev_page > 0 %} <div class="d-flex gap-2">
<a href="{{ path('app_food_order_archive', {'page': prev_page}) }}">previous page</a> | {% if prev_page > 0 %}
{% endif %} <a class="btn btn-outline-secondary btn-sm" href="{{ path('app_food_order_archive', {'page': prev_page}) }}">previous page</a>
{% if next_page > current_page %} {% endif %}
<a href="{{ path('app_food_order_archive', {'page': next_page}) }}">next page</a> {% if next_page > current_page %}
{% endif %} <a class="btn btn-outline-secondary btn-sm" href="{{ path('app_food_order_archive', {'page': next_page}) }}">next page</a>
{% endif %}
</div>
{% endblock %} {% endblock %}

View file

@ -1,2 +1,4 @@
{{ include('_form.html.twig') }} <div class="mb-4">
{{ include('_form.html.twig') }}
</div>

View file

@ -3,9 +3,9 @@
{% block title %}FoodOrder{% endblock %} {% block title %}FoodOrder{% endblock %}
{% block body %} {% block body %}
<h1>FoodOrder</h1> <h1 class="mb-4">FoodOrder</h1>
<table class="table"> <table class="table table-bordered table-striped w-auto">
<tbody> <tbody>
<tr> <tr>
<th>Vendor</th> <th>Vendor</th>
@ -29,16 +29,18 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<a class="button" href="{{ path('app_food_order_index') }}">back to list</a> <div class="mb-3 d-flex gap-2">
{% if(food_order.isClosed) %} <a class="btn btn-secondary" href="{{ path('app_food_order_index') }}">back to list</a>
<a class="button" href="{{ path('app_food_order_open', {'id': food_order.id}) }}">reopen</a> {% if(food_order.isClosed) %}
{% else %} <a class="btn btn-warning" href="{{ path('app_food_order_open', {'id': food_order.id}) }}">reopen</a>
<a class="button" href="{{ path('app_food_order_close', {'id': food_order.id}) }}">close</a> {% else %}
{% endif %} <a class="btn btn-success" href="{{ path('app_food_order_close', {'id': food_order.id}) }}">close</a>
{% endif %}
</div>
<h2>Items</h2> <h2 class="mt-5">Items</h2>
<table class="table"> <table class="table table-hover">
<thead> <thead class="table-light">
<tr> <tr>
<th>Index</th> <th>Index</th>
<th>username</th> <th>username</th>
@ -57,15 +59,15 @@
<td> <td>
{% if(food_order.isClosed) %} {% if(food_order.isClosed) %}
{% else %} {% else %}
<a href="{{ path('app_order_item_edit', {'id': item.id}) }}">edit</a> <a class="btn btn-sm btn-outline-primary me-1" href="{{ path('app_order_item_edit', {'id': item.id}) }}">edit</a>
<a href="{{ path('app_order_item_copy', {'id': item.id}) }}">copy</a> <a class="btn btn-sm btn-outline-secondary me-1" href="{{ path('app_order_item_copy', {'id': item.id}) }}">copy</a>
<a href="{{ path('app_order_item_delete', {'id': item.id}) }}">remove</a> <a class="btn btn-sm btn-outline-danger" href="{{ path('app_order_item_delete', {'id': item.id}) }}">remove</a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a class="button" href="{{ path('app_order_item_new', {'foodOrder': food_order.id}) }}">New Item</a> <a class="btn btn-primary mt-2" href="{{ path('app_order_item_new', {'foodOrder': food_order.id}) }}">New Item</a>
{% endblock %} {% endblock %}

View file

@ -4,6 +4,6 @@
<td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td> <td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td> <td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td> <td>
<a href="{{ path('app_food_order_show', {'id': food_order.id}) }}">show</a> <a class="btn btn-sm btn-outline-info" href="{{ path('app_food_order_show', {'id': food_order.id}) }}">show</a>
</td> </td>
</tr> </tr>

View file

@ -1,4 +1,4 @@
{{ form_start(form) }} {{ form_start(form, {'attr': {'class': 'mb-3'}}) }}
{{ form_widget(form) }} {{ form_widget(form, {'attr': {'class': 'form-control'}}) }}
<button class="btn">{{ button_label|default('Save') }}</button> <button class="btn btn-primary mt-2">{{ button_label|default('Save') }}</button>
{{ form_end(form) }} {{ form_end(form) }}

View file

@ -3,9 +3,11 @@
{% block title %}Edit FoodVendor{% endblock %} {% block title %}Edit FoodVendor{% endblock %}
{% block body %} {% block body %}
<h1>Edit FoodVendor</h1> <h1 class="mb-4">Edit FoodVendor</h1>
{{ include('food_vendor/_form.html.twig', {'button_label': 'Update'}) }} <div class="mb-4">
{{ include('food_vendor/_form.html.twig', {'button_label': 'Update'}) }}
</div>
<a href="{{ path('app_food_vendor_index') }}">back to list</a> <a class="btn btn-secondary" href="{{ path('app_food_vendor_index') }}">back to list</a>
{% endblock %} {% endblock %}

View file

@ -3,10 +3,10 @@
{% block title %}FoodVendor index{% endblock %} {% block title %}FoodVendor index{% endblock %}
{% block body %} {% block body %}
<h1>FoodVendor index</h1> <h1 class="mb-4">FoodVendor index</h1>
<table class="table"> <table class="table table-striped table-hover">
<thead> <thead class="table-light">
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>actions</th> <th>actions</th>
@ -17,17 +17,17 @@
<tr> <tr>
<td>{{ food_vendor.name }}</td> <td>{{ food_vendor.name }}</td>
<td> <td>
<a href="{{ path('app_food_vendor_show', {'id': food_vendor.id}) }}">show</a> <a class="btn btn-sm btn-outline-info me-1" href="{{ path('app_food_vendor_show', {'id': food_vendor.id}) }}">show</a>
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}">edit</a> <a class="btn btn-sm btn-outline-primary" href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}">edit</a>
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="3">no records found</td> <td colspan="3" class="text-center text-muted">no records found</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{{ path('app_food_vendor_new') }}">Create new</a> <a class="btn btn-primary" href="{{ path('app_food_vendor_new') }}">Create new</a>
{% endblock %} {% endblock %}

View file

@ -3,9 +3,11 @@
{% block title %}New FoodVendor{% endblock %} {% block title %}New FoodVendor{% endblock %}
{% block body %} {% block body %}
<h1>Create new FoodVendor</h1> <h1 class="mb-4">Create new FoodVendor</h1>
{{ include('food_vendor/_form.html.twig') }} <div class="mb-4">
{{ include('food_vendor/_form.html.twig') }}
</div>
<a href="{{ path('app_food_vendor_index') }}">back to list</a> <a class="btn btn-secondary" href="{{ path('app_food_vendor_index') }}">back to list</a>
{% endblock %} {% endblock %}

View file

@ -3,9 +3,9 @@
{% block title %}FoodVendor{% endblock %} {% block title %}FoodVendor{% endblock %}
{% block body %} {% block body %}
<h1>FoodVendor</h1> <h1 class="mb-4">FoodVendor</h1>
<table class="table"> <table class="table table-bordered w-auto">
<tbody> <tbody>
<tr> <tr>
<th>Name</th> <th>Name</th>
@ -18,23 +18,22 @@
</tbody> </tbody>
</table> </table>
<section> <section class="mb-4">
<h2>known menuitems</h2> <h2>known menuitems</h2>
<ul> <ul class="list-group list-group-flush">
{% for item in food_vendor.menuItems %} {% for item in food_vendor.menuItems %}
<li> <li class="list-group-item">
<a href="{{ path('app_menu_item_show', {'id': item.id}) }}">{{ item.name }}</a> <a href="{{ path('app_menu_item_show', {'id': item.id}) }}">{{ item.name }}</a>
{% if(item.aliasOf) %} {% if(item.aliasOf) %}
(alias of: {{ item.aliasOf.name }}) <span class="text-muted">(alias of: {{ item.aliasOf.name }})</span>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</section> </section>
<div class="d-flex gap-2">
<a href="{{ path('app_food_vendor_index') }}">back to list</a> <a class="btn btn-secondary" href="{{ path('app_food_vendor_index') }}">back to list</a>
<a class="btn btn-primary" href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}">edit</a>
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}">edit</a> </div>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
<form method="post" action="{{ path('app_menu_item_delete', {'id': menu_item.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');"> <form method="post" action="{{ path('app_menu_item_delete', {'id': menu_item.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ menu_item.id) }}"> <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ menu_item.id) }}">
<button class="btn">Delete</button> <button class="btn btn-danger btn-sm">Delete</button>
</form> </form>

View file

@ -1,4 +1,4 @@
{{ form_start(form) }} {{ form_start(form, {'attr': {'class': 'mb-3'}}) }}
{{ form_widget(form) }} {{ form_widget(form, {'attr': {'class': 'form-control'}}) }}
<button class="btn">{{ button_label|default('Save') }}</button> <button class="btn btn-primary mt-2">{{ button_label|default('Save') }}</button>
{{ form_end(form) }} {{ form_end(form) }}

View file

@ -3,9 +3,13 @@
{% block title %}Edit MenuItem{% endblock %} {% block title %}Edit MenuItem{% endblock %}
{% block body %} {% block body %}
<h1>Edit MenuItem</h1> <h1 class="mb-4">Edit MenuItem</h1>
{{ include('menu_item/_form.html.twig', {'button_label': 'Update'}) }} <div class="mb-4">
{{ include('menu_item/_form.html.twig', {'button_label': 'Update'}) }}
</div>
{{ include('menu_item/_delete_form.html.twig') }} <div class="d-flex gap-2">
{{ include('menu_item/_delete_form.html.twig') }}
</div>
{% endblock %} {% endblock %}

View file

@ -3,10 +3,10 @@
{% block title %}MenuItem index{% endblock %} {% block title %}MenuItem index{% endblock %}
{% block body %} {% block body %}
<h1>MenuItem index</h1> <h1 class="mb-4">MenuItem index</h1>
<table class="table"> <table class="table table-striped table-hover">
<thead> <thead class="table-light">
<tr> <tr>
<th>Id</th> <th>Id</th>
<th>Name</th> <th>Name</th>
@ -19,17 +19,17 @@
<td>{{ menu_item.id }}</td> <td>{{ menu_item.id }}</td>
<td>{{ menu_item.name }}</td> <td>{{ menu_item.name }}</td>
<td> <td>
<a href="{{ path('app_menu_item_show', {'id': menu_item.id}) }}">show</a> <a class="btn btn-sm btn-outline-info me-1" href="{{ path('app_menu_item_show', {'id': menu_item.id}) }}">show</a>
<a href="{{ path('app_menu_item_edit', {'id': menu_item.id}) }}">edit</a> <a class="btn btn-sm btn-outline-primary" href="{{ path('app_menu_item_edit', {'id': menu_item.id}) }}">edit</a>
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="3">no records found</td> <td colspan="3" class="text-center text-muted">no records found</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{{ path('app_menu_item_new') }}">Create new</a> <a class="btn btn-primary" href="{{ path('app_menu_item_new') }}">Create new</a>
{% endblock %} {% endblock %}

View file

@ -3,9 +3,11 @@
{% block title %}New MenuItem{% endblock %} {% block title %}New MenuItem{% endblock %}
{% block body %} {% block body %}
<h1>Create new MenuItem</h1> <h1 class="mb-4">Create new MenuItem</h1>
{{ include('menu_item/_form.html.twig') }} <div class="mb-4">
{{ include('menu_item/_form.html.twig') }}
</div>
<a href="{{ path('app_menu_item_index') }}">back to list</a> <a class="btn btn-secondary" href="{{ path('app_menu_item_index') }}">back to list</a>
{% endblock %} {% endblock %}

View file

@ -3,9 +3,9 @@
{% block title %}MenuItem{% endblock %} {% block title %}MenuItem{% endblock %}
{% block body %} {% block body %}
<h1>MenuItem</h1> <h1 class="mb-4">MenuItem</h1>
<table class="table"> <table class="table table-bordered w-auto">
<tbody> <tbody>
<tr> <tr>
<th>Id</th> <th>Id</th>
@ -25,9 +25,9 @@
<tr> <tr>
<th>Aliases</th> <th>Aliases</th>
<td> <td>
<ul> <ul class="list-group list-group-flush">
{% for alias in menu_item.aliases %} {% for alias in menu_item.aliases %}
<li>{{ alias.name }}</li> <li class="list-group-item">{{ alias.name }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
@ -36,9 +36,9 @@
</tbody> </tbody>
</table> </table>
<a href="{{ path('app_food_vendor_show', { 'id': menu_item.foodVendor.id}) }}">back to list</a> <div class="d-flex gap-2">
<a class="btn btn-secondary" href="{{ path('app_food_vendor_show', { 'id': menu_item.foodVendor.id}) }}">back to list</a>
<a href="{{ path('app_menu_item_edit', {'id': menu_item.id}) }}">edit</a> <a class="btn btn-primary" href="{{ path('app_menu_item_edit', {'id': menu_item.id}) }}">edit</a>
{{ include('menu_item/_delete_form.html.twig') }}
{{ include('menu_item/_delete_form.html.twig') }} </div>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{{ form_start(form) }} {{ form_start(form, {'attr': {'class': 'mb-3'}}) }}
{{ form_widget(form) }} {{ form_widget(form, {'attr': {'class': 'form-control'}}) }}
<button class="btn">{{ button_label|default('Save') }}</button> <button class="btn btn-primary mt-2">{{ button_label|default('Save') }}</button>
{{ form_end(form) }} {{ form_end(form) }}

View file

@ -3,9 +3,11 @@
{% block title %}Edit OrderItem{% endblock %} {% block title %}Edit OrderItem{% endblock %}
{% block body %} {% block body %}
<h1>Edit OrderItem</h1> <h1 class="mb-4">Edit OrderItem</h1>
{{ include('order_item/_form.html.twig', {'button_label': 'Update'}) }} <div class="mb-4">
{{ include('order_item/_form.html.twig', {'button_label': 'Update'}) }}
</div>
<a href="{{ path('app_food_order_show', {'id': order_item.foodOrder.id}) }}">back to list</a> <a class="btn btn-secondary" href="{{ path('app_food_order_show', {'id': order_item.foodOrder.id}) }}">back to list</a>
{% endblock %} {% endblock %}

View file

@ -3,30 +3,32 @@
{% block title %}New OrderItem{% endblock %} {% block title %}New OrderItem{% endblock %}
{% block body %} {% block body %}
<h1>Create new OrderItem</h1> <h1 class="mb-4">Create new OrderItem</h1>
{{ include('order_item/_form.html.twig') }} <div class="mb-4">
{{ include('order_item/_form.html.twig') }}
</div>
<hr /> <hr />
{% if food_order.foodVendor.menuLink != '' %} {% if food_order.foodVendor.menuLink != '' %}
<a href="{{ food_order.foodVendor.menuLink }}" target="_blank"> <a class="btn btn-outline-secondary mb-3" href="{{ food_order.foodVendor.menuLink }}" target="_blank">
External link to Menu External link to Menu
</a> </a>
{% endif %} {% endif %}
<div> <div class="mb-2">
<b>click a button to select a given menuitem</b> <b>click a button to select a given menuitem</b>
</div> </div>
<div> <div class="mb-3 d-flex flex-wrap gap-2">
{% for menuItem in menuItems %} {% for menuItem in menuItems %}
<a href="#" data-menu-item>{{ menuItem.name }}</a> <a class="btn btn-outline-info btn-sm" href="#" data-menu-item>{{ menuItem.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
<hr /> <hr />
<a class="button" href="{{ path('app_food_order_show', { 'id': food_order.id}) }}">back to list</a> <a class="btn btn-secondary" href="{{ path('app_food_order_show', { 'id': food_order.id}) }}">back to list</a>
<script> <script>
document.querySelectorAll('[data-menu-item]').forEach(function(element) { document.querySelectorAll('[data-menu-item]').forEach(function(element) {

View file

@ -3,7 +3,9 @@
{% block title %}Tell me your name{% endblock %} {% block title %}Tell me your name{% endblock %}
{% block body %} {% block body %}
<h1>Tell me your name</h1> <h1 class="mb-4">Tell me your name</h1>
<p>By submitting the form, you agree that your username will be stored as a cookie.</p> <p class="mb-3">By submitting the form, you agree that your username will be stored as a cookie.</p>
{{ include('_form.html.twig') }} <div class="mb-4">
{{ include('_form.html.twig') }}
</div>
{% endblock %} {% endblock %}

View file

@ -100,15 +100,15 @@ describe(FoodOrderController::class, function (): void {
$crawler = $this->client->request('GET', "{$this->path}{$order->getId()}"); $crawler = $this->client->request('GET', "{$this->path}{$order->getId()}");
$this->assertResponseIsSuccessful(); $this->assertResponseIsSuccessful();
$tdContent = $crawler->filter( $tdContent = $crawler->filter(
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(3)' 'table.table-hover tbody tr:nth-child(1) td:nth-child(3)'
)->text(); )->text();
$this->assertEquals('A', $tdContent); $this->assertEquals('A', $tdContent);
$tdContent = $crawler->filter( $tdContent = $crawler->filter(
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(2) > td:nth-child(3)' 'table.table-hover tbody tr:nth-child(2) td:nth-child(3)'
)->text(); )->text();
$this->assertEquals('B', $tdContent); $this->assertEquals('B', $tdContent);
$tdContent = $crawler->filter( $tdContent = $crawler->filter(
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(3) > td:nth-child(3)' 'table.table-hover tbody tr:nth-child(3) td:nth-child(3)'
)->text(); )->text();
$this->assertEquals('C', $tdContent); $this->assertEquals('C', $tdContent);
}); });

View file

@ -119,7 +119,7 @@ describe(FoodVendorController::class, function (): void {
)->text(); )->text();
$this->assertSame('My Title', $nameNode); $this->assertSame('My Title', $nameNode);
$itemNodes = $crawler->filter('li'); $itemNodes = $crawler->filter('ul.list-group li.list-group-item');
$this->assertCount(4, $itemNodes); $this->assertCount(4, $itemNodes);
}); });