Compare commits

..

16 commits

Author SHA1 Message Date
4d2ae3a6ab #98 make background more bonkers 2025-06-21 13:35:30 +00:00
98feafa3fc
ammend 2025-06-21 15:20:08 +02:00
c8e6af6896
#97: add emojis to foodvendor 2025-06-21 15:15:47 +02:00
e2167fa19f
haaaah 2025-06-18 20:54:19 +02:00
c29e09ccc0
hmmmmm 2025-06-18 20:47:13 +02:00
a5e54e9f5b
lol; 2025-06-18 20:26:48 +02:00
9b1e3d98f0
uiuiiu 2025-06-18 20:12:54 +02:00
5cb66c5012
booty 2025-06-18 20:12:17 +02:00
c99032044d
booty 2025-06-18 19:28:14 +02:00
6bb49e8f79
fixie 2025-06-18 16:56:49 +02:00
300c8cafc9 added a /api/food_orders/latest/ endpoint to recieve the latest food order 2025-06-17 21:26:05 +02:00
ee32852789
api api 2025-06-15 13:57:17 +02:00
9d2f0204e3
locki 2025-06-15 13:03:18 +02:00
a948e992d8
remove trash 2025-06-15 12:58:37 +02:00
f731b46f86
fixi 2025-06-15 12:57:18 +02:00
937973e8e9
upidati 2025-06-15 12:55:41 +02:00
77 changed files with 3107 additions and 3656 deletions

4
.env.dev Normal file
View file

@ -0,0 +1,4 @@
###> symfony/framework-bundle ###
APP_SECRET=11c8937d48993fb3aee1a476413161f5
###< symfony/framework-bundle ###

View file

@ -3,7 +3,7 @@ jobs:
ls:
runs-on: docker
container:
image: git.php.fail/lubiana/container/php:8.4.3-ci
image: git.php.fail/lubiana/container/php:8.4.8-ci
steps:
- name: Manually checkout
env:

View file

@ -6,7 +6,7 @@ jobs:
ls:
runs-on: docker
container:
image: git.php.fail/lubiana/container/php:8.4.3-ci
image: git.php.fail/lubiana/container/php:8.4.8-ci
steps:
- name: Manually checkout
env:

View file

@ -4,7 +4,7 @@ jobs:
ls:
runs-on: docker
container:
image: git.php.fail/lubiana/container/php:8.4.3-ci
image: git.php.fail/lubiana/container/php:8.4.8-ci
steps:
- name: Manually checkout
env:

9
.gitignore vendored
View file

@ -18,6 +18,15 @@
.DS_Store
###> squizlabs/php_codesniffer ###
/.phpcs-cache
/phpcs.xml
###< squizlabs/php_codesniffer ###
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/

View file

@ -1,156 +0,0 @@
# Development Guidelines
This document provides guidelines and instructions for developing and maintaining the Futtern project.
## Build/Configuration Instructions
### Environment Setup
1. **PHP Requirements**: The project requires PHP 8.4 or higher with the following extensions:
- ctype
- iconv
2. **Composer**: Install dependencies using Composer:
```bash
composer install
```
3. **Environment Configuration**: Copy `.env` to `.env.local` and adjust the settings as needed for your local environment.
### Development Server
Start the Symfony development server:
```bash
symfony server:start
```
### Building Assets
The project uses Tailwind CSS via the symfonycasts/tailwind-bundle:
```bash
# Install importmap assets
symfony console importmap:install
```
## Deployment
The project includes several deployment scripts:
1. **Prepare for Deployment**:
```bash
./deploy/prepare-deploy.sh
```
This script creates a clean copy of the application with only production dependencies.
2. **Local Deployment**:
```bash
./deploy/local-deploy.sh
```
This script deploys the application to a remote server, backing up the database and restarting the service.
3. **Update After Deployment**:
```bash
./deploy/update.sh
```
This script is run on the remote server to clear cache, warm up cache, and run database migrations.
## Testing Information
### Testing Framework
The project uses Pest PHP, a testing framework built on top of PHPUnit, for testing. Tests are organized into:
- **Feature Tests**: For testing controllers and API endpoints
- **Unit Tests**: For testing individual components
### Running Tests
Run all tests:
```bash
composer test
# or
./vendor/bin/pest
```
Run specific tests:
```bash
./vendor/bin/pest tests/Unit/ExampleTest.php
```
Run tests in parallel:
```bash
./vendor/bin/pest --parallel
```
### Creating Tests
1. **Unit Tests**: Create files in the `tests/Unit` directory.
2. **Feature Tests**:
- Controller tests: Create files in `tests/Feature/Controller`
- API tests: Create files in `tests/Feature/Api`
### Example Test
Here's a simple example of a Pest PHP test:
```php
<?php declare(strict_types=1);
test('example test', function (): void {
expect(true)->toBeTrue();
expect(1 + 1)->toBe(2);
expect('hello world')->toContain('world');
});
test('array operations', function (): void {
$array = [1, 2, 3];
expect($array)->toHaveCount(3);
expect($array)->toContain(2);
expect($array[0])->toBe(1);
});
```
### Test Base Classes
- `DbWebTest`: Base class for controller tests
- `DbApiTestCase`: Base class for API tests
## Code Quality and Style
### Code Style
The project uses Easy Coding Standard (ECS) for code style checking:
```bash
composer lint
# or
./vendor/bin/ecs --fix
```
The code style is defined in `ecs.php` and includes:
- Custom rules from `Lubiana\CodeQuality\LubiSetList::ECS`
- All classes should be declared as final
### Code Refactoring
The project uses Rector for automated code refactoring:
```bash
./vendor/bin/rector
```
The refactoring rules are defined in `rector.php` and include:
- Custom rules from `Lubiana\CodeQuality\LubiSetList::RECTOR`
- StaticClosureRector is skipped in the tests directory
### Development Workflow
1. Make changes to the code
2. Run tests to ensure functionality: `composer test`
3. Fix code style issues: `composer lint`
4. Commit and push changes
5. Deploy using the deployment scripts
## Debugging
- Use the Symfony Web Profiler in development mode
- Check logs in `var/log/`
- For API debugging, use the API Platform documentation at `/api`

