ba-cafe

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 05489e5946838385840a4797bdf1a189c2369685
parent d7fae29c3ec64935fa70871bf85d7908fe8e0837
Author: Sunny <122193933+Sunny-JP@users.noreply.github.com>
Date:   Tue, 27 Jan 2026 00:40:59 +0900

Merge pull request #28 from Sunny-JP/v4-sunny-dev

V4 sunny dev
Diffstat:
Msrc/app/api/tap/route.ts | 117++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/app/globals.css | 8++------
Msrc/components/Settings.tsx | 55+++++++++----------------------------------------------
3 files changed, 85 insertions(+), 95 deletions(-)

diff --git a/src/app/api/tap/route.ts b/src/app/api/tap/route.ts @@ -5,67 +5,98 @@ import { shouldScheduleNotification } from '@/lib/timeUtils'; export const runtime = 'edge'; -const APP_ID = process.env.NEXT_PUBLIC_ONESIGNAL_APP_ID; -const API_KEY = process.env.ONESIGNAL_REST_API_KEY; - export async function POST(request: Request) { try { - const { tapTime, onesignalId, ticket1Time, ticket2Time } = await request.json(); + const body = await request.json(); + const { tapTime, onesignalId, ticket1Time, ticket2Time } = body; const authHeader = request.headers.get('Authorization'); - if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 }); - const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { global: { headers: { Authorization: authHeader } } } + { + global: { + headers: { + Authorization: authHeader ?? '', + }, + }, + } ); + if (!authHeader) return NextResponse.json({ error: 'No token' }, { status: 401 }); + const { data: { user }, error: authError } = await supabase.auth.getUser(); - if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + if (authError || !user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } - const { data: profile } = await supabase.from('profiles').select('tap_history').eq('id', user.id).single(); - const newHistory = tapTime ? [...(profile?.tap_history || []), tapTime] : (profile?.tap_history || []); + const { data: profile } = await supabase + .from('profiles') + .select('tap_history') + .eq('id', user.id) + .single(); - const { error: upsertError } = await supabase.from('profiles').upsert({ + let newHistory = [...(profile?.tap_history || [])]; + if (tapTime) newHistory.push(tapTime); + + const upsertData: any = { id: user.id, - updated_at: new Date().toISOString(), - tap_history: newHistory, - onesignal_id: onesignalId || undefined, - ticket1_time: ticket1Time ? new Date(ticket1Time).toISOString() : (ticket1Time === null ? null : undefined), - ticket2_time: ticket2Time ? new Date(ticket2Time).toISOString() : (ticket2Time === null ? null : undefined), - }); + updated_at: new Date().toISOString() + }; + + if (tapTime) upsertData.tap_history = newHistory; + if (onesignalId) upsertData.onesignal_id = onesignalId; + if (ticket1Time !== undefined) upsertData.ticket1_time = ticket1Time ? new Date(ticket1Time).toISOString() : null; + if (ticket2Time !== undefined) upsertData.ticket2_time = ticket2Time ? new Date(ticket2Time).toISOString() : null; - if (upsertError) throw new Error(upsertError.message); + const { error: upsertError } = await supabase + .from('profiles') + .upsert(upsertData); - if (tapTime && shouldScheduleNotification(new Date(tapTime))) { - await scheduleNotification(user.id, tapTime); + if (upsertError) { + console.error("DB Upsert Error:", upsertError); + throw new Error(upsertError.message); + } + + if (tapTime) { + if (shouldScheduleNotification(new Date(tapTime))) { + const sendAfter = new Date(tapTime); + sendAfter.setHours(sendAfter.getHours() + 3); // Production + // sendAfter.setSeconds(sendAfter.getSeconds() + 180); // Testing + + const randomMsg = messages + ? messages[Math.floor(Math.random() * messages.length)] + : { title: "Cafe Timer", body: "カフェ業務の時間です" }; + + const notificationPayload = { + app_id: process.env.NEXT_PUBLIC_ONESIGNAL_APP_ID, + include_aliases: { + external_id: [user.id] + }, + target_channel: "push", + contents: { en: randomMsg.body, ja: randomMsg.body }, + headings: { en: randomMsg.title, ja: randomMsg.title }, + send_after: sendAfter.toISOString(), + }; + + const osRes = await fetch("https://onesignal.com/api/v1/notifications", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Basic ${process.env.ONESIGNAL_REST_API_KEY}` + }, + body: JSON.stringify(notificationPayload) + }); + + if (!osRes.ok) { + console.error("OneSignal Error:", await osRes.text()); + } + } } return NextResponse.json({ success: true, history: newHistory }); + } catch (error: any) { - console.error("[Error] Critical API Error:", error); + console.error("API Error:", error); return NextResponse.json({ error: error.message }, { status: 500 }); } -} - -async function scheduleNotification(userId: string, tapTime: string) { - const sendAfter = new Date(tapTime); - sendAfter.setHours(sendAfter.getHours() + 3); - const msg = messages[Math.floor(Math.random() * messages.length)] || { title: "Cafe Timer", body: "カフェ業務の時間です" }; - - await fetch("https://onesignal.com/api/v1/notifications", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Basic ${API_KEY}` - }, - body: JSON.stringify({ - app_id: APP_ID, - include_aliases: { external_id: [userId] }, - target_channel: "push", - contents: { en: msg.body, ja: msg.body }, - headings: { en: msg.title, ja: msg.title }, - send_after: sendAfter.toISOString(), - }) - }); } \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css @@ -189,11 +189,8 @@ body { border-style: none; min-height: 8cqw; transition: all 0.2s; - line-height: 1.2; + line-height: 1.1; font-weight: 700; - border-left: 1.5px solid var(--muted); - border-bottom: 1.5px solid var(--muted); - background: rgba(255, 255, 255, 0.5); clip-path: polygon(0 0, 88% 0, 100% 28%, 100% 100%, 0 100%); } .slot-default { @@ -309,8 +306,7 @@ body { } .cal-cell { container-type: inline-size; - border-left: 1px solid var(--muted); - border-bottom: 1px solid var(--muted); + background-color: var(--card); } .bg-tap-1 { diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx @@ -5,7 +5,6 @@ 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" /> @@ -127,14 +126,10 @@ const Settings = () => { return ( <div className="p-4"> <div className="space-y-4"> - {/* Menu List */} <ul className="space-y-1"> - {menuItems.map((item, index) => ( + {menuItems.map((item) => ( <li key={item.label}> - <Link - href={item.path} - className="btn-setting flex items-center p-2 rounded transition-colors" - > + <Link href={item.path} className="btn-setting flex items-center p-2 rounded transition-colors"> <MenuItemIcon /> <span>{item.label}</span> </Link> @@ -142,28 +137,18 @@ const Settings = () => { ))} </ul> - {/* Color Theme Picker */} <div className="p-2 rounded-xl"> <div className="flex justify-between items-center mb-2"> <label className="text-xs font-mono 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} + 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%))` - }} + 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 && ( <> @@ -180,37 +165,15 @@ const Settings = () => { </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" - > + <div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> + <button onClick={handleNotificationClick} className="btn-setting 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 onClick={handleDeleteData} 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></>)} </button> - - <button - onClick={handleLogoutClick} - className="cal-nav flex items-center justify-center p-2 rounded transition-opacity active:opacity-70" - > + <button onClick={handleLogoutClick} className="btn-setting flex items-center justify-center p-2 rounded transition-opacity active:opacity-70"> <LogoutIcon /> <span>ログアウト</span> </button>