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
-