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
2 changes: 1 addition & 1 deletion app/components/AppFooter.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup vapor lang="ts">
import { useVfjsI18n } from "../composables/useVfjsI18n";

const { t } = useVfjsI18n();
Expand Down
39 changes: 30 additions & 9 deletions app/components/AppHeader.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { renderToString } from "@vue/server-renderer";
import { vaporInteropPlugin } from "@vue/runtime-vapor";
import { mount } from "@vue/test-utils";
import { createSSRApp, nextTick } from "vue";
import { nextTick } from "vue";
import { afterEach, describe, expect, it } from "vite-plus/test";
import AppHeader from "./AppHeader.vue";

Expand All @@ -13,23 +13,44 @@ describe("AppHeader", () => {
});

it("初回表示の配色セレクターでシステム設定を選択する", async () => {
const html = await renderToString(createSSRApp(AppHeader));
const host = document.createElement("div");
document.body.append(host);
mount(AppHeader, {
attachTo: host,
global: {
plugins: [vaporInteropPlugin],
},
});
await nextTick();

expect(html).toContain('<option value="system" selected>');
expect(html).not.toContain('<option value="light" selected>');
try {
const select = host.querySelector("select");
expect(select?.value).toBe("system");
expect(host.innerHTML).not.toContain('<option value="light" selected>');
} finally {
host.remove();
}
});

it("保存済みの配色設定をマウント後に反映する", async () => {
localStorage.setItem(STORAGE_KEY, "light");

const wrapper = mount(AppHeader);
const host = document.createElement("div");
document.body.append(host);
mount(AppHeader, {
attachTo: host,
global: {
plugins: [vaporInteropPlugin],
},
});
await nextTick();

const select = wrapper.find("select").element as HTMLSelectElement;
const select = host.querySelector("select") as HTMLSelectElement;
expect(select.value).toBe("light");
expect(document.documentElement.getAttribute("data-color-scheme")).toBe("light");

await wrapper.find("select").setValue("system");
wrapper.unmount();
select.value = "system";
select.dispatchEvent(new Event("change"));
host.remove();
});
});
6 changes: 4 additions & 2 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup vapor lang="ts">
import AppLogoMark from "./AppLogoMark.vue";
import { useColorScheme } from "../composables/useColorScheme";
import { useVfjsI18n } from "../composables/useVfjsI18n";
Expand All @@ -20,7 +20,9 @@ const { scheme, setScheme } = useColorScheme();
href="/"
class="flex items-center gap-[10px] font-display font-semibold text-[15px] whitespace-nowrap leading-[1.15] text-ink no-underline hover:text-accent transition-colors"
>
<AppLogoMark class="w-4 h-auto text-ink" />
<span class="contents">
<AppLogoMark class="w-4 h-auto text-ink" />
</span>
<span>Vue Fes Japan Speakers</span>
</a>

Expand Down
2 changes: 2 additions & 0 deletions app/components/AppLogoMark.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<script setup vapor lang="ts"></script>

<template>
<svg
aria-hidden="true"
Expand Down
20 changes: 11 additions & 9 deletions app/components/AppMasthead.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
<script setup vapor lang="ts">
import AppLogoMark from "./AppLogoMark.vue";
import { useVfjsI18n } from "../composables/useVfjsI18n";

