Tweak lifecycle to make more stateless, fix some styling
This commit is contained in:
parent
5ea7e69f5a
commit
cd2d59f298
10 changed files with 497 additions and 427 deletions
29
CLAUDE.md
29
CLAUDE.md
|
|
@ -7,12 +7,39 @@ A Drupal 11 site for Riverside Physical Therapy. Nearly all frontend work lives
|
||||||
## Running locally
|
## Running locally
|
||||||
|
|
||||||
```bash
|
```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
|
docker compose exec app drush cr # clear Drupal cache
|
||||||
npm run watch # Tailwind CSS watcher (run on host, not in container)
|
npm run watch # Tailwind CSS watcher (run on host, not in container)
|
||||||
npm run build # minified production build
|
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.
|
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
|
## Stack
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ A Drupal-based appointment scheduling site for booking sessions between patients
|
||||||
docker compose up --build
|
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`
|
Admin login: `admin` / `admin` at `/user/login`
|
||||||
|
|
||||||
## Makefile commands
|
## Makefile commands
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ services:
|
||||||
DEBUG: "${DEBUG:-true}"
|
DEBUG: "${DEBUG:-true}"
|
||||||
POSTMARK_API_KEY: "${POSTMARK_API_KEY:?POSTMARK_API_KEY is required}"
|
POSTMARK_API_KEY: "${POSTMARK_API_KEY:?POSTMARK_API_KEY is required}"
|
||||||
BASE_URL: "${BASE_URL:-http://localhost:8080}"
|
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:
|
volumes:
|
||||||
- ./web/sites/default/files:/var/www/html/web/sites/default/files
|
- ./web/sites/default/files:/var/www/html/web/sites/default/files
|
||||||
- ./web/sites/default/settings.php:/var/www/html/web/sites/default/settings.php
|
- ./web/sites/default/settings.php:/var/www/html/web/sites/default/settings.php
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,15 @@ cd /var/www/html
|
||||||
|
|
||||||
DRUSH="vendor/bin/drush --root=/var/www/html/web"
|
DRUSH="vendor/bin/drush --root=/var/www/html/web"
|
||||||
|
|
||||||
HAS_TABLES=$($DRUSH sql:query \
|
echo "[entrypoint] Preparing database..."
|
||||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='users';" \
|
|
||||||
2>/dev/null || echo "0")
|
|
||||||
|
|
||||||
if [ "$HAS_TABLES" != "1" ]; then
|
if [ "${DRUPAL_FAST:-}" = "1" ]; then
|
||||||
echo "[entrypoint] Fresh database, installing Drupal..."
|
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 \
|
$DRUSH site:install standard \
|
||||||
--site-name="$SITE_NAME" \
|
--site-name="$SITE_NAME" \
|
||||||
--account-name=admin \
|
--account-name=admin \
|
||||||
|
|
@ -46,19 +49,18 @@ $DRUSH en -y symfony_mailer && \
|
||||||
$DRUSH en -y riverside_pt && \
|
$DRUSH en -y riverside_pt && \
|
||||||
echo "[entrypoint] riverside_pt enabled." || echo "[entrypoint] WARNING: riverside_pt failed."
|
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 theme:enable starterkit_theme claro_compact -y && \
|
||||||
$DRUSH config:set system.theme default starterkit_theme -y && \
|
$DRUSH config:set system.theme default starterkit_theme -y && \
|
||||||
$DRUSH config:set system.theme admin claro_compact -y && \
|
$DRUSH config:set system.theme admin claro_compact -y && \
|
||||||
echo "[entrypoint] Themes set." || echo "[entrypoint] WARNING: theme enable failed."
|
echo "[entrypoint] Themes set." || echo "[entrypoint] WARNING: theme enable failed."
|
||||||
|
|
||||||
$DRUSH config:set system.site page.front /home -y && \
|
$DRUSH config:set system.site page.front /home -y && \
|
||||||
echo "[entrypoint] Front page set." || echo "[entrypoint] WARNING: front page set failed."
|
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."
|
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."
|
$DRUSH cache:rebuild >/dev/null 2>&1 && echo "[entrypoint] Cache rebuilt."
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ module.exports = {
|
||||||
screens: {
|
screens: {
|
||||||
'md': '920px',
|
'md': '920px',
|
||||||
},
|
},
|
||||||
|
fontFamily: {
|
||||||
|
hedvig: ['Hedvig Letters Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Hedvig+Letters+Sans:wght@400;500;600&display=swap');
|
||||||
|
|
||||||
*, ::before, ::after{
|
*, ::before, ::after{
|
||||||
--tw-border-spacing-x: 0;
|
--tw-border-spacing-x: 0;
|
||||||
--tw-border-spacing-y: 0;
|
--tw-border-spacing-y: 0;
|
||||||
|
|
@ -639,10 +641,6 @@ video {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixed {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.absolute{
|
.absolute{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
@ -667,18 +665,6 @@ video {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-4 {
|
|
||||||
right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-4 {
|
|
||||||
top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.z-50 {
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.m-0{
|
.m-0{
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
@ -724,6 +710,10 @@ video {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-2{
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.block{
|
.block{
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
@ -776,10 +766,6 @@ video {
|
||||||
height: 420px;
|
height: 420px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-8 {
|
|
||||||
height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-\[calc\(100\%_-_100px\)\]{
|
.max-h-\[calc\(100\%_-_100px\)\]{
|
||||||
max-height: calc(100% - 100px);
|
max-height: calc(100% - 100px);
|
||||||
}
|
}
|
||||||
|
|
@ -808,10 +794,6 @@ video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-8 {
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.min-w-0{
|
.min-w-0{
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
@ -1048,10 +1030,6 @@ video {
|
||||||
background-color: rgb(255 255 255 / 0.9);
|
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));
|
background-image: linear-gradient(to top, var(--tw-gradient-stops));
|
||||||
}
|
}
|
||||||
|
|
@ -1097,10 +1075,6 @@ video {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-4 {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.px-2{
|
.px-2{
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
|
|
@ -1171,16 +1145,6 @@ video {
|
||||||
padding-bottom: 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;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -1205,6 +1169,10 @@ video {
|
||||||
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-hedvig{
|
||||||
|
font-family: Hedvig Letters Sans, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
.text-2xl{
|
.text-2xl{
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
|
@ -1271,9 +1239,8 @@ video {
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-xl {
|
.text-\[2\.25rem\]{
|
||||||
font-size: 1.25rem;
|
font-size: 2.25rem;
|
||||||
line-height: 1.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-bold{
|
.font-bold{
|
||||||
|
|
@ -1375,10 +1342,6 @@ video {
|
||||||
color: rgb(255 255 255 / 0.8);
|
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;
|
text-decoration-line: none;
|
||||||
}
|
}
|
||||||
|
|
@ -1395,12 +1358,6 @@ video {
|
||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
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: 2px solid transparent;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
|
|
@ -1482,11 +1439,6 @@ video {
|
||||||
color: rgb(255 255 255 / var(--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;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Hedvig+Letters+Sans:wght@400;500;600&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,32 @@ use Drupal\user\Entity\Role;
|
||||||
use Drupal\field\Entity\FieldStorageConfig;
|
use Drupal\field\Entity\FieldStorageConfig;
|
||||||
use Drupal\field\Entity\FieldConfig;
|
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() {
|
function riverside_pt_install() {
|
||||||
// Pass 1: types, roles, and field storages (no inter-dependencies).
|
_riverside_pt_rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
|
||||||
|
// --- Content types (idempotent) ---
|
||||||
|
if (!$storage_node_type->load('appointment')) {
|
||||||
NodeType::create([
|
NodeType::create([
|
||||||
'type' => 'appointment',
|
'type' => 'appointment',
|
||||||
'name' => 'Appointment',
|
'name' => 'Appointment',
|
||||||
|
|
@ -17,7 +41,9 @@ function riverside_pt_install() {
|
||||||
'new_revision' => FALSE,
|
'new_revision' => FALSE,
|
||||||
'display_submitted' => FALSE,
|
'display_submitted' => FALSE,
|
||||||
])->save();
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$storage_node_type->load('provider_availability')) {
|
||||||
NodeType::create([
|
NodeType::create([
|
||||||
'type' => 'provider_availability',
|
'type' => 'provider_availability',
|
||||||
'name' => 'Provider Availability',
|
'name' => 'Provider Availability',
|
||||||
|
|
@ -25,24 +51,25 @@ function riverside_pt_install() {
|
||||||
'new_revision' => FALSE,
|
'new_revision' => FALSE,
|
||||||
'display_submitted' => FALSE,
|
'display_submitted' => FALSE,
|
||||||
])->save();
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Role (idempotent) ---
|
||||||
|
if (!$storage_role->load('provider')) {
|
||||||
Role::create(['id' => 'provider', 'label' => 'Provider'])->save();
|
Role::create(['id' => 'provider', 'label' => 'Provider'])->save();
|
||||||
|
}
|
||||||
|
|
||||||
FieldStorageConfig::create([
|
// --- Field storages (idempotent) ---
|
||||||
'field_name' => 'field_appointment_date',
|
$field_storages = [
|
||||||
|
'field_appointment_date' => [
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'type' => 'datetime',
|
'type' => 'datetime',
|
||||||
'settings' => ['datetime_type' => 'datetime'],
|
'settings' => ['datetime_type' => 'datetime'],
|
||||||
])->save();
|
],
|
||||||
|
'field_duration_minutes' => [
|
||||||
FieldStorageConfig::create([
|
|
||||||
'field_name' => 'field_duration_minutes',
|
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
])->save();
|
],
|
||||||
|
'field_service_type' => [
|
||||||
FieldStorageConfig::create([
|
|
||||||
'field_name' => 'field_service_type',
|
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'type' => 'list_string',
|
'type' => 'list_string',
|
||||||
'settings' => [
|
'settings' => [
|
||||||
|
|
@ -53,58 +80,60 @@ function riverside_pt_install() {
|
||||||
'neurological_pt' => 'Neurological PT',
|
'neurological_pt' => 'Neurological PT',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
])->save();
|
],
|
||||||
|
'field_provider' => [
|
||||||
FieldStorageConfig::create([
|
|
||||||
'field_name' => 'field_provider',
|
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'type' => 'entity_reference',
|
'type' => 'entity_reference',
|
||||||
'settings' => ['target_type' => 'user'],
|
'settings' => ['target_type' => 'user'],
|
||||||
])->save();
|
],
|
||||||
|
'field_start_datetime' => [
|
||||||
FieldStorageConfig::create([
|
|
||||||
'field_name' => 'field_start_datetime',
|
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'type' => 'datetime',
|
'type' => 'datetime',
|
||||||
'settings' => ['datetime_type' => 'datetime'],
|
'settings' => ['datetime_type' => 'datetime'],
|
||||||
])->save();
|
],
|
||||||
|
'field_end_datetime' => [
|
||||||
FieldStorageConfig::create([
|
|
||||||
'field_name' => 'field_end_datetime',
|
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'type' => 'datetime',
|
'type' => 'datetime',
|
||||||
'settings' => ['datetime_type' => 'datetime'],
|
'settings' => ['datetime_type' => 'datetime'],
|
||||||
])->save();
|
],
|
||||||
|
];
|
||||||
|
|
||||||
// Clear field definition cache so FieldConfig::preSave() can find the storages.
|
foreach ($field_storages as $field_name => $definition) {
|
||||||
|
if (!$storage_field_storage->load("node.$field_name")) {
|
||||||
|
FieldStorageConfig::create([
|
||||||
|
'field_name' => $field_name,
|
||||||
|
] + $definition)->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear field definition cache so FieldConfig creation can see the storages.
|
||||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||||
|
|
||||||
// Pass 2: field configs (depend on storages being in the DB).
|
// --- Field configs (idempotent) ---
|
||||||
FieldConfig::create([
|
$field_configs = [
|
||||||
|
// Appointment bundle
|
||||||
|
'node.appointment.field_appointment_date' => [
|
||||||
'field_name' => 'field_appointment_date',
|
'field_name' => 'field_appointment_date',
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'bundle' => 'appointment',
|
'bundle' => 'appointment',
|
||||||
'label' => 'Appointment Date',
|
'label' => 'Appointment Date',
|
||||||
'required' => TRUE,
|
'required' => TRUE,
|
||||||
])->save();
|
],
|
||||||
|
'node.appointment.field_duration_minutes' => [
|
||||||
FieldConfig::create([
|
|
||||||
'field_name' => 'field_duration_minutes',
|
'field_name' => 'field_duration_minutes',
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'bundle' => 'appointment',
|
'bundle' => 'appointment',
|
||||||
'label' => 'Duration (Minutes)',
|
'label' => 'Duration (Minutes)',
|
||||||
'required' => TRUE,
|
'required' => TRUE,
|
||||||
])->save();
|
],
|
||||||
|
'node.appointment.field_service_type' => [
|
||||||
FieldConfig::create([
|
|
||||||
'field_name' => 'field_service_type',
|
'field_name' => 'field_service_type',
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'bundle' => 'appointment',
|
'bundle' => 'appointment',
|
||||||
'label' => 'Service Type',
|
'label' => 'Service Type',
|
||||||
'required' => TRUE,
|
'required' => TRUE,
|
||||||
])->save();
|
],
|
||||||
|
'node.appointment.field_provider' => [
|
||||||
FieldConfig::create([
|
|
||||||
'field_name' => 'field_provider',
|
'field_name' => 'field_provider',
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'bundle' => 'appointment',
|
'bundle' => 'appointment',
|
||||||
|
|
@ -116,9 +145,9 @@ function riverside_pt_install() {
|
||||||
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
|
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
])->save();
|
],
|
||||||
|
// Provider availability bundle
|
||||||
FieldConfig::create([
|
'node.provider_availability.field_provider' => [
|
||||||
'field_name' => 'field_provider',
|
'field_name' => 'field_provider',
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'bundle' => 'provider_availability',
|
'bundle' => 'provider_availability',
|
||||||
|
|
@ -130,24 +159,30 @@ function riverside_pt_install() {
|
||||||
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
|
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
])->save();
|
],
|
||||||
|
'node.provider_availability.field_start_datetime' => [
|
||||||
FieldConfig::create([
|
|
||||||
'field_name' => 'field_start_datetime',
|
'field_name' => 'field_start_datetime',
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'bundle' => 'provider_availability',
|
'bundle' => 'provider_availability',
|
||||||
'label' => 'Start',
|
'label' => 'Start',
|
||||||
'required' => TRUE,
|
'required' => TRUE,
|
||||||
])->save();
|
],
|
||||||
|
'node.provider_availability.field_end_datetime' => [
|
||||||
FieldConfig::create([
|
|
||||||
'field_name' => 'field_end_datetime',
|
'field_name' => 'field_end_datetime',
|
||||||
'entity_type' => 'node',
|
'entity_type' => 'node',
|
||||||
'bundle' => 'provider_availability',
|
'bundle' => 'provider_availability',
|
||||||
'label' => 'End',
|
'label' => 'End',
|
||||||
'required' => TRUE,
|
'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 {
|
try {
|
||||||
_riverside_pt_build_navigation();
|
_riverside_pt_build_navigation();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\riverside_pt\Commands;
|
||||||
|
|
||||||
|
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||||
|
use Drush\Attributes as CLI;
|
||||||
|
use Drush\Commands\DrushCommands;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drush commands for Riverside PT site management.
|
||||||
|
*/
|
||||||
|
final class RiversidePtCommands extends DrushCommands implements ContainerInjectionInterface {
|
||||||
|
|
||||||
|
public static function create(ContainerInterface $container): self {
|
||||||
|
return new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the entire Riverside PT site structure from code.
|
||||||
|
*
|
||||||
|
* This is the command used by the Docker entrypoint on every start
|
||||||
|
* (unless DRUPAL_FAST=1 is passed).
|
||||||
|
*/
|
||||||
|
#[CLI\Command(name: 'riverside:rebuild', aliases: ['rrb'])]
|
||||||
|
#[CLI\Usage(name: 'drush riverside:rebuild', description: 'Rebuild content types, fields, roles, and navigation from code. Safe to run repeatedly.')]
|
||||||
|
public function rebuild(): void {
|
||||||
|
$this->output()->writeln('<info>Rebuilding Riverside PT site structure from code...</info>');
|
||||||
|
|
||||||
|
// 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('<info>Rebuild complete.</info>');
|
||||||
|
$this->logger()->success('Riverside PT structure has been rebuilt from code.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,8 +46,8 @@
|
||||||
|
|
||||||
<section class="py-16 px-6 bg-white">
|
<section class="py-16 px-6 bg-white">
|
||||||
<div class="max-w-[1040px] mx-auto mb-12">
|
<div class="max-w-[1040px] mx-auto mb-12">
|
||||||
<p class="text-xs tracking-widest uppercase text-[#306f8e] font-semibold text-center mb-3">Bringing Relief</p>
|
<p class="text-sm tracking-widest uppercase text-[#306f8e] font-semibold text-center mb-2">Bringing Relief</p>
|
||||||
<h2 class="text-[2.75rem] font-serif font-normal text-gray-900 leading-tight">Our Wide Range of Physical Therapy Services</h2>
|
<h2 class="text-[2.25rem] font-serif font-normal text-gray-900 leading-tight">Our Wide Range of Physical Therapy Services</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 max-w-[1040px] mx-auto">
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 max-w-[1040px] mx-auto">
|
||||||
<div class="flex flex-col border border-[#b8d4dc] bg-white overflow-hidden">
|
<div class="flex flex-col border border-[#b8d4dc] bg-white overflow-hidden">
|
||||||
|
|
@ -96,8 +96,8 @@
|
||||||
<p class="text-[4.5rem] font-serif text-[#306f8e] leading-none">15</p>
|
<p class="text-[4.5rem] font-serif text-[#306f8e] leading-none">15</p>
|
||||||
<p class="text-xs tracking-widest uppercase text-[#306f8e] font-semibold mt-2">Years Open</p>
|
<p class="text-xs tracking-widest uppercase text-[#306f8e] font-semibold mt-2">Years Open</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="font-hedvig">
|
||||||
<p class="text-[4.5rem] font-serif text-[#306f8e] leading-none">300</p>
|
<p class="text-[4.5rem] text-[#306f8e] leading-none">300</p>
|
||||||
<p class="text-xs tracking-widest uppercase text-[#306f8e] font-semibold mt-2">Patients Served</p>
|
<p class="text-xs tracking-widest uppercase text-[#306f8e] font-semibold mt-2">Patients Served</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue