bootystrap #87

Open
lubiana wants to merge 12 commits from bootystrap into main
28 changed files with 9876 additions and 1338 deletions

View file

@ -2,36 +2,38 @@ on: [pull_request]
jobs:
ls:
runs-on: docker
container:
image: git.php.fail/lubiana/container/php:8.4.1-ci
steps:
- name: Manually checkout
env:
REPO: '${{ github.repository }}'
TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GIT_SERVER: 'git.hannover.ccc.de'
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: https://github.com/shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, ctype, iconv, pdo, dom, fileinfo, intl, zip
tools: composer:v2
- name: Get Composer cache directory
id: composer-cache
run: |
git clone --branch $GITHUB_HEAD_REF https://${TOKEN}@${GIT_SERVER}/${REPO}.git .
git fetch
git checkout $GITHUB_HEAD_REF
- name: composer install
env:
COMPOSER_CACHE_DIR: /opt/hostedtoolcache/.composer/cache/files
run: |
mkdir -p ${{ env.COMPOSER_CACHE_DIR }}
composer install
- name: lint
run: composer lint
- name: test
run: composer test
- name: GIT commit and push all changed files
env:
CI_COMMIT_MESSAGE: Continuous Integration Fixes
CI_COMMIT_AUTHOR: Continuous Integration
run: |
if [[ -n "$(git status -s)" ]]; then
git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}"
git config --global user.email "gitbot@users.noreply.php.fail"
git commit -am "${{ env.CI_COMMIT_MESSAGE }}"
git push
fi
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: php-8.4-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
php-8.4-composer-
- name: Clean up invalid Composer GitHub token
run: composer config --global --unset github-oauth.github.com || true
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-scripts
- name: Run Symfony auto-scripts
run: composer run-script post-install-cmd
- name: Run PestPHP tests (parallel)
run: composer test

View file

@ -5,9 +5,12 @@ on:
jobs:
ls:
runs-on: docker
container:
image: git.php.fail/lubiana/container/php:8.4.1-ci
steps:
- name: setup php
uses: https://github.com/shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: 'composer'
- name: Manually checkout
env:
REPO: '${{ github.repository }}'
@ -38,4 +41,4 @@ jobs:
git config --global user.email "gitbot@users.noreply.php.fail"
git commit -am "${{ env.CI_COMMIT_MESSAGE }}"
git push
fi
fi

View file

@ -4,8 +4,13 @@ jobs:
ls:
runs-on: docker
container:
image: git.php.fail/lubiana/container/php:8.4.1-ci
image: php:8.4-cli
steps:
- name: Install dependencies
run: |
apt-get update
apt-get install -y git openssh-client rsync
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- name: Manually checkout
env:
REPO: '${{ github.repository }}'
@ -43,7 +48,3 @@ jobs:
rsync -avz --delete deploy/ ${USERNAME}@${HOST}:${TARGETDIR} --exclude=var
# run update script
ssh ${USERNAME}@${HOST} /home/c3h-futtern/futtern/update.sh

7
.gitignore vendored
View file

@ -17,3 +17,10 @@
###< phpunit/phpunit ###
.DS_Store
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###

111
.junie/guidelines.md Normal file
View file

