diff --git a/CLAUDE.md b/CLAUDE.md index 56e36d8..88a6855 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,12 +7,39 @@ A Drupal 11 site for Riverside Physical Therapy. Nearly all frontend work lives ## Running locally ```bash -docker compose up # starts app on http://localhost:8080 +docker compose up # starts app on http://localhost:8080 (full DB wipe + rebuild from code by default) docker compose exec app drush cr # clear Drupal cache npm run watch # Tailwind CSS watcher (run on host, not in container) npm run build # minified production build ``` +### Database & site rebuild behavior + +By default, **every** `docker compose up` performs a full database wipe followed by a complete reinstall + rebuild of the site structure from code: + +- Drops the database +- Runs `drush site:install standard` +- Enables modules (including `riverside_pt`) +- Runs `drush riverside:rebuild` (the single source of truth for content types, fields, roles, and navigation) + +This means the site is **always** built exactly the same way from the code in `riverside_pt.install` and the Drush command. There is no persistent data between restarts unless you opt out. + +**Faster iteration (preserve the database):** + +```bash +DRUPAL_FAST=1 docker compose up +``` + +This skips the wipe + `site:install` but still runs `drush riverside:rebuild` and the rest of the startup steps. Use this when you want quicker starts during active development and don't need a completely clean slate. + +You can also run the rebuild manually at any time: + +```bash +docker compose exec app drush riverside:rebuild +# or the short alias +docker compose exec app drush rrb +``` + The custom module directory is volume-mounted, so template/CSS/JS edits are live without rebuilding the Docker image. `settings.php` and `development.services.yml` are also volume-mounted. ## Stack diff --git a/README.md b/README.md index c47aa99..9ffe073 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ A Drupal-based appointment scheduling site for booking sessions between patients docker compose up --build ``` +**Default behavior**: Every start performs a full database wipe + rebuilds the entire site from code (content types, fields, menu, etc.). See CLAUDE.md for details and the `DRUPAL_FAST=1` escape hatch for faster iteration. + Admin login: `admin` / `admin` at `/user/login` ## Makefile commands diff --git a/docker-compose.yml b/docker-compose.yml index 318e9b4..159bc14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,10 @@ services: DEBUG: "${DEBUG:-true}" POSTMARK_API_KEY: "${POSTMARK_API_KEY:?POSTMARK_API_KEY is required}" BASE_URL: "${BASE_URL:-http://localhost:8080}" + # DRUPAL_FAST=1 skips the automatic full database wipe + reinstall on every start. + # Use this for faster iteration when you don't want a completely fresh site. + # Default (no flag) = always wipe DB and rebuild everything from code. + DRUPAL_FAST: "${DRUPAL_FAST:-}" volumes: - ./web/sites/default/files:/var/www/html/web/sites/default/files - ./web/sites/default/settings.php:/var/www/html/web/sites/default/settings.php diff --git a/docker/php/entrypoint.sh b/docker/php/entrypoint.sh index dee2564..86a143d 100644 --- a/docker/php/entrypoint.sh +++ b/docker/php/entrypoint.sh @@ -22,12 +22,15 @@ cd /var/www/html DRUSH="vendor/bin/drush --root=/var/www/html/web" -HAS_TABLES=$($DRUSH sql:query \ - "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='users';" \ - 2>/dev/null || echo "0") +echo "[entrypoint] Preparing database..." -if [ "$HAS_TABLES" != "1" ]; then - echo "[entrypoint] Fresh database, installing Drupal..." +if [ "${DRUPAL_FAST:-}" = "1" ]; then + echo "[entrypoint] DRUPAL_FAST=1 — skipping database wipe and full site reinstall." +else + echo "[entrypoint] Full rebuild mode (default). Dropping database..." + $DRUSH sql:drop -y || true + + echo "[entrypoint] Installing Drupal (standard profile)..." $DRUSH site:install standard \ --site-name="$SITE_NAME" \ --account-name=admin \ @@ -46,19 +49,18 @@ $DRUSH en -y symfony_mailer && \ $DRUSH en -y riverside_pt && \ echo "[entrypoint] riverside_pt enabled." || echo "[entrypoint] WARNING: riverside_pt failed." +echo "[entrypoint] Rebuilding site structure from code (riverside:rebuild)..." +$DRUSH riverside:rebuild || echo "[entrypoint] WARNING: riverside:rebuild encountered an issue." + +# Re-assert a few key pieces (cheap and safe). $DRUSH theme:enable starterkit_theme claro_compact -y && \ $DRUSH config:set system.theme default starterkit_theme -y && \ $DRUSH config:set system.theme admin claro_compact -y && \ echo "[entrypoint] Themes set." || echo "[entrypoint] WARNING: theme enable failed." + $DRUSH config:set system.site page.front /home -y && \ echo "[entrypoint] Front page set." || echo "[entrypoint] WARNING: front page set failed." -if ls /var/www/html/config/sync/*.yml >/dev/null 2>&1; then - echo "[entrypoint] Importing configuration..." - $DRUSH config:import -y && \ - echo "[entrypoint] Config imported." || echo "[entrypoint] WARNING: config import failed." -fi - npm run build --prefix /var/www/html >/dev/null 2>&1 && echo "[entrypoint] Tailwind built." || echo "[entrypoint] WARNING: Tailwind build failed." $DRUSH cache:rebuild >/dev/null 2>&1 && echo "[entrypoint] Cache rebuilt." diff --git a/tailwind.config.js b/tailwind.config.js index 55a7dc0..7250cf5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -10,6 +10,9 @@ module.exports = { screens: { 'md': '920px', }, + fontFamily: { + hedvig: ['Hedvig Letters Sans', 'sans-serif'], + }, }, }, plugins: [], diff --git a/web/modules/custom/riverside_pt/css/app.css b/web/modules/custom/riverside_pt/css/app.css index 435c21d..104241b 100644 --- a/web/modules/custom/riverside_pt/css/app.css +++ b/web/modules/custom/riverside_pt/css/app.css @@ -1,4 +1,6 @@ -*, ::before, ::after { +@import url('https://fonts.googleapis.com/css2?family=Hedvig+Letters+Sans:wght@400;500;600&display=swap'); + +*, ::before, ::after{ --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; @@ -52,7 +54,7 @@ --tw-contain-style: ; } -::backdrop { +::backdrop{ --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; --tw-translate-x: 0; @@ -558,36 +560,36 @@ video { /* Hide Olivero/theme chrome we don't use */ -.container { +.container{ width: 100%; } -@media (min-width: 640px) { - .container { +@media (min-width: 640px){ + .container{ max-width: 640px; } } -@media (min-width: 920px) { - .container { +@media (min-width: 920px){ + .container{ max-width: 920px; } } -@media (min-width: 1024px) { - .container { +@media (min-width: 1024px){ + .container{ max-width: 1024px; } } -@media (min-width: 1280px) { - .container { +@media (min-width: 1280px){ + .container{ max-width: 1280px; } } -@media (min-width: 1536px) { - .container { +@media (min-width: 1536px){ + .container{ max-width: 1536px; } } @@ -635,778 +637,733 @@ video { transform: translateY(-10px) rotate(-45deg); } -.static { +.static{ position: static; } -.fixed { - position: fixed; -} - -.absolute { +.absolute{ position: absolute; } -.relative { +.relative{ position: relative; } -.inset-0 { +.inset-0{ inset: 0px; } -.left-3 { +.left-3{ left: 0.75rem; } -.right-3 { +.right-3{ right: 0.75rem; } -.top-1\/2 { +.top-1\/2{ top: 50%; } -.right-4 { - right: 1rem; -} - -.top-4 { - top: 1rem; -} - -.z-50 { - z-index: 50; -} - -.m-0 { +.m-0{ margin: 0px; } -.mx-auto { +.mx-auto{ margin-left: auto; margin-right: auto; } -.mb-0 { +.mb-0{ margin-bottom: 0px; } -.mb-1\.5 { +.mb-1\.5{ margin-bottom: 0.375rem; } -.mb-10 { +.mb-10{ margin-bottom: 2.5rem; } -.mb-12 { +.mb-12{ margin-bottom: 3rem; } -.mb-3 { +.mb-3{ margin-bottom: 0.75rem; } -.mb-8 { +.mb-8{ margin-bottom: 2rem; } -.ml-auto { +.ml-auto{ margin-left: auto; } -.mt-0 { +.mt-0{ margin-top: 0px; } -.mt-2 { +.mt-2{ margin-top: 0.5rem; } -.block { +.mb-2{ + margin-bottom: 0.5rem; +} + +.block{ display: block; } -.inline-block { +.inline-block{ display: inline-block; } -.flex { +.flex{ display: flex; } -.inline-flex { +.inline-flex{ display: inline-flex; } -.grid { +.grid{ display: grid; } -.hidden { +.hidden{ display: none; } -.h-1 { +.h-1{ height: 0.25rem; } -.h-10 { +.h-10{ height: 2.5rem; } -.h-11 { +.h-11{ height: 2.75rem; } -.h-3 { +.h-3{ height: 0.75rem; } -.h-48 { +.h-48{ height: 12rem; } -.h-72 { +.h-72{ height: 18rem; } -.h-\[420px\] { +.h-\[420px\]{ height: 420px; } -.h-8 { - height: 2rem; -} - -.max-h-\[calc\(100\%_-_100px\)\] { +.max-h-\[calc\(100\%_-_100px\)\]{ max-height: calc(100% - 100px); } -.min-h-\[560px\] { +.min-h-\[560px\]{ min-height: 560px; } -.w-10 { +.w-10{ width: 2.5rem; } -.w-11 { +.w-11{ width: 2.75rem; } -.w-3 { +.w-3{ width: 0.75rem; } -.w-\[45\%\] { +.w-\[45\%\]{ width: 45%; } -.w-full { +.w-full{ width: 100%; } -.w-8 { - width: 2rem; -} - -.min-w-0 { +.min-w-0{ min-width: 0px; } -.max-w-\[1040px\] { +.max-w-\[1040px\]{ max-width: 1040px; } -.max-w-\[1200px\] { +.max-w-\[1200px\]{ max-width: 1200px; } -.max-w-\[680px\] { +.max-w-\[680px\]{ max-width: 680px; } -.max-w-lg { +.max-w-lg{ max-width: 32rem; } -.flex-1 { +.flex-1{ flex: 1 1 0%; } -.shrink-0 { +.shrink-0{ flex-shrink: 0; } -.basis-full { +.basis-full{ flex-basis: 100%; } -.-translate-y-1\/2 { +.-translate-y-1\/2{ --tw-translate-y: -50%; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.transform { +.transform{ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.cursor-pointer { +.cursor-pointer{ cursor: pointer; } -.resize { +.resize{ resize: both; } -.list-none { +.list-none{ list-style-type: none; } -.grid-cols-1 { +.grid-cols-1{ grid-template-columns: repeat(1, minmax(0, 1fr)); } -.flex-col { +.flex-col{ flex-direction: column; } -.flex-wrap { +.flex-wrap{ flex-wrap: wrap; } -.items-center { +.items-center{ align-items: center; } -.justify-end { +.justify-end{ justify-content: flex-end; } -.justify-center { +.justify-center{ justify-content: center; } -.gap-1 { +.gap-1{ gap: 0.25rem; } -.gap-1\.5 { +.gap-1\.5{ gap: 0.375rem; } -.gap-12 { +.gap-12{ gap: 3rem; } -.gap-2 { +.gap-2{ gap: 0.5rem; } -.gap-3 { +.gap-3{ gap: 0.75rem; } -.gap-4 { +.gap-4{ gap: 1rem; } -.gap-6 { +.gap-6{ gap: 1.5rem; } -.gap-8 { +.gap-8{ gap: 2rem; } -.gap-\[1vw\] { +.gap-\[1vw\]{ gap: 1vw; } -.gap-x-8 { +.gap-x-8{ -moz-column-gap: 2rem; column-gap: 2rem; } -.gap-y-0 { +.gap-y-0{ row-gap: 0px; } -.self-end { +.self-end{ align-self: flex-end; } -.self-center { +.self-center{ align-self: center; } -.overflow-hidden { +.overflow-hidden{ overflow: hidden; } -.whitespace-nowrap { +.whitespace-nowrap{ white-space: nowrap; } -.rounded { +.rounded{ border-radius: 0.25rem; } -.rounded-\[3px\] { +.rounded-\[3px\]{ border-radius: 3px; } -.rounded-full { +.rounded-full{ border-radius: 9999px; } -.rounded-lg { +.rounded-lg{ border-radius: 0.5rem; } -.rounded-none { +.rounded-none{ border-radius: 0px; } -.border { +.border{ border-width: 1px; } -.border-0 { +.border-0{ border-width: 0px; } -.border-2 { +.border-2{ border-width: 2px; } -.border-\[\#1e3a5f\] { +.border-\[\#1e3a5f\]{ --tw-border-opacity: 1; border-color: rgb(30 58 95 / var(--tw-border-opacity, 1)); } -.border-\[\#306f8e\] { +.border-\[\#306f8e\]{ --tw-border-opacity: 1; border-color: rgb(48 111 142 / var(--tw-border-opacity, 1)); } -.border-\[\#b8d4dc\] { +.border-\[\#b8d4dc\]{ --tw-border-opacity: 1; border-color: rgb(184 212 220 / var(--tw-border-opacity, 1)); } -.border-gray-300 { +.border-gray-300{ --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); } -.border-white\/60 { +.border-white\/60{ border-color: rgb(255 255 255 / 0.6); } -.bg-\[\#1e3a5f\] { +.bg-\[\#1e3a5f\]{ --tw-bg-opacity: 1; background-color: rgb(30 58 95 / var(--tw-bg-opacity, 1)); } -.bg-\[\#2d5f7a\] { +.bg-\[\#2d5f7a\]{ --tw-bg-opacity: 1; background-color: rgb(45 95 122 / var(--tw-bg-opacity, 1)); } -.bg-\[\#306f8e\] { +.bg-\[\#306f8e\]{ --tw-bg-opacity: 1; background-color: rgb(48 111 142 / var(--tw-bg-opacity, 1)); } -.bg-\[\#89a0a0\] { +.bg-\[\#89a0a0\]{ --tw-bg-opacity: 1; background-color: rgb(137 160 160 / var(--tw-bg-opacity, 1)); } -.bg-\[\#dde8f0\] { +.bg-\[\#dde8f0\]{ --tw-bg-opacity: 1; background-color: rgb(221 232 240 / var(--tw-bg-opacity, 1)); } -.bg-current { +.bg-current{ background-color: currentColor; } -.bg-transparent { +.bg-transparent{ background-color: transparent; } -.bg-white { +.bg-white{ --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); } -.bg-white\/90 { +.bg-white\/90{ background-color: rgb(255 255 255 / 0.9); } -.bg-black\/50 { - background-color: rgb(0 0 0 / 0.5); -} - -.bg-gradient-to-t { +.bg-gradient-to-t{ background-image: linear-gradient(to top, var(--tw-gradient-stops)); } -.from-black\/60 { +.from-black\/60{ --tw-gradient-from: rgb(0 0 0 / 0.6) var(--tw-gradient-from-position); --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } -.via-black\/30 { +.via-black\/30{ --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), rgb(0 0 0 / 0.3) var(--tw-gradient-via-position), var(--tw-gradient-to); } -.to-transparent { +.to-transparent{ --tw-gradient-to: transparent var(--tw-gradient-to-position); } -.object-cover { +.object-cover{ -o-object-fit: cover; object-fit: cover; } -.object-\[center_0\%\] { +.object-\[center_0\%\]{ -o-object-position: center 0%; object-position: center 0%; } -.p-0 { +.p-0{ padding: 0px; } -.p-2 { +.p-2{ padding: 0.5rem; } -.p-5 { +.p-5{ padding: 1.25rem; } -.p-6 { +.p-6{ padding: 1.5rem; } -.p-4 { - padding: 1rem; -} - -.px-2 { +.px-2{ padding-left: 0.5rem; padding-right: 0.5rem; } -.px-3\.5 { +.px-3\.5{ padding-left: 0.875rem; padding-right: 0.875rem; } -.px-4 { +.px-4{ padding-left: 1rem; padding-right: 1rem; } -.px-5 { +.px-5{ padding-left: 1.25rem; padding-right: 1.25rem; } -.px-6 { +.px-6{ padding-left: 1.5rem; padding-right: 1.5rem; } -.px-\[4em\] { +.px-\[4em\]{ padding-left: 4em; padding-right: 4em; } -.py-1 { +.py-1{ padding-top: 0.25rem; padding-bottom: 0.25rem; } -.py-16 { +.py-16{ padding-top: 4rem; padding-bottom: 4rem; } -.py-2 { +.py-2{ padding-top: 0.5rem; padding-bottom: 0.5rem; } -.py-20 { +.py-20{ padding-top: 5rem; padding-bottom: 5rem; } -.py-24 { +.py-24{ padding-top: 6rem; padding-bottom: 6rem; } -.py-3 { +.py-3{ padding-top: 0.75rem; padding-bottom: 0.75rem; } -.py-8 { +.py-8{ padding-top: 2rem; padding-bottom: 2rem; } -.py-\[1em\] { +.py-\[1em\]{ padding-top: 1em; padding-bottom: 1em; } -.px-8 { - padding-left: 2rem; - padding-right: 2rem; -} - -.py-6 { - padding-top: 1.5rem; - padding-bottom: 1.5rem; -} - -.pb-4 { +.pb-4{ padding-bottom: 1rem; } -.pt-0 { +.pt-0{ padding-top: 0px; } -.pt-4 { +.pt-4{ padding-top: 1rem; } -.pt-\[78px\] { +.pt-\[78px\]{ padding-top: 78px; } -.text-center { +.text-center{ text-align: center; } -.font-serif { +.font-serif{ font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; } -.text-2xl { +.font-hedvig{ + font-family: Hedvig Letters Sans, sans-serif; +} + +.text-2xl{ font-size: 1.5rem; line-height: 2rem; } -.text-3xl { +.text-3xl{ font-size: 1.875rem; line-height: 2.25rem; } -.text-\[1\.5vw\] { +.text-\[1\.5vw\]{ font-size: 1.5vw; } -.text-\[15px\] { +.text-\[15px\]{ font-size: 15px; } -.text-\[17px\] { +.text-\[17px\]{ font-size: 17px; } -.text-\[1vw\] { +.text-\[1vw\]{ font-size: 1vw; } -.text-\[2\.75rem\] { +.text-\[2\.75rem\]{ font-size: 2.75rem; } -.text-\[4\.5rem\] { +.text-\[4\.5rem\]{ font-size: 4.5rem; } -.text-\[clamp\(1\.5rem\2c 3\.5vw\2c 3\.25rem\)\] { +.text-\[clamp\(1\.5rem\2c 3\.5vw\2c 3\.25rem\)\]{ font-size: clamp(1.5rem, 3.5vw, 3.25rem); } -.text-\[clamp\(2\.5rem\2c 4vw\2c 3\.75rem\)\] { +.text-\[clamp\(2\.5rem\2c 4vw\2c 3\.75rem\)\]{ font-size: clamp(2.5rem, 4vw, 3.75rem); } -.text-\[clamp\(2\.5rem\2c 5vw\2c 4rem\)\] { +.text-\[clamp\(2\.5rem\2c 5vw\2c 4rem\)\]{ font-size: clamp(2.5rem, 5vw, 4rem); } -.text-base { +.text-base{ font-size: 1rem; line-height: 1.5rem; } -.text-lg { +.text-lg{ font-size: 1.125rem; line-height: 1.75rem; } -.text-sm { +.text-sm{ font-size: 0.875rem; line-height: 1.25rem; } -.text-xs { +.text-xs{ font-size: 0.75rem; line-height: 1rem; } -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; +.text-\[2\.25rem\]{ + font-size: 2.25rem; } -.font-bold { +.font-bold{ font-weight: 700; } -.font-medium { +.font-medium{ font-weight: 500; } -.font-normal { +.font-normal{ font-weight: 400; } -.font-semibold { +.font-semibold{ font-weight: 600; } -.uppercase { +.uppercase{ text-transform: uppercase; } -.leading-\[1\.1\] { +.leading-\[1\.1\]{ line-height: 1.1; } -.leading-none { +.leading-none{ line-height: 1; } -.leading-relaxed { +.leading-relaxed{ line-height: 1.625; } -.leading-tight { +.leading-tight{ line-height: 1.25; } -.tracking-widest { +.tracking-widest{ letter-spacing: 0.1em; } -.text-\[\#1e3a5f\] { +.text-\[\#1e3a5f\]{ --tw-text-opacity: 1; color: rgb(30 58 95 / var(--tw-text-opacity, 1)); } -.text-\[\#1e3a8a\] { +.text-\[\#1e3a8a\]{ --tw-text-opacity: 1; color: rgb(30 58 138 / var(--tw-text-opacity, 1)); } -.text-\[\#306f8e\] { +.text-\[\#306f8e\]{ --tw-text-opacity: 1; color: rgb(48 111 142 / var(--tw-text-opacity, 1)); } -.text-blue-900 { +.text-blue-900{ --tw-text-opacity: 1; color: rgb(30 58 138 / var(--tw-text-opacity, 1)); } -.text-gray-400 { +.text-gray-400{ --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity, 1)); } -.text-gray-500 { +.text-gray-500{ --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity, 1)); } -.text-gray-600 { +.text-gray-600{ --tw-text-opacity: 1; color: rgb(75 85 99 / var(--tw-text-opacity, 1)); } -.text-gray-700 { +.text-gray-700{ --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity, 1)); } -.text-gray-800 { +.text-gray-800{ --tw-text-opacity: 1; color: rgb(31 41 55 / var(--tw-text-opacity, 1)); } -.text-gray-900 { +.text-gray-900{ --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity, 1)); } -.text-white { +.text-white{ --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } -.text-white\/80 { +.text-white\/80{ color: rgb(255 255 255 / 0.8); } -.text-white\/70 { - color: rgb(255 255 255 / 0.7); -} - -.no-underline { +.no-underline{ text-decoration-line: none; } -.shadow { +.shadow{ --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.shadow-sm { +.shadow-sm{ --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.shadow-2xl { - --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); - --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.outline-none { +.outline-none{ outline: 2px solid transparent; outline-offset: 2px; } -.transition { +.transition{ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; @@ -1414,214 +1371,209 @@ video { transition-duration: 150ms; } -.transition-\[transform\2c opacity\] { +.transition-\[transform\2c opacity\]{ transition-property: transform,opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } -.transition-colors { +.transition-colors{ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } -.transition-transform { +.transition-transform{ transition-property: transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } -.duration-200 { +.duration-200{ transition-duration: 200ms; } -.duration-\[250ms\] { +.duration-\[250ms\]{ transition-duration: 250ms; } -.ease-in-out { +.ease-in-out{ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } -.\[text-shadow\:-56\.21px_2\.55px_10\.22px_\#0000001A\] { +.\[text-shadow\:-56\.21px_2\.55px_10\.22px_\#0000001A\]{ text-shadow: -56.21px 2.55px 10.22px #0000001A; } -.hover\:border-\[\#152a45\]:hover { +.hover\:border-\[\#152a45\]:hover{ --tw-border-opacity: 1; border-color: rgb(21 42 69 / var(--tw-border-opacity, 1)); } -.hover\:bg-\[\#152a45\]:hover { +.hover\:bg-\[\#152a45\]:hover{ --tw-bg-opacity: 1; background-color: rgb(21 42 69 / var(--tw-bg-opacity, 1)); } -.hover\:bg-\[\#1e4a60\]:hover { +.hover\:bg-\[\#1e4a60\]:hover{ --tw-bg-opacity: 1; background-color: rgb(30 74 96 / var(--tw-bg-opacity, 1)); } -.hover\:bg-\[\#3a6a7a\]:hover { +.hover\:bg-\[\#3a6a7a\]:hover{ --tw-bg-opacity: 1; background-color: rgb(58 106 122 / var(--tw-bg-opacity, 1)); } -.hover\:bg-white\/10:hover { +.hover\:bg-white\/10:hover{ background-color: rgb(255 255 255 / 0.1); } -.hover\:text-\[\#1e3a8a\]:hover { +.hover\:text-\[\#1e3a8a\]:hover{ --tw-text-opacity: 1; color: rgb(30 58 138 / var(--tw-text-opacity, 1)); } -.hover\:text-white:hover { +.hover\:text-white:hover{ --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } -.hover\:text-gray-600:hover { - --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity, 1)); -} - -.disabled\:opacity-30:disabled { +.disabled\:opacity-30:disabled{ opacity: 0.3; } -@media (min-width: 640px) { - .sm\:block { +@media (min-width: 640px){ + .sm\:block{ display: block; } - .sm\:inline-block { + .sm\:inline-block{ display: inline-block; } - .sm\:hidden { + .sm\:hidden{ display: none; } - .sm\:w-auto { + .sm\:w-auto{ width: auto; } - .sm\:grow { + .sm\:grow{ flex-grow: 1; } - .sm\:grow-\[2\] { + .sm\:grow-\[2\]{ flex-grow: 2; } - .sm\:basis-\[10\%\] { + .sm\:basis-\[10\%\]{ flex-basis: 10%; } - .sm\:basis-\[34\%\] { + .sm\:basis-\[34\%\]{ flex-basis: 34%; } - .sm\:basis-\[40\%\] { + .sm\:basis-\[40\%\]{ flex-basis: 40%; } - .sm\:basis-\[50\%\] { + .sm\:basis-\[50\%\]{ flex-basis: 50%; } - .sm\:basis-\[58\%\] { + .sm\:basis-\[58\%\]{ flex-basis: 58%; } - .sm\:basis-\[8\%\] { + .sm\:basis-\[8\%\]{ flex-basis: 8%; } - .sm\:grid-cols-2 { + .sm\:grid-cols-2{ grid-template-columns: repeat(2, minmax(0, 1fr)); } - .sm\:justify-center { + .sm\:justify-center{ justify-content: center; } - .sm\:px-0 { + .sm\:px-0{ padding-left: 0px; padding-right: 0px; } - .sm\:pb-0 { + .sm\:pb-0{ padding-bottom: 0px; } } -@media (min-width: 920px) { - .md\:fixed { +@media (min-width: 920px){ + .md\:fixed{ position: fixed; } - .md\:left-0 { + .md\:left-0{ left: 0px; } - .md\:right-0 { + .md\:right-0{ right: 0px; } - .md\:top-0 { + .md\:top-0{ top: 0px; } - .md\:z-50 { + .md\:z-50{ z-index: 50; } - .md\:hidden { + .md\:hidden{ display: none; } - .md\:h-\[520px\] { + .md\:h-\[520px\]{ height: 520px; } - .md\:h-full { + .md\:h-full{ height: 100%; } - .md\:w-\[46\%\] { + .md\:w-\[46\%\]{ width: 46%; } - .md\:flex-row { + .md\:flex-row{ flex-direction: row; } - .md\:gap-20 { + .md\:gap-20{ gap: 5rem; } - .md\:gap-8 { + .md\:gap-8{ gap: 2rem; } - .md\:bg-\[red\] { + .md\:bg-\[red\]{ --tw-bg-opacity: 1; background-color: rgb(255 0 0 / var(--tw-bg-opacity, 1)); } } -@media (min-width: 1024px) { - .lg\:bg-\[blue\] { +@media (min-width: 1024px){ + .lg\:bg-\[blue\]{ --tw-bg-opacity: 1; background-color: rgb(0 0 255 / var(--tw-bg-opacity, 1)); } } -@media (min-width: 1280px) { - .xl\:grid-cols-4 { +@media (min-width: 1280px){ + .xl\:grid-cols-4{ grid-template-columns: repeat(4, minmax(0, 1fr)); } } diff --git a/web/modules/custom/riverside_pt/css/tailwind.css b/web/modules/custom/riverside_pt/css/tailwind.css index dc07848..9b85a69 100644 --- a/web/modules/custom/riverside_pt/css/tailwind.css +++ b/web/modules/custom/riverside_pt/css/tailwind.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Hedvig+Letters+Sans:wght@400;500;600&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/web/modules/custom/riverside_pt/riverside_pt.install b/web/modules/custom/riverside_pt/riverside_pt.install index f5cf9b4..adcdb11 100644 --- a/web/modules/custom/riverside_pt/riverside_pt.install +++ b/web/modules/custom/riverside_pt/riverside_pt.install @@ -8,146 +8,181 @@ use Drupal\user\Entity\Role; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldConfig; +/** + * Implements hook_install(). + * + * Delegates to the rebuild function so that normal module installs and + * container rebuilds both produce the same result. + */ function riverside_pt_install() { - // Pass 1: types, roles, and field storages (no inter-dependencies). - NodeType::create([ - 'type' => 'appointment', - 'name' => 'Appointment', - 'description' => 'A booking between a Patient and a Provider at a particular time.', - 'new_revision' => FALSE, - 'display_submitted' => FALSE, - ])->save(); + _riverside_pt_rebuild(); +} - NodeType::create([ - 'type' => 'provider_availability', - 'name' => 'Provider Availability', - 'description' => 'A window of time during which a Provider is available for appointments.', - 'new_revision' => FALSE, - 'display_submitted' => FALSE, - ])->save(); +/** + * Rebuilds the Riverside PT site structure from code. + * + * This function is idempotent and can be safely called multiple times. + * It is the single source of truth for content types, fields, roles, + * and navigation used by the Docker entrypoint. + */ +function _riverside_pt_rebuild(): void { + $entity_type_manager = \Drupal::entityTypeManager(); + $storage_node_type = $entity_type_manager->getStorage('node_type'); + $storage_role = $entity_type_manager->getStorage('user_role'); + $storage_field_storage = $entity_type_manager->getStorage('field_storage_config'); + $storage_field_config = $entity_type_manager->getStorage('field_config'); - Role::create(['id' => 'provider', 'label' => 'Provider'])->save(); + // --- Content types (idempotent) --- + if (!$storage_node_type->load('appointment')) { + NodeType::create([ + 'type' => 'appointment', + 'name' => 'Appointment', + 'description' => 'A booking between a Patient and a Provider at a particular time.', + 'new_revision' => FALSE, + 'display_submitted' => FALSE, + ])->save(); + } - FieldStorageConfig::create([ - 'field_name' => 'field_appointment_date', - 'entity_type' => 'node', - 'type' => 'datetime', - 'settings' => ['datetime_type' => 'datetime'], - ])->save(); + if (!$storage_node_type->load('provider_availability')) { + NodeType::create([ + 'type' => 'provider_availability', + 'name' => 'Provider Availability', + 'description' => 'A window of time during which a Provider is available for appointments.', + 'new_revision' => FALSE, + 'display_submitted' => FALSE, + ])->save(); + } - FieldStorageConfig::create([ - 'field_name' => 'field_duration_minutes', - 'entity_type' => 'node', - 'type' => 'integer', - ])->save(); + // --- Role (idempotent) --- + if (!$storage_role->load('provider')) { + Role::create(['id' => 'provider', 'label' => 'Provider'])->save(); + } - FieldStorageConfig::create([ - 'field_name' => 'field_service_type', - 'entity_type' => 'node', - 'type' => 'list_string', - 'settings' => [ - 'allowed_values' => [ - 'diagnostic' => 'Diagnostic', - 'sports_rehab' => 'Sports Rehab', - 'pre_post_surgical_rehab' => 'Pre/Post-Surgical Rehab', - 'neurological_pt' => 'Neurological PT', + // --- Field storages (idempotent) --- + $field_storages = [ + 'field_appointment_date' => [ + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => ['datetime_type' => 'datetime'], + ], + 'field_duration_minutes' => [ + 'entity_type' => 'node', + 'type' => 'integer', + ], + 'field_service_type' => [ + 'entity_type' => 'node', + 'type' => 'list_string', + 'settings' => [ + 'allowed_values' => [ + 'diagnostic' => 'Diagnostic', + 'sports_rehab' => 'Sports Rehab', + 'pre_post_surgical_rehab' => 'Pre/Post-Surgical Rehab', + 'neurological_pt' => 'Neurological PT', + ], ], ], - ])->save(); + 'field_provider' => [ + 'entity_type' => 'node', + 'type' => 'entity_reference', + 'settings' => ['target_type' => 'user'], + ], + 'field_start_datetime' => [ + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => ['datetime_type' => 'datetime'], + ], + 'field_end_datetime' => [ + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => ['datetime_type' => 'datetime'], + ], + ]; - FieldStorageConfig::create([ - 'field_name' => 'field_provider', - 'entity_type' => 'node', - 'type' => 'entity_reference', - 'settings' => ['target_type' => 'user'], - ])->save(); + foreach ($field_storages as $field_name => $definition) { + if (!$storage_field_storage->load("node.$field_name")) { + FieldStorageConfig::create([ + 'field_name' => $field_name, + ] + $definition)->save(); + } + } - FieldStorageConfig::create([ - 'field_name' => 'field_start_datetime', - 'entity_type' => 'node', - 'type' => 'datetime', - 'settings' => ['datetime_type' => 'datetime'], - ])->save(); - - FieldStorageConfig::create([ - 'field_name' => 'field_end_datetime', - 'entity_type' => 'node', - 'type' => 'datetime', - 'settings' => ['datetime_type' => 'datetime'], - ])->save(); - - // Clear field definition cache so FieldConfig::preSave() can find the storages. + // Clear field definition cache so FieldConfig creation can see the storages. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); - // Pass 2: field configs (depend on storages being in the DB). - FieldConfig::create([ - 'field_name' => 'field_appointment_date', - 'entity_type' => 'node', - 'bundle' => 'appointment', - 'label' => 'Appointment Date', - 'required' => TRUE, - ])->save(); - - FieldConfig::create([ - 'field_name' => 'field_duration_minutes', - 'entity_type' => 'node', - 'bundle' => 'appointment', - 'label' => 'Duration (Minutes)', - 'required' => TRUE, - ])->save(); - - FieldConfig::create([ - 'field_name' => 'field_service_type', - 'entity_type' => 'node', - 'bundle' => 'appointment', - 'label' => 'Service Type', - 'required' => TRUE, - ])->save(); - - FieldConfig::create([ - 'field_name' => 'field_provider', - 'entity_type' => 'node', - 'bundle' => 'appointment', - 'label' => 'Provider', - 'required' => TRUE, - 'settings' => [ - 'handler' => 'default:user', - 'handler_settings' => [ - 'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']], + // --- Field configs (idempotent) --- + $field_configs = [ + // Appointment bundle + 'node.appointment.field_appointment_date' => [ + 'field_name' => 'field_appointment_date', + 'entity_type' => 'node', + 'bundle' => 'appointment', + 'label' => 'Appointment Date', + 'required' => TRUE, + ], + 'node.appointment.field_duration_minutes' => [ + 'field_name' => 'field_duration_minutes', + 'entity_type' => 'node', + 'bundle' => 'appointment', + 'label' => 'Duration (Minutes)', + 'required' => TRUE, + ], + 'node.appointment.field_service_type' => [ + 'field_name' => 'field_service_type', + 'entity_type' => 'node', + 'bundle' => 'appointment', + 'label' => 'Service Type', + 'required' => TRUE, + ], + 'node.appointment.field_provider' => [ + 'field_name' => 'field_provider', + 'entity_type' => 'node', + 'bundle' => 'appointment', + 'label' => 'Provider', + 'required' => TRUE, + 'settings' => [ + 'handler' => 'default:user', + 'handler_settings' => [ + 'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']], + ], ], ], - ])->save(); - - FieldConfig::create([ - 'field_name' => 'field_provider', - 'entity_type' => 'node', - 'bundle' => 'provider_availability', - 'label' => 'Provider', - 'required' => TRUE, - 'settings' => [ - 'handler' => 'default:user', - 'handler_settings' => [ - 'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']], + // Provider availability bundle + 'node.provider_availability.field_provider' => [ + 'field_name' => 'field_provider', + 'entity_type' => 'node', + 'bundle' => 'provider_availability', + 'label' => 'Provider', + 'required' => TRUE, + 'settings' => [ + 'handler' => 'default:user', + 'handler_settings' => [ + 'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']], + ], ], ], - ])->save(); + 'node.provider_availability.field_start_datetime' => [ + 'field_name' => 'field_start_datetime', + 'entity_type' => 'node', + 'bundle' => 'provider_availability', + 'label' => 'Start', + 'required' => TRUE, + ], + 'node.provider_availability.field_end_datetime' => [ + 'field_name' => 'field_end_datetime', + 'entity_type' => 'node', + 'bundle' => 'provider_availability', + 'label' => 'End', + 'required' => TRUE, + ], + ]; - FieldConfig::create([ - 'field_name' => 'field_start_datetime', - 'entity_type' => 'node', - 'bundle' => 'provider_availability', - 'label' => 'Start', - 'required' => TRUE, - ])->save(); - - FieldConfig::create([ - 'field_name' => 'field_end_datetime', - 'entity_type' => 'node', - 'bundle' => 'provider_availability', - 'label' => 'End', - 'required' => TRUE, - ])->save(); + foreach ($field_configs as $field_id => $definition) { + if (!$storage_field_config->load($field_id)) { + FieldConfig::create($definition)->save(); + } + } + // Navigation and basic site settings (these are intentionally destructive on menu). try { _riverside_pt_build_navigation(); } diff --git a/web/modules/custom/riverside_pt/src/Commands/RiversidePtCommands.php b/web/modules/custom/riverside_pt/src/Commands/RiversidePtCommands.php new file mode 100644 index 0000000..8a8a115 --- /dev/null +++ b/web/modules/custom/riverside_pt/src/Commands/RiversidePtCommands.php @@ -0,0 +1,43 @@ +output()->writeln('Rebuilding Riverside PT site structure from code...'); + + // Make the helper functions from riverside_pt.install available. + \Drupal::moduleHandler()->loadInclude('riverside_pt', 'install'); + + if (!function_exists('_riverside_pt_rebuild')) { + $this->logger()->error('Could not load _riverside_pt_rebuild().'); + return; + } + + _riverside_pt_rebuild(); + + $this->output()->writeln('Rebuild complete.'); + $this->logger()->success('Riverside PT structure has been rebuilt from code.'); + } +} diff --git a/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig b/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig index 2553ccb..67db45e 100644 --- a/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig +++ b/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig @@ -46,8 +46,8 @@
-

Bringing Relief

-

Our Wide Range of Physical Therapy Services

+

Bringing Relief

+

Our Wide Range of Physical Therapy Services

@@ -96,8 +96,8 @@

15

Years Open

-
-

300

+
+

300

Patients Served