diff --git a/assets/controllers/mobilenav_controller.js b/assets/controllers/mobilenav_controller.js new file mode 100644 index 0000000..6e64254 --- /dev/null +++ b/assets/controllers/mobilenav_controller.js @@ -0,0 +1,22 @@ +import { Controller } from '@hotwired/stimulus'; + +/* + * Off-canvas mobile navigation drawer. Ported from myep. + * + * Two instances cooperate through a Stimulus outlet: the opener button in the header and the + * drawer itself. Purely client-side -- htmx is not involved. + */ +export default class extends Controller { + static outlets = ['mobilenav']; + static classes = ['closed']; + + trigger({ params: { mode } }) { + this.mobilenavOutlet.toggle(mode); + } + + toggle(mode) { + // Added only now, so the drawer does not animate into place on first paint. + this.element.classList.add('transition-transform', 'duration-300'); + this.element.classList.toggle(this.closedClass, 'close' === mode); + } +} diff --git a/assets/icons/lucide/calendar-days.svg b/assets/icons/lucide/calendar-days.svg new file mode 100644 index 0000000..8b4b794 --- /dev/null +++ b/assets/icons/lucide/calendar-days.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/lucide/layout-grid.svg b/assets/icons/lucide/layout-grid.svg new file mode 100644 index 0000000..e1dae6c --- /dev/null +++ b/assets/icons/lucide/layout-grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/lucide/log-out.svg b/assets/icons/lucide/log-out.svg new file mode 100644 index 0000000..3fa1f9f --- /dev/null +++ b/assets/icons/lucide/log-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/lucide/menu.svg b/assets/icons/lucide/menu.svg new file mode 100644 index 0000000..a3ad789 --- /dev/null +++ b/assets/icons/lucide/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/lucide/x.svg b/assets/icons/lucide/x.svg new file mode 100644 index 0000000..7e18c3f --- /dev/null +++ b/assets/icons/lucide/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/composer.json b/composer.json index e3db310..04ad75b 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/orm": "^3.7", "phpdocumentor/reflection-docblock": "^6.0", "phpstan/phpdoc-parser": "^2.3", + "symfony/apache-pack": "*", "symfony/asset": "7.4.*", "symfony/asset-mapper": "7.4.*", "symfony/console": "7.4.*", @@ -37,6 +38,7 @@ "symfony/string": "7.4.*", "symfony/translation": "7.4.*", "symfony/twig-bundle": "7.4.*", + "symfony/ux-icons": "^3.4", "symfony/validator": "7.4.*", "symfony/web-link": "7.4.*", "symfony/yaml": "7.4.*", @@ -90,7 +92,7 @@ }, "extra": { "symfony": { - "allow-contrib": false, + "allow-contrib": true, "require": "7.4.*" } }, diff --git a/composer.lock b/composer.lock index 57a48ca..796d440 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "9cd4746d1b88efb4f3d1ecf392a958a9", + "content-hash": "ef19a7def0151d8b452378a05ec4c324", "packages": [ { "name": "composer/semver", @@ -1903,6 +1903,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": "v7.4.8", @@ -7260,6 +7286,99 @@ ], "time": "2026-08-21T15:52:48+00:00" }, + { + "name": "symfony/ux-icons", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-icons.git", + "reference": "117b5d9bc9652f364495d586235b9e1f1f7c9d52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-icons/zipball/117b5d9bc9652f364495d586235b9e1f1f7c9d52", + "reference": "117b5d9bc9652f364495d586235b9e1f1f7c9d52", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0" + }, + "conflict": { + "symfony/flex": "<1.13", + "symfony/ux-twig-component": "<2.21" + }, + "require-dev": { + "phpunit/phpunit": "^11.1|^12.0", + "psr/log": "^2|^3", + "symfony/asset-mapper": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/ux-twig-component": "^2.14|^3.0", + "zenstruck/console-test": "^1.5" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ux", + "name": "symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\Icons\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + }, + { + "name": "Simon André", + "email": "smn.andre@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Renders local and remote SVG icons in your Twig templates.", + "homepage": "https://symfony.com", + "keywords": [ + "icons", + "svg", + "symfony-ux", + "twig" + ], + "support": { + "source": "https://github.com/symfony/ux-icons/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-29T16:44:14+00:00" + }, { "name": "symfony/validator", "version": "v7.4.18", diff --git a/config/bundles.php b/config/bundles.php index daace3e..0dcf330 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -12,4 +12,5 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\UX\Icons\UXIconsBundle::class => ['all' => true], ]; diff --git a/config/packages/ux_icons.yaml b/config/packages/ux_icons.yaml new file mode 100644 index 0000000..e2c4739 --- /dev/null +++ b/config/packages/ux_icons.yaml @@ -0,0 +1,27 @@ +# Full configuration reference: https://symfony.com/bundles/ux-icons/current/index.html#configuration +ux_icons: + + # Default HTML attributes to add to all icons + default_icon_attributes: + + # Use current text color to fill the icon + fill: currentColor + + # Default dimensions + height: '1em' + width: '1em' + + # Throw an exception if an icon is not + ignore_not_found: false + + +when@dev: + ux_icons: + iconify: + # Persist each on-demand icon to disk on first render, so you don't forget to commit it before deploying (like `ux:icons:lock`) + auto_lock: true + +when@prod: + ux_icons: + # Suppress exceptions for missing icons in production + ignore_not_found: true diff --git a/config/reference.php b/config/reference.php index a10dcd7..052ac9b 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1470,6 +1470,26 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * generate_final_classes?: bool|Param, // Default: true * generate_final_entities?: bool|Param, // Default: false * } + * @psalm-type UxIconsConfig = array{ + * icon_dir?: scalar|Param|null, // The local directory where icons are stored. // Default: "%kernel.project_dir%/assets/icons" + * default_icon_attributes?: array, + * icon_sets?: array, + * suffixes?: array, + * }>, + * }>, + * aliases?: array, + * iconify?: bool|array{ // Configuration for the remote icon service. + * enabled?: bool|Param, // Default: true + * on_demand?: bool|Param, // Whether to download icons "on demand". // Default: true + * auto_lock?: bool|Param, // Persist "on demand" icons to the local icon directory (see "icon_dir"). Recommended in dev only. Requires "on_demand" to be enabled. // Default: false + * endpoint?: scalar|Param|null, // The endpoint for the Iconify icons API. // Default: "https://api.iconify.design" + * }, + * ignore_not_found?: bool|Param, // Ignore error when an icon is not found. Set to 'true' to fail silently. // Default: false + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1482,6 +1502,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * twig_extra?: TwigExtraConfig, * security?: SecurityConfig, * monolog?: MonologConfig, + * ux_icons?: UxIconsConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1497,6 +1518,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * security?: SecurityConfig, * monolog?: MonologConfig, * maker?: MakerConfig, + * ux_icons?: UxIconsConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -1510,6 +1532,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * twig_extra?: TwigExtraConfig, * security?: SecurityConfig, * monolog?: MonologConfig, + * ux_icons?: UxIconsConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -1524,6 +1547,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * twig_extra?: TwigExtraConfig, * security?: SecurityConfig, * monolog?: MonologConfig, + * ux_icons?: UxIconsConfig, * }, * ... + Options -MultiViews + + + + # 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=308,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] + + + + + # 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 + + diff --git a/src/Controller/BookingController.php b/src/Controller/BookingController.php new file mode 100644 index 0000000..1eb8693 --- /dev/null +++ b/src/Controller/BookingController.php @@ -0,0 +1,18 @@ +render('booking/index.html.twig'); + } +} diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php new file mode 100644 index 0000000..154932b --- /dev/null +++ b/src/Controller/DashboardController.php @@ -0,0 +1,18 @@ +render('dashboard/index.html.twig'); + } +} diff --git a/symfony.lock b/symfony.lock index ff240df..8727581 100644 --- a/symfony.lock +++ b/symfony.lock @@ -50,6 +50,18 @@ "bin/phpunit" ] }, + "symfony/apache-pack": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5d454ec6cc4c700ed3d963f3803e1d427d9669fb" + }, + "files": [ + "public/.htaccess" + ] + }, "symfony/asset-mapper": { "version": "7.4", "recipe": { @@ -270,6 +282,19 @@ "templates/base.html.twig" ] }, + "symfony/ux-icons": { + "version": "3.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.4", + "ref": "16a3114526e46593b494471d1654dfa46831beb5" + }, + "files": [ + "assets/icons/symfony.svg", + "config/packages/ux_icons.yaml" + ] + }, "symfony/validator": { "version": "7.4", "recipe": { diff --git a/templates/_partials/_flashes.html.twig b/templates/_partials/_flashes.html.twig new file mode 100644 index 0000000..c69c24b --- /dev/null +++ b/templates/_partials/_flashes.html.twig @@ -0,0 +1,13 @@ +{# Flat inline alerts for now. myep renders these as Toastify toasts, but that means adopting + toastify-js as a dependency -- deferred until the toast/OOB work (plan step 7). #} +{% set levels = { error: 'bg-red-600', warning: 'bg-red-600', success: 'bg-emerald-600', info: 'bg-primary' } %} + +{% for level, messages in app.flashes(levels|keys) %} +
+ {% for message in messages %} +
+ {{ message|trans }} +
+ {% endfor %} +
+{% endfor %} diff --git a/templates/_partials/_nav.html.twig b/templates/_partials/_nav.html.twig new file mode 100644 index 0000000..868d963 --- /dev/null +++ b/templates/_partials/_nav.html.twig @@ -0,0 +1,25 @@ +{# Plain nav instead of myep's knp-menu builder: a handful of links does not justify the + indirection. Active state is a prefix match on the current route name, which mirrors what + myep's menu matcher does with its 'routes' extras. #} +{% set current_route = app.request.attributes.get('_route') %} +{% set items = [ + { route: 'app_dashboard', label: 'Übersicht', icon: 'lucide:layout-grid' }, + { route: 'app_booking', label: 'Meine Buchungen', icon: 'lucide:calendar-days' }, +] %} + +
+ +
diff --git a/templates/base.html.twig b/templates/base.html.twig index 04b94f6..c8d2e2a 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -1,24 +1,23 @@ - + - {% block title %}Welcome!{% endblock %} - - {% block stylesheets %} - {% endblock %} + + {# Internal tool -- keep it out of search engines. #} + + {% block title %}ephub{% endblock %} + + + {# The Tailwind build is pulled in by assets/app.js's `import './styles/app.css'`, + so importmap() emits the for it. This block is for anything outside that graph. #} + {% block stylesheets %}{% endblock %} {% block javascripts %} {% block importmap %}{{ importmap('app') }}{% endblock %} {% endblock %} - - {% set frankenphp_hot_reload = app.request.server.get('FRANKENPHP_HOT_RELOAD') %} - {% if frankenphp_hot_reload %} - - - - {% endif %} - + {# No hx-boost: htmx handles fragments only, the browser owns navigation. #} + {% block body %}{% endblock %} diff --git a/templates/booking/index.html.twig b/templates/booking/index.html.twig new file mode 100644 index 0000000..f0b2bbb --- /dev/null +++ b/templates/booking/index.html.twig @@ -0,0 +1,7 @@ +{% extends 'layout.html.twig' %} + +{% block title %}Meine Buchungen · ephub{% endblock %} + +{% block content %} +

Meine Buchungen

+{% endblock %} diff --git a/templates/dashboard/index.html.twig b/templates/dashboard/index.html.twig new file mode 100644 index 0000000..cf05999 --- /dev/null +++ b/templates/dashboard/index.html.twig @@ -0,0 +1,8 @@ +{% extends 'layout.html.twig' %} + +{% block title %}Übersicht · ephub{% endblock %} + +{% block content %} +

Übersicht

+

Schreibtische und Parkplätze buchen.

+{% endblock %} diff --git a/templates/layout.html.twig b/templates/layout.html.twig new file mode 100644 index 0000000..6b3468f --- /dev/null +++ b/templates/layout.html.twig @@ -0,0 +1,43 @@ +{% extends 'base.html.twig' %} + +{% block body %} +
+
+ + ephub + + +
+ + + +
+ {% block content %}{% endblock %} +
+
+ +
+
+
+ ephub + +
+
+ {% include '_partials/_nav.html.twig' %} +
+
+
+ + {% include '_partials/_flashes.html.twig' %} +{% endblock %}