View file

@ -1,3 +0,0 @@
workers:
tailwind:
cmd: ['symfony', 'console', 'tailwind:build', '--watch']

View file

@ -4,5 +4,21 @@
* 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

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

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

@ -0,0 +1,568 @@
/* 🌈✨ 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

@ -7,47 +7,50 @@
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/doctrine-orm": "^4.0.0",
"api-platform/symfony": "4.1.4",
"doctrine/dbal": "^4.1",
"doctrine/doctrine-bundle": "^2.12",
"doctrine/doctrine-migrations-bundle": "^3.3.1",
"doctrine/orm": "^3.2.1",
"api-platform/doctrine-orm": "*",
"api-platform/symfony": "*",
"doctrine/dbal": "^4.2.3",
"doctrine/doctrine-bundle": "^2.14.1",
"doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.4.0",
"nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.6",
"phpdocumentor/reflection-docblock": "^5.6.2",
"phpstan/phpdoc-parser": "^1.33",
"psr/clock": "^1.0",
"symfony/asset": "7.2.*",
"symfony/console": "7.1.*",
"symfony/dotenv": "7.1.*",
"symfony/expression-language": "7.2.*",
"symfony/flex": "^2.4.6",
"symfony/form": "7.1.*",
"symfony/framework-bundle": "7.1.*",
"symfony/property-access": "7.2.*",
"symfony/property-info": "7.2.*",
"symfony/runtime": "7.1.*",
"symfony/security-bundle": "7.2.*",
"symfony/security-csrf": "7.1.*",
"symfony/serializer": "7.2.*",
"symfony/twig-bundle": "7.1.*",
"symfony/uid": "7.1.*",
"symfony/validator": "7.1.*",
"symfony/yaml": "7.1.*",
"symfonycasts/tailwind-bundle": "^0.10.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.*",
"symfony/security-bundle": "7.3.*",
"symfony/security-csrf": "7.3.*",
"symfony/serializer": "7.3.*",
"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"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.0",
"liip/test-fixtures-bundle": "^3.2",
"doctrine/doctrine-fixtures-bundle": "^4.1",
"liip/test-fixtures-bundle": "^3.4",
"lubiana/code-quality": "^1.7.2",
"pestphp/pest": "^3.6",
"symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*",
"symfony/http-client": "7.2.*",
"symfony/maker-bundle": "^1.60",
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*",
"symplify/config-transformer": "^12.3.4"
"pestphp/pest": "^3.8.2",
"symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/maker-bundle": "^1.63",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*",
"symplify/config-transformer": "^12.4.0"
},
"config": {
"allow-plugins": {
@ -81,7 +84,8 @@
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*",
"symfony/polyfill-php83": "*"
"symfony/polyfill-php83": "*",
"symfony/polyfill-php84": "*"
},
"scripts": {
"auto-scripts": {
@ -108,7 +112,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.2.*"
"require": "7.3.*"
}
}
}

2977
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,10 +8,11 @@ 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 Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle;
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
return [
FrameworkBundle::class => [
@ -43,14 +44,17 @@ return [
NelmioCorsBundle::class => [
'all' => true,
],
ApiPlatformBundle::class => [
'all' => true,
],
LiipTestFixturesBundle::class => [
'dev' => true,
'test' => true,
],
SymfonycastsTailwindBundle::class => [
ApiPlatformBundle::class => [
'all' => true,
],
TwigExtraBundle::class => [
'all' => true,
],
MonologBundle::class => [
'all' => true,
],
];

View file

@ -4,10 +4,8 @@ use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigura
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('api_platform', [
'title' => 'Futtern API',
'title' => 'Hello API Platform',
'version' => '1.0.0',
'show_webby' => false,
'enable_swagger' => true,
'defaults' => [
'stateless' => true,
'cache_headers' => [

20
config/packages/csrf.php Normal file
View file

@ -0,0 +1,20 @@
<?php declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('framework', [
'form' => [
'csrf_protection' => [
'token_id' => 'submit',
],
],
'csrf_protection' => [
'stateless_token_ids' => [
'submit',
'authenticate',
'logout',
],
],
]);
};

View file

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

@ -3,7 +3,9 @@
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('symfonycasts_tailwind', [
'binary_version' => 'v3.4.17',
$containerConfigurator->extension('framework', [
'property_info' => [
'with_constructor_extractor' => true,
],
]);
};

View file

@ -1,17 +1,16 @@
<?php declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Config\TwigConfig;
return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('twig', [
'file_name_pattern' => '*.twig',
'globals' => [
'favicon' => '@App\Service\Favicon',
],
]);
return static function (
ContainerConfigurator $containerConfigurator,
TwigConfig $twig,
): void {
if ($containerConfigurator->env() === 'test') {
$containerConfigurator->extension('twig', [
'strict_variables' => true,
]);
$twig->strictVariables(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="public bin config migrations src templates composer.json composer.lock symfony.lock .env"
pathsToCopy="assets public bin config migrations src templates composer.json composer.lock symfony.lock .env importmap.php"
for path in $pathsToCopy
do

View file

@ -5,4 +5,7 @@ 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

@ -16,4 +16,17 @@ return [
'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

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

@ -1,20 +0,0 @@
<?php declare(strict_types=1);
use PhpStyler\Config;
use PhpStyler\Files;
use PhpStyler\Styler;
return new Config(
styler: new Styler(lineLen: 79),
files: new Files(
__DIR__ . '/bin',
__DIR__ . '/public',
__DIR__ . '/src',
__DIR__ . '/config',
__DIR__ . '/tests',
__DIR__ . '/php-styler.php',
__DIR__ . '/ecs.php',
__DIR__ . '/rector.php',
),
cache: __DIR__ . '/.php-styler.cache',
);

View file

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

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3,48 +3,76 @@
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Repository\FoodOrderRepository;
use App\State\OpenFoodOrderProvider;
use App\State\LatestOrderProvider;
use App\State\OpenOrdersProvider;
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;
#[ORM\Entity(repositoryClass: FoodOrderRepository::class)]
#[GetCollection(
uriTemplate: '/food_orders/open',
provider: OpenFoodOrderProvider::class,
#[ApiResource(
operations: [
new GetCollection(
uriTemplate: 'food_orders/open',
description: 'Get only open orders',
provider: OpenOrdersProvider::class,
),
new Get(
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,
new Post,
new Put,
new Delete,
]
)]
#[ApiResource]
#[ORM\Entity(repositoryClass: FoodOrderRepository::class)]
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;
@ -57,6 +85,7 @@ class FoodOrder
return $this->id;
}
#[Groups(['food_order:read'])]
public function getCreatedAt(): DateTimeImmutable
{
return $this->id->getDateTime();

View file

@ -7,20 +7,27 @@ 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;
/**
@ -36,13 +43,23 @@ 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;
@ -150,4 +167,19 @@ 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

@ -12,8 +12,8 @@ use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Uid\Ulid;
#[ORM\Entity(repositoryClass: MenuItemRepository::class)]
#[ApiResource]
#[ORM\Entity(repositoryClass: MenuItemRepository::class)]
class MenuItem
{
#[ORM\Column(length: 255)]

View file

@ -7,16 +7,19 @@ 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;
#[ORM\Entity(repositoryClass: OrderItemRepository::class)]
#[ApiResource]
#[ORM\Entity(repositoryClass: OrderItemRepository::class)]
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')]
@ -25,11 +28,13 @@ 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(
@ -37,6 +42,7 @@ 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,6 +17,7 @@ final class FoodVendorType extends AbstractType
->add('name')
->add('menuLink')
->add('phone')
->add('emojis')
;
}

View file

@ -62,4 +62,13 @@ final class FoodOrderRepository extends ServiceEntityRepository
->getQuery()
->getResult();
}
public function findLatestOrder(): FoodOrder|null
{
return $this->createQueryBuilder('alias')
->orderBy('alias.id', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
}

View file

@ -0,0 +1,22 @@
<?php declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\FoodOrder;
use App\Repository\FoodOrderRepository;
use Override;
final readonly class LatestOrderProvider implements ProviderInterface
{
public function __construct(
private FoodOrderRepository $repository
) {}
#[Override]
public function provide(Operation $operation, array $uriVariables = [], array $context = []): FoodOrder|null
{
return $this->repository->findLatestOrder();
}
}

View file

@ -4,17 +4,21 @@ namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\FoodOrder;
use App\Repository\FoodOrderRepository;
use Override;
final readonly class OpenFoodOrderProvider implements ProviderInterface
final readonly class OpenOrdersProvider implements ProviderInterface
{
public function __construct(
private FoodOrderRepository $repository
) {}
/**
* @return FoodOrder[]
*/
#[Override]
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
return $this->repository->findOpenOrders();
}

