Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions app/components/feature/CpFavoriteViewToggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup lang="ts">
import type { Item } from '~/components/shared/CpGroupButton.vue'
import { useI18n } from 'vue-i18n'
import CpButton from '~/components/shared/CpButton.vue'
import CpGroupButton from '~/components/shared/CpGroupButton.vue'

type SessionViewItemKey = 'all' | 'favorite'

const model = defineModel<SessionViewItemKey>({ required: true })

const { t } = useI18n()

const viewItems = computed<Item<SessionViewItemKey>[]>(() => [
{ key: 'all', label: t('view.all') },
{ key: 'favorite', label: t('view.favorite'), icon: 'tabler:bookmark' },
])

function switchView() {
model.value = model.value === 'all' ? 'favorite' : 'all'
}
</script>

<template>
<!-- Flush segments so the toggle's height matches the adjacent search field. -->
<CpGroupButton
v-model="model"
class="hidden md:inline-flex"
:items="viewItems"
/>
<div class="border border-gray-200 rounded-md bg-white inline-flex overflow-hidden md:hidden">
<CpButton
:aria-label="model === 'favorite' ? t('view.favorite') : t('view.all')"
:aria-pressed="model === 'favorite'"
:class="model === 'favorite' ? '!bg-yellow-400 !text-white' : '!bg-white !text-black'"
variant="basic"
@click="switchView"
Comment thread
mirumodapon marked this conversation as resolved.
>
<Icon
:name="model === 'favorite' ? 'tabler:star-filled' : 'tabler:star'"
size="18"
/>
</CpButton>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</template>

<i18n lang="yaml">
en:
view:
all: 'Sessions'
favorite: 'Favorites'
zh:
view:
all: '議程'
favorite: '收藏'
</i18n>
68 changes: 39 additions & 29 deletions app/components/feature/CpSessionFilterBar.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { FilterOption } from '~/composables/useSessionFilter'
import CpGroupButton from '~/components/shared/CpGroupButton.vue'
import CpTextField from '~/components/shared/CpTextField.vue'
import CpSessionFilterDropdown from './CpSessionFilterDropdown.vue'
import CpSessionViewToggle from './CpSessionViewToggle.vue'

export type TableViewMode = 'track' | 'table'

Expand All @@ -17,43 +17,53 @@ const searchQuery = defineModel<string>('searchQuery', { default: '' })

const { t } = useI18n()

