Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

59 changed files with 1327 additions and 2373 deletions

5
.gitignore vendored
View file

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

View file

@ -1,24 +0,0 @@
/*
* 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 emojiButtonListener from './javascript/emoji-button.js';
import 'bootstrap';
import { initRadioState } from './javascript/radioState.js';
document.addEventListener('DOMContentLoaded', () => {
initRadioState();
emojiButtonListener();
});

View file

@ -1,14 +0,0 @@
const emojiButtonListener = function () {
const buttons = document.querySelectorAll('.emoji-buttons .btn.btn-primary');
buttons.forEach(button => {
button.addEventListener('click', function() {
const emojiField = document.querySelector('#food_vendor_emojis');
if (emojiField) {
emojiField.value += this.textContent;
}
});
});
}
export default emojiButtonListener;

View file

@ -1,19 +0,0 @@
// 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

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

View file

@ -1,136 +0,0 @@
// 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

@ -1,55 +0,0 @@
// 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

@ -1,35 +0,0 @@
// 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

@ -1,18 +0,0 @@
// 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 };

View file

@ -1,179 +0,0 @@
/*
* =================================================================================================
* 💖 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

@ -1,30 +0,0 @@
/* 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;
}
}

View file

@ -1,568 +0,0 @@
/* 🌈✨ 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 {
background: linear-gradient(270deg, var(--bs-pink), var(--bs-purple), var(--bs-cyan), var(--bs-yellow), var(--bs-green), var(--bs-orange), var(--bs-red), var(--bs-pink));
background-size: 1600% 1600%;
animation: rainbowGradient 10s ease infinite;
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,14 +18,12 @@
"phpstan/phpdoc-parser": "^1.33",
"psr/clock": "^1.0",
"symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.3.*",
"symfony/flex": "^2.7.1",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/monolog-bundle": "^3.10",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/runtime": "7.3.*",
@ -35,9 +33,7 @@
"symfony/twig-bundle": "7.3.*",
"symfony/uid": "7.3.*",
"symfony/validator": "7.3.*",
"symfony/yaml": "7.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
"symfony/yaml": "7.3.*"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.1",
@ -90,8 +86,7 @@
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"

849
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "923bae46e3b7f783b6c25f8f68f97991",
"content-hash": "c21233664fb5c4d25afb8a8cdc29213a",
"packages": [
{
"name": "api-platform/core",
@ -222,87 +222,6 @@
},
"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",
"version": "2.3.0",
@ -1422,109 +1341,6 @@
},
"time": "2025-01-24T11:45:48+00:00"
},
{
"name": "monolog/monolog",
"version": "3.9.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/log": "^2.0 || ^3.0"
},
"provide": {
"psr/log-implementation": "3.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^3.0",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7 || ^8",
"ext-json": "*",
"graylog2/gelf-php": "^1.4.2 || ^2.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"php-console/php-console": "^3.1.8",
"phpstan/phpstan": "^2",
"phpstan/phpstan-deprecation-rules": "^2",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^10.5.17 || ^11.0.7",
"predis/predis": "^1.1 || ^2",
"rollbar/rollbar": "^4.0",
"ruflin/elastica": "^7 || ^8",
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"ext-openssl": "Required to send log messages using SSL",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "https://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/3.9.0"
},
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2025-03-24T10:02:05+00:00"
},
{
"name": "nelmio/cors-bundle",
"version": "2.5.0",
@ -2184,86 +2000,6 @@
],
"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",
"version": "v7.3.0",
@ -3757,179 +3493,6 @@
],
"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",
"version": "v7.3.0",
@ -4123,165 +3686,6 @@
],
"time": "2025-05-29T07:47:32+00:00"
},
{
"name": "symfony/monolog-bridge",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bridge.git",
"reference": "1b188c8abbbef25b111da878797514b7a8d33990"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/1b188c8abbbef25b111da878797514b7a8d33990",
"reference": "1b188c8abbbef25b111da878797514b7a8d33990",
"shasum": ""
},
"require": {
"monolog/monolog": "^3",
"php": ">=8.2",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"symfony/console": "<6.4",
"symfony/http-foundation": "<6.4",
"symfony/security-core": "<6.4"
},
"require-dev": {
"symfony/console": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/mailer": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/security-core": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"type": "symfony-bridge",
"autoload": {
"psr-4": {
"Symfony\\Bridge\\Monolog\\": ""
},
"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": "Provides integration for Monolog with various Symfony components",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/monolog-bridge/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-03-21T12:17:46+00:00"
},
{
"name": "symfony/monolog-bundle",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bundle.git",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"shasum": ""
},
"require": {
"monolog/monolog": "^1.25.1 || ^2.0 || ^3.0",
"php": ">=7.2.5",
"symfony/config": "^5.4 || ^6.0 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^6.3 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MonologBundle\\": ""
},
"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": "Symfony MonologBundle",
"homepage": "https://symfony.com",
"keywords": [
"log",
"logging"
],
"support": {
"issues": "https://github.com/symfony/monolog-bundle/issues",
"source": "https://github.com/symfony/monolog-bundle/tree/v3.10.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": "2023-11-06T17:08:13+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v7.3.0",
@ -6667,80 +6071,6 @@
],
"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",
"version": "v3.21.1",
@ -10328,6 +9658,179 @@
],
"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",
"version": "v1.63.0",
@ -10781,7 +10284,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@ -10789,7 +10292,7 @@
"ext-ctype": "*",
"ext-iconv": "*"
},
"platform-dev": {},
"platform-dev": [],
"platform-overrides": {
"php": "8.4"
},

View file

@ -8,11 +8,9 @@ use Liip\TestFixturesBundle\LiipTestFixturesBundle;
use Nelmio\CorsBundle\NelmioCorsBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Bundle\MonologBundle\MonologBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
return [
FrameworkBundle::class => [
@ -51,10 +49,4 @@ return [
ApiPlatformBundle::class => [
'all' => true,
],
TwigExtraBundle::class => [
'all' => true,
],
MonologBundle::class => [
'all' => true,
],
];

View file

@ -1,21 +0,0 @@
<?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,95 +0,0 @@
<?php declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('monolog', [
'channels' => [
'deprecation',
],
]);
if ($containerConfigurator->env() === 'dev') {
$containerConfigurator->extension('monolog', [
'handlers' => [
'main' => [
'type' => 'stream',
'path' => '%kernel.logs_dir%/%kernel.environment%.log',
'level' => 'debug',
'channels' => [
'!event',
],
],
'console' => [
'type' => 'console',
'process_psr_3_messages' => false,
'channels' => [
'!event',
'!doctrine',
'!console',
],
],
],
]);
}
if ($containerConfigurator->env() === 'test') {
$containerConfigurator->extension('monolog', [
'handlers' => [
'main' => [
'type' => 'fingers_crossed',
'action_level' => 'error',
'handler' => 'nested',
'excluded_http_codes' => [
404,
405,
],
'channels' => [
'!event',
],
],
'nested' => [
'type' => 'stream',
'path' => '%kernel.logs_dir%/%kernel.environment%.log',
'level' => 'debug',
],
],
]);
}
if ($containerConfigurator->env() === 'prod') {
$containerConfigurator->extension('monolog', [
'handlers' => [
'main' => [
'type' => 'fingers_crossed',
'action_level' => 'error',
'handler' => 'nested',
'excluded_http_codes' => [
404,
405,
],
'buffer_size' => 50,
],
'nested' => [
'type' => 'stream',
'path' => 'php://stderr',
'level' => 'debug',
'formatter' => 'monolog.formatter.json',
],
'console' => [
'type' => 'console',
'process_psr_3_messages' => false,
'channels' => [
'!event',
'!doctrine',
],
],
'deprecation' => [
'type' => 'stream',
'channels' => [
'deprecation',
],
'path' => 'php://stderr',
'formatter' => 'monolog.formatter.json',
],
],
]);
}
};

View file

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

View file

@ -8,7 +8,7 @@ fi
mkdir $TARGETDIR
cd $TARGETDIR || return
pathsToCopy="assets public bin config migrations src templates composer.json composer.lock symfony.lock .env importmap.php"
pathsToCopy="public bin config migrations src templates composer.json composer.lock symfony.lock .env"
for path in $pathsToCopy
do

View file

@ -5,7 +5,4 @@ systemctl --user start pod-futtern
sleep 2
podman exec -it futtern-php /var/www/html/bin/console cache:clear
podman exec -it futtern-php /var/www/html/bin/console cache:warmup
podman exec -it futtern-php /var/www/html/bin/console asset-map:compile
echo 'yes' | podman exec -it futtern-php /var/www/html/bin/console doctrine:migrations:migrate

View file

@ -1,32 +0,0 @@
<?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' => '2.0.5',
],
];

View file

@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250621131822 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE food_vendor ADD COLUMN emojis VARCHAR(30) DEFAULT NULL
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TEMPORARY TABLE __temp__food_vendor AS SELECT name, phone, menu_link, id FROM food_vendor
SQL);
$this->addSql(<<<'SQL'
DROP TABLE food_vendor
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE food_vendor (name VARCHAR(50) NOT NULL, phone VARCHAR(50) DEFAULT '', menu_link VARCHAR(255) DEFAULT NULL, id BLOB NOT NULL, PRIMARY KEY(id))
SQL);
$this->addSql(<<<'SQL'
INSERT INTO food_vendor (name, phone, menu_link, id) SELECT name, phone, menu_link, id FROM __temp__food_vendor
SQL);
$this->addSql(<<<'SQL'
DROP TABLE __temp__food_vendor
SQL);
}
}

View file

@ -0,0 +1,965 @@
/* 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;
}
}

1
public/static/css/new.min.css vendored Normal file
View file

@ -0,0 +1 @@
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}

1
public/static/css/simple.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
public/static/css/water.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
public/static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -9,15 +9,14 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Repository\FoodOrderRepository;
use App\State\LatestOrderProvider;
use App\State\OpenOrdersProvider;
use App\State\LatestOrderProvider;
use DateInterval;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Uid\Ulid;
use function iterator_to_array;
@ -29,13 +28,10 @@ use function iterator_to_array;
description: 'Get only open orders',
provider: OpenOrdersProvider::class,
),
new Get(
new GetCollection(
uriTemplate: 'food_orders/latest',
description: 'Get the latest created order',
provider: LatestOrderProvider::class,
normalizationContext: [
'groups' => ['food_order:read', 'food_order:latest'],
]
),
new GetCollection,
new Get,
@ -48,31 +44,26 @@ use function iterator_to_array;
class FoodOrder
{
#[ORM\Column(nullable: true)]
#[Groups(['food_order:read'])]
private DateTimeImmutable|null $closedAt = null;
#[ORM\ManyToOne(inversedBy: 'foodOrders')]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['food_order:read', 'food_order:latest'])]
private FoodVendor|null $foodVendor = null;
/**
* @var Collection<int, OrderItem>
*/
#[ORM\OneToMany(targetEntity: OrderItem::class, mappedBy: 'foodOrder', orphanRemoval: true)]
#[Groups(['food_order:read', 'food_order:latest'])]
private Collection $orderItems;
#[ORM\Column(length: 255, options: [
'default' => 'nobody',
])]
#[Groups(['food_order:read'])]
private string|null $createdBy = 'nobody';
public function __construct(
#[ORM\Id]
#[ORM\Column(type: UlidType::NAME, unique: true)]
#[Groups(['food_order:read'])]
private Ulid|null $id = new Ulid
) {
$this->id ??= new Ulid;
@ -85,7 +76,6 @@ class FoodOrder
return $this->id;
}
#[Groups(['food_order:read'])]
public function getCreatedAt(): DateTimeImmutable
{
return $this->id->getDateTime();

View file

@ -7,27 +7,20 @@ use App\Repository\FoodVendorRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Uid\Ulid;
use Symfony\Component\Validator\Constraints\Length;
use function mb_strlen;
#[ORM\Entity(repositoryClass: FoodVendorRepository::class)]
#[ApiResource]
class FoodVendor
{
#[ORM\Column(length: 50)]
#[Groups(['food_order:latest', 'food_vendor:read'])]
private string|null $name = null;
#[ORM\Column(length: 50, nullable: true, options: [
'default' => '',
])]
#[Groups(['food_order:latest', 'food_vendor:read'])]
private string|null $phone = null;
/**
@ -43,23 +36,13 @@ class FoodVendor
private Collection $menuItems;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['food_order:latest'])]
private string|null $menuLink = null;
/**
* String of emojis (max 30 characters)
*/
#[ORM\Column(length: 30, nullable: true)]
#[Groups(['food_order:latest', 'food_vendor:read'])]
#[Length(max: 10)]
private string|null $emojis = null;
public function __construct(
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Column(type: UlidType::NAME, unique: true)]
#[ORM\CustomIdGenerator(class: UlidGenerator::class)]
#[Groups(['food_order:latest'])]
private Ulid|null $id = new Ulid
) {
$this->id ??= new Ulid;
@ -167,19 +150,4 @@ class FoodVendor
$this->phone = $phone;
return $this;
}
public function getEmojis(): string|null
{
return $this->emojis;
}
public function setEmojis(string|null $emojis): static
{
if ($emojis !== null && mb_strlen($emojis) > 30) {
throw new InvalidArgumentException('A maximum of 30 characters is allowed for emojis');
}
$this->emojis = $emojis;
return $this;
}
}

