chore: variety資料夾改名vedio並同步連結
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# 打歌舞台後台|乙級練習版
|
||||
|
||||
1. 開啟 XAMPP Control Panel,啟動 Apache 與 MySQL。
|
||||
2. 進入 phpMyAdmin,匯入 `setup.sql`。
|
||||
3. 將本資料夾放入 `C:\xampp\htdocs\`,或在此資料夾執行:
|
||||
`C:\xampp\php\php.exe -S 127.0.0.1:8080`
|
||||
4. 開啟 `http://127.0.0.1:8080/login.php`。
|
||||
5. 練習帳號:`admin`;密碼:`1234`。
|
||||
|
||||
練習內容:Session 登入、PDO、CRUD、預備敘述、防 XSS、CSRF、列表排序、顯示/隱藏與 JSON API。
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$rows = $pdo->query('SELECT stage_date,youtube_id,station,program,title,note,sort_order FROM stages WHERE is_visible=1 ORDER BY sort_order,stage_date,id')->fetchAll();
|
||||
echo json_encode(['stages'=>$rows], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
|
||||
const DB_HOST = '127.0.0.1';
|
||||
const DB_NAME = 'three_words_10th';
|
||||
const DB_USER = 'root';
|
||||
const DB_PASS = '';
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
exit('資料庫連線失敗,請先啟動 XAMPP MySQL 並匯入 setup.sql。');
|
||||
}
|
||||
|
||||
function e(?string $value): string { return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8'); }
|
||||
function loggedIn(): bool { return isset($_SESSION['admin_id']); }
|
||||
function requireLogin(): void { if (!loggedIn()) { header('Location: login.php'); exit; } }
|
||||
function csrf(): string {
|
||||
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(24));
|
||||
return $_SESSION['csrf'];
|
||||
}
|
||||
function checkCsrf(): void {
|
||||
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) exit('表單驗證失敗');
|
||||
}
|
||||
function youtubeId(string $url): string {
|
||||
if (preg_match('~(?:youtu\.be/|youtube\.com/(?:watch\?v=|embed/|shorts/))([\w-]{11})~', $url, $m)) return $m[1];
|
||||
return preg_match('/^[\w-]{11}$/', $url) ? $url : '';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php'; requireLogin(); checkCsrf();
|
||||
$stmt = $pdo->prepare('DELETE FROM stages WHERE id = ?');
|
||||
$stmt->execute([(int)($_POST['id'] ?? 0)]);
|
||||
header('Location: index.php');
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
requireLogin();
|
||||
$edit = ['id'=>'','stage_date'=>'','youtube_id'=>'','station'=>'','program'=>'','title'=>'','note'=>'','sort_order'=>0,'is_visible'=>1];
|
||||
if (isset($_GET['edit'])) {
|
||||
$stmt = $pdo->prepare('SELECT * FROM stages WHERE id = ?'); $stmt->execute([(int)$_GET['edit']]);
|
||||
$edit = $stmt->fetch() ?: $edit;
|
||||
}
|
||||
$stages = $pdo->query('SELECT * FROM stages ORDER BY sort_order ASC, stage_date ASC, id ASC')->fetchAll();
|
||||
?>
|
||||
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>打歌舞台後台</title><link rel="stylesheet" href="style.css"></head><body>
|
||||
<header><div><p class="eyebrow">THREE WORDS · 10TH</p><h1>打歌舞台後台</h1></div><nav><a href="stages.php" target="_blank">查看前台 ↗</a><a href="api.php" target="_blank">JSON API ↗</a><a href="logout.php">登出</a></nav></header>
|
||||
<main class="adminGrid">
|
||||
<section class="panel formPanel"><div class="panelTitle"><span>01</span><h2><?=$edit['id']?'修改舞台':'新增舞台'?></h2></div>
|
||||
<form method="post" action="save.php"><input type="hidden" name="csrf" value="<?=csrf()?>"><input type="hidden" name="id" value="<?=e((string)$edit['id'])?>">
|
||||
<div class="two"><label>播出日期<input type="date" name="stage_date" required value="<?=e($edit['stage_date'])?>"></label><label>顯示順序<input type="number" name="sort_order" value="<?=e((string)$edit['sort_order'])?>"></label></div>
|
||||
<label>YouTube 網址或影片 ID<input name="youtube_url" required placeholder="https://www.youtube.com/watch?v=..." value="<?=e($edit['youtube_id'])?>"></label>
|
||||
<div class="two"><label>電視台<input name="station" required placeholder="SBS" value="<?=e($edit['station'])?>"></label><label>節目名稱<input name="program" required placeholder="人氣歌謠" value="<?=e($edit['program'])?>"></label></div>
|
||||
<label>舞台標題<input name="title" required value="<?=e($edit['title'])?>"></label><label>備註<textarea name="note" rows="3"><?=e($edit['note'])?></textarea></label>
|
||||
<label class="check"><input type="checkbox" name="is_visible" value="1" <?=$edit['is_visible']?'checked':''?>> 前台顯示</label><div class="formActions"><button><?=$edit['id']?'儲存修改':'新增資料'?></button><?php if($edit['id']):?><a href="index.php">取消修改</a><?php endif;?></div></form></section>
|
||||
<section class="panel listPanel"><div class="panelTitle"><span>02</span><h2>資料列表</h2><b><?=count($stages)?> 筆</b></div>
|
||||
<div class="tableWrap"><table><thead><tr><th>順序</th><th>日期/縮圖</th><th>電視台/舞台</th><th>狀態</th><th>操作</th></tr></thead><tbody>
|
||||
<?php foreach($stages as $row):?><tr><td><?=e((string)$row['sort_order'])?></td><td><time><?=e($row['stage_date'])?></time><img src="https://i.ytimg.com/vi/<?=e($row['youtube_id'])?>/mqdefault.jpg" alt="影片縮圖"></td><td><strong><?=e($row['station'])?> · <?=e($row['program'])?></strong><span><?=e($row['title'])?></span></td><td><i class="status <?=$row['is_visible']?'on':''?>"><?=$row['is_visible']?'顯示':'隱藏'?></i></td><td class="ops"><a href="?edit=<?=$row['id']?>">修改</a><form method="post" action="delete.php" onsubmit="return confirm('確定刪除這筆資料?')"><input type="hidden" name="csrf" value="<?=csrf()?>"><input type="hidden" name="id" value="<?=$row['id']?>"><button>刪除</button></form></td></tr><?php endforeach; ?>
|
||||
</tbody></table></div></section></main></body></html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
if (loggedIn()) { header('Location: index.php'); exit; }
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
checkCsrf();
|
||||
$stmt = $pdo->prepare('SELECT * FROM admins WHERE account = ? AND password = SHA2(?, 256)');
|
||||
$stmt->execute([trim($_POST['account'] ?? ''), $_POST['password'] ?? '']);
|
||||
if ($admin = $stmt->fetch()) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['admin_id'] = $admin['id'];
|
||||
$_SESSION['admin_account'] = $admin['account'];
|
||||
header('Location: index.php'); exit;
|
||||
}
|
||||
$error = '帳號或密碼錯誤';
|
||||
}
|
||||
?>
|
||||
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>後台登入</title><link rel="stylesheet" href="style.css"></head>
|
||||
<body class="loginPage"><main class="loginCard"><p class="eyebrow">THREE WORDS · ADMIN</p><h1>舞台資料管理</h1><p>乙級練習:Session 登入與資料庫驗證</p><?php if ($error): ?><div class="alert"><?=e($error)?></div><?php endif; ?>
|
||||
<form method="post"><input type="hidden" name="csrf" value="<?=csrf()?>"><label>帳號<input name="account" required autofocus value="admin"></label><label>密碼<input type="password" name="password" required value="1234"></label><button>登入後台</button></form></main></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php'; requireLogin(); checkCsrf();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$youtube = youtubeId(trim($_POST['youtube_url'] ?? ''));
|
||||
if (!$youtube) exit('YouTube 網址或 ID 格式錯誤');
|
||||
$data = [$_POST['stage_date'] ?? '', $youtube, trim($_POST['station'] ?? ''), trim($_POST['program'] ?? ''), trim($_POST['title'] ?? ''), trim($_POST['note'] ?? ''), (int)($_POST['sort_order'] ?? 0), isset($_POST['is_visible']) ? 1 : 0];
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare('UPDATE stages SET stage_date=?,youtube_id=?,station=?,program=?,title=?,note=?,sort_order=?,is_visible=? WHERE id=?');
|
||||
$data[] = $id; $stmt->execute($data);
|
||||
} else {
|
||||
$stmt = $pdo->prepare('INSERT INTO stages(stage_date,youtube_id,station,program,title,note,sort_order,is_visible) VALUES(?,?,?,?,?,?,?,?)');
|
||||
$stmt->execute($data);
|
||||
}
|
||||
header('Location: index.php');
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE DATABASE IF NOT EXISTS three_words_10th CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE three_words_10th;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account VARCHAR(50) NOT NULL UNIQUE,
|
||||
password CHAR(64) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stages (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
stage_date DATE NOT NULL,
|
||||
youtube_id VARCHAR(11) NOT NULL,
|
||||
station VARCHAR(30) NOT NULL,
|
||||
program VARCHAR(100) NOT NULL,
|
||||
title VARCHAR(150) NOT NULL,
|
||||
note VARCHAR(255) NOT NULL DEFAULT '',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_visible TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO admins (account, password) VALUES ('admin', SHA2('1234', 256));
|
||||
INSERT INTO stages (stage_date, youtube_id, station, program, title, note, sort_order)
|
||||
SELECT '2016-12-04', '8HTqr3Wp-R0', 'SBS', '인기가요 · Inkigayo', 'THREE WORDS · Goodbye Stage', '十週年收藏舞台', 10
|
||||
WHERE NOT EXISTS (SELECT 1 FROM stages WHERE youtube_id = '8HTqr3Wp-R0');
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php require __DIR__ . '/config.php'; $stages=$pdo->query('SELECT * FROM stages WHERE is_visible=1 ORDER BY sort_order,stage_date,id')->fetchAll(); ?>
|
||||
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>THREE WORDS 打歌舞台</title><link rel="stylesheet" href="style.css"></head><body class="front"><main><p class="eyebrow">MUSIC SHOW ARCHIVE</p><h1>打歌舞台</h1><div class="frontTable"><div class="frontHead"><span>DATE</span><span>VIDEO</span><span>STATION</span></div><?php foreach($stages as $s):?><article><time><?=e($s['stage_date'])?></time><a href="https://youtu.be/<?=e($s['youtube_id'])?>" target="_blank"><img src="https://i.ytimg.com/vi/<?=e($s['youtube_id'])?>/hqdefault.jpg" alt="<?=e($s['title'])?>"><div><strong><?=e($s['title'])?></strong><small>WATCH ↗</small></div></a><div class="tv"><strong><?=e($s['station'])?></strong><span><?=e($s['program'])?></span></div></article><?php endforeach;?></div><a class="back" href="index.php">回後台</a></main></body></html>
|
||||
@@ -0,0 +1,2 @@
|
||||
:root{--ink:#07101d;--cream:#f2ede2;--gold:#efb53f;--blue:#16334d;--muted:#69727d}*{box-sizing:border-box}body{margin:0;background:#e8e2d6;color:var(--ink);font-family:Arial,"Microsoft JhengHei",sans-serif}a{color:inherit}.eyebrow{font:700 10px Arial;letter-spacing:.3em;color:#8a6c19}header{display:flex;justify-content:space-between;align-items:end;padding:32px 4vw;background:var(--ink);color:var(--cream)}h1{margin:8px 0 0;font:900 clamp(35px,5vw,68px)/1 serif}header nav{display:flex;gap:18px;flex-wrap:wrap}header nav a{text-decoration:none;border-bottom:1px solid var(--gold);padding-bottom:4px}.adminGrid{display:grid;grid-template-columns:minmax(310px,.7fr) minmax(600px,1.6fr);gap:24px;padding:28px 4vw}.panel{background:var(--cream);padding:25px;box-shadow:0 15px 45px rgba(7,16,29,.08)}.panelTitle{display:flex;align-items:center;gap:13px;margin-bottom:24px;border-bottom:1px solid #aaa;padding-bottom:15px}.panelTitle span{display:grid;place-content:center;width:35px;height:35px;border-radius:50%;background:var(--gold);font-weight:900}.panelTitle h2{margin:0;font:800 28px serif}.panelTitle b{margin-left:auto;color:var(--muted)}label{display:grid;gap:8px;margin:0 0 17px;font-size:13px;font-weight:700}input,textarea{width:100%;border:1px solid #aaa;background:#fffdf7;padding:11px;font:inherit}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.check{display:flex;align-items:center}.check input{width:auto}.formActions{display:flex;align-items:center;gap:15px}.formActions button,.loginCard button{border:0;background:var(--gold);padding:13px 20px;font-weight:900;cursor:pointer}.tableWrap{overflow:auto}table{width:100%;border-collapse:collapse;min-width:760px}th,td{text-align:left;border-bottom:1px solid #aaa;padding:12px 10px;vertical-align:middle}th{font:700 9px Arial;letter-spacing:.15em;color:#76682f}td img{display:block;width:130px;aspect-ratio:16/9;object-fit:cover;margin-top:8px}td strong,td span{display:block}td span{margin-top:7px;color:var(--muted);font-size:13px}.status{display:inline-block;padding:5px 8px;background:#bbb;font-style:normal;font-size:12px}.status.on{background:var(--gold)}.ops{display:flex;gap:9px;align-items:center}.ops form{margin:0}.ops button{border:0;background:transparent;color:#9b2f22;text-decoration:underline;cursor:pointer}.loginPage{min-height:100vh;display:grid;place-content:center;background:radial-gradient(circle at 80% 10%,#3f5c72,var(--ink) 55%)}.loginCard{width:min(440px,90vw);padding:38px;background:var(--cream);box-shadow:0 30px 90px #0006}.loginCard h1{font-size:48px}.alert{padding:10px;background:#ffd9d2;color:#922;margin:15px 0}.front{background:var(--cream)}.front main{padding:80px 8vw}.front h1{font-size:90px}.frontTable{border-top:2px solid var(--ink);margin-top:50px}.frontHead,.frontTable article{display:grid;grid-template-columns:.55fr 2fr .8fr;gap:25px;align-items:center}.frontHead{padding:12px 0;border-bottom:1px solid #aaa;font:700 9px Arial;letter-spacing:.2em}.frontTable article{padding:22px 0;border-bottom:1px solid #aaa}.frontTable time{font:900 30px Arial}.frontTable article>a{display:grid;grid-template-columns:220px 1fr;gap:20px;align-items:center;text-decoration:none}.frontTable img{width:100%;aspect-ratio:16/9;object-fit:cover}.frontTable small{display:block;margin-top:10px;color:#8a6c19}.tv strong{display:block;font:900 48px Arial;color:#c48f17}.tv span{font-size:12px}.back{display:inline-block;margin-top:30px}.frontTable article>a:hover img{filter:brightness(.75)}
|
||||
@media(max-width:900px){.adminGrid{grid-template-columns:1fr}.frontHead{display:none}.frontTable article{grid-template-columns:1fr}.frontTable article>a{grid-template-columns:1fr}.front h1{font-size:60px}}@media(max-width:560px){header{align-items:start;flex-direction:column;gap:22px}.adminGrid{padding:18px}.two{grid-template-columns:1fr}.front main{padding:55px 6vw}}
|
||||
@@ -0,0 +1,20 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root{--ink:#07101d;--cream:#f2ede2;--gold:#efb53f;--blue:#16334d;--line:rgba(255,255,255,.22)}
|
||||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--ink);color:var(--cream);font-family:Arial,"Noto Sans TC","Microsoft JhengHei",sans-serif}button,a{font:inherit}
|
||||
.hero{position:relative;min-height:100svh;overflow:hidden;background:radial-gradient(circle at 76% 18%,#80583c 0,#243f57 18%,#0a1726 49%,#050b13 100%);transition:1s}.hero.isLit{background:radial-gradient(circle at 73% 23%,#ffc676 0,#355e7c 25%,#0b1d31 54%,#050b13 100%)}
|
||||
nav{position:absolute;z-index:6;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:26px 4vw;border-bottom:1px solid var(--line);font:700 10px Arial;letter-spacing:.28em}.logo{font-size:24px;letter-spacing:-.08em}.logo span{margin-left:7px;color:var(--gold);font-size:12px;vertical-align:top}
|
||||
.light{position:absolute;width:46vw;height:46vw;right:-4vw;top:-12vw;border-radius:50%;background:rgba(255,194,111,.16);filter:blur(30px);transition:1s}.isLit .light{background:rgba(255,202,115,.38);transform:scale(1.15)}
|
||||
.frames{position:absolute;inset:12vh 5vw 8vh 42vw;perspective:900px}.frames i{position:absolute;display:block;width:36%;height:72%;border:1px solid rgba(255,255,255,.42);background:linear-gradient(135deg,rgba(255,255,255,.06),transparent);backdrop-filter:blur(2px);transform:rotateY(-14deg) rotateZ(4deg);box-shadow:0 30px 80px rgba(0,0,0,.24)}.frames i:nth-child(1){left:0;top:12%}.frames i:nth-child(2){left:29%;top:2%}.frames i:nth-child(3){left:58%;top:15%}
|
||||
.noise{position:absolute;inset:0;opacity:.14;pointer-events:none;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E")}
|
||||
.heroText{position:relative;z-index:4;padding:16vh 0 11vh 7vw;width:min(760px,90vw)}.kicker,.sectionTag{font:700 10px/1.5 Arial;letter-spacing:.34em;color:var(--gold)}h1{margin:28px 0 12px;font:900 clamp(76px,12vw,178px)/.72 Arial;letter-spacing:-.09em}h1 span{display:block}.outline{color:transparent;-webkit-text-stroke:1.5px rgba(242,237,226,.75)}
|
||||
.anniversary{display:flex;align-items:center;gap:16px;margin:24px 0}.anniversary strong{font:900 clamp(72px,9vw,132px)/.7 Arial;color:var(--gold);letter-spacing:-.08em}.anniversary p{font:700 16px/1.15 Arial;letter-spacing:.12em}.dates{font:700 12px Arial;letter-spacing:.2em}.dates b{color:var(--gold);margin:0 10px}.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:36px}.actions button{padding:15px 20px;border:1px solid transparent;cursor:pointer;transition:.25s}.primary{background:var(--gold);color:var(--ink);font-weight:800}.secondary{background:transparent;color:var(--cream);border-color:rgba(255,255,255,.5)!important}.actions button:hover{transform:translateY(-3px)}.threeDots{position:absolute;z-index:4;right:4vw;bottom:36px;display:flex;gap:10px}.threeDots b{width:8px;height:8px;border:1px solid var(--cream);border-radius:50%}.threeDots b:first-child{background:var(--gold);border-color:var(--gold)}
|
||||
.statement,.words,.memory{padding:100px 8vw}.statement{display:grid;grid-template-columns:1fr 3fr;gap:6vw;background:var(--gold);color:var(--ink)}.statement .sectionTag{color:var(--ink)}.hangul{font:700 13px Arial;letter-spacing:.24em}.statement h2{font:900 clamp(48px,8vw,112px)/1 serif;margin:20px 0 42px;letter-spacing:-.06em}.bigCopy{font:700 clamp(25px,3.4vw,48px)/1.55 serif;margin:0}
|
||||
.words{background:var(--cream);color:var(--ink)}.words .sectionTag{color:#7c682f}.wordGrid{display:grid;grid-template-columns:repeat(3,1fr);margin-top:65px;border-top:1px solid #999}.wordGrid article{padding:28px 32px 45px 0;margin-right:32px;border-right:1px solid #aaa}.wordGrid span{font-size:11px;color:#786d51}.wordGrid h3{font:900 clamp(44px,7vw,100px)/1 Arial;letter-spacing:-.07em;margin:28px 0 16px}.wordGrid p{color:#59606a;line-height:1.7}
|
||||
.lyricsPreview{position:relative;overflow:hidden;display:grid;grid-template-columns:1fr 2fr;gap:7vw;padding:100px 8vw;background:radial-gradient(circle at 8% 15%,#28455f 0,#102337 32%,#07101d 74%);border-top:1px solid rgba(255,255,255,.12)}.lyricsPreview:after{content:"3";position:absolute;right:-.04em;bottom:-.32em;font:900 min(48vw,680px)/1 Arial;color:rgba(255,255,255,.025);pointer-events:none}.lyricsMeta{position:relative;z-index:1}.lyricsMeta>span{display:block;margin-top:38px;font:900 clamp(38px,5vw,72px)/.95 Arial;letter-spacing:-.06em;color:transparent;-webkit-text-stroke:1px rgba(242,237,226,.5)}.lyricsLines{position:relative;z-index:1;border-left:1px solid rgba(239,181,63,.6);padding-left:clamp(25px,4vw,64px)}.lyricsLines p{margin:0 0 18px;font:700 clamp(22px,3vw,44px)/1.25 serif;letter-spacing:-.02em}.lyricsLines p:nth-child(2n){color:#aebdca}.lyricsLines a{display:inline-flex;align-items:center;gap:22px;margin-top:28px;padding:15px 20px;background:var(--gold);color:var(--ink);font-weight:800;text-decoration:none;transition:.25s}.lyricsLines a:hover{transform:translateY(-3px);box-shadow:0 15px 35px rgba(0,0,0,.28)}.lyricsLines a span{font-size:18px}
|
||||
.memory{background:#0c1a2a}.memory>.sectionTag{display:block;margin-bottom:55px}.memory article{display:grid;grid-template-columns:1fr 2fr;gap:6vw;border-top:1px solid var(--line);padding:40px 0}.memory strong{font:900 clamp(42px,7vw,96px)/1 Arial;color:var(--gold);letter-spacing:-.06em}.memory h3{font:700 clamp(25px,3vw,42px)/1.2 serif;margin:0 0 16px}.memory p{max-width:650px;color:#b9c0c8;line-height:1.8;margin:0}
|
||||
.video{padding:90px 8vw;background:#d9d0bf;color:var(--ink);display:grid;grid-template-columns:minmax(250px,.75fr) minmax(0,1.5fr);align-items:center;gap:7vw}.video .sectionTag{color:#796727}.video h2{font:800 clamp(42px,5vw,76px)/1.12 serif;margin:25px 0 32px}.videoHeading>a{display:inline-block;color:var(--ink);font-weight:800;text-decoration:none;border-bottom:1px solid var(--ink);padding-bottom:5px}.videoHeading>a:hover{color:#796727}.videoFrame{position:relative;width:100%;aspect-ratio:16/9;background:#050b13;box-shadow:0 35px 80px rgba(7,16,29,.25);overflow:hidden}.videoFrame:before{content:"";position:absolute;inset:-1px;border:1px solid rgba(255,255,255,.22);pointer-events:none;z-index:1}.videoFrame iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
|
||||
.stageArchive{padding:100px 8vw;background:var(--cream);color:var(--ink)}.stageIntro{display:grid;grid-template-columns:1fr 1.5fr;gap:20px 6vw;align-items:end;margin-bottom:55px}.stageIntro .sectionTag{color:#796727;grid-column:1/-1}.stageIntro h2{margin:0;font:900 clamp(54px,8vw,108px)/.9 serif;letter-spacing:-.06em}.stageIntro>p:last-child{max-width:470px;margin:0;color:#59606a;line-height:1.8}.stageTable{border-top:2px solid var(--ink)}.stageHead,.stageRow{display:grid;grid-template-columns:.55fr 2fr .8fr;gap:28px;align-items:center}.stageHead{padding:15px 0;border-bottom:1px solid #9e998e;font:700 9px Arial;letter-spacing:.2em;color:#76682f}.stageRow{padding:24px 0;border-bottom:1px solid #aaa59b}.stageRow time{font:900 clamp(22px,2.5vw,38px)/1 Arial;letter-spacing:-.04em}.stageVideo{display:grid;grid-template-columns:minmax(150px,240px) 1fr;gap:24px;align-items:center;color:var(--ink);text-decoration:none}.stageThumb{position:relative;display:block;aspect-ratio:16/9;overflow:hidden;background:#0a1726}.stageThumb img{width:100%;height:100%;object-fit:cover;transition:.35s}.stageThumb b{position:absolute;inset:50% auto auto 50%;translate:-50% -50%;width:46px;height:46px;border-radius:50%;display:grid;place-content:center;padding-left:3px;background:var(--gold);box-shadow:0 8px 25px rgba(0,0,0,.3)}.stageVideo:hover img{transform:scale(1.05)}.stageVideo>span:last-child{display:grid;gap:12px}.stageVideo strong{font:800 clamp(18px,2vw,28px)/1.25 serif}.stageVideo small{font:700 9px Arial;letter-spacing:.15em;color:#796727}.station{display:grid;gap:8px}.station strong{font:900 clamp(30px,4vw,58px)/1 Arial;color:#c48f17}.station span{font-size:12px;color:#59606a}
|
||||
footer{display:flex;justify-content:space-between;gap:20px;padding:26px 4vw;background:#040910;color:#8d97a4;font:10px Arial;letter-spacing:.16em}
|
||||
@media(max-width:720px){.hero{min-height:820px}.frames{inset:12vh -18vw 29vh 36vw;opacity:.8}.heroText{padding:17vh 7vw 8vh}.heroText h1{font-size:72px}.statement{grid-template-columns:1fr;padding:70px 7vw}.words,.memory{padding:75px 7vw}.wordGrid{grid-template-columns:1fr}.wordGrid article{border-right:0;border-bottom:1px solid #aaa;margin:0}.lyricsPreview{grid-template-columns:1fr;padding:75px 7vw}.lyricsMeta>span{margin:22px 0 36px}.lyricsLines{padding-left:24px}.lyricsLines p{font-size:24px}.memory article{grid-template-columns:1fr}.video{grid-template-columns:1fr;padding:65px 7vw;gap:42px}.video h2{font-size:48px}.stageArchive{padding:75px 7vw}.stageIntro{grid-template-columns:1fr}.stageIntro .sectionTag{grid-column:auto}.stageHead{display:none}.stageRow{grid-template-columns:1fr;gap:18px;padding:28px 0}.stageVideo{grid-template-columns:1fr}.stageRow time{font-size:30px}.station{grid-template-columns:auto 1fr;align-items:baseline}.station strong{font-size:38px}footer{flex-direction:column}}
|
||||
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "THREE WORDS · 10th Anniversary",
|
||||
description: "SECHSKIES〈세 단어 (THREE WORDS)〉發行十週年紀念頁:2016.10.07—2026.10.07。",
|
||||
icons: { icon: "/favicon.svg" },
|
||||
openGraph: {
|
||||
title: "THREE WORDS · 10th Anniversary",
|
||||
description: "現在、這裡、我們。十年後,再次相遇。",
|
||||
images: [{ url: "/og.png", width: 1536, height: 1024 }],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return <html lang="zh-Hant"><body>{children}</body></html>;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
const moments = [
|
||||
{ year: "2016.10.07", title: "重新相遇", text: "睽違十六年後,SECHSKIES 以〈세 단어 (THREE WORDS)〉帶回重逢的聲音。" },
|
||||
{ year: "2026.10.07", title: "十年以後", text: "旋律走過十年,當時的感動仍停在現在、這裡、我們之間。" },
|
||||
];
|
||||
|
||||
const stages = [
|
||||
{
|
||||
date: "2016.12.04",
|
||||
videoId: "8HTqr3Wp-R0",
|
||||
station: "SBS",
|
||||
program: "인기가요 · Inkigayo",
|
||||
note: "THREE WORDS · Goodbye Stage",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const [lit, setLit] = useState(false);
|
||||
|
||||
function downloadPoster() {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1080;
|
||||
canvas.height = 1350;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const glow = ctx.createRadialGradient(780, 300, 10, 780, 300, 900);
|
||||
glow.addColorStop(0, "#e0a16b");
|
||||
glow.addColorStop(.34, "#264765");
|
||||
glow.addColorStop(1, "#07101d");
|
||||
ctx.fillStyle = glow;
|
||||
ctx.fillRect(0, 0, 1080, 1350);
|
||||
ctx.strokeStyle = "rgba(255,255,255,.45)";
|
||||
ctx.lineWidth = 2;
|
||||
[150, 390, 630].forEach((x, index) => {
|
||||
ctx.strokeRect(x, 180 + index * 70, 420, 660);
|
||||
});
|
||||
ctx.fillStyle = "#f5efe2";
|
||||
ctx.font = "700 106px Arial";
|
||||
ctx.fillText("THREE", 72, 980);
|
||||
ctx.fillText("WORDS", 72, 1085);
|
||||
ctx.fillStyle = "#efb53f";
|
||||
ctx.font = "700 36px Arial";
|
||||
ctx.fillText("10TH ANNIVERSARY", 76, 1155);
|
||||
ctx.fillStyle = "#f5efe2";
|
||||
ctx.font = "30px Arial";
|
||||
ctx.fillText("2016.10.07 — 2026.10.07", 76, 1225);
|
||||
ctx.font = "26px Arial";
|
||||
ctx.fillText("NOW · HERE · US", 76, 1285);
|
||||
const link = document.createElement("a");
|
||||
link.download = "three-words-10th-anniversary.png";
|
||||
link.href = canvas.toDataURL("image/png");
|
||||
link.click();
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className={`hero ${lit ? "isLit" : ""}`}>
|
||||
<nav>
|
||||
<span className="logo">3W<span>10</span></span>
|
||||
<span>2016 — 2026</span>
|
||||
</nav>
|
||||
|
||||
<div className="light" aria-hidden="true" />
|
||||
<div className="frames" aria-hidden="true"><i /><i /><i /></div>
|
||||
<div className="noise" aria-hidden="true" />
|
||||
|
||||
<div className="heroText">
|
||||
<p className="kicker">SECHSKIES · 세 단어</p>
|
||||
<h1><span>THREE</span><span className="outline">WORDS</span></h1>
|
||||
<div className="anniversary"><strong>10</strong><p>TH<br />ANNIVERSARY</p></div>
|
||||
<p className="dates">2016.10.07 <b>—</b> 2026.10.07</p>
|
||||
<div className="actions">
|
||||
<button className="primary" onClick={() => setLit(!lit)}>{lit ? "讓記憶停在這裡" : "點亮第十年的光"} <span>✦</span></button>
|
||||
<button className="secondary" onClick={downloadPoster}>下載 IG 紀念圖 ↓</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="threeDots"><b /><b /><b /></div>
|
||||
</section>
|
||||
|
||||
<section className="statement">
|
||||
<p className="sectionTag">01 / TEN YEARS LATER</p>
|
||||
<div>
|
||||
<p className="hangul">지금 · 여기 · 우리</p>
|
||||
<h2>現在。這裡。我們。</h2>
|
||||
<p className="bigCopy">有些歌不只是被聽見,<br />而是替一段等待留下名字。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="words">
|
||||
<p className="sectionTag">02 / THREE WORDS</p>
|
||||
<div className="wordGrid">
|
||||
<article><span>01</span><h3>NOW</h3><p>十年後的此刻,重新按下播放。</p></article>
|
||||
<article><span>02</span><h3>HERE</h3><p>回到那個讓我們再次相遇的地方。</p></article>
|
||||
<article><span>03</span><h3>US</h3><p>歌曲仍在,我們也仍然在這裡。</p></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="lyricsPreview">
|
||||
<div className="lyricsMeta">
|
||||
<p className="sectionTag">03 / LYRICS PREVIEW</p>
|
||||
<span>세 단어<br />THREE WORDS</span>
|
||||
</div>
|
||||
<div className="lyricsLines">
|
||||
<p>I just wanna be with you</p>
|
||||
<p>내가 사는 이유</p>
|
||||
<p>다시는 멀리 가지 않을게요</p>
|
||||
<p>I'll always be here for you</p>
|
||||
<p>세월이 지난 후</p>
|
||||
<a href="https://jekkinoopy.github.io/sechskies/extra/lyrics.html" target="_blank" rel="noreferrer">完整歌詞與歌曲資料 <span>↗</span></a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="memory">
|
||||
<p className="sectionTag">04 / FROM THEN TO NOW</p>
|
||||
{moments.map((moment) => <article key={moment.year}><strong>{moment.year}</strong><div><h3>{moment.title}</h3><p>{moment.text}</p></div></article>)}
|
||||
</section>
|
||||
|
||||
<section className="video">
|
||||
<div className="videoHeading">
|
||||
<p className="sectionTag">OFFICIAL MUSIC VIDEO</p>
|
||||
<h2>再聽一次,<br />十年前的三個詞。</h2>
|
||||
<a href="https://www.youtube.com/watch?v=m7Zt_p9S-yg" target="_blank" rel="noreferrer">在 YouTube 開啟 ↗</a>
|
||||
</div>
|
||||
<div className="videoFrame">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/m7Zt_p9S-yg?rel=0"
|
||||
title="SECHSKIES - 세 단어 (THREE WORDS) Official Music Video"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="stageArchive">
|
||||
<div className="stageIntro">
|
||||
<p className="sectionTag">05 / MUSIC SHOW ARCHIVE</p>
|
||||
<h2>打歌舞台</h2>
|
||||
<p>把當年的舞台依日期收好,從十年前重新播放。</p>
|
||||
</div>
|
||||
<div className="stageTable" role="table" aria-label="THREE WORDS 打歌舞台列表">
|
||||
<div className="stageHead" role="row">
|
||||
<span role="columnheader">DATE</span>
|
||||
<span role="columnheader">VIDEO</span>
|
||||
<span role="columnheader">STATION</span>
|
||||
</div>
|
||||
{stages.map((stage) => (
|
||||
<div className="stageRow" role="row" key={stage.videoId}>
|
||||
<time role="cell" dateTime={stage.date}>{stage.date}</time>
|
||||
<a className="stageVideo" role="cell" href={`https://www.youtube.com/watch?v=${stage.videoId}`} target="_blank" rel="noreferrer">
|
||||
<span className="stageThumb">
|
||||
<img src={`https://i.ytimg.com/vi/${stage.videoId}/hqdefault.jpg`} alt={`${stage.program} ${stage.note} 影片縮圖`} />
|
||||
<b>▶</b>
|
||||
</span>
|
||||
<span><strong>{stage.note}</strong><small>WATCH ON YOUTUBE ↗</small></span>
|
||||
</a>
|
||||
<div className="station" role="cell"><strong>{stage.station}</strong><span>{stage.program}</span></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer><span>FAN-MADE 10TH ANNIVERSARY TRIBUTE</span><span>SECHSKIES · THREE WORDS · 2016—2026</span></footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -0,0 +1,6 @@
|
||||
壓縮包包含:
|
||||
十週年網站原始碼
|
||||
CSS 與分享圖
|
||||
最新的打歌舞台表格
|
||||
PHP/MySQL 乙級練習後台
|
||||
沒有包含 node_modules、建置檔或 Git 資料。D 槽原專案目前未被修改。
|
||||
Reference in New Issue
Block a user