Settings.tsx (6937B)
1 "use client"; 2 3 import { useState } from 'react'; 4 import { useAuth, supabase } from "@/hooks/useAuth"; 5 import OneSignal from 'react-onesignal'; 6 7 const LogoutIcon = ({ className = 'h-5 w-5 mr-2' }: { className?: string }) => ( 8 <svg xmlns="http://www.w3.org/2000/svg" className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> 9 <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" /> 10 </svg> 11 ); 12 const MenuItemIcon = () => ( 13 <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-3 text-gray-500" fill="none" viewBox="0 1 24 24" stroke="currentColor" strokeWidth={2}> 14 <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /> 15 </svg> 16 ); 17 const TrashIcon = ({ className = 'h-5 w-5 mr-2' }: { className?: string }) => ( 18 <svg xmlns="http://www.w3.org/2000/svg" className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> 19 <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V5a2 2 0 012-2h2a2 2 0 012 2v2M7 7h10" /> 20 </svg> 21 ); 22 const BellIcon = ({ className = 'h-5 w-5 mr-2' }: { className?: string }) => ( 23 <svg xmlns="http://www.w3.org/2000/svg" className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> 24 <path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> 25 </svg> 26 ); 27 28 interface SettingsProps { 29 onOpenContent: (key: string) => void; 30 } 31 32 const Settings = ({ onOpenContent }: SettingsProps) => { 33 const { isLoggedIn, logout, avatarUrl, displayName } = useAuth(); 34 const [isDeleting, setIsDeleting] = useState(false); 35 const [isPushLoading, setIsPushLoading] = useState(false); 36 37 const menuItems = [ 38 { label: 'About', key: 'about' }, 39 { label: '使い方ガイド', key: 'guide' }, 40 { label: 'FAQ', key: 'faq' }, 41 { label: '利用規約', key: 'terms' }, 42 { label: 'プライバシーポリシー', key: 'privacy' }, 43 { label: '運営者情報など', key: 'operator' }, 44 ]; 45 46 const handleNotificationClick = async () => { 47 if (isPushLoading) return; 48 setIsPushLoading(true); 49 50 try { 51 if (typeof window === 'undefined' || !OneSignal.User) return; 52 53 const isAllowed = await OneSignal.Notifications.requestPermission(); 54 if (!isAllowed) { 55 alert("通知がブロックされています。ブラウザの設定で許可してください。"); 56 return; 57 } 58 59 const { data: { session } } = await supabase.auth.getSession(); 60 if (session?.user) { 61 await OneSignal.login(session.user.id); 62 } 63 64 const isOptedIn = OneSignal.User.PushSubscription.optedIn; 65 if (isOptedIn) { 66 await OneSignal.User.PushSubscription.optOut(); 67 alert("この端末の通知をOFFにしました。"); 68 } else { 69 await OneSignal.User.PushSubscription.optIn(); 70 alert("この端末の通知をONにしました!"); 71 } 72 73 } catch (e) { 74 console.error("Notification setting error:", e); 75 alert("設定の切り替えに失敗しました。"); 76 } finally { 77 setIsPushLoading(false); 78 } 79 }; 80 81 const handleLogoutClick = async () => { 82 if (window.confirm("ログアウトしますか?")) { 83 await logout(); 84 window.location.href = '/'; 85 } 86 }; 87 88 const handleDeleteData = async () => { 89 const firstConfirm = window.confirm("本当に全データを削除しますか?\nこの操作は取り消せません。"); 90 if (!firstConfirm) return; 91 92 const secondConfirm = window.confirm("本当に、すべての記録を完全に消去してもよろしいですか?\nこの操作は取り消せません。"); 93 if (!secondConfirm) return; 94 95 setIsDeleting(true); 96 try { 97 const { error } = await supabase.rpc('delete_user_account'); 98 if (error) throw error; 99 alert("すべてのデータを削除しました。"); 100 await logout(); 101 window.location.href = "/"; 102 } catch (err: any) { 103 console.error(err); 104 alert("削除に失敗しました: " + err.message); 105 } finally { 106 setIsDeleting(false); 107 } 108 }; 109 110 return ( 111 <div className="p-4"> 112 <div className="space-y-4"> 113 <ul className="space-y-1"> 114 {menuItems.map((item) => ( 115 <li key={item.key}> 116 <button 117 onClick={() => onOpenContent(item.key)} 118 className="btn-setting flex items-center w-full p-2 rounded transition-colors" 119 > 120 <MenuItemIcon /> 121 <span>{item.label}</span> 122 </button> 123 </li> 124 ))} 125 </ul> 126 127 <div className="mt-8 border-t pt-4"> 128 {isLoggedIn && ( 129 <> 130 <div className="flex items-center gap-4 mb-4 p-2 rounded-lg"> 131 {avatarUrl ? ( 132 <img src={avatarUrl} alt="avatar" className="w-10 h-10 rounded-full"/> 133 ) : ( 134 <div className="w-10 h-10 rounded-full flex items-center justify-center bg-gray-200"> 135 <span className="text-xl">?</span> 136 </div> 137 )} 138 <div className="flex flex-col"> 139 <span className="font-semibold">{displayName}</span> 140 </div> 141 </div> 142 143 <div className="grid grid-cols-1 gap-4"> 144 <button 145 onClick={handleNotificationClick} 146 disabled={isPushLoading} 147 className="btn-setting flex items-center justify-center p-2 rounded transition-opacity active:opacity-70 disabled:opacity-50" 148 > 149 <BellIcon /> 150 <span>{isPushLoading ? '設定中...' : '通知設定'}</span> 151 </button> 152 <button 153 onClick={handleDeleteData} 154 disabled={isDeleting} 155 className="btn-setting flex items-center justify-center p-2 rounded transition-opacity active:opacity-70" 156 > 157 {isDeleting ? (<><TrashIcon /><span>削除中…</span></>) : (<><TrashIcon /><span>データ削除</span></>)} 158 </button> 159 <button 160 onClick={handleLogoutClick} 161 className="btn-setting flex items-center justify-center p-2 rounded transition-opacity active:opacity-70" 162 > 163 <LogoutIcon /> 164 <span>ログアウト</span> 165 </button> 166 </div> 167 </> 168 )} 169 </div> 170 </div> 171 </div> 172 ); 173 }; 174 175 export default Settings;