diff --git a/docker-compose.yml b/docker-compose.yml index 232bc1e..8a615aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: TZ: Asia/Tokyo volumes: - ./public/thumbnails:/app/public/thumbnails + - ./public/css:/app/public/css db: image: mysql:8.0 volumes: diff --git a/public/css/errors.css b/public/css/errors.css new file mode 100644 index 0000000..c662412 --- /dev/null +++ b/public/css/errors.css @@ -0,0 +1,31 @@ +.error-container { + text-align: center; + margin-top: 100px; + font-family: Arial, sans-serif; +} + +.error-container h1 { + font-size: 5em; + margin-bottom: 20px; + color: #ff6b6b; +} + +.error-container p { + font-size: 1.5em; + margin-bottom: 30px; + color: #333; +} + +.home-link { + display: inline-block; + padding: 10px 20px; + background-color: #007bff; + color: #fff; + text-decoration: none; + border-radius: 5px; + font-size: 1em; +} + +.home-link:hover { + background-color: #0056b3; +} diff --git a/public/css/header.css b/public/css/header.css new file mode 100644 index 0000000..65a76e3 --- /dev/null +++ b/public/css/header.css @@ -0,0 +1,48 @@ +.header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 60px; + background-color: #333; + color: white; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 1000; + box-sizing: border-box; +} + +.header-left a { + color: white; + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + white-space: nowrap; +} + +.header-right { + display: flex; + gap: 20px; + margin-right: 10px; + white-space: nowrap; +} + +.header-link { + color: white; + text-decoration: none; + font-size: 1em; + transition: color 0.2s; +} + +.header-link:hover { + color: #ddd; +} + +@media (max-width: 768px) { + .header-right { + margin-right: 5px; + } +} diff --git a/public/css/schedule.css b/public/css/schedule.css index f283db1..4048a79 100644 --- a/public/css/schedule.css +++ b/public/css/schedule.css @@ -1,59 +1,69 @@ .schedule-container { - width: 80%; - margin: 0 auto; - } - - .date-header { - font-size: 1.5em; - margin: 20px 0; - font-weight: bold; - } - - .banners { - display: flex; - flex-wrap: wrap; - gap: 20px; - } - - .banner { - display: flex; - flex-direction: row; - align-items: center; - text-decoration: none; - color: black; - background-color: #f9f9f9; - border: 1px solid #ddd; - border-radius: 8px; - overflow: hidden; - width: calc(50% - 10px); /* 2列になるように調整 */ - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transition: transform 0.2s, box-shadow 0.2s; - } - - .banner:hover { - transform: scale(1.02); - box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); - } - - .thumbnail img { - width: 150px; - height: auto; - object-fit: cover; - } - - .details { - padding: 10px; - flex: 1; - } - - .time-channel { - font-size: 1em; - margin-bottom: 5px; - color: #333; - } - - .title { - font-size: 1.2em; - font-weight: bold; - color: #555; - } + width: 80%; + margin: 0 auto; +} + +.date-header { + font-size: 1.5em; + margin: 20px 0; + font-weight: bold; + min-width: 550px; +} + +.banners { + display: flex; + flex-wrap: wrap; + gap: 20px; +} +.banner { + display: flex; + flex-direction: row; + align-items: center; + text-decoration: none; + color: black; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 8px; + overflow: hidden; + flex: 1 1 calc(50% - 13px); + max-width: calc(50% - 13px); + min-width: 550px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.banner:hover { + transform: scale(1.02); + box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); +} + +.thumbnail img { + width: 150px; + height: auto; + object-fit: cover; +} + +.details { + padding: 10px; + flex: 1; +} + +.time-channel { + font-size: 1em; + margin-bottom: 5px; + color: #333; +} + +.title { + font-size: 1.2em; + font-weight: bold; + color: #555; +} + +.header-spacer { + height: 60px; +} + +.main-content { + margin-top: 20px; +} diff --git a/public/css/terms.css b/public/css/terms.css new file mode 100644 index 0000000..73b0359 --- /dev/null +++ b/public/css/terms.css @@ -0,0 +1,47 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + } + + .terms-container { + max-width: 800px; + margin: 80px auto; /* ヘッダーの高さを考慮して調整 */ + padding: 20px; + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } + + h1 { + font-size: 2em; + margin-bottom: 20px; + text-align: center; + color: #333333; + } + + h2 { + font-size: 1.5em; + margin-top: 20px; + margin-bottom: 10px; + color: #444444; + border-bottom: 2px solid #ddd; + padding-bottom: 5px; + } + + p { + font-size: 1em; + line-height: 1.6; + margin-bottom: 15px; + color: #555555; + } + + a { + color: #0066cc; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index d1e1e5b..af761d1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import cron from "node-cron"; import express from "express"; import apiRoutes from "./routes/api"; import adminRoutes from "./routes/admin"; +import userRoutes from "./routes/user"; import basicAuth from "express-basic-auth"; import { runMigrations } from "./utils/migrate"; import methodOverride from "method-override"; @@ -46,16 +47,33 @@ const startServer = async () => { // 管理画面ルート app.use("/admin", adminRoutes); + // ユーザー向けルート + app.use("/", userRoutes); + // 管理画面 API app.use("/admin/api", apiRoutes); - // ユーザー向けルート - // トップページ - app.use("/", scheduleRoutes); - // 静的ファイルの提供 app.use("/thumbnails", express.static("public/thumbnails")); app.use("/css", express.static("public/css")); + app.use("/js", express.static("public/js")); + + // エラーハンドリング + app.use((req, res, next) => { + res.status(404).render("errors/404"); + }); + + app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error("Unhandled error:", err); + + if (err.message.includes("permission")) { + res.status(403).render("errors/403"); + } else if (res.statusCode === 503) { + res.status(503).render("errors/503"); + } else { + res.status(500).render("errors/500"); + } + }); // サーバー起動 app.listen(WEB_PORT, () => { diff --git a/src/routes/schedule.ts b/src/routes/schedule.ts index ed5212e..a708a69 100644 --- a/src/routes/schedule.ts +++ b/src/routes/schedule.ts @@ -1,5 +1,6 @@ import express from "express"; import { LiveSchedule, Channel } from "../models"; +import { Op } from "sequelize"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; @@ -11,8 +12,14 @@ const router = express.Router(); router.get("/", async (req, res) => { try { + const now = dayjs().tz("Asia/Tokyo").startOf("day"); // 当日0時 const liveSchedules = await LiveSchedule.findAll({ include: [{ model: Channel }], + where: { + start_time: { + [Op.gte]: now.toDate(), // Sequelize の Op を使用 + }, + }, order: [["start_time", "ASC"]], }); diff --git a/src/routes/user.ts b/src/routes/user.ts new file mode 100644 index 0000000..b9a0782 --- /dev/null +++ b/src/routes/user.ts @@ -0,0 +1,14 @@ +import express from "express"; +import scheduleRoutes from "./schedule"; + +const router = express.Router(); + +// スケジュールページ(トップページ) +router.use("/", scheduleRoutes); + +// 利用規約ページ +router.get("/terms", (req, res) => { + res.render("terms"); +}); + +export default router; diff --git a/views/admin/channels.ejs b/views/admin/channels.ejs index 5e2c0cd..25d8c4b 100644 --- a/views/admin/channels.ejs +++ b/views/admin/channels.ejs @@ -1,49 +1,49 @@ - -
- - -ID | -Name | -Channel Handle | -YouTube ID | -Actions | -
---|
<%= channel.id %> | -<%= channel.name %> | -<%= channel.channel_handle %> | -<%= channel.youtube_id %> | -- - - - - | +ID | +Name | +Channel Handle | +YouTube ID | +Actions |
---|
Welcome to the Admin Dashboard!
- + + + + + + <%- include("../partials/head") %> + +Welcome to the Admin Dashboard!
+ diff --git a/views/admin/new-channel.ejs b/views/admin/new-channel.ejs index 1216f7a..23a2674 100644 --- a/views/admin/new-channel.ejs +++ b/views/admin/new-channel.ejs @@ -1,25 +1,25 @@ - - - - -No upcoming live streams found.
- <% } else { %> -No upcoming live streams found.
+ <% } else { %> +