@ -0,0 +1,111 @@
# Development Guidelines for Futtern
This document provides essential information for developers working on the Futtern project.
## Build and Configuration
### PHP Requirements
- PHP 8.4 or higher
- Required extensions: ctype, iconv
### Database Configuration
- Default: SQLite (`var/data.db`)
- Can be configured for MySQL or PostgreSQL by updating the `DATABASE_URL` in `.env` or `.env.local`
### Installation Steps
1. Clone the repository
2. Install PHP dependencies:
```bash
composer install
```
3. Install Node.js dependencies:
```bash
npm install
```
4. Copy static assets to Encore directory:
```bash
mkdir -p assets/css assets/js
cp public/static/css/water.min.css assets/css/
cp public/static/js/htmx.min.js assets/js/
```
5. Build frontend assets:
```bash
npm run dev # For development
npm run build # For production
```
6. Create database schema:
```bash
php bin/console doctrine:schema:create
```
7. Load fixtures (optional):
```bash
php bin/console doctrine:fixtures:load
```
### Development Workflow
- Run `npm run watch` to automatically rebuild assets when files change
- Use Symfony CLI for local development: `symfony server:start`
## Testing
### Test Configuration
- Tests use Pest PHP (built on PHPUnit)
- Test environment is configured in `.env.test`
- Tests are organized in `tests/Feature` and `tests/Unit` directories
### Running Tests
- Run all tests: `composer test`
- Run specific tests: `composer test -- --filter=TestName`
- Run tests in parallel: `composer test` (parallel is the default)
### Test Types
1. **Unit Tests** (`tests/Unit/`):
- Test individual classes in isolation
- Extend `TestCase` or use standalone Pest tests
- Example:
```php
test('Favicon returns SVG string', function (): void {
$favicon = new Favicon();
$result = (string)$favicon;
expect($result)
->toBeString()
->toContain('data:image/svg+xml')
->toContain('transform=\'rotate(')
->toContain('viewBox=\'0 0 512 512\'');
})
->covers(Favicon::class);
```
2. **Feature Tests** (`tests/Feature/`):
- Controller tests: Extend `DbWebTest` for web controllers
- API tests: Extend `DbApiTestCase` for API endpoints
- Each test gets a fresh database
### Creating New Tests
1. Determine the appropriate test type (Unit or Feature)
2. Create a new test file in the corresponding directory
3. Use Pest's `test()` function to define tests
4. Use `expect()` for assertions
5. Use `->covers()` to specify which classes are covered by the test
## Code Quality
### Code Style
- The project uses Easy Coding Standard (ECS) for code style checking
- Configuration is in `ecs.php`
- All classes should be marked as `final` unless they're meant to be extended
- Run code style checks: `composer lint`
### Code Quality Tools
- Rector is used for automated code refactoring
- Configuration is in `rector.php`
- Run code quality checks: `composer lint`
### Development Practices
- Use strict types in all PHP files: `<?php declare(strict_types=1);`
- Follow PSR-12 coding standards
- Use type hints for all method parameters and return types
- Use constructor property promotion where appropriate
- Use readonly properties where appropriate
- Use PHP 8.4 features where appropriate

35
README.md Normal file
View file

@ -0,0 +1,35 @@
# Asset Management with Webpack Encore
This project uses Webpack Encore for asset management including JavaScript and CSS minification.
## Setup
Before using the application, you need to set up the front-end assets:
1. Install Node.js dependencies:
```
npm install
```
2. Copy the existing static assets to the Encore assets directory:
```
mkdir -p assets/css assets/js
cp public/static/css/water.min.css assets/css/
cp public/static/js/htmx.min.js assets/js/
```
3. Build the assets:
For development:
```
npm run dev
```
For production (with minification):
```
npm run build
```
## Development
During development, you can use the watch command to automatically rebuild assets when files change:

11
assets/app.js Normal file
View file

@ -0,0 +1,11 @@
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';
import * as bootstrap from 'bootstrap';

4
assets/styles/app.scss Normal file
View file

@ -0,0 +1,4 @@
/* Import water.min.css */
@import "bootstrap/scss/bootstrap.scss";
/* You can add your other custom styles here */

View file

@ -7,21 +7,21 @@
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/doctrine-orm": "^4.0.0",
"api-platform/doctrine-orm": "^4.1.12",
"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",
"doctrine/dbal": "^4.2.3",
"doctrine/doctrine-bundle": "^2.14",
"doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.3.3",
"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/flex": "^2.6.0",
"symfony/form": "7.1.*",
"symfony/framework-bundle": "7.1.*",
"symfony/property-access": "7.2.*",
@ -33,20 +33,21 @@
"symfony/twig-bundle": "7.1.*",
"symfony/uid": "7.1.*",
"symfony/validator": "7.1.*",
"symfony/webpack-encore-bundle": "^2.2",
"symfony/yaml": "7.1.*"
},
"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.3",
"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/maker-bundle": "^1.63",
"symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*",
"symplify/config-transformer": "^12.3.4"
"symplify/config-transformer": "^12.4.0"
},
"config": {
"allow-plugins": {

625
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,17 +1,13 @@
<?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',
],
]);
if ($containerConfigurator->env() === 'test') {
$containerConfigurator->extension('twig', [
'strict_variables' => true,
]);
return static function (TwigConfig $twigConfig, ContainerConfigurator $c): void {
$twigConfig->fileNamePattern('*.twig');
$twigConfig->global('favicon', '@App\Service\Favicon');
$twigConfig->formThemes(['bootstrap_5_layout.html.twig']);
if ($c->env() === 'test') {
$twigConfig->strictVariables(true);
}
};

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Config\WebpackEncoreConfig;
return static function (WebpackEncoreConfig $webpackEncoreConfig, ContainerConfigurator $containerConfigurator): void {
$webpackEncoreConfig->outputPath('%kernel.project_dir%/public/build');
$webpackEncoreConfig->scriptAttributes('defer', true);
$webpackEncoreConfig->cache('%kernel.debug%');
};

View file

@ -8,13 +8,15 @@ fi
mkdir $TARGETDIR
cd $TARGETDIR || return
pathsToCopy="public bin config migrations src templates composer.json composer.lock symfony.lock .env"
pathsToCopy="public bin config migrations src templates composer.json composer.lock symfony.lock .env assets package.json package.lock webpack.config.js"
for path in $pathsToCopy
do
cp -r ../../"$path" ./
done
npm install
npm run build
APP_ENV=prod composer install --no-dev -a
rm -rf ./var/cache

9051
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@symfony/webpack-encore": "^4.5.0",
"autoprefixer": "^10.4.21",
"core-js": "^3.36.0",
"css-loader": "^7.1.2",
"file-loader": "^6.2.0",
"postcss-loader": "^8.1.1",
"regenerator-runtime": "^0.14.1",
"sass": "^1.87.0",
"sass-loader": "^14.2.1",
"style-loader": "^4.0.0",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-notifier": "^1.15.0"
},
"license": "UNLICENSED",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"browserslist": [
"> 0.5%",
"last 2 versions",
"Firefox ESR",
"not dead"
],
"dependencies": {
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.5"
}
}

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

@ -223,5 +223,21 @@
"config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml"
]
},
"symfony/webpack-encore-bundle": {
"version": "2.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.0",
"ref": "9ef5412a4a2a8415aca3a3f2b4edd3866aab9a19"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/webpack_encore.yaml",
"package.json",
"webpack.config.js"
]
}
}

View file

