commit 203dc0db096efc9b3470268b003ddc49357eaf79
parent b4666e45467dbe63cd5edf28d87f659b9ded7b41
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date: Sat, 7 Feb 2026 22:20:02 +0900
Merge pull request #51 from Sunny-JP/v4-sunny-dev
V4 sunny dev
Diffstat:
9 files changed, 385 insertions(+), 469 deletions(-)
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
@@ -1,37 +0,0 @@
-import Link from 'next/link';
-
-export default function AboutPage() {
- return (
- <div className="p-6">
- <div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 border border-muted">
- <h1 className="text-3xl font-bold mb-6 text-foreground">About</h1>
-
- <div className="space-y-4 font-normal">
- <p>
- 「Cafe Timer」は、ゲーム『ブルーアーカイブ』のカフェ機能を支援する非公式ファンメイドツールです。<br />
- 生徒さんのカフェ訪問タイミングを通知でお知らせし、先生方の業務をサポートします。
- </p>
-
- <h2 className="text-xl font-bold mt-8 mb-4 text-foreground">主な機能</h2>
- <ul className="list-disc pl-5 space-y-2">
- <li>生徒さんの再タップ可能タイミングのカウントダウン</li>
- <li>生徒さんの訪問タイミング(4:00/16:00 JST)のカウントダウン</li>
- <li>招待券のクールタイム管理</li>
- <li>タップ履歴のカレンダー表示</li>
- </ul>
- <h2 className="text-xl font-bold mt-8 mb-4 text-foreground">対応プラットフォーム</h2>
- <p>
- PCおよびスマートフォンの最新ブラウザで動作します。<br />
- PWAに対応しているため、ホーム画面に追加してアプリのように使うことができます。
- </p>
- </div>
-
- <div className="mt-8 pt-4">
- <Link href="/" className="btn-setting inline-block text-center w-full">
- ホームに戻る
- </Link>
- </div>
- </div>
- </div>
- );
-}-
\ No newline at end of file
diff --git a/src/app/faq/page.tsx b/src/app/faq/page.tsx
@@ -1,109 +0,0 @@
-import Link from 'next/link';
-
-export default function FaqPage() {
- return (
- <div className="p-6">
- <div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 border border-muted">
- <h1 className="text-3xl font-bold mb-6 text-foreground">FAQ</h1>
-
- <div className="space-y-4 text-muted-foreground">
- <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
- 🟦 機能
- </h2>
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: 記録を間違えてタップしてしまいました。削除できますか?</h3>
- <p className="font-normal text-sm">
- A: Tap履歴を個別に編集・削除することはできません。データベースへの接続増加を伴うため、現時点では実装予定はありません。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: 追加招待券には対応しますか?</h3>
- <p className="font-normal text-sm">
- A: 今のところは対応していません。が、要望が多ければ検討します。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: Tap履歴のエクスポートはできますか?</h3>
- <p className="font-normal text-sm">
- A: テキストデータなどによるエクスポートは、今のところは対応していません。SNSなどへの共有は、月ごとの画像エクスポート機能をご使用ください。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: The history calendar doesn't match the dates I actually tapped.</h3>
- <p className="font-normal text-sm">
- A: The historical calendar considers the period from 4:00 AM to 3:59 AM the following day (Japan Standard Time) as one day. If you reside in a country using a time zone different from UTC+9, please understand this may not match the actual date you use.</p>
- </div>
-
- <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
- 🟨 システム
- </h2>
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: オフラインでも利用できますか?</h3>
- <p className="font-normal text-sm">
- A: 利用できません。インターネットに接続が必要です。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: アプリを閉じてもタイマー・チケットは維持されますか?</h3>
- <p className="font-normal text-sm">
- A: はい、維持されます。時間はサーバー側に保存されているため、ブラウザを閉じたり、別の端末からアクセスしたりしても、最新の状態が反映されます。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: データのバックアップは必要ですか?</h3>
- <p className="font-normal text-sm">
- A: ログインに使用しているDiscordアカウントに紐づいてクラウド上に保存されるため、ログアウトしてもデータが消えることはありません。ただし、データの永久保存については保証しておりませんので、各自で別途記録しておくことをおすすめします。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: 複数の端末で同時に使えますか?</h3>
- <p className="font-normal text-sm">
- A: 可能ですが、データの整合性を保つために、他方の端末では再読み込みを行ってから操作を開始してください。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: 1つの端末で複数アカウントを管理できますか?</h3>
- <p className="font-normal text-sm">
- A: 可能ですが、本サイトはアカウントを切り替えて使うシステムを実装していませんので、都度ログアウト・ログインが必要となります。また、通知機能は端末ごとに管理されており、1つの端末につき1つのアカウントのみ通知を受信できますのでご了承ください。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: このサービスは無料で使えますか?</h3>
- <p className="font-normal text-sm">
- A: はい、すべての機能を無料でご利用いただけます。ただし、個人開発のサービスであるため、予告なく仕様変更やサービス停止を行う可能性がある点はあらかじめご了承ください。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: 通知が届きません。</h3>
- <p className="font-normal text-sm">
- A: ブラウザの設定で通知が「許可」になっているかご確認ください。また、本サイトの通知設定をやり直してみてください(使い方ガイドのページも参照)。ご利用の端末に起因する通知の不着は、申し訳ありませんが対応いたしかねます。利用規約にもある通り、通知は必ず届くことを保証しておりません。</p>
- </div>
-
- <div className="flex flex-col gap-1">
- <h3 className="font-bold text-foreground">
- Q: Discord以外のログイン方法はありますか?</h3>
- <p className="font-normal text-sm">
- A: 現在はDiscord認証のみに対応しています。本サイトはアカウントを切り替えて使用する設計になっていないため、他の認証システムを導入する予定もありません。</p>
- </div>
- </div>
-
- <div className="mt-8 pt-4">
- <Link href="/" className="btn-setting inline-block text-center w-full">
- ホームに戻る
- </Link>
- </div>
- </div>
- </div>
- );
-}-
\ No newline at end of file
diff --git a/src/app/guide/page.tsx b/src/app/guide/page.tsx
@@ -1,79 +0,0 @@
-import Link from 'next/link';
-
-export default function GuidePage() {
- return (
- <div className="p-6">
- <div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 border border-muted">
- <h1 className="text-3xl font-bold mb-6 text-foreground">使い方ガイド</h1>
-
- <div className="space-y-6 font-normal">
- <section>
- <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
- 🟦 カフェタイマーの使い方
- </h2>
- <ul className="list-disc pl-5 space-y-2">
- <li>
- <strong>Next Tap:</strong> 生徒さんと触れ合うことができるまでの時間をカウントダウンします。「Tap」ボタンを押すと履歴が記録され、カウントダウンが始まります。
- </li>
- <li>
- <strong>Next Call:</strong> 生徒さんを招待するチケット(招待券)のクールタイムを管理します。使用した時刻にボタンを押すと、次の使用可能時刻までのカウントダウンが始まります。
- </li>
- </ul>
- </section>
- <section>
- <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
- 🟨 Tap履歴の使い方
- </h2>
- <ul className="list-disc pl-5 space-y-2">
- <li>
- <strong>SAVE IMAGE:</strong> Tap履歴を月ごとに画像で保存できます。SNSへの投稿などにご利用いただけます。
- </li>
- </ul>
- </section>
- <section>
- <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
- 🟦 履歴と同期
- </h2>
- <p>
- Discordログインにより、タップ履歴がサーバーに保存されます。
- 同じアカウントでログインすると、PCやスマホなど異なる端末でも同じデータを共有・同期できます。
- </p>
- </section>
- <section>
- <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
- 🟨 通知設定について
- </h2>
- <p className="mb-4">
- メニュー内の「通知設定」から、生徒さんと触れ合える時刻をプッシュ通知でお知らせします。
- 4:00/16:00 JSTについては、時刻が固定されている&負荷軽減のため、通知を行いません。<br />
- </p>
- <div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4 text-sm mb-4">
- <p className="font-bold mb-2">ℹ️ お願い</p>
- <p>
- 通知登録されている端末数が増加すると、運営者の大人のカードの出番がやってきてしまいます。
- </p>
- <p className="mt-1">
- 通知登録は1ユーザーあたり2端末までを目安とし、通知が不要な端末は都度登録解除していただくよう、何卒ご協力をお願いいたします。
- </p>
- </div>
- <div className="bg-red-500/10 border border-red-500/50 rounded-lg p-4 text-sm">
- <p className="font-bold mb-2">⚠️ 通知設定がうまくいかない場合</p>
- <p>
- <strong>AdGuard</strong> や <strong>uBlock Origin</strong> などの広告ブロック(コンテンツブロッカー)機能を使用している場合、通知システムが「トラッキング」と誤認され、許可ダイアログが表示されないことがあります。
- </p>
- <p className="mt-1">
- 通知設定を行う際は、<strong>このサイト(ドメイン)を許可リストに追加する</strong>か、<strong>一時的に機能をOFF</strong>にしてからページを再読み込み・通知設定を行ってください。
- </p>
- </div>
- </section>
- </div>
-
- <div className="mt-8 pt-4">
- <Link href="/" className="btn-setting inline-block text-center w-full">
- ホームに戻る
- </Link>
- </div>
- </div>
- </div>
- );
-}-
\ No newline at end of file
diff --git a/src/app/operator/page.tsx b/src/app/operator/page.tsx
@@ -1,66 +0,0 @@
-import Link from 'next/link';
-
-export default function OperatorPage() {
- const updateLogs = [
- { date: '2026-02-05', content: 'Ver.1.0 リリース' },
- ];
-
- return (
- <div className="p-6">
- <div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 border border-muted">
- <h1 className="text-3xl font-bold mb-6 text-foreground">運営者情報・変更履歴</h1>
-
- <div className="space-y-6 text-muted-foreground">
- {/* 運営・開発情報 */}
- <div className="grid grid-cols-1 gap-6">
- <div className="flex flex-col gap-2">
- <h3 className="font-bold text-foreground border-b border-muted pb-1">運営代表・開発</h3>
- <p className="font-normal">
- さにー X: <a href="https://x.com/156miyako" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">@156miyako</a>
- HP: <a href="https://rabbit1.cc" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">https://rabbit1.cc</a>
- </p>
- </div>
- <div className="flex flex-col gap-2">
- <h3 className="font-bold text-foreground border-b border-muted pb-1">開発協力</h3>
- <p className="font-normal">
- ハチか X: <a href="https://x.com/bite_sour_sweet" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">@bite_sour_sweet</a>
- </p>
- </div>
- </div>
-
- {/* 変更履歴セクション */}
- <div className="flex flex-col gap-1 pb-2">
- <h3 className="font-bold text-foreground border-b border-muted pb-1">変更履歴</h3>
- <div className="bg-background/50 rounded-md p-1.5 max-h-48 overflow-y-auto space-y-2 text-sm">
- {updateLogs.map((log, index) => (
- <div key={index} className="flex gap-3 border-b border-muted/30 pb-1.5 last:border-0">
- <span className="font-mono shrink-0">{log.date}</span>
- <p className="text-foreground/80">{log.content}</p>
- </div>
- ))}
- </div>
- </div>
-
- {/* 連絡先 */}
- <div className="flex flex-col gap-2">
- <h3 className="font-bold text-foreground border-b border-muted pb-1">連絡先・フィードバック</h3>
- <p className="text-sm leading-relaxed">
- 不具合報告やご要望は、
- <a href="https://github.com/Sunny-JP/hw-ba-cafe" target="_blank" rel="noopener noreferrer" className="mx-1 hover:underline text-blue-400 font-bold">
- GitHub
- </a>
- または X (Twitter) までお願いいたします。<br />
- 個人開発のため全ての対応は難しい場合がありますが、Pull Requestなど積極的に受け付けています。
- </p>
- </div>
- </div>
-
- <div className="mt-8 pt-4">
- <Link href="/" className="btn-setting inline-block text-center w-full">
- ホームに戻る
- </Link>
- </div>
- </div>
- </div>
- );
-}-
\ No newline at end of file
diff --git a/src/app/page.tsx b/src/app/page.tsx
@@ -10,9 +10,36 @@ import HistoryCalendar from "@/components/HistoryCalendar";
import Settings from "@/components/Settings";
import SidePanel from "@/components/SidePanel";
import { CALENDAR_LIMITS } from "@/lib/timeUtils";
+import { OVERLAY_CONTENTS } from "@/components/pages";
type Tab = 'timer' | 'history';
+const Overlay = ({ contentKey, onClose }: { contentKey: string; onClose: () => void }) => {
+ const content = OVERLAY_CONTENTS[contentKey];
+ 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>
+ </div>
+ </div>
+ );
+};
+
export default function Home() {
const [activeTab, setActiveTab] = useState<Tab>('timer');
const [session, setSession] = useState<any>(null);
@@ -25,18 +52,16 @@ export default function Home() {
const [isDataLoaded, setIsDataLoaded] = useState(false);
const [isAuthChecking, setIsAuthChecking] = useState(true);
const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);
+ const [overlayKey, setOverlayKey] = useState<string | null>(null);
const isInitialFetched = useRef(false);
const fetchMonthlyData = useCallback(async (year: number, month: number) => {
const { data: { session: s } } = await supabase.auth.getSession();
if (!s?.user?.id) return [];
-
try {
const { data, error } = await supabase.rpc('get_taps_by_logical_month', {
- target_user_id: s.user.id,
- target_year: year,
- target_month: month
+ target_user_id: s.user.id, target_year: year, target_month: month
});
if (error) throw error;
return (data || []).map((t: any) => new Date(t.tap_time).getTime());
@@ -50,9 +75,7 @@ export default function Home() {
const targetDate = new Date(year, month - 1, 1);
const minLimit = new Date(CALENDAR_LIMITS.MIN.getFullYear(), CALENDAR_LIMITS.MIN.getMonth(), 1);
const maxLimit = new Date(CALENDAR_LIMITS.MAX.getFullYear(), CALENDAR_LIMITS.MAX.getMonth(), 1);
-
if (targetDate < minLimit || targetDate > maxLimit) return;
-
setCalendarDate(targetDate);
const data = await fetchMonthlyData(year, month);
setCalendarHistory(data);
@@ -61,86 +84,54 @@ export default function Home() {
const loadInitialData = useCallback(async () => {
if (isInitialFetched.current) return;
const { data: { session: s } } = await supabase.auth.getSession();
- if (!s?.user?.id) {
- setIsAuthChecking(false);
- return;
- }
-
+ if (!s?.user?.id) { setIsAuthChecking(false); return; }
isInitialFetched.current = true;
-
try {
const now = new Date();
const jstNow = new Date(now.getTime() + 9 * 60 * 60 * 1000);
let year = jstNow.getUTCFullYear();
let month = jstNow.getUTCMonth() + 1;
-
if (jstNow.getUTCDate() === 1 && jstNow.getUTCHours() < 4) {
- const prev = new Date(jstNow);
- prev.setUTCDate(0);
- year = prev.getUTCFullYear();
- month = prev.getUTCMonth() + 1;
+ const prev = new Date(jstNow); prev.setUTCDate(0);
+ year = prev.getUTCFullYear(); month = prev.getUTCMonth() + 1;
}
-
setCalendarDate(new Date(year, month - 1, 1));
-
const [profileRes, monthlyData] = await Promise.all([
supabase.from('profiles').select('ticket1_time, ticket2_time').eq('id', s.user.id).single(),
fetchMonthlyData(year, month)
]);
-
if (profileRes.data) {
if (profileRes.data.ticket1_time) setTicket1Time(new Date(profileRes.data.ticket1_time));
if (profileRes.data.ticket2_time) setTicket2Time(new Date(profileRes.data.ticket2_time));
}
-
setCalendarHistory(monthlyData);
setTimerHistory(monthlyData);
setIsDataLoaded(true);
- } catch (e) {
- console.error("Load failed:", e);
- isInitialFetched.current = false;
- } finally {
- setIsAuthChecking(false);
- }
+ } finally { setIsAuthChecking(false); }
}, [fetchMonthlyData]);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session: s } }) => {
setSession(s);
- if (s) {
- loadInitialData();
- } else {
- setIsAuthChecking(false);
- }
+ if (s) loadInitialData(); else setIsAuthChecking(false);
});
-
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, s) => {
setSession(s);
- if (s) {
- loadInitialData();
- } else {
- setIsAuthChecking(false);
- setIsDataLoaded(false);
- isInitialFetched.current = false;
- }
+ if (s) loadInitialData(); else { setIsAuthChecking(false); setIsDataLoaded(false); isInitialFetched.current = false; }
});
-
return () => subscription.unsubscribe();
}, [loadInitialData]);
const handleTap = async () => {
if (!session || isSyncing) return;
setIsSyncing(true);
- const now = new Date();
- now.setMilliseconds(0);
+ const now = new Date(); now.setMilliseconds(0);
const ms = now.getTime();
-
setTimerHistory(prev => [...prev, ms]);
const tapJST = new Date(now.getTime() + 9 * 60 * 60 * 1000);
if (tapJST.getUTCFullYear() === calendarDate.getFullYear() && (tapJST.getUTCMonth() + 1) === (calendarDate.getMonth() + 1)) {
setCalendarHistory(prev => [...prev, ms]);
}
-
try {
const { data: { session: curS } } = await supabase.auth.getSession();
if (curS) {
@@ -156,8 +147,7 @@ export default function Home() {
const handleInvite = async (num: 1 | 2) => {
if (!session || isSyncing) return;
setIsSyncing(true);
- const now = new Date();
- now.setMilliseconds(0);
+ const now = new Date(); now.setMilliseconds(0);
if (num === 1) setTicket1Time(now); else setTicket2Time(now);
try {
const { data: { session: curS } } = await supabase.auth.getSession();
@@ -171,25 +161,19 @@ export default function Home() {
} finally { setIsSyncing(false); }
};
- // 1. セッション確認中またはデータロード中のLoading
if (isAuthChecking || (session && !isDataLoaded)) {
- return (
- <div className="flex justify-center items-center h-screen bg-background font-bold text-foreground">
- Loading...
- </div>
- );
+ return <div className="flex justify-center items-center h-screen bg-background font-bold">Loading...</div>;
}
- // 2. 未ログイン時のログイン画面
if (!session) {
return (
- <div className="flex flex-col items-center justify-center min-h-screen bg-background p-8">
- <div className="timer-card text-center bg-card border border-muted p-8 rounded-2xl shadow-lg max-w-sm w-full">
+ <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">
<h2 className="text-2xl font-bold mb-2">Welcome!</h2>
<p className="mb-8 text-muted-foreground text-sm">利用するにはログインしてください</p>
<button
onClick={() => supabase.auth.signInWithOAuth({ provider: 'discord', options: { redirectTo: window.location.origin } })}
- className="w-full py-4 rounded-xl text-lg font-bold bg-[#5865F2] text-white hover:brightness-110 shadow-md transition-all"
+ className="w-full py-4 rounded-xl text-lg font-bold bg-[#5865F2] text-white shadow-md transition-all active:scale-95"
>
Discord Login
</button>
@@ -198,15 +182,19 @@ export default function Home() {
);
}
- // 3. メインコンテンツ
+ const lastTapTime = timerHistory.length ? new Date(timerHistory[timerHistory.length-1]) : null;
+
return (
<div className="bg-background h-screen flex flex-col">
<OneSignalInit />
<Header isLoggedIn={!!session} onMenuClick={() => setIsSidePanelOpen(true)} />
+
+ {overlayKey && <Overlay contentKey={overlayKey} onClose={() => setOverlayKey(null)} />}
+
<main className="flex-1 flex flex-col pt-16 pb-16 min-[1000px]:pb-0">
<div className="min-[1000px]:hidden flex-1">
{activeTab === 'timer' && (
- <TimerDashboard tapHistory={timerHistory} lastTapTime={timerHistory.length ? new Date(timerHistory[timerHistory.length-1]) : null} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} />
+ <TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} />
)}
{activeTab === 'history' && (
<div className="p-4">
@@ -216,12 +204,14 @@ export default function Home() {
</div>
<div className="hidden min-[1000px]:flex flex-1 items-center justify-center p-6 h-[calc(100vh-64px)] overflow-hidden">
<div className="grid grid-cols-2 gap-6 w-full max-w-[160svh] items-stretch mx-auto">
- <TimerDashboard tapHistory={timerHistory} lastTapTime={timerHistory.length ? new Date(timerHistory[timerHistory.length-1]) : null} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} />
+ <TimerDashboard tapHistory={timerHistory} lastTapTime={lastTapTime} ticket1Time={ticket1Time} ticket2Time={ticket2Time} onTap={handleTap} onInvite={handleInvite} isSyncing={isSyncing} isDataLoaded={isDataLoaded} />
<HistoryCalendar tapHistory={calendarHistory} currentDate={calendarDate} onMonthChange={handleMonthChange} />
</div>
</div>
<BottomNavBar activeTab={activeTab} setActiveTab={setActiveTab} />
- <SidePanel isOpen={isSidePanelOpen} onClose={() => setIsSidePanelOpen(false)} title="Settings"><Settings /></SidePanel>
+ <SidePanel isOpen={isSidePanelOpen} onClose={() => setIsSidePanelOpen(false)}>
+ <Settings onOpenContent={(key) => { setOverlayKey(key); setIsSidePanelOpen(false); }} />
+ </SidePanel>
</main>
</div>
);
diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx
@@ -1,41 +0,0 @@
-import Link from 'next/link';
-
-export default function PrivacyPage() {
- return (
- <div className="p-6">
- <div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 border border-muted">
- <h1 className="text-3xl font-bold mb-6 text-foreground">プライバシーポリシー</h1>
-
- <div className="space-y-4 font-normal text-sm">
- <p>当サイト(以下、「当方」)は、ユーザーの個人情報の取扱いについて、以下のとおりプライバシーポリシー(以下、「本ポリシー」)を定めます。</p>
-
- <h3 className="font-bold text-foreground mt-4">1. 収集する情報</h3>
- <ul className="list-disc pl-5">
- <li><b>認証情報</b>: Discordログイン時に提供されるユーザーID、メールアドレス、プロフィール画像。これらはユーザーの識別とデータ保存のためにのみ使用されます。</li>
- <li><b>利用データ</b>: タイマーのタップ履歴、設定情報。</li>
- <li><b>通知トークン</b>: プッシュ通知を送信するために必要なトークン。</li>
- </ul>
-
- <h3 className="font-bold text-foreground mt-4">2. 利用目的</h3>
- <ul className="list-disc pl-5">
- <li>本サービスの提供・運営のため</li>
- <li>通知機能の提供のため</li>
- <li>不正アクセスの防止のため</li>
- </ul>
-
- <h3 className="font-bold text-foreground mt-4">3. 第三者への提供</h3>
- <p>当方は、法令に基づく場合を除き、予めユーザーの同意を得ることなく第三者に個人情報を提供することはありません。ただし、本サービスの基盤としてSupabaseを使用しており、データはSupabaseのサーバー上で管理されます。</p>
-
- <h3 className="font-bold text-foreground mt-4">4. データの削除</h3>
- <p>ユーザーは設定画面の「データ削除」ボタンから、いつでも自身の全データをサーバーから完全に削除することができます。</p>
- </div>
-
- <div className="mt-8 pt-4">
- <Link href="/" className="btn-setting inline-block text-center w-full">
- ホームに戻る
- </Link>
- </div>
- </div>
- </div>
- );
-}-
\ No newline at end of file
diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx
@@ -1,50 +0,0 @@
-import Link from 'next/link';
-
-export default function TermsPage() {
- return (
- <div className="p-6">
- <div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 border border-muted">
- <h1 className="text-3xl font-bold mb-6 text-foreground">利用規約</h1>
-
- <div className="space-y-4 font-normal text-sm">
- <p>この利用規約(以下、「本規約」といいます。)は、本ウェブサイト上で提供するサービス(以下、「本サービス」といいます。)の利用条件を定めるものです。</p>
-
- <h3 className="font-bold text-foreground mt-4">1. 免責事項</h3>
- <ul className="list-disc pl-5">
- <li>本サービスは現状有姿で提供され、その正確性、完全性、有用性を保証するものではありません。</li>
- <li>本サービスの利用により生じた損害(ゲーム内での損失、データ消失など)について、運営者は一切の責任を負いません。</li>
- <li>通知機能はブラウザや設定、端末、ネットワーク環境等に依存するため、確実に届くことを保証するものではありません。</li>
- </ul>
-
- <h3 className="font-bold text-foreground mt-4">2. 禁止事項</h3>
- <ul className="list-disc pl-5">
- <li>本サービスのサーバーに過度な負荷をかける行為<br />(Discordアカウントを複数用いてのログインは、良識の範囲内に限り許可します)</li>
- <li>不正アクセス行為</li>
- <li>本ツールを営利目的に使用する行為</li>
- <li>その他、運営者が不適切と判断する行為</li>
- </ul>
-
- <h3 className="font-bold text-foreground mt-4">3. サービスの変更・停止</h3>
- <ul className="list-disc pl-5">
- <li>運営者は、ユーザーに通知することなく、本サービスの内容を変更または提供を中止することができるものとします。</li>
- </ul>
-
- <h3 className="font-bold text-foreground mt-4">4. 著作権</h3>
- <ul className="list-disc pl-5">
- <li>本サービスは個人によって制作された非公式ツールであり、株式会社Yostar、NEXON Games、およびその他の関連団体とは一切関係ありません。</li>
- <li>本サービス中の『ブルーアーカイブ』に関係するデータ等の著作権は、株式会社Yostar、NEXON Games、およびその他の権利者に帰属します。</li>
- <li>本サービスのファビコンおよびアプリアイコンは、<a href="https://www.flaticon.com/free-icons/coffee-break" title="coffee break icons" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">Freepik社</a> のライセンスの下で使用しています。</li>
- </ul>
-
- <p className="text-right">2026年1月1日 発効</p>
- </div>
-
- <div className="mt-8 pt-4">
- <Link href="/" className="btn-setting inline-block text-center w-full">
- ホームに戻る
- </Link>
- </div>
- </div>
- </div>
- );
-}-
\ No newline at end of file
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
@@ -1,7 +1,6 @@
"use client";
import { useState } from 'react';
-import Link from 'next/link';
import { useAuth, supabase } from "@/hooks/useAuth";
import OneSignal from 'react-onesignal';
@@ -26,18 +25,22 @@ const BellIcon = ({ className = 'h-5 w-5 mr-2' }: { className?: string }) => (
</svg>
);
-const Settings = () => {
+interface SettingsProps {
+ onOpenContent: (key: string) => void;
+}
+
+const Settings = ({ onOpenContent }: SettingsProps) => {
const { isLoggedIn, logout, avatarUrl, displayName } = useAuth();
const [isDeleting, setIsDeleting] = useState(false);
const [isPushLoading, setIsPushLoading] = useState(false);
const menuItems = [
- { label: 'About', path: '/about' },
- { label: '使い方ガイド', path: '/guide' },
- { label: 'FAQ', path: '/faq' },
- { label: '利用規約', path: '/terms' },
- { label: 'プライバシーポリシー', path: '/privacy' },
- { label: '運営者情報・変更履歴', path: '/operator' },
+ { label: 'About', key: 'about' },
+ { label: '使い方ガイド', key: 'guide' },
+ { label: 'FAQ', key: 'faq' },
+ { label: '利用規約', key: 'terms' },
+ { label: 'プライバシーポリシー', key: 'privacy' },
+ { label: '運営者情報・変更履歴', key: 'operator' },
];
const handleNotificationClick = async () => {
@@ -82,7 +85,7 @@ const Settings = () => {
}
};
-const handleDeleteData = async () => {
+ const handleDeleteData = async () => {
const firstConfirm = window.confirm("本当に全データを削除しますか?\nこの操作は取り消せません。");
if (!firstConfirm) return;
@@ -91,13 +94,11 @@ const handleDeleteData = async () => {
setIsDeleting(true);
try {
- const { data: { user } } = await supabase.auth.getUser();
- if (user) {
- await supabase.from('profiles').delete().eq('id', user.id);
- await logout();
- alert("データを削除しました。");
- window.location.href = "/";
- }
+ const { error } = await supabase.rpc('delete_user_account');
+ if (error) throw error;
+ alert("すべてのデータを削除しました。");
+ await logout();
+ window.location.href = "/";
} catch (err: any) {
console.error(err);
alert("削除に失敗しました: " + err.message);
@@ -105,16 +106,20 @@ const handleDeleteData = async () => {
setIsDeleting(false);
}
};
+
return (
<div className="p-4">
<div className="space-y-4">
<ul className="space-y-1">
{menuItems.map((item) => (
- <li key={item.label}>
- <Link href={item.path} className="btn-setting flex items-center p-2 rounded transition-colors">
+ <li key={item.key}>
+ <button
+ onClick={() => onOpenContent(item.key)}
+ className="btn-setting flex items-center w-full p-2 rounded transition-colors"
+ >
<MenuItemIcon />
<span>{item.label}</span>
- </Link>
+ </button>
</li>
))}
</ul>
@@ -149,7 +154,7 @@ const handleDeleteData = async () => {
disabled={isDeleting}
className="btn-setting flex items-center justify-center p-2 rounded transition-opacity active:opacity-70"
>
- {isDeleting ? (<><TrashIcon className="mr-2 opacity-50" /><span>削除中…</span></>) : (<><TrashIcon /><span>データ削除</span></>)}
+ {isDeleting ? (<><TrashIcon /><span>削除中…</span></>) : (<><TrashIcon /><span>データ削除</span></>)}
</button>
<button
onClick={handleLogoutClick}
diff --git a/src/components/pages.tsx b/src/components/pages.tsx
@@ -0,0 +1,308 @@
+"use client";
+
+import React from 'react';
+
+export const OVERLAY_CONTENTS: Record<string, { title: string; body: React.ReactNode }> = {
+ about: {
+ title: "About",
+ body: (
+ <div className="space-y-4 font-normal">
+ <p>
+ 「Cafe Timer」は、ゲーム『ブルーアーカイブ』のカフェ機能を支援する非公式ファンメイドツールです。<br />
+ 生徒さんのカフェ訪問タイミングを通知でお知らせし、先生方の業務をサポートします。
+ </p>
+ <h2 className="text-xl font-bold mt-8 mb-4 text-foreground">主な機能</h2>
+ <ul className="list-disc pl-5 space-y-2">
+ <li>生徒さんの再タップ可能タイミングのカウントダウン</li>
+ <li>生徒さんの訪問タイミング(4:00/16:00 JST)のカウントダウン</li>
+ <li>招待券のクールタイム管理</li>
+ <li>タップ履歴のカレンダー表示</li>
+ </ul>
+ <h2 className="text-xl font-bold mt-8 mb-4 text-foreground">対応プラットフォーム</h2>
+ <p>
+ PCおよびスマートフォンの最新ブラウザで動作します。<br />
+ PWAに対応しているため、ホーム画面に追加してアプリのように使うことができます。
+ </p>
+ </div>
+ ),
+ },
+ guide: {
+ title: "使い方ガイド",
+ body: (
+ <div className="space-y-6 font-normal">
+ <section>
+ <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
+ 🟦 カフェタイマーの使い方
+ </h2>
+ <ul className="list-disc pl-5 space-y-2">
+ <li>
+ <strong>Next Tap:</strong> 生徒さんと触れ合うことができるまでの時間をカウントダウンします。「Tap」ボタンを押すと履歴が記録され、カウントダウンが始まります。
+ </li>
+ <li>
+ <strong>Next Call:</strong> 生徒さんを招待するチケット(招待券)のクールタイムを管理します。使用した時刻にボタンを押すと、次の使用可能時刻までのカウントダウンが始まります。
+ </li>
+ </ul>
+ </section>
+ <section>
+ <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
+ 🟨 Tap履歴の使い方
+ </h2>
+ <ul className="list-disc pl-5 space-y-2">
+ <li>
+ <strong>SAVE IMAGE:</strong> Tap履歴を月ごとに画像で保存できます。SNSへの投稿などにご利用いただけます。
+ </li>
+ </ul>
+ </section>
+ <section>
+ <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
+ 🟦 履歴と同期
+ </h2>
+ <p>
+ Discordログインにより、タップ履歴がサーバーに保存されます。
+ 同じアカウントでログインすると、PCやスマホなど異なる端末でも同じデータを共有・同期できます。
+ </p>
+ </section>
+ <section>
+ <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
+ 🟨 通知設定について
+ </h2>
+ <p className="mb-4">
+ メニュー内の「通知設定」から、生徒さんと触れ合える時刻をプッシュ通知でお知らせします。
+ 4:00/16:00 JSTについては、時刻が固定されている&負荷軽減のため、通知を行いません。<br />
+ </p>
+ <div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4 text-sm mb-4">
+ <p className="font-bold mb-2">ℹ️ お願い</p>
+ <p>
+ 通知登録されている端末数が増加すると、運営者の大人のカードの出番がやってきてしまいます。
+ </p>
+ <p className="mt-1">
+ 通知登録は1ユーザーあたり2端末までを目安とし、通知が不要な端末は都度登録解除していただくよう、何卒ご協力をお願いいたします。
+ </p>
+ </div>
+ <div className="bg-red-500/10 border border-red-500/50 rounded-lg p-4 text-sm">
+ <p className="font-bold mb-2">⚠️ 通知設定がうまくいかない場合</p>
+ <p>
+ AdGuard や uBlock Origin などの広告ブロック(コンテンツブロッカー)機能を使用している場合、通知システムが「トラッキング」と誤認され、許可ダイアログが表示されないことがあります。
+ </p>
+ <p className="mt-1">
+ 通知設定を行う際は、このサイト(ドメイン)を許可リストに追加するか、一時的に機能をOFFにしてからページを再読み込み・通知設定を行ってください。
+ </p>
+ </div>
+ </section>
+ </div>
+ ),
+ },
+ faq: {
+ title: "FAQ",
+ body: (
+ <div className="space-y-4 font-normal">
+ <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
+ 🟦 機能
+ </h2>
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: 記録を間違えてタップしてしまいました。削除できますか?</h3>
+ <p className="font-normal text-sm">
+ A: Tap履歴を個別に編集・削除することはできません。データベースへの接続増加を伴うため、現時点では実装予定はありません。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: 追加招待券には対応しますか?</h3>
+ <p className="font-normal text-sm">
+ A: 今のところは対応していません。が、要望が多ければ検討します。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: Tap履歴のエクスポートはできますか?</h3>
+ <p className="font-normal text-sm">
+ A: テキストデータなどによるエクスポートは、今のところは対応していません。SNSなどへの共有は、月ごとの画像エクスポート機能をご使用ください。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: The history calendar doesn't match the dates I actually tapped.</h3>
+ <p className="font-normal text-sm">
+ A: The historical calendar considers the period from 4:00 AM to 3:59 AM the following day (Japan Standard Time) as one day. If you reside in a country using a time zone different from UTC+9, please understand this may not match the actual date you use.</p>
+ </div>
+
+ <h2 className="text-xl font-bold mb-3 text-foreground border-b border-muted pb-2">
+ 🟨 システム
+ </h2>
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: オフラインでも利用できますか?</h3>
+ <p className="font-normal text-sm">
+ A: 利用できません。インターネットに接続が必要です。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: アプリを閉じてもタイマー・チケットは維持されますか?</h3>
+ <p className="font-normal text-sm">
+ A: はい、維持されます。時間はサーバー側に保存されているため、ブラウザを閉じたり、別の端末からアクセスしたりしても、最新の状態が反映されます。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: データのバックアップは必要ですか?</h3>
+ <p className="font-normal text-sm">
+ A: ログインに使用しているDiscordアカウントに紐づいてクラウド上に保存されるため、ログアウトしてもデータが消えることはありません。ただし、データの永久保存については保証しておりませんので、各自で別途記録しておくことをおすすめします。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: 複数の端末で同時に使えますか?</h3>
+ <p className="font-normal text-sm">
+ A: 可能ですが、データの整合性を保つために、他方の端末では再読み込みを行ってから操作を開始してください。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: 1つの端末で複数アカウントを管理できますか?</h3>
+ <p className="font-normal text-sm">
+ A: 可能ですが、本サイトはアカウントを切り替えて使うシステムを実装していませんので、都度ログアウト・ログインが必要となります。また、通知機能は端末ごとに管理されており、1つの端末につき1つのアカウントのみ通知を受信できますのでご了承ください。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: このサービスは無料で使えますか?</h3>
+ <p className="font-normal text-sm">
+ A: はい、すべての機能を無料でご利用いただけます。ただし、個人開発のサービスであるため、予告なく仕様変更やサービス停止を行う可能性がある点はあらかじめご了承ください。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: 通知が届きません。</h3>
+ <p className="font-normal text-sm">
+ A: ブラウザの設定で通知が「許可」になっているかご確認ください。また、本サイトの通知設定をやり直してみてください(使い方ガイドのページも参照)。ご利用の端末に起因する通知の不着は、申し訳ありませんが対応いたしかねます。利用規約にもある通り、通知は必ず届くことを保証しておりません。</p>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ <h3 className="font-bold text-foreground">
+ Q: Discord以外のログイン方法はありますか?</h3>
+ <p className="font-normal text-sm">
+ A: 現在はDiscord認証のみに対応しています。本サイトはアカウントを切り替えて使用する設計になっていないため、他の認証システムを導入する予定もありません。</p>
+ </div>
+ </div>
+ ),
+ },
+ terms: {
+ title: "利用規約",
+ body: (
+ <div className="space-y-4 font-normal text-sm">
+ <p>この利用規約(以下、「本規約」といいます。)は、本ウェブサイト上で提供するサービス(以下、「本サービス」といいます。)の利用条件を定めるものです。</p>
+
+ <h3 className="font-bold text-foreground mt-4">1. 免責事項</h3>
+ <ul className="list-disc pl-5">
+ <li>本サービスは現状有姿で提供され、その正確性、完全性、有用性を保証するものではありません。</li>
+ <li>本サービスの利用により生じた損害(ゲーム内での損失、データ消失など)について、運営者は一切の責任を負いません。</li>
+ <li>通知機能はブラウザや設定、端末、ネットワーク環境等に依存するため、確実に届くことを保証するものではありません。</li>
+ </ul>
+
+ <h3 className="font-bold text-foreground mt-4">2. 禁止事項</h3>
+ <ul className="list-disc pl-5">
+ <li>本サービスのサーバーに過度な負荷をかける行為<br />(Discordアカウントを複数用いてのログインは、良識の範囲内に限り許可します)</li>
+ <li>不正アクセス行為</li>
+ <li>本ツールを営利目的に使用する行為</li>
+ <li>その他、運営者が不適切と判断する行為</li>
+ </ul>
+
+ <h3 className="font-bold text-foreground mt-4">3. サービスの変更・停止</h3>
+ <ul className="list-disc pl-5">
+ <li>運営者は、ユーザーに通知することなく、本サービスの内容を変更または提供を中止することができるものとします。</li>
+ </ul>
+
+ <h3 className="font-bold text-foreground mt-4">4. 著作権</h3>
+ <ul className="list-disc pl-5">
+ <li>本サービスは個人によって制作された非公式ツールであり、株式会社Yostar、NEXON Games、およびその他の関連団体とは一切関係ありません。</li>
+ <li>本サービス中の『ブルーアーカイブ』に関係するデータ等の著作権は、株式会社Yostar、NEXON Games、およびその他の権利者に帰属します。</li>
+ <li>本サービスのファビコンおよびアプリアイコンは、<a href="https://www.flaticon.com/free-icons/coffee-break" title="coffee break icons" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">Freepik社</a> のライセンスの下で使用しています。</li>
+ </ul>
+
+ <p className="text-right">2026年1月1日 発効</p>
+ </div>
+ ),
+ },
+ privacy: {
+ title: "プライバシーポリシー",
+ body: (
+ <div className="space-y-4 font-normal text-sm">
+ <p>当サイト(以下、「当方」)は、ユーザーの個人情報の取扱いについて、以下のとおりプライバシーポリシー(以下、「本ポリシー」)を定めます。</p>
+
+ <h3 className="font-bold text-foreground mt-4">1. 収集する情報</h3>
+ <ul className="list-disc pl-5">
+ <li><b>認証情報</b>: Discordログイン時に提供されるユーザーID、メールアドレス、プロフィール画像URL。これらはユーザーの識別とデータ保存のためにのみ使用されます。</li>
+ <li><b>利用データ</b>: タイマーのタップ履歴、設定情報。</li>
+ <li><b>通知トークン</b>: プッシュ通知を送信するために必要なトークン。</li>
+ </ul>
+
+ <h3 className="font-bold text-foreground mt-4">2. 利用目的</h3>
+ <ul className="list-disc pl-5">
+ <li>本サービスの提供・運営のため</li>
+ <li>通知機能の提供のため</li>
+ <li>不正アクセスの防止のため</li>
+ </ul>
+
+ <h3 className="font-bold text-foreground mt-4">3. 第三者への提供</h3>
+ <p>当方は、法令に基づく場合を除き、予めユーザーの同意を得ることなく第三者に個人情報を提供することはありません。ただし、本サービスの基盤としてSupabaseを使用しており、データはSupabaseのサーバー上で管理されます。</p>
+
+ <h3 className="font-bold text-foreground mt-4">4. データの削除</h3>
+ <p>ユーザーは設定画面の「データ削除」ボタンから、いつでも自身の全データをサーバーから完全に削除することができます。</p>
+ </div>
+ ),
+ },
+ operator: {
+ title: "運営者情報・変更履歴",
+ body: (() => {
+ const updateLogs = [
+ { date: '2026-02-08', content: '初回リリース' },
+ ];
+
+ return (
+ <div className="space-y-4 font-normal">
+ <div className="grid grid-cols-1 gap-6">
+ <div className="flex flex-col gap-2">
+ <h3 className="font-bold text-foreground border-b border-muted pb-1">運営代表・開発</h3>
+ <p className="font-normal">
+ さにー<span className="ml-2 mr-1">X:</span><a href="https://x.com/156miyako" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">@156miyako</a><br />
+ <span className="ml-11 mr-1">HP:</span><a href="https://rabbit1.cc" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">https://rabbit1.cc</a>
+ </p>
+ </div>
+ <div className="flex flex-col gap-2">
+ <h3 className="font-bold text-foreground border-b border-muted pb-1">開発協力</h3>
+ <p className="font-normal">
+ ハチか<span className="ml-2 mr-1">X:</span><a href="https://x.com/bite_sour_sweet" target="_blank" rel="noopener noreferrer" className="hover:underline text-blue-400">@bite_sour_sweet</a>
+ </p>
+ </div>
+ </div>
+
+ <div className="flex flex-col gap-1 pb-2">
+ <h3 className="font-bold text-foreground border-b border-muted pb-1">変更履歴</h3>
+ <div className="bg-background/50 rounded-md p-1.5 max-h-48 overflow-y-auto space-y-2 text-sm">
+ {updateLogs.map((log, index) => (
+ <div key={index} className="flex gap-3 border-b border-muted/30 pb-1.5 last:border-0">
+ <span className="font-mono shrink-0">{log.date}</span>
+ <p className="text-foreground/80">{log.content}</p>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ <div className="flex flex-col gap-2">
+ <h3 className="font-bold text-foreground border-b border-muted pb-1">連絡先・フィードバック</h3>
+ <p className="text-sm leading-relaxed">
+ 不具合報告やご要望は、
+ <a href="https://github.com/Sunny-JP/hw-ba-cafe" target="_blank" rel="noopener noreferrer" className="mx-1 hover:underline text-blue-400">
+ GitHub
+ </a>
+ または X (Twitter) までお願いいたします。
+ 個人開発のため全てのご意見への対応は難しい場合がありますが、Pull Requestなど積極的に受け付けています。
+ </p>
+ </div>
+ </div>
+ );
+ })(),
+ },
+};+
\ No newline at end of file