commit c88e3d4fd04f2ca72381434bc7e8eeb69ef82403
parent 56f0dfe982ed7b790e64cb69652cb9a45fbf362f
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date: Sun, 1 Feb 2026 01:25:48 +0900
Merge pull request #44 from Sunny-JP/v4-sunny-dev
V4 sunny dev
Diffstat:
7 files changed, 144 insertions(+), 75 deletions(-)
diff --git a/README.md b/README.md
@@ -1,5 +1,19 @@
# Cafe Timer
+「Cafe Timer」は、ゲーム『ブルーアーカイブ』のカフェ機能を支援する非公式ファンメイドツールです。<br />
+生徒さんのカフェ訪問タイミングを通知でお知らせし、先生方の業務をサポートします。
+
+### 主な機能
+- 生徒さんの再タップ可能タイミングのカウントダウン
+- 生徒さんの訪問タイミング(4:00/16:00 JST)のカウントダウン
+- 招待券のクールタイム管理
+- タップ履歴のカレンダー表示
+
+### 対応プラットフォーム
+PCおよびスマートフォンの最新ブラウザで動作します。<br />
+PWAに対応しているため、ホーム画面に追加してアプリのように使うことができます。
+<br /><br />
+
## 技術スタック
* **Framework:** Next.js (App Router)
diff --git a/next.config.ts b/next.config.ts
@@ -2,17 +2,17 @@ import type { NextConfig } from "next";
import withPWAInit from "@ducanh2912/next-pwa";
const cspHeader = `
- default-src 'self';
- script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.onesignal.com https://onesignal.com https://static.cloudflareinsights.com;
+default-src 'self';
+ script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.onesignal.com https://onesignal.com https://static.cloudflareinsights.com https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline';
- img-src 'self' blob: data: https://lh3.googleusercontent.com https://cdn.discordapp.com https://*.onesignal.com;
+ img-src 'self' blob: data: https://lh3.googleusercontent.com https://cdn.discordapp.com https://*.onesignal.com https://www.google-analytics.com https://www.googletagmanager.com;
font-src 'self';
- connect-src 'self' https://*.supabase.co wss://*.supabase.co https://*.onesignal.com https://onesignal.com https://cloudflareinsights.com;
+ connect-src 'self' https://*.supabase.co wss://*.supabase.co https://*.onesignal.com https://onesignal.com https://cloudflareinsights.com https://www.google-analytics.com;
object-src 'none';
base-uri 'self';
- form-action 'self';
- frame-ancestors 'none';
upgrade-insecure-requests;
+ frame-ancestors 'none';
+ form-action 'self';
`.replace(/\s{2,}/g, " ").trim();
const nextConfig: NextConfig = {
@@ -48,7 +48,7 @@ const nextConfig: NextConfig = {
],
},
{
- source: '/(.*)',
+ source: '/:path*',
headers: [
{ key: 'Content-Security-Policy', value: cspHeader },
{ key: 'X-Frame-Options', value: 'DENY' },
diff --git a/package-lock.json b/package-lock.json
@@ -9,13 +9,15 @@
"version": "0.1.0",
"dependencies": {
"@ducanh2912/next-pwa": "^10.2.9",
+ "@next/third-parties": "^16.1.6",
"@react-oauth/google": "^0.12.2",
"@supabase/supabase-js": "^2.89.0",
"chart.js": "^4.5.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"html-to-image": "^1.11.13",
- "next": "16.0.10",
+ "lucide-react": "^0.563.0",
+ "next": "^16.1.6",
"react": "19.2.3",
"react-chartjs-2": "^5.3.1",
"react-dom": "19.2.3",
@@ -2309,9 +2311,9 @@
}
},
"node_modules/@next/env": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
- "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
+ "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -2325,9 +2327,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz",
- "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
+ "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
"cpu": [
"arm64"
],
@@ -2341,9 +2343,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz",
- "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
+ "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
"cpu": [
"x64"
],
@@ -2357,9 +2359,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz",
- "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
+ "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
"cpu": [
"arm64"
],
@@ -2373,9 +2375,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz",
- "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
+ "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
"cpu": [
"arm64"
],
@@ -2389,9 +2391,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz",
- "integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
+ "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
"cpu": [
"x64"
],
@@ -2405,9 +2407,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz",
- "integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
+ "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
"cpu": [
"x64"
],
@@ -2421,9 +2423,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz",
- "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
+ "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
"cpu": [
"arm64"
],
@@ -2437,9 +2439,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz",
- "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
+ "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
"cpu": [
"x64"
],
@@ -2452,6 +2454,19 @@
"node": ">= 10"
}
},
+ "node_modules/@next/third-parties": {
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-16.1.6.tgz",
+ "integrity": "sha512-/cLY1egaH529ylSMSK+C8dA3nWDLL4hOFR4fca9OLWWxjcNwzsbuq2pPb/tmdWL9Zj3K1nTjd1pWQoSlaDQ0VA==",
+ "license": "MIT",
+ "dependencies": {
+ "third-party-capital": "1.0.20"
+ },
+ "peerDependencies": {
+ "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0",
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -7128,6 +7143,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.563.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
+ "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -7276,13 +7300,14 @@
"peer": true
},
"node_modules/next": {
- "version": "16.0.10",
- "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
- "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
+ "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.0.10",
+ "@next/env": "16.1.6",
"@swc/helpers": "0.5.15",
+ "baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -7294,14 +7319,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.0.10",
- "@next/swc-darwin-x64": "16.0.10",
- "@next/swc-linux-arm64-gnu": "16.0.10",
- "@next/swc-linux-arm64-musl": "16.0.10",
- "@next/swc-linux-x64-gnu": "16.0.10",
- "@next/swc-linux-x64-musl": "16.0.10",
- "@next/swc-win32-arm64-msvc": "16.0.10",
- "@next/swc-win32-x64-msvc": "16.0.10",
+ "@next/swc-darwin-arm64": "16.1.6",
+ "@next/swc-darwin-x64": "16.1.6",
+ "@next/swc-linux-arm64-gnu": "16.1.6",
+ "@next/swc-linux-arm64-musl": "16.1.6",
+ "@next/swc-linux-x64-gnu": "16.1.6",
+ "@next/swc-linux-x64-musl": "16.1.6",
+ "@next/swc-win32-arm64-msvc": "16.1.6",
+ "@next/swc-win32-x64-msvc": "16.1.6",
"sharp": "^0.34.4"
},
"peerDependencies": {
@@ -8695,6 +8720,12 @@
}
}
},
+ "node_modules/third-party-capital": {
+ "version": "1.0.20",
+ "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz",
+ "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==",
+ "license": "ISC"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
diff --git a/package.json b/package.json
@@ -11,13 +11,15 @@
},
"dependencies": {
"@ducanh2912/next-pwa": "^10.2.9",
+ "@next/third-parties": "^16.1.6",
"@react-oauth/google": "^0.12.2",
"@supabase/supabase-js": "^2.89.0",
"chart.js": "^4.5.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"html-to-image": "^1.11.13",
- "next": "16.0.10",
+ "lucide-react": "^0.563.0",
+ "next": "^16.1.6",
"react": "19.2.3",
"react-chartjs-2": "^5.3.1",
"react-dom": "19.2.3",
diff --git a/src/app/globals.css b/src/app/globals.css
@@ -11,7 +11,7 @@
--secondary-foreground: #5a7d9a;
--muted: #d0e1f0;
--muted-foreground: #8ba8c2;
- --nav-foreground: #607d8b;
+ --nav-foreground: #394a52;
--tap-h: 200;
--tap-s: 100%;
--tap-bg-0: #ffffff;
@@ -32,11 +32,11 @@
--card-foreground: #f5f0f7;
--primary: hsl(var(--tap-h) var(--tap-s) 50%);
--primary-foreground: #f5f0f7;
- --secondary: #d7d5e8;
+ --secondary: #d1d7e9;
--secondary-foreground: #a0abc2;
--muted: #4b5068;
--muted-foreground: #7d8dad;
- --nav-foreground: #96a4bd;
+ --nav-foreground: #a3a5aa;
--tap-h: 235;
--tap-s: 40%;
--tap-bg-0: #0f0e1a;
@@ -89,7 +89,7 @@ body {
color: var(--foreground);
font-family: var(--font-mandali), var(--font-kosugi), "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
font-weight: 700;
- padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom));
+ padding-bottom: calc(var(--bottom-nav-height));
}
.btn-setting {
@@ -110,28 +110,41 @@ body {
.btn-nav {
flex: 1 1 0%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
padding-top: 14px;
- padding-bottom: 14px;
- text-align: center;
- font-size: 4.5cqw;
- line-height: 1.25;
- font-weight: 700;
+ padding-bottom: 16px;
color: var(--nav-foreground);
background-color: var(--background);
transition: all 0.2s ease-in-out;
+ cursor: pointer;
+}
+.btn-nav svg {
+ transition: stroke-width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ stroke-width: 1.5;
+}
+.btn-nav.active svg {
+ stroke-width: 2.2;
}
.btn-nav:hover {
color: var(--primary);
+ opacity: 50%;
}
.dark .btn-nav:hover {
- color: #f5f0f7;
+ color: var(--foreground);
+ opacity: 90%;
}
.btn-nav.active,
.btn-nav:hover.active {
- background-color: var(--primary);
- color: var(--primary-foreground);
+ color: var(--primary);
+ opacity: 100%;
+}
+.dark .btn-nav.active,
+.dark .btn-nav:hover.active {
+ color: var(--foreground);
}
-
.dashboard-container {
container-type: inline-size;
display: flex;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Mandali, Kosugi_Maru } from "next/font/google";
+import { GoogleAnalytics } from '@next/third-parties/google';
import "./globals.css";
import Providers from "./providers";
@@ -65,6 +66,7 @@ export default function RootLayout({
return (
<html lang="ja" className="h-full">
<body className={`${mandali.variable} ${kosugi.variable} h-full`}>
+ <GoogleAnalytics gaId="G-FQSKNGBJ7W" />
<Providers>
{children}
</Providers>
diff --git a/src/components/BottomNavBar.tsx b/src/components/BottomNavBar.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Timer, CalendarDays } from 'lucide-react';
type Tab = 'timer' | 'history';
@@ -8,27 +9,32 @@ interface BottomNavBarProps {
}
const BottomNavBar: React.FC<BottomNavBarProps> = ({ activeTab, setActiveTab }) => {
- const tabs: { id: Tab; label: string }[] = [
- { id: 'timer', label: 'Timer' },
- { id: 'history', label: 'History' },
+ const tabs = [
+ { id: 'timer' as const, icon: Timer },
+ { id: 'history' as const, icon: CalendarDays },
];
return (
<div
- className="fixed bottom-0 left-0 right-0 border-t desktop-hidden flex items-center"
+ className="fixed bottom-0 left-0 right-0 desktop-hidden"
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
>
- {tabs.map((tab) => (
- <button
- key={tab.id}
- onClick={() => setActiveTab(tab.id)}
- className={`btn-nav ${activeTab === tab.id ? 'active' : ''}`}
- >
- {tab.label}
- </button>
- ))}
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.id;
+
+ return (
+ <button
+ key={tab.id}
+ onClick={() => setActiveTab(tab.id)}
+ className={`btn-nav ${isActive ? 'active' : ''}`}
+ >
+ <Icon size={30} />
+ </button>
+ );
+ })}
</div>
);
};
-export default BottomNavBar;
+export default BottomNavBar;+
\ No newline at end of file