Skip to content

Commit

Permalink
Merge pull request #136 from OneBusAway/more-fnf
Browse files Browse the repository at this point in the history
Build a homegrown Accordion Component
  • Loading branch information
aaronbrethorst authored Nov 25, 2024
2 parents edbcaa7 + 082dc64 commit 607b394
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 139 deletions.
2 changes: 1 addition & 1 deletion src/components/StandalonePage.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div class="mx-auto h-full max-w-5xl overflow-y-auto pt-4">
<div class="mx-auto h-full max-w-5xl overflow-y-auto">
<slot></slot>
</div>
74 changes: 74 additions & 0 deletions src/components/containers/Accordion.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script>
import { setContext } from 'svelte';
import { writable, derived } from 'svelte/store';
// Props to track all possible item IDs
export let items = [];
// Store for animation state
const skipAnimation = writable(false);
// Create a store to track multiple active items using a Set
const activeItems = writable(new Set());
// Create dispatch for activeChanged event
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
// Methods to open/close all items
export const openAll = (animate = true) => {
skipAnimation.set(!animate);
activeItems.set(new Set(items));
dispatch('openAll', { animated: animate });
// Reset skip animation after a short delay
setTimeout(() => skipAnimation.set(false), 0);
};
export const closeAll = (animate = true) => {
skipAnimation.set(!animate);
activeItems.set(new Set());
dispatch('closeAll', { animated: animate });
// Reset skip animation after a short delay
setTimeout(() => skipAnimation.set(false), 0);
};
// Watch for changes to activeItems and dispatch event
$: {
dispatch('activeChanged', { activeItems: Array.from($activeItems) });
}
// Provide context for child AccordionItems and expose methods
setContext('accordion', {
registerItem: (id) => {
// Add the item ID to our items array if not already present
if (!items.includes(id)) {
items = [...items, id];
}
const isActive = derived(activeItems, ($activeItems) => $activeItems.has(id));
return {
isActive,
skipAnimation,
activate: () => {
activeItems.update((items) => {
const newItems = new Set(items);
if (newItems.has(id)) {
newItems.delete(id);
} else {
newItems.add(id);
}
return newItems;
});
}
};
},
openAll,
closeAll
});
</script>

<div
class="divide-y divide-gray-200 border-y border-gray-200 dark:divide-gray-700 dark:border-gray-700"
>
<slot />
</div>
47 changes: 47 additions & 0 deletions src/components/containers/AccordionItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script>
import { getContext } from 'svelte';
import { slide } from 'svelte/transition';
const id = crypto.randomUUID();
const { registerItem } = getContext('accordion');
const { isActive, skipAnimation, activate } = registerItem(id);
function toggle() {
activate();
}
</script>

<div class="relative">
<div class="sticky top-0 z-0 bg-white dark:bg-gray-800">
<button
type="button"
class="flex w-full items-center justify-between py-3 text-left text-base font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
class:text-gray-900={$isActive}
class:dark:text-white={$isActive}
on:click={toggle}
aria-expanded={$isActive}
>
<slot name="header" />
<svg
class="h-6 w-6 shrink-0 transition-transform"
class:rotate-180={$isActive}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>

{#if $isActive}
<div
transition:slide|local={{
duration: $skipAnimation ? 0 : 300
}}
>
<div class="py-3">
<slot />
</div>
</div>
{/if}
</div>
100 changes: 100 additions & 0 deletions src/components/schedule-for-stop/RouteScheduleTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script>
import { t } from 'svelte-i18n';
export let schedule;
function formatHour(hour) {
const hourInt = +hour;
if (hourInt === 0) return '12';
if (hourInt > 12) return hourInt - 12;
return hourInt;
}
function renderScheduleTable(schedule) {
const stopTimes = Object.entries(schedule.stopTimes);
const amTimes = stopTimes.filter(([hour]) => +hour < 12);
const pmTimes = stopTimes.filter(([hour]) => +hour >= 12);
return {
amTimes,
pmTimes
};
}
function extractMinutes(arrivalTime) {
return arrivalTime.replace(/[AP]M/, '').split(':')[1];
}
</script>

<div class="overflow-x-auto">
<table class="mt-4 w-full table-auto rounded-lg border border-gray-200 shadow-lg">
<thead class="bg-gray-100 text-gray-800">
<tr>
<th class="cursor-pointer px-6 py-3 text-left">{$t('schedule_for_stop.hour')}</th>
<th class="cursor-pointer px-6 py-3 text-left">{$t('schedule_for_stop.minutes')}</th>
</tr>
</thead>
<tbody>
<tr class="bg-gray-50 hover:bg-gray-100">
<td colspan="2" class="px-6 py-3 font-semibold text-gray-700">AM</td>
</tr>
{#if renderScheduleTable(schedule).amTimes.length === 0}
<tr>
<td colspan="2" class="border px-6 py-3 text-center text-gray-500">
{$t('schedule_for_stop.no_am_schedules_available')}
</td>
</tr>
{:else}
{#each renderScheduleTable(schedule).amTimes as [hour, times]}
<tr class="hover:bg-gray-100">
<td
class="border px-6 py-3 text-center text-lg font-semibold"
title="Full Time: {hour}:{extractMinutes(times[0].arrivalTime)}"
>
{formatHour(hour)} <span class="text-sm text-gray-600">AM</span>
</td>
<td class="border px-6 py-3 text-lg">
{#each times as stopTime, index (index)}
<span>
{extractMinutes(stopTime.arrivalTime)}
{index < times.length - 1 ? ', ' : ''}
</span>
{/each}
</td>
</tr>
{/each}
{/if}

<tr class="bg-gray-50 hover:bg-gray-100">
<td colspan="2" class="px-6 py-3 font-semibold text-gray-700">PM</td>
</tr>
{#if renderScheduleTable(schedule).pmTimes.length === 0}
<tr>
<td colspan="2" class="border px-6 py-3 text-center text-gray-500">
{$t('schedule_for_stop.no_pm_schedules_available')}
</td>
</tr>
{:else}
{#each renderScheduleTable(schedule).pmTimes as [hour, times]}
<tr class="hover:bg-gray-100">
<td
class="border px-6 py-3 text-center text-lg font-semibold"
title="Full Time: {hour}:{extractMinutes(times[0].arrivalTime)}"
>
{formatHour(hour)} <span class="text-sm text-gray-600">PM</span>
</td>
<td class="border px-6 py-3 text-lg">
{#each times as stopTime, index (index)}
<span>
{extractMinutes(stopTime.arrivalTime)}
{index < times.length - 1 ? ', ' : ''}
</span>
{/each}
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
114 changes: 0 additions & 114 deletions src/components/schedule-for-stop/ScheduleAccordionItem.svelte

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/stops/StopPageHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
export let stopDirection;
</script>

<div class="mb-8 text-center">
<div class="my-4 text-center">
<h1 class="flex items-center justify-center gap-2 text-3xl font-bold text-green-700">
{stopName}
</h1>
Expand Down
Loading

0 comments on commit 607b394

Please sign in to comment.