View file

@ -1,11 +1,11 @@
{
"api-platform/symfony": {
"version": "4.0",
"api-platform/core": {
"version": "4.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "4.0",
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
"ref": "cb9e6b8ceb9b62f32d41fc8ad72a25d5bd674c6d"
},
"files": [
"config/packages/api_platform.yaml",
@ -13,13 +13,22 @@
"src/ApiResource/.gitignore"
]
},
"doctrine/doctrine-bundle": {
"version": "2.12",
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.12",
"ref": "7266981c201efbbe02ae53c87f8bb378e3f825ae"
"version": "1.0",
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
}
},
"doctrine/doctrine-bundle": {
"version": "2.14",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "620b57f496f2e599a6015a9fa222c2ee0a32adcb"
},
"files": [
"config/packages/doctrine.yaml",
@ -28,7 +37,7 @@
]
},
"doctrine/doctrine-fixtures-bundle": {
"version": "4.0",
"version": "4.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -40,7 +49,7 @@
]
},
"doctrine/doctrine-migrations-bundle": {
"version": "3.3",
"version": "3.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -53,7 +62,7 @@
]
},
"liip/test-fixtures-bundle": {
"version": "3.2.1"
"version": "3.4.0"
},
"nelmio/cors-bundle": {
"version": "2.5",
@ -68,39 +77,46 @@
]
},
"phpstan/phpstan": {
"version": "1.11",
"version": "1.12",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.0",
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
}
},
"files": [
"phpstan.dist.neon"
]
},
"phpunit/phpunit": {
"version": "11.4",
"version": "11.5",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "9.6",
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
"version": "11.1",
"ref": "c6658a60fc9d594805370eacdf542c3d6b5c0869"
},
"files": [
".env.test",
"phpunit.xml.dist",
"tests/bootstrap.php"
"tests/bootstrap.php",
"bin/phpunit"
]
},
"squizlabs/php_codesniffer": {
"version": "3.10",
"version": "3.13",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "3.6",
"ref": "1019e5c08d4821cb9b77f4891f8e9c31ff20ac6f"
}
},
"files": [
"phpcs.xml.dist"
]
},
"symfony/asset-mapper": {
"version": "7.2",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -115,7 +131,7 @@
]
},
"symfony/console": {
"version": "7.1",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -127,24 +143,37 @@
]
},
"symfony/flex": {
"version": "2.4",
"version": "2.7",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
"version": "2.4",
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
},
"files": [
".env"
".env",
".env.dev"
]
},
"symfony/form": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
},
"files": [
"config/packages/csrf.yaml"
]
},
"symfony/framework-bundle": {
"version": "7.1",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.0",
"ref": "6356c19b9ae08e7763e4ba2d9ae63043efc75db5"
"version": "7.3",
"ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9"
},
"files": [
"config/packages/cache.yaml",
@ -154,11 +183,12 @@
"config/services.yaml",
"public/index.php",
"src/Controller/.gitignore",
"src/Kernel.php"
"src/Kernel.php",
".editorconfig"
]
},
"symfony/maker-bundle": {
"version": "1.60",
"version": "1.63",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -166,8 +196,32 @@
"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": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
},
"files": [
"config/packages/property_info.yaml"
]
},
"symfony/routing": {
"version": "7.1",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -180,7 +234,7 @@
]
},
"symfony/security-bundle": {
"version": "7.2",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -193,7 +247,7 @@
]
},
"symfony/twig-bundle": {
"version": "7.1",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -206,7 +260,7 @@
]
},
"symfony/uid": {
"version": "7.1",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -215,7 +269,7 @@
}
},
"symfony/validator": {
"version": "7.1",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -227,28 +281,19 @@
]
},
"symfony/web-profiler-bundle": {
"version": "7.1",
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.1",
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
"version": "7.3",
"ref": "5b2b543e13942495c0003f67780cb4448af9e606"
},
"files": [
"config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml"
]
},
"symfonycasts/tailwind-bundle": {
"version": "0.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "0.8",
"ref": "4ea7c9488fdce8943520daf3fdc31e93e5b59c64"
},
"files": [
"config/packages/symfonycasts_tailwind.yaml"
]
"twig/extra-bundle": {
"version": "v3.21.0"
}
}

