Add front page, nav rebuild, and CI workflow
- Home controller with hero and services sections - Nav rebuilt on install: Home, Services, About, FAQ, Contact (CTA), Book An Appointment (CTA) - Entrypoint uses IS_SETUP check so failed installs retry on restart - Gitea Actions workflow builds and pushes multi-arch image to forge.quinefoundation.com Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d9e6d852d9
commit
ed6ff4fbf6
9 changed files with 374 additions and 20 deletions
33
.gitea/workflows/build.yml
Normal file
33
.gitea/workflows/build.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: Build and push image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: forge.quinefoundation.com
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: forge.quinefoundation.com/ironmagma/riverside:latest
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
DB_HOST="${DB_HOST:-postgres}"
|
||||
DB_USER="${DB_USER:-drupal}"
|
||||
|
|
@ -19,39 +18,50 @@ HAS_TABLES=$($DRUSH sql:query \
|
|||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='config';" \
|
||||
2>/dev/null || echo "0")
|
||||
|
||||
if [ "$HAS_TABLES" = "1" ]; then
|
||||
echo "[entrypoint] Database populated, importing configuration..."
|
||||
$DRUSH config:import -y 2>/dev/null && \
|
||||
echo "[entrypoint] Config imported." || \
|
||||
echo "[entrypoint] No config to import, continuing."
|
||||
$DRUSH theme:enable claro_compact -y
|
||||
else
|
||||
IS_SETUP=$($DRUSH sql:query \
|
||||
"SELECT COUNT(*) FROM config WHERE name='core.extension' AND data LIKE '%riverside_pt%';" \
|
||||
2>/dev/null || echo "0")
|
||||
|
||||
if [ "$HAS_TABLES" != "1" ]; then
|
||||
echo "[entrypoint] Fresh database, installing Drupal..."
|
||||
$DRUSH site:install standard \
|
||||
--site-name="${SITE_NAME:-Portfolio}" \
|
||||
--account-name=admin \
|
||||
--account-pass="${ADMIN_PASS:-admin}" \
|
||||
-y
|
||||
-y || { echo "[entrypoint] FATAL: site:install failed."; exit 1; }
|
||||
echo "[entrypoint] Drupal installed."
|
||||
fi
|
||||
|
||||
echo "[entrypoint] Enabling modules..."
|
||||
$DRUSH en -y views views_ui field_ui text options link datetime
|
||||
$DRUSH en -y webform webform_ui
|
||||
$DRUSH en -y symfony_mailer
|
||||
if [ "$IS_SETUP" != "1" ]; then
|
||||
echo "[entrypoint] Running setup (first boot or recovery from failed setup)..."
|
||||
|
||||
$DRUSH en -y riverside_pt
|
||||
echo "[entrypoint] Modules enabled."
|
||||
$DRUSH en -y views views_ui field_ui text options link datetime && \
|
||||
echo "[entrypoint] Core modules enabled." || echo "[entrypoint] WARNING: core modules failed."
|
||||
$DRUSH en -y webform webform_ui && \
|
||||
echo "[entrypoint] Webform enabled." || echo "[entrypoint] WARNING: webform failed."
|
||||
$DRUSH en -y symfony_mailer && \
|
||||
echo "[entrypoint] Mailer enabled." || echo "[entrypoint] WARNING: symfony_mailer failed."
|
||||
$DRUSH en -y riverside_pt && \
|
||||
echo "[entrypoint] riverside_pt enabled." || echo "[entrypoint] WARNING: riverside_pt failed."
|
||||
|
||||
echo "[entrypoint] Setting themes..."
|
||||
$DRUSH theme:enable olivero claro_compact
|
||||
echo "[entrypoint] Themes set."
|
||||
$DRUSH config:set system.site page.front /home -y && \
|
||||
echo "[entrypoint] Front page set." || echo "[entrypoint] WARNING: front page config failed."
|
||||
|
||||
$DRUSH theme:enable olivero claro_compact -y && \
|
||||
echo "[entrypoint] Themes set." || echo "[entrypoint] WARNING: theme enable failed."
|
||||
|
||||
if ls /var/www/html/config/sync/*.yml >/dev/null 2>&1; then
|
||||
echo "[entrypoint] Importing configuration from sync dir..."
|
||||
$DRUSH config:import -y
|
||||
$DRUSH config:import -y || echo "[entrypoint] WARNING: config import failed."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[entrypoint] Setup complete."
|
||||
else
|
||||
echo "[entrypoint] Setup already complete, importing configuration..."
|
||||
$DRUSH config:import -y 2>/dev/null && \
|
||||
echo "[entrypoint] Config imported." || \
|
||||
echo "[entrypoint] No config to import, continuing."
|
||||
fi
|
||||
|
||||
echo "[entrypoint] Starting services..."
|
||||
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
|
|
|
|||
143
web/modules/custom/riverside_pt/css/front.css
Normal file
143
web/modules/custom/riverside_pt/css/front.css
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/* Hide Drupal's default page-title block on the front page — our H1 is inside the hero */
|
||||
.path-frontpage .block-page-title-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Hero ───────────────────────────────────────────────────── */
|
||||
.rpt-hero {
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
padding: 5rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rpt-hero__inner {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.rpt-hero__heading {
|
||||
font-size: clamp(2rem, 5vw, 3.25rem);
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rpt-hero__body {
|
||||
font-size: 1.125rem;
|
||||
color: #4b5563;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.rpt-hero__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Shared buttons ─────────────────────────────────────────── */
|
||||
.rpt-btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rpt-btn--primary {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border: 2px solid #3b82f6;
|
||||
}
|
||||
|
||||
.rpt-btn--primary:hover,
|
||||
.rpt-btn--primary:focus {
|
||||
background: #1d4ed8;
|
||||
border-color: #1d4ed8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rpt-btn--secondary {
|
||||
background: transparent;
|
||||
color: #3b82f6;
|
||||
border: 2px solid #3b82f6;
|
||||
}
|
||||
|
||||
.rpt-btn--secondary:hover,
|
||||
.rpt-btn--secondary:focus {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rpt-btn--outline {
|
||||
background: transparent;
|
||||
color: #3b82f6;
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.rpt-btn--outline:hover,
|
||||
.rpt-btn--outline:focus {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Services section ───────────────────────────────────────── */
|
||||
.rpt-services {
|
||||
padding: 4rem 1.5rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.rpt-services__header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.rpt-services__heading {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.rpt-services__subtitle {
|
||||
font-size: 1.0625rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.rpt-services__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.rpt-service-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.rpt-service-card__title {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.rpt-service-card__body {
|
||||
font-size: 0.9375rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
flex: 1;
|
||||
}
|
||||
26
web/modules/custom/riverside_pt/css/nav.css
Normal file
26
web/modules/custom/riverside_pt/css/nav.css
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
.primary-nav__menu-link.nav-cta {
|
||||
border: 2px solid #3b82f6;
|
||||
border-radius: 999px;
|
||||
color: #3b82f6;
|
||||
padding: 0.25rem 1rem;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.primary-nav__menu-link.nav-cta:hover,
|
||||
.primary-nav__menu-link.nav-cta:focus {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.primary-nav__menu-link.nav-cta--primary {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.primary-nav__menu-link.nav-cta--primary:hover,
|
||||
.primary-nav__menu-link.nav-cta--primary:focus {
|
||||
background: #1d4ed8;
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\menu_link_content\Entity\MenuLinkContent;
|
||||
use Drupal\path_alias\Entity\PathAlias;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
|
|
@ -140,4 +143,60 @@ function riverside_pt_install() {
|
|||
'required' => TRUE,
|
||||
]),
|
||||
], fn($entity) => $entity->save());
|
||||
|
||||
try {
|
||||
_riverside_pt_build_navigation();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
\Drupal::logger('riverside_pt')->error('Navigation setup failed: @msg', ['@msg' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
\Drupal::configFactory()->getEditable('system.site')
|
||||
->set('page.front', '/home')
|
||||
->save();
|
||||
}
|
||||
|
||||
function _riverside_pt_build_navigation(): void {
|
||||
$em = \Drupal::entityTypeManager();
|
||||
|
||||
// Remove whatever links Standard profile put in the main menu.
|
||||
foreach ($em->getStorage('menu_link_content')->loadByProperties(['menu_name' => 'main']) as $link) {
|
||||
$link->delete();
|
||||
}
|
||||
|
||||
// Create placeholder basic pages.
|
||||
foreach (['Services', 'About', 'FAQ', 'Contact'] as $title) {
|
||||
$existing = $em->getStorage('node')->loadByProperties(['title' => $title, 'type' => 'page']);
|
||||
if ($existing) {
|
||||
continue;
|
||||
}
|
||||
$node = Node::create(['type' => 'page', 'title' => $title, 'status' => 1]);
|
||||
$node->save();
|
||||
PathAlias::create([
|
||||
'path' => '/node/' . $node->id(),
|
||||
'alias' => '/' . strtolower($title),
|
||||
'langcode' => 'en',
|
||||
])->save();
|
||||
}
|
||||
|
||||
// Build the primary navigation.
|
||||
$defs = [
|
||||
['title' => 'Home', 'uri' => 'route:<front>', 'weight' => 0, 'class' => NULL],
|
||||
['title' => 'Services', 'uri' => 'internal:/services', 'weight' => 1, 'class' => NULL],
|
||||
['title' => 'About', 'uri' => 'internal:/about', 'weight' => 2, 'class' => NULL],
|
||||
['title' => 'FAQ', 'uri' => 'internal:/faq', 'weight' => 3, 'class' => NULL],
|
||||
['title' => 'Contact', 'uri' => 'internal:/contact', 'weight' => 4, 'class' => 'nav-cta'],
|
||||
['title' => 'Book An Appointment', 'uri' => 'internal:/schedule', 'weight' => 5, 'class' => 'nav-cta nav-cta--primary'],
|
||||
];
|
||||
|
||||
foreach ($defs as $def) {
|
||||
$options = $def['class'] ? ['attributes' => ['class' => explode(' ', $def['class'])]] : [];
|
||||
MenuLinkContent::create([
|
||||
'title' => $def['title'],
|
||||
'link' => ['uri' => $def['uri'], 'options' => $options],
|
||||
'menu_name' => 'main',
|
||||
'weight' => $def['weight'],
|
||||
'enabled' => TRUE,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
front:
|
||||
css:
|
||||
theme:
|
||||
css/front.css: {}
|
||||
|
||||
navigation:
|
||||
css:
|
||||
theme:
|
||||
css/nav.css: {}
|
||||
|
||||
schedule:
|
||||
css:
|
||||
theme:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ use Drupal\Core\Breadcrumb\Breadcrumb;
|
|||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
|
||||
function riverside_pt_page_attachments(array &$attachments): void {
|
||||
$attachments['#attached']['library'][] = 'riverside_pt/navigation';
|
||||
}
|
||||
|
||||
function riverside_pt_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void {
|
||||
if ($route_match->getRouteName() === 'riverside_pt.booking') {
|
||||
$breadcrumb = new Breadcrumb();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
riverside_pt.home:
|
||||
path: '/home'
|
||||
defaults:
|
||||
_controller: '\Drupal\riverside_pt\Controller\HomeController::page'
|
||||
_title: 'Riverside Physical Therapy'
|
||||
requirements:
|
||||
_permission: 'access content'
|
||||
|
||||
riverside_pt.schedule:
|
||||
path: '/schedule'
|
||||
defaults:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\riverside_pt\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
|
||||
class HomeController extends ControllerBase {
|
||||
|
||||
public function page(): array {
|
||||
return [
|
||||
'#type' => 'inline_template',
|
||||
'#template' => self::template(),
|
||||
'#attached' => ['library' => ['riverside_pt/front']],
|
||||
];
|
||||
}
|
||||
|
||||
private static function template(): string {
|
||||
return <<<'TWIG'
|
||||
<section class="rpt-hero">
|
||||
<div class="rpt-hero__inner">
|
||||
<h1 class="rpt-hero__heading">Heal your body</h1>
|
||||
<p class="rpt-hero__body">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
<div class="rpt-hero__actions">
|
||||
<a href="/schedule" class="rpt-btn rpt-btn--primary">Book an Appointment</a>
|
||||
<a href="/services" class="rpt-btn rpt-btn--secondary">View Our Services</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rpt-services">
|
||||
<div class="rpt-services__header">
|
||||
<h2 class="rpt-services__heading">Bringing Relief</h2>
|
||||
<p class="rpt-services__subtitle">Our wide range of services</p>
|
||||
</div>
|
||||
<div class="rpt-services__grid">
|
||||
<div class="rpt-service-card">
|
||||
<h3 class="rpt-service-card__title">Diagnostic Evaluation</h3>
|
||||
<p class="rpt-service-card__body">Comprehensive assessment to identify the root cause of your pain and build a personalized recovery plan.</p>
|
||||
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
|
||||
</div>
|
||||
<div class="rpt-service-card">
|
||||
<h3 class="rpt-service-card__title">Sports Rehabilitation</h3>
|
||||
<p class="rpt-service-card__body">Targeted programs to help athletes recover from injury and return to peak performance safely.</p>
|
||||
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
|
||||
</div>
|
||||
<div class="rpt-service-card">
|
||||
<h3 class="rpt-service-card__title">Pre/Post-Surgical Rehab</h3>
|
||||
<p class="rpt-service-card__body">Expert care before and after surgery to maximize recovery outcomes and restore full function.</p>
|
||||
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
|
||||
</div>
|
||||
<div class="rpt-service-card">
|
||||
<h3 class="rpt-service-card__title">Neurological Physical Therapy</h3>
|
||||
<p class="rpt-service-card__body">Specialized therapy for nervous system conditions, helping you regain strength and independence.</p>
|
||||
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
TWIG;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue