-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathscript.js
More file actions
371 lines (319 loc) · 14.6 KB
/
Copy pathscript.js
File metadata and controls
371 lines (319 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// --- トップページの季節Hero背景 ---
// テスト終了後に null へ戻すと、実際の日付判定に切り替わる。
const SEASONAL_HERO_TEST_DATE = null;
const initSeasonalHero = () => {
const hero = document.querySelector("[data-seasonal-hero]");
if (!hero) return;
const basePath = hero.dataset.seasonalBase?.replace(/\/$/, "");
if (!basePath) return;
const targetDate = SEASONAL_HERO_TEST_DATE ? new Date(SEASONAL_HERO_TEST_DATE) : new Date();
const japanDateParts = new Intl.DateTimeFormat("en-US", {
timeZone: "Asia/Tokyo",
year: "numeric",
month: "2-digit",
}).formatToParts(targetDate);
const year = japanDateParts.find(({ type }) => type === "year")?.value;
const month = japanDateParts.find(({ type }) => type === "month")?.value;
if (!year || !month) return;
const imagePath = `${basePath}/hero-${year}-${month}.webp`;
const imageUrl = new URL(imagePath, document.baseURI).href;
const seasonalImage = new Image();
seasonalImage.decoding = "async";
seasonalImage.fetchPriority = "high";
seasonalImage.addEventListener(
"load",
() => {
// CSS変数を外部CSSから使用しても参照起点がずれないよう、絶対URLを渡す。
hero.style.setProperty("--seasonal-hero-image", `url("${imageUrl}")`);
hero.classList.add("hero--seasonal");
},
{ once: true },
);
// 該当月の画像がない場合は、CSSの既存背景をそのまま使用する。
seasonalImage.src = imageUrl;
};
// --- 🌟 ページ読み込み完了時の処理を一元管理 ---
document.addEventListener("DOMContentLoaded", () => {
initSeasonalHero();
// 🌟 追加:ヘッダーのスクロール連動エフェクト
window.addEventListener("scroll", () => {
const header = document.querySelector("header");
if (header) {
// 下に50px以上スクロールしたら 'scrolled' クラスを付与する
header.classList.toggle("scrolled", window.scrollY > 50);
}
});
// スクロールフェードイン制御の開始(各ページで描画されたカードも監視対象になる)
initScrollReveal();
// ==========================================================================
// 🌟 追加:スクロールプログレスバーの生成と制御
// ==========================================================================
// 1. バーの要素を動的に作成してbodyに追加
const progressBar = document.createElement("div");
progressBar.id = "scroll-indicator";
document.body.appendChild(progressBar);
// 2. スクロール量に応じて幅を計算する関数
window.addEventListener("scroll", () => {
// 現在のスクロール位置
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
// ページ全体の高さ - 表示領域の高さ = スクロール可能な総距離
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
// 割合を計算(0〜100)
const scrolled = (winScroll / height) * 100;
// バーの幅に反映
progressBar.style.width = scrolled + "%";
});
});
// --- ハンバーガーメニュー制御 ---
const initNav = () => {
const burger = document.querySelector(".burger");
const nav = document.querySelector(".nav-links");
const navLinks = document.querySelectorAll(".nav-links li");
if (!burger || !nav) return; // ヘッダーがないページでのエラー防止
burger.addEventListener("click", () => {
nav.classList.toggle("nav-active");
navLinks.forEach((link, index) => {
if (link.style.animation) {
link.style.animation = "";
} else {
link.style.animation = `navLinkFade 0.5s ease forwards ${index / 7 + 0.3}s`;
}
});
burger.classList.toggle("toggle");
});
};
// --- スクロールフェードイン制御 ---
const initScrollReveal = () => {
const reveals = document.querySelectorAll(".reveal");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("active");
}
});
},
{ threshold: 0.1 },
);
reveals.forEach((reveal) => {
observer.observe(reveal);
});
};
// --- 🌟 ナビゲーション定義(一元管理) ---
const NAV_ITEMS = [
{ key: "guide", label: "歩き方", path: "/guide/" },
{ key: "about", label: "運営", path: "/about/" },
{ key: "events", label: "イベント", path: "/events/" },
{ key: "calendar", label: "カレンダー", path: "/calendar/" },
{ key: "contents", label: "コンテンツ", path: "/contents/" },
{ key: "links", label: "リンク集", path: "/links/" },
];
// --- 🌟 ページタイトル一元設定 ---
const PAGE_TITLES = {
home: "SnowVillage",
guide: "歩き方",
about: "運営",
mayors: "Mayors",
neighbors: "Neighbors",
departments: "部門紹介",
alumni: "歴代の貢献者",
events: "イベント",
calendar: "カレンダー",
contents: "コンテンツ",
links: "リンク集",
join: "Slackに参加",
news: "お知らせ",
};
const setPageTitle = (menuKey) => {
const name = PAGE_TITLES[menuKey];
document.title = !name || menuKey === "home" ? "SnowVillage" : `${name} - SnowVillage`;
};
// --- 🌟 favicon / ホーム画面アイコン / PWA マニフェストを全ページ共通で注入 ---
const injectFavicon = (pathToRoot) => {
const head = document.head;
const addLink = (rel, attrs) => {
if (document.querySelector(`link[rel='${rel}']`)) return;
const link = document.createElement("link");
link.rel = rel;
Object.entries(attrs).forEach(([k, v]) => link.setAttribute(k, v));
head.appendChild(link);
};
const addMeta = (name, content) => {
if (document.querySelector(`meta[name='${name}']`)) return;
const meta = document.createElement("meta");
meta.name = name;
meta.content = content;
head.appendChild(meta);
};
addLink("icon", { type: "image/png", href: `${pathToRoot}/images/brand/favicon.png` });
// iOS ホーム画面アイコン(180×180、システムが自動で角丸処理)
addLink("apple-touch-icon", { sizes: "180x180", href: `${pathToRoot}/images/brand/apple-touch-icon.png` });
// Android / PWA マニフェスト
addLink("manifest", { href: `${pathToRoot}/site.webmanifest` });
// モバイル/PWA テーマカラー(アドレスバー色など)
addMeta("theme-color", "#1a365d");
// iOS PWA 用ステータスバー設定
addMeta("apple-mobile-web-app-capable", "yes");
addMeta("apple-mobile-web-app-status-bar-style", "black-translucent");
addMeta("apple-mobile-web-app-title", "SnowVillage");
};
// --- 🌟 共通ヘッダーの生成 ---
const renderHeader = (pathToRoot, activeMenu) => {
const headerContainer = document.getElementById("header-container");
if (!headerContainer) return;
injectFavicon(pathToRoot);
setPageTitle(activeMenu);
const logoSrc = `${pathToRoot}/images/brand/logo-white.png`;
const logoHtml = `<a href="${pathToRoot}/" class="logo logo-home" aria-label="SnowVillage">
<img src="${logoSrc}" alt="" class="logo-mark" />
<span class="logo-text">SnowVillage</span>
</a>`;
// about 配下のサブページも「運営」タブをアクティブ扱い
const isActive = (key) => {
if (key === "about") return ["about", "mayors", "neighbors", "departments", "alumni"].includes(activeMenu);
return key === activeMenu;
};
const navItemsHtml = NAV_ITEMS.map(
(item) => `<li><a href="${pathToRoot}${item.path}" class="${isActive(item.key) ? "active" : ""}">${item.label}</a></li>`
).join("");
headerContainer.innerHTML = `
<header>
<div class="container nav-container">
${logoHtml}
<ul class="nav-links">
${navItemsHtml}
<li><a href="https://usergroups.snowflake.com/snowvillage/" target="_blank" rel="noopener noreferrer" class="btn">Slackに参加</a></li>
<li id="nav-slider"></li>
</ul>
<div class="burger">
<div class="line1"></div><div class="line2"></div><div class="line3"></div>
</div>
</div>
</header>
`;
initNav();
initNavSlider();
};
// --- 🌟 スライディング・ナビゲーションの制御 ---
const initNavSlider = () => {
const navLinks = document.querySelector(".nav-links");
const slider = document.getElementById("nav-slider");
const links = document.querySelectorAll(".nav-links a:not(.btn)");
if (!navLinks || !slider) return;
const moveSlider = (element) => {
if (!element) return;
const rect = element.getBoundingClientRect();
const parentRect = navLinks.getBoundingClientRect();
slider.style.left = `${rect.left - parentRect.left}px`;
slider.style.width = `${rect.width}px`;
slider.style.opacity = "1";
};
const activeLink = document.querySelector(".nav-links a.active");
if (activeLink) {
setTimeout(() => moveSlider(activeLink), 150);
}
links.forEach((link) => {
link.addEventListener("mouseenter", (e) => moveSlider(e.target));
});
navLinks.addEventListener("mouseleave", () => {
const currentActive = document.querySelector(".nav-links a.active");
if (currentActive) moveSlider(currentActive);
});
};
// --- 🌟 共通フッターの生成 ---
const renderFooter = () => {
const footerContainer = document.getElementById("footer-container");
if (!footerContainer) return;
// 現在見ているページのURLを自動取得してシェア用にエンコード
const currentUrl = encodeURIComponent(window.location.href);
const shareText = encodeURIComponent("SnowVillage 公式サイトをチェック!");
const hashtag = "SnowVillage";
// 🌟 X (旧Twitter) 用のシェアURL生成
const xShareUrl = `https://x.com/intent/tweet?text=${shareText}&url=${currentUrl}&hashtags=${hashtag}`;
// 🌟 LinkedIn 用のシェアURL生成(直接投稿画面を開くパラメータ)
const linkedInShareUrl = `https://www.linkedin.com/feed/?shareActive=true&text=${shareText}%0A%23${hashtag}%0A${currentUrl}`;
footerContainer.innerHTML = `
<style>
/* フッター専用のSNSアイコンホバーエフェクト */
.footer-sns-link {
color: var(--text-light);
transition: transform 0.3s ease, color 0.3s ease;
display: inline-flex;
align-items: center;
}
.footer-sns-link:hover {
transform: translateY(-3px);
}
.footer-sns-link.x-icon:hover {
color: #ffffff; /* Xは白に光る */
}
.footer-sns-link.li-icon:hover {
color: #0a66c2; /* LinkedInは公式ブルーに光る */
}
</style>
<footer style="padding: 40px 0; background-color: var(--secondary); border-top: 1px solid rgba(255,255,255,0.1);">
<div class="container" style="display: flex; flex-direction: column; align-items: center; gap: 20px;">
<div style="display: flex; align-items: center; gap: 20px;">
<span style="color: var(--text-light); font-size: 0.9rem; font-weight: 600; letter-spacing: 1px;">SHARE ON</span>
<a href="${xShareUrl}" target="_blank" rel="noopener noreferrer" class="footer-sns-link x-icon" aria-label="Share on X">
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></svg>
</a>
<a href="${linkedInShareUrl}" target="_blank" rel="noopener noreferrer" class="footer-sns-link li-icon" aria-label="Share on LinkedIn">
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77z"></path></svg>
</a>
</div>
<p style="color: var(--text-light); font-size: 0.85rem; margin: 0;">© 2026 SnowVillage</p>
</div>
</footer>
`;
};
// --- 🌟 ニュース生成 ---
function loadLatestNews() {
const newsList = document.getElementById("news-ticker-list");
const archiveLink = document.getElementById("news-archive-link");
if (!newsList || typeof newsData === "undefined" || newsData.length === 0) return;
// 最新の5件だけを取得
const latestFive = newsData.slice(0, 5);
// 🌟 修正:mapにindex引数を追加
newsList.innerHTML = latestFive
.map(
(item, index) => `
<li class="news-item">
<a href="${item.link}" class="news-link" ${item.isExternal ? 'target="_blank" rel="noopener noreferrer"' : ""}>
<span class="news-date">${item.date}</span>
<span class="news-badge-slot">${index === 0 ? '<span class="news-badge-new">NEW</span>' : ""}</span>
<span class="news-text">${item.text}</span>
<span class="news-arrow">→</span>
</a>
</li>
`,
)
.join("");
// 6件以上データがあれば、アーカイブへのリンクを表示
if (newsData.length > 5 && archiveLink) {
archiveLink.style.display = "block";
}
}
// --- 🎮 イースターエッグ:コナミコマンド監視 ---
// コマンド: ↑ ↑ ↓ ↓ ← → ← → B A
const konamiCode = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "KeyB", "KeyA"];
let konamiPosition = 0;
document.addEventListener("keydown", (e) => {
// 入力されたキーがコマンドの現在の位置と一致するか
if (e.code === konamiCode[konamiPosition]) {
konamiPosition++;
// 最後まで正しく入力されたら
if (konamiPosition === konamiCode.length) {
// 🎉 エフェクト&隠しページへ遷移
alert("❄️ Easter Egg Unlocked! ❄️\nWelcome to SnowTris!");
// ルートからの絶対パスか、相対パスで /secret/ へ遷移
// (ローカル環境・本番環境に合わせて適宜調整してください)
const basePath = window.location.pathname.includes("/SnowVillage") ? "/SnowVillage" : "";
window.location.href = `${basePath}/secret/`;
konamiPosition = 0; // リセット
}
} else {
// 間違えたら最初からやり直し
konamiPosition = 0;
}
});