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
|
||||
|
||||
```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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ module.exports = {
|
|||
screens: {
|
||||
'md': '920px',
|
||||
},
|
||||
fontFamily: {
|
||||
hedvig: ['Hedvig Letters Sans', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,32 @@ 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).
|
||||
_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([
|
||||
'type' => 'appointment',
|
||||
'name' => 'Appointment',
|
||||
|
|
@ -17,7 +41,9 @@ function riverside_pt_install() {
|
|||
'new_revision' => FALSE,
|
||||
'display_submitted' => FALSE,
|
||||
])->save();
|
||||
}
|
||||
|
||||
if (!$storage_node_type->load('provider_availability')) {
|
||||
NodeType::create([
|
||||
'type' => 'provider_availability',
|
||||
'name' => 'Provider Availability',
|
||||
|
|
@ -25,24 +51,25 @@ function riverside_pt_install() {
|
|||
'new_revision' => FALSE,
|
||||
'display_submitted' => FALSE,
|
||||
])->save();
|
||||
}
|
||||
|
||||
// --- Role (idempotent) ---
|
||||
if (!$storage_role->load('provider')) {
|
||||
Role::create(['id' => 'provider', 'label' => 'Provider'])->save();
|
||||
}
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_appointment_date',
|
||||
// --- Field storages (idempotent) ---
|
||||
$field_storages = [
|
||||
'field_appointment_date' => [
|
||||
'entity_type' => 'node',
|
||||
'type' => 'datetime',
|
||||
'settings' => ['datetime_type' => 'datetime'],
|
||||
])->save();
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_duration_minutes',
|
||||
],
|
||||
'field_duration_minutes' => [
|
||||
'entity_type' => 'node',
|
||||
'type' => 'integer',
|
||||
])->save();
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_service_type',
|
||||
],
|
||||
'field_service_type' => [
|
||||
'entity_type' => 'node',
|
||||
'type' => 'list_string',
|
||||
'settings' => [
|
||||
|
|
@ -53,58 +80,60 @@ function riverside_pt_install() {
|
|||
'neurological_pt' => 'Neurological PT',
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_provider',
|
||||
],
|
||||
'field_provider' => [
|
||||
'entity_type' => 'node',
|
||||
'type' => 'entity_reference',
|
||||
'settings' => ['target_type' => 'user'],
|
||||
])->save();
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_start_datetime',
|
||||
],
|
||||
'field_start_datetime' => [
|
||||
'entity_type' => 'node',
|
||||
'type' => 'datetime',
|
||||
'settings' => ['datetime_type' => 'datetime'],
|
||||
])->save();
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_end_datetime',
|
||||
],
|
||||
'field_end_datetime' => [
|
||||
'entity_type' => 'node',
|
||||
'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();
|
||||
|
||||
// Pass 2: field configs (depend on storages being in the DB).
|
||||
FieldConfig::create([
|
||||
// --- 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,
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
],
|
||||
'node.appointment.field_duration_minutes' => [
|
||||
'field_name' => 'field_duration_minutes',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'appointment',
|
||||
'label' => 'Duration (Minutes)',
|
||||
'required' => TRUE,
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
],
|
||||
'node.appointment.field_service_type' => [
|
||||
'field_name' => 'field_service_type',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'appointment',
|
||||
'label' => 'Service Type',
|
||||
'required' => TRUE,
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
],
|
||||
'node.appointment.field_provider' => [
|
||||
'field_name' => 'field_provider',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'appointment',
|
||||
|
|
@ -116,9 +145,9 @@ function riverside_pt_install() {
|
|||
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
],
|
||||
// Provider availability bundle
|
||||
'node.provider_availability.field_provider' => [
|
||||
'field_name' => 'field_provider',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'provider_availability',
|
||||
|
|
@ -130,24 +159,30 @@ function riverside_pt_install() {
|
|||
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
],
|
||||
'node.provider_availability.field_start_datetime' => [
|
||||
'field_name' => 'field_start_datetime',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'provider_availability',
|
||||
'label' => 'Start',
|
||||
'required' => TRUE,
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
],
|
||||
'node.provider_availability.field_end_datetime' => [
|
||||
'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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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>
|
||||
<h2 class="text-[2.75rem] font-serif font-normal text-gray-900 leading-tight">Our Wide Range of Physical Therapy Services</h2>
|
||||
<p class="text-sm tracking-widest uppercase text-[#306f8e] font-semibold text-center mb-2">Bringing Relief</p>
|
||||
<h2 class="text-[2.25rem] font-serif font-normal text-gray-900 leading-tight">Our Wide Range of Physical Therapy Services</h2>
|
||||
</div>
|
||||
<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">
|
||||
|
|
@ -96,8 +96,8 @@
|
|||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[4.5rem] font-serif text-[#306f8e] leading-none">300</p>
|
||||
<div class="font-hedvig">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue