wip: continue implementation

This commit is contained in:
Björn Fromme
2024-11-27 11:31:48 +01:00
parent b5d52b0053
commit 84ab6446ea
81 changed files with 9127 additions and 898 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: myep-next
type: php
docroot: public
php_version: "8.2"
webserver_type: nginx-fpm
webserver_type: apache-fpm
xdebug_enabled: false
additional_hostnames: []
additional_fqdns: []
+6 -5
View File
@@ -1,4 +1,3 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
@@ -19,7 +18,9 @@
/phpunit.xml
###< symfony/phpunit-bridge ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
+8 -7
View File
@@ -1,9 +1,10 @@
import './bootstrap.js';
/*
* Welcome to your app's main JavaScript file!
*
* This file will be included onto the page via the importmap() Twig function,
* which should already be in your base.html.twig.
*/
import '@picocss/pico';
import './styles/app.css';
import htmx from 'htmx.org'
window.htmx = htmx
htmx.config.includeIndicatorStyles = false
htmx.config.historyEnabled = false
htmx.config.historyCacheSize = 0
htmx.config.allowScriptTags = false
htmx.config.withCredentials = true
+6 -11
View File
@@ -1,12 +1,7 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
import { startStimulusApp } from '@symfony/stimulus-bridge';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);
import htmx from 'htmx.org'
window.htmx = htmx
htmx.config.includeIndicatorStyles = false
htmx.config.historyEnabled = false
htmx.config.historyCacheSize = 0
htmx.config.allowScriptTags = false
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.[jt]sx?$/
));
@@ -0,0 +1,5 @@
import { Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['field']
}
@@ -0,0 +1,21 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static classes = [ 'hidden' ]
static targets = [ 'container' ]
static values = { show: Array }
toggle(event) {
let fieldType = event.target.type
let formValue = event.target.value
let showContainer
if ('checkbox' !== fieldType && 'radio' !== fieldType) {
showContainer = this.showValue.indexOf(formValue) !== -1
} else {
let toggleValue = event.target.dataset.selectToggleValue
showContainer = true === event.target.checked && this.showValue.includes(toggleValue)
}
this.containerTarget.classList.toggle(this.hiddenClass, false === showContainer)
}
}
+39
View File
@@ -0,0 +1,39 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static classes = [ 'closed' ]
static targets = [ 'toggle', 'icon' ]
static values = {
open: {
type: Boolean,
default: true,
},
}
initialize() {
this.target = this.hasToggleTarget ? this.toggleTarget : this.element
}
toggle() {
this.openValue = !this.openValue
}
close() {
this.openValue = false
}
open() {
this.openValue = true
}
openValueChanged(open) {
this.target.classList.toggle(this.closedClass, false === open)
if (false === this.hasIconTarget) {
return
}
this.iconTarget.classList.toggle('rotate-90', true === open)
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
+236
View File
@@ -0,0 +1,236 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="icon-dots" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
</symbol>
<symbol id="icon-feedback" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
</symbol>
<symbol id="icon-login" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</symbol>
<symbol id="icon-logout" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</symbol>
<symbol id="icon-alert" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</symbol>
<symbol id="icon-close" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</symbol>
<symbol id="icon-back" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
</symbol>
<symbol id="icon-list" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</symbol>
<symbol id="icon-menu" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</symbol>
<symbol id="icon-edit" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</symbol>
<symbol id="icon-plus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</symbol>
<symbol id="icon-minus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</symbol>
<symbol id="icon-search" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</symbol>
<symbol id="icon-user" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</symbol>
<symbol id="icon-users" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</symbol>
<symbol id="icon-delete" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</symbol>
<symbol id="icon-check" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</symbol>
<symbol id="icon-upload" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</symbol>
<symbol id="icon-download" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</symbol>
<symbol id="icon-sort" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</symbol>
<symbol id="icon-arrow-left" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
</symbol>
<symbol id="icon-arrow-right" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</symbol>
<symbol id="icon-document" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</symbol>
<symbol id="icon-filter" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
</symbol>
<symbol id="icon-refresh" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</symbol>
<symbol id="icon-settings" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</symbol>
<symbol id="icon-at" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" d="M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 10-2.636 6.364M16.5 12V8.25" />
</symbol>
<symbol id="icon-mail" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
</symbol>
<symbol id="icon-house" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205l3 1m1.5.5l-1.5-.5M6.75 7.364V3h-3v18m3-13.636l10.5-3.819" />
</symbol>
<symbol id="icon-phone" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
</symbol>
<symbol id="icon-mobile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</symbol>
<symbol id="icon-calendar" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" />
</symbol>
<symbol id="icon-hand" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.05 4.575a1.575 1.575 0 10-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 013.15 0v1.5m-3.15 0l.075 5.925m3.075.75V4.575m0 0a1.575 1.575 0 013.15 0V15M6.9 7.575a1.575 1.575 0 10-3.15 0v8.175a6.75 6.75 0 006.75 6.75h2.018a5.25 5.25 0 003.712-1.538l1.732-1.732a5.25 5.25 0 001.538-3.712l.003-2.024a.668.668 0 01.198-.471 1.575 1.575 0 10-2.228-2.228 3.818 3.818 0 00-1.12 2.687M6.9 7.575V12m6.27 4.318A4.49 4.49 0 0116.35 15m.002 0h-.002" />
</symbol>
<symbol id="icon-copy" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6" />
</symbol>
<symbol id="icon-photo" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</symbol>
<symbol id="icon-print" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-locked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</symbol>
<symbol id="icon-unlocked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</symbol>
<symbol id="icon-mask" fill="currentColor" stroke="none" viewBox="0 0 576 512">
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M288 64C39.52 64 0 182.1 0 273.5C0 379.5 78.8 448 176 448c27.33 0 51.21-6.516 66.11-36.79l19.93-40.5C268.3 358.6 278.1 352.4 288 352.1c9.9 .3711 19.7 6.501 25.97 18.63l19.93 40.5C348.8 441.5 372.7 448 400 448c97.2 0 176-68.51 176-174.5C576 182.1 536.5 64 288 64zM400 400c-18.12 0-19.56-2.924-23.04-9.986l-20.35-41.33c-13.87-26.86-38.85-43.53-66.82-44.57L288 304L286.2 304.1c-27.96 1.049-52.94 17.71-67.24 45.41l-19.93 40.49C195.6 397.1 194.1 400 176 400c-75.36 0-128-52.04-128-126.5C48 229.3 48 112 288 112s240 117.3 240 161.5C528 347.1 475.4 400 400 400zM160 192C124.7 192 96 220.7 96 256s28.65 64 64 64c35.35 0 64-28.65 64-64S195.3 192 160 192zM416 192c-35.35 0-64 28.65-64 64s28.65 64 64 64c35.35 0 64-28.65 64-64S451.3 192 416 192z"/>
</symbol>
<symbol id="icon-excel" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM229.1 233.3L192 280.9L154.9 233.3C146.8 222.8 131.8 220.9 121.3 229.1C110.8 237.2 108.9 252.3 117.1 262.8L161.6 320l-44.53 57.25c-8.156 10.47-6.25 25.56 4.188 33.69C125.7 414.3 130.8 416 135.1 416c7.156 0 14.25-3.188 18.97-9.25L192 359.1l37.06 47.65C233.8 412.8 240.9 416 248 416c5.125 0 10.31-1.656 14.72-5.062c10.44-8.125 12.34-23.22 4.188-33.69L222.4 320l44.53-57.25c8.156-10.47 6.25-25.56-4.188-33.69C252.2 220.9 237.2 222.8 229.1 233.3z"/>
</symbol>
<symbol id="icon-pdf" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM202 286.1c.877-2.688 1.74-5.398 2.582-8.145c1.434-5.762 7.488-31.54 7.488-52.47C212.1 207 197.1 192 178.6 192C160.1 192 145.1 207 145.1 225.5c0 .2969 .1641 28.81 13.85 62.3c-7.035 19.36-15.57 38.8-25.41 57.93c-21.49 10.11-39.24 22.23-52.8 36.07c-6.234 6.438-9.367 14.74-9.367 24.72c0 18.45 15.01 33.46 33.46 33.46c10.8 0 20.98-5.227 27.22-13.98c7.322-10.28 18.38-26.9 30.47-48.95c15.8-6.352 33.88-11.72 53.88-16c13.55 9.578 28.9 17.29 45.71 22.95c4.527 1.551 9.402 2.348 14.43 2.348c20.26 0 36.13-16.19 36.13-36.86c0-20.33-16.54-36.87-36.87-36.87h-3.705c-2.727 .125-20.51 1.141-45.37 5.367C216.9 308.9 208.6 298.3 202 286.1zM110.2 410.4c-3.273 4.688-12.03 2.777-12.03-5.312c0-1.754 .6289-3.43 1.729-4.555c9.02-9.219 19.94-17.05 31.85-23.72C122.3 393.1 114.3 404.7 110.2 410.4zM178.6 218.8c3.693 0 6.703 3.008 6.703 6.703c0 15.21-4.109 34.84-5.746 42.1C172.1 245 171.9 227.2 171.9 225.5C171.9 221.8 174.9 218.8 178.6 218.8zM162.3 348.3c6.611-13.48 13.22-28.46 19.38-44.7c6.389 10.92 14.56 21.86 24.96 31.97C192.6 338.8 177.4 342.9 162.3 348.3zM272.4 339.5h3.352c5.539 0 10.05 4.5 10.05 10.79c0 5.129-4.176 9.32-9.32 9.32c-2.029 0-4.059-.3164-5.852-.9414c-12.33-4.137-23.11-9.32-32.54-15.19C258.3 340.3 272.1 339.5 272.4 339.5z"/>
</symbol>
<symbol id="icon-file" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M365.3 93.38l-74.63-74.64C278.6 6.743 262.3 0 245.4 0L64-.0001c-35.35 0-64 28.65-64 64l.0065 384c0 35.35 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM320 464H64.02c-8.836 0-15.1-7.163-16-15.1L48 64.13c-.0004-8.837 7.163-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1v288C336 456.8 328.8 464 320 464z"/>
</symbol>
<symbol id="icon-sigma" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M335.1 136l.0005-56H83.59l165 158.7C253.3 243.2 255.1 249.5 255.1 256s-2.656 12.78-7.375 17.31L83.59 432h252.4l-.0005-56c0-13.25 10.75-24 24-24C373.2 352 384 362.8 384 376v80c0 13.25-10.75 24-24 24H23.99c-9.782 0-18.59-5.938-22.25-15.03s-1.438-19.47 5.625-26.28L197.4 256L7.364 73.31C.3015 66.5-1.917 56.13 1.739 47.03S14.21 32 23.99 32h336C373.2 32 384 42.75 384 56v80C384 149.3 373.2 160 359.1 160C346.7 160 335.1 149.3 335.1 136z"/>
</symbol>
<symbol id="icon-percent" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M376.1 71.03c-9.375-9.375-24.56-9.375-33.94 0l-336 336c-9.375 9.375-9.375 24.56 0 33.94C11.72 445.7 17.84 448 24 448s12.28-2.344 16.97-7.031l336-336C386.3 95.59 386.3 80.41 376.1 71.03zM64 176c26.51 0 48-21.49 48-48S90.51 80 64 80C37.49 80 16 101.5 16 128S37.49 176 64 176zM320 336c-26.51 0-48 21.49-48 48s21.49 48 48 48c26.51 0 48-21.49 48-48S346.5 336 320 336z"/>
</symbol>
<symbol id="icon-eye" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</symbol>
<symbol id="icon-profile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33" />
</symbol>
<symbol id="icon-chart" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</symbol>
<symbol id="icon-info" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</symbol>
<symbol id="icon-star" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
</symbol>
<symbol id="icon-cancel" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</symbol>
<symbol id="icon-bell" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</symbol>
<symbol id="icon-folder" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-power" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
</symbol>
<symbol id="icon-no-symbol" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636" />
</symbol>
<symbol id="icon-bus" xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24" stroke="currentColor" fill="none">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M18 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 17h-2v-11a1 1 0 0 1 1 -1h14a5 7 0 0 1 5 7v5h-2m-4 0h-8" />
<path d="M16 5l1.5 7l4.5 0" />
<path d="M2 10l15 0" />
<path d="M7 5l0 5" />
<path d="M12 5l0 5" />
</symbol>
<symbol id="icon-ski" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M380.7 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM2.7 268.9c6.1-11.8 20.6-16.3 32.4-10.2L239.8 365l49.6-74.5-82.7-87.9c-9.5-10.1-14.4-22.4-15.1-34.9l87.7 42 49.4 52.5c12.8 13.6 14.5 34.1 4.2 49.6l-50.2 75.4 135.8 70.5c13.6 7.1 29.8 7.2 43.6 .3l15.2-7.6c11.9-5.9 26.3-1.1 32.2 10.7s1.1 26.3-10.7 32.2l-15.2 7.6c-27.5 13.7-59.9 13.5-87.2-.7L12.9 301.3C1.2 295.2-3.4 280.7 2.7 268.9zM118.9 65.6L137 74.2l8.7-17.4c4-7.9 13.6-11.1 21.5-7.2s11.1 13.6 7.2 21.5l-8.5 16.9 55.5 26.6c1.7-.9 3.5-1.7 5.4-2.5l80.9-32.4c32.4-13 68.6 6.5 75.6 40.7l12.3 59.7 62.9 30.1c12 5.7 17 20 11.3 32s-20 17-32 11.3l-54.2-25.9c-1-.3-1.9-.6-2.8-1l-229.1-110-9.2 18.4c-4 7.9-13.6 11.1-21.5 7.2s-11.1-13.6-7.2-21.5l9-18-17.6-8.4c-8-3.8-11.3-13.4-7.5-21.3s13.4-11.3 21.3-7.5zm217.3 64.7c-1-4.9-6.2-7.7-10.8-5.8l-45.7 18.3 65.5 31.5-9-43.9z"/>
</symbol>
<symbol id="icon-snowboard" fill="currentColor" stroke="nonw" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M204.3 3c11.6-6.4 26.2-2.3 32.6 9.3l31.9 57.4c1.3 2.3 3.6 3.8 6.2 4.1l33.8 3.4c24.1 2.4 46.9 12 65.4 27.6L503.5 213.6c10.1 8.5 11.4 23.7 2.9 33.8s-23.7 11.4-33.8 2.9l-68.2-57.5-82.3 43.9 24.7 18.9c24.9 19 34.6 51.9 24.1 81.4l-28.2 79c-1.4 3.8-3.6 7-6.3 9.6l93.3 35.7c4.6 1.8 9.4 2.6 14.3 2.6H472c13.3 0 24 10.7 24 24s-10.7 24-24 24H443.8c-10.8 0-21.4-2-31.5-5.8L60.1 371.3c-11.5-4.4-22-11.2-30.8-20L7 329c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.4 22.4c4 4 8.7 7.1 14 9.1l19.4 7.4c-2.9-11.4 2.9-23.4 14-28.1L192 272l0-68.2c0-21.2 12-40.6 31-50.1L284.4 123l-14.1-1.4c-18.3-1.8-34.5-12.5-43.4-28.5L195 35.7c-6.4-11.6-2.3-26.2 9.3-32.6zm91.8 407.2c-.3-3.4 .1-6.9 1.3-10.3l28.2-79c3.5-9.8 .3-20.8-8-27.1L240 234.4l0 42.9c0 16.1-9.7 30.7-24.6 36.9L134 348.2l162.1 62.1zM259.6 189l20.5 15.7 84.6-45.1-17.2-14.5L259.6 189zM384 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0z"/>
</symbol>
<symbol id="icon-hourglass" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M24 0C10.7 0 0 10.7 0 24S10.7 48 24 48h8V67c0 40.3 16 79 44.5 107.5L158.1 256 76.5 337.5C48 366 32 404.7 32 445v19H24c-13.3 0-24 10.7-24 24s10.7 24 24 24H360c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8V445c0-40.3-16-79-44.5-107.5L225.9 256l81.5-81.5C336 146 352 107.3 352 67V48h8c13.3 0 24-10.7 24-24s-10.7-24-24-24H24zM192 289.9l81.5 81.5C293 391 304 417.4 304 445v19H80V445c0-27.6 11-54 30.5-73.5L192 289.9zm0-67.9l-81.5-81.5C91 121 80 94.6 80 67V48H304V67c0 27.6-11 54-30.5 73.5L192 222.1z"/>
</symbol>
<symbol id="icon-flag-a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#fff" d="M640 480H0V0h640z"/><path fill="#ed2939" d="M640 480H0V320h640zm0-319.9H0V.1h640z"/></g></symbol>
<symbol id="icon-flag-ch" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#d52b1e" d="M0 0h640v480H0z"/><g fill="#fff"><path d="M170 195h300v90H170z"/><path d="M275 90h90v300h-90z"/></g></g></symbol>
<symbol id="icon-flag-d" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><path fill="#ffce00" d="M0 320h640v160H0z"/><path d="M0 0h640v160H0z"/><path fill="#d00" d="M0 160h640v160H0z"/></symbol>
<symbol id="icon-flag-f" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#00267f" d="M0 0h213.3v480H0z"/><path fill="#f31830" d="M426.7 0H640v480H426.7z"/></g></symbol>
<symbol id="icon-flag-i" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#009246" d="M0 0h213.3v480H0z"/><path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/></g></symbol>
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<symbol id="icon-spinner" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="0s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="0s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="-0.9s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="-0.9s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
</g>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 31 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 243 74" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><g><path d="M161.8,41.6c2.2,0 3.8,1.6 3.8,3.8c0,2 -1.3,3.3 -2.8,3.7l3.3,5.8l-1.8,0l-3.1,-5.6l-3.7,0l0,5.6l-1.6,0l0,-13.3l5.9,0Zm-4.2,6.3l4.2,0c1.2,0 2.2,-1 2.2,-2.5c0,-1.6 -1.1,-2.5 -2.6,-2.5l-3.8,0l0,5Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M181.9,41.6l0,1.4l-7.5,0l0,4.8l6.5,0l0,1.4l-6.5,0l0,4.4l7.7,0l0,1.3l-9.3,0l0,-13.3l9.1,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><rect x="189.1" y="41.6" width="1.6" height="13.3" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M197.7,54.1l0.4,-1.3c0.5,0.2 2.3,0.9 4.1,0.9c2.2,0 3.3,-0.9 3.3,-2.2c0,-1.7 -1.7,-2.1 -3.4,-2.6c-2.1,-0.6 -4.3,-1.3 -4.3,-3.9c0,-2.4 1.8,-3.6 4.5,-3.6c1.6,0 3.3,0.5 4.2,0.9l-0.4,1.3c0,0 -2,-0.8 -3.7,-0.8c-1.8,0 -3,0.9 -3,2.2c0,1.6 1.8,2.1 3.4,2.6c2.1,0.6 4.3,1.3 4.3,3.9c0,2.5 -2,3.6 -4.7,3.6c-2.1,0 -3.9,-0.6 -4.7,-1Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M223,41.6l0,1.4l-7.5,0l0,4.8l6.5,0l0,1.4l-6.5,0l0,4.4l7.7,0l0,1.3l-9.3,0l0,-13.3l9.1,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M231.8,41.6l4.2,6.3l2.9,4.5l-0.1,-4.5l0,-6.3l1.6,0l0,13.3l-1.7,0l-4.2,-6.5l-2.8,-4.4l0.1,4.5l0,6.4l-1.6,0l0,-13.3l1.6,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M165.1,60.1l0,1.4l-7.5,0l0,4.8l6.5,0l0,1.4l-6.5,0l0,4.4l7.7,0l0,1.4l-9.3,0l0,-13.4l9.1,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M171.2,60.1l4,11.3l4,-11.3l1.7,0l-4.8,13.3l-1.8,0l-4.8,-13.3l1.7,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M195.1,60.1l0,1.4l-7.5,0l0,4.8l6.5,0l0,1.4l-6.5,0l0,4.4l7.7,0l0,1.4l-9.3,0l0,-13.4l9.1,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M202.6,60.1l4.2,6.3l2.9,4.5l-0.1,-4.5l0,-6.3l1.6,0l0,13.3l-1.7,0l-4.2,-6.5l-2.8,-4.4l0.1,4.5l0,6.4l-1.6,0l0,-13.3l1.6,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M226.9,60.1l0,1.4l-4.5,0l0,11.9l-1.6,0l0,-12l-4.5,0l0,-1.4l10.6,0l0,0.1Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M231,72.5l0.4,-1.3c0.5,0.2 2.3,0.9 4.1,0.9c2.2,0 3.3,-0.9 3.3,-2.2c0,-1.7 -1.7,-2.1 -3.4,-2.6c-2.1,-0.6 -4.3,-1.3 -4.3,-3.9c0,-2.4 1.8,-3.6 4.5,-3.6c1.6,0 3.3,0.5 4.2,0.9l-0.4,1.3c0,0 -2,-0.8 -3.7,-0.8c-1.8,0 -3,0.9 -3,2.2c0,1.6 1.8,2.1 3.4,2.6c2.1,0.6 4.3,1.3 4.3,3.9c0,2.5 -2,3.6 -4.7,3.6c-2,0 -3.9,-0.6 -4.7,-1Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M178.7,0.9l0,5.3l-16.2,0l0,8.3l13.8,0l0,5.4l-13.8,0l0,7.6l16.5,0l0,5.3l-22.9,0l0,-31.9l22.6,0Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M203.6,7.9c0,2.7 -1.6,5.7 -5.2,7.9l5.5,6.4c0,0 1.1,-2 1.1,-4.6l4.8,0c-0.1,4.9 -2.4,8.7 -2.4,8.7l5.9,6.8l-7.1,0l-2.4,-2.8c-2.2,1.9 -5.1,3 -8.4,3c-6,0 -11.4,-3.5 -11.4,-10.1c0,-4.3 2.8,-7 5.7,-9c-1.6,-2.1 -2.8,-3.5 -2.8,-6.3c0,-5.5 4.6,-7.6 8.2,-7.6c3.6,0 8.5,2.1 8.5,7.6Zm-13.7,15.2c0,3 2.2,4.9 5.7,4.9c1.8,0 3.4,-0.5 4.7,-1.6l-7,-8.1c-2,1.1 -3.4,2.6 -3.4,4.8Zm2.9,-14.9c0,1.6 1,2.5 2.1,3.7c1.8,-0.8 3.1,-2.2 3.1,-3.8c0,-1.9 -1.3,-2.6 -2.6,-2.6c-1.7,0 -2.6,1.1 -2.6,2.7Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M233,0.9c5.4,0 9.8,4.5 9.8,10.2c0,6.1 -4.4,10.4 -9.7,10.4l-7.7,0l0,11.4l-6.4,0l0,-32l14,0Zm-7.6,15.2l6.4,0c3,0 4.7,-2.2 4.7,-5c0,-3.5 -2.7,-4.8 -5.2,-4.8l-5.9,0l0,9.8Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M107,0.3c-36.4,5.5 -81.6,19.1 -81.6,38.2c0,17.7 41.8,13.2 41.8,13.2c0,0 -67,22.3 -67,-7c0,-33.2 84.5,-44.4 106.8,-44.4Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M17,73.5c36.4,-5.4 81.6,-19.1 81.6,-38.2c0,-17.7 -41.8,-13.2 -41.8,-13.2c0,0 67,-22.3 67,7c0,33.3 -84.5,44.4 -106.8,44.4Z" style="fill:#f8c62f;fill-rule:nonzero;"/><g><path d="M60.7,34.3l-12.8,4l12.2,0.8l-0.4,-2.4l1,-2.4Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M75.9,35.2l-9.2,2.9l-3.5,9.7l-1.7,-11.3l4,-10.7l1.7,7.9l8.7,1.5Z" style="fill:#f8c62f;fill-rule:nonzero;"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+42
View File
@@ -0,0 +1,42 @@
/* lato-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: url('../fonts/lato-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* lato-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: url('../fonts/lato-v24-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* lato-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: url('../fonts/lato-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* lato-900 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Lato';
font-style: normal;
font-weight: 900;
src: url('../fonts/lato-v24-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
.htmx-indicator{
@apply invisible;
}
.htmx-request .htmx-indicator{
@apply visible;
}
.htmx-request.htmx-indicator{
@apply visible;
}
+5
View File
@@ -0,0 +1,5 @@
@import "components/forms.css";
@import "components/button.css";
@import "components/menu.css";
/*@import "components/toast.css";*/
/*@import "components/tooltip.css";*/
+5 -19
View File
@@ -1,21 +1,7 @@
#content {
position: relative;
}
@import "tailwindcss/base";
@import "_base.css";
#loading-indicator {
display: none;
}
@import "tailwindcss/components";
@import "_components.css";
#loading-indicator.htmx-request {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
}
[disabled] {
cursor: not-allowed;
}
@import "tailwindcss/utilities";
+16
View File
@@ -0,0 +1,16 @@
.btn {
@apply inline-flex space-x-1 justify-center rounded-lg text-sm uppercase font-semibold py-2.5 px-4;
@apply bg-primary text-white hover:bg-primary/80 shadow-md;
}
.btn--small {
@apply text-xs py-1 px-2 rounded-md;
}
.btn--secondary {
@apply bg-secondary text-gray-800 hover:bg-secondary/80;
}
.btn--active {
@apply shadow-none;
}
+4
View File
@@ -0,0 +1,4 @@
label.required:after {
@apply inline-block text-sm;
content: '*';
}
+12
View File
@@ -0,0 +1,12 @@
.menu--main {
@apply flex items-center space-x-2;
}
.menu--main a {
@apply uppercase text-lg font-semibold pb-1 px-2 border-b-2 border-white;
@apply hover:border-primary;
}
.menu--main .current a {
@apply border-primary;
}
+20
View File
@@ -0,0 +1,20 @@
@import "toastify-js/src/toastify.css";
.toastify {
@apply shadow-lg rounded;
}
.toastify--success {
@apply text-gray-100;
background: theme('colors.emerald.600');
}
.toastify--warning {
@apply text-gray-100;
background: theme('colors.red.600');
}
.toastify--info {
@apply text-gray-100;
background: theme('colors.blue.600');
}
+18
View File
@@ -0,0 +1,18 @@
@import "tippy.js/dist/tippy.css";
.tippy-box {
background-color: theme('colors.primary');
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: theme('colors.primary');
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: theme('colors.primary');
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: theme('colors.primary');
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: theme('colors.primary');
}
+9 -6
View File
@@ -12,12 +12,13 @@
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.3",
"knplabs/knp-menu-bundle": "^3.4",
"nelexa/zip": "^4.0",
"nesbot/carbon": "^3.8",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.0",
"symfony/apache-pack": "^1.0",
"symfony/asset": "6.4.*",
"symfony/asset-mapper": "6.4.*",
"symfony/console": "6.4.*",
"symfony/doctrine-messenger": "6.4.*",
"symfony/dotenv": "6.4.*",
@@ -44,9 +45,11 @@
"symfony/uid": "6.4.*",
"symfony/validator": "6.4.*",
"symfony/web-link": "6.4.*",
"symfony/webpack-encore-bundle": "^2.2",
"symfony/yaml": "6.4.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/intl-extra": "^3.13",
"twig/html-extra": "^3.15",
"twig/intl-extra": "*",
"twig/twig": "^2.12|^3.0"
},
"config": {
@@ -79,8 +82,7 @@
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
@@ -94,8 +96,9 @@
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.4.*"
"allow-contrib": true,
"require": "6.4.*",
"docker": true
}
},
"require-dev": {
Generated
+340 -196
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d79e5d8a80d5e98bfd3dadadc9b6182c",
"content-hash": "6960d43262b323bf7a4a4f757087bd3d",
"packages": [
{
"name": "carbonphp/carbon-doctrine-types",
@@ -75,87 +75,6 @@
],
"time": "2023-12-11T17:09:12+00:00"
},
{
"name": "composer/semver",
"version": "3.4.3",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "[email protected]",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "[email protected]",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "[email protected]",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.3"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-09-19T14:15:21+00:00"
},
{
"name": "doctrine/cache",
"version": "2.2.0",
@@ -497,16 +416,16 @@
},
{
"name": "doctrine/doctrine-bundle",
"version": "2.13.0",
"version": "2.13.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git",
"reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563"
"reference": "2740ad8b8739b39ab37d409c972b092f632b025a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca59d84b8e63143ce1aed90cdb333ba329d71563",
"reference": "ca59d84b8e63143ce1aed90cdb333ba329d71563",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/2740ad8b8739b39ab37d409c972b092f632b025a",
"reference": "2740ad8b8739b39ab37d409c972b092f632b025a",
"shasum": ""
},
"require": {
@@ -520,7 +439,7 @@
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
"symfony/deprecation-contracts": "^2.1 || ^3",
"symfony/doctrine-bridge": "^5.4.19 || ^6.0.7 || ^7.0",
"symfony/doctrine-bridge": "^5.4.46 || ^6.4.3 || ^7.0.3",
"symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0",
"symfony/polyfill-php80": "^1.15",
"symfony/service-contracts": "^1.1.1 || ^2.0 || ^3"
@@ -597,7 +516,7 @@
],
"support": {
"issues": "https://github.com/doctrine/DoctrineBundle/issues",
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.0"
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.1"
},
"funding": [
{
@@ -613,7 +532,7 @@
"type": "tidelift"
}
],
"time": "2024-09-01T09:46:40+00:00"
"time": "2024-11-08T23:27:54+00:00"
},
{
"name": "doctrine/doctrine-migrations-bundle",
@@ -1449,6 +1368,144 @@
],
"time": "2023-10-06T06:47:41+00:00"
},
{
"name": "knplabs/knp-menu",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/KnpLabs/KnpMenu.git",
"reference": "c39403f7c427d1b72cc56f38df0a075b4b9191fe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/KnpLabs/KnpMenu/zipball/c39403f7c427d1b72cc56f38df0a075b4b9191fe",
"reference": "c39403f7c427d1b72cc56f38df0a075b4b9191fe",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"conflict": {
"twig/twig": "<1.42.3 || >=2,<2.9"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.6",
"psr/container": "^1.0 || ^2.0",
"symfony/http-foundation": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^7.0",
"symfony/routing": "^5.4 || ^6.0 || ^7.0",
"twig/twig": "^2.16 || ^3.0"
},
"suggest": {
"twig/twig": "for the TwigRenderer and the integration with your templates"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Knp\\Menu\\": "src/Knp/Menu"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "KnpLabs",
"homepage": "https://knplabs.com"
},
{
"name": "Christophe Coevoet",
"email": "[email protected]"
},
{
"name": "The Community",
"homepage": "https://github.com/KnpLabs/KnpMenu/contributors"
}
],
"description": "An object oriented menu library",
"homepage": "https://knplabs.com",
"keywords": [
"menu",
"tree"
],
"support": {
"issues": "https://github.com/KnpLabs/KnpMenu/issues",
"source": "https://github.com/KnpLabs/KnpMenu/tree/v3.5.0"
},
"time": "2024-03-23T15:35:09+00:00"
},
{
"name": "knplabs/knp-menu-bundle",
"version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/KnpLabs/KnpMenuBundle.git",
"reference": "6a1e3e1f4131f9a5a967e36717a1fe680c183637"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/KnpLabs/KnpMenuBundle/zipball/6a1e3e1f4131f9a5a967e36717a1fe680c183637",
"reference": "6a1e3e1f4131f9a5a967e36717a1fe680c183637",
"shasum": ""
},
"require": {
"knplabs/knp-menu": "^3.3",
"php": "^8.1",
"symfony/deprecation-contracts": "^2.5 | ^3.3",
"symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5 | ^11.0.3",
"symfony/expression-language": "^5.4 | ^6.0 | ^7.0",
"symfony/phpunit-bridge": "^6.0 | ^7.0",
"symfony/templating": "^5.4 | ^6.0 | ^7.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Knp\\Bundle\\MenuBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Knplabs",
"homepage": "http://knplabs.com"
},
{
"name": "Christophe Coevoet",
"email": "[email protected]"
},
{
"name": "Symfony Community",
"homepage": "https://github.com/KnpLabs/KnpMenuBundle/contributors"
}
],
"description": "This bundle provides an integration of the KnpMenu library",
"keywords": [
"menu"
],
"support": {
"issues": "https://github.com/KnpLabs/KnpMenuBundle/issues",
"source": "https://github.com/KnpLabs/KnpMenuBundle/tree/v3.4.2"
},
"time": "2024-06-03T08:48:36+00:00"
},
{
"name": "monolog/monolog",
"version": "3.8.0",
@@ -2312,6 +2369,32 @@
},
"time": "2024-09-11T13:17:53+00:00"
},
{
"name": "symfony/apache-pack",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/apache-pack.git",
"reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/apache-pack/zipball/3aa5818d73ad2551281fc58a75afd9ca82622e6c",
"reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c",
"shasum": ""
},
"type": "symfony-pack",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A pack for Apache support in Symfony",
"support": {
"issues": "https://github.com/symfony/apache-pack/issues",
"source": "https://github.com/symfony/apache-pack/tree/master"
},
"time": "2017-12-12T01:46:35+00:00"
},
{
"name": "symfony/asset",
"version": "v6.4.13",
@@ -2381,85 +2464,6 @@
],
"time": "2024-10-25T15:07:50+00:00"
},
{
"name": "symfony/asset-mapper",
"version": "v6.4.15",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset-mapper.git",
"reference": "21fe29b637ddaada739d3d6cd55694a7b95d2343"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset-mapper/zipball/21fe29b637ddaada739d3d6cd55694a7b95d2343",
"reference": "21fe29b637ddaada739d3d6cd55694a7b95d2343",
"shasum": ""
},
"require": {
"composer/semver": "^3.0",
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/filesystem": "^5.4|^6.0|^7.0",
"symfony/http-client": "^6.3|^7.0"
},
"conflict": {
"symfony/framework-bundle": "<6.4"
},
"require-dev": {
"symfony/asset": "^5.4|^6.0|^7.0",
"symfony/browser-kit": "^5.4|^6.0|^7.0",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/event-dispatcher-contracts": "^3.0",
"symfony/finder": "^5.4|^6.0|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/http-foundation": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0",
"symfony/web-link": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\AssetMapper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "[email protected]"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Maps directories of assets & makes them available in a public directory with versioned filenames.",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/asset-mapper/tree/v6.4.15"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-11-09T07:17:14+00:00"
},
{
"name": "symfony/cache",
"version": "v6.4.14",
@@ -7568,6 +7572,78 @@
],
"time": "2024-09-25T14:18:03+00:00"
},
{
"name": "symfony/webpack-encore-bundle",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/webpack-encore-bundle.git",
"reference": "e335394b68a775a9b2bd173a8ba4fd2001f3870c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/e335394b68a775a9b2bd173a8ba4fd2001f3870c",
"reference": "e335394b68a775a9b2bd173a8ba4fd2001f3870c",
"shasum": ""
},
"require": {
"php": ">=8.1.0",
"symfony/asset": "^5.4 || ^6.2 || ^7.0",
"symfony/config": "^5.4 || ^6.2 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.2 || ^7.0",
"symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0"
},
"require-dev": {
"symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0",
"symfony/http-client": "^5.4 || ^6.2 || ^7.0",
"symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0",
"symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0",
"symfony/web-link": "^5.4 || ^6.2 || ^7.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"name": "symfony/webpack-encore",
"url": "https://github.com/symfony/webpack-encore"
}
},
"autoload": {
"psr-4": {
"Symfony\\WebpackEncoreBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration of your Symfony app with Webpack Encore",
"support": {
"issues": "https://github.com/symfony/webpack-encore-bundle/issues",
"source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.2.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-10-02T07:27:19+00:00"
},
{
"name": "symfony/yaml",
"version": "v6.4.13",
@@ -7642,23 +7718,23 @@
},
{
"name": "twig/extra-bundle",
"version": "v3.13.0",
"version": "v3.15.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
"reference": "21a9a7aa9f79d4493bb6fed4eb2794339f9551f5"
"reference": "9746573ca4bc1cd03a767a183faadaf84e0c31fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/21a9a7aa9f79d4493bb6fed4eb2794339f9551f5",
"reference": "21a9a7aa9f79d4493bb6fed4eb2794339f9551f5",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/9746573ca4bc1cd03a767a183faadaf84e0c31fa",
"reference": "9746573ca4bc1cd03a767a183faadaf84e0c31fa",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/framework-bundle": "^5.4|^6.4|^7.0",
"symfony/twig-bundle": "^5.4|^6.4|^7.0",
"twig/twig": "^3.0|^4.0"
"twig/twig": "^3.2|^4.0"
},
"require-dev": {
"league/commonmark": "^1.0|^2.0",
@@ -7700,7 +7776,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.13.0"
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.15.0"
},
"funding": [
{
@@ -7712,20 +7788,88 @@
"type": "tidelift"
}
],
"time": "2024-09-01T20:39:12+00:00"
"time": "2024-09-26T19:22:23+00:00"
},
{
"name": "twig/intl-extra",
"version": "v3.13.0",
"name": "twig/html-extra",
"version": "v3.15.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/intl-extra.git",
"reference": "1b8d78c5db08bdc61015fd55009d2e84b3aa7e38"
"url": "https://github.com/twigphp/html-extra.git",
"reference": "2086023d3ffc4bae2b1115f715d17f97fd013665"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/1b8d78c5db08bdc61015fd55009d2e84b3aa7e38",
"reference": "1b8d78c5db08bdc61015fd55009d2e84b3aa7e38",
"url": "https://api.github.com/repos/twigphp/html-extra/zipball/2086023d3ffc4bae2b1115f715d17f97fd013665",
"reference": "2086023d3ffc4bae2b1115f715d17f97fd013665",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/mime": "^5.4|^6.4|^7.0",
"twig/twig": "^3.13|^4.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Twig\\Extra\\Html\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "[email protected]",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
}
],
"description": "A Twig extension for HTML",
"homepage": "https://twig.symfony.com",
"keywords": [
"html",
"twig"
],
"support": {
"source": "https://github.com/twigphp/html-extra/tree/v3.15.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2024-09-30T06:41:48+00:00"
},
{
"name": "twig/intl-extra",
"version": "v3.15.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/intl-extra.git",
"reference": "92a127a58857597acc6eca2e34d5ef90057dcc59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/92a127a58857597acc6eca2e34d5ef90057dcc59",
"reference": "92a127a58857597acc6eca2e34d5ef90057dcc59",
"shasum": ""
},
"require": {
@@ -7764,7 +7908,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/intl-extra/tree/v3.13.0"
"source": "https://github.com/twigphp/intl-extra/tree/v3.15.0"
},
"funding": [
{
@@ -7776,20 +7920,20 @@
"type": "tidelift"
}
],
"time": "2024-09-03T13:08:40+00:00"
"time": "2024-09-16T10:21:35+00:00"
},
{
"name": "twig/twig",
"version": "v3.14.2",
"version": "v3.15.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a"
"reference": "2d5b3964cc21d0188633d7ddce732dc8e874db02"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a",
"reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/2d5b3964cc21d0188633d7ddce732dc8e874db02",
"reference": "2d5b3964cc21d0188633d7ddce732dc8e874db02",
"shasum": ""
},
"require": {
@@ -7843,7 +7987,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.14.2"
"source": "https://github.com/twigphp/Twig/tree/v3.15.0"
},
"funding": [
{
@@ -7855,7 +7999,7 @@
"type": "tidelift"
}
],
"time": "2024-11-07T12:36:22+00:00"
"time": "2024-11-17T15:59:19+00:00"
},
{
"name": "webmozart/assert",
@@ -7919,16 +8063,16 @@
"packages-dev": [
{
"name": "deployer/deployer",
"version": "v7.5.5",
"version": "v7.5.6",
"source": {
"type": "git",
"url": "https://github.com/deployphp/deployer.git",
"reference": "1b37d639e0ffad980f6cc3873859b16de29ecad7"
"reference": "4f43595502870cd499a839c4ec676f58e70143c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/deployphp/deployer/zipball/1b37d639e0ffad980f6cc3873859b16de29ecad7",
"reference": "1b37d639e0ffad980f6cc3873859b16de29ecad7",
"url": "https://api.github.com/repos/deployphp/deployer/zipball/4f43595502870cd499a839c4ec676f58e70143c4",
"reference": "4f43595502870cd499a839c4ec676f58e70143c4",
"shasum": ""
},
"require": {
@@ -7969,7 +8113,7 @@
"type": "github"
}
],
"time": "2024-11-08T13:18:44+00:00"
"time": "2024-11-26T15:31:46+00:00"
},
{
"name": "masterminds/html5",
@@ -10242,7 +10386,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@@ -10251,6 +10395,6 @@
"ext-iconv": "*",
"ext-simplexml": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
+2
View File
@@ -12,4 +12,6 @@ return [
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
];
-11
View File
@@ -1,11 +0,0 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn
+45
View File
@@ -0,0 +1,45 @@
webpack_encore:
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
output_path: '%kernel.project_dir%/public/build'
# If multiple builds are defined (as shown below), you can disable the default build:
# output_path: false
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
# preload: true
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
# strict_mode: false
# If you have multiple builds:
# builds:
# frontend: '%kernel.project_dir%/public/frontend/build'
# pass the build name as the 3rd argument to the Twig functions
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
framework:
assets:
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
#when@prod:
# webpack_encore:
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
# # Available in version 1.2
# cache: true
#when@test:
# webpack_encore:
# strict_mode: false
+10 -4
View File
@@ -10,6 +10,8 @@ services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$xmlPath: '%kernel.project_dir%/var/xmlexport'
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
@@ -29,10 +31,14 @@ services:
bpn_url: '%env(APP_BPN_ENDPOINT)%'
debug: '%env(bool:APP_BPN_DEBUG)%'
App\BusProNet\DataLoader\TravelDataLoader:
App\Twig\AppRuntime:
arguments:
$xmlPath: '%kernel.project_dir%/var/xmlexport'
$intlExtension: '@twig.extension.intl'
App\BusProNet\DataLoader\PickupDataLoader:
App\Menu\MenuBuilder:
arguments:
$xmlPath: '%kernel.project_dir%/var/xmlexport'
$factory: '@knp_menu.factory'
tags:
- name: knp_menu.menu_builder
method: createMainMenu
alias: main
-35
View File
@@ -1,35 +0,0 @@
<?php
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'htmx.org' => [
'version' => '2.0.3',
],
'@picocss/pico' => [
'version' => '2.0.6',
],
'@picocss/pico/css/pico.min.css' => [
'version' => '2.0.6',
'type' => 'css',
],
];
+6966
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "myep-next",
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@hotwired/stimulus": "^3.0.0",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/webpack-encore": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.20",
"core-js": "^3.38.0",
"file-loader": "^6.2.0",
"htmx.org": "^1.9.12",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"regenerator-runtime": "^0.13.9",
"tailwindcss": "^3.4.15",
"webpack": "^5.74.0",
"webpack-cli": "^5.1.0",
"webpack-notifier": "^1.15.0"
},
"license": "WTFPL",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
plugins: {
'postcss-import': {},
tailwindcss: {},
autoprefixer: {},
}
}
+70
View File
@@ -0,0 +1,70 @@
# Use the front controller as index file. It serves as a fallback solution when
# every other rewrite/redirect fails (e.g. in an aliased environment without
# mod_rewrite). Additionally, this reduces the matching process for the
# start page (path "/") because otherwise Apache will apply the rewriting rules
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
DirectoryIndex index.php
# By default, Apache does not evaluate symbolic links if you did not enable this
# feature in your server configuration. Uncomment the following line if you
# install assets as symlinks or if you experience problems related to symlinks
# when compiling LESS/Sass/CoffeScript assets.
# Options +SymLinksIfOwnerMatch
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
# to the front controller "/index.php" but be rewritten to "/index.php/index".
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
<IfModule mod_rewrite.c>
# This Option needs to be enabled for RewriteRule, otherwise it will show an error like
# 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'
Options +SymLinksIfOwnerMatch
RewriteEngine On
# Determine the RewriteBase automatically and set it as environment variable.
# If you are using Apache aliases to do mass virtual hosting or installed the
# project in a subdirectory, the base path will be prepended to allow proper
# resolution of the index.php file and to redirect to the correct URI. It will
# work in environments without path prefix as well, providing a safe, one-size
# fits all solution. But as you do not need it in this case, you can comment
# the following 2 lines to eliminate the overhead.
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
RewriteRule .* - [E=BASE:%1]
# Sets the HTTP_AUTHORIZATION header removed by Apache
RewriteCond %{HTTP:Authorization} .+
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
# Redirect to URI without front controller to prevent duplicate content
# (with and without `/index.php`). Only do this redirect on the initial
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
# endless redirect loop (request -> rewrite to front controller ->
# redirect -> request -> ...).
# So in case you get a "too many redirects" error or you always get redirected
# to the start page because your Apache does not expose the REDIRECT_STATUS
# environment variable, you have 2 choices:
# - disable this feature by commenting the following 2 lines or
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
# following RewriteCond (best solution)
RewriteCond %{ENV:REDIRECT_STATUS} =""
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
# If the requested filename exists, simply serve it.
# We only want to let Apache serve files and not directories.
# Rewrite all other queries to the front controller.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ %{ENV:BASE}/index.php [L]
</IfModule>
<IfModule !mod_rewrite.c>
<IfModule mod_alias.c>
# When mod_rewrite is not available, we instruct a temporary redirect of
# the start page to the front controller explicitly so that the website
# and the generated links can still be used.
RedirectMatch 307 ^/$ /index.php/
# RedirectTemp cannot be used instead
</IfModule>
</IfModule>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 124 74" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><g><path d="M107,0.3c-36.4,5.5 -81.6,19.1 -81.6,38.2c0,17.7 41.8,13.2 41.8,13.2c0,0 -67,22.3 -67,-7c0,-33.2 84.5,-44.4 106.8,-44.4Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M17,73.5c36.4,-5.4 81.6,-19.1 81.6,-38.2c0,-17.7 -41.8,-13.2 -41.8,-13.2c0,0 67,-22.3 67,7c0,33.3 -84.5,44.4 -106.8,44.4Z" style="fill:#f8c62f;fill-rule:nonzero;"/><g><path d="M60.7,34.3l-12.8,4l12.2,0.8l-0.4,-2.4l1,-2.4Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M75.9,35.2l-9.2,2.9l-3.5,9.7l-1.7,-11.3l4,-10.7l1.7,7.9l8.7,1.5Z" style="fill:#f8c62f;fill-rule:nonzero;"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 995 B