const viewModeItems = computed<{ key: TableViewMode, label: string, icon: string }[]>(() => [
const desktopModeItems = computed<{ key: TableViewMode, label: string, icon: string }[]>(() => [
{ key: 'track', label: t('viewMode.track'), icon: 'tabler:layout-rows' },
{ key: 'table', label: t('viewMode.table'), icon: 'tabler:layout-columns' },
])

const mobileModeItems = computed<{ key: TableViewMode, label: string, icon: string, srOnlyLabel: boolean }[]>(() => [
{ key: 'track', label: t('viewMode.track'), icon: 'tabler:clock', srOnlyLabel: true },
{ key: 'table', label: t('viewMode.table'), icon: 'tabler:list-details', srOnlyLabel: true },
])
</script>

<template>
<div class="flex flex-col gap-3 w-[var(--viewport-width,100vw)] items-stretch sm:flex-row sm:items-center sm:justify-between">
<div class="flex shrink-0 gap-3 items-center justify-center sm:justify-start">
<CpSessionViewToggle
v-model="viewMode"
class="hidden sm:inline-flex"
:items="viewModeItems"
/>
<CpSessionFilterDropdown
v-if="!preview"
v-model="selectedTagIds"
icon="tabler:tag"
:options="tagOptions"
type="tags"
/>
</div>
<div class="flex flex-wrap gap-3 w-[var(--viewport-width,100vw)] items-stretch sm:flex-nowrap">
<CpGroupButton
v-model="viewMode"
class="hidden md:inline-flex"
:items="desktopModeItems"
/>

<!-- Controls below the search field on mobile, to its left on desktop. -->
<div
<CpGroupButton
v-model="viewMode"
class="hidden sm:inline-flex md:hidden"
:items="mobileModeItems"
/>

<CpSessionFilterDropdown
v-if="!preview"
class="flex flex-col-reverse gap-3 items-center sm:flex-row sm:items-center"
>
<slot name="controls" />
v-model="selectedTagIds"
icon="tabler:tag"
:options="tagOptions"
type="tags"
/>

<span class="flex-1" />

<CpTextField
v-model="searchQuery"
class="min-w-0 w-full sm:flex-none sm:h-9 sm:w-80"
:clear-label="t('clear')"
:placeholder="t('placeholder')"
/>
</div>
<slot
v-if="!preview"
name="controls"
/>

<CpTextField
v-if="!preview"
v-model="searchQuery"
class="min-w-0 w-full order-first sm:flex-none sm:h-9 sm:w-60 sm:order-none"
:clear-label="t('clear')"
:placeholder="t('placeholder')"
/>
</div>
</template>

Expand Down
2 changes: 1 addition & 1 deletion app/components/feature/CpSessionFilterDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function clearSelection() {

<div
v-if="open"
class="mt-1 p-3 border border-gray-300 rounded-md bg-gray-50 flex flex-col gap-2 max-w-[calc(100vw-32px)] w-72 shadow-md left-1/2 absolute z-dropdown sm:p-2 sm:gap-1 sm:max-w-[calc(100vw-64px)] -translate-x-1/2 sm:translate-x-0 sm:left-0"
class="mt-1 p-3 border border-gray-300 rounded-md bg-gray-50 flex flex-col gap-2 max-w-[calc(100vw-32px)] w-72 shadow-md left-0 absolute z-dropdown sm:p-2 sm:gap-1 sm:max-w-[calc(100vw-64px)]"
>
<CpTextField
v-model="searchQuery"
Expand Down
41 changes: 25 additions & 16 deletions app/components/feature/CpSessionItem.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script setup lang="ts">
import CpBadge from '~/components/shared/CpBadge.vue'

defineProps<{
// Session link target, rendered as an overlay so the bookmark isn't nested in the anchor.
to: string
Expand All @@ -11,6 +9,11 @@ defineProps<{
room?: string
tags: string[]
favorite?: boolean
track: {
id?: number
name: string
color: string
}
// Resolved by the parent to avoid a per-card i18n scope (see CpSessionTable).
favoriteLabel?: string
// Shared-list preview: show the filled bookmark as a static, non-toggleable indicator.
Expand All @@ -24,10 +27,8 @@ defineEmits<{

<template>
<div
class="p-2 border rounded transition-colors relative"
:class="favorite
? 'text-cp-orange-700 border-cp-orange-300 bg-cp-orange-50'
: 'text-primary-600 border-primary-100 bg-primary-50'"
class="p-2 border border-l-4 border-gray-300 rounded transition-colors relative"
:style="{ 'border-left-color': track.color }"
>
<!-- Overlay link, so the bookmark is a sibling rather than nested in the anchor.
Both are absolute; the bookmark follows in DOM order, so it paints on top. -->
Expand All @@ -42,42 +43,50 @@ defineEmits<{
aria-hidden="true"
class="text-xl text-cp-orange-600 leading-none p-1 pointer-events-none right-1 top-1 absolute"
>
<Icon name="tabler:bookmark-filled" />
<Icon name="tabler:star-filled" />
</span>
<button
v-else
:aria-label="favoriteLabel"
:aria-pressed="favorite"
class="text-xl leading-none p-1 rounded cursor-pointer transition-colors right-1 top-1 absolute"
:class="favorite
? 'text-cp-orange-600 hover:text-cp-orange-700'
: 'text-primary-300 hover:text-primary-500'"
? 'text-yellow-400'
: 'text-gray-600'"
type="button"
@click.prevent.stop="$emit('toggleFavorite')"
>
<Icon :name="favorite ? 'tabler:bookmark-filled' : 'tabler:bookmark'" />
<Icon :name="favorite ? 'tabler:star-filled' : 'tabler:star'" />
</button>
<div class="pr-6 flex flex-col">
<h3 class="text-base text-inherit font-normal my-1">
<span
class="text-xs text-white font-medium px-3 py-1 rounded-full flex gap-0.5 w-max items-center"
:style="{ backgroundColor: track.color }"
>
{{ track.name }}
</span>
Comment on lines +62 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the track pill against an empty track.name.

Unlike room (guarded with v-if="room && room.trim() !== ''" a few lines below), this pill renders unconditionally. For track-less sessions, track.name resolves to '' (per CpSessionList.vue's localeName fallback), leaving a colored, empty, rounded pill floating in the card.

🛡️ Proposed fix
       <span
+        v-if="track.name"
         class="text-xs text-white font-medium px-3 py-1 rounded-full flex gap-0.5 w-max items-center"
         :style="{ backgroundColor: track.color }"
       >
         {{ track.name }}
       </span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
class="text-xs text-white font-medium px-3 py-1 rounded-full flex gap-0.5 w-max items-center"
:style="{ backgroundColor: track.color }"
>
{{ track.name }}
</span>
<span
v-if="track.name"
class="text-xs text-white font-medium px-3 py-1 rounded-full flex gap-0.5 w-max items-center"
:style="{ backgroundColor: track.color }"
>
{{ track.name }}
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/feature/CpSessionItem.vue` around lines 62 - 67, Update the
track pill in CpSessionItem.vue to render only when track.name is non-empty
after trimming whitespace, matching the existing room guard behavior. Keep the
pill’s styling and displayed track.name unchanged when a valid name is present.

<h3 class="text-base text-inherit font-bold my-1">
{{ title }}
</h3>
<p class="text-sm my-1">
{{ speaker }}
</p>
<div class="my-1 flex gap-2 items-center">
<template v-if="room && room.trim() !== ''">
<span>{{ room }}</span>
<span>•</span>
</template>
<time class="text-base opacity-50">{{ start }} ~ {{ end }}</time>
</div>
<p class="text-sm my-1">
{{ speaker }}
</p>

<p class="flex gap-1">
<CpBadge
<span
v-for="tag in tags"
:key="tag"
class="text-xs text-gray-600 font-medium px-3 py-1 rounded-full bg-stone-100"
>
{{ tag }}
</CpBadge>
</span>
</p>
</div>
</div>
Expand Down
88 changes: 66 additions & 22 deletions app/components/feature/CpSessionList.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script lang="ts" setup>
import type { SessionSummary } from '#shared/types/session'
import type { SessionSummary, SessionTrack } from '#shared/types/session'
import { useI18n } from '#imports'
import { useFavoriteLabel, useFavorites } from '~/composables/useFavorites'
import { DEFAULT_TRACK_COLOR, trackKey } from '~/utils/tracks'
import CpSessionItem from './CpSessionItem.vue'

const { sessions: _sessions, preview = false } = defineProps<{
const { sessions: _sessions, trackColors, preview = false } = defineProps<{
sessions: SessionSummary[]
trackColors: Map<string, string>
// Shared-list preview: render every session as a read-only favorite.
preview?: boolean
}>()
Expand All @@ -15,6 +17,11 @@ const localePath = useLocalePath()
const { isFavorite, toggleFavorite } = useFavorites()
const favoriteLabel = useFavoriteLabel(t)

function localeName(name?: SessionTrack['name']) {
const { en = '', 'zh-hant': zh = '' } = name ?? {}
return locale.value === 'zh' ? zh || en : en || zh
}

const sessions = computed(() => {
if (!_sessions) {
return {}
Expand All @@ -31,7 +38,12 @@ const sessions = computed(() => {
room: locale.value === 'zh'
? (session.room?.['zh-hant'] || session.room?.en || '')
: (session.room?.en || session.room?.['zh-hant'] || ''),
tags: [],
tags: session.tags,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Localize difficulty tags in the mobile list

For sessions with a normalized difficulty, passing session.tags through unchanged makes the Chinese mobile schedule display API enums such as Elementary and Professional. The filter and desktop schedule already translate these values (useSessionFilter.ts and CpSessionTable.vue), so apply the same locale mapping before rendering the newly populated mobile badges.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

track: {
id: session.track?.id,
name: localeName(session.track?.name),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Label or hide the badge for sessions without a track

When Pretalx supplies a null or unknown track—which parseTrack and SessionSummarySchema explicitly allow—localeName(undefined) produces an empty string, but CpSessionItem still renders the colored track pill. Such sessions therefore show an unexplained blank badge on mobile; use a localized “Other” fallback as the desktop track view does, or omit the badge.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

color: trackColors.get(trackKey(session)) ?? DEFAULT_TRACK_COLOR,
},
})),
(session) => session.start,
)
Expand All @@ -41,30 +53,60 @@ const times = computed(() => Object.keys(sessions.value).sort())
</script>

<template>
<div class="flex flex-col gap-6 w-[var(--viewport-width,100vw)] isolate">
<div class="p-3 flex flex-col gap-6 w-[var(--viewport-width,100vw)] isolate">
<section
v-for="time in times"
:key="time"
class="group/section"
>
<h3 class="text-lg text-primary-400 font-medium mb-2 py-1 bg-white top-0 sticky z-content">
{{ time }}
<input
:id="`session-time-${time}`"
checked
class="sr-only"
type="checkbox"
>
<h3 class="text-sm font-medium mb-2 bg-white top-0 sticky z-content">
<label
class="text-primary-400 py-1 flex gap-1 w-full cursor-pointer select-none items-center justify-between"
:for="`session-time-${time}`"
>
<div class="flex gap-x-2 items-center">
<span class="w-[5ch] block">{{ time }}</span>
<Icon
class="text-md text-primary-400"
name="tabler:circle"
/>
<span v-if="sessions?.[time] && sessions[time].length > 1">
{{ t('concurrent', { count: sessions[time].length }) }}
</span>
Comment thread
mirumodapon marked this conversation as resolved.
Comment thread
mirumodapon marked this conversation as resolved.
</div>
<Icon
class="text-sm transition-transform duration-300 group-has-[input:not(:checked)]/section:-rotate-180"
name="tabler:chevron-up"
/>
</label>
</h3>
<div class="flex flex-col gap-2">
<CpSessionItem
v-for="session in sessions[time]"
:key="session.id"
:end="session.end"
:favorite="preview || isFavorite(session.id)"
:favorite-label="favoriteLabel(session.id, preview)"
:readonly="preview"
:room="session.room"
:speaker="session.speakers"
:start="session.start"
:tags="session.tags"
:title="session.title"
:to="localePath(`/session/${session.id}`)"
@toggle-favorite="toggleFavorite(session.id)"
/>
<div class="grid grid-rows-[1fr] transition-[grid-template-rows] duration-300 ease-in-out group-has-[input:not(:checked)]/section:grid-rows-[0fr]">
<div class="overflow-hidden">
<div class="pb-2 flex flex-col gap-2">
<CpSessionItem
v-for="session in sessions[time]"
:key="session.id"
:end="session.end"
:favorite="preview || isFavorite(session.id)"
:favorite-label="favoriteLabel(session.id, preview)"
:readonly="preview"
:room="session.room"
:speaker="session.speakers"
:start="session.start"
:tags="session.tags"
:title="session.title"
:to="localePath(`/session/${session.id}`)"
:track="session.track"
@toggle-favorite="toggleFavorite(session.id)"
/>
</div>
</div>
</div>
</section>
</div>
Expand All @@ -74,7 +116,9 @@ const times = computed(() => Object.keys(sessions.value).sort())
en:
add: 'Add to favorites'
remove: 'Remove from favorites'
concurrent: '{count} sessions starting at the same time'
zh:
add: '加入收藏'
remove: '取消收藏'
concurrent: '{count} 場同時開始'
</i18n>
2 changes: 1 addition & 1 deletion app/components/feature/CpSessionShareButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function share() {
size="18"
/>
</template>
<span class="hidden sm:inline">{{ copied ? t('copied') : t('share') }}</span>
<span class="hidden md:inline">{{ copied ? t('copied') : t('share') }}</span>
</CpButton>
</template>

Expand Down
Loading