View file

@ -7,7 +7,6 @@ use App\Repository\OrderItemRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Uid\Ulid;
#[ApiResource]
@ -15,11 +14,9 @@ use Symfony\Component\Uid\Ulid;
class OrderItem
{
#[ORM\Column(length: 255)]
#[Groups(['food_order:latest'])]
private string|null $name = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['food_order:latest'])]
private string|null $extras = null;
#[ORM\ManyToOne(inversedBy: 'orderItems')]
@ -28,13 +25,11 @@ class OrderItem
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['food_order:latest'])]
private MenuItem|null $menuItem = null;
#[ORM\Column(length: 255, options: [
'default' => 'nobody',
])]
#[Groups(['food_order:latest'])]
private string|null $createdBy = 'nobody';
public function __construct(
@ -42,7 +37,6 @@ class OrderItem
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\Column(type: UlidType::NAME, unique: true)]
#[ORM\CustomIdGenerator(class: UlidGenerator::class)]
#[Groups(['food_order:latest'])]
private Ulid|null $id = new Ulid
) {
$this->id ??= new Ulid;

View file

@ -17,7 +17,6 @@ final class FoodVendorType extends AbstractType
->add('name')
->add('menuLink')
->add('phone')
->add('emojis')
;
}

View file

@ -63,7 +63,10 @@ final class FoodOrderRepository extends ServiceEntityRepository
->getResult();
}
public function findLatestOrder(): FoodOrder|null
/**
* @return FoodOrder|null
*/
public function findLatestOrder(): ?FoodOrder
{
return $this->createQueryBuilder('alias')
->orderBy('alias.id', 'DESC')

View file

@ -15,7 +15,7 @@ final readonly class LatestOrderProvider implements ProviderInterface
) {}
#[Override]
public function provide(Operation $operation, array $uriVariables = [], array $context = []): FoodOrder|null
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?FoodOrder
{
return $this->repository->findLatestOrder();
}

