Skip to content

Commit 6a24666

Browse files
authored
Merge pull request #96 from GoCon/feature/workshop-page
✨ ワークショップ詳細ページ作成
2 parents 82696a5 + c7bf86a commit 6a24666

File tree

7 files changed

+431
-6
lines changed

7 files changed

+431
-6
lines changed

src/assets/icon/back.svg

Lines changed: 3 additions & 0 deletions
Loading

src/assets/icon/room.svg

Lines changed: 3 additions & 0 deletions
Loading

src/components/SpeakerInfo.astro

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
---
2+
import speakers from "../data/speakers.json";
3+
import { Image } from "astro:assets";
4+
import XLogo from "../assets/x-logo.svg";
5+
6+
interface Props {
7+
/**
8+
* 登壇者ID
9+
*/
10+
speakerId: string;
11+
}
12+
13+
// 指定されたIDを元に登壇者情報を取得
14+
const { speakerId } = Astro.props;
15+
16+
const speaker = speakers.find((s) => s.id === speakerId);
17+
18+
if (!speaker) {
19+
return null;
20+
}
21+
---
22+
23+
<style>
24+
.speaker {
25+
padding: 40px;
26+
border: 2px solid var(--surface-tertiary);
27+
border-radius: 16px;
28+
background-color: var(--surface-primary);
29+
30+
@media screen and (max-width: 860px) {
31+
padding: 24px;
32+
}
33+
}
34+
35+
.speaker-header {
36+
display: flex;
37+
flex-direction: column;
38+
gap: 8px;
39+
margin-bottom: 8px;
40+
}
41+
42+
.speaker-info {
43+
display: flex;
44+
align-items: center;
45+
gap: 8px;
46+
}
47+
48+
.speaker-thumbnail {
49+
width: 44px;
50+
height: 44px;
51+
object-fit: cover;
52+
border-radius: 50%;
53+
}
54+
55+
.speaker-name {
56+
margin: 0;
57+
font-size: 14px;
58+
font-weight: 700;
59+
}
60+
61+
.speaker-socials {
62+
display: flex;
63+
align-items: center;
64+
gap: 8px;
65+
padding: 0 16px;
66+
67+
& a {
68+
transition: ease opacity 0.3s;
69+
70+
&:hover {
71+
opacity: 0.5;
72+
}
73+
}
74+
75+
& svg {
76+
height: 24px;
77+
width: auto;
78+
vertical-align: middle;
79+
}
80+
}
81+
82+
.speaker-tagline {
83+
margin: 0;
84+
font-size: 12px;
85+
font-weight: 700;
86+
color: var(--text-secondary);
87+
}
88+
89+
.speaker-bio {
90+
margin: 0;
91+
font-size: 14px;
92+
line-height: 1.75;
93+
white-space: pre-wrap;
94+
}
95+
</style>
96+
97+
<article class="speaker">
98+
<header class="speaker-header">
99+
<div class="speaker-info">
100+
<Image
101+
src={speaker.profilePicture}
102+
alt={speaker.fullName}
103+
width={24}
104+
height={24}
105+
loading="lazy"
106+
class="speaker-thumbnail"
107+
/>
108+
<p class="speaker-name">{speaker.fullName}</p>
109+
<div class="speaker-socials">
110+
{
111+
speaker.links.map((link) => {
112+
if (link.linkType === "Twitter") {
113+
return (
114+
<a
115+
href={link.url}
116+
target="_blank"
117+
rel="noopener noreferrer"
118+
aria-label={`${speaker.fullName}さんのX (Twitter)を新しいタブで開く`}
119+
>
120+
<XLogo />
121+
</a>
122+
);
123+
}
124+
125+
// 対応していないlinkTypeの場合は表示しない
126+
return null;
127+
})
128+
}
129+
</div>
130+
</div>
131+
<p class="speaker-tagline">{speaker.tagLine}</p>
132+
</header>
133+
<p class="speaker-bio">{speaker.bio}</p>
134+
</article>

src/components/Timetable/WorkshopCard.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ const currentLocale = Astro.currentLocale || "ja";
120120
<a
121121
href={getRelativeLocaleUrl(
122122
currentLocale,
123-
`/workshop/${room.session.id}`,
123+
`/workshops/${room.session.id}`,
124124
)}
125125
class="card-title-link"
126126
>