View file

@ -1,12 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./assets/**/*.js",
"./templates/**/*.html.twig",
],
darkMode: 'media', // Use 'media' to respect the user's system preference
theme: {
extend: {},
},
plugins: [],
}

View file

@ -1,24 +1,4 @@
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
{% for field in form %}
{% if field.vars.name != '_token' %}
<div class="space-y-2">
{{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900 dark:text-gray-100'}}) }}
<div>
{{ form_widget(field, {'attr': {'class': 'block w-full rounded-md border-0 py-1.5 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:ring-2 focus:ring-inset focus:ring-indigo-600 dark:focus:ring-indigo-500 sm:text-sm sm:leading-6'}}) }}
</div>
{% if field.vars.errors|length > 0 %}
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
{{ form_errors(field) }}
</div>
{% endif %}
{% if field.vars.help is defined and field.vars.help %}
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ field.vars.help }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="mt-6 flex items-center justify-end gap-x-6">
<a href="{{ app.request.headers.get('referer', '/') }}" class="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100">Cancel</a>
<button type="submit" class="rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500">{{ button_label|default('Save') }}</button>
</div>
{{ 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_end(form) }}

View file

@ -1,93 +1,65 @@
<!DOCTYPE html>
<html class="h-full bg-gray-100 dark:bg-gray-900">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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 %} - Futtern</title>
<link rel="icon" type="image/svg+xml" href="{{ favicon }}" />
<script src="/static/js/htmx.min.js"></script>
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{% endblock %}
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" type="image/svg+xml"
href="{{ favicon }}" />
{% block javascripts %}
{{ importmap() }}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
</head>
<body class="h-full dark:text-gray-100">
<div class="min-h-full">
<nav class="bg-indigo-600 dark:bg-indigo-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
<span class="text-white text-xl font-bold">Futtern</span>
</div>
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-4">
<a href="{{ path('app_food_order_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">Orders</a>
<a href="{{ path('app_food_vendor_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">Vendors</a>
<a href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">Create Issue</a>
<a href="/api" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">API</a>
</div>
</div>
</div>
<div class="hidden md:block">
<div class="ml-4 flex items-center md:ml-6">
<div class="text-white">
Hello {{ app.request.cookies.get('username', 'nobody') }} -
<a href="{{ path('username') }}" class="text-indigo-200 hover:text-white">change name</a>
</div>
</div>
</div>
<div class="-mr-2 flex md:hidden">
<!-- Mobile menu button -->
<button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 dark:bg-indigo-800 p-2 text-indigo-200 hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600 dark:focus:ring-offset-indigo-800" aria-controls="mobile-menu" aria-expanded="false">
<span class="absolute -inset-0.5"></span>
<span class="sr-only">Open main menu</span>
<!-- Menu open: "hidden", Menu closed: "block" -->
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
</div>
</div>
</div>
<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>
<!-- Mobile menu, show/hide based on menu state. -->
<div class="md:hidden" id="mobile-menu">
<div class="space-y-1 px-2 pb-3 pt-2 sm:px-3">
<a href="{{ path('app_food_order_index') }}" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Orders</a>
<a href="{{ path('app_food_vendor_index') }}" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Vendors</a>
<a href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Create Issue</a>
<a href="/api" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">API</a>
</div>
<div class="border-t border-indigo-700 pb-3 pt-4">
<div class="flex items-center px-5">
<div class="text-base font-medium text-white">
Hello {{ app.request.cookies.get('username', 'nobody') }} -
<a href="{{ path('username') }}" class="text-indigo-200 hover:text-white">change name</a>
</div>
<input type="radio" class="btn-check" name="mode" id="enhanced" autocomplete="off">
<label class="btn btn-outline-primary" for="enhanced">
Enhanced
<span class="emoji-normal">✨</span>
<span class="emoji-enhanced">✨🍑🍆</span>
<span class="emoji-bonkers">✨🍑🍆💦👅💋😈🏳️‍🌈</span>
</label>
<input type="radio" class="btn-check" name="mode" id="bonkers" autocomplete="off">
<label class="btn btn-outline-primary" for="bonkers">
Bonkers
<span class="emoji-normal">💦</span>
<span class="emoji-enhanced">💦🍑🍆</span>
<span class="emoji-bonkers">💦🍑🍆👅💋😈🏳️‍🌈✨🥵😳🤤😍🥴💕💗💘💝💞💟💌💏💑</span>
</label>
</div>
<span class="navbar-text">
Hello {{ app.request.cookies.get('username', 'nobody') }} - <a class="text-light" href="{{ path('username') }}">change name</a>
</span>
</div>
</div>
</nav>
<header class="bg-white dark:bg-gray-800 shadow">
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">{% block header %}{% endblock %}</h1>
</div>
</header>
<main>
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
{% block body %}{% endblock %}
</div>
</div>
</main>
</div>
</header>
<main class="container pb-5">
{% block body %}{% endblock %}
</main>
</body>
</html>

