fix: Replace Prisma with native D1 and fix timezone issues
- Created native D1 database layer (src/lib/d1.ts) to bypass Prisma fs.readdir issues - Updated all API routes to use direct D1 queries - Added date-utils.ts with local date helpers to fix UTC timezone mismatch - Calendar now correctly colors today's usage - Data persists correctly across page refreshes
This commit is contained in:
parent
14c45eeb24
commit
bf9da84553
@ -1,8 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
// Required for OpenNext/Cloudflare Workers
|
// No special configuration needed - using native D1 API
|
||||||
serverExternalPackages: ["@prisma/client"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getPrismaWithD1 } from '@/lib/prisma';
|
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
import { getAchievementsD1, getAchievementD1, createAchievementD1 } from '@/lib/d1';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -9,16 +9,12 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const achievements = await getAchievementsD1(session.user.id);
|
||||||
const achievements = await prisma.achievement.findMany({
|
|
||||||
where: { userId: session.user.id },
|
|
||||||
orderBy: { unlockedAt: 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
achievements.map((a) => ({
|
achievements.map((a) => ({
|
||||||
badgeId: a.badgeId,
|
badgeId: a.badgeId,
|
||||||
unlockedAt: a.unlockedAt.toISOString(),
|
unlockedAt: a.unlockedAt,
|
||||||
substance: a.substance,
|
substance: a.substance,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@ -42,39 +38,26 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Missing badgeId or substance' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing badgeId or substance' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
// Check if already exists
|
||||||
|
const existing = await getAchievementD1(session.user.id, badgeId, substance);
|
||||||
// Check if achievement already exists
|
|
||||||
const existing = await prisma.achievement.findUnique({
|
|
||||||
where: {
|
|
||||||
userId_badgeId_substance: {
|
|
||||||
userId: session.user.id,
|
|
||||||
badgeId,
|
|
||||||
substance,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
badgeId: existing.badgeId,
|
badgeId: existing.badgeId,
|
||||||
unlockedAt: existing.unlockedAt.toISOString(),
|
unlockedAt: existing.unlockedAt,
|
||||||
substance: existing.substance,
|
substance: existing.substance,
|
||||||
alreadyUnlocked: true,
|
alreadyUnlocked: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const achievement = await prisma.achievement.create({
|
const achievement = await createAchievementD1(session.user.id, badgeId, substance);
|
||||||
data: {
|
|
||||||
userId: session.user.id,
|
if (!achievement) {
|
||||||
badgeId,
|
return NextResponse.json({ error: 'Failed to unlock achievement' }, { status: 500 });
|
||||||
substance,
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
badgeId: achievement.badgeId,
|
badgeId: achievement.badgeId,
|
||||||
unlockedAt: achievement.unlockedAt.toISOString(),
|
unlockedAt: achievement.unlockedAt,
|
||||||
substance: achievement.substance,
|
substance: achievement.substance,
|
||||||
alreadyUnlocked: false,
|
alreadyUnlocked: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getPrismaWithD1 } from '@/lib/prisma';
|
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
import { getPreferencesD1, upsertPreferencesD1 } from '@/lib/d1';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -9,10 +9,7 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const preferences = await getPreferencesD1(session.user.id);
|
||||||
const preferences = await prisma.userPreferences.findUnique({
|
|
||||||
where: { userId: session.user.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!preferences) {
|
if (!preferences) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@ -29,7 +26,7 @@ export async function GET() {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
substance: preferences.substance,
|
substance: preferences.substance,
|
||||||
trackingStartDate: preferences.trackingStartDate,
|
trackingStartDate: preferences.trackingStartDate,
|
||||||
hasCompletedSetup: preferences.hasCompletedSetup,
|
hasCompletedSetup: !!preferences.hasCompletedSetup,
|
||||||
dailyGoal: preferences.dailyGoal,
|
dailyGoal: preferences.dailyGoal,
|
||||||
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
|
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
|
||||||
userName: preferences.userName,
|
userName: preferences.userName,
|
||||||
@ -63,53 +60,28 @@ export async function POST(request: NextRequest) {
|
|||||||
lastNicotineUsageTime?: string;
|
lastNicotineUsageTime?: string;
|
||||||
lastWeedUsageTime?: string;
|
lastWeedUsageTime?: string;
|
||||||
};
|
};
|
||||||
const {
|
|
||||||
substance,
|
|
||||||
trackingStartDate,
|
|
||||||
hasCompletedSetup,
|
|
||||||
dailyGoal,
|
|
||||||
quitPlan,
|
|
||||||
userName,
|
|
||||||
userAge,
|
|
||||||
religion,
|
|
||||||
lastNicotineUsageTime,
|
|
||||||
lastWeedUsageTime
|
|
||||||
} = body;
|
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const preferences = await upsertPreferencesD1(session.user.id, {
|
||||||
const preferences = await prisma.userPreferences.upsert({
|
substance: body.substance,
|
||||||
where: { userId: session.user.id },
|
trackingStartDate: body.trackingStartDate,
|
||||||
update: {
|
hasCompletedSetup: body.hasCompletedSetup ? 1 : 0,
|
||||||
substance,
|
dailyGoal: body.dailyGoal,
|
||||||
trackingStartDate,
|
quitPlanJson: body.quitPlan ? JSON.stringify(body.quitPlan) : undefined,
|
||||||
hasCompletedSetup,
|
userName: body.userName,
|
||||||
dailyGoal,
|
userAge: body.userAge,
|
||||||
quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
|
religion: body.religion,
|
||||||
userName,
|
lastNicotineUsageTime: body.lastNicotineUsageTime,
|
||||||
userAge,
|
lastWeedUsageTime: body.lastWeedUsageTime,
|
||||||
religion,
|
|
||||||
lastNicotineUsageTime,
|
|
||||||
lastWeedUsageTime,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
userId: session.user.id,
|
|
||||||
substance,
|
|
||||||
trackingStartDate,
|
|
||||||
hasCompletedSetup,
|
|
||||||
dailyGoal,
|
|
||||||
quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
|
|
||||||
userName,
|
|
||||||
userAge,
|
|
||||||
religion,
|
|
||||||
lastNicotineUsageTime,
|
|
||||||
lastWeedUsageTime,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!preferences) {
|
||||||
|
return NextResponse.json({ error: 'Failed to save preferences' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
substance: preferences.substance,
|
substance: preferences.substance,
|
||||||
trackingStartDate: preferences.trackingStartDate,
|
trackingStartDate: preferences.trackingStartDate,
|
||||||
hasCompletedSetup: preferences.hasCompletedSetup,
|
hasCompletedSetup: !!preferences.hasCompletedSetup,
|
||||||
dailyGoal: preferences.dailyGoal,
|
dailyGoal: preferences.dailyGoal,
|
||||||
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
|
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
|
||||||
userName: preferences.userName,
|
userName: preferences.userName,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getPrismaWithD1 } from '@/lib/prisma';
|
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
import { getReminderSettingsD1, upsertReminderSettingsD1 } from '@/lib/d1';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -9,10 +9,7 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const settings = await getReminderSettingsD1(session.user.id);
|
||||||
const settings = await prisma.reminderSettings.findUnique({
|
|
||||||
where: { userId: session.user.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@ -22,7 +19,7 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
enabled: settings.enabled,
|
enabled: !!settings.enabled,
|
||||||
reminderTime: settings.reminderTime,
|
reminderTime: settings.reminderTime,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -41,22 +38,18 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = await request.json() as { enabled?: boolean; reminderTime?: string };
|
const body = await request.json() as { enabled?: boolean; reminderTime?: string };
|
||||||
const { enabled, reminderTime } = body;
|
const { enabled, reminderTime } = body;
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const settings = await upsertReminderSettingsD1(
|
||||||
const settings = await prisma.reminderSettings.upsert({
|
session.user.id,
|
||||||
where: { userId: session.user.id },
|
enabled ?? false,
|
||||||
update: {
|
reminderTime ?? '09:00'
|
||||||
enabled: enabled ?? false,
|
);
|
||||||
reminderTime: reminderTime ?? '09:00',
|
|
||||||
},
|
if (!settings) {
|
||||||
create: {
|
return NextResponse.json({ error: 'Failed to save reminder settings' }, { status: 500 });
|
||||||
userId: session.user.id,
|
}
|
||||||
enabled: enabled ?? false,
|
|
||||||
reminderTime: reminderTime ?? '09:00',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
enabled: settings.enabled,
|
enabled: !!settings.enabled,
|
||||||
reminderTime: settings.reminderTime,
|
reminderTime: settings.reminderTime,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getPrismaWithD1 } from '@/lib/prisma';
|
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
import { getSavingsConfigD1, upsertSavingsConfigD1 } from '@/lib/d1';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -9,10 +9,7 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const config = await getSavingsConfigD1(session.user.id);
|
||||||
const config = await prisma.savingsConfig.findUnique({
|
|
||||||
where: { userId: session.user.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return NextResponse.json(null);
|
return NextResponse.json(null);
|
||||||
@ -53,27 +50,19 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const config = await upsertSavingsConfigD1(
|
||||||
const config = await prisma.savingsConfig.upsert({
|
session.user.id,
|
||||||
where: { userId: session.user.id },
|
costPerUnit,
|
||||||
update: {
|
unitsPerDay,
|
||||||
costPerUnit,
|
substance,
|
||||||
unitsPerDay,
|
savingsGoal,
|
||||||
savingsGoal: savingsGoal ?? null,
|
goalName,
|
||||||
goalName: goalName ?? null,
|
currency
|
||||||
currency: currency ?? 'USD',
|
);
|
||||||
substance,
|
|
||||||
},
|
if (!config) {
|
||||||
create: {
|
return NextResponse.json({ error: 'Failed to save savings config' }, { status: 500 });
|
||||||
userId: session.user.id,
|
}
|
||||||
costPerUnit,
|
|
||||||
unitsPerDay,
|
|
||||||
savingsGoal: savingsGoal ?? null,
|
|
||||||
goalName: goalName ?? null,
|
|
||||||
currency: currency ?? 'USD',
|
|
||||||
substance,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
costPerUnit: config.costPerUnit,
|
costPerUnit: config.costPerUnit,
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getPrismaWithD1 } from '@/lib/prisma';
|
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
import {
|
||||||
|
getUsageEntriesD1,
|
||||||
|
getUsageEntryD1,
|
||||||
|
upsertUsageEntryD1,
|
||||||
|
deleteUsageEntryD1
|
||||||
|
} from '@/lib/d1';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -9,11 +14,7 @@ export async function GET() {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
const entries = await getUsageEntriesD1(session.user.id);
|
||||||
const entries = await prisma.usageEntry.findMany({
|
|
||||||
where: { userId: session.user.id },
|
|
||||||
orderBy: { date: 'desc' },
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
entries.map((e) => ({
|
entries.map((e) => ({
|
||||||
@ -42,44 +43,18 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
// Add to existing count
|
||||||
|
const entry = await upsertUsageEntryD1(session.user.id, date, count, substance, true);
|
||||||
|
|
||||||
// Upsert: add to existing count or create new entry
|
if (!entry) {
|
||||||
const existing = await prisma.usageEntry.findUnique({
|
return NextResponse.json({ error: 'Failed to save usage entry' }, { status: 500 });
|
||||||
where: {
|
|
||||||
userId_date_substance: {
|
|
||||||
userId: session.user.id,
|
|
||||||
date: date,
|
|
||||||
substance: substance,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
const updated = await prisma.usageEntry.update({
|
|
||||||
where: { id: existing.id },
|
|
||||||
data: { count: existing.count + count },
|
|
||||||
});
|
|
||||||
return NextResponse.json({
|
|
||||||
date: updated.date,
|
|
||||||
count: updated.count,
|
|
||||||
substance: updated.substance,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const created = await prisma.usageEntry.create({
|
|
||||||
data: {
|
|
||||||
userId: session.user.id,
|
|
||||||
date: date,
|
|
||||||
count: count,
|
|
||||||
substance: substance,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return NextResponse.json({
|
|
||||||
date: created.date,
|
|
||||||
count: created.count,
|
|
||||||
substance: created.substance,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
date: entry.date,
|
||||||
|
count: entry.count,
|
||||||
|
substance: entry.substance,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving usage entry:', error);
|
console.error('Error saving usage entry:', error);
|
||||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
@ -100,25 +75,12 @@ export async function PUT(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
|
||||||
|
|
||||||
// Set the exact count (replace, not add)
|
// Set the exact count (replace, not add)
|
||||||
const entry = await prisma.usageEntry.upsert({
|
const entry = await upsertUsageEntryD1(session.user.id, date, count, substance, false);
|
||||||
where: {
|
|
||||||
userId_date_substance: {
|
if (!entry) {
|
||||||
userId: session.user.id,
|
return NextResponse.json({ error: 'Failed to update usage entry' }, { status: 500 });
|
||||||
date: date,
|
}
|
||||||
substance: substance,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: { count: count },
|
|
||||||
create: {
|
|
||||||
userId: session.user.id,
|
|
||||||
date: date,
|
|
||||||
count: count,
|
|
||||||
substance: substance,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
date: entry.date,
|
date: entry.date,
|
||||||
@ -146,15 +108,7 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Missing date or substance' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing date or substance' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = await getPrismaWithD1();
|
await deleteUsageEntryD1(session.user.id, date, substance);
|
||||||
|
|
||||||
await prisma.usageEntry.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id,
|
|
||||||
date,
|
|
||||||
substance,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import { SavingsTrackerCard } from './SavingsTrackerCard';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { PlusCircle } from 'lucide-react';
|
import { PlusCircle } from 'lucide-react';
|
||||||
import { useTheme } from '@/lib/theme-context';
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
import { getTodayString } from '@/lib/date-utils';
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
user: User;
|
user: User;
|
||||||
@ -118,7 +119,7 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
}, [loadData, checkAndUnlockAchievements]);
|
}, [loadData, checkAndUnlockAchievements]);
|
||||||
|
|
||||||
const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'muslim' | 'jewish' | 'secular' }) => {
|
const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'muslim' | 'jewish' | 'secular' }) => {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = getTodayString();
|
||||||
const newPrefs: UserPreferences = {
|
const newPrefs: UserPreferences = {
|
||||||
substance: data.substance,
|
substance: data.substance,
|
||||||
trackingStartDate: today,
|
trackingStartDate: today,
|
||||||
@ -143,7 +144,7 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = getTodayString();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
await saveUsageEntryAsync({
|
await saveUsageEntryAsync({
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { UsageEntry } from '@/lib/storage';
|
import { UsageEntry } from '@/lib/storage';
|
||||||
import { Cigarette, Leaf } from 'lucide-react';
|
import { Cigarette, Leaf } from 'lucide-react';
|
||||||
import { useTheme } from '@/lib/theme-context';
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
import { getTodayString, getLocalDateString } from '@/lib/date-utils';
|
||||||
|
|
||||||
interface StatsCardProps {
|
interface StatsCardProps {
|
||||||
usageData: UsageEntry[];
|
usageData: UsageEntry[];
|
||||||
@ -16,7 +17,7 @@ export function StatsCard({ usageData, substance }: StatsCardProps) {
|
|||||||
|
|
||||||
// Calculate stats
|
// Calculate stats
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const todayStr = today.toISOString().split('T')[0];
|
const todayStr = getTodayString();
|
||||||
const todayUsage = substanceData.find((e) => e.date === todayStr)?.count ?? 0;
|
const todayUsage = substanceData.find((e) => e.date === todayStr)?.count ?? 0;
|
||||||
|
|
||||||
// Last 7 days
|
// Last 7 days
|
||||||
@ -37,7 +38,7 @@ export function StatsCard({ usageData, substance }: StatsCardProps) {
|
|||||||
for (let i = 0; i <= 30; i++) {
|
for (let i = 0; i <= 30; i++) {
|
||||||
const checkDate = new Date(today);
|
const checkDate = new Date(today);
|
||||||
checkDate.setDate(checkDate.getDate() - i);
|
checkDate.setDate(checkDate.getDate() - i);
|
||||||
const dateStr = checkDate.toISOString().split('T')[0];
|
const dateStr = getLocalDateString(checkDate);
|
||||||
const dayUsage = substanceData.find((e) => e.date === dateStr)?.count ?? -1;
|
const dayUsage = substanceData.find((e) => e.date === dateStr)?.count ?? -1;
|
||||||
|
|
||||||
if (dayUsage === 0) {
|
if (dayUsage === 0) {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { UsageEntry, UserPreferences, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
|
import { UsageEntry, UserPreferences, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
|
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
|
||||||
import { useTheme } from '@/lib/theme-context';
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
import { getLocalDateString, getTodayString } from '@/lib/date-utils';
|
||||||
import { DailyInspirationCard } from './DailyInspirationCard';
|
import { DailyInspirationCard } from './DailyInspirationCard';
|
||||||
|
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpd
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
const getUsageForDate = (date: Date, substance: 'nicotine' | 'weed'): number => {
|
const getUsageForDate = (date: Date, substance: 'nicotine' | 'weed'): number => {
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
const dateStr = getLocalDateString(date);
|
||||||
const entry = usageData.find((e) => e.date === dateStr && e.substance === substance);
|
const entry = usageData.find((e) => e.date === dateStr && e.substance === substance);
|
||||||
return entry?.count ?? 0;
|
return entry?.count ?? 0;
|
||||||
};
|
};
|
||||||
@ -60,8 +61,8 @@ export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpd
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
const dateStr = selectedDate.toISOString().split('T')[0];
|
const dateStr = getLocalDateString(selectedDate);
|
||||||
const todayStr = new Date().toISOString().split('T')[0];
|
const todayStr = getTodayString();
|
||||||
const newNicotineCount = parseInt(editNicotineCount, 10) || 0;
|
const newNicotineCount = parseInt(editNicotineCount, 10) || 0;
|
||||||
const newWeedCount = parseInt(editWeedCount, 10) || 0;
|
const newWeedCount = parseInt(editWeedCount, 10) || 0;
|
||||||
|
|
||||||
@ -105,7 +106,7 @@ export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpd
|
|||||||
|
|
||||||
const handleClearDay = async () => {
|
const handleClearDay = async () => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
const dateStr = selectedDate.toISOString().split('T')[0];
|
const dateStr = getLocalDateString(selectedDate);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
clearDayDataAsync(dateStr, 'nicotine'),
|
clearDayDataAsync(dateStr, 'nicotine'),
|
||||||
clearDayDataAsync(dateStr, 'weed'),
|
clearDayDataAsync(dateStr, 'weed'),
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { getUsageForDate } from '@/lib/storage';
|
import { getUsageForDate } from '@/lib/storage';
|
||||||
|
import { getTodayString } from '@/lib/date-utils';
|
||||||
import { Cigarette, Leaf } from 'lucide-react';
|
import { Cigarette, Leaf } from 'lucide-react';
|
||||||
|
|
||||||
interface UsagePromptDialogProps {
|
interface UsagePromptDialogProps {
|
||||||
@ -31,7 +32,7 @@ export function UsagePromptDialog({
|
|||||||
const [selectedSubstance, setSelectedSubstance] = useState<'nicotine' | 'weed' | null>(null);
|
const [selectedSubstance, setSelectedSubstance] = useState<'nicotine' | 'weed' | null>(null);
|
||||||
const [count, setCount] = useState('1');
|
const [count, setCount] = useState('1');
|
||||||
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = getTodayString();
|
||||||
const nicotineCount = typeof window !== 'undefined' ? getUsageForDate(today, 'nicotine', userId) : 0;
|
const nicotineCount = typeof window !== 'undefined' ? getUsageForDate(today, 'nicotine', userId) : 0;
|
||||||
const weedCount = typeof window !== 'undefined' ? getUsageForDate(today, 'weed', userId) : 0;
|
const weedCount = typeof window !== 'undefined' ? getUsageForDate(today, 'weed', userId) : 0;
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
UsageEntry,
|
UsageEntry,
|
||||||
QuitPlan,
|
QuitPlan,
|
||||||
} from "@/lib/storage";
|
} from "@/lib/storage";
|
||||||
|
import { getTodayString } from "@/lib/date-utils";
|
||||||
|
|
||||||
type SubstanceType = 'nicotine' | 'weed';
|
type SubstanceType = 'nicotine' | 'weed';
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ export function useUsageLogs() {
|
|||||||
const logUsage = useCallback(
|
const logUsage = useCallback(
|
||||||
(puffs: number, date?: string) => {
|
(puffs: number, date?: string) => {
|
||||||
const prefs = getPreferences();
|
const prefs = getPreferences();
|
||||||
const logDate = date || new Date().toISOString().split("T")[0];
|
const logDate = date || getTodayString();
|
||||||
const newEntry: UsageEntry = {
|
const newEntry: UsageEntry = {
|
||||||
date: logDate,
|
date: logDate,
|
||||||
count: puffs,
|
count: puffs,
|
||||||
|
|||||||
329
src/lib/d1.ts
Normal file
329
src/lib/d1.ts
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
// Native D1 database client for Cloudflare Workers
|
||||||
|
// This bypasses Prisma which has compatibility issues with the Workers runtime
|
||||||
|
|
||||||
|
export interface D1Result<T> {
|
||||||
|
results: T[];
|
||||||
|
success: boolean;
|
||||||
|
meta: {
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the D1 database binding from Cloudflare context
|
||||||
|
*/
|
||||||
|
export function getD1(): D1Database | null {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const { getCloudflareContext } = require('@opennextjs/cloudflare');
|
||||||
|
const ctx = getCloudflareContext();
|
||||||
|
return ctx?.env?.DB || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[getD1] Failed to get Cloudflare context:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ USER PREFERENCES ============
|
||||||
|
|
||||||
|
export interface UserPreferencesRow {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
substance: string;
|
||||||
|
trackingStartDate: string | null;
|
||||||
|
hasCompletedSetup: number; // SQLite boolean
|
||||||
|
dailyGoal: number | null;
|
||||||
|
userName: string | null;
|
||||||
|
userAge: number | null;
|
||||||
|
religion: string | null;
|
||||||
|
lastNicotineUsageTime: string | null;
|
||||||
|
lastWeedUsageTime: string | null;
|
||||||
|
quitPlanJson: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPreferencesD1(userId: string): Promise<UserPreferencesRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT * FROM UserPreferences WHERE userId = ?'
|
||||||
|
).bind(userId).first<UserPreferencesRow>();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertPreferencesD1(userId: string, data: Partial<UserPreferencesRow>): Promise<UserPreferencesRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const existing = await getPreferencesD1(userId);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const id = existing?.id || crypto.randomUUID();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Update
|
||||||
|
const updates: string[] = [];
|
||||||
|
const values: unknown[] = [];
|
||||||
|
|
||||||
|
if (data.substance !== undefined) { updates.push('substance = ?'); values.push(data.substance); }
|
||||||
|
if (data.trackingStartDate !== undefined) { updates.push('trackingStartDate = ?'); values.push(data.trackingStartDate); }
|
||||||
|
if (data.hasCompletedSetup !== undefined) { updates.push('hasCompletedSetup = ?'); values.push(data.hasCompletedSetup ? 1 : 0); }
|
||||||
|
if (data.dailyGoal !== undefined) { updates.push('dailyGoal = ?'); values.push(data.dailyGoal); }
|
||||||
|
if (data.userName !== undefined) { updates.push('userName = ?'); values.push(data.userName); }
|
||||||
|
if (data.userAge !== undefined) { updates.push('userAge = ?'); values.push(data.userAge); }
|
||||||
|
if (data.religion !== undefined) { updates.push('religion = ?'); values.push(data.religion); }
|
||||||
|
if (data.lastNicotineUsageTime !== undefined) { updates.push('lastNicotineUsageTime = ?'); values.push(data.lastNicotineUsageTime); }
|
||||||
|
if (data.lastWeedUsageTime !== undefined) { updates.push('lastWeedUsageTime = ?'); values.push(data.lastWeedUsageTime); }
|
||||||
|
if (data.quitPlanJson !== undefined) { updates.push('quitPlanJson = ?'); values.push(data.quitPlanJson); }
|
||||||
|
|
||||||
|
updates.push('updatedAt = ?');
|
||||||
|
values.push(now);
|
||||||
|
values.push(userId);
|
||||||
|
|
||||||
|
await db.prepare(
|
||||||
|
`UPDATE UserPreferences SET ${updates.join(', ')} WHERE userId = ?`
|
||||||
|
).bind(...values).run();
|
||||||
|
} else {
|
||||||
|
// Insert
|
||||||
|
await db.prepare(
|
||||||
|
`INSERT INTO UserPreferences (id, userId, substance, trackingStartDate, hasCompletedSetup, dailyGoal, userName, userAge, religion, lastNicotineUsageTime, lastWeedUsageTime, quitPlanJson, createdAt, updatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
).bind(
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
data.substance || 'nicotine',
|
||||||
|
data.trackingStartDate || null,
|
||||||
|
data.hasCompletedSetup ? 1 : 0,
|
||||||
|
data.dailyGoal || null,
|
||||||
|
data.userName || null,
|
||||||
|
data.userAge || null,
|
||||||
|
data.religion || null,
|
||||||
|
data.lastNicotineUsageTime || null,
|
||||||
|
data.lastWeedUsageTime || null,
|
||||||
|
data.quitPlanJson || null,
|
||||||
|
now,
|
||||||
|
now
|
||||||
|
).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPreferencesD1(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ USAGE ENTRIES ============
|
||||||
|
|
||||||
|
export interface UsageEntryRow {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
substance: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsageEntriesD1(userId: string): Promise<UsageEntryRow[]> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT * FROM UsageEntry WHERE userId = ? ORDER BY date DESC'
|
||||||
|
).bind(userId).all<UsageEntryRow>();
|
||||||
|
|
||||||
|
return result.results || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsageEntryD1(userId: string, date: string, substance: string): Promise<UsageEntryRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT * FROM UsageEntry WHERE userId = ? AND date = ? AND substance = ?'
|
||||||
|
).bind(userId, date, substance).first<UsageEntryRow>();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertUsageEntryD1(userId: string, date: string, count: number, substance: string, addToExisting = false): Promise<UsageEntryRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const existing = await getUsageEntryD1(userId, date, substance);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const id = existing?.id || crypto.randomUUID();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
const newCount = addToExisting ? existing.count + count : count;
|
||||||
|
await db.prepare(
|
||||||
|
'UPDATE UsageEntry SET count = ?, updatedAt = ? WHERE id = ?'
|
||||||
|
).bind(newCount, now, existing.id).run();
|
||||||
|
} else {
|
||||||
|
await db.prepare(
|
||||||
|
`INSERT INTO UsageEntry (id, userId, date, count, substance, createdAt, updatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
).bind(id, userId, date, count, substance, now, now).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getUsageEntryD1(userId, date, substance);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUsageEntryD1(userId: string, date: string, substance: string): Promise<void> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return;
|
||||||
|
|
||||||
|
await db.prepare(
|
||||||
|
'DELETE FROM UsageEntry WHERE userId = ? AND date = ? AND substance = ?'
|
||||||
|
).bind(userId, date, substance).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ ACHIEVEMENTS ============
|
||||||
|
|
||||||
|
export interface AchievementRow {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
badgeId: string;
|
||||||
|
unlockedAt: string;
|
||||||
|
substance: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAchievementsD1(userId: string): Promise<AchievementRow[]> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT * FROM Achievement WHERE userId = ? ORDER BY unlockedAt DESC'
|
||||||
|
).bind(userId).all<AchievementRow>();
|
||||||
|
|
||||||
|
return result.results || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAchievementD1(userId: string, badgeId: string, substance: string): Promise<AchievementRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT * FROM Achievement WHERE userId = ? AND badgeId = ? AND substance = ?'
|
||||||
|
).bind(userId, badgeId, substance).first<AchievementRow>();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAchievementD1(userId: string, badgeId: string, substance: string): Promise<AchievementRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const existing = await getAchievementD1(userId, badgeId, substance);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
await db.prepare(
|
||||||
|
`INSERT INTO Achievement (id, userId, badgeId, unlockedAt, substance)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`
|
||||||
|
).bind(id, userId, badgeId, now, substance).run();
|
||||||
|
|
||||||
|
return getAchievementD1(userId, badgeId, substance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ REMINDER SETTINGS ============
|
||||||
|
|
||||||
|
export interface ReminderSettingsRow {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
enabled: number; // SQLite boolean
|
||||||
|
reminderTime: string;
|
||||||
|
lastNotifiedDate: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReminderSettingsD1(userId: string): Promise<ReminderSettingsRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT * FROM ReminderSettings WHERE userId = ?'
|
||||||
|
).bind(userId).first<ReminderSettingsRow>();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertReminderSettingsD1(userId: string, enabled: boolean, reminderTime: string): Promise<ReminderSettingsRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const existing = await getReminderSettingsD1(userId);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const id = existing?.id || crypto.randomUUID();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await db.prepare(
|
||||||
|
'UPDATE ReminderSettings SET enabled = ?, reminderTime = ?, updatedAt = ? WHERE userId = ?'
|
||||||
|
).bind(enabled ? 1 : 0, reminderTime, now, userId).run();
|
||||||
|
} else {
|
||||||
|
await db.prepare(
|
||||||
|
`INSERT INTO ReminderSettings (id, userId, enabled, reminderTime, createdAt, updatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`
|
||||||
|
).bind(id, userId, enabled ? 1 : 0, reminderTime, now, now).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getReminderSettingsD1(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ SAVINGS CONFIG ============
|
||||||
|
|
||||||
|
export interface SavingsConfigRow {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
costPerUnit: number;
|
||||||
|
unitsPerDay: number;
|
||||||
|
savingsGoal: number | null;
|
||||||
|
goalName: string | null;
|
||||||
|
currency: string;
|
||||||
|
substance: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSavingsConfigD1(userId: string): Promise<SavingsConfigRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const result = await db.prepare(
|
||||||
|
'SELECT * FROM SavingsConfig WHERE userId = ?'
|
||||||
|
).bind(userId).first<SavingsConfigRow>();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertSavingsConfigD1(
|
||||||
|
userId: string,
|
||||||
|
costPerUnit: number,
|
||||||
|
unitsPerDay: number,
|
||||||
|
substance: string,
|
||||||
|
savingsGoal?: number | null,
|
||||||
|
goalName?: string | null,
|
||||||
|
currency?: string
|
||||||
|
): Promise<SavingsConfigRow | null> {
|
||||||
|
const db = getD1();
|
||||||
|
if (!db) return null;
|
||||||
|
|
||||||
|
const existing = await getSavingsConfigD1(userId);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const id = existing?.id || crypto.randomUUID();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await db.prepare(
|
||||||
|
'UPDATE SavingsConfig SET costPerUnit = ?, unitsPerDay = ?, savingsGoal = ?, goalName = ?, currency = ?, substance = ?, updatedAt = ? WHERE userId = ?'
|
||||||
|
).bind(costPerUnit, unitsPerDay, savingsGoal ?? null, goalName ?? null, currency || 'USD', substance, now, userId).run();
|
||||||
|
} else {
|
||||||
|
await db.prepare(
|
||||||
|
`INSERT INTO SavingsConfig (id, userId, costPerUnit, unitsPerDay, savingsGoal, goalName, currency, substance, createdAt, updatedAt)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
).bind(id, userId, costPerUnit, unitsPerDay, savingsGoal ?? null, goalName ?? null, currency || 'USD', substance, now, now).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSavingsConfigD1(userId);
|
||||||
|
}
|
||||||
17
src/lib/date-utils.ts
Normal file
17
src/lib/date-utils.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Get a date string in YYYY-MM-DD format using LOCAL timezone.
|
||||||
|
* This prevents timezone issues where evening times become the next day in UTC.
|
||||||
|
*/
|
||||||
|
export function getLocalDateString(date: Date = new Date()): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today's date string in YYYY-MM-DD format using LOCAL timezone.
|
||||||
|
*/
|
||||||
|
export function getTodayString(): string {
|
||||||
|
return getLocalDateString(new Date());
|
||||||
|
}
|
||||||
@ -10,17 +10,19 @@ const globalForPrisma = globalThis as unknown as {
|
|||||||
* Get Prisma client for Cloudflare Workers with D1.
|
* Get Prisma client for Cloudflare Workers with D1.
|
||||||
* This should be called in each request handler to get a fresh client with D1 binding.
|
* This should be called in each request handler to get a fresh client with D1 binding.
|
||||||
*/
|
*/
|
||||||
export async function getPrismaWithD1(): Promise<PrismaClient> {
|
export function getPrismaWithD1(): PrismaClient {
|
||||||
// Try to get Cloudflare context (only works in Workers environment)
|
// Try to get Cloudflare context (only works in Workers environment)
|
||||||
try {
|
try {
|
||||||
const { getCloudflareContext } = await import('@opennextjs/cloudflare');
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const { env } = await getCloudflareContext();
|
const { getCloudflareContext } = require('@opennextjs/cloudflare');
|
||||||
|
const ctx = getCloudflareContext();
|
||||||
|
|
||||||
if (env?.DB) {
|
if (ctx?.env?.DB) {
|
||||||
const adapter = new PrismaD1(env.DB);
|
const adapter = new PrismaD1(ctx.env.DB);
|
||||||
return new PrismaClient({ adapter });
|
return new PrismaClient({ adapter });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error('[getPrismaWithD1] Failed to get Cloudflare context:', error);
|
||||||
// Not in Cloudflare Workers environment, fall back to local client
|
// Not in Cloudflare Workers environment, fall back to local client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,5 +35,12 @@
|
|||||||
"pattern": "www.quittraq.com/*",
|
"pattern": "www.quittraq.com/*",
|
||||||
"zone_name": "quittraq.com"
|
"zone_name": "quittraq.com"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
// Observability
|
||||||
|
"observability": {
|
||||||
|
"logs": {
|
||||||
|
"enabled": false,
|
||||||
|
"invocation_logs": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user