@ -2,44 +2,59 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0000ff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#222222" media="(prefers-color-scheme: dark)">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" type="image/svg+xml"
href="{{ favicon }}" />
{% set currentDate = "now"|date("d") %}
{% if currentDate % 4 == 0 %}
<link rel="stylesheet" href="/static/css/new.min.css">
{% elseif currentDate % 4 == 1 %}
<link rel="stylesheet" href="/static/css/simple.min.css">
{% elseif currentDate % 4 == 2 %}
<link rel="stylesheet" href="/static/css/water.min.css">
{% else %}
<link rel="stylesheet" href="/static/css/fieber.css">
{% endif %}
<style>
label{
display: block;
}
</style>
{# Use Encore assets for CSS and JS #}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
<script src="/static/js/htmx.min.js"></script>
</head>
<body>
<header>
<p>Hello {{ app.request.cookies.get('username', 'nobody') }} - <a href="{{ path('username') }}">change name</a></p>
<nav>
<a href="{{ path('app_food_order_index') }}">Orders</a> /
<a href="{{ path('app_food_vendor_index') }}">Vendors</a> /
<a
href="https://git.hannover.ccc.de/lubiana/futtern/issues/new"
target="_blank"
>Create Issue</a> /
<a href="/api">API</a>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<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="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank">Create Issue</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/api" target="_blank">API</a>
</li>
</ul>
<div class="d-flex">
<span class="navbar-text me-2">
Hello {{ app.request.cookies.get('username', 'nobody') }}
<a href="{{ path('username') }}">Change name</a>
</span>
</div>
</div>
</div>
</nav>
</header>
<main>
{% block body %}{% endblock %}
<main class="py-4">
<div class="container">
{% block body %}{% endblock %}
</div>
</main>
</body>
</html>

View file

@ -9,6 +9,7 @@
hx-get="{{ path('app_food_order_new') }}"
hx-trigger="click"
hx-target="closest div"
class="btn btn-primary"
>Create new</button>
</div>
<hr>

View file

@ -1,2 +1,10 @@
{{ include('_form.html.twig') }}
<div class="card shadow-sm mb-4">
<div class="card-body">
{{ include('_form.html.twig') }}
</div>
</div>
<a href="{{ path('app_food_order_index') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to list
</a>

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') }}
<hr />
<hr class="my-4" />
{% if food_order.foodVendor.menuLink != '' %}
<a href="{{ food_order.foodVendor.menuLink }}" target="_blank">
External link to Menu
<a href="{{ food_order.foodVendor.menuLink }}" class="btn btn-info mb-3" target="_blank">
<i class="bi bi-link-45deg"></i> External link to Menu
</a>
{% endif %}
<div>
<b>click a button to select a given menuitem</b>
<div class="mb-3">
<b>Click a button to select a given menu item:</b>
</div>
<div>
<div class="mb-4">
{% for menuItem in menuItems %}
<a href="#" data-menu-item>{{ menuItem.name }}</a>
<a href="#" class="btn btn-outline-secondary me-2 mb-2" data-menu-item>{{ menuItem.name }}</a>
{% endfor %}
</div>
<hr />
<hr class="my-4" />
<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}) }}">
<i class="bi bi-arrow-left"></i> Back to list
</a>
<script>
document.querySelectorAll('[data-menu-item]').forEach(function(element) {

View file

@ -0,0 +1,20 @@
<?php declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Service\Favicon;
use function expect;
use function test;
test('Favicon returns SVG string', function (): void {
$favicon = new Favicon();
$result = (string)$favicon;
expect($result)
->toBeString()
->toContain('data:image/svg+xml')
->toContain('transform=\'rotate(')
->toContain('viewBox=\'0 0 512 512\'');
})
->covers(Favicon::class);

84
webpack.config.js Normal file
View file

@ -0,0 +1,84 @@
const Encore = require('@symfony/webpack-encore');
// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or subdirectory deploy
//.setManifestKeyPrefix('build/')
/*
* ENTRY CONFIG
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
*/
.addEntry('app', './assets/app.js')
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
//.enableStimulusBridge('./assets/controllers.json')
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()
/*
* FEATURE CONFIG
*
* Enable & configure other features below. For a full
* list of features, see:
* https://symfony.com/doc/current/frontend.html#adding-more-features
*/
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())
// configure Babel
// .configureBabel((config) => {
// config.plugins.push('@babel/plugin-proposal-class-properties');
// })
// enables and configure @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = '3.36';
})
// enables Sass/SCSS support
.enableSassLoader()
// uncomment if you use TypeScript
//.enableTypeScriptLoader()
// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())
// Enable CSS and JS minification in production
.enablePostCssLoader((options) => {
options.postcssOptions = {
plugins: [
require('autoprefixer')
]
};
})
// When in production, minify CSS and JS automatically
// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
;
module.exports = Encore.getWebpackConfig();