View file

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

View file

@ -1,69 +1,48 @@
{% extends 'base.html.twig' %}
{% block title %}Food Orders{% endblock %}
{% block header %}Food Orders{% endblock %}
{% block title %}FoodOrder index{% endblock %}
{% block body %}
<div class="sm:flex sm:items-center sm:justify-between mb-6">
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-500 dark:text-gray-400">
Manage your food orders and create new ones.
</p>
</div>
<div class="mt-4 flex sm:ml-4 sm:mt-0">
<button
<h1 class="mb-4">FoodOrder index</h1>
<div class="mb-3">
<button
class="btn btn-primary"
hx-get="{{ path('app_food_order_new') }}"
hx-trigger="click"
hx-target="#new-order-form"
class="block rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500"
>Create new order</button>
</div>
hx-target="closest div"
>Create new</button>
</div>
<div id="new-order-form" class="mb-8"></div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10 sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">Created By</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Vendor</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Created At</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Closed At</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
{% for food_order in food_orders %}
{{ include('food_order/table_row.html.twig') }}
{% endfor %}
{% if food_orders|length < 10 %}
<tr>
<td colspan="5" class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-500 dark:text-gray-400 sm:pl-6">
Check the <a href="{{ path('app_food_order_archive') }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">archive</a>
for older orders
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-center gap-x-6">
<hr>
<table class="table table-striped table-hover">
<thead class="table-light">
<tr>
<th>CreatedBy</th>
<th>Vendor</th>
<th>CreatedAt</th>
<th>ClosedAt</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for food_order in food_orders %}
{{ include('food_order/table_row.html.twig') }}
{% endfor %}
{% if food_orders|length < 10 %}
<tr>
<td colspan="5" class="text-center text-muted">
check the <a href="{{ path('app_food_order_archive') }}">archive</a>
for older orders
</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="d-flex gap-2">
{% if prev_page > 0 %}
<a href="{{ path('app_food_order_archive', {'page': prev_page}) }}" class="rounded-md bg-white dark:bg-gray-700 px-3.5 py-2.5 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">Previous Page</a>
<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 href="{{ path('app_food_order_archive', {'page': next_page}) }}" class="rounded-md bg-white dark:bg-gray-700 px-3.5 py-2.5 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">Next Page</a>
<a class="btn btn-outline-secondary btn-sm" href="{{ path('app_food_order_archive', {'page': next_page}) }}">next page</a>
{% endif %}
</div>
{% endblock %}

View file

@ -1,11 +1,4 @@
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">Create New Order</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>Fill out the form below to create a new food order.</p>
</div>
<div class="mt-5">
{{ include('_form.html.twig') }}
</div>
</div>
<div class="mb-4">
{{ include('_form.html.twig') }}
</div>