+6
View File
@@ -0,0 +1,6 @@
{
"icons": [
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
]
}
+22 -1
View File
@@ -17,7 +17,6 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ApiClient
@@ -77,6 +76,28 @@ class ApiClient
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
}
/**
* @throws ApiClientException
*/
public function updateNewsletterRegistration(string $email, string $password, PersonalData $personalData): Notification|PersonalData
{
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'art' => 'Newsletter',
'email' => $email,
'passwort' => $password,
'idadresse' => $personalData->addressId,
'newsletter' => [
'email' => $email,
'anmeldung' => $personalData->communication->newsletter ? 'True' : 'False',
],
];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
}
/**
* @throws ApiClientException
*/
@@ -4,10 +4,11 @@ namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\Availability;
use App\BusProNet\Model\BaseData;
use App\BusProNet\TypeConversionTrait;
class AvailabilitiesResponseParser
{
use ResponseParserTrait;
use TypeConversionTrait;
public function parse(\SimpleXMLElement $xml): BaseData
{
@@ -10,10 +10,11 @@ use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Surcharge;
use App\BusProNet\TypeConversionTrait;
class BookingResponseParser
{
use ResponseParserTrait;
use TypeConversionTrait;
public function parse(\SimpleXMLElement $xml): Booking
{
@@ -39,7 +40,7 @@ class BookingResponseParser
$booking->applicant = $this->parsePersonalData($xml->anmelder);
$participantsStatus = $this->stringToArray((string) $xml->status_teilnehmer, '/');
$booking->participantsStatus = $this->arrayToOneBased($participantsStatus);
$booking->participantsStatus = array_combine(range(1, count($participantsStatus)), $participantsStatus);
$booking->participants = $this->parseParticipants($xml);
@@ -120,7 +121,7 @@ class BookingResponseParser
return $personalData;
}
private function parseServices(\SimpleXMLElement $xml, string $type): array
private function parseServices(\SimpleXMLElement $xml, string $category): array
{
$services = [];
@@ -130,6 +131,8 @@ class BookingResponseParser
$service = new Service();
$service->id = $id;
$service->category = $category;
$service->source = Service::SOURCE_BOOKING;
$service->label = (string) $attributes['leistung'];
$service->dateFrom = $attributes['termin'] ? $this->stringToDate((string) $attributes['termin']) : null;
$service->dateTo = $attributes['terminbis'] ? $this->stringToDate((string) $attributes['terminbis']) : null;
@@ -144,7 +147,7 @@ class BookingResponseParser
}, $this->stringToArray((string) $attributes['einzelpreis'], '/')
);
$service->individualPrice = array_combine($mapping, $individualPrices);
if (Service::CATEGORY_TRANSPORTATION === $type) {
if (Service::CATEGORY_TRANSPORTATION === $category) {
$service->direction = (string) $attributes['richtung'];
}
@@ -3,10 +3,11 @@
namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\TypeConversionTrait;
class BookingUpdateResponseParser
{
use ResponseParserTrait;
use TypeConversionTrait;
public function parse(\SimpleXMLElement $xml): BookingUpdate
{
@@ -4,10 +4,11 @@ namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking;
use App\BusProNet\TypeConversionTrait;
class BookingsResponseParser
{
use ResponseParserTrait;
use TypeConversionTrait;
public function parse(\SimpleXMLElement $xml): BaseData
{
@@ -23,6 +24,7 @@ class BookingsResponseParser
$booking->price = $this->stringToFloat((string) $item->preis);
$booking->bookingDate = $this->stringToDateTime((string) $item->buchungsdatum);
$booking->travel = (string) $item->reise;
$booking->travelId = (int) $item->idreise;
$booking->travelDate = $this->stringToDate((string) $item->reisedatum);
$booking->document = $this->stringToBool((string) $item->reisedokument);
$booking->payment = $this->stringToFloat((string) $item->zahlung);
@@ -2,13 +2,14 @@
namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\CrmSelection;
use App\BusProNet\Model\CrmSelectionGroup;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\TypeConversionTrait;
class CrmAttributesResponseParser
{
use ResponseParserTrait;
use TypeConversionTrait;
private const BPN_CRM_ID_ADMIN = 1292;
private const BPN_CRM_ID_MANAGER = 1293;
@@ -4,10 +4,11 @@ namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\MutableData;
use App\BusProNet\TypeConversionTrait;
class MutableDataResponseParser
{
use ResponseParserTrait;
use TypeConversionTrait;
public function parse(\SimpleXMLElement $xml): BaseData
{
@@ -5,9 +5,12 @@ namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\Address;
use App\BusProNet\Model\Communication;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\TypeConversionTrait;
class PersonalDataResponseParser
{
use TypeConversionTrait;
public function parse(\SimpleXMLElement $xml): PersonalData
{
$addressXml = $xml->adressdaten;
@@ -30,24 +33,25 @@ class PersonalDataResponseParser
$personalData->weight = $xml->sonstiges2 ? (int) $xml->sonstiges2 : null;
$personalData->shoeSize = $xml->sonstiges3 ? (int) $xml->sonstiges3 : null;
$postalXml = $addressXml->anschrift;
if ($postalXml = $addressXml->anschrift) {
$address = new Address();
$address->street = (string) $postalXml->strasse;
$address->postCode = (string) $postalXml->plz;
$address->city = (string) $postalXml->ort;
$address->country = (string) $postalXml->land;
$address = new Address();
$address->street = (string) $postalXml->strasse;
$address->postCode = (string) $postalXml->plz;
$address->city = (string) $postalXml->ort;
$address->country = (string) $postalXml->land;
$personalData->address = $address;
}
$personalData->address = $address;
if ($contactXml = $addressXml->kommunikation) {
$communication = new Communication();
$communication->phone = (string) $contactXml->telefonprivat;
$communication->mobile = (string) $contactXml->telefonmobil;
$communication->email = (string) $contactXml->email;
$communication->newsletter = $this->stringToBool((string) $contactXml->newsletter);
$contactXml = $addressXml->kommunikation;
$communication = new Communication();
$communication->phone = (string) $contactXml->telefonprivat;
$communication->mobile = (string) $contactXml->telefonmobil;
$communication->email = (string) $contactXml->email;
$personalData->communication = $communication;
$personalData->communication = $communication;
}
return $personalData;
}
@@ -33,6 +33,7 @@ class ResponseParser
switch ($subType) {
case 'Adressdaten':
case 'Adressdaten_Ändern':
case 'Newsletter':
return (new PersonalDataResponseParser())->parse($xml);
case 'SelektionCRM':
case 'SelektionCRM_Ändern':
@@ -2,12 +2,12 @@
namespace App\BusProNet\DataLoader;
use App\BusProNet\ApiResponseParser\ResponseParserTrait;
use App\BusProNet\TypeConversionTrait;
use Symfony\Contracts\Cache\CacheInterface;
abstract class AbstractDataLoader
{
use ResponseParserTrait;
use TypeConversionTrait;
public function __construct(protected readonly CacheInterface $cache, protected readonly ?string $xmlPath = null)
{
+77 -12
View File
@@ -11,38 +11,90 @@ use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use Symfony\Component\Finder\Finder;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class TravelDataLoader extends AbstractDataLoader
{
public function __construct(
private readonly HotelDataLoader $hotelDataLoader,
CacheInterface $cache,
?string $xmlPath = null
) {
parent::__construct($cache, $xmlPath);
}
public function generateMapping(): array
{
return $this->cache->get('bpn_travels_mapping', function (ItemInterface $item) {
$item->expiresAfter(3600);
$finder = new Finder();
$finder->files()->in($this->xmlPath)->name('Ziel_*.xml');
$mapping = [];
$hotels = $this->hotelDataLoader->loadAll();
foreach ($finder as $file) {
$xml = simplexml_load_file($file->getRealPath());
foreach ($xml->reise->termin as $travelXml) {
$attributes = $travelXml->attributes();
$travelId = (int) $attributes['idbuspro'];
$mapping[$travelId] = [
'id' => (int) $attributes['idbuspro'],
'label' => (string) $travelXml->text,
'dateFrom' => $this->stringToDate((string) $attributes['termin']),
'dateTo' => $this->stringToDate((string) $attributes['bis']),
'hotels' => [],
'file' => $file->getRealPath(),
];
foreach ($travelXml->hotel as $hotelXml) {
$hotelId = (int) $hotelXml->attributes()['idbuspro'];
$mapping[$travelId]['hotels'][$hotelId] = [
'id' => $hotelId,
'label' => $hotels[$hotelId]->name,
];
}
}
}
return $mapping;
});
}
public function loadById(int $id, ?string $filename = null): ?Travel
{
if (null !== $filename) {
return $this->loadXml($id, $filename);
}
$finder = new Finder();
$finder->files()->in($this->xmlPath)->name('Ziel_*.xml');
$mapping = $this->generateMapping();
foreach ($finder as $file) {
if (null !== $xml = $this->loadXml($id, $file->getRealPath())) {
return $xml;
}
if (false === isset($mapping[$id])) {
return null;
}
return null;
$file = $mapping[$id]['file'];
return $this->loadXml($id, $file);
}
private function loadXml(int $id, string $filename): ?Travel
{
$xml = simplexml_load_file($filename);
foreach ($xml->reise->termin as $travel) {
if ($id === (int) $travel->attributes()['idbuspro']) {
return $this->parseXml($travel);
}
$travelXml = $xml->xpath(sprintf('//reise/termin[@idbuspro="%d"]', $id));
if (0 === count($travelXml)) {
return null;
}
return null;
return $this->parseXml($travelXml[0]);
}
public function parseXml(\SimpleXMLElement $xml): Travel
@@ -100,6 +152,7 @@ class TravelDataLoader extends AbstractDataLoader
$serviceId = (int) $attributes['idbuspro'];
$service = new Service();
$service->source = Service::SOURCE_TRAVEL;
$service->category = Service::CATEGORY_ADDITIONAL;
$service->id = $serviceId;
$service->subType = (string) $attributes['unterart'];
@@ -125,6 +178,7 @@ class TravelDataLoader extends AbstractDataLoader
$serviceId = (int) $attributes['idbuspro'];
$service = new Service();
$service->source = Service::SOURCE_TRAVEL;
$service->category = Service::CATEGORY_TRANSPORTATION;
$service->id = $serviceId;
$service->subType = (string) $attributes['unterart'];
@@ -223,4 +277,15 @@ class TravelDataLoader extends AbstractDataLoader
}
}
}
public function patchBookings(BaseData $bookings): void
{
foreach ($bookings->getItems() as $booking) {
if (null === $travelId = $booking->travelId) {
continue;
}
$booking->travelData = $this->loadById($travelId);
}
}
}
+1
View File
@@ -16,6 +16,7 @@ class Booking
public ?int $travelId = null;
public ?string $travelCode = null;
public ?\DateTimeImmutable $travelDate = null;
public ?Travel $travelData = null;
public ?int $hotelId = null;
public ?string $hotelName = null;
public ?bool $document = null;
+2
View File
@@ -7,6 +7,7 @@ class Communication
public ?string $phone = null;
public ?string $mobile = null;
public ?string $email = null;
public bool $newsletter = false;
public function toPayload(): array
{
@@ -14,6 +15,7 @@ class Communication
'email' => $this->email,
'telefonmobil' => $this->mobile,
'telefonprivat' => $this->phone,
'newsletter' => $this->newsletter ? 'True' : 'False',
];
}
}
+5
View File
@@ -8,10 +8,13 @@ class PersonalData
{
public ?int $addressId = null;
public ?int $personId = null;
#[Assert\NotBlank(message: 'Bitte angeben')]
public ?string $name = null;
#[Assert\NotBlank(message: 'Bitte angeben')]
public ?string $firstName = null;
public ?string $salutation = null;
public ?string $title = null;
public ?string $gender = null;
@@ -20,8 +23,10 @@ class PersonalData
public ?string $shoeSize = null;
public ?string $weight = null;
public ?\DateTimeImmutable $dateOfBirth = null;
#[Assert\Valid()]
public ?Address $address = null;
#[Assert\Valid()]
public ?Communication $communication = null;
+4
View File
@@ -4,6 +4,9 @@ namespace App\BusProNet\Model;
class Service
{
public const SOURCE_TRAVEL = 'travel';
public const SOURCE_BOOKING = 'booking';
public const CATEGORY_ADDITIONAL = 'additional';
public const CATEGORY_TRANSPORTATION = 'transportation';
@@ -14,6 +17,7 @@ class Service
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
public ?int $id = null;
public ?string $source = null;
public ?string $category = null;
public ?string $status = null;
public bool $mandatory = false;
@@ -1,19 +1,12 @@
<?php
namespace App\BusProNet\ApiResponseParser;
namespace App\BusProNet;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
trait ResponseParserTrait
trait TypeConversionTrait
{
private function arrayToOneBased(array $data): array
{
$keys = range(1, count($data));
return array_combine($keys, $data);
}
protected function stringToArray(string $string, string $separator = ','): array
{
return explode($separator, $string);
@@ -24,11 +17,21 @@ trait ResponseParserTrait
return (float) str_replace(['.', ','], ['', '.'], $string);
}
protected function floatToString(float $float): string
{
return number_format($float, 2, ',', '');
}
protected function stringToBool(string $string): bool
{
return 'True' === $string;
}
protected function boolToString(bool $bool): string
{
return $bool ? 'True' : 'False';
}
protected function stringToDate(string $string): ?\DateTimeImmutable
{
try {
+16 -6
View File
@@ -7,13 +7,14 @@ use App\BusProNet\DataLoader\PickupDataLoader;
use App\BusProNet\DataLoader\TravelDataLoader;
use App\Form\BookingType;
use App\Form\Model\BookingData;
use App\Htmx\HxRedirectResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class EditController extends AbstractController
{
@@ -21,6 +22,7 @@ class EditController extends AbstractController
private readonly ApiClient $apiClient,
private readonly TravelDataLoader $travelDataLoader,
private readonly PickupDataLoader $pickupDataLoader,
private readonly CacheInterface $cache,
private readonly Security $security,
) {
}
@@ -35,8 +37,14 @@ class EditController extends AbstractController
return $this->security->logout();
}
// Fetch original bookingData data via API
$bookingData = $this->apiClient->getBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $id);
// Fetch original bookingData data via API and cache result for a short ttl
$cacheKeyList = sprintf('bpn_bookings_%s', sha1($bpnUser->getUserIdentifier()));
$cacheKeySingle = sprintf('bpn_booking_%d', $id);
$bookingData = $this->cache->get($cacheKeySingle, function (ItemInterface $item) use ($bpnUser, $id) {
$item->expiresAfter(300);
return $this->apiClient->getBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $id);
});
if (null === $bookingData) {
$this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar');
@@ -68,10 +76,9 @@ class EditController extends AbstractController
$formData = BookingData::fromBooking($bookingData, $travelData);
$form = $this->createForm(BookingType::class, $formData, [
'attr' => ['novalidate' => 'novalidate'],
'hx_post' => $this->generateUrl('app_booking_edit', ['id' => $id]),
'hx_target' => '#app',
'hx_swap' => 'innerHTML show:top',
'attr' => ['novalidate' => 'novalidate'],
]);
$form->handleRequest($request);
@@ -79,9 +86,12 @@ class EditController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
$this->apiClient->updateBooking($formData, false);
$this->cache->delete($cacheKeySingle);
$this->cache->delete($cacheKeyList);
$this->addFlash('success', 'Buchung erfolgreich aktualisiert');
return new HxRedirectResponse($this->generateUrl('app_booking_edit', ['id' => $id]));
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
}
return $this->render('booking/edit.html.twig', [
+15 -1
View File
@@ -3,17 +3,22 @@
namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\DataLoader\TravelDataLoader;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class IndexController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelDataLoader $travelDataLoader,
private readonly CacheInterface $cache,
private readonly Security $security,
) {
}
@@ -28,7 +33,16 @@ class IndexController extends AbstractController
return $this->security->logout();
}
$bookings = $this->apiClient->getBookings($bpnUser->getEmail(), $bpnUser->getPassword());
// Cache bookings for a short ttl
$cacheKey = sprintf('bpn_bookings_%s', sha1($bpnUser->getUserIdentifier()));
$bookings = $this->cache->get($cacheKey, function (ItemInterface $item) use ($bpnUser) {
$item->expiresAfter(300);
$bookings = $this->apiClient->getBookings($bpnUser->getEmail(), $bpnUser->getPassword());
$this->travelDataLoader->patchBookings($bookings);
return $bookings;
});
return $this->render('booking/index.html.twig', [
'bookings' => $bookings->getItems(),
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
#[Route('/', name: 'app_index')]
#[IsGranted('PUBLIC_ACCESS')]
public function index(): Response
{
return $this->render('index/index.html.twig');
}
}
+28 -1
View File
@@ -42,7 +42,7 @@ class PersonalDataController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
try {
$this->apiClient->updatePersonalData($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData);
$this->addFlash('success', 'Personal data updated.');
$this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert');
} catch (ApiClientException $e) {
$this->addFlash('error', $e->getMessage());
}
@@ -55,4 +55,31 @@ class PersonalDataController extends AbstractController
'form' => $form->createView(),
]);
}
#[Route('/personal-data/newsletter', name: 'app_personal_data_newsletter', methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function newsletter(Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$personalData = $this
->apiClient
->getPersonalData($bpnUser->getEmail(), $bpnUser->getPassword())
;
$personalData->communication->newsletter = !$personalData->communication->newsletter;
try {
$this->apiClient->updateNewsletterRegistration($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData);
$this->addFlash('success', 'Deine Anmeldung zum Newsletter wurde aktualisiert');
} catch (ApiClientException $e) {
$this->addFlash('error', $e->getMessage());
}
return $this->redirectToRoute('app_personal_data');
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
#[Route('/', name: 'app_login')]
#[Route('/login', name: 'app_login')]
#[IsGranted('PUBLIC_ACCESS')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\EventListener;
use App\Htmx\HxRedirectResponse;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
#[AsEventListener(event: LogoutEvent::class, method: 'onLogout')]
class LogoutListener
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
{
}
public function onLogout(LogoutEvent $event): void
{
$event->setResponse(new HxRedirectResponse($this->urlGenerator->generate('app_index')));
}
}
+26 -8
View File
@@ -24,11 +24,11 @@ final class BookingType extends AbstractType
$form->add('participants', CollectionType::class, [
'entry_type' => ParticipantType::class,
'entry_options' => [
'selectable_courses' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_COURSES),
'selectable_ski_passes' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_SKI_PASS),
'selectable_services' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_ADDITIONAL),
'selectable_board' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_BOARD),
'selectable_rentals' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_RENTALS),
'selectable_courses' => $this->mergeSelectableServices($data, Service::TOKEN_COURSES),
'selectable_ski_passes' => $this->mergeSelectableServices($data, Service::TOKEN_SKI_PASS),
'selectable_services' => $this->mergeSelectableServices($data, Service::TOKEN_ADDITIONAL),
'selectable_board' => $this->mergeSelectableServices($data, Service::TOKEN_BOARD),
'selectable_rentals' => $this->mergeSelectableServices($data, Service::TOKEN_RENTALS),
'selectable_transportation_services_to' => $travelData
->getTransportationServicesByDirection('HIN'),
'selectable_transportation_services_fro' => $travelData
@@ -46,10 +46,28 @@ final class BookingType extends AbstractType
});
}
private function mergeSelectableServices(BookingData $data, mixed $subType): array
{
// combine selectable services from travel data with additional services
// from booking data
$selectableServices = $data->travel->getAdditionalServicesByGroup($subType);
$selectableServiceIds = array_map(function (Service $service) {
return $service->id;
}, $selectableServices);
foreach ($data->booking->getAdditionalServicesByGroup($subType) as $item) {
if (false === in_array($item->id, $selectableServiceIds)) {
$selectableServices[] = $item;
}
}
return $selectableServices;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults(['data_class' => BookingData::class])
;
$resolver->setDefaults([
'data_class' => BookingData::class,
]);
}
}
+173 -125
View File
@@ -51,7 +51,7 @@ class ParticipantType extends AbstractType
'preferred_choices' => ['D', 'A', 'CH'],
])
->add('email', EmailType::class, [
'label' => 'Email',
'label' => 'E-Mail',
])
->add('mobile', TextType::class, [
'label' => 'Telefon (mobil)',
@@ -71,142 +71,190 @@ class ParticipantType extends AbstractType
;
}
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
/** @var ParticipantData $participantData */
$participantData = $event->getData();
$participantIndex = $participantData->index;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
/** @var ParticipantData $participantData */
$participantData = $event->getData();
$participantIndex = $participantData->index;
$form = $event->getForm();
$form = $event->getForm();
$commonChoiceFieldOptions = [
'multiple' => true,
'expanded' => true,
'choice_value' => 'id',
'choice_label' => function (?Service $service) use ($participantIndex) {
if (null === $service) {
return null;
}
$price = $service->individualPrice[$participantIndex] ?? $service->price;
if (null === $price || 0.0 === $price) {
return $service->label;
}
$commonChoiceFieldOptions = [
'required' => false,
'multiple' => true,
'expanded' => true,
'choice_value' => 'id',
'choice_label' => function (?Service $service) use ($participantIndex) {
if (null === $service) {
return null;
}
$price = $service->individualPrice[$participantIndex] ?? $service->price;
if (null === $price || 0.0 === $price) {
return $service->label;
}
return sprintf('%s (%s€)',
$service->label,
number_format($price, 2, ',', '.')
);
},
'choice_attr' => function (?Service $service) {
if (true === $service->mandatory) {
return sprintf('%s (%s €)',
$service->label,
number_format($price, 2, ',', '.')
);
},
'choice_attr' => function (?Service $service) {
$attributes = [
'data-available' => $service->available,
'data-availabilities-target' => 'field',
];
// forcibly select mandatory services
if (true === $service->mandatory) {
$attributes['checked'] = true;
$attributes['disabled'] = true;
}
// disable selection of services only available in booking data
if (Service::SOURCE_BOOKING === $service->source) {
$attributes['disabled'] = true;
}
return $attributes;
},
'choice_filter' => function (Service $service) use ($participantData) {
// remove choices only available in booking data and
// which are not mapped to the current participant
if (
Service::SOURCE_TRAVEL === $service->source ||
Service::CATEGORY_ADDITIONAL !== $service->category
) {
return true;
}
return in_array($participantData->index, $service->mapping);
},
];
// Additional services
if (true === $options['additional_services_mutable']) {
if (0 < count($options['selectable_courses'])) {
$form->add('courses', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Kurse',
'choices' => $options['selectable_courses'],
]);
}
if (0 < count($options['selectable_services'])) {
$form->add('additionalServices', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Zusatzleistungen',
'choices' => $options['selectable_services'],
]);
}
if (0 < count($options['selectable_ski_passes'])) {
$form->add('skiPass', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Skipass',
'choices' => $options['selectable_ski_passes'],
]);
}
if (0 < count($options['selectable_board'])) {
$form->add('board', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verpflegung',
'choices' => $options['selectable_board'],
]);
}
if (0 < count($options['selectable_rentals'])) {
$form->add('rentals', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verleih',
'choices' => $options['selectable_rentals'],
]);
}
}
// Transportation
if (true === $options['transportation_services_mutable']) {
$form
->add('transportationServiceTo', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Anreise',
'multiple' => false,
'choices' => $options['selectable_transportation_services_to'],
'choice_attr' => function (?Service $service) {
return [
'checked' => true,
'disabled' => true,
'data-available' => $service->available,
'data-availabilities-target' => 'field',
'data-action' => 'select-toggle#toggle',
'data-select-toggle-value' => $service->subType,
];
}
},
])
->add('transportationServiceFro', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Rückreise',
'multiple' => false,
'choices' => $options['selectable_transportation_services_fro'],
]);
}
return [];
},
];
// Pickup
if (true === $options['pickups_mutable']) {
$form
->add('pickup', ChoiceType::class, [
'label' => 'Zustieg',
'multiple' => false,
'expanded' => false,
'choices' => $options['selectable_pickups'],
'choice_value' => 'id',
'choice_label' => function (?Pickup $pickup) {
if (null === $pickup) {
return null;
}
// Additional services
if (true === $options['additional_services_mutable']) {
$price = $pickup->price;
if (0 < count($options['selectable_courses'])) {
$form->add('courses', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Kurse',
'choices' => $options['selectable_courses'],
]);
}
if (0 < count($options['selectable_services'])) {
$form->add('additionalServices', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Zusatzleistungen',
'choices' => $options['selectable_services'],
]);
}
if (0 < count($options['selectable_ski_passes'])) {
$form->add('skiPass', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Skipass',
'choices' => $options['selectable_ski_passes'],
]);
}
if (0 < count($options['selectable_board'])) {
$form->add('board', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verpflegung',
'choices' => $options['selectable_board'],
]);
}
if (0 < count($options['selectable_rentals'])) {
$form->add('rentals', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Verleih',
'choices' => $options['selectable_rentals'],
]);
}
if (null === $price || 0.0 === $price) {
return $pickup->city;
}
return sprintf('%s (%s €)',
$pickup->city,
number_format($price, 2, ',', '.')
);
},
]);
}
})
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$data = $event->getData();
$form = $event->getForm();
$transportationId = $data['transportationServiceTo'];
$transportation = $options['travel']->pickups[$transportationId] ?? null;
if (null !== $transportation && 'PKW' === $transportation->subType) {
$form->remove('pickup');
unset($data['pickup']);
}
// forcibly select mandatory services that potentially have been disabled in PRE_SET_DATA
$mandatoryServices = array_filter($options['selectable_services'], function (Service $service) use ($data) {
return true === $service->mandatory ||
(Service::SOURCE_BOOKING === $service->source && in_array($data['index'], $service->mapping));
});
if (0 < count($mandatoryServices)) {
if (false === isset($data['additionalServices'])) {
$data['additionalServices'] = [];
}
// Transportation
if (true === $options['transportation_services_mutable']) {
$form
->add('transportationServiceTo', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Anreise',
'multiple' => false,
'choices' => $options['selectable_transportation_services_to'],
])
->add('transportationServiceFro', ChoiceType::class, [
...$commonChoiceFieldOptions,
'label' => 'Rückreise',
'multiple' => false,
'choices' => $options['selectable_transportation_services_fro'],
]);
}
$mandatoryServiceIds = array_map(function (Service $service) {
return $service->id;
}, $mandatoryServices);
// Pickup
if (true === $options['pickups_mutable']) {
$form
->add('pickup', ChoiceType::class, [
'label' => 'Zustieg (bei Busanreise)',
'multiple' => false,
'expanded' => false,
'choices' => $options['selectable_pickups'],
'choice_value' => 'id',
'choice_label' => function (?Pickup $pickup) {
if (null === $pickup) {
return null;
}
$allServiceIds = [...$mandatoryServiceIds, ...$data['additionalServices']];
$data['additionalServices'] = array_unique($allServiceIds);
}
$price = $pickup->price;
if (null === $price || 0.0 === $price) {
return $pickup->city;
}
return sprintf('%s (%s€)',
$pickup->city,
number_format($price, 2, ',', '.')
);
},
]);
}
})
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$data = $event->getData();
$form = $event->getForm();
$transportationId = $data['transportationServiceTo'];
$transportation = $options['travel']->pickups[$transportationId] ?? null;
if (null !== $transportation && 'PKW' === $transportation->subType) {
$form->remove('pickup');
unset($data['pickup']);
}
})
;
$event->setData($data);
});
}
public function configureOptions(OptionsResolver $resolver): void
+3 -3
View File
@@ -18,9 +18,9 @@ class PersonalDataType extends AbstractType
->add('gender', ChoiceType::class, [
'label' => 'Gender',
'choices' => [
'M' => 'M',
'W' => 'W',
'D' => 'D',
'männlich' => 'M',
'weiblich' => 'W',
'divers' => 'D',
],
])
->add('firstName', TextType::class, [
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Menu;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
class MenuBuilder
{
public function __construct(
private readonly FactoryInterface $factory,
) {
}
public function createMainMenu(): ItemInterface
{
$menu = $this->factory->createItem('root', [
'childrenAttributes' => [
'class' => 'menu menu--main',
],
]);
$menu->addChild('Meine Daten', [
'route' => 'app_personal_data',
'linkAttributes' => [
'title' => 'Persönliche Daten',
],
]);
$menu->addChild('Meine Buchungen', [
'route' => 'app_bookings',
'linkAttributes' => [
'title' => 'Meine Buchungen',
],
'extras' => [
'routes' => [
'app_booking_edit',
],
],
]);
$menu->addChild('Logout', [
'route' => 'app_logout',
'linkAttributes' => [
'title' => 'Logout',
],
]);
return $menu;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class AppExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]),
];
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace App\Twig;
use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface;
use Twig\Extra\Intl\IntlExtension;
class AppRuntime implements RuntimeExtensionInterface
{
public function __construct(private readonly IntlExtension $intlExtension)
{
}
public function fileIconFilter(Environment $environment, string $mimeType, ?string $classes = 'w-4 h-4'): string
{
$icon = match ($mimeType) {
'application/pdf' => 'pdf',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'excel',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'word',
default => 'download',
};
return $this->renderIcon($environment, $icon, $classes);
}
public function formatBytes(int $bytes, ?int $precision = 2): string
{
$size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)) . @$size[$factor];
}
public function formatMoney(int $amount): string
{
// Amounts are stored as integers so divide by 100 first
$amount = $amount / 100;
return $this->intlExtension->formatCurrency($amount, 'EUR');
}
public function renderIcon(Environment $environment, string $icon, string $classes = 'w-5 h-5'): string
{
return $environment->render('_partials/_icon.html.twig', [
'icon' => $icon,
'class' => $classes,
]);
}
}
+26 -10
View File
@@ -26,6 +26,9 @@
"migrations/.gitignore"
]
},
"knplabs/knp-menu-bundle": {
"version": "v3.4.2"
},
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
@@ -40,19 +43,16 @@
"tests/bootstrap.php"
]
},
"symfony/asset-mapper": {
"version": "6.4",
"symfony/apache-pack": {
"version": "1.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "6.4",
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
"version": "1.0",
"ref": "0f18b4decdf5695d692c1d0dfd65516a07a6adf1"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/asset_mapper.yaml",
"importmap.php"
"public/.htaccess"
]
},
"symfony/console": {
@@ -285,7 +285,23 @@
"config/routes/web_profiler.yaml"
]
},
"symfony/webpack-encore-bundle": {
"version": "2.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.0",
"ref": "35ee1edee91a1a7137095f53db7ed0c20884b745"
},
"files": [
"assets/app.js",
"assets/styles/app.css",
"config/packages/webpack_encore.yaml",
"package.json",
"webpack.config.js"
]
},
"twig/extra-bundle": {
"version": "v3.13.0"
"version": "v3.15.0"
}
}
+33
View File
@@ -0,0 +1,33 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./templates/**/*.twig',
'./src/Twig/*.php',
'./src/Menu/*.php',
],
safelist: ['rotate-90'],
theme: {
container: {
center: true,
padding: '2rem',
},
extend: {
fontFamily: {
sans: ['Lato', 'sans-serif'],
},
fontWeight: {
semibold: '700',
bold: '900',
},
colors: {
primary: '#3d8ccb',
secondary: '#f9c700',
}
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}
+12
View File
@@ -0,0 +1,12 @@
{% apply spaceless %}
{% for message in messages %}
{% if message is iterable %}
{% set message = message.id | trans(message.parameters | default({}), message.domain | default(null), message.locale | default(null)) | raw %}
{% else %}
{% set message = message | trans | raw %}
{% endif %}
<div>
{{ message }}
</div>
{% endfor %}
{% endapply %}
+9
View File
@@ -0,0 +1,9 @@
{% if app.session.flashBag.peek('error')|length > 0 %}
{% include '_partials/_alert.html.twig' with { level: 'warning', messages: app.flashes('error') } %}
{% endif %}
{% if app.session.flashBag.peek('success')|length > 0 %}
{% include '_partials/_alert.html.twig' with { level: 'success', messages: app.flashes('success') } %}
{% endif %}
{% if app.session.flashBag.peek('info')|length > 0 %}
{% include '_partials/_alert.html.twig' with { level: 'info', messages: app.flashes('info') } %}
{% endif %}
+37
View File
@@ -0,0 +1,37 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg class="{{ class }}" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="0s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="0s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="-0.9s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="-0.9s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+6 -8
View File
@@ -2,18 +2,16 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
<title>{% block title %}MyE&P.next{% endblock %}</title>
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
<body>
<div class="container">
{% block body %}{% endblock %}
</div>
<body class="bg-white text-gray-700 font-sans antialiased text-sm">
{% block body %}{% endblock %}
{% include '_partials/_flashes.html.twig' %}
</body>
</html>
+128 -163
View File
@@ -10,42 +10,39 @@
{{ form_errors(form) }}
</article>
{% endif %}
<article>
<header>
<strong>Reisedaten</strong>
</header>
<table>
<tbody>
<tr>
<th>
<strong>Reise</strong>
</th>
<td>
<div class="flex flex-col space-y-8 pb-4">
<div class="p-8 border border-gray-300 rounded-md"
{{ stimulus_controller('availabilities')|stimulus_controller('toggle', { 'open': true }, { 'closed': 'hidden' }) }}>
<div role="button" class="flex items-center justify-between" {{ stimulus_action('toggle', 'toggle', 'click') }}>
<span class="block text-3xl font-semibold">Reisedaten</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4 pt-4" {{ stimulus_target('toggle', 'toggle') }}>
<div>
<h3 class="text-lg font-semibold">
Destination
</h3>
{{ bookingData.travel }}
</td>
</tr>
<tr>
<th>
<strong>Reiseedatum</strong>
</th>
<td>
</div>
<div>
<h3 class="text-lg font-semibold">
Reisedatum
</h3>
{{ bookingData.travelDate|date('d.m.Y') }}
</td>
</tr>
<tr>
<th>
<strong>Buchungsdatum</strong>
</th>
<td>
</div>
<div>
<h3 class="text-lg font-semibold">
Buchungsdatum
</h3>
{{ bookingData.bookingDate|date('d.m.Y') }}
</td>
</tr>
<tr>
<th>
<strong>Inklusivleistungen</strong>
</th>
<td>
<ul>
</div>
<div>
<h3 class="text-lg font-semibold">
Inklusivleistungen
</h3>
<ul class="list-disc pl-4">
{% for selectionGroup in travelData.includedServices %}
{% for service in selectionGroup.selections %}
<li>
@@ -54,136 +51,104 @@
{% endfor %}
{% endfor %}
</ul>
</td>
</tr>
</tbody>
</table>
</article>
{% for child in form.participants %}
{% set participant = child.vars.data %}
<details{% if not child.vars.valid %} open{% endif %}>
<summary role="button" class="outline">
Teilnehmer {{ participant.index }}
</summary>
<article>
<header>
<strong>Persönliche Daten</strong>
</header>
<fieldset class="grid">
{% if travelData.participantDataMutable %}
{{ form_row(child.firstName) }}
{{ form_row(child.lastName) }}
{{ form_row(child.gender) }}
{% else %}
Teilnehmerdaten nicht mehr änderbar
{% endif %}
</fieldset>
<fieldset class="grid">
{% if travelData.participantDataMutable %}
{{ form_row(child.dateOfBirth) }}
{{ form_row(child.nationality) }}
{% else %}
Teilnehmerdaten nicht mehr änderbar
{% endif %}
</fieldset>
<fieldset class="grid">
{% if travelData.participantDataMutable %}
{{ form_row(child.email) }}
{{ form_row(child.mobile) }}
{% else %}
Teilnehmerdaten nicht mehr änderbar
{% endif %}
</fieldset>
<fieldset class="grid">
{% if travelData.participantDataMutable %}
{{ form_row(child.height) }}
{{ form_row(child.shoeSize) }}
{{ form_row(child.weight) }}
{% else %}
Teilnehmerdaten nicht mehr änderbar
{% endif %}
</fieldset>
</article>
<article>
<header>
<strong>Unterkunft</strong>
</header>
{% set room = bookingData.roomForParticipant(participant.index) %}
{{ room.label }} ({{ room.individualPrice[participant.index]|format_currency('EUR') }})
</article>
<article>
<header>
<strong>Leistungen</strong>
</header>
<fieldset class="grid">
{% if travelData.additionalServicesMutable %}
{% if child.courses is defined %}
{{ form_row(child.courses) }}
{% endif %}
{% if child.skiPass is defined %}
{{ form_row(child.skiPass) }}
{% endif %}
{% else %}
Leistungen nicht mehr änderbar
{% endif %}
</fieldset>
<fieldset class="grid">
{% if travelData.additionalServicesMutable %}
{% if child.additionalServices is defined %}
{{ form_row(child.additionalServices) }}
{% endif %}
{% if child.board is defined %}
{{ form_row(child.board) }}
{% endif %}
{% else %}
Leistungen nicht mehr änderbar
{% endif %}
</fieldset>
<fieldset class="grid">
{% if travelData.additionalServicesMutable %}
{% if child.rentals is defined %}
{{ form_row(child.rentals) }}
{% endif %}
{% else %}
Leistungen nicht mehr änderbar
{% endif %}
</fieldset>
<fieldset class="grid">
{% if travelData.additionalServicesMutable %}
{{ form_row(child.transportationServiceTo) }}
{{ form_row(child.transportationServiceFro) }}
{% else %}
Beförderungsleistung nicht mehr änderbar
{% endif %}
{% if child.pickup is defined %}
{% if travelData.pickupsMutable %}
{{ form_row(child.pickup) }}
{% else %}
Zustieg nicht mehr änderbar
{% endif %}
{% endif %}
</fieldset>
</article>
<article>
<header>
<strong>Gesamtpreis</strong>
</header>
{{ bookingData.priceForParticipant(participant.index)|format_currency('EUR') }}
</article>
</details>
{% endfor %}
{{ form_rest(form) }}
<button type="submit">
Aktualisieren
</button>
{{ form_end(form) }}
<p>
<button type="button"
hx-get="{{ path('app_bookings') }}"
hx-target="#app"
hx-swap="innerHTML show:top"
hx-indicator="#loading-indicator">
</div>
</div>
</div>
{% for child in form.participants %}
{% set participant = child.vars.data %}
<div class="{{ html_classes('p-8 border rounded-md', { 'border-gray-300': child.vars.valid, 'border-red-500': not child.vars.valid }) }}" {{ stimulus_controller('availabilities')|stimulus_controller('toggle', {'open': child.vars.valid == false}, { 'closed': 'hidden' }) }}>
<div role="button" class="flex items-center justify-between" {{ stimulus_action('toggle', 'toggle', 'click') }}>
<div class="text-xl font-semibold">Teilnehmer:in {{ participant.index }}: {{ participant.firstName }} {{ participant.lastName }}</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</div>
<div class="hidden" {{ stimulus_target('toggle', 'toggle') }}>
<div class="flex flex-col space-y-4 pt-4">
<div>
<h3 class="text-xl font-semibold pb-2">
Persönliche Daten
</h3>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4 pb-4">
{{ form_row(child.firstName) }}
{{ form_row(child.lastName) }}
{{ form_row(child.gender) }}
{{ form_row(child.dateOfBirth) }}
{{ form_row(child.nationality) }}
</fieldset>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4 pb-4">
{{ form_row(child.email) }}
{{ form_row(child.mobile) }}
</fieldset>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4">
{{ form_row(child.height) }}
{{ form_row(child.shoeSize) }}
{{ form_row(child.weight) }}
</fieldset>
</div>
<div>
<h3 class="text-xl font-semibold pb-2">
Unterkunft
</h3>
{% set room = bookingData.roomForParticipant(participant.index) %}
{{ room.label }} ({{ room.individualPrice[participant.index]|format_currency('EUR') }})
</div>
<div>
<h3 class="text-xl font-semibold pb-2">
Leistungen
</h3>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4">
{% if child.courses is defined %}
{{ form_row(child.courses) }}
{% endif %}
{% if child.skiPass is defined %}
{{ form_row(child.skiPass) }}
{% endif %}
{% if child.additionalServices is defined %}
{{ form_row(child.additionalServices) }}
{% endif %}
{% if child.board is defined %}
{{ form_row(child.board) }}
{% endif %}
{% if child.rentals is defined %}
{{ form_row(child.rentals) }}
{% endif %}
</fieldset>
</div>
<div>
<h3 class="text-xl font-semibold pb-2">
Hin-/Rückreise
</h3>
<fieldset class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-4" {{ stimulus_controller('select-toggle', { 'show': ['BUS'] }, { 'hidden': 'hidden' }) }}>
{{ form_row(child.transportationServiceTo) }}
{{ form_row(child.transportationServiceFro) }}
{% if child.pickup is defined %}
<div {{ stimulus_target('select-toggle', 'container') }}>
{{ form_row(child.pickup) }}
</div>
{% endif %}
</fieldset>
</div>
<div>
<h3 class="text-xl font-semibold pb-2">
Gesamtpreis
</h3>
<span class="text-2xl font-medium">{{ bookingData.priceForParticipant(participant.index)|format_currency('EUR') }}</span>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="flex justify-between">
<a href="{{ path('app_bookings') }}"
class="btn">
zurück
</a>
<button type="submit" class="btn btn--secondary">
Aktualisieren
</button>
</p>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{% endblock %}
+65 -61
View File
@@ -1,71 +1,75 @@
{% extends 'layout.html.twig' %}
{% block content %}
<div class="overflow-auto">
<table>
<thead>
<tr>
<th scope="col">
Buchungsdatum/<br>Reisedatum
</th>
<th scope="col">
Reise
</th>
<th scope="col">
Vorgangsnr.
</th>
<th scope="col">
Preis/<br>offen
</th>
<th scope="col">
Status
</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for booking in bookings %}
<tr>
<td>
{{ booking.bookingDate | date('d.m.Y') }}/<br>{{ booking.travelDate | date('d.m.Y') }}
</td>
<td>
{{ booking.travel }}
</td>
<td>
{{ booking.bookingNumber }}
</td>
<td>
{{ booking.price|format_currency('EUR') }}/<br>{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
</td>
<td>
{{ booking.status }}
</td>
<td>
{% if booking.editable %}
<button type="button"
hx-get="{{ path('app_booking_edit', { 'id': booking.id }) }}"
hx-target="#app"
hx-indicator="#loading-indicator">
bearbeiten
</button>
<h2 class="text-2xl font-semibold pb-4">
Meine Buchungen
</h2>
<div class="space-y-8">
{% for booking in bookings %}
<div class="p-4 border border-gray-300 rounded-md overflow-hidden" {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden' }) }}>
<div role="button" class="flex items-center justify-between py-2" {{ stimulus_action('toggle', 'toggle', 'click') }}>
<div class="text-lg md:text-xl font-semibold">
{% if booking.travelData %}
{{ booking.travel }}, {{ booking.travelData.dateFrom | date('d.m.Y') }} - {{ booking.travelData.dateTo | date('d.m.Y') }}
{% else %}
{{ booking.travel }}, {{ booking.travelDate | date('d.m.Y') }}
{% endif %}
<a href="{{ path('app_booking_confirmation', { 'id': booking.id }) }}" target="_blank">
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}>
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</div>
<div class="flex flex-col space-y-4 sm:flex-row sm:items-start sm:space-x-2" {{ stimulus_target('toggle', 'toggle') }}>
<div class="flex-1 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div>
<div class="font-semibold pb-2">
Buchungsdatum
</div>
{{ booking.bookingDate | date('d.m.Y') }}
</div>
<div>
<div class="font-semibold pb-2">
Buchungsnummer
</div>
{{ booking.bookingNumber }}
</div>
<div>
<div class="font-semibold pb-2">
Reisepreis
</div>
{{ booking.price|format_currency('EUR') }}/<br>{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
</div>
<div>
<div class="font-semibold pb-2">
Status
</div>
{{ booking.status }}
</div>
</div>
<div class="flex sm:flex-col space-x-2 sm:space-x-0 sm:space-y-1">
{% if booking.editable %}
<a href="{{ path('app_booking_edit', { 'id': booking.id }) }}"
class="btn btn--small btn--secondary">
bearbeiten
</a>
{% endif %}
<a href="{{ path('app_booking_confirmation', { 'id': booking.id }) }}"
class="btn btn--small"
target="_blank">
Bestätigung
</a>
<a href="{{ path('app_booking_documents', { 'id': booking.id }) }}" target="_blank">
<a href="{{ path('app_booking_documents', { 'id': booking.id }) }}"
class="btn btn--small"
target="_blank">
Reisedokumente
</a>
</td>
</tr>
{% else %}
<tr>
<td>
Keine Daten
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div>
Keine Daten
</div>
{% endfor %}
</div>
{% endblock %}
+134 -41
View File
@@ -1,62 +1,155 @@
{% use 'form_div_layout.html.twig' %}
{%- block form_widget_simple -%}
{%- set type = type|default('text') -%}
{%- if type == 'range' or type == 'color' -%}
{# Attribute "required" is not supported #}
{%- set required = false -%}
{%- endif -%}
{%- if errors|length > 0 -%}
{% set attr = attr|merge({ 'aria-invalid': 'true', 'aria-describedby': 'error-' ~ id }) %}
{%- endif -%}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{%- endblock form_widget_simple -%}
{%- block form_row -%}
<fieldset{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
<legend>
<strong>
{{- label -}}
</strong>
</legend>
{{- form_widget(form) -}}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<div{% with {attr: row_attr} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_errors(form) -}}
{{- form_help(form) -}}
</fieldset>
</div>
{%- endblock form_row -%}
{%- block form_label -%}
{% set class = 'block font-semibold pb-2' %}
{% if errors|length %}
{% set class = class ~ ' text-red-500' %}
{% endif %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ class)|trim}) %}
{{ parent() }}
{%- endblock form_label -%}
{%- block form_errors -%}
{%- if errors|length > 0 -%}
<small id="error-{{ id }}">
<ul class="pb-2">
{%- for error in errors -%}
<div>{{ error.message }}</div>
<li class="text-red-500">{{ error.message }}</li>
{%- endfor -%}
</small>
</ul>
{%- endif -%}
{%- endblock form_errors -%}
{%- block choice_widget_expanded -%}
{%- for child in form %}
{{- form_widget(child) -}}
{% endfor -%}
{%- endblock choice_widget_expanded -%}
{%- block form_widget_simple -%}
{%- set type = type|default('text') -%}
{%- if type != 'hidden' -%}
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
{%- if errors|length -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
{% else %}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
{%- endif -%}
{%- if disabled is defined and disabled == true -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
{%- endif -%}
{%- endif -%}
{{ parent() }}
{%- if disabled is defined and disabled == true -%}
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
{%- endif -%}
{%- endblock form_widget_simple -%}
{%- block textarea_widget -%}
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
{%- if errors|length -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
{% else %}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
{%- endif -%}
{%- if disabled is defined and disabled == true -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
{%- endif -%}
{{ parent() }}
{%- if disabled is defined and disabled == true -%}
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
{%- endif -%}
{%- endblock textarea_widget -%}
{%- block checkbox_widget -%}
<label for="{{ id }}">
{{ parent() }}
{{ label|trans|raw }}
{%- if attr.disabled is defined and attr.disabled == true and attr.checked == true -%}
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
{%- endif -%}
</label>
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' h-4 w-4 rounded text-primary')|trim }) -%}
{%- if errors|length -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-red-500 text-red-500 ring-red-500 focus:ring-red-500' }) -%}
{% else %}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-gray-300 focus:ring-primary' }) -%}
{%- endif -%}
{%- if attr.disabled is defined and attr.disabled == true -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
{%- endif -%}
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{%- endblock checkbox_widget -%}
{%- block radio_widget -%}
<label for="{{ id }}">
{{ parent() }}
{{ label|trans|raw }}
</label>
{%- if attr.disabled is defined and attr.disabled == true and attr.checked == true -%}
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' h-4 w-4 rounded-full text-primary')|trim }) -%}
{%- if errors|length -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-red-500 text-red-500 ring-red-500 focus:ring-red-500' }) -%}
{% else %}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' border-gray-300 focus:ring-primary' }) -%}
{%- endif -%}
{%- if attr.disabled is defined and attr.disabled == true -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
{%- endif -%}
<input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{%- endblock radio_widget -%}
{% block checkbox_row %}
<div class="relative flex items-start">
<div class="flex h-6 items-center">
{{ form_widget(form) }}
</div>
<div class="{{ html_classes('ml-3 leading-6', { 'text-red-500': errors|length }) }}">
<label for="{{ form.vars.id }}">
{{ form.vars.label }}
</label>
{{- form_errors(form) -}}
</div>
</div>
{% endblock %}
{%- block choice_widget_collapsed -%}
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%}
{%- if errors|length -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
{% else %}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
{%- endif -%}
{%- if disabled is defined and disabled == true -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%}
{%- endif -%}
{{ parent() }}
{%- if disabled is defined and disabled == true -%}
<input type="hidden" name="{{ form.vars.full_name }}" value="{{ form.vars.value }}">
{%- endif -%}
{%- endblock radio_widget -%}
{%- endblock choice_widget_collapsed -%}
{%- block choice_widget_expanded -%}
{%- for child in form %}
<div class="relative flex items-start pb-2">
<div class="min-w-0 flex-1 text-sm leading-4">
<label for="{{ child.vars.id }}" class="select-none font-medium text-gray-900">
{{ child.vars.label }}
</label>
</div>
<div class="ml-3 flex h-4 items-center">
{{- form_widget(child) -}}
</div>
</div>
{% endfor -%}
{%- endblock choice_widget_expanded -%}
{%- block multiselect_widget -%}
<div class="relative" {{ stimulus_controller('multiselect', { 'placeholder': form.vars.empty_label }) }}>
<div {{ stimulus_action('multiselect', 'toggle', 'click')}}
role="button"
class="relative rounded-md border border-gray-300 flex items-start space-x-2 px-3">
<div {{ stimulus_target('multiselect', 'label') }} class="flex-1 min-h-8 py-1.5 text-gray-900 text-sm"></div>
{{ icon('dots', 'w-4 h-4 mt-2') }}
</div>
<div {{ stimulus_target('multiselect', 'dropdown')}} class="absolute z-10 top-full -mt-1.5 left-0 inset-x-0 max-h-48 overflow-y-scroll px-3 py-2 bg-white shadow-lg rounded-md rounded-t-none border border-gray-300 border-t-0 hidden">
{% for item in form.children %}
{{ form_row(item, { 'attr': { 'data-action': 'multiselect#update', 'data-multiselect-target': 'choice', 'data-label': item.vars.label } }) }}
{% endfor %}
</div>
</div>
{%- endblock multiselect_widget -%}
+7
View File
@@ -0,0 +1,7 @@
{% extends 'base.html.twig' %}
{% block body %}
<div hx-get="{{ path('app_login') }}"
hx-trigger="load"
hx-swap="outerHTML"></div>
{% endblock %}
+18 -55
View File
@@ -1,58 +1,21 @@
{% extends 'base.html.twig' %}
{% block body %}
<div id="app">
<div class="container py-8"
id="app"
hx-boost="true"
hx-indicator="#loading-indicator">
<div class="relative z-20 flex items-center justify-between pb-4">
<img class="h-8 w-auto" src="{{ asset('build/images/logo.svg') }}" alt="My E&amp;P Team">
{% if is_granted('ROLE_USER') %}
<nav>
<ul>
<li><strong>MyE&amp;P BETA</strong></li>
</ul>
<ul>
<li>
<a href="#"
class="contrast"
hx-get="{{ path('app_personal_data') }}"
hx-target="#app"
hx-swap="innerHTML"
hx-indicator="#loading-indicator">
Persönliche Daten
</a>
</li>
<li>
<a href="#"
class="contrast"
hx-get="{{ path('app_bookings') }}"
hx-target="#app"
hx-swap="innerHTML"
hx-indicator="#loading-indicator">
Buchungen
</a>
</li>
<li>
<a href="#"
class="contrast"
hx-get="{{ path('app_logout') }}"
hx-target="#app"
hx-swap="innerHTML"
hx-indicator="#loading-indicator">
Logout
</a>
</li>
</ul>
</nav>
{% endif %}
<div id="content">
{% for label, messages in app.flashes %}
{% for message in messages %}
<article>
{{ message }}
</article>
{% endfor %}
{% endfor %}
{% block content %}{% endblock %}
<div id="loading-indicator">
<progress />
<div class="hidden lg:block">
{{ knp_menu_render(knp_menu_get('main')) }}
</div>
</div>
{% endif %}
</div>
{% endblock %}
<div id="content">
{% include '_partials/_flashes.html.twig' %}
{% block content %}{% endblock %}
</div>
<div class="htmx-indicator fixed top-0 left-0 inset-0 z-10 bg-white/90 flex items-center justify-center"
id="loading-indicator">
{% include '_partials/_spinner.html.twig' with { 'class': 'w-8 h-8'} %}
</div>
</div>
+51 -23
View File
@@ -1,32 +1,60 @@
{% extends 'layout.html.twig' %}
{% block content %}
<h2>
Persönliche Daten
</h2>
{{ form_start(form) }}
<div class="grid">
<div class="grid md:grid-cols-2 gap-x-16 gap-y-4">
<div>
{{ form_row(form.gender) }}
{{ form_row(form.firstName) }}
{{ form_row(form.name) }}
{{ form_row(form.dateOfBirth) }}
<h2 class="text-2xl font-semibold pb-4">
Persönliche Daten
</h2>
{{ form_start(form) }}
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
{{ form_row(form.firstName) }}
{{ form_row(form.name) }}
{{ form_row(form.gender) }}
{{ form_row(form.dateOfBirth) }}
</div>
<h3 class="text-xl font-semibold pb-2">
Anschrift
</h3>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2">
{{ form_row(form.street) }}
</div>
{{ form_row(form.postCode) }}
{{ form_row(form.city) }}
{{ form_row(form.country) }}
</div>
<h3 class="text-xl font-semibold pb-2">
Kontakt
</h3>
<div class="grid grid-cols-2 gap-x-8 gap-y-4 pb-4">
<div class="col-span-2">
{{ form_row(form.email) }}
</div>
{{ form_row(form.phone) }}
{{ form_row(form.mobile) }}
</div>
<button type="submit" class="btn">
Speichern
</button>
{{ form_rest(form) }}
{{ form_end(form) }}
</div>
<div>
{{ form_row(form.street) }}
{{ form_row(form.postCode) }}
{{ form_row(form.city) }}
{{ form_row(form.country) }}
</div>
<div>
{{ form_row(form.email) }}
{{ form_row(form.phone) }}
{{ form_row(form.mobile) }}
<h2 class="text-2xl font-semibold pb-4">
Newsletter
</h2>
<p class="pb-4">
Du bist aktuell {% if not personalData.communication.newsletter %}<strong>nicht</strong> {% endif%} zum Newsletter
angemeldet.
</p>
<button type="button" class="btn" hx-post="{{ path('app_personal_data_newsletter') }}" hx-target="#app">
{% if personalData.communication.newsletter %}
vom Newsletter abmelden
{% else %}
zum Newsletter anmelden
{% endif %}
</button>
</div>
</div>
{{ form_rest(form) }}
<button type="submit">
Speichern
</button>
{{ form_end(form) }}
{% endblock %}
+49 -48
View File
@@ -1,52 +1,53 @@
{% extends 'layout.html.twig' %}
{% block content %}
{% if error %}
<div class="flex items-center space-x-1 bg-red-500 text-white p-2 rounded-md mb-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span class="flex-1 text-sm">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
</div>
{% endif %}
<form hx-post="{{ path('app_login') }}"
hx-target="#app"
hx-swap="innerHTML"
hx-indicator="#loading-indicator">
<div class="mb-6">
<label for="username" class="block leading-6 text-gray-900">
E-Mail:
</label>
<input type="email"
id="username"
name="_username"
value="{{ last_username }}"
class="mt-2 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-primary sm:text-sm sm:leading-6"
required
autofocus
autocomplete="username">
</div>
<div class="mb-6">
<label for="password" class="block leading-6 text-gray-900">
Passwort:
</label>
<input type="password"
id="password"
name="_password"
class="mt-2 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-primary sm:text-sm sm:leading-6"
required
autocomplete="current-password">
</div>
<div class="flex items-start mb-6">
<label for="rememberme" class="flex h-6 items-center">
<input type="checkbox" id="rememberme" name="_remember_me" class="h-4 w-4 rounded text-primary">
<span class="block ml-3 leading-6">angemeldet bleiben</span>
</label>
</div>
<button type="submit" class="btn w-full">
Login
</button>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
</form>
<div class="p-8 max-w-screen-sm border border-gray-300 rounded-md mx-auto">
{% if error %}
<div class="flex items-center space-x-1 bg-red-500 text-white p-2 rounded-md mb-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span class="flex-1 text-sm">{{ error.messageKey|trans(error.messageData, 'security') }}</span>
</div>
{% endif %}
<h2 class="text-2xl font-semibold pb-4">
Login MyE&amp;P
</h2>
<form action="{{ path('app_login') }}" method="post">
<div class="mb-6">
<label for="username" class="block leading-6 text-gray-900">
E-Mail:
</label>
<input type="email"
id="username"
name="_username"
value="{{ last_username }}"
class="mt-2 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-primary sm:text-sm sm:leading-6"
required
autofocus
autocomplete="username">
</div>
<div class="mb-6">
<label for="password" class="block leading-6 text-gray-900">
Passwort:
</label>
<input type="password"
id="password"
name="_password"
class="mt-2 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-primary sm:text-sm sm:leading-6"
required
autocomplete="current-password">
</div>
<div class="flex items-start mb-6">
<label for="rememberme" class="flex h-6 items-center">
<input type="checkbox" id="rememberme" name="_remember_me" class="h-4 w-4 rounded text-primary">
<span class="block ml-3 leading-6">angemeldet bleiben</span>
</label>
</div>
<button type="submit" class="btn w-full">
Login
</button>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
</form>
</div>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
const Encore = require('@symfony/webpack-encore');
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}
Encore
.setOutputPath('public/build/')
.setPublicPath('/build')
.addEntry('app', './assets/app.js')
.splitEntryChunks()
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
.enableStimulusBridge('./assets/controllers.json')
.enableStimulusBridge('./assets/controllers.json')
.enableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
.configureBabelPresetEnv((config) => {
config.useBuiltIns = 'usage';
config.corejs = '3.23';
})
.enablePostCssLoader()
.copyFiles([
{from: './assets/images', to: 'images/[path][name].[hash:8].[ext]'},
])
;
module.exports = Encore.getWebpackConfig();