View file

@ -115,21 +115,6 @@
"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": {
"version": "7.3",
"recipe": {
@ -196,18 +181,6 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/monolog-bundle": {
"version": "3.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.7",
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
},
"files": [
"config/packages/monolog.yaml"
]
},
"symfony/property-info": {
"version": "7.3",
"recipe": {
@ -292,8 +265,5 @@
"config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml"
]
},
"twig/extra-bundle": {
"version": "v3.21.0"
}
}

View file

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

View file

@ -3,61 +3,42 @@
<head>
<meta charset="UTF-8">
<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>
<link rel="icon" type="image/svg+xml"
href="{{ favicon }}" />
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
{% set currentDate = "now"|date("d") %}
{% if currentDate % 4 == 0 %}
<link rel="stylesheet" href="/static/css/new.min.css">
{% 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>
<body>
<header class="mb-4">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<span class="navbar-brand">Futtern</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<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>
<header>
<p>Hello {{ app.request.cookies.get('username', 'nobody') }} - <a href="{{ path('username') }}">change name</a></p>
<nav>
<a href="{{ path('app_food_order_index') }}">Orders</a> /
<a href="{{ path('app_food_vendor_index') }}">Vendors</a> /
<a
href="https://git.hannover.ccc.de/lubiana/futtern/issues/new"
target="_blank"
>Create Issue</a> /
<a href="/api">API</a>
</nav>
</header>
<main class="container pb-5">
<main>
{% block body %}{% endblock %}
</main>
</body>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,127 +3,9 @@
{% block title %}Edit FoodVendor{% endblock %}
{% block body %}
<h1 class="mb-4">Edit FoodVendor</h1>
<h1>Edit FoodVendor</h1>
<div class="mb-4">
{{ include('food_vendor/_form.html.twig', {'button_label': 'Update'}) }}
</div>
{{ include('food_vendor/_form.html.twig', {'button_label': 'Update'}) }}
<div class="mb-4 emoji-buttons" data-role="emoji-selector">
<button class="btn btn-primary">🍕</button>
<button class="btn btn-primary">🍔</button>
<button class="btn btn-primary">🌮</button>
<button class="btn btn-primary">🍜</button>
<button class="btn btn-primary">🥗</button>
<button class="btn btn-primary">☕</button>
<button class="btn btn-primary">🍣</button>
<button class="btn btn-primary">🍤</button>
<button class="btn btn-primary">🍦</button>
<button class="btn btn-primary">🍩</button>
<button class="btn btn-primary">🍪</button>
<button class="btn btn-primary">🍰</button>
<button class="btn btn-primary">🍫</button>
<button class="btn btn-primary">🍿</button>
<button class="btn btn-primary">🍩</button>
<button class="btn btn-primary">🍭</button>
<button class="btn btn-primary">🍮</button>
<button class="btn btn-primary">🍯</button>
<button class="btn btn-primary">🍎</button>
<button class="btn btn-primary">🍊</button>
<button class="btn btn-primary">🍋</button>
<button class="btn btn-primary">🍌</button>
<button class="btn btn-primary">🍉</button>
<button class="btn btn-primary">🍇</button>
<button class="btn btn-primary">🍓</button>
<button class="btn btn-primary">🍈</button>
<button class="btn btn-primary">🍒</button>
<button class="btn btn-primary">🍍</button>
<button class="btn btn-primary">🥭</button>
<button class="btn btn-primary">🥥</button>
<button class="btn btn-primary">🥝</button>
<button class="btn btn-primary">🍅</button>
<button class="btn btn-primary">🍆</button>
<button class="btn btn-primary">🥑</button>
<button class="btn btn-primary">🥦</button>
<button class="btn btn-primary">🥒</button>
<button class="btn btn-primary">🌽</button>
<button class="btn btn-primary">🥕</button>
<button class="btn btn-primary">🧄</button>
<button class="btn btn-primary">🧅</button>
<button class="btn btn-primary">🥔</button>
<button class="btn btn-primary">🍠</button>
<button class="btn btn-primary">🥐</button>
<button class="btn btn-primary">🥯</button>
<button class="btn btn-primary">🍞</button>
<button class="btn btn-primary">🥖</button>
<button class="btn btn-primary">🥨</button>
<button class="btn btn-primary">🧀</button>
<button class="btn btn-primary">🥚</button>
<button class="btn btn-primary">🍳</button>
<button class="btn btn-primary">🥞</button>
<button class="btn btn-primary">🧇</button>
<button class="btn btn-primary">🥓</button>
<button class="btn btn-primary">🥩</button>
<button class="btn btn-primary">🍗</button>
<button class="btn btn-primary">🍖</button>
<button class="btn btn-primary">🌭</button>
<button class="btn btn-primary">🍔</button>
<button class="btn btn-primary">🍟</button>
<button class="btn btn-primary">🍕</button>
<button class="btn btn-primary">🥪</button>
<button class="btn btn-primary">🌮</button>
<button class="btn btn-primary">🌯</button>
<button class="btn btn-primary">🥙</button>
<button class="btn btn-primary">🧆</button>
<button class="btn btn-primary">🥘</button>
<button class="btn btn-primary">🍲</button>
<button class="btn btn-primary">🥣</button>
<button class="btn btn-primary">🥗</button>
<button class="btn btn-primary">🍿</button>
<button class="btn btn-primary">🧈</button>
<button class="btn btn-primary">🧂</button>
<button class="btn btn-primary">🥫</button>
<button class="btn btn-primary">🍱</button>
<button class="btn btn-primary">🍛</button>
<button class="btn btn-primary">🍚</button>
<button class="btn btn-primary">🍙</button>
<button class="btn btn-primary">🍘</button>
<button class="btn btn-primary">🍢</button>
<button class="btn btn-primary">🍡</button>
<button class="btn btn-primary">🍧</button>
<button class="btn btn-primary">🍨</button>
<button class="btn btn-primary">🍦</button>
<button class="btn btn-primary">🥧</button>
<button class="btn btn-primary">🍰</button>
<button class="btn btn-primary">🎂</button>
<button class="btn btn-primary">🍮</button>
<button class="btn btn-primary">🍭</button>
<button class="btn btn-primary">🍬</button>
<button class="btn btn-primary">🍫</button>
<button class="btn btn-primary">🍿</button>
<button class="btn btn-primary">🍩</button>
<button class="btn btn-primary">🍪</button>
<button class="btn btn-primary">🌰</button>
<button class="btn btn-primary">🥜</button>
<button class="btn btn-primary">🍯</button>
<button class="btn btn-primary">🥛</button>
<button class="btn btn-primary">🍼</button>
<button class="btn btn-primary">☕</button>
<button class="btn btn-primary">🍵</button>
<button class="btn btn-primary">🍶</button>
<button class="btn btn-primary">🍾</button>
<button class="btn btn-primary">🍷</button>
<button class="btn btn-primary">🍸</button>
<button class="btn btn-primary">🍹</button>
<button class="btn btn-primary">🍺</button>
<button class="btn btn-primary">🍻</button>
<button class="btn btn-primary">🥂</button>
<button class="btn btn-primary">🥃</button>
<button class="btn btn-primary">🥤</button>
<button class="btn btn-primary">🧃</button>
<button class="btn btn-primary">🧉</button>
<button class="btn btn-primary">🧊</button>
</div>
<a class="btn btn-secondary" href="{{ path('app_food_vendor_index') }}">back to list</a>
<a href="{{ path('app_food_vendor_index') }}">back to list</a>
{% endblock %}

View file

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

View file

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

View file

@ -3,9 +3,9 @@
{% block title %}FoodVendor{% endblock %}
{% block body %}
<h1 class="mb-4">FoodVendor</h1>
<h1>FoodVendor</h1>
<table class="table table-bordered w-auto">
<table class="table">
<tbody>
<tr>
<th>Name</th>
@ -18,22 +18,23 @@
</tbody>
</table>
<section class="mb-4">
<section>
<h2>known menuitems</h2>
<ul class="list-group list-group-flush">
<ul>
{% for item in food_vendor.menuItems %}
<li class="list-group-item">
<li>
<a href="{{ path('app_menu_item_show', {'id': item.id}) }}">{{ item.name }}</a>
{% if(item.aliasOf) %}
<span class="text-muted">(alias of: {{ item.aliasOf.name }})</span>
(alias of: {{ item.aliasOf.name }})
{% endif %}
</li>
{% endfor %}
</ul>
</section>
<div class="d-flex gap-2">
<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>
</div>
<a href="{{ path('app_food_vendor_index') }}">back to list</a>
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}">edit</a>
{% 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?');">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ menu_item.id) }}">
<button class="btn btn-danger btn-sm">Delete</button>
<button class="btn">Delete</button>
</form>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -100,15 +100,15 @@ describe(FoodOrderController::class, function (): void {
$crawler = $this->client->request('GET', "{$this->path}{$order->getId()}");
$this->assertResponseIsSuccessful();
$tdContent = $crawler->filter(
'table.table-hover tbody tr:nth-child(1) td:nth-child(3)'
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(3)'
)->text();
$this->assertEquals('A', $tdContent);
$tdContent = $crawler->filter(
'table.table-hover tbody tr:nth-child(2) td:nth-child(3)'
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(2) > td:nth-child(3)'
)->text();
$this->assertEquals('B', $tdContent);
$tdContent = $crawler->filter(
'table.table-hover tbody tr:nth-child(3) td:nth-child(3)'
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(3) > td:nth-child(3)'
)->text();
$this->assertEquals('C', $tdContent);
});
@ -230,6 +230,31 @@ describe(FoodOrderController::class, function (): void {
$openOrder = $this->repository->find($order->getId());
$this->assertTrue($openOrder->isClosed());
});
test('api_latest_order', function (): void {
// Create an older order
$olderOrder = new FoodOrder($this->generateOldUlid());
$olderOrder->setFoodVendor($this->vendor);
$this->manager->persist($olderOrder);
// Create the latest order
$latestOrder = new FoodOrder;
$latestOrder->setFoodVendor($this->vendor);
$this->manager->persist($latestOrder);
$this->manager->flush();
// Test the API endpoint
$this->client->request('GET', '/api/food_orders/latest');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
$response = json_decode($this->client->getResponse()->getContent(), true);
$this->assertIsArray($response);
$this->assertArrayHasKey('@id', $response);
$this->assertStringContainsString($latestOrder->getId()->__toString(), $response['@id']);
});
})
->covers(
FoodOrderController::class,

View file

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

View file

@ -5,7 +5,6 @@ namespace App\Tests\Unit\Entity;
use App\Entity\FoodOrder;
use App\Entity\FoodVendor;
use App\Entity\MenuItem;
use InvalidArgumentException;
use Symfony\Component\Uid\Ulid;
use function describe;
@ -21,17 +20,6 @@ describe(FoodVendor::class, function (): void {
$vendor->setPhone('1234567890');
$this->assertEquals('1234567890', $vendor->getPhone());
// Test emojis field
$this->assertNull($vendor->getEmojis());
$emojis = '😀😂🎉👍❤️';
$vendor->setEmojis($emojis);
$this->assertEquals($emojis, $vendor->getEmojis());
// Test emojis validation
$tooManyEmojis = '😀😂🎉👍❤️🚀🎈🎁🎊🎋🎍🎎🎏🎐🎑🎒🎓🎔🎕🎖🎗🎘🎙🎚🎛🎜🎝🎞🎟🎠🎡🎢';
$this->expectException(InvalidArgumentException::class);
$vendor->setEmojis($tooManyEmojis);
$this->assertCount(0, $vendor->getFoodOrders());
$order1 = new FoodOrder;
$vendor->addFoodOrder($order1);