View file

@ -1,101 +1,73 @@
{% extends 'base.html.twig' %}
{% block title %}Order Details{% endblock %}
{% block header %}Order Details{% endblock %}
{% block title %}FoodOrder{% endblock %}
{% block body %}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg mb-8">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Order Information</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">Details about the food order from {{ food_order.foodVendor.name }}.</p>
</div>
<div class="border-t border-gray-200 dark:border-gray-700">
<dl>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Vendor</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.foodVendor.name }}</dd>
</div>
<div class="bg-white dark:bg-gray-800 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Vendor Phone</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.foodVendor.phone }}</dd>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Created By</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.createdBy }}</dd>
</div>
<div class="bg-white dark:bg-gray-800 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Created At</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</dd>
</div>
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Closed At</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : 'Not closed yet' }}</dd>
</div>
</dl>
</div>
</div>
<h1 class="mb-4">FoodOrder</h1>
<div class="flex space-x-4 mb-8">
<a href="{{ path('app_food_order_index') }}" class="inline-flex items-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
Back to list
</a>
<table class="table table-bordered table-striped w-auto">
<tbody>
<tr>
<th>Vendor</th>
<td>{{ food_order.foodVendor.name }}</td>
</tr>
<tr>
<th>Vendorphone</th>
<td>{{ food_order.foodVendor.phone }}</td>
</tr>
<tr>
<th>Created By</th>
<td>{{ food_order.createdBy }}</td>
</tr>
<tr>
<th>CreatedAt</th>
<td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
</tr>
<tr>
<th>ClosedAt</th>
<td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
</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 href="{{ path('app_food_order_open', {'id': food_order.id}) }}" class="inline-flex items-center rounded-md bg-green-600 dark:bg-green-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 dark:hover:bg-green-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600 dark:focus-visible:outline-green-500">
Reopen Order
</a>
<a class="btn btn-warning" href="{{ path('app_food_order_open', {'id': food_order.id}) }}">reopen</a>
{% else %}
<a href="{{ path('app_food_order_close', {'id': food_order.id}) }}" class="inline-flex items-center rounded-md bg-red-600 dark:bg-red-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 dark:hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:focus-visible:outline-red-500">
Close Order
</a>
<a class="btn btn-success" href="{{ path('app_food_order_close', {'id': food_order.id}) }}">close</a>
{% endif %}
</div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg mb-8">
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Order Items</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">Items included in this order.</p>
</div>
<div>
<a href="{{ path('app_order_item_new', {'foodOrder': food_order.id}) }}" class="inline-flex items-center rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500">
Add New Item
</a>
</div>
</div>
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">#</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Username</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Item Name</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Extras</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
{% for item in food_order.orderItemsSortedByName %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-6">{{ loop.index }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ item.createdBy }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ item.name }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ item.extras }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
{% if(food_order.isClosed) %}
<span class="text-gray-400 dark:text-gray-500">Order closed</span>
{% else %}
<a href="{{ path('app_order_item_edit', {'id': item.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-2">Edit</a>
<a href="{{ path('app_order_item_copy', {'id': item.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-2">Copy</a>
<a href="{{ path('app_order_item_delete', {'id': item.id}) }}" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">Remove</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<h2 class="mt-5">Items</h2>
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Index</th>
<th>username</th>
<th>name</th>
<th>extras</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for item in food_order.orderItemsSortedByName %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.createdBy }}</td>
<td>{{ item.name }}</td>
<td>{{ item.extras }}</td>
<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>
{% 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>
{% endblock %}

View file

@ -1,9 +1,9 @@
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-6">{{ food_order.createdBy }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ food_order.foodVendor.name }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<a href="{{ path('app_food_order_show', {'id': food_order.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">View<span class="sr-only">, order from {{ food_order.createdBy }}</span></a>
<tr>
<td>{{ food_order.createdBy }}</td>
<td>{{ food_order.foodVendor.name }}</td>
<td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
<td>
<a class="btn btn-sm btn-outline-info" href="{{ path('app_food_order_show', {'id': food_order.id}) }}">show</a>
</td>
</tr>

View file

@ -1,24 +1,4 @@
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
{% for field in form %}
{% if field.vars.name != '_token' %}
<div class="space-y-2">
{{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900'}}) }}
<div>
{{ form_widget(field, {'attr': {'class': 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6'}}) }}
</div>
{% if field.vars.errors|length > 0 %}
<div class="mt-1 text-sm text-red-600">
{{ form_errors(field) }}
</div>
{% endif %}
{% if field.vars.help is defined and field.vars.help %}
<p class="mt-1 text-sm text-gray-500">{{ field.vars.help }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="mt-6 flex items-center justify-end gap-x-6">
<a href="{{ app.request.headers.get('referer', '/') }}" class="text-sm font-semibold leading-6 text-gray-900">Cancel</a>
<button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">{{ button_label|default('Save') }}</button>
</div>
{{ 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_end(form) }}

View file

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

View file

@ -1,54 +1,33 @@
{% extends 'base.html.twig' %}
{% block title %}Food Vendors{% endblock %}
{% block header %}Food Vendors{% endblock %}
{% block title %}FoodVendor index{% endblock %}
{% block body %}
<div class="sm:flex sm:items-center sm:justify-between mb-6">
<div class="min-w-0 flex-1">
<p class="text-sm text-gray-500 dark:text-gray-400">
Manage your food vendors and create new ones.
</p>
</div>
<div class="mt-4 flex sm:ml-4 sm:mt-0">
<a href="{{ path('app_food_vendor_new') }}" class="block rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500">
Create new vendor
</a>
</div>
</div>
<h1 class="mb-4">FoodVendor index</h1>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10 sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">Name</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
{% for food_vendor in food_vendors %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-6">{{ food_vendor.name }}</td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<a href="{{ path('app_food_vendor_show', {'id': food_vendor.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-2">View</a>
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">Edit</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-500 dark:text-gray-400 sm:pl-6">No vendors found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<table class="table table-striped table-hover">
<thead class="table-light">
<tr>
<th>Name</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for food_vendor in food_vendors %}
<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>
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="text-center text-muted">no records found</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="btn btn-primary" href="{{ path('app_food_vendor_new') }}">Create new</a>
{% endblock %}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Save') }}</button>
{{ 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_end(form) }}

View file

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

View file

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

View file

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

View file

@ -3,9 +3,9 @@
{% block title %}MenuItem{% endblock %}
{% block body %}
<h1>MenuItem</h1>
<h1 class="mb-4">MenuItem</h1>
<table class="table">
<table class="table table-bordered w-auto">
<tbody>
<tr>
<th>Id</th>
@ -25,9 +25,9 @@
<tr>
<th>Aliases</th>
<td>
<ul>
<ul class="list-group list-group-flush">
{% for alias in menu_item.aliases %}
<li>{{ alias.name }}</li>
<li class="list-group-item">{{ alias.name }}</li>
{% endfor %}
</ul>
</td>
@ -36,9 +36,9 @@
</tbody>
</table>
<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') }}
<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>
{% endblock %}

View file

@ -1,4 +1,4 @@
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Save') }}</button>
{{ 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_end(form) }}

View file

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

View file

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

View file

@ -2,15 +2,10 @@
{% block title %}Tell me your name{% endblock %}
{% block header %}Tell me your name{% endblock %}
{% block body %}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="max-w-xl text-sm text-gray-500 dark:text-gray-400 mb-6">
<p>By submitting the form, you agree that your username will be stored as a cookie.</p>
</div>
{{ include('_form.html.twig') }}
</div>
<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>
{% endblock %}

View file

@ -1,26 +0,0 @@
<?php declare(strict_types=1);
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use App\DataFixtures\AppFixtures;
use Doctrine\ORM\EntityManagerInterface;
use Liip\TestFixturesBundle\Services\DatabaseToolCollection;
use Override;
abstract class DbApiTestCase extends ApiTestCase
{
protected EntityManagerInterface $manager;
protected Client $client;
#[Override]
protected function setUp(): void
{
parent::setUp();
$this->client = static::createClient();
$this->manager = static::getContainer()->get('doctrine')->getManager();
$toolKit = self::getContainer()->get(DatabaseToolCollection::class)->get();
$toolKit->loadFixtures([AppFixtures::class]);
}
}

View file

@ -1,48 +0,0 @@
<?php declare(strict_types=1);
test('orders', function (): void {
$response = $this->client->request('GET', '/api/food_orders');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/api/contexts/FoodOrder',
'@id' => '/api/food_orders',
'@type' => 'Collection',
'totalItems' => 1,
]);
$array = $response->toArray();
expect($array['member'][0]['orderItems'])->toHaveCount(10);
});
test('open orders', function (): void {
$response = $this->client->request('GET', '/api/food_orders/open');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/api/contexts/FoodOrder',
'@id' => '/api/food_orders/open',
'@type' => 'Collection',
]);
// Get the response content and verify that all returned orders are open
$array = $response->toArray();
// If we have no items, we should still have a successful response
if ($array['totalItems'] === 0) {
return;
}
// For each order in the response, check that it is not closed
foreach ($array['member'] as $order) {
// An order is considered open if either:
// 1. closedAt is null or
// 2. closedAt is in the future
$closedAt = isset($order['closedAt']) ? new DateTimeImmutable($order['closedAt']) : null;
$now = new DateTimeImmutable;
// Assert that the order is open (not closed)
$isOpen = (! $closedAt instanceof DateTimeImmutable || $closedAt > $now);
expect($isOpen)
->toBeTrue('Order should be open but is closed');
}
});

View file

@ -44,7 +44,7 @@ describe(FoodOrderController::class, function (): void {
$crawler = $this->client->request('GET', "{$this->path}list");
$this->assertResponseStatusCodeSame(200);
$this->assertPageTitleContains('Food Orders');
$this->assertPageTitleContains('FoodOrder index');
$this->assertCount(
1,
$crawler->filter('td')
@ -99,29 +99,17 @@ describe(FoodOrderController::class, function (): void {
$crawler = $this->client->request('GET', "{$this->path}{$order->getId()}");
$this->assertResponseIsSuccessful();
// Find all tables and get the last one (order items table)
$tables = $crawler->filter('table');
$lastTable = $tables->eq($tables->count() - 1);
// Get the item names from the table rows
$rows = $lastTable->filter('tbody tr');
$tdContent = $rows->eq(0)
->filter('td')
->eq(2)
->text();
$tdContent = $crawler->filter(
'table.table-hover tbody tr:nth-child(1) td:nth-child(3)'
)->text();
$this->assertEquals('A', $tdContent);
$tdContent = $rows->eq(1)
->filter('td')
->eq(2)
->text();
$tdContent = $crawler->filter(
'table.table-hover tbody tr:nth-child(2) td:nth-child(3)'
)->text();
$this->assertEquals('B', $tdContent);
$tdContent = $rows->eq(2)
->filter('td')
->eq(2)
->text();
$tdContent = $crawler->filter(
'table.table-hover tbody tr:nth-child(3) td:nth-child(3)'
)->text();
$this->assertEquals('C', $tdContent);
});
@ -136,12 +124,12 @@ describe(FoodOrderController::class, function (): void {
$this->manager->flush();
$crawler = $this->client->request('GET', "{$this->path}list");
$this->assertResponseStatusCodeSame(200);
$this->assertPageTitleContains('Food Orders');
$this->assertPageTitleContains('FoodOrder index');
$this->assertElementContainsCount(
$crawler,
'td',
1,
'for older orders'
'older orders'
);
$this->assertElementContainsCount(
$crawler,
@ -151,7 +139,7 @@ describe(FoodOrderController::class, function (): void {
);
});
test('paginatedFirstPage', function (int $page, int $prevPage, int $nextPage, int $items = 20): void {
test('paginatedFirstPage', function (int $page, int $prevPage, int $nextPage, int $items = 10): void {
foreach (range(1, 35) as $i) {
$order = new FoodOrder($this->generateOldUlid());
$order->setFoodVendor($this->vendor);
@ -162,7 +150,7 @@ describe(FoodOrderController::class, function (): void {
$this->manager->flush();
$crawler = $this->client->request('GET', "{$this->path}list/archive/{$page}");
$this->assertResponseStatusCodeSame(200);
$this->assertPageTitleContains('Food Orders');
$this->assertPageTitleContains('FoodOrder index');
$this->assertElementContainsCount(
$crawler,
'td',
@ -172,14 +160,14 @@ describe(FoodOrderController::class, function (): void {
if ($prevPage > 0) {
$prevPage = $prevPage === 1 ? '' : "/{$prevPage}";
$node = $crawler->filter('a')
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'Previous Page')
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'previous page')
->first();
$target = $node->attr('href');
$this->assertTrue(str_ends_with((string) $target, $prevPage));
}
if ($prevPage > 3) {
$node = $crawler->filter('a')
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'Next Page')
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'next page')
->first();
$target = $node->attr('href');
$this->assertTrue(str_ends_with((string) $target, "/{$nextPage}"));
@ -190,7 +178,7 @@ describe(FoodOrderController::class, function (): void {
[1, 0, 2],
[2, 1, 3],
[3, 2, 4],
[4, 3, 0, 10],
[4, 3, 0, 5],
]
);
@ -242,7 +230,6 @@ describe(FoodOrderController::class, function (): void {
$openOrder = $this->repository->find($order->getId());
$this->assertTrue($openOrder->isClosed());
});
})
->covers(
FoodOrderController::class,

View file

@ -29,7 +29,7 @@ describe(FoodVendorController::class, function (): void {
$this->client->request('GET', $this->path);
$this->assertResponseStatusCodeSame(200);
$this->assertPageTitleContains('Food Vendors');
$this->assertPageTitleContains('FoodVendor index');
});
test('new', function (): void {
@ -119,7 +119,7 @@ describe(FoodVendorController::class, function (): void {
)->text();
$this->assertSame('My Title', $nameNode);
$itemNodes = $crawler->filter('li');
$itemNodes = $crawler->filter('ul.list-group li.list-group-item');
$this->assertCount(4, $itemNodes);
});

View file

@ -136,9 +136,10 @@ describe(MenuItemController::class, function (): void {
$this->assertTrue($menuItem->isDeleted());
$crawler = $this->client->request('GET', '/order/item/new/' . $order->getId());
$count = $crawler->filter('form')
$count = $crawler->filter('body > main:nth-child(2) > div:nth-child(5)')
->children()
->count();
$this->assertSame(1, $count);
$this->assertSame(2, $count);
$this->assertResponseIsSuccessful();

View file

@ -60,7 +60,8 @@ describe(OrderItemController::class, function (): void {
sprintf('%snew/%s', $this->path, $this->order->getId())
);
$children = $crawler->filter('form');
$children = $crawler->filter('body > main:nth-child(2) > div:nth-child(5)')
->children();
$this->assertCount(1, $children);

View file

@ -1,6 +1,5 @@
<?php declare(strict_types=1);
use App\Tests\DbApiTestCase;
use App\Tests\DbWebTest;
/*
@ -16,8 +15,6 @@ use App\Tests\DbWebTest;
pest()
->extends(DbWebTest::class)->in('Feature/Controller/*.php');
pest()
->extends(DbApiTestCase::class)->in('Feature/Api/*.php');
/*
|--------------------------------------------------------------------------

View file

@ -5,6 +5,7 @@ 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;
@ -20,6 +21,17 @@ 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);

View file

@ -1,21 +0,0 @@
<?php declare(strict_types=1);
test('example test', function (): void {
// This is a simple example test
expect(true)
->toBeTrue();
expect(1 + 1)
->toBe(2);
expect('hello world')
->toContain('world');
});
test('array operations', function (): void {
$array = [1, 2, 3];
expect($array)
->toHaveCount(3);
expect($array)
->toContain(2);
expect($array[0])->toBe(1);
});