Merge branch 'main' of https://github.com/jekkinoopy/sechskies
# Conflicts: # .vscode/settings.json # AGENTS.md
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
/* ==========================================
|
||||
水晶男孩 2026 萬年曆專用樣式 (calendar.css)
|
||||
對齊全站暖紙面 section 視覺
|
||||
========================================== */
|
||||
|
||||
:root {
|
||||
--cal-grid-gap: 8px;
|
||||
--cal-border-soft: rgba(255, 204, 0, 0.18);
|
||||
--cal-border-strong: rgba(255, 204, 0, 0.28);
|
||||
--cal-panel-shadow: 0 14px 34px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.calendar-page-container {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px 60px;
|
||||
}
|
||||
|
||||
/* 控制面板 */
|
||||
.calendar-controls {
|
||||
background: var(--color-surface-soft);
|
||||
border: 1px solid var(--cal-border-soft);
|
||||
border-radius: 18px;
|
||||
padding: 22px;
|
||||
margin-bottom: 22px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
box-shadow: var(--cal-panel-shadow);
|
||||
}
|
||||
|
||||
.calendar-select-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.calendar-select {
|
||||
background: #fffef9;
|
||||
color: var(--color-text-dark);
|
||||
border: 1px solid rgba(255, 204, 0, 0.38);
|
||||
border-radius: 999px;
|
||||
padding: 9px 14px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.calendar-select:hover,
|
||||
.calendar-select:focus {
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: 0 0 0 3px rgba(255, 204, 0, 0.2);
|
||||
}
|
||||
|
||||
.calendar-btn {
|
||||
background: var(--color-brand);
|
||||
color: #1a1a1a;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
padding: 8px 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.calendar-btn:hover {
|
||||
background: #ffe066;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 分類標籤按鈕列 */
|
||||
.calendar-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
background: #fffdf6;
|
||||
color: var(--color-text-dark);
|
||||
border: 1px solid rgba(255, 204, 0, 0.38);
|
||||
border-radius: 999px;
|
||||
padding: 7px 15px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-chip.active,
|
||||
.filter-chip:hover {
|
||||
background: var(--color-brand);
|
||||
color: #1a1a1a;
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
|
||||
/* 萬年曆主要區塊:維持原雙欄版型 */
|
||||
.calendar-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
gap: 22px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.calendar-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 日曆卡片區 */
|
||||
.calendar-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--cal-border-soft);
|
||||
border-radius: 18px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--cal-panel-shadow);
|
||||
}
|
||||
|
||||
.calendar-header-title {
|
||||
text-align: center;
|
||||
font-size: 1.52rem;
|
||||
color: var(--color-text-dark);
|
||||
margin-bottom: 18px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* 星期標頭 */
|
||||
.calendar-weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: var(--color-brand-dark);
|
||||
padding-bottom: 11px;
|
||||
border-bottom: 1px solid rgba(255, 204, 0, 0.22);
|
||||
margin-bottom: 11px;
|
||||
}
|
||||
|
||||
.calendar-weekday {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.calendar-weekday.weekend {
|
||||
color: #9b5e47;
|
||||
}
|
||||
|
||||
/* 日期網格 */
|
||||
.calendar-days-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: var(--cal-grid-gap);
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
aspect-ratio: 1;
|
||||
min-height: 68px;
|
||||
background: #fffefb;
|
||||
border: 1px solid rgba(255, 204, 0, 0.12);
|
||||
border-radius: 10px;
|
||||
padding: 7px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.day-cell:hover {
|
||||
background: var(--color-brand-light);
|
||||
border-color: var(--cal-border-strong);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.day-cell.other-month {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.day-cell.other-month:hover {
|
||||
transform: none;
|
||||
border-color: rgba(255, 204, 0, 0.12);
|
||||
background: #fffefb;
|
||||
}
|
||||
|
||||
.day-cell.today {
|
||||
border: 2px solid var(--color-brand);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 204, 0, 0.16);
|
||||
}
|
||||
|
||||
.day-cell.selected {
|
||||
background: #fff4c7;
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
|
||||
.day-num {
|
||||
font-size: 0.94rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
.day-cell.weekend .day-num {
|
||||
color: #8a4e40;
|
||||
}
|
||||
|
||||
/* 事件標記改為低調金色點 */
|
||||
.day-events {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.day-birthday-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-top: 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-brand);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.event-dot.birthday,
|
||||
.event-dot.debut,
|
||||
.event-dot.concert {
|
||||
background: var(--color-brand);
|
||||
}
|
||||
|
||||
/* 生日頭像縮圖小標籤 */
|
||||
.day-avatar-badge {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-brand);
|
||||
object-fit: cover;
|
||||
margin: 0;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.day-member-name {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-dark);
|
||||
line-height: 1;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* 演唱會微縮標籤 */
|
||||
.day-concert-badge {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-brand);
|
||||
object-fit: cover;
|
||||
margin: 2px auto 0;
|
||||
}
|
||||
|
||||
/* 當月紀念日清單 */
|
||||
.events-sidebar {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--cal-border-soft);
|
||||
border-radius: 18px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--cal-panel-shadow);
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-text-dark);
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 204, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-title i {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.events-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.events-empty {
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
padding: 28px 0;
|
||||
}
|
||||
|
||||
/* 個別事件卡片 */
|
||||
.event-card {
|
||||
background: var(--color-surface-soft);
|
||||
border: 1px solid rgba(255, 204, 0, 0.16);
|
||||
border-left: 4px solid var(--color-brand);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.event-card:hover {
|
||||
transform: translateX(2px);
|
||||
border-color: rgba(255, 204, 0, 0.32);
|
||||
}
|
||||
|
||||
.event-card.type-birthday,
|
||||
.event-card.type-debut,
|
||||
.event-card.type-concert {
|
||||
border-left-color: var(--color-brand);
|
||||
}
|
||||
|
||||
.event-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.event-date-badge {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-brand-dark);
|
||||
background: rgba(255, 204, 0, 0.2);
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 1.02rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-dark);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
.event-media-img {
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid rgba(255, 204, 0, 0.24);
|
||||
}
|
||||
|
||||
/* 生日卡片呈現 */
|
||||
.birthday-featured-card {
|
||||
background: var(--color-surface-soft);
|
||||
border: 1px solid rgba(255, 204, 0, 0.22);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.birthday-photo-wrap {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
aspect-ratio: 210 / 297;
|
||||
border-radius: 12px;
|
||||
border: 2px solid var(--color-brand);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: #111;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.birthday-photo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center 28%;
|
||||
}
|
||||
|
||||
.birthday-info {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.birthday-info h4 {
|
||||
font-size: 1.3rem;
|
||||
color: var(--color-text-dark);
|
||||
margin: 0 0 2px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.birthday-info h4 a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.birthday-info .en-name {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-brand-dark);
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.birthday-info .bday-date {
|
||||
font-size: 0.92rem;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 演唱會卡片呈現 */
|
||||
.concert-featured-card {
|
||||
background: var(--color-surface-soft);
|
||||
border: 1px solid rgba(255, 204, 0, 0.22);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.concert-featured-card img {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
aspect-ratio: 210 / 297;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
object-position: center 28%;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid rgba(255, 204, 0, 0.24);
|
||||
}
|
||||
|
||||
/* Modal 彈窗 */
|
||||
.calendar-modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.calendar-modal-backdrop.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.calendar-modal-content {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--cal-border-strong);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #7d7464;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
.modal-photo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--color-brand);
|
||||
object-fit: cover;
|
||||
margin: 0 auto 16px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-date-title {
|
||||
color: var(--color-brand-dark);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.modal-member-name {
|
||||
color: var(--color-text-dark);
|
||||
font-size: 1.32rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.modal-member-name a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.modal-member-meta {
|
||||
color: var(--color-brand-dark);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal-desc {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-member-link {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.modal-event-box {
|
||||
background: rgba(255, 204, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.modal-event-title {
|
||||
color: var(--color-brand-dark);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.modal-concert-photo {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
aspect-ratio: 210 / 297;
|
||||
max-height: none;
|
||||
object-fit: cover;
|
||||
object-position: center 28%;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 204, 0, 0.28);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.calendar-page-container {
|
||||
padding: 24px 14px 50px;
|
||||
}
|
||||
|
||||
.calendar-controls,
|
||||
.calendar-card,
|
||||
.events-sidebar {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.calendar-header-title {
|
||||
font-size: 1.33rem;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.day-avatar-badge {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.day-member-name {
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
.birthday-photo-wrap {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -2256,6 +2256,45 @@ header.inner-page-header .sub-title {
|
||||
padding: 4px 20px;
|
||||
}
|
||||
|
||||
/* 地區切換(台灣/海外):比成員 tab 高一階,用深底色跟它區隔 */
|
||||
.region-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 18px auto 0;
|
||||
max-width: 1100px;
|
||||
padding: 4px 20px;
|
||||
}
|
||||
|
||||
.region-tab-btn {
|
||||
border: 1px solid var(--color-bg-raised);
|
||||
background: var(--color-bg-raised);
|
||||
color: #f4f4f4;
|
||||
border-radius: 999px;
|
||||
padding: 8px 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.region-tab-btn:hover,
|
||||
.region-tab-btn.active {
|
||||
background: var(--color-brand);
|
||||
color: #2f2f2f;
|
||||
border-color: var(--color-brand);
|
||||
}
|
||||
|
||||
.region-empty-state {
|
||||
text-align: center;
|
||||
color: #8b7423;
|
||||
font-size: 0.95rem;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
#itinerary-search {
|
||||
display: block;
|
||||
max-width: 1100px;
|
||||
@@ -2328,6 +2367,16 @@ header.inner-page-header .sub-title {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.map-embed-empty {
|
||||
text-align: center;
|
||||
color: #8b7423;
|
||||
font-size: 0.88rem;
|
||||
padding: 24px 16px;
|
||||
background: #fffef8;
|
||||
border: 1px dashed #e4dbb8;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.map-embed-label {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.88rem;
|
||||
@@ -2450,6 +2499,7 @@ header.inner-page-header .sub-title {
|
||||
.photo-frame {
|
||||
width: 118px;
|
||||
flex: 0 0 118px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.photo-frame img {
|
||||
@@ -2459,6 +2509,21 @@ header.inner-page-header .sub-title {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 圖片授權標示:僅供合規(CC BY / CC BY-SA 需姓名標示),刻意做得很小、低對比,不搶視覺 */
|
||||
.photo-credit {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 3px;
|
||||
font-size: 0.5rem;
|
||||
line-height: 1;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
|
||||
letter-spacing: 0.01em;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-content .date {
|
||||
display: inline-block;
|
||||
font-size: 0.72rem;
|
||||
@@ -2495,6 +2560,10 @@ header.inner-page-header .sub-title {
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.detail-photo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-photo img {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
@@ -2502,6 +2571,12 @@ header.inner-page-header .sub-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-photo .photo-credit {
|
||||
font-size: 0.62rem;
|
||||
right: 8px;
|
||||
bottom: 6px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
+404
-304
@@ -1,304 +1,404 @@
|
||||
(() => {
|
||||
const canvas = document.getElementById("particles");
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
let particles = [];
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.size = Math.random() * 4 + 1;
|
||||
this.speedX = Math.random() * 0.5 - 0.25;
|
||||
this.speedY = Math.random() * 0.5 + 0.2;
|
||||
}
|
||||
update() {
|
||||
this.x += this.speedX;
|
||||
this.y += this.speedY;
|
||||
if (this.y > canvas.height) this.y = 0;
|
||||
}
|
||||
draw() {
|
||||
ctx.fillStyle = "#FFDD00";
|
||||
ctx.globalAlpha = 0.6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function initParticles() {
|
||||
particles = [];
|
||||
for (let i = 0; i < 80; i += 1) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
}
|
||||
|
||||
function animateParticles() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for (const particle of particles) {
|
||||
particle.update();
|
||||
particle.draw();
|
||||
}
|
||||
requestAnimationFrame(animateParticles);
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
initParticles();
|
||||
animateParticles();
|
||||
window.addEventListener("resize", () => {
|
||||
resizeCanvas();
|
||||
initParticles();
|
||||
});
|
||||
}
|
||||
|
||||
const tabs = Array.from(document.querySelectorAll(".tab-btn"));
|
||||
const cards = Array.from(document.querySelectorAll(".polaroid-card"));
|
||||
const searchInput = document.getElementById("itinerary-search");
|
||||
const itineraryList = document.querySelector(".itinerary-list");
|
||||
const detailPanel = document.getElementById("detail-panel");
|
||||
const detailDate = document.getElementById("detail-date");
|
||||
const detailTitle = document.getElementById("detail-title");
|
||||
const detailDesc = document.getElementById("detail-desc");
|
||||
const detailQuote = document.getElementById("detail-quote");
|
||||
const detailTag = document.getElementById("detail-tag");
|
||||
const detailLink = document.getElementById("detail-link");
|
||||
const detailImage = document.getElementById("detail-image");
|
||||
const memberMapFrame = document.getElementById("member-map");
|
||||
const mapEmbedSingle = document.getElementById("map-embed-single");
|
||||
const mapEmbedAll = document.getElementById("map-embed-all");
|
||||
const openMapLink = document.getElementById("open-map-link");
|
||||
const openMapLinksDual = document.getElementById("open-map-links-dual");
|
||||
const openMapLinkKo = document.getElementById("open-map-link-ko");
|
||||
const openMapLinkEun = document.getElementById("open-map-link-eun");
|
||||
const dayGroups = new Map();
|
||||
|
||||
let currentMember = "eun";
|
||||
let currentKeyword = "";
|
||||
const memberMaps = {
|
||||
eun: {
|
||||
embed: "https://www.google.com/maps/d/embed?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI&ehbc=2E312F",
|
||||
open: "https://www.google.com/maps/d/u/0/viewer?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI"
|
||||
},
|
||||
ko: {
|
||||
embed: "https://www.google.com/maps/d/embed?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw&hl=zh-TW&ehbc=2E312F",
|
||||
open: "https://www.google.com/maps/d/u/0/viewer?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw"
|
||||
}
|
||||
};
|
||||
const memberTripPrefix = {
|
||||
ko: "高高父子遊台灣",
|
||||
eun: "殷空少搭飛機去台灣高雄"
|
||||
};
|
||||
const memberSortOrder = { eun: 0, ko: 1 };
|
||||
|
||||
function normalizeMember(member) {
|
||||
if (member === "kogo") return "ko";
|
||||
return member;
|
||||
}
|
||||
|
||||
function getMemberLabel(member) {
|
||||
const normalized = normalizeMember(member);
|
||||
if (normalized === "eun") return "殷志源";
|
||||
if (normalized === "ko") return "高志溶";
|
||||
return "未分類";
|
||||
}
|
||||
|
||||
function getDayToken(rawDate) {
|
||||
const match = String(rawDate || "").match(/Day\s*(\d+)/i);
|
||||
if (!match) return String(rawDate || "未分類");
|
||||
return "Day " + match[1];
|
||||
}
|
||||
|
||||
function getGroupLabel(card) {
|
||||
const date = getDayToken(card.dataset.date || "未分類");
|
||||
const normalized = normalizeMember(card.dataset.member || "");
|
||||
const prefix = memberTripPrefix[normalized] || getMemberLabel(normalized);
|
||||
return prefix + "_" + date;
|
||||
}
|
||||
|
||||
function setDualMapVisible(isAll) {
|
||||
if (mapEmbedSingle && mapEmbedAll) {
|
||||
mapEmbedSingle.hidden = isAll ? true : false;
|
||||
mapEmbedAll.hidden = isAll ? false : true;
|
||||
mapEmbedAll.setAttribute("aria-hidden", isAll ? "false" : "true");
|
||||
}
|
||||
if (openMapLink && openMapLinksDual) {
|
||||
openMapLink.hidden = isAll ? true : false;
|
||||
openMapLinksDual.hidden = isAll ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
function updateMapByMember(member) {
|
||||
const normalized = normalizeMember(member);
|
||||
if (normalized === "all") {
|
||||
setDualMapVisible(true);
|
||||
if (openMapLinkKo) openMapLinkKo.href = memberMaps.ko.open;
|
||||
if (openMapLinkEun) openMapLinkEun.href = memberMaps.eun.open;
|
||||
return;
|
||||
}
|
||||
setDualMapVisible(false);
|
||||
const mapConfig = memberMaps[normalized];
|
||||
if (!mapConfig || !memberMapFrame) return;
|
||||
memberMapFrame.src = mapConfig.embed;
|
||||
if (openMapLink) openMapLink.href = mapConfig.open;
|
||||
}
|
||||
|
||||
function getCardSearchText(card) {
|
||||
return [
|
||||
card.dataset.date || "",
|
||||
card.dataset.title || "",
|
||||
card.dataset.desc || "",
|
||||
card.dataset.tag || "",
|
||||
].join(" ").toLowerCase();
|
||||
}
|
||||
|
||||
function createDayGroup(dayLabel) {
|
||||
const group = document.createElement("section");
|
||||
group.className = "day-group";
|
||||
group.dataset.day = dayLabel;
|
||||
|
||||
const toggle = document.createElement("button");
|
||||
toggle.type = "button";
|
||||
toggle.className = "day-toggle";
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
toggle.innerHTML = "<span>" + dayLabel + "</span><i class='fa-solid fa-chevron-down'></i>";
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "day-cards";
|
||||
body.style.display = "none";
|
||||
|
||||
group.appendChild(toggle);
|
||||
group.appendChild(body);
|
||||
itineraryList.appendChild(group);
|
||||
dayGroups.set(dayLabel, { group, toggle, body });
|
||||
return dayGroups.get(dayLabel);
|
||||
}
|
||||
|
||||
function setDayExpanded(dayGroup, shouldExpand) {
|
||||
dayGroup.toggle.setAttribute("aria-expanded", shouldExpand ? "true" : "false");
|
||||
dayGroup.body.style.display = shouldExpand ? "grid" : "none";
|
||||
dayGroup.group.classList.toggle("expanded", shouldExpand);
|
||||
}
|
||||
|
||||
function initializeDayGroups() {
|
||||
const sortedCards = [...cards].sort((a, b) => {
|
||||
const memberA = normalizeMember(a.dataset.member || "");
|
||||
const memberB = normalizeMember(b.dataset.member || "");
|
||||
const memberDiff = (memberSortOrder[memberA] ?? 99) - (memberSortOrder[memberB] ?? 99);
|
||||
if (memberDiff !== 0) return memberDiff;
|
||||
|
||||
const dayA = Number((getDayToken(a.dataset.date).match(/\d+/) || [999])[0]);
|
||||
const dayB = Number((getDayToken(b.dataset.date).match(/\d+/) || [999])[0]);
|
||||
return dayA - dayB;
|
||||
});
|
||||
|
||||
sortedCards.forEach((card) => {
|
||||
const dayLabel = getGroupLabel(card);
|
||||
const dayGroup = dayGroups.get(dayLabel) || createDayGroup(dayLabel);
|
||||
dayGroup.body.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function updateDayGroupVisibility() {
|
||||
dayGroups.forEach(({ group, body, toggle }) => {
|
||||
const visibleCards = Array.from(body.querySelectorAll(".polaroid-card")).filter((card) => card.style.display !== "none");
|
||||
group.style.display = visibleCards.length ? "" : "none";
|
||||
if (!visibleCards.length) {
|
||||
setDayExpanded({ group, toggle, body }, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensureExpandedForCard(card) {
|
||||
const dayGroup = dayGroups.get(getGroupLabel(card));
|
||||
if (!dayGroup) return;
|
||||
setDayExpanded(dayGroup, true);
|
||||
}
|
||||
|
||||
function setActiveCard(card, options = {}) {
|
||||
const { expandDay = true } = options;
|
||||
cards.forEach((item) => item.classList.remove("active"));
|
||||
card.classList.add("active");
|
||||
if (expandDay) {
|
||||
ensureExpandedForCard(card);
|
||||
}
|
||||
const mapKey = currentMember === "all" ? "all" : currentMember;
|
||||
updateMapByMember(mapKey);
|
||||
|
||||
detailDate.textContent = card.dataset.date || "";
|
||||
detailTitle.textContent = card.dataset.title || "";
|
||||
detailDesc.textContent = card.dataset.desc || "";
|
||||
detailQuote.textContent = "「" + (card.dataset.quote || "") + "」";
|
||||
detailTag.textContent = card.dataset.tag || "";
|
||||
detailLink.href = card.dataset.link || "#";
|
||||
detailImage.src = card.querySelector("img").src;
|
||||
detailImage.alt = "節目截圖 - " + (card.dataset.title || "行程地點");
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
let firstVisible = null;
|
||||
cards.forEach((card) => {
|
||||
const cardMember = normalizeMember(card.dataset.member || "");
|
||||
const matchedMember = currentMember === "all" || cardMember === currentMember;
|
||||
const matchedKeyword = !currentKeyword || getCardSearchText(card).includes(currentKeyword);
|
||||
const matched = matchedMember && matchedKeyword;
|
||||
card.style.display = matched ? "" : "none";
|
||||
if (matched && !firstVisible) firstVisible = card;
|
||||
});
|
||||
updateDayGroupVisibility();
|
||||
const activeVisibleCard = cards.find((card) => card.classList.contains("active") && card.style.display !== "none");
|
||||
const fallbackCard = activeVisibleCard || firstVisible;
|
||||
if (fallbackCard) {
|
||||
setActiveCard(fallbackCard, { expandDay: false });
|
||||
} else {
|
||||
updateMapByMember(currentMember === "all" ? "all" : currentMember);
|
||||
}
|
||||
}
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
tabs.forEach((item) => {
|
||||
item.classList.remove("active");
|
||||
item.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
tab.classList.add("active");
|
||||
tab.setAttribute("aria-pressed", "true");
|
||||
currentMember = normalizeMember(tab.dataset.member || "all");
|
||||
applyFilter();
|
||||
});
|
||||
});
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", (event) => {
|
||||
currentKeyword = event.target.value.trim().toLowerCase();
|
||||
applyFilter();
|
||||
});
|
||||
}
|
||||
|
||||
itineraryList.addEventListener("click", (event) => {
|
||||
const toggle = event.target.closest(".day-toggle");
|
||||
if (!toggle) return;
|
||||
const group = toggle.closest(".day-group");
|
||||
if (!group) return;
|
||||
const dayGroup = dayGroups.get(group.dataset.day);
|
||||
if (!dayGroup) return;
|
||||
const isExpanded = dayGroup.toggle.getAttribute("aria-expanded") === "true";
|
||||
setDayExpanded(dayGroup, !isExpanded);
|
||||
});
|
||||
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener("click", () => {
|
||||
setActiveCard(card, { expandDay: true });
|
||||
if (window.innerWidth < 920) {
|
||||
detailPanel.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
initializeDayGroups();
|
||||
applyFilter();
|
||||
})();
|
||||
(() => {
|
||||
const canvas = document.getElementById("particles");
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
let particles = [];
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
class Particle {
|
||||
constructor() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.size = Math.random() * 4 + 1;
|
||||
this.speedX = Math.random() * 0.5 - 0.25;
|
||||
this.speedY = Math.random() * 0.5 + 0.2;
|
||||
}
|
||||
update() {
|
||||
this.x += this.speedX;
|
||||
this.y += this.speedY;
|
||||
if (this.y > canvas.height) this.y = 0;
|
||||
}
|
||||
draw() {
|
||||
ctx.fillStyle = "#FFDD00";
|
||||
ctx.globalAlpha = 0.6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function initParticles() {
|
||||
particles = [];
|
||||
for (let i = 0; i < 80; i += 1) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
}
|
||||
|
||||
function animateParticles() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for (const particle of particles) {
|
||||
particle.update();
|
||||
particle.draw();
|
||||
}
|
||||
requestAnimationFrame(animateParticles);
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
initParticles();
|
||||
animateParticles();
|
||||
window.addEventListener("resize", () => {
|
||||
resizeCanvas();
|
||||
initParticles();
|
||||
});
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById("itinerary-search");
|
||||
|
||||
/**
|
||||
* 每個 .region-panel(例如 #taipei、#global)都是獨立的地區區塊,各自的
|
||||
* member-tabs/地圖/地點卡片互不影響。新增行程時只要在對應 .region-panel
|
||||
* 裡加一顆 .tab-btn(帶 data-trip-name / data-map-embed / data-map-open,
|
||||
* 沒有地圖就不要寫 data-map-embed),以及對應的 .polaroid-card,不需要動這支
|
||||
* 檔案。「全部行程」的地圖會依當時有設定地圖的 tab 自動組出來;完全沒有
|
||||
* 設定地圖的行程,地圖區塊會自動收起來,只留卡片列表。
|
||||
*/
|
||||
function initRegionController(panel) {
|
||||
const tabs = Array.from(panel.querySelectorAll(":scope > .member-tabs .tab-btn"));
|
||||
const cards = Array.from(panel.querySelectorAll(".polaroid-card"));
|
||||
const itineraryList = panel.querySelector(".itinerary-list");
|
||||
const detailPanel = panel.querySelector(".detail-panel");
|
||||
const detailImage = panel.querySelector(".detail-photo img");
|
||||
const detailDate = panel.querySelector(".detail-content .date");
|
||||
const detailTitle = panel.querySelector(".detail-content h3");
|
||||
const detailDesc = panel.querySelector(".detail-content p");
|
||||
const detailQuote = panel.querySelector(".detail-content blockquote");
|
||||
const detailTag = panel.querySelector(".detail-content .tag");
|
||||
const detailLink = panel.querySelector(".detail-content .detail-link");
|
||||
|
||||
const mapSticky = panel.querySelector(".map-sticky");
|
||||
const mapEmbedSingle = mapSticky ? mapSticky.querySelector(".map-embed-single") : null;
|
||||
const memberMapFrame = mapEmbedSingle ? mapEmbedSingle.querySelector(".map-my-maps-frame") : null;
|
||||
const mapEmbedEmpty = mapSticky ? mapSticky.querySelector(".map-embed-empty") : null;
|
||||
const mapEmbedAll = mapSticky ? mapSticky.querySelector(".map-embed-all") : null;
|
||||
const openMapLink = mapSticky ? mapSticky.querySelector(".map-link") : null;
|
||||
const openMapLinksDual = mapSticky ? mapSticky.querySelector(".map-links-dual") : null;
|
||||
|
||||
const dayGroups = new Map();
|
||||
|
||||
/* 舊資料裡卡片用 data-member="kogo",tab 用 data-member="ko";新地區請直接讓兩邊字串一致,不需要這個對照 */
|
||||
function normalizeMember(member) {
|
||||
if (member === "kogo") return "ko";
|
||||
return member;
|
||||
}
|
||||
|
||||
const tripConfig = {};
|
||||
tabs.forEach((tab) => {
|
||||
const member = normalizeMember(tab.dataset.member || "");
|
||||
if (!member || member === "all") return;
|
||||
tripConfig[member] = {
|
||||
tripName: tab.dataset.tripName || tab.textContent.trim(),
|
||||
memberLabel: tab.dataset.memberLabel || tab.textContent.trim(),
|
||||
sortOrder: Number.isFinite(Number(tab.dataset.sortOrder)) ? Number(tab.dataset.sortOrder) : 99,
|
||||
mapEmbed: tab.dataset.mapEmbed || "",
|
||||
mapOpen: tab.dataset.mapOpen || "",
|
||||
};
|
||||
});
|
||||
|
||||
const initialTab = tabs.find((tab) => tab.classList.contains("active")) || tabs[0];
|
||||
let currentMember = initialTab ? normalizeMember(initialTab.dataset.member || "all") : "all";
|
||||
let currentKeyword = "";
|
||||
|
||||
function getMemberLabel(member) {
|
||||
return (tripConfig[member] && tripConfig[member].memberLabel) || "未分類";
|
||||
}
|
||||
|
||||
function getDayToken(rawDate) {
|
||||
const match = String(rawDate || "").match(/Day\s*(\d+)/i);
|
||||
if (!match) return String(rawDate || "未分類");
|
||||
return "Day " + match[1];
|
||||
}
|
||||
|
||||
function getGroupLabel(card) {
|
||||
const date = getDayToken(card.dataset.date || "未分類");
|
||||
const member = normalizeMember(card.dataset.member || "");
|
||||
const prefix = (tripConfig[member] && tripConfig[member].tripName) || getMemberLabel(member);
|
||||
return prefix + "_" + date;
|
||||
}
|
||||
|
||||
function tripsWithMaps() {
|
||||
return Object.keys(tripConfig)
|
||||
.filter((member) => tripConfig[member].mapEmbed)
|
||||
.sort((a, b) => tripConfig[a].sortOrder - tripConfig[b].sortOrder);
|
||||
}
|
||||
|
||||
function buildMapPairs(members) {
|
||||
if (!mapEmbedAll) return;
|
||||
mapEmbedAll.innerHTML = "";
|
||||
members.forEach((member) => {
|
||||
const trip = tripConfig[member];
|
||||
const pair = document.createElement("div");
|
||||
pair.className = "map-embed-pair";
|
||||
|
||||
const label = document.createElement("p");
|
||||
label.className = "map-embed-label";
|
||||
label.textContent = trip.tripName;
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.className = "map-my-maps-frame";
|
||||
iframe.loading = "lazy";
|
||||
iframe.referrerPolicy = "no-referrer-when-downgrade";
|
||||
iframe.title = trip.tripName + " 踩點地圖";
|
||||
iframe.src = trip.mapEmbed;
|
||||
|
||||
pair.appendChild(label);
|
||||
pair.appendChild(iframe);
|
||||
mapEmbedAll.appendChild(pair);
|
||||
});
|
||||
}
|
||||
|
||||
function buildOpenLinksDual(members) {
|
||||
if (!openMapLinksDual) return;
|
||||
openMapLinksDual.innerHTML = "";
|
||||
members.forEach((member) => {
|
||||
const trip = tripConfig[member];
|
||||
if (!trip.mapOpen) return;
|
||||
const a = document.createElement("a");
|
||||
a.className = "map-link";
|
||||
a.target = "_blank";
|
||||
a.href = trip.mapOpen;
|
||||
a.textContent = "開啟「" + trip.tripName + "」My Maps";
|
||||
openMapLinksDual.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMapByMember(member) {
|
||||
if (!mapSticky) return;
|
||||
if (!Object.keys(tripConfig).length) {
|
||||
mapSticky.hidden = true;
|
||||
return;
|
||||
}
|
||||
mapSticky.hidden = false;
|
||||
|
||||
if (member === "all") {
|
||||
const withMaps = tripsWithMaps();
|
||||
if (mapEmbedSingle) mapEmbedSingle.hidden = true;
|
||||
if (openMapLink) openMapLink.hidden = true;
|
||||
if (mapEmbedAll) {
|
||||
buildMapPairs(withMaps);
|
||||
mapEmbedAll.hidden = withMaps.length === 0;
|
||||
}
|
||||
if (openMapLinksDual) {
|
||||
buildOpenLinksDual(withMaps);
|
||||
openMapLinksDual.hidden = withMaps.length === 0;
|
||||
}
|
||||
if (mapEmbedEmpty) mapEmbedEmpty.hidden = withMaps.length > 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const trip = tripConfig[member];
|
||||
const hasMap = !!(trip && trip.mapEmbed);
|
||||
if (mapEmbedAll) mapEmbedAll.hidden = true;
|
||||
if (openMapLinksDual) openMapLinksDual.hidden = true;
|
||||
if (mapEmbedSingle) mapEmbedSingle.hidden = !hasMap;
|
||||
if (memberMapFrame && hasMap) memberMapFrame.src = trip.mapEmbed;
|
||||
if (openMapLink) {
|
||||
openMapLink.hidden = !hasMap;
|
||||
if (hasMap) openMapLink.href = trip.mapOpen || "#";
|
||||
}
|
||||
if (mapEmbedEmpty) mapEmbedEmpty.hidden = hasMap;
|
||||
}
|
||||
|
||||
function getCardSearchText(card) {
|
||||
return [
|
||||
card.dataset.date || "",
|
||||
card.dataset.title || "",
|
||||
card.dataset.desc || "",
|
||||
card.dataset.tag || "",
|
||||
].join(" ").toLowerCase();
|
||||
}
|
||||
|
||||
function createDayGroup(dayLabel) {
|
||||
const group = document.createElement("section");
|
||||
group.className = "day-group";
|
||||
group.dataset.day = dayLabel;
|
||||
|
||||
const toggle = document.createElement("button");
|
||||
toggle.type = "button";
|
||||
toggle.className = "day-toggle";
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
toggle.innerHTML = "<span>" + dayLabel + "</span><i class='fa-solid fa-chevron-down'></i>";
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "day-cards";
|
||||
body.style.display = "none";
|
||||
|
||||
group.appendChild(toggle);
|
||||
group.appendChild(body);
|
||||
itineraryList.appendChild(group);
|
||||
dayGroups.set(dayLabel, { group, toggle, body });
|
||||
return dayGroups.get(dayLabel);
|
||||
}
|
||||
|
||||
function setDayExpanded(dayGroup, shouldExpand) {
|
||||
dayGroup.toggle.setAttribute("aria-expanded", shouldExpand ? "true" : "false");
|
||||
dayGroup.body.style.display = shouldExpand ? "grid" : "none";
|
||||
dayGroup.group.classList.toggle("expanded", shouldExpand);
|
||||
}
|
||||
|
||||
function initializeDayGroups() {
|
||||
if (!itineraryList || !cards.length) return;
|
||||
const sortedCards = [...cards].sort((a, b) => {
|
||||
const memberA = normalizeMember(a.dataset.member || "");
|
||||
const memberB = normalizeMember(b.dataset.member || "");
|
||||
const sortA = (tripConfig[memberA] && tripConfig[memberA].sortOrder) ?? 99;
|
||||
const sortB = (tripConfig[memberB] && tripConfig[memberB].sortOrder) ?? 99;
|
||||
if (sortA !== sortB) return sortA - sortB;
|
||||
|
||||
const dayA = Number((getDayToken(a.dataset.date).match(/\d+/) || [999])[0]);
|
||||
const dayB = Number((getDayToken(b.dataset.date).match(/\d+/) || [999])[0]);
|
||||
return dayA - dayB;
|
||||
});
|
||||
|
||||
sortedCards.forEach((card) => {
|
||||
const dayLabel = getGroupLabel(card);
|
||||
const dayGroup = dayGroups.get(dayLabel) || createDayGroup(dayLabel);
|
||||
dayGroup.body.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function updateDayGroupVisibility() {
|
||||
dayGroups.forEach(({ group, body, toggle }) => {
|
||||
const visibleCards = Array.from(body.querySelectorAll(".polaroid-card")).filter((card) => card.style.display !== "none");
|
||||
group.style.display = visibleCards.length ? "" : "none";
|
||||
if (!visibleCards.length) {
|
||||
setDayExpanded({ group, toggle, body }, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensureExpandedForCard(card) {
|
||||
const dayGroup = dayGroups.get(getGroupLabel(card));
|
||||
if (!dayGroup) return;
|
||||
setDayExpanded(dayGroup, true);
|
||||
}
|
||||
|
||||
function setActiveCard(card, options = {}) {
|
||||
const { expandDay = true } = options;
|
||||
cards.forEach((item) => item.classList.remove("active"));
|
||||
card.classList.add("active");
|
||||
if (expandDay) {
|
||||
ensureExpandedForCard(card);
|
||||
}
|
||||
updateMapByMember(currentMember === "all" ? "all" : currentMember);
|
||||
|
||||
if (!detailPanel) return;
|
||||
if (detailDate) detailDate.textContent = card.dataset.date || "";
|
||||
if (detailTitle) detailTitle.textContent = card.dataset.title || "";
|
||||
if (detailDesc) detailDesc.textContent = card.dataset.desc || "";
|
||||
if (detailQuote) detailQuote.textContent = "「" + (card.dataset.quote || "") + "」";
|
||||
if (detailTag) detailTag.textContent = card.dataset.tag || "";
|
||||
if (detailLink) detailLink.href = card.dataset.link || "#";
|
||||
const cardImg = card.querySelector("img");
|
||||
if (detailImage && cardImg) {
|
||||
detailImage.src = cardImg.src;
|
||||
detailImage.alt = "節目截圖 - " + (card.dataset.title || "行程地點");
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
let firstVisible = null;
|
||||
cards.forEach((card) => {
|
||||
const cardMember = normalizeMember(card.dataset.member || "");
|
||||
const matchedMember = currentMember === "all" || cardMember === currentMember;
|
||||
const matchedKeyword = !currentKeyword || getCardSearchText(card).includes(currentKeyword);
|
||||
const matched = matchedMember && matchedKeyword;
|
||||
card.style.display = matched ? "" : "none";
|
||||
if (matched && !firstVisible) firstVisible = card;
|
||||
});
|
||||
updateDayGroupVisibility();
|
||||
const activeVisibleCard = cards.find((card) => card.classList.contains("active") && card.style.display !== "none");
|
||||
const fallbackCard = activeVisibleCard || firstVisible;
|
||||
if (fallbackCard) {
|
||||
setActiveCard(fallbackCard, { expandDay: false });
|
||||
} else {
|
||||
updateMapByMember(currentMember === "all" ? "all" : currentMember);
|
||||
}
|
||||
}
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
tabs.forEach((item) => {
|
||||
item.classList.remove("active");
|
||||
item.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
tab.classList.add("active");
|
||||
tab.setAttribute("aria-pressed", "true");
|
||||
currentMember = normalizeMember(tab.dataset.member || "all");
|
||||
applyFilter();
|
||||
});
|
||||
});
|
||||
|
||||
if (itineraryList) {
|
||||
itineraryList.addEventListener("click", (event) => {
|
||||
const toggle = event.target.closest(".day-toggle");
|
||||
if (!toggle) return;
|
||||
const group = toggle.closest(".day-group");
|
||||
if (!group) return;
|
||||
const dayGroup = dayGroups.get(group.dataset.day);
|
||||
if (!dayGroup) return;
|
||||
const isExpanded = dayGroup.toggle.getAttribute("aria-expanded") === "true";
|
||||
setDayExpanded(dayGroup, !isExpanded);
|
||||
});
|
||||
}
|
||||
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener("click", () => {
|
||||
setActiveCard(card, { expandDay: true });
|
||||
if (window.innerWidth < 920 && detailPanel) {
|
||||
detailPanel.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
initializeDayGroups();
|
||||
applyFilter();
|
||||
|
||||
return {
|
||||
setKeyword(keyword) {
|
||||
currentKeyword = keyword;
|
||||
applyFilter();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const regionPanels = Array.from(document.querySelectorAll(".region-panel"));
|
||||
const controllers = regionPanels.map((panel) => initRegionController(panel));
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", (event) => {
|
||||
const keyword = event.target.value.trim().toLowerCase();
|
||||
controllers.forEach((controller) => controller.setKeyword(keyword));
|
||||
});
|
||||
}
|
||||
|
||||
const regionTabs = Array.from(document.querySelectorAll("[data-region-tabs] .region-tab-btn"));
|
||||
if (regionTabs.length && regionPanels.length) {
|
||||
regionTabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const region = tab.dataset.region;
|
||||
regionPanels.forEach((panel) => {
|
||||
panel.hidden = panel.dataset.regionPanel !== region;
|
||||
});
|
||||
regionTabs.forEach((item) => {
|
||||
const isActive = item === tab;
|
||||
item.classList.toggle("active", isActive);
|
||||
item.setAttribute("aria-pressed", isActive ? "true" : "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user