commit 0710fa6bfe41f1df47238b74300b12fa0666caad
parent 2d4a3b5ca9003ad2572f2cc70caf5ebf39f21eb3
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:15:05 +0900
Merge pull request #53 from Sunny-JP/v4-sunny-dev
V4 sunny dev
Diffstat:
4 files changed, 107 insertions(+), 56 deletions(-)
diff --git a/README.md b/README.md
@@ -2,27 +2,30 @@
「Cafe Timer」は、ゲーム『ブルーアーカイブ』のカフェ機能を支援する非公式ファンメイドツールです。<br />
生徒さんのカフェ訪問タイミングを通知でお知らせし、先生方の業務をサポートします。
-
+
### 主な機能
-- 生徒さんの再タップ可能タイミングのカウントダウン
-- 生徒さんの訪問タイミング(4:00/16:00 JST)のカウントダウン
-- 招待券のクールタイム管理
-- タップ履歴のカレンダー表示
+- **高精度カウントダウン**: 生徒さんの再タップタイミング(3時間)および訪問タイミング(4:00/16:00 JST)をカウントダウン。
+- **招待券管理**: 招待券使用後のクールタイムを可視化。
+- **タップ履歴のアーカイブ**: 月ごとのタップ履歴をカレンダー形式で確認。画像として保存・共有も可能。
+- **プッシュ通知**: ブラウザを閉じていても、OneSignal 連携により次のタイミングを通知。
### 対応プラットフォーム
-PCおよびスマートフォンの最新ブラウザで動作します。<br />
-PWAに対応しているため、ホーム画面に追加してアプリのように使うことができます。
+PCおよびスマートフォンの最新ブラウザに対応。<br />
+PWA (Progressive Web App) に対応しているため、ホーム画面に追加することでネイティブアプリのような体験を提供します。
<br /><br />
## 技術スタック
-* **Framework:** Next.js (App Router)
-* **Deployment:** Cloudflare Pages
-* **Build Tool:** @cloudflare/next-on-pages
-* **PWA:** @ducanh2912/next-pwa
-* **Database:** Supabase
-* **Push Notification:** OneSignal
-* **Styling:** Tailwind CSS
+| カテゴリ | 技術 |
+| :------------ | :-------------------------------- |
+| Framework | Next.js (App Router) |
+| Styling | Tailwind CSS |
+| Database/Auth | Supabase (PostgreSQL) |
+| Notification | OneSignal SDK |
+| Deployment | Cloudflare Pages |
+| Build Tool | @cloudflare/next-on-pages |
+| PWA | @ducanh2912/next-pwa |
+<br />
## セットアップ & 開発
@@ -31,33 +34,51 @@ PWAに対応しているため、ホーム画面に追加してアプリのよ
npm install
```
-### 2. 開発サーバーの起動
+### 2. 環境変数の設定
+`.env.local` を作成し、以下のキーを設定してください。
+
+- `NEXT_PUBLIC_SUPABASE_URL`
+- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
+- `NEXT_PUBLIC_ONESIGNAL_APP_ID`
+- `ONESIGNAL_REST_API_KEY`
+
+### 3. 開発サーバーの起動
```
npm run dev
```
+<br />
## ビルド & デプロイ
-Cloudflare Pages でのビルドには特定の設定が必要です。
+Cloudflare Pages での動作に最適化されています。
### Cloudflare Pages ビルド設定
- Build command: `npx @cloudflare/next-on-pages@1`
- Build output directory: `.vercel/output/static`
-- Compatibility Date: `2024-12-30`
-- Compatibility Flags: `nodejs_compat`
-
-### 設定
-1. output 設定の禁止: `next.config.ts`で`output: 'standalone'`や`output: 'export'`は設定しません。設定すると、Cloudflare 向けのビルドプロセスと競合し、404エラーの原因となります。
-2. Edge Runtime の指定: APIルート(`src/app/api/**/route.ts`)を Cloudflare Workers 上で動作させるため、各ファイルに以下の指定が必要です。
-``` TypeScript
-export const runtime = 'edge';
-```
-
-3. Webpack の明示的使用: Next.js 16 + PWA プラグイン環境において、ビルドの整合性を保つために`package.json`で Webpack モードを指定しています。
-``` JSON
-"build": "next build --webpack"
-```
+## 主なディレクトリ構造
+- `src/app/`: Next.js メインロジック
+ - `api/tap/route.ts`: カフェタップ記録、プロフィール更新、OneSignal 通知予約を行う Edge Runtime API
+ - `layout.tsx`: アプリケーションの共通レイアウト構造
+ - `page.tsx`: メインのダッシュボード画面。タブ切り替えやオーバーレイ表示を管理
+ - `providers.tsx`: 認証やテーマなどのコンテキストプロバイダー
+ - `manifest.ts`: PWA 設定を定義するマニフェストファイル
+ - `globals.css`: スタイル定義
+- `src/components/`: 再利用可能な UI コンポーネント
+ - `Header.tsx` / `BottomNavBar.tsx`: ナビゲーション UI
+ - `TimerDashboard.tsx`: カウントダウン表示とタップ操作のメイン UI
+ - `HistoryCalendar.tsx`: 月別タップ履歴を表示するカレンダー
+ - `Settings.tsx`: 通知設定、データ削除、ログアウト、各種情報へのリンクを管理
+ - `SidePanel.tsx`: 設定画面などを表示するスライドパネル
+ - `pages.tsx`: 利用規約やプライバシーポリシーなどの定型文コンテンツを格納
+ - `OneSignalInit.tsx`: OneSignal SDK の初期化コンポーネント
+- `src/hooks/`: カスタムフック
+ - `useAuth.tsx`: Supabase Auth を利用した認証状態の管理
+ - `useTheme.tsx`: ダークモードなどのテーマ切り替え管理
+- `src/lib/`: ユーティリティと定数
+ - `timeUtils.ts`: ブルーアーカイブの仕様(4:00/16:00更新)に基づいた時刻計算ロジック
+ - `messages.ts`: プッシュ通知で使用するランダムメッセージ集
+- `public/`: アイコン画像や静的アセット
+<br /><br />
-## ディレクトリ構造
-- `src/app/api/`: Cloudflare Workers (Edge Runtime) 上で動作する API エンドポイント
-- `public/`: アイコン、マニフェスト、OneSignal SDK 等の静的アセット
+## 免責事項
+本ツールは個人による非公式のファンメイド作品であり、株式会社NEXON、株式会社Yostar、および『ブルーアーカイブ』公式とは一切関係ありません。+
\ No newline at end of file
diff --git a/src/app/globals.css b/src/app/globals.css
@@ -105,7 +105,13 @@ body {
transition: all 0.2s ease-in-out;
cursor: pointer;
}
-.btn-setting:hover {
+.btn-close {
+ color: var(--secondary-foreground);
+ transition: all 0.2s ease-in-out;
+ cursor: pointer;
+}
+.btn-setting:hover,
+.btn-close:hover {
color: var(--secondary);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
@@ -19,22 +19,26 @@ const Overlay = ({ contentKey, onClose }: { contentKey: string; onClose: () => v
if (!content) return null;
return (
- <div className="fixed inset-0 bg-(--background) z-100 overflow-y-auto">
- <div className="max-w-4xl mx-auto p-8 pt-12">
- <div className="bg-card rounded-lg shadow-md p-8 border border-muted">
- <h1 className="text-3xl font-bold mb-6 text-foreground">{content.title}</h1>
- <div className="text-foreground">
- {content.body}
- </div>
- <div className="mt-8 pt-4">
- <button
- onClick={onClose}
- className="btn-setting inline-block text-center w-full"
- >
- <span>閉じる</span>
- </button>
- </div>
+ <div className="fixed inset-0 bg-(--background) z-100 flex justify-center items-start p-6">
+ <div className="w-full max-w-4xl max-h-[93svh] mt-2 rounded-2xl shadow-xl border flex flex-col overflow-hidden">
+ <div className="border-b border-dashed flex justify-between items-center z-20">
+ <h1 className="text-2xl px-6 py-4 font-bold truncate mr-4">
+ {content.title}
+ </h1>
+ <button
+ onClick={onClose}
+ className="btn-close px-6 py-4 cursor-pointer"
+ aria-label="Close"
+ >
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+ </svg>
+ </button>
+ </div>
+ <div className="flex-1 overflow-y-auto p-6 font-normal">
+ {content.body}
</div>
+ <div className="h-2" />
</div>
</div>
);
@@ -122,6 +126,25 @@ export default function Home() {
return () => subscription.unsubscribe();
}, [loadInitialData]);
+ useEffect(() => {
+ const RELOAD_THRESHOLD = 3;
+ const RELOAD_INTERVAL = 5000;
+ const now = Date.now();
+ const lastReload = sessionStorage.getItem('last_reload_time');
+ const reloadCount = parseInt(sessionStorage.getItem('reload_count') || '0');
+ if (lastReload && now - parseInt(lastReload) < RELOAD_INTERVAL) {
+ const newCount = reloadCount + 1;
+ sessionStorage.setItem('reload_count', newCount.toString());
+ if (newCount >= RELOAD_THRESHOLD) {
+ alert("短時間に連続してリロードされています。サーバ負荷軽減のため、しばらく時間を置いてから操作してください。");
+ sessionStorage.setItem('reload_count', '0');
+ }
+ } else {
+ sessionStorage.setItem('reload_count', '1');
+ }
+ sessionStorage.setItem('last_reload_time', now.toString());
+ }, []);
+
const handleTap = async () => {
if (!session || isSyncing) return;
setIsSyncing(true);
@@ -168,7 +191,7 @@ export default function Home() {
if (!session) {
return (
<div className="flex flex-col items-center justify-center min-h-screen p-8 bg-background">
- <div className="timer-card text-center bg-card p-8 rounded-2xl shadow-lg max-w-sm w-full border border-muted">
+ <div className="timer-card text-center p-8 rounded-2xl shadow-lg max-w-sm w-full border border-muted">
<h2 className="text-2xl font-bold mb-2">Welcome!</h2>
<p className="mb-8 text-muted-foreground text-sm">利用するにはログインしてください</p>
<button
diff --git a/src/components/pages.tsx b/src/components/pages.tsx
@@ -110,7 +110,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
<h3 className="font-bold text-foreground">
Q: 追加招待券には対応しますか?</h3>
<p className="font-normal text-sm">
- A: 今のところは対応していません。が、要望が多ければ検討します。</p>
+ A: 今のところは対応していません。</p>
</div>
<div className="flex flex-col gap-1">
@@ -155,7 +155,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
<h3 className="font-bold text-foreground">
Q: 複数の端末で同時に使えますか?</h3>
<p className="font-normal text-sm">
- A: 可能ですが、データの整合性を保つために、他方の端末では再読み込みを行ってから操作を開始してください。</p>
+ A: 複数端末でお使いいただけますが、複数端末での同時操作はデータの不整合を引き起こす可能性があります。データの整合性を保つために、他方の端末では再読み込みを行ってから操作を開始してください。</p>
</div>
<div className="flex flex-col gap-1">
@@ -192,7 +192,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
title: "利用規約",
body: (
<div className="space-y-4 font-normal text-sm">
- <p>この利用規約(以下、「本規約」といいます。)は、本ウェブサイト上で提供するサービス(以下、「本サービス」といいます。)の利用条件を定めるものです。</p>
+ <p>この利用規約は、本ウェブサイト上で提供するサービス(以下、「本サービス」といいます。)の利用条件を定めるものです。</p>
<h3 className="font-bold text-foreground mt-6">1. 免責事項</h3>
<ul className="list-disc pl-5">
@@ -205,7 +205,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
<ul className="list-disc pl-5">
<li>本サービスのサーバーに過度な負荷をかける行為</li>
<li>不正アクセス行為</li>
- <li>本ツールを営利目的に使用する行為</li>
+ <li>本サービスを営利目的に使用する行為</li>
<li>日本国における法令に反する行為</li>
<li>その他、運営者が不適切と判断する行為</li>
<li>禁止事項に違反した場合は、アカウント及び保存されたデータの削除、IPアドレス遮断などの相当な措置を行うことがあります。</li>
@@ -231,7 +231,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
title: "プライバシーポリシー",
body: (
<div className="space-y-4 font-normal text-sm">
- <p>当サイト(以下、「当方」)は、ユーザーの個人情報の取扱いについて、以下のとおりプライバシーポリシー(以下、「本ポリシー」)を定めます。</p>
+ <p>当サイトは、ユーザーの個人情報の取扱いについて、以下のとおりプライバシーポリシーを定めます。</p>
<h3 className="font-bold text-foreground mt-6">1. 収集する情報</h3>
<ul className="list-disc pl-5">
@@ -258,7 +258,7 @@ export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.React
</ul>
<h3 className="font-bold text-foreground mt-6">4. 第三者への提供</h3>
- <p>当方は、法令に基づく場合を除き、予めユーザーの同意を得ることなく第三者に個人情報を提供することはありません。ただし、本サービスの基盤としてSupabaseを使用しており、データはSupabaseのサーバー上で管理されます。</p>
+ <p>当サイトは、法令に基づく場合を除き、予めユーザーの同意を得ることなく第三者に個人情報を提供することはありません。ただし、本サービスの基盤としてSupabaseを使用しており、データはSupabaseのサーバー上で管理されます。</p>
<h3 className="font-bold text-foreground mt-6">5. データの削除</h3>
<p>ユーザーは設定画面の「データ削除」ボタンから、いつでも自身の全データをサーバーから完全に削除することができます。なお、アクセス解析によって収集された匿名情報については、本操作の対象外となります。</p>