src/layouts/Layout.astro

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,31 @@ interface Props {
1616
title?: string;
1717
/** ページタイトル(英語部分) */
1818
titleEn?: string;
19+
/** metaタグのtitleやog:titleなどを直接指定するためのプロパティ */
20+
metaTitle?: string;
1921
}
2022
21-
const { title, titleEn } = Astro.props;
23+
const { title, titleEn, metaTitle } = Astro.props;
2224
2325
const currentLocale = Astro.currentLocale || "ja";
2426
2527
// 現在のロケールに応じてタイトルを設定
2628
const currentLocaleTitle = currentLocale === "ja" ? title : titleEn;
2729
2830
// ページタイトルの指定があれば固定のページ名と合わせて表示する
29-
const pageTitle =
30-
!!currentLocaleTitle && currentLocaleTitle.length > 0
31-
? `${currentLocaleTitle} | ${constants.pageTitle}`
32-
: constants.pageTitle;
31+
const pageTitle = (() => {
32+
// metaTitleが指定されている場合は優先的に使用
33+
if (metaTitle && metaTitle.length > 0) {
34+
return `${metaTitle} | ${constants.pageTitle}`;
35+
}
36+
37+
// 現在のロケールに応じたタイトルがあれば使用
38+
if (!!currentLocaleTitle && currentLocaleTitle.length > 0) {
39+
return `${currentLocaleTitle} | ${constants.pageTitle}`;
40+
}
41+
42+
return constants.pageTitle;
43+
})();
3344
---
3445

3546
<style>

src/pages/en/workshops/[id].astro

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
import type { GetStaticPaths } from "astro";
3+
import sessions from "../../../data/sessions.json";
4+
import Layout from "../../../layouts/Layout.astro";
5+
import "../../../styles/sub-page.css";
6+
import { getRelativeLocaleUrl } from "astro:i18n";
7+
import Back from "../../../assets/icon/back.svg";
8+
import Room from "../../../assets/icon/room.svg";
9+
import { getFormattedTime } from "../../../utils/timetable";
10+
import SpeakerInfo from "../../../components/SpeakerInfo.astro";
11+
12+
export const getStaticPaths = (() => {
13+
// MEMO: SSGされるページ数を減らすため、ワークショップのセッションのみを抽出
14+
const workshopSession =
15+
sessions.at(0)?.sessions.filter((session) =>
16+
session.categories.some((category) =>
17+
// Session Typeが"Workshop" (389623) のものを抽出
18+
category.categoryItems.some((item) => item.id === 389623),
19+
),
20+
) ?? [];
21+
22+
return workshopSession.map((session) => ({
23+
params: { id: session.id },
24+
}));
25+
}) satisfies GetStaticPaths;
26+
27+
const { id } = Astro.params;
28+
29+
const currentLocale = Astro.currentLocale || "ja";
30+
31+
// 現在のIDに対応するセッションを取得
32+
const session = sessions.at(0)?.sessions.find((s) => s.id === id);
33+
34+
// 現在のIDに対応するセッションが存在しない場合は表示しない
35+
if (!session) {
36+
return null;
37+
}
38+
---
39+
40+
<style>
41+
.session-header {
42+
display: flex;
43+
flex-direction: column;
44+
gap: 24px;
45+
margin-bottom: 40px;
46+
}
47+
48+
.back-link {
49+
margin: 0;
50+
51+
& a {
52+
display: inline-flex;
53+
align-items: center;
54+
gap: 4px;
55+
font-weight: 700;
56+
text-decoration: none;
57+
color: var(--text-primary);
58+
transition: ease opacity 0.3s;
59+
60+
&:hover {
61+
opacity: 0.7;
62+
}
63+
}
64+
}
65+
66+
.session-header .section-title {
67+
margin: 0;
68+
}
69+
70+
.session-room {
71+
display: flex;
72+
align-items: center;
73+
gap: 8px;
74+
width: fit-content;
75+
padding: 4px 24px;
76+
background-color: var(--primitive-light-cyan);
77+
border-radius: 16px;
78+
font-weight: 700;
79+
line-height: 1.75;
80+
81+
@media screen and (max-width: 860px) {
82+
font-size: 14px;
83+
}
84+
}
85+
86+
.description {
87+
white-space: pre-wrap;
88+
line-height: 1.75;
89+
}
90+
91+
.session-footer {
92+
margin-top: 40px;
93+
}
94+
95+
.speakers-list {
96+
display: flex;
97+
flex-direction: column;
98+
gap: 16px;
99+
}
100+
</style>
101+
102+
<Layout title="ワークショップ" titleEn="Workshop" metaTitle={session.title}>
103+
<article>
104+
<header class="session-header">
105+
<p class="back-link">
106+
<a href={getRelativeLocaleUrl(currentLocale, `/timetable`)}>
107+
<Back />Back to Timetable
108+
</a>
109+
</p>
110+
<h2 class="section-title">{session.title}</h2>
111+
<p class="session-room">
112+
<Room />
113+
<span>
114+
{session.room}
115+
</span>
116+
<span>
117+
{getFormattedTime(session.startsAt)} -{" "}
118+
{getFormattedTime(session.endsAt)}
119+
</span>
120+
</p>
121+
</header>
122+
<div class="contents">
123+
<div class="description">
124+
{session.description}
125+
</div>
126+
</div>
127+
<footer class="session-footer">
128+
<div class="speakers-list">
129+
{
130+
session.speakers.map((speaker) => (
131+
<SpeakerInfo speakerId={speaker.id} />
132+
))
133+
}
134+
</div>
135+
</footer>
136+
</article>
137+
</Layout>

0 commit comments

Comments
 (0)