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:
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>