Skip to content
Draft
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
162 changes: 162 additions & 0 deletions app/components/feature/CpSessionInfoCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

const props = defineProps<{
title: string
time: string
speakers: {
id?: string
name: string
bio: string
avatar?: string
}[]
room: string
coWrite?: string
tags: string[]
description: string
}>()

const { t } = useI18n()

const speakerNames = computed(() =>
props.speakers.map((s) => s.name).join(', '),
)
</script>

<template>
<article class="p-6 bg-white flex flex-col gap-3">
<header>
<h1 class="text-2xl text-primary-400 leading-tight font-bold mb-4">
{{ title }}
</h1>
<table class="border-separate border-spacing-4">
<tbody>
<tr class="border-none">
<td class="text-gray-400">
<Icon name="tabler:clock" />
{{ t("time") }}
</td>
<td class="text-gray-700">
{{ time }}
</td>
</tr>
<tr class="border-none">
<td class="text-gray-400">
<Icon name="tabler:user" />
{{ t("speaker") }}
</td>
<td class="text-gray-700">
{{ speakerNames }}
</td>
</tr>
<tr class="border-none">
<td class="text-gray-400">
<Icon name="tabler:map-pin" />
{{ t("room") }}
</td>
<td class="text-gray-700">
{{ room }}
</td>
</tr>
<tr class="border-none">
<td class="text-gray-400">
<Icon name="tabler:file-text" />
{{ t("co-write") }}
</td>
<td class="text-gray-700">
<a
v-if="coWrite?.startsWith('http')"
class="underline cursor-pointer"
:href="coWrite"
>{{ coWrite }}</a>
<span
v-else
>{{ coWrite }}</span>
</td>
</tr>
</tbody>
</table>
<div
class="mt-5 pb-3 border-b border-gray-200 flex flex-wrap gap-2"
>
<span
v-for="tag in tags"
:key="tag"
class="text-xs text-primary-700 font-medium px-3 py-1 rounded-full bg-primary-100"
>
{{ tag }}
</span>
</div>
</header>
<section>
<h2 class="text-lg text-primary-400 font-bold my-3">
{{ t("abstract") }}
</h2>
<div
class="text-gray-700 leading-relaxed whitespace-pre-wrap"
>
<MDC :value="description" />
</div>
</section>
<section>
<h2 class="text-lg text-primary-400 font-bold my-2">
{{ t("speaker") }}
</h2>
<div class="flex flex-col gap-6">
<div
v-for="speaker in speakers"
:key="speaker.name"
class="flex flex-col gap-4"
>
<div class="flex gap-4 items-center">
<div
class="border-2 border-primary-100 rounded-full bg-gray-100 flex-shrink-0 h-16 w-16 overflow-hidden"
>
<img
v-if="speaker.avatar"
:alt="speaker.name"
class="h-full w-full object-cover"
:src="speaker.avatar"
>
<div
v-else
class="text-gray-400 flex h-full w-full items-center justify-center"
>
<Icon
class="h-8 w-8"
name="tabler:user"
/>
</div>
</div>
<div>
<h3 class="text-lg text-gray-700 font-bold">
{{ speaker.name }}
</h3>
</div>
</div>
<div
class="text-sm text-gray-700 leading-relaxed whitespace-pre-wrap"
>
<MDC :value="speaker.bio" />
</div>
</div>
</div>
</section>
</article>
</template>

<i18n lang="yaml">
en:
abstract: Abstract
co-write: Co-write
room: Room
speaker: Speaker
time: time
zh:
abstract: 摘要
room: 位置
speaker: 講者
time: 時間
co-write: 共筆
</i18n>
8 changes: 8 additions & 0 deletions app/pages/session/index.vue → app/pages/session.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { prerenderRoutes } from 'nuxt/app'
import { useI18n } from 'vue-i18n'
import CpSessionDaySelector from '~/components/feature/CpSessionDaySelector.vue'
import CpSessionTable from '~/components/feature/CpSessionTable.vue'
Expand All @@ -14,10 +15,17 @@ const selectedDay = computed({
get: () => manualSelectedDay.value ?? days.value[0] ?? null,
set: (value) => void (manualSelectedDay.value = value),
})

prerenderRoutes(
Object.values(data.value ?? {})
.flat()
.map((s) => `/session/${s.id}`),
)
</script>

<template>
<main>
<NuxtPage />
<template v-if="selectedDay">
<CpSessionDaySelector
v-model="selectedDay"
Expand Down
102 changes: 102 additions & 0 deletions app/pages/session/[id].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script setup lang="ts">
import type { SessionDetail } from '#shared/types/session'
import CpSessionInfoCard from '~/components/feature/CpSessionInfoCard.vue'

const { locale } = useI18n()
const route = useRoute()
const router = useRouter()
const localePath = useLocalePath()

const { data } = await useFetch<SessionDetail>(`/api/session/${route.params.id}`)

const localeKey = computed(() => locale.value === 'zh' ? 'zh' : 'en')

const sessionInfo = computed(() => {
if (!data.value) {
return null
}

const content = data.value[localeKey.value]
const room = locale.value === 'zh'
? (data.value.room?.['zh-hans'] || data.value.room?.en || '')
: (data.value.room?.en || data.value.room?.['zh-hans'] || '')

return {
coWrite: data.value.co_write ?? undefined,
description: content.describe,
room,
speakers: data.value.speakers.map((speaker) => ({
id: speaker.id,
avatar: speaker.avatar ?? undefined,
bio: speaker[localeKey.value].bio,
name: speaker[localeKey.value].name,
})),
tags: data.value.tags,
time: `${data.value.start?.slice(11, 16) ?? ''} ~ ${data.value.end?.slice(11, 16) ?? ''}`,
title: content.title,
}
})

function close() {
router.push(localePath('/session'))
}
Comment thread
mirumodapon marked this conversation as resolved.

function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
close()
}
}

onMounted(() => {
document.body.style.overflow = 'hidden'
window.addEventListener('keydown', onKeydown)
})

onUnmounted(() => {
document.body.style.overflow = ''
window.removeEventListener('keydown', onKeydown)
})
</script>

<template>
<div
:aria-label="sessionInfo?.title"
aria-modal="true"
class="bg-black/50 inset-0 fixed z-50"
role="dialog"
@click.self="close"
>
<div class="bg-white flex flex-row-reverse h-full w-120 right-0 top-0 fixed">
<div>
<!-- AD -->
</div>

<div class="p-3 h-full overflow-y-auto">
<div class="flex top-0 justify-end sticky z-10">
<button
aria-label="close"
class="text-gray-500 rounded-full flex h-8 w-8 transition-colors items-center justify-center hover:bg-gray-100"
type="button"
@click="close"
>
<Icon
class="h-4 w-4"
name="tabler:x"
/>
</button>
Comment thread
mirumodapon marked this conversation as resolved.
</div>

<CpSessionInfoCard
v-if="sessionInfo"
:co-write="sessionInfo.coWrite"
:description="sessionInfo.description"
:room="sessionInfo.room"
:speakers="sessionInfo.speakers"
:tags="sessionInfo.tags"
:time="sessionInfo.time"
:title="sessionInfo.title"
/>
</div>
</div>
</div>
</template>
19 changes: 19 additions & 0 deletions app/router.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { RouterConfig } from '@nuxt/schema'

const SESSION_PATH_RE = /^\/(?:[^/]+\/)?session(?:\/|$)/

export default {
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}

const isSessionPath = (path: string) => SESSION_PATH_RE.test(path)

if (isSessionPath(to.path) && isSessionPath(from.path)) {
return false
}

return { top: 0 }
},
} satisfies RouterConfig