-
Notifications
You must be signed in to change notification settings - Fork 4
Session Table #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mirumodapon
wants to merge
24
commits into
main
Choose a base branch
from
feat/session-ui
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Session Table #102
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
0b23139
feat(ui): add `CpSessionItem` and `CpBadge` Component
mirumodapon 4246e38
feat(ui): refactor toggle functionality in CpPopup component
mirumodapon b996ce8
feat(ui): implement logic of CpSessionTable component for session gri…
mirumodapon 4894702
feat(ui): remove text-justify class from title in CpSessionItem compo…
mirumodapon e691378
style: fix with eslint
mirumodapon dd57390
feat(ui): integrate drag scroll functionality in CpSessionTable compo…
mirumodapon 9d6fccc
feat: remove useless statement
mirumodapon cfe19ce
feat(ui): implement CpSessionDaySelector for day selection in session…
mirumodapon 06a17a2
fix(ui): session item should can be clicked
mirumodapon 8c25dc9
fix: current session item url
mirumodapon 60e5965
fix(ui): correct styling issues in CpSessionItem components
mirumodapon 7528640
fix(types): update PretalxData interface to allow numeric keys in map…
mirumodapon e6f13ef
perf(ui): improve drag scroll event handling and cleanup
mirumodapon 01a1ee6
fix: improve parseMinutes function to handle ISO time strings correctly
mirumodapon acd5d07
fix(ui): update session selection logic and add i18n support for no s…
mirumodapon c58e4ac
fix(ui): adjust time label generation to exclude end time
mirumodapon 224c10c
feat(ui): enhance button styling for day selection in CpSessionDaySel…
mirumodapon 2403534
feat(ui): correct heading level for session title in CpSessionItem
mirumodapon 70f24da
fix(ui): remove unused selectedDay prop from CpSessionDaySelector
mirumodapon 385b726
perf(ui): simplify time parsing logic in CpSessionTable
mirumodapon f2bbade
perf(ui): remove unused type import for session data in index.vue
mirumodapon 3a352e3
fix(types): update PretalxData map type to use string keys only
mirumodapon 4962965
fix(ui): adjust grid-row calculation for time labels in CpSessionTable
mirumodapon e0141e5
fix(ui): refactor selectedDay handling to use computed property with …
mirumodapon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <script setup lang="ts"> | ||
| const props = defineProps<{ days: string[] }>() | ||
|
|
||
| const selectedDay = defineModel<string>() | ||
|
|
||
| function formatDayLabel(day: string) { | ||
| return new Intl.DateTimeFormat('en-US', { | ||
| day: 'numeric', | ||
| month: 'short', | ||
| timeZone: 'Asia/Taipei', | ||
| }) | ||
| .format(new Date(`${day}T00:00:00+08:00`)) | ||
| .replace(' ', '.') | ||
| } | ||
|
|
||
| const activeDay = computed(() => selectedDay.value ?? props.days[0] ?? '') | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="px-6 pb-4 pt-3 border-b border-primary-100 flex justify-center"> | ||
| <div class="flex flex-wrap gap-3 items-center justify-center"> | ||
| <button | ||
| v-for="day in days" | ||
| :key="day" | ||
| class="text-2xl text-white font-bold px-8 py-2 border-3 border-primary-700 rounded-full min-w-34 cursor-pointer italic shadow-[0_4px_0_0_var(--un-shadow-color)] shadow-primary-700 transition-colors" | ||
| :class="day === activeDay ? 'bg-primary-400' : 'bg-gray-200'" | ||
| type="button" | ||
| @click="selectedDay = day" | ||
| > | ||
| {{ formatDayLabel(day) }} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <script setup lang="ts"> | ||
| import CpBadge from '~/components/shared/CpBadge.vue' | ||
|
|
||
| defineProps<{ | ||
| title: string | ||
| start: string | ||
| end: string | ||
| speaker: string | ||
| tags: string[] | ||
| }>() | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="text-primary-600 p-2 border border-primary-100 rounded bg-primary-50 relative"> | ||
| <div class="flex flex-col"> | ||
| <h3 class="text-base text-inherit font-normal my-1"> | ||
| {{ title }} | ||
| </h3> | ||
| <time class="text-base opacity-50">{{ start }} ~ {{ end }}</time> | ||
| <p class="text-sm my-1"> | ||
| {{ speaker }} | ||
| </p> | ||
| <p class="flex gap-1"> | ||
| <CpBadge | ||
| v-for="tag in tags" | ||
| :key="tag" | ||
| > | ||
| {{ tag }} | ||
| </CpBadge> | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| <script setup lang="ts"> | ||
| import type { SessionSummary } from '#shared/types/session' | ||
| import { useI18n } from '#imports' | ||
| import { useDragScroll } from '~/composables/useDragScroll' | ||
| import CpSessionItem from './CpSessionItem.vue' | ||
|
|
||
| const { sessions: _sessions, day, timeRange, interval, rowHeight } = defineProps<{ | ||
| day: string | ||
| timeRange: [string, string] | ||
| sessions: SessionSummary[] | ||
| interval: number | ||
| rowHeight: number | ||
| }>() | ||
|
|
||
| const { locale } = useI18n() | ||
|
|
||
| const { containerRef, isDragging } = useDragScroll({ vertical: false }) | ||
|
|
||
| function parseMinutes(isoStr: string) { | ||
| const match = isoStr.match(/T(\d{2}):(\d{2})/) | ||
| if (!match) { | ||
| throw new Error(`Invalid ISO time string: ${isoStr}`) | ||
| } | ||
| const [, hours, minutes] = match | ||
| return Number(hours) * 60 + Number(minutes) | ||
| } | ||
|
|
||
| const timeStart = computed(() => parseMinutes(`T${timeRange[0]}`)) | ||
| const timeEnd = computed(() => parseMinutes(`T${timeRange[1]}`)) | ||
| const totalGridRows = computed(() => Math.round((timeEnd.value - timeStart.value) / interval)) | ||
|
|
||
| function formatTime(minutes: number) { | ||
| const h = Math.floor(minutes / 60) | ||
| const m = minutes % 60 | ||
| return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}` | ||
| } | ||
|
|
||
| function toRow(minutes: number) { | ||
| return Math.round((minutes - timeStart.value) / interval) + 2 | ||
| } | ||
|
|
||
| const rooms = computed(() => { | ||
| if (!_sessions) { | ||
| return [] | ||
| } | ||
|
|
||
| const rooms = _sessions | ||
| .map((session) => session.room?.en) | ||
| .filter((value) => value !== undefined) | ||
|
|
||
| return [...new Set(rooms)].sort() satisfies string[] | ||
| }) | ||
|
|
||
| const timeLabels = computed(() => { | ||
| const labels: { label: string, row: number }[] = [] | ||
| let m = Math.ceil(timeStart.value / 30) * 30 | ||
| while (m < timeEnd.value) { | ||
| labels.push({ label: formatTime(m), row: toRow(m) }) | ||
| m += 30 | ||
| } | ||
| return labels | ||
| }) | ||
|
|
||
| const sessions = computed(() => { | ||
| if (!_sessions) { | ||
| return [] | ||
| } | ||
|
|
||
| return _sessions | ||
| .filter((session) => [ | ||
| session.start?.startsWith(day), | ||
| session.end, | ||
| session.room, | ||
| ].every(Boolean)) | ||
|
pan93412 marked this conversation as resolved.
|
||
| .map((session) => { | ||
| const startMins = parseMinutes(session.start!) | ||
| const endMins = parseMinutes(session.end!) | ||
|
|
||
| const { title } = session[locale.value] | ||
| const speaker = session.speakers?.map((s) => s[locale.value].name).join(', ') | ||
|
|
||
| return { | ||
| id: session.id, | ||
| title, | ||
| speaker, | ||
| start: session.start!.slice(11, 16), | ||
| end: session.end!.slice(11, 16), | ||
| row: [toRow(startMins), toRow(endMins)], | ||
| col: rooms.value.findIndex((r) => session.room!.en === r) + 2, | ||
| tags: [], | ||
| } | ||
| }) | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div | ||
| ref="containerRef" | ||
| class="border border-gray-200 rounded-xl grid overflow-x-auto" | ||
| :class="isDragging ? 'cursor-grabbing select-none' : 'cursor-grab'" | ||
| :style="{ | ||
| gridTemplateColumns: `4.5rem repeat(${rooms.length}, minmax(11rem, 1fr))`, | ||
| gridTemplateRows: `3rem repeat(${totalGridRows}, ${rowHeight}px)`, | ||
| }" | ||
| > | ||
| <div | ||
| class="border-b border-gray-200 bg-gray-50 left-0 top-0 sticky z-20" | ||
| :style="{ | ||
| 'grid-row': 1, | ||
| 'grid-column': 1, | ||
| }" | ||
| /> | ||
|
|
||
| <div | ||
| v-for="(room, i) in rooms" | ||
| :key="room" | ||
| class="text-sm text-primary-400 font-medium border-b border-gray-200 bg-gray-50 flex items-center justify-center" | ||
| :style="{ | ||
| 'grid-row': 1, | ||
| 'grid-column': i + 2, | ||
| }" | ||
| > | ||
| {{ room }} | ||
| </div> | ||
|
|
||
| <template | ||
| v-for="label in timeLabels" | ||
| :key="label.label" | ||
| > | ||
| <div | ||
| class="text-xs text-gray-400 pr-2 pt-0.5 text-right border-t border-gray-100 bg-gray-50 flex items-start justify-center" | ||
| :style="{ | ||
| 'grid-row': `${label.row} / ${label.row + 30 / interval}`, | ||
| 'grid-column': 1, | ||
| }" | ||
| > | ||
|
rileychh marked this conversation as resolved.
|
||
| {{ label.label }} | ||
| </div> | ||
| <div | ||
| v-for="(_, i) in rooms" | ||
| :key="i" | ||
| class="border-t border-gray-100" | ||
| :style="{ | ||
| 'grid-row': label.row, | ||
| 'grid-column': i + 2, | ||
| }" | ||
| /> | ||
| </template> | ||
|
|
||
| <NuxtLink | ||
| v-for="session in sessions" | ||
| :key="session.id" | ||
| class="overflow-hidden" | ||
| :draggable="false" | ||
| :style="{ | ||
| 'grid-row': `${session.row[0]} / ${session.row[1]}`, | ||
| 'grid-column': session.col, | ||
| }" | ||
| :to="`/session/${session.id}`" | ||
|
rileychh marked this conversation as resolved.
|
||
| @dragstart.prevent | ||
| > | ||
| <CpSessionItem | ||
| class="h-full" | ||
| :end="session.end" | ||
| :speaker="session.speaker" | ||
| :start="session.start" | ||
| :tags="session.tags" | ||
| :title="session.title" | ||
| /> | ||
| </NuxtLink> | ||
| </div> | ||
| </template> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <script setup lang="ts"> | ||
| defineProps<{ text?: string }>() | ||
| </script> | ||
|
|
||
| <template> | ||
| <span class="text-xs text-primary-600 font-semibold px-2 py-0.5 text-center rounded-full bg-primary-100 inline-block"> | ||
| <slot v-if="$slots.default" /> | ||
| <template v-else> | ||
| {{ text }} | ||
| </template> | ||
| </span> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| interface UseDragScrollOptions { | ||
| horizontal?: boolean | ||
| vertical?: boolean | ||
| } | ||
|
|
||
| export function useDragScroll({ horizontal = true, vertical = true }: UseDragScrollOptions = {}) { | ||
| const containerRef = ref<HTMLElement>() | ||
| const isDragging = ref(false) | ||
| const suppressClick = ref(false) | ||
|
|
||
| const dragState = reactive({ | ||
| pointerId: -1, | ||
| startX: 0, | ||
| startY: 0, | ||
| scrollLeft: 0, | ||
| scrollTop: 0, | ||
| }) | ||
|
|
||
| function handlePointerDown(event: PointerEvent) { | ||
| if (event.button !== 0 || !containerRef.value) { | ||
| return | ||
| } | ||
|
|
||
| dragState.pointerId = event.pointerId | ||
| dragState.startX = event.clientX | ||
| dragState.startY = event.clientY | ||
| dragState.scrollLeft = containerRef.value.scrollLeft | ||
| dragState.scrollTop = containerRef.value.scrollTop | ||
|
mirumodapon marked this conversation as resolved.
|
||
| isDragging.value = true | ||
| suppressClick.value = false | ||
|
|
||
| window.addEventListener('pointermove', handlePointerMove) | ||
| window.addEventListener('pointerup', stopDragging) | ||
| window.addEventListener('pointercancel', stopDragging) | ||
| } | ||
|
|
||
| function handlePointerMove(event: PointerEvent) { | ||
| if (!isDragging.value || !containerRef.value) { | ||
| return | ||
| } | ||
|
|
||
| event.preventDefault() | ||
|
|
||
| const deltaX = event.clientX - dragState.startX | ||
| const deltaY = event.clientY - dragState.startY | ||
|
|
||
| const moved = (horizontal && Math.abs(deltaX) > 4) || (vertical && Math.abs(deltaY) > 4) | ||
| if (moved) { | ||
| suppressClick.value = true | ||
| } | ||
|
|
||
| if (horizontal) { | ||
| containerRef.value.scrollLeft = dragState.scrollLeft - deltaX | ||
| } | ||
| if (vertical) { | ||
| containerRef.value.scrollTop = dragState.scrollTop - deltaY | ||
| } | ||
| } | ||
|
|
||
| function stopDragging(event?: PointerEvent) { | ||
| if (event && event.pointerId !== dragState.pointerId) { | ||
| return | ||
| } | ||
|
|
||
| dragState.pointerId = -1 | ||
| isDragging.value = false | ||
|
|
||
| window.removeEventListener('pointermove', handlePointerMove) | ||
| window.removeEventListener('pointerup', stopDragging) | ||
| window.removeEventListener('pointercancel', stopDragging) | ||
| } | ||
|
|
||
| function handleClickCapture(event: MouseEvent) { | ||
| if (!suppressClick.value) { | ||
| return | ||
| } | ||
|
|
||
| event.preventDefault() | ||
| event.stopPropagation() | ||
| suppressClick.value = false | ||
| } | ||
|
|
||
| function handleDragStart(event: DragEvent) { | ||
| event.preventDefault() | ||
| } | ||
|
|
||
| watchEffect((onCleanup) => { | ||
| const el = containerRef.value | ||
| if (!el) { | ||
| return | ||
| } | ||
|
|
||
| el.addEventListener('pointerdown', handlePointerDown) | ||
| el.addEventListener('click', handleClickCapture, { capture: true }) | ||
| el.addEventListener('dragstart', handleDragStart) | ||
|
|
||
| onCleanup(() => { | ||
| el.removeEventListener('pointerdown', handlePointerDown) | ||
| el.removeEventListener('click', handleClickCapture, { capture: true }) | ||
| el.removeEventListener('dragstart', handleDragStart) | ||
| // Clean up window listeners in case drag was in progress | ||
| window.removeEventListener('pointermove', handlePointerMove) | ||
| window.removeEventListener('pointerup', stopDragging) | ||
| window.removeEventListener('pointercancel', stopDragging) | ||
| }) | ||
| }) | ||
|
|
||
| return { | ||
| containerRef, | ||
| isDragging, | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.