commit edaf41b4ab2e2464dc31cb89e3b9688f1c6da7c4
parent ebff170695e61f0a70335e09dfa720178cad92a5
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date: Sat, 24 Jan 2026 23:27:16 +0900
Merge pull request #22 from Sunny-JP/v4-sunny-dev
fix api
Diffstat:
2 files changed, 128 insertions(+), 124 deletions(-)
diff --git a/src/app/api/tap/route.ts b/src/app/api/tap/route.ts
@@ -62,23 +62,20 @@ async function cleanupDevices(userId: string, currentId: string) {
const data = await res.json();
const subs = data.subscriptions || (data.identity && data.identity.subscriptions) || [];
- const allPushSubs = subs.filter((s: any) => {
- return String(s.type).includes("Push") || String(s.type) === "1";
- });
-
- const otherDevices = allPushSubs.filter((s: any) => s.id !== currentId);
+ const others = subs.filter((s: any) =>
+ (String(s.type).includes("Push") || String(s.type) === "1") && s.id !== currentId
+ );
- otherDevices.sort((a: any, b: any) => {
+ others.sort((a: any, b: any) => {
const timeA = Number(a.last_active || a.created_at || 0);
const timeB = Number(b.last_active || b.created_at || 0);
return timeB - timeA;
});
- if (allPushSubs.length > 2) {
- const toDelete = otherDevices.slice(1);
-
+ const toDelete = others.slice(1);
+
+ if (toDelete.length > 0) {
for (const sub of toDelete) {
- console.log(`[Cleanup] Removing old subscription to make room: ${sub.id}`);
await fetch(`https://onesignal.com/api/v1/apps/${APP_ID}/subscriptions/${sub.id}`, {
method: "DELETE",
headers: { "Authorization": `Basic ${API_KEY}` }
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
@@ -5,6 +5,7 @@ import Link from 'next/link';
import { useAuth, supabase } from "@/hooks/useAuth";
import OneSignal from 'react-onesignal';
+// --- Icons ---
const LogoutIcon = ({ className = 'h-5 w-5 mr-2' }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
@@ -26,9 +27,7 @@ const BellIcon = ({ className = 'h-5 w-5 mr-2' }: { className?: string }) => (
</svg>
);
-interface SettingsProps {}
-
-const Settings = ({}: SettingsProps) => {
+const Settings = () => {
const { isLoggedIn, logout, avatarUrl, displayName } = useAuth();
const [isDeleting, setIsDeleting] = useState(false);
const [hue, setHue] = useState(120);
@@ -48,7 +47,7 @@ const Settings = ({}: SettingsProps) => {
document.documentElement.style.setProperty('--tap-h', val.toString());
localStorage.setItem('theme-hue', val.toString());
};
-
+
const menuItems = [
{ label: 'About', path: '/about' },
{ label: '使い方ガイド', path: '/guide' },
@@ -57,43 +56,42 @@ const Settings = ({}: SettingsProps) => {
{ label: '運営者情報', path: '/operator' },
];
-const handleNotificationClick = async () => {
+ const handleNotificationClick = async () => {
try {
if (!OneSignal.User) {
- alert("通知システムが読み込まれていません。\n\n「広告ブロッカー」や「コンテンツブロッカー」を使用している場合は、このサイトだけOFFにしてリロードしてください。");
+ alert("通知システムが読み込まれていません。\n広告ブロッカーをOFFにしてリロードしてください。");
return;
}
- const permission = Notification.permission;
-
- if (permission === 'denied') {
- alert("【通知がブロックされています】\n\nブラウザの設定で通知が「ブロック」になっています。\nこのサイトの「通知」を許可(またはリセット)してください。");
+ if (Notification.permission === 'denied') {
+ alert("通知がブロックされています。ブラウザの設定で許可してください。");
return;
}
- const isOptedIn = OneSignal.User.PushSubscription.optedIn;
- const currentSubscriptionId = OneSignal.User.PushSubscription.id; // 現在のデバイスID取得
+ const isOptedIn = OneSignal.User.PushSubscription.optedIn;
- if (isOptedIn) {
- await OneSignal.User.PushSubscription.optOut();
- alert("通知をOFFにしました。");
- } else {
- await OneSignal.Notifications.requestPermission();
- await OneSignal.User.PushSubscription.optIn();
- alert("通知をONにしました!");
- }
+ if (isOptedIn) {
+ await OneSignal.User.PushSubscription.optOut();
+ alert("通知をOFFにしました。");
+ } else {
+ await OneSignal.Notifications.requestPermission();
+ await OneSignal.User.PushSubscription.optIn();
+ alert("通知をONにしました!");
+ }
- // 通知ボタンを押したタイミングで API を叩き、整理を実行させる
- await fetch('/api/tap', { // 実際のパスに合わせてください
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`
- },
- body: JSON.stringify({
- onesignalId: currentSubscriptionId // これをトリガーにする
- })
- });
+ await new Promise(r => setTimeout(r, 1500));
+ const currentSubscriptionId = OneSignal.User.PushSubscription.id;
+
+ await fetch('/api/tap', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`
+ },
+ body: JSON.stringify({
+ onesignalId: currentSubscriptionId
+ })
+ });
} catch (e: any) {
console.error("Notification Setup Error:", e);
@@ -102,15 +100,14 @@ const handleNotificationClick = async () => {
};
const handleLogoutClick = async () => {
- if (window.confirm("ログアウトしますか?\n(データは削除されません)")) {
+ if (window.confirm("ログアウトしますか?")) {
await logout();
window.location.href = '/';
}
};
const handleDeleteData = async () => {
- if (!window.confirm("本当に全データを削除しますか?\n(この操作は取り消せません)")) return;
-
+ if (!window.confirm("本当に全データを削除しますか?")) return;
setIsDeleting(true);
try {
const { data: { user } } = await supabase.auth.getUser();
@@ -129,91 +126,101 @@ const handleNotificationClick = async () => {
};
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">
- <MenuItemIcon />
- <span>{item.label}</span>
- </Link>
- </li>
- ))}
- </ul>
- <div className="p-2">
- <div className="flex justify-between items-center mb-1">
- <label className="text-xs font-mono opacity-60">Calendar Color</label>
- <span className="text-xs font-mono opacity-60">Hue: {hue}</span>
- </div>
- <input
- type="range"
- min="0"
- max="360"
- value={hue}
- onChange={handleHueChange}
- className="w-full h-2 rounded-lg appearance-none cursor-pointer"
- style={{
- background: `linear-gradient(to right,
- hsl(0, 80%, 50%), hsl(60, 80%, 50%), hsl(120, 80%, 50%),
- hsl(180, 80%, 50%), hsl(240, 80%, 50%), hsl(300, 80%, 50%), hsl(360, 80%, 50%))`
- }}
- />
- </div>
- <div className="mt-8 border-t pt-4">
- {isLoggedIn && (
- <>
- <div className="flex items-center gap-4 mb-4 p-2">
- {avatarUrl ? (
- <img src={avatarUrl} alt="user avatar" className="w-10 h-10 rounded-full" />
- ) : (
- <div className="w-10 h-10 rounded-full bg-gray-300 flex items-center justify-center">
- <span className="text-xl">?</span>
- </div>
- )}
- <div className="flex flex-col">
- <span className="font-semibold">{displayName}</span>
- </div>
- </div>
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
- <button
- onClick={handleNotificationClick}
- className="cal-nav flex items-center justify-center p-2 rounded w-full"
+ <div className="p-4">
+ <div className="space-y-4">
+ {/* Menu List */}
+ <ul className="space-y-1">
+ {menuItems.map((item, index) => (
+ <li key={item.label}>
+ <Link
+ href={item.path}
+ className="btn-setting flex items-center p-2 rounded transition-colors"
>
- <BellIcon />
- <span>通知設定</span>
- </button>
-
- <button
- onClick={handleDeleteData}
- disabled={isDeleting}
- className="cal-nav flex items-center justify-center p-2 rounded"
- >
- {isDeleting ? (
- <>
- <TrashIcon className="h-5 w-5 mr-2 opacity-50" />
- <span>削除中…</span>
- </>
+ <MenuItemIcon />
+ <span>{item.label}</span>
+ </Link>
+ </li>
+ ))}
+ </ul>
+
+ {/* Color Theme Picker */}
+ <div className="p-3 rounded-xl">
+ <div className="flex justify-between items-center mb-2">
+ <label className="text-xs font-semibold opacity-70">Theme Color</label>
+ <span className="text-xs font-mono opacity-60">Hue: {hue}</span>
+ </div>
+ <input
+ type="range"
+ min="0"
+ max="360"
+ value={hue}
+ onChange={handleHueChange}
+ className="w-full h-2 rounded-lg appearance-none cursor-pointer"
+ style={{
+ background: `linear-gradient(to right,
+ hsl(0, 80%, 50%), hsl(60, 80%, 50%), hsl(120, 80%, 50%),
+ hsl(180, 80%, 50%), hsl(240, 80%, 50%), hsl(300, 80%, 50%), hsl(360, 80%, 50%))`
+ }}
+ />
+ </div>
+
+ {/* Account Section */}
+ <div className="mt-8 border-t pt-4">
+ {isLoggedIn && (
+ <>
+ <div className="flex items-center gap-4 mb-4 p-2 rounded-lg">
+ {avatarUrl ? (
+ <img src={avatarUrl} alt="avatar" className="w-10 h-10 rounded-full"/>
) : (
- <>
- <TrashIcon />
- <span>データ削除</span>
- </>
+ <div className="w-10 h-10 rounded-full flex items-center justify-center">
+ <span className="text-xl">?</span>
+ </div>
)}
- </button>
- <button
- onClick={handleLogoutClick}
- className="cal-nav flex items-center justify-center p-2 rounded"
- >
- <LogoutIcon />
- <span>ログアウト</span>
- </button>
- </div>
- </>
- )}
+ <div className="flex flex-col">
+ <span className="font-semibold">{displayName}</span>
+ </div>
+ </div>
+
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
+ <button
+ onClick={handleNotificationClick}
+ className="cal-nav flex items-center justify-center p-2 rounded transition-opacity active:opacity-70"
+ >
+ <BellIcon />
+ <span>通知設定</span>
+ </button>
+
+ <button
+ onClick={handleDeleteData}
+ disabled={isDeleting}
+ className="cal-nav 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>
+ </>
+ )}
+ </button>
+
+ <button
+ onClick={handleLogoutClick}
+ className="cal-nav flex items-center justify-center p-2 rounded transition-opacity active:opacity-70"
+ >
+ <LogoutIcon />
+ <span>ログアウト</span>
+ </button>
+ </div>
+ </>
+ )}
+ </div>
</div>
</div>
- </div>
);
};