defineProps<{
const { stats } = defineProps<{
stats: { speakers: number; talks: number; years: number };
}>();

Expand All @@ -18,7 +18,9 @@ const { t } = useVfjsI18n();
<div
class="basis-0 grow-999 min-inline-[60%] flex flex-wrap items-center gap-[clamp(18px,2.4vw,34px)]"
>
<AppLogoMark class="basis-15 grow-1 max-w-[60px] h-auto text-ink" />
<span class="contents">
<AppLogoMark class="basis-15 grow-1 max-w-[60px] h-auto text-ink" />
</span>
<!-- ページタイトル -->
<h1
class="basis-0 grow-999 min-inline-[80%] m-0 font-display font-[500] text-[clamp(38px,6.2vw,104px)] leading-[0.96] text-ink break-keep"
Expand All @@ -33,23 +35,23 @@ const { t } = useVfjsI18n();
<!-- 総スピーカー数 -->
<div>
<b class="text-ink font-[500] not-italic">
{{ stats.speakers }}
{{ $props.stats.speakers }}
</b>
{{ t.meta_speakers }}
<span>{{ t.meta_speakers }}</span>
</div>
<!-- 総トーク数 -->
<div>
<b class="text-ink font-[500] not-italic">
{{ stats.talks }}
{{ $props.stats.talks }}
</b>
{{ t.meta_talks }}
<span>{{ t.meta_talks }}</span>
</div>
<!-- 開催年数 -->
<div>
<b class="text-ink font-[500] not-italic">
{{ stats.years }}
{{ $props.stats.years }}
</b>
{{ t.meta_years }}
<span>{{ t.meta_years }}</span>
</div>
</aside>
</div>
Expand Down
82 changes: 50 additions & 32 deletions app/components/ChronicleView.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup vapor lang="ts">
import { computed } from "vue";
import type { SpeakerWithYear, AcceptedYear } from "../../types";
import { compareLexicalJa } from "../utils/stringCollate";
Expand All @@ -7,7 +7,7 @@ import SpeakerFilterBar from "./SpeakerFilterBar.vue";
import YearFilterBar from "./YearFilterBar.vue";
import { useVfjsI18n } from "../composables/useVfjsI18n";

const props = defineProps<{
const { allSpeakers, query, selectedSpeaker, selectedYear } = defineProps<{
allSpeakers: SpeakerWithYear[];
selectedYear: AcceptedYear | "all";
selectedSpeaker: string;
Expand All @@ -20,11 +20,23 @@ const emit = defineEmits<{
"update:query": [string];
}>();

function updateSelectedYear(value: AcceptedYear | "all") {
emit("update:selectedYear", value);
}

function updateSelectedSpeaker(value: string) {
emit("update:selectedSpeaker", value);
}

function updateQuery(value: string) {
emit("update:query", value);
}

const { t, lang } = useVfjsI18n();

const uniqueNames = computed(() => {
const set = new Set<string>();
props.allSpeakers.forEach((s) => s.name.forEach((n) => set.add(n)));
allSpeakers.forEach((s) => s.name.forEach((n) => set.add(n)));
return Array.from(set).sort(compareLexicalJa);
});

Expand All @@ -33,18 +45,18 @@ const speakerOptions = computed(() =>
);

const counts = computed(() => {
const c: Record<string, number> = { all: props.allSpeakers.length };
const c: Record<string, number> = { all: allSpeakers.length };
for (const y of YEARS) {
c[y] = props.allSpeakers.filter((s) => s.year === y).length;
c[y] = allSpeakers.filter((s) => s.year === y).length;
}
return c;
});

const filtered = computed(() => {
const q = props.query.trim().toLowerCase();
return props.allSpeakers.filter((s) => {
if (props.selectedYear !== "all" && s.year !== props.selectedYear) return false;
if (props.selectedSpeaker !== "all" && !s.name.includes(props.selectedSpeaker)) return false;
const q = query.trim().toLowerCase();
return allSpeakers.filter((s) => {
if (selectedYear !== "all" && s.year !== selectedYear) return false;
if (selectedSpeaker !== "all" && !s.name.includes(selectedSpeaker)) return false;
if (q) {
const hay = (s.title || "") + " " + s.name.join(" ");
if (!hay.toLowerCase().includes(q)) return false;
Expand All @@ -57,28 +69,34 @@ const grouped = computed(() => {
const map = new Map<string, SpeakerWithYear[]>();
for (const y of YEARS) map.set(y, []);
for (const s of filtered.value) map.get(s.year)?.push(s);
return Array.from(map.entries()).filter(([, arr]) => arr.length > 0);
return Array.from(map.entries())
.filter(([, talks]) => talks.length > 0)
.map(([year, talks]) => ({ year, talks }));
});
</script>

<template>
<main>
<section>
<!-- スピーカー名・キーワードによるフィルターバー -->
<SpeakerFilterBar
:query="query"
:selected-speaker="selectedSpeaker"
:speaker-options="speakerOptions"
@update:query="emit('update:query', $event)"
@update:selected-speaker="emit('update:selectedSpeaker', $event)"
/>
<div class="contents">
<SpeakerFilterBar
:query="$props.query"
:selected-speaker="$props.selectedSpeaker"
:speaker-options="speakerOptions"
@update:query="updateQuery"
@update:selected-speaker="updateSelectedSpeaker"
/>
</div>

<!-- 開催年度によるフィルターバー -->
<YearFilterBar
:selected-year="selectedYear"
:counts="counts"
@update:selected-year="emit('update:selectedYear', $event)"
/>
<div class="contents">
<YearFilterBar
:selected-year="$props.selectedYear"
:counts="counts"
@update:selected-year="updateSelectedYear"
/>
</div>

<!-- フィルター結果が0件のとき -->
<div
Expand All @@ -92,31 +110,31 @@ const grouped = computed(() => {
<ol class="list-none p-0 m-0" :aria-label="t.nav_all_label">
<!-- 各年度グループ -->
<li
v-for="[year, arr] in grouped"
:key="year"
v-for="group in grouped"
:key="group.year"
class="grid grid-cols-[minmax(200px,260px)_1fr] gap-[clamp(24px,4vw,64px)] border-b border-rule items-start px-pad-x py-[clamp(32px,5vw,72px)] max-[800px]:grid-cols-1 max-[800px]:gap-8"
>
<!-- 年度ラベル(スクロール時に上部に固定) -->
<div
class="flex flex-col items-start sticky top-[72px] max-[800px]:static"
:id="`year-${year}`"
:id="`year-${group.year}`"
>
<!-- 年度テキスト(表示専用) -->
<span
class="font-display font-[500] text-[clamp(72px,8vw,120px)] leading-[0.85] tabular-nums text-ink"
aria-hidden="true"
>
{{ year }}
{{ group.year }}
</span>
<!-- その年のトーク総数 -->
<span class="font-mono text-[12px] tracking-[0.08em] uppercase text-ink-3 mt-2.5">
{{ t.year_total_talks(arr.length) }}
{{ t.year_total_talks(group.talks.length) }}
</span>
<!-- 年度別ページへの矢印リンク -->
<a
class="font-mono text-[24px] tracking-[0.08em] uppercase text-ink hover:text-accent transition-colors border-b border-current pb-0.5 no-underline mt-3.5"
:href="`/${year}`"
:aria-label="`${year} speakers`"
:href="`/${group.year}`"
:aria-label="`${group.year} speakers`"
>
</a>
Expand All @@ -126,8 +144,8 @@ const grouped = computed(() => {
<ol class="list-none p-0 m-0 border-t border-rule-soft">
<!-- 各スピーカー行 -->
<li
v-for="(s, i) in arr"
:key="`${year}-${i}`"
v-for="(s, i) in group.talks"
:key="`${group.year}-${i}`"
class="grid grid-cols-[56px_1fr] gap-4 py-[18px] border-b border-rule-soft items-start"
>
<!-- 行番号(表示専用) -->
Expand All @@ -146,7 +164,7 @@ const grouped = computed(() => {
:href="`/speakers/${encodeURIComponent(n)}`"
>
<ruby v-if="s.nameRuby?.[ni] && lang === 'ja'" lang="ja">
{{ n }}
<span>{{ n }}</span>
<rt>{{ s.nameRuby[ni] }}</rt>
</ruby>
<span v-else>{{ lang === "en" && s.nameEn?.[ni] ? s.nameEn[ni] : n }}</span>
Expand Down
Loading