diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 434f8d38..787303ae 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -1,25 +1,53 @@ on: push: branches: [main] + workflow_dispatch: + +concurrency: + group: deploy-main + cancel-in-progress: true jobs: deploy: - runs-on: docker + runs-on: self-hosted steps: - - uses: actions/checkout@v4 - - - name: Restart Nomad allocation + - name: Build Astro and restart Nomad env: NOMAD_ADDR: ${{ secrets.NOMAD_ADDR }} NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }} + REPO_URL: https://git.bcgen.ie/familyfedie/familyfedie-website.git run: | - set -e - ALLOC_ID=$(curl -sf \ - -H "X-Nomad-Token: $NOMAD_TOKEN" \ - "$NOMAD_ADDR/v1/job/familyfed/allocations" \ - | python3 -c "import sys,json; allocs=json.load(sys.stdin); print(next(a['ID'] for a in allocs if a['ClientStatus']=='running'))") + set -eu + rm -rf source + export REF_NAME="${GITHUB_REF_NAME:-main}" + + nix --extra-experimental-features "nix-command flakes" \ + shell nixpkgs#git \ + -c git clone --depth 1 --branch "$REF_NAME" "$REPO_URL" source + + cd source + nix --extra-experimental-features "nix-command flakes" \ + shell nixpkgs#nodejs_24 \ + -c npm ci + rm -rf dist + nix --extra-experimental-features "nix-command flakes" \ + shell nixpkgs#nodejs_24 \ + -c npm run build + + test -f dist/index.html + test -f dist/speeches/index.html + test -f dist/assets/icons/familyfed-favicon.png + + ALLOC_ID=$(nix --extra-experimental-features "nix-command flakes" \ + shell nixpkgs#curl nixpkgs#jq \ + -c sh -c 'curl -sf -H "X-Nomad-Token: $NOMAD_TOKEN" "$NOMAD_ADDR/v1/job/familyfed/allocations" | jq -r "map(select(.ClientStatus == \"running\"))[0].ID"') + + test -n "$ALLOC_ID" + test "$ALLOC_ID" != "null" echo "Stopping allocation to trigger fresh prestart: $ALLOC_ID" - curl -sf -X POST \ - -H "X-Nomad-Token: $NOMAD_TOKEN" \ - "$NOMAD_ADDR/v1/allocation/$ALLOC_ID/stop" - echo "Deploy triggered — Nomad will schedule a new alloc that re-fetches the site." + nix --extra-experimental-features "nix-command flakes" \ + shell nixpkgs#curl \ + -c curl -sf -X POST \ + -H "X-Nomad-Token: $NOMAD_TOKEN" \ + "$NOMAD_ADDR/v1/allocation/$ALLOC_ID/stop" + echo "Deploy triggered. Nomad will schedule a new alloc that re-fetches the site." diff --git a/assets/icons/familyfed-favicon.png b/assets/icons/familyfed-favicon.png new file mode 100644 index 00000000..1c82fb24 Binary files /dev/null and b/assets/icons/familyfed-favicon.png differ diff --git a/assets/icons/familyfed-logo.webp b/assets/icons/familyfed-logo.webp new file mode 100644 index 00000000..1f2c0d9a Binary files /dev/null and b/assets/icons/familyfed-logo.webp differ diff --git a/astro.config.mjs b/astro.config.mjs index 33c3ea43..b1b39a5a 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,6 +1,7 @@ import { defineConfig } from "astro/config"; export default defineConfig({ + site: "https://familyfed.ie", output: "static", outDir: "dist", build: { diff --git a/css/site.css b/css/site.css index b6efa167..ac567378 100644 --- a/css/site.css +++ b/css/site.css @@ -30,7 +30,7 @@ body.custom-background { background-color: #ffffff; } .speech-filters { display: grid; - grid-template-columns: minmax(240px, 2fr) repeat(3, minmax(150px, 1fr)) auto; + grid-template-columns: minmax(240px, 2fr) repeat(2, minmax(150px, 1fr)) auto; gap: 12px; align-items: end; margin: 0 0 18px; @@ -100,18 +100,16 @@ body.custom-background { background-color: #ffffff; } } .speech-result p { - margin: 0 0 10px; + margin: 0; } -.speech-result-meta, -.speech-result-categories { +.speech-result-meta { display: flex; flex-wrap: wrap; gap: 6px; } -.speech-result-meta span, -.speech-result-categories span { +.speech-result-meta span { display: inline-flex; align-items: center; min-height: 22px; @@ -158,238 +156,6 @@ body.custom-background { background-color: #ffffff; } } } -/* Events calendar. */ -.events-page-shell { - box-sizing: border-box; - width: min(1100px, calc(100% - 40px)); - margin: 0 auto; - padding: 28px 0 44px; -} - -.events-page-header { - margin: 0 0 22px; -} - -.events-page-kicker { - margin: 0 0 6px; - color: #5f7f38; - font-size: 13px; - font-weight: 700; - letter-spacing: 0; - text-transform: uppercase; -} - -.events-page-header h1, -.events-calendar-heading h2, -.events-past h2, -.events-past-item h3 { - color: #2f332c; -} - -.events-page-header h1 { - margin: 0 0 8px; - font-size: 32px; - line-height: 1.18; -} - -.events-page-header p:last-child { - max-width: 680px; - margin: 0; - color: #5a5d56; - font-size: 16px; - line-height: 1.55; -} - -.events-calendar-panel, -.events-past { - border: 1px solid #e5e2d8; - border-radius: 8px; - background: #ffffff; -} - -.events-calendar-panel { - overflow: hidden; -} - -.events-calendar-heading { - display: flex; - flex-wrap: wrap; - gap: 12px 18px; - align-items: center; - justify-content: space-between; - padding: 20px 22px; - border-bottom: 1px solid #e5e2d8; - background: #fafbf7; -} - -.events-calendar-heading h2, -.events-past h2 { - margin: 0; - font-size: 24px; - line-height: 1.25; -} - -.events-calendar-heading p { - margin: 0; - padding: 7px 11px; - border: 1px solid #d6e3c1; - border-radius: 999px; - color: #3d662c; - background: #f0f7e8; - font-size: 14px; - line-height: 1.25; -} - -.events-calendar-grid { - display: grid; - grid-template-columns: repeat(7, minmax(0, 1fr)); - background: #e9e5db; - gap: 1px; -} - -.events-calendar-weekday, -.events-calendar-day { - box-sizing: border-box; - min-width: 0; - background: #ffffff; -} - -.events-calendar-weekday { - min-height: 42px; - padding: 12px 8px; - color: #676961; - font-size: 12px; - font-weight: 700; - line-height: 1.2; - text-align: center; - text-transform: uppercase; -} - -.events-calendar-day { - display: flex; - align-items: flex-start; - min-height: 92px; - padding: 10px; - color: #34372f; - font-size: 15px; - font-weight: 700; - line-height: 1; -} - -.events-calendar-day--weekend { - background: #fbfaf7; -} - -.events-calendar-day--outside-month { - color: #97978f; - background: #f5f4ef; -} - -.events-calendar-day--today { - color: #ffffff; - background: #5f7f38; -} - -.events-past { - margin-top: 24px; - padding: 22px; -} - -.events-past-list { - display: grid; - gap: 14px; - margin-top: 16px; -} - -.events-past-item { - display: grid; - grid-template-columns: 76px minmax(0, 1fr); - gap: 16px; - align-items: center; - padding: 14px; - border: 1px solid #ece8dd; - border-radius: 6px; - background: #fdfdfb; -} - -.events-past-date { - display: grid; - place-items: center; - min-height: 76px; - border: 1px solid #d9d3c5; - border-radius: 6px; - color: #575b50; - background: #ffffff; - line-height: 1.1; - text-align: center; - text-transform: uppercase; -} - -.events-past-date strong { - color: #bf4d28; - font-size: 27px; - line-height: 1; -} - -.events-past-date span { - font-size: 12px; - font-weight: 700; -} - -.events-past-item p { - margin: 0 0 4px; - color: #6b6d66; - font-size: 13px; -} - -.events-past-item h3 { - margin: 0; - font-size: 20px; - line-height: 1.25; -} - -@media (max-width: 760px) { - .events-page-shell { - width: min(100% - 24px, 1100px); - padding-top: 20px; - } - - .events-page-header h1 { - font-size: 27px; - } - - .events-calendar-heading, - .events-past { - padding: 16px; - } - - .events-calendar-grid { - overflow-x: auto; - grid-template-columns: repeat(7, minmax(52px, 1fr)); - } - - .events-calendar-weekday { - min-height: 36px; - padding: 10px 4px; - font-size: 11px; - } - - .events-calendar-day { - min-height: 58px; - padding: 8px; - font-size: 14px; - } - - .events-past-item { - grid-template-columns: 64px minmax(0, 1fr); - gap: 12px; - } - - .events-past-date { - min-height: 64px; - } -} - /* Contact page form. */ .contact-form { display: grid; @@ -448,3 +214,251 @@ body.custom-background { background-color: #ffffff; } margin: 0; color: #5c5c56; } + +/* Static events calendar. */ +.events-calendar-page .entry-content { + max-width: 100%; +} + +.familyfed-calendar { + display: grid; + gap: 16px; +} + +.calendar-toolbar, +.calendar-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +.calendar-toolbar { + justify-content: space-between; +} + +.calendar-title { + margin: 0; + color: #333333; + font-size: 28px; + line-height: 1.2; + text-align: center; +} + +.calendar-nav-button, +.calendar-secondary-button { + min-height: 38px; + border: 1px solid #d8d8d0; + border-radius: 4px; + color: #333333; + background: #ffffff; + cursor: pointer; +} + +.calendar-nav-button { + width: 42px; + font-size: 26px; + line-height: 1; +} + +.calendar-secondary-button { + padding: 8px 12px; +} + +.calendar-nav-button:hover, +.calendar-nav-button:focus, +.calendar-secondary-button:hover, +.calendar-secondary-button:focus { + border-color: #bf4d28; + color: #bf4d28; +} + +.calendar-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(240px, 300px); + gap: 18px; + align-items: start; +} + +.calendar-grid-wrap, +.calendar-panel { + border: 1px solid #eeeeee; + border-radius: 6px; + background: #ffffff; +} + +.calendar-weekdays, +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); +} + +.calendar-weekdays { + border-bottom: 1px solid #eeeeee; + background: #fbfbf8; +} + +.calendar-weekdays span { + padding: 10px 8px; + color: #5c5c56; + font-size: 12px; + font-weight: 700; + text-align: center; + text-transform: uppercase; +} + +.calendar-day { + display: grid; + grid-template-rows: auto 1fr; + gap: 6px; + min-height: 112px; + padding: 8px; + border: 0; + border-right: 1px solid #eeeeee; + border-bottom: 1px solid #eeeeee; + color: #333333; + background: #ffffff; + text-align: left; + cursor: pointer; +} + +.calendar-day:nth-child(7n) { + border-right: 0; +} + +.calendar-day:hover, +.calendar-day:focus { + outline: 2px solid #bf4d28; + outline-offset: -2px; +} + +.calendar-day-muted { + color: #8c8c86; + background: #fbfbf8; +} + +.calendar-day-today .calendar-day-number { + border-color: #d5e6b3; + color: #365d26; + background: #f2f7e8; +} + +.calendar-day-selected { + box-shadow: inset 0 0 0 2px #bf4d28; +} + +.calendar-day-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + min-height: 28px; + border: 1px solid transparent; + border-radius: 50%; + font-weight: 700; +} + +.calendar-day-events { + display: grid; + gap: 4px; + align-content: start; +} + +.calendar-event-pill, +.calendar-event-more { + overflow: hidden; + padding: 3px 6px; + border-radius: 4px; + font-size: 12px; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.calendar-event-pill { + color: #365d26; + background: #f2f7e8; +} + +.calendar-event-more { + color: #5c5c56; + background: #f7f7f2; +} + +.calendar-panel { + padding: 16px; +} + +.calendar-panel h3 { + margin: 0 0 12px; + font-size: 20px; +} + +.calendar-event-list { + display: grid; + gap: 12px; +} + +.calendar-event-card { + padding: 12px; + border: 1px solid #eeeeee; + border-radius: 6px; + background: #fbfbf8; +} + +.calendar-event-card h4 { + margin: 0 0 6px; + font-size: 18px; + line-height: 1.25; +} + +.calendar-event-card p { + margin: 0 0 7px; +} + +.calendar-event-date { + color: #5c5c56; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +.calendar-empty { + margin: 0; + color: #5c5c56; +} + +@media (max-width: 900px) { + .calendar-layout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .calendar-title { + font-size: 22px; + } + + .calendar-actions { + align-items: stretch; + } + + .calendar-actions > * { + width: 100%; + } + + .calendar-weekdays span { + padding: 8px 2px; + font-size: 11px; + } + + .calendar-day { + min-height: 78px; + padding: 5px; + } + + .calendar-event-pill, + .calendar-event-more { + font-size: 11px; + } +} diff --git a/src/components/SiteLayout.astro b/src/components/SiteLayout.astro index 82344a8e..b56edd27 100644 --- a/src/components/SiteLayout.astro +++ b/src/components/SiteLayout.astro @@ -14,7 +14,10 @@ const { parabolaSettings = { masonry: "0", magazine: "0", mobile: "1", fitvids: "1" }, } = Astro.props; const parabolaSettingsScript = `var parabola_settings = ${JSON.stringify(parabolaSettings)};`; -const canonicalUrl = canonical ?? pathname; +const siteOrigin = Astro.site?.origin ?? "https://familyfed.ie"; +const absoluteUrl = (value) => value ? new URL(value, siteOrigin).toString() : undefined; +const canonicalUrl = absoluteUrl(canonical ?? pathname); +const imageUrl = absoluteUrl(image); const metaTitle = title .replace(/–|–/g, "-") .replace(/‘|’|‘|’/g, "'") @@ -27,7 +30,6 @@ const metaTitle = title - {description && <meta name="description" content={description} />} {robots && <meta name="robots" content={robots} />} @@ -35,9 +37,11 @@ const metaTitle = title <meta property="og:title" content={metaTitle} /> {description && <meta property="og:description" content={description} />} {canonicalUrl && <meta property="og:url" content={canonicalUrl} />} - {image && <meta property="og:image" content={image} />} + {imageUrl && <meta property="og:image" content={imageUrl} />} <meta property="og:type" content={ogType} /> <meta name="twitter:card" content="summary_large_image" /> + <link rel="icon" href="/assets/icons/familyfed-favicon.png" type="image/png" /> + <link rel="apple-touch-icon" href="/assets/icons/familyfed-favicon.png" /> <link rel="stylesheet" id="content-blocks-css" href="/css/block-library.css?ver=6.6.5" type="text/css" media="all" /> <link rel="stylesheet" id="pdfprnt_frontend-css" href="/assets/vendor/pdf/css/frontend.css?ver=2.4.0" type="text/css" media="all" /> <link rel="stylesheet" id="parabola-fonts-css" href="/assets/theme/parabola/fonts/fontfaces.css?ver=2.4.1" type="text/css" media="all" /> @@ -48,6 +52,7 @@ const metaTitle = title <script is:inline type="text/javascript" src="/js/jquery.min.js?ver=2.0.2" id="jquery-js"></script> <script is:inline type="text/javascript" id="parabola-frontend-js-extra" set:html={parabolaSettingsScript}></script> <script is:inline type="text/javascript" src="/assets/theme/parabola/js/frontend.js?ver=2.4.1" id="parabola-frontend-js"></script> + <slot name="head" /> {canonicalUrl && <link rel="canonical" href={canonicalUrl} />} </head> <body class={bodyClass}> diff --git a/src/content/pages/contact.html b/src/content/pages/contact.html index 8716e87b..22b68c99 100644 --- a/src/content/pages/contact.html +++ b/src/content/pages/contact.html @@ -5,9 +5,14 @@ <p>We’re conveniently located in Dublin City Centre, directly across from the James Joyce Center. Feel free to drop by anytime, since most days someone is around to offer you a cup of tea and a friendly chat about our beliefs.</p> <p>Our address is:</p> <p><strong>19 North Great Georges St.</strong><br /><strong> Dublin 1, Ireland</strong></p> -<p>Feel free to email us any questions you may have or to book an appointment to hear our lecture series of the Divine Principle or to organize a religious event at our church center: <a href="mailto:directors@familyfed.ie">directors@familyfed.ie</a></p> +<p>Feel free to email us any questions you may have or to book an appointment to hear our lecture series of the Divine Principle or to organize a religious event at our church center.</p> +<ul> + <li>Directors: <a href="mailto:directors@familyfed.ie">directors@familyfed.ie</a></li> + <li>General enquiries: <a href="mailto:info@familyfed.ie">info@familyfed.ie</a></li> + <li>Media: <a href="mailto:media@familyfed.ie">media@familyfed.ie</a></li> +</ul> -<form id="contact-form" class="contact-form" action="mailto:directors@familyfed.ie" method="post" enctype="text/plain"> +<form id="contact-form" class="contact-form" action="mailto:directors@familyfed.ie,info@familyfed.ie,media@familyfed.ie" method="post" enctype="text/plain"> <div class="contact-field"> <label for="contact-name">Name</label> <input id="contact-name" name="name" type="text" autocomplete="name" required /> diff --git a/src/content/pages/events.html b/src/content/pages/events.html index 4366ba25..da62580e 100644 --- a/src/content/pages/events.html +++ b/src/content/pages/events.html @@ -1,110 +1,47 @@ <div id="main"> <div id="forbottom"> - <div style="clear:both;"> </div> + <div style="clear:both;"></div> + <section id="container" class="one-column events-calendar-page"> + <div id="content" role="main"> + <div class="post page type-page status-publish hentry"> + <h1 class="entry-title">Events Calendar</h1> + <div class="entry-content"> + <div class="familyfed-calendar" data-calendar> + <div class="calendar-toolbar" aria-label="Calendar navigation"> + <button class="calendar-nav-button" type="button" data-calendar-prev aria-label="Previous month">‹</button> + <h2 id="calendar-month-title" class="calendar-title" aria-live="polite"></h2> + <button class="calendar-nav-button" type="button" data-calendar-next aria-label="Next month">›</button> + </div> - <main class="events-page-shell" id="content" role="main"> - <header class="events-page-header"> - <p class="events-page-kicker">Calendar</p> - <h1>Upcoming Events</h1> - <p>FFWPU Ireland community events will appear here when they are scheduled.</p> - </header> + <div class="calendar-actions"> + <button class="calendar-secondary-button" type="button" data-calendar-today>This Month</button> + <button class="calendar-secondary-button" type="button" data-calendar-export>Export Calendar</button> + </div> - <section class="events-calendar-panel" aria-labelledby="events-calendar-heading"> - <div class="events-calendar-heading"> - <h2 id="events-calendar-heading">June 2026</h2> - <p role="status">There are no upcoming events.</p> - </div> + <div class="calendar-layout"> + <div class="calendar-grid-wrap"> + <div class="calendar-weekdays" aria-hidden="true"> + <span>Mon</span> + <span>Tue</span> + <span>Wed</span> + <span>Thu</span> + <span>Fri</span> + <span>Sat</span> + <span>Sun</span> + </div> + <div class="calendar-grid" role="grid" aria-labelledby="calendar-month-title" data-calendar-grid></div> + </div> - <div class="events-calendar-grid" role="grid" aria-readonly="true" aria-label="June 2026 calendar"> - <div class="events-calendar-weekday" role="columnheader">Mon</div> - <div class="events-calendar-weekday" role="columnheader">Tue</div> - <div class="events-calendar-weekday" role="columnheader">Wed</div> - <div class="events-calendar-weekday" role="columnheader">Thu</div> - <div class="events-calendar-weekday" role="columnheader">Fri</div> - <div class="events-calendar-weekday" role="columnheader">Sat</div> - <div class="events-calendar-weekday" role="columnheader">Sun</div> - - <time class="events-calendar-day" datetime="2026-06-01" role="gridcell">1</time> - <time class="events-calendar-day" datetime="2026-06-02" role="gridcell">2</time> - <time class="events-calendar-day" datetime="2026-06-03" role="gridcell">3</time> - <time class="events-calendar-day" datetime="2026-06-04" role="gridcell">4</time> - <time class="events-calendar-day" datetime="2026-06-05" role="gridcell">5</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-06" role="gridcell">6</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-07" role="gridcell">7</time> - <time class="events-calendar-day" datetime="2026-06-08" role="gridcell">8</time> - <time class="events-calendar-day" datetime="2026-06-09" role="gridcell">9</time> - <time class="events-calendar-day" datetime="2026-06-10" role="gridcell">10</time> - <time class="events-calendar-day events-calendar-day--today" datetime="2026-06-11" role="gridcell" aria-label="June 11, 2026">11</time> - <time class="events-calendar-day" datetime="2026-06-12" role="gridcell">12</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-13" role="gridcell">13</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-14" role="gridcell">14</time> - <time class="events-calendar-day" datetime="2026-06-15" role="gridcell">15</time> - <time class="events-calendar-day" datetime="2026-06-16" role="gridcell">16</time> - <time class="events-calendar-day" datetime="2026-06-17" role="gridcell">17</time> - <time class="events-calendar-day" datetime="2026-06-18" role="gridcell">18</time> - <time class="events-calendar-day" datetime="2026-06-19" role="gridcell">19</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-20" role="gridcell">20</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-21" role="gridcell">21</time> - <time class="events-calendar-day" datetime="2026-06-22" role="gridcell">22</time> - <time class="events-calendar-day" datetime="2026-06-23" role="gridcell">23</time> - <time class="events-calendar-day" datetime="2026-06-24" role="gridcell">24</time> - <time class="events-calendar-day" datetime="2026-06-25" role="gridcell">25</time> - <time class="events-calendar-day" datetime="2026-06-26" role="gridcell">26</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-27" role="gridcell">27</time> - <time class="events-calendar-day events-calendar-day--weekend" datetime="2026-06-28" role="gridcell">28</time> - <time class="events-calendar-day" datetime="2026-06-29" role="gridcell">29</time> - <time class="events-calendar-day" datetime="2026-06-30" role="gridcell">30</time> - <time class="events-calendar-day events-calendar-day--outside-month" datetime="2026-07-01" role="gridcell">1</time> - <time class="events-calendar-day events-calendar-day--outside-month" datetime="2026-07-02" role="gridcell">2</time> - <time class="events-calendar-day events-calendar-day--outside-month" datetime="2026-07-03" role="gridcell">3</time> - <time class="events-calendar-day events-calendar-day--outside-month events-calendar-day--weekend" datetime="2026-07-04" role="gridcell">4</time> - <time class="events-calendar-day events-calendar-day--outside-month events-calendar-day--weekend" datetime="2026-07-05" role="gridcell">5</time> - </div> - </section> - - <section class="events-past" aria-labelledby="events-past-heading"> - <h2 id="events-past-heading">Latest Past Events</h2> - - <div class="events-past-list"> - <article class="events-past-item"> - <time class="events-past-date" datetime="2015-03-25"> - <span>Mar</span> - <strong>25</strong> - <span>2015</span> - </time> - <div> - <p>March 25, 2015</p> - <h3>Ahn Shi Il</h3> + <aside class="calendar-panel" aria-live="polite"> + <h3 data-calendar-panel-title>Upcoming Events</h3> + <div class="calendar-event-list" data-calendar-list></div> + </aside> + </div> </div> - </article> - - <article class="events-past-item"> - <time class="events-past-date" datetime="2015-03-17"> - <span>Mar</span> - <strong>17</strong> - <span>2015</span> - </time> - <div> - <p>March 17, 2015</p> - <h3>Ahn Shi Il</h3> - </div> - </article> - - <article class="events-past-item"> - <time class="events-past-date" datetime="2015-03-09"> - <span>Mar</span> - <strong>9</strong> - <span>2015</span> - </time> - <div> - <p>March 9, 2015</p> - <h3>Ahn Shi Il</h3> - </div> - </article> + </div> </div> - </section> - </main> - + </div> + </section> <div style="clear:both;"></div> </div> </div> diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 211d7475..8fafbe9e 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -47,7 +47,7 @@ import articleHtml from "../content/pages/contact.html?raw"; status.textContent = "Opening your email app with the message ready to send."; } - window.location.href = `mailto:directors@familyfed.ie?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + window.location.href = `mailto:directors@familyfed.ie,info@familyfed.ie,media@familyfed.ie?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; }); })(); </script> diff --git a/src/pages/events.astro b/src/pages/events.astro index 015f5993..7c8bef36 100644 --- a/src/pages/events.astro +++ b/src/pages/events.astro @@ -9,7 +9,303 @@ import mainHtml from "../content/pages/events.html?raw"; bodyClass="archive post-type-archive post-type-archive-tribe_events custom-background metaslider-plugin parabola-image-three caption-light meta-light parabola_triagles parabola-menu-left" pathname="/events.html" canonical="/events.html" - robots="noindex, follow, max-image-preview:large" > <Fragment set:html={mainHtml} /> + <script is:inline slot="scripts"> + (() => { + const calendar = document.querySelector("[data-calendar]"); + + if (!calendar) { + return; + } + + const recurringEvents = [ + { + id: "sunday-service", + title: "Sunday Service", + startTime: "11:00", + endTime: "12:00", + location: "19 North Great Georges St., Dublin 1, Ireland", + details: "Weekly Sunday Service video upload and community worship.", + url: "/services.html", + weekday: 0, + }, + ]; + + const monthTitle = calendar.querySelector("[data-calendar-panel-title]"); + const grid = calendar.querySelector("[data-calendar-grid]"); + const list = calendar.querySelector("[data-calendar-list]"); + const title = calendar.querySelector("#calendar-month-title"); + const today = new Date(); + let visibleMonth = new Date(today.getFullYear(), today.getMonth(), 1); + let selectedDate = toDateKey(today); + + const monthFormatter = new Intl.DateTimeFormat("en-IE", { month: "long", year: "numeric" }); + const dayFormatter = new Intl.DateTimeFormat("en-IE", { weekday: "long", day: "numeric", month: "long", year: "numeric" }); + + function pad(value) { + return String(value).padStart(2, "0"); + } + + function toDateKey(date) { + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; + } + + function fromDateKey(key) { + const [year, month, day] = key.split("-").map(Number); + return new Date(year, month - 1, day); + } + + function addDays(date, days) { + const next = new Date(date); + next.setDate(next.getDate() + days); + return next; + } + + function isSameMonth(date, month) { + return date.getFullYear() === month.getFullYear() && date.getMonth() === month.getMonth(); + } + + function monthStartGridDate(month) { + const first = new Date(month.getFullYear(), month.getMonth(), 1); + const mondayIndex = (first.getDay() + 6) % 7; + return addDays(first, -mondayIndex); + } + + function monthEndGridDate(month) { + const last = new Date(month.getFullYear(), month.getMonth() + 1, 0); + const sundayIndex = (7 - last.getDay()) % 7; + return addDays(last, sundayIndex); + } + + function eventsBetween(start, end) { + const events = []; + + for (let date = new Date(start); date <= end; date = addDays(date, 1)) { + for (const recurringEvent of recurringEvents) { + if (date.getDay() !== recurringEvent.weekday) { + continue; + } + + const dateKey = toDateKey(date); + events.push({ + ...recurringEvent, + id: `${recurringEvent.id}-${dateKey}`, + date: dateKey, + }); + } + } + + return events; + } + + function formatEventTime(event) { + return `${event.startTime} - ${event.endTime}`; + } + + function renderGrid() { + const start = monthStartGridDate(visibleMonth); + const end = monthEndGridDate(visibleMonth); + const events = eventsBetween(start, end); + const eventsByDate = new Map(); + + for (const event of events) { + const dayEvents = eventsByDate.get(event.date) || []; + dayEvents.push(event); + eventsByDate.set(event.date, dayEvents); + } + + title.textContent = monthFormatter.format(visibleMonth); + grid.innerHTML = ""; + + for (let date = new Date(start); date <= end; date = addDays(date, 1)) { + const dateKey = toDateKey(date); + const dayEvents = eventsByDate.get(dateKey) || []; + const cell = document.createElement("button"); + cell.type = "button"; + cell.className = "calendar-day"; + cell.dataset.date = dateKey; + cell.setAttribute("role", "gridcell"); + cell.setAttribute("aria-label", `${dayFormatter.format(date)}${dayEvents.length ? `, ${dayEvents.length} event` : ""}`); + + if (!isSameMonth(date, visibleMonth)) { + cell.classList.add("calendar-day-muted"); + } + + if (dateKey === toDateKey(today)) { + cell.classList.add("calendar-day-today"); + } + + if (dateKey === selectedDate) { + cell.classList.add("calendar-day-selected"); + cell.setAttribute("aria-selected", "true"); + } + + const number = document.createElement("span"); + number.className = "calendar-day-number"; + number.textContent = String(date.getDate()); + cell.append(number); + + const eventStack = document.createElement("span"); + eventStack.className = "calendar-day-events"; + for (const event of dayEvents.slice(0, 2)) { + const pill = document.createElement("span"); + pill.className = "calendar-event-pill"; + pill.textContent = event.title; + eventStack.append(pill); + } + + if (dayEvents.length > 2) { + const more = document.createElement("span"); + more.className = "calendar-event-more"; + more.textContent = `+${dayEvents.length - 2} more`; + eventStack.append(more); + } + + cell.append(eventStack); + cell.addEventListener("click", () => { + selectedDate = dateKey; + render(); + }); + grid.append(cell); + } + } + + function renderList() { + const selected = fromDateKey(selectedDate); + const monthStart = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth(), 1); + const monthEnd = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 0); + const selectedEvents = eventsBetween(selected, selected); + const upcomingEvents = eventsBetween(monthStart, monthEnd) + .filter((event) => event.date >= toDateKey(today)) + .slice(0, 8); + const events = selectedEvents.length ? selectedEvents : upcomingEvents; + + monthTitle.textContent = selectedEvents.length + ? dayFormatter.format(selected) + : "Upcoming Events"; + list.innerHTML = ""; + + if (!events.length) { + const empty = document.createElement("p"); + empty.className = "calendar-empty"; + empty.textContent = "No events for this view."; + list.append(empty); + return; + } + + for (const event of events) { + const item = document.createElement("article"); + item.className = "calendar-event-card"; + const eventDate = fromDateKey(event.date); + + const date = document.createElement("p"); + date.className = "calendar-event-date"; + date.textContent = dayFormatter.format(eventDate); + + const heading = document.createElement("h4"); + heading.textContent = event.title; + + const time = document.createElement("p"); + time.textContent = formatEventTime(event); + + const location = document.createElement("p"); + location.textContent = event.location; + + const details = document.createElement("p"); + details.textContent = event.details; + + const link = document.createElement("a"); + link.href = event.url; + link.textContent = "Sunday Services"; + + item.append(date, heading, time, location, details, link); + list.append(item); + } + } + + function icsDate(dateKey, time) { + const [year, month, day] = dateKey.split("-"); + const [hours, minutes] = time.split(":"); + return `${year}${month}${day}T${hours}${minutes}00`; + } + + function escapeIcs(value) { + return String(value) + .replace(/\\/g, "\\\\") + .replace(/;/g, "\\;") + .replace(/,/g, "\\,") + .replace(/\n/g, "\\n"); + } + + function exportCalendar() { + const start = new Date(today.getFullYear(), today.getMonth(), 1); + const end = new Date(today.getFullYear() + 1, today.getMonth() + 1, 0); + const events = eventsBetween(start, end); + const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); + const siteUrl = "https://familyfed.ie"; + const lines = [ + "BEGIN:VCALENDAR", + "VERSION:2.0", + "PRODID:-//FFWPU Ireland//Events Calendar//EN", + "CALSCALE:GREGORIAN", + "METHOD:PUBLISH", + ]; + + for (const event of events) { + lines.push( + "BEGIN:VEVENT", + `UID:${event.id}@familyfed.ie`, + `DTSTAMP:${stamp}`, + `DTSTART:${icsDate(event.date, event.startTime)}`, + `DTEND:${icsDate(event.date, event.endTime)}`, + `SUMMARY:${escapeIcs(event.title)}`, + `DESCRIPTION:${escapeIcs(event.details)}`, + `LOCATION:${escapeIcs(event.location)}`, + `URL:${siteUrl}${event.url}`, + "END:VEVENT", + ); + } + + lines.push("END:VCALENDAR"); + + const blob = new Blob([lines.join("\r\n")], { type: "text/calendar;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "familyfed-events.ics"; + document.body.append(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + } + + function render() { + renderGrid(); + renderList(); + } + + calendar.querySelector("[data-calendar-prev]")?.addEventListener("click", () => { + visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() - 1, 1); + selectedDate = toDateKey(new Date(visibleMonth.getFullYear(), visibleMonth.getMonth(), 1)); + render(); + }); + + calendar.querySelector("[data-calendar-next]")?.addEventListener("click", () => { + visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 1); + selectedDate = toDateKey(new Date(visibleMonth.getFullYear(), visibleMonth.getMonth(), 1)); + render(); + }); + + calendar.querySelector("[data-calendar-today]")?.addEventListener("click", () => { + visibleMonth = new Date(today.getFullYear(), today.getMonth(), 1); + selectedDate = toDateKey(today); + render(); + }); + + calendar.querySelector("[data-calendar-export]")?.addEventListener("click", exportCalendar); + + render(); + })(); + </script> </SiteLayout> diff --git a/src/pages/speeches/index.astro b/src/pages/speeches/index.astro index 0165085b..d37f001b 100644 --- a/src/pages/speeches/index.astro +++ b/src/pages/speeches/index.astro @@ -5,12 +5,11 @@ import { getSpeechArchive, uniqueSorted } from "../../lib/speeches"; const speeches = getSpeechArchive(); const years = uniqueSorted(speeches.map((speech) => speech.year), "desc"); const speakers = uniqueSorted(speeches.map((speech) => speech.speaker)); -const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.length ? speech.categories : [speech.category])); --- <SiteLayout title="Speeches Archive – FFWPU Ireland" - description="Search the FFWPU Ireland archive of speeches by Rev. Sun Myung Moon and Dr. Hak Ja Han Moon by year, speaker, category, and text." + description="Search the FFWPU Ireland archive of speeches by Rev. Sun Myung Moon and Dr. Hak Ja Han Moon by year, speaker, and text." bodyClass="page-template page-template-templates page-template-template-onecolumn page-template-templatestemplate-onecolumn-php page custom-background tribe-no-js metaslider-plugin parabola-image-three caption-light meta-light parabola_triagles parabola-menu-left" pathname="/speeches/" canonical="/speeches/" @@ -47,14 +46,6 @@ const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.l </select> </div> - <div class="speech-filter"> - <label for="speech-category">Category</label> - <select id="speech-category" name="category"> - <option value="">All categories</option> - {categories.map((category) => <option value={category}>{category}</option>)} - </select> - </div> - <button class="speech-reset" type="reset">Reset filters</button> </form> @@ -74,9 +65,6 @@ const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.l <a href={speech.url}>{speech.title}</a> </h2> <p>{speech.excerpt}</p> - <div class="speech-result-categories"> - {(speech.categories.length ? speech.categories : [speech.category]).map((category) => <span>{category}</span>)} - </div> </article> )) } @@ -98,9 +86,8 @@ const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.l const searchInput = document.getElementById("speech-search"); const yearSelect = document.getElementById("speech-year"); const speakerSelect = document.getElementById("speech-speaker"); - const categorySelect = document.getElementById("speech-category"); - if (!form || !results || !count || !searchInput || !yearSelect || !speakerSelect || !categorySelect) { + if (!form || !results || !count || !searchInput || !yearSelect || !speakerSelect) { return; } @@ -120,7 +107,6 @@ const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.l q: searchInput.value.trim(), year: yearSelect.value, speaker: speakerSelect.value, - category: categorySelect.value, }; } @@ -141,7 +127,6 @@ const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.l searchInput.value = params.get("q") || ""; yearSelect.value = params.get("year") || ""; speakerSelect.value = params.get("speaker") || ""; - categorySelect.value = params.get("category") || ""; } function matchesSpeech(speech, filters) { @@ -151,9 +136,6 @@ const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.l if (filters.speaker && speech.speaker !== filters.speaker) { return false; } - if (filters.category && !speech.categories.includes(filters.category) && speech.category !== filters.category) { - return false; - } if (!filters.q) { return true; } @@ -188,11 +170,7 @@ const categories = uniqueSorted(speeches.flatMap((speech) => speech.categories.l const excerpt = document.createElement("p"); excerpt.textContent = speech.excerpt; - const categories = document.createElement("div"); - categories.className = "speech-result-categories"; - appendMeta(categories, speech.categories.length ? speech.categories : [speech.category]); - - article.append(meta, title, excerpt, categories); + article.append(meta, title, excerpt); return article; }