diff --git a/.gitignore b/.gitignore
index 5ef6a52..0aa6b11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+# database
+*.db
+*.db-journal
diff --git a/bun.lock b/bun.lock
index 0f6a66a..68e9d93 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,6 +5,7 @@
"": {
"name": "quit_smoking_website",
"dependencies": {
+ "@prisma/client": "5",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
@@ -15,8 +16,10 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
+ "dotenv": "^17.2.3",
"lucide-react": "^0.563.0",
"next": "16.1.4",
+ "prisma": "5",
"react": "19.2.3",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.3",
@@ -206,6 +209,18 @@
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
+ "@prisma/client": ["@prisma/client@5.22.0", "", { "peerDependencies": { "prisma": "*" }, "optionalPeers": ["prisma"] }, "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA=="],
+
+ "@prisma/debug": ["@prisma/debug@5.22.0", "", {}, "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ=="],
+
+ "@prisma/engines": ["@prisma/engines@5.22.0", "", { "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "@prisma/fetch-engine": "5.22.0", "@prisma/get-platform": "5.22.0" } }, "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA=="],
+
+ "@prisma/engines-version": ["@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "", {}, "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ=="],
+
+ "@prisma/fetch-engine": ["@prisma/fetch-engine@5.22.0", "", { "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "@prisma/get-platform": "5.22.0" } }, "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA=="],
+
+ "@prisma/get-platform": ["@prisma/get-platform@5.22.0", "", { "dependencies": { "@prisma/debug": "5.22.0" } }, "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q=="],
+
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@@ -484,6 +499,8 @@
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
+ "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.278", "", {}, "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw=="],
@@ -568,6 +585,8 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
@@ -812,6 +831,8 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+ "prisma": ["prisma@5.22.0", "", { "dependencies": { "@prisma/engines": "5.22.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "prisma": "build/index.js" } }, "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A=="],
+
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
diff --git a/package.json b/package.json
index ea9efd5..45b5112 100644
--- a/package.json
+++ b/package.json
@@ -4,11 +4,13 @@
"private": true,
"scripts": {
"dev": "next dev",
- "build": "next build",
+ "build": "prisma generate && next build",
"start": "next start",
- "lint": "eslint"
+ "lint": "eslint",
+ "postinstall": "prisma generate"
},
"dependencies": {
+ "@prisma/client": "5",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
@@ -19,8 +21,10 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
+ "dotenv": "^17.2.3",
"lucide-react": "^0.563.0",
"next": "16.1.4",
+ "prisma": "5",
"react": "19.2.3",
"react-day-picker": "^9.13.0",
"react-dom": "19.2.3",
diff --git a/prisma/migrations/20260124050535_init/migration.sql b/prisma/migrations/20260124050535_init/migration.sql
new file mode 100644
index 0000000..8d88e2d
--- /dev/null
+++ b/prisma/migrations/20260124050535_init/migration.sql
@@ -0,0 +1,32 @@
+-- CreateTable
+CREATE TABLE "UserPreferences" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "substance" TEXT NOT NULL DEFAULT 'nicotine',
+ "trackingStartDate" TEXT,
+ "hasCompletedSetup" BOOLEAN NOT NULL DEFAULT false,
+ "dailyGoal" INTEGER,
+ "userName" TEXT,
+ "userAge" INTEGER,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "quitPlanJson" TEXT
+);
+
+-- CreateTable
+CREATE TABLE "UsageEntry" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "date" TEXT NOT NULL,
+ "count" INTEGER NOT NULL,
+ "substance" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "UsageEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "UserPreferences" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserPreferences_userId_key" ON "UserPreferences"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UsageEntry_userId_date_substance_key" ON "UsageEntry"("userId", "date", "substance");
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..2a5a444
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "sqlite"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..fc3e593
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,43 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = "file:./dev.db"
+}
+
+model UserPreferences {
+ id String @id @default(cuid())
+ userId String @unique
+ substance String @default("nicotine")
+ trackingStartDate String?
+ hasCompletedSetup Boolean @default(false)
+ dailyGoal Int?
+ userName String?
+ userAge Int?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Quit plan fields stored as JSON string
+ quitPlanJson String?
+
+ usageEntries UsageEntry[]
+}
+
+model UsageEntry {
+ id String @id @default(cuid())
+ userId String
+ date String
+ count Int
+ substance String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ userPreferences UserPreferences? @relation(fields: [userId], references: [userId])
+
+ @@unique([userId, date, substance])
+}
diff --git a/src/app/api/preferences/route.ts b/src/app/api/preferences/route.ts
new file mode 100644
index 0000000..88bc976
--- /dev/null
+++ b/src/app/api/preferences/route.ts
@@ -0,0 +1,89 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { prisma } from '@/lib/prisma';
+import { getSession } from '@/lib/session';
+
+export async function GET() {
+ try {
+ const session = await getSession();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const preferences = await prisma.userPreferences.findUnique({
+ where: { userId: session.user.id },
+ });
+
+ if (!preferences) {
+ return NextResponse.json({
+ substance: 'nicotine',
+ trackingStartDate: null,
+ hasCompletedSetup: false,
+ dailyGoal: null,
+ quitPlan: null,
+ userName: null,
+ userAge: null,
+ });
+ }
+
+ return NextResponse.json({
+ substance: preferences.substance,
+ trackingStartDate: preferences.trackingStartDate,
+ hasCompletedSetup: preferences.hasCompletedSetup,
+ dailyGoal: preferences.dailyGoal,
+ quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
+ userName: preferences.userName,
+ userAge: preferences.userAge,
+ });
+ } catch (error) {
+ console.error('Error fetching preferences:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const session = await getSession();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const { substance, trackingStartDate, hasCompletedSetup, dailyGoal, quitPlan, userName, userAge } = body;
+
+ const preferences = await prisma.userPreferences.upsert({
+ where: { userId: session.user.id },
+ update: {
+ substance,
+ trackingStartDate,
+ hasCompletedSetup,
+ dailyGoal,
+ quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
+ userName,
+ userAge,
+ },
+ create: {
+ userId: session.user.id,
+ substance,
+ trackingStartDate,
+ hasCompletedSetup,
+ dailyGoal,
+ quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
+ userName,
+ userAge,
+ },
+ });
+
+ return NextResponse.json({
+ substance: preferences.substance,
+ trackingStartDate: preferences.trackingStartDate,
+ hasCompletedSetup: preferences.hasCompletedSetup,
+ dailyGoal: preferences.dailyGoal,
+ quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
+ userName: preferences.userName,
+ userAge: preferences.userAge,
+ });
+ } catch (error) {
+ console.error('Error saving preferences:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/src/app/api/usage/route.ts b/src/app/api/usage/route.ts
new file mode 100644
index 0000000..a966cf5
--- /dev/null
+++ b/src/app/api/usage/route.ts
@@ -0,0 +1,149 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { prisma } from '@/lib/prisma';
+import { getSession } from '@/lib/session';
+
+export async function GET() {
+ try {
+ const session = await getSession();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const entries = await prisma.usageEntry.findMany({
+ where: { userId: session.user.id },
+ orderBy: { date: 'desc' },
+ });
+
+ return NextResponse.json(
+ entries.map((e) => ({
+ date: e.date,
+ count: e.count,
+ substance: e.substance,
+ }))
+ );
+ } catch (error) {
+ console.error('Error fetching usage data:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const session = await getSession();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const { date, count, substance } = body;
+
+ // Upsert: add to existing count or create new entry
+ const existing = await prisma.usageEntry.findUnique({
+ where: {
+ userId_date_substance: {
+ userId: session.user.id,
+ date,
+ 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,
+ count,
+ substance,
+ },
+ });
+ return NextResponse.json({
+ date: created.date,
+ count: created.count,
+ substance: created.substance,
+ });
+ }
+ } catch (error) {
+ console.error('Error saving usage entry:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function PUT(request: NextRequest) {
+ try {
+ const session = await getSession();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const body = await request.json();
+ const { date, count, substance } = body;
+
+ // Set the exact count (replace, not add)
+ const entry = await prisma.usageEntry.upsert({
+ where: {
+ userId_date_substance: {
+ userId: session.user.id,
+ date,
+ substance,
+ },
+ },
+ update: { count },
+ create: {
+ userId: session.user.id,
+ date,
+ count,
+ substance,
+ },
+ });
+
+ return NextResponse.json({
+ date: entry.date,
+ count: entry.count,
+ substance: entry.substance,
+ });
+ } catch (error) {
+ console.error('Error updating usage entry:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
+export async function DELETE(request: NextRequest) {
+ try {
+ const session = await getSession();
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(request.url);
+ const date = searchParams.get('date');
+ const substance = searchParams.get('substance');
+
+ if (!date || !substance) {
+ return NextResponse.json({ error: 'Missing date or substance' }, { status: 400 });
+ }
+
+ await prisma.usageEntry.deleteMany({
+ where: {
+ userId: session.user.id,
+ date,
+ substance,
+ },
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Error deleting usage entry:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
index ec80446..1e77bf9 100644
--- a/src/components/Dashboard.tsx
+++ b/src/components/Dashboard.tsx
@@ -3,15 +3,14 @@
import { useState, useEffect, useCallback } from 'react';
import { User } from '@/lib/session';
import {
- getUsageData,
- getPreferences,
- savePreferences,
- saveUsageEntry,
+ fetchPreferences,
+ fetchUsageData,
+ savePreferencesAsync,
+ saveUsageEntryAsync,
shouldShowUsagePrompt,
hasOneWeekOfData,
calculateWeeklyAverage,
generateQuitPlan,
- setCurrentUserId,
UserPreferences,
UsageEntry,
} from '@/lib/storage';
@@ -36,32 +35,34 @@ export function Dashboard({ user }: DashboardProps) {
const [isLoading, setIsLoading] = useState(true);
const [refreshKey, setRefreshKey] = useState(0);
- const loadData = useCallback(() => {
- // Always pass user.id explicitly to ensure correct data is loaded
- const prefs = getPreferences(user.id);
- const usage = getUsageData(user.id);
+ const loadData = useCallback(async () => {
+ const [prefs, usage] = await Promise.all([
+ fetchPreferences(),
+ fetchUsageData(),
+ ]);
setPreferences(prefs);
setUsageData(usage);
setRefreshKey(prev => prev + 1);
return prefs;
- }, [user.id]);
+ }, []);
useEffect(() => {
- // Set the current user ID for all storage operations
- setCurrentUserId(user.id);
+ const init = async () => {
+ const prefs = await loadData();
- const prefs = loadData();
+ if (!prefs.hasCompletedSetup) {
+ setShowSetup(true);
+ } else if (shouldShowUsagePrompt()) {
+ setShowUsagePrompt(true);
+ }
- if (!prefs.hasCompletedSetup) {
- setShowSetup(true);
- } else if (shouldShowUsagePrompt()) {
- setShowUsagePrompt(true);
- }
+ setIsLoading(false);
+ };
- setIsLoading(false);
- }, [user.id, loadData]);
+ init();
+ }, [loadData]);
- const handleSetupComplete = (data: { substance: 'nicotine' | 'weed'; name: string; age: number }) => {
+ const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number }) => {
const today = new Date().toISOString().split('T')[0];
const newPrefs: UserPreferences = {
substance: data.substance,
@@ -72,14 +73,14 @@ export function Dashboard({ user }: DashboardProps) {
userName: data.name,
userAge: data.age,
};
- savePreferences(newPrefs, user.id);
+ await savePreferencesAsync(newPrefs);
setPreferences(newPrefs);
setShowSetup(false);
setShowUsagePrompt(true);
setRefreshKey(prev => prev + 1);
};
- const handleUsageSubmit = (count: number) => {
+ const handleUsageSubmit = async (count: number) => {
if (!preferences) {
setShowUsagePrompt(false);
return;
@@ -87,26 +88,26 @@ export function Dashboard({ user }: DashboardProps) {
if (count > 0) {
const today = new Date().toISOString().split('T')[0];
- saveUsageEntry({
+ await saveUsageEntryAsync({
date: today,
count,
substance: preferences.substance,
- }, user.id);
+ });
}
setShowUsagePrompt(false);
// Reload data and force calendar refresh
- const usage = getUsageData(user.id);
+ const usage = await fetchUsageData();
setUsageData(usage);
setRefreshKey(prev => prev + 1);
};
- const handleGeneratePlan = () => {
+ const handleGeneratePlan = async () => {
if (!preferences) return;
- const plan = generateQuitPlan(preferences.substance, user.id);
+ const plan = generateQuitPlan(preferences.substance);
const updatedPrefs = { ...preferences, quitPlan: plan };
- savePreferences(updatedPrefs, user.id);
+ await savePreferencesAsync(updatedPrefs);
setPreferences(updatedPrefs);
};
@@ -159,9 +160,9 @@ export function Dashboard({ user }: DashboardProps) {
diff --git a/src/components/UsageCalendar.tsx b/src/components/UsageCalendar.tsx
index dfc4427..ecefa9d 100644
--- a/src/components/UsageCalendar.tsx
+++ b/src/components/UsageCalendar.tsx
@@ -12,7 +12,7 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
-import { UsageEntry, getUsageForDate, setUsageForDate, clearDayData } from '@/lib/storage';
+import { UsageEntry, getUsageForDate, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
interface UsageCalendarProps {
@@ -22,7 +22,7 @@ interface UsageCalendarProps {
userId: string;
}
-export function UsageCalendar({ usageData, substance, onDataUpdate, userId }: UsageCalendarProps) {
+export function UsageCalendar({ usageData, substance, onDataUpdate }: UsageCalendarProps) {
const [selectedDate, setSelectedDate] = useState(undefined);
const [editCount, setEditCount] = useState('');
const [isEditing, setIsEditing] = useState(false);
@@ -37,16 +37,16 @@ export function UsageCalendar({ usageData, substance, onDataUpdate, userId }: Us
setSelectedDate(date);
const dateStr = date.toISOString().split('T')[0];
- const currentCount = getUsageForDate(dateStr, substance, userId);
+ const currentCount = getUsageForDate(dateStr, substance);
setEditCount(currentCount.toString());
setIsEditing(true);
};
- const handleSave = () => {
+ const handleSave = async () => {
if (selectedDate) {
const dateStr = selectedDate.toISOString().split('T')[0];
const newCount = parseInt(editCount, 10) || 0;
- setUsageForDate(dateStr, newCount, substance, userId);
+ await setUsageForDateAsync(dateStr, newCount, substance);
onDataUpdate();
}
setIsEditing(false);
@@ -60,10 +60,10 @@ export function UsageCalendar({ usageData, substance, onDataUpdate, userId }: Us
setEditCount('');
};
- const handleClearDay = () => {
+ const handleClearDay = async () => {
if (selectedDate) {
const dateStr = selectedDate.toISOString().split('T')[0];
- clearDayData(dateStr, substance, userId);
+ await clearDayDataAsync(dateStr, substance);
onDataUpdate();
}
setIsEditing(false);
diff --git a/src/components/UserHeader.tsx b/src/components/UserHeader.tsx
index 326ffb3..617de1c 100644
--- a/src/components/UserHeader.tsx
+++ b/src/components/UserHeader.tsx
@@ -3,7 +3,7 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { User } from '@/lib/session';
-import { getPreferences } from '@/lib/storage';
+import { fetchPreferences } from '@/lib/storage';
import { useEffect, useState } from 'react';
interface UserHeaderProps {
@@ -14,9 +14,12 @@ export function UserHeader({ user }: UserHeaderProps) {
const [userName, setUserName] = useState(null);
useEffect(() => {
- const prefs = getPreferences(user.id);
- setUserName(prefs.userName);
- }, [user.id]);
+ const loadUserName = async () => {
+ const prefs = await fetchPreferences();
+ setUserName(prefs.userName);
+ };
+ loadUserName();
+ }, []);
const initials = [user.firstName?.[0], user.lastName?.[0]]
.filter(Boolean)
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
new file mode 100644
index 0000000..a8dd9fa
--- /dev/null
+++ b/src/lib/prisma.ts
@@ -0,0 +1,11 @@
+import { PrismaClient } from '@prisma/client';
+
+const globalForPrisma = globalThis as unknown as {
+ prisma: PrismaClient | undefined;
+};
+
+export const prisma = globalForPrisma.prisma ?? new PrismaClient();
+
+if (process.env.NODE_ENV !== 'production') {
+ globalForPrisma.prisma = prisma;
+}
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
index 8bccfb9..b250ded 100644
--- a/src/lib/storage.ts
+++ b/src/lib/storage.ts
@@ -1,5 +1,5 @@
// Client-side storage utilities for tracking data
-// All data is stored per-user using their unique ID as a prefix
+// Now uses API calls to persist data in SQLite database
export interface UsageEntry {
date: string; // ISO date string YYYY-MM-DD
@@ -24,120 +24,179 @@ export interface QuitPlan {
baselineAverage: number;
}
-const CURRENT_USER_KEY = 'quit_smoking_current_user';
+const defaultPreferences: UserPreferences = {
+ substance: 'nicotine',
+ trackingStartDate: null,
+ hasCompletedSetup: false,
+ dailyGoal: null,
+ quitPlan: null,
+ userName: null,
+ userAge: null,
+};
-function getStorageKey(baseKey: string, userId?: string): string {
- const uid = userId || getCurrentUserId();
- if (!uid) return baseKey;
- return `${baseKey}_${uid}`;
+// Cache for preferences and usage data to avoid excessive API calls
+let preferencesCache: UserPreferences | null = null;
+let usageDataCache: UsageEntry[] | null = null;
+
+export function clearCache(): void {
+ preferencesCache = null;
+ usageDataCache = null;
}
-export function setCurrentUserId(userId: string): void {
- if (typeof window === 'undefined') return;
- localStorage.setItem(CURRENT_USER_KEY, userId);
+// These functions are kept for backwards compatibility but no longer used
+export function setCurrentUserId(_userId: string): void {
+ // No-op - user ID is now managed by session
}
export function getCurrentUserId(): string | null {
- if (typeof window === 'undefined') return null;
- return localStorage.getItem(CURRENT_USER_KEY);
+ return null;
}
-export function getUsageData(userId?: string): UsageEntry[] {
- if (typeof window === 'undefined') return [];
- const key = getStorageKey('quit_smoking_usage', userId);
- const data = localStorage.getItem(key);
- return data ? JSON.parse(data) : [];
-}
-
-export function saveUsageEntry(entry: UsageEntry, userId?: string): void {
- if (typeof window === 'undefined') return;
- const key = getStorageKey('quit_smoking_usage', userId);
- const data = getUsageData(userId);
- const existingIndex = data.findIndex(
- (e) => e.date === entry.date && e.substance === entry.substance
- );
-
- if (existingIndex >= 0) {
- data[existingIndex].count += entry.count;
- } else {
- data.push(entry);
+// Async API functions
+export async function fetchPreferences(): Promise {
+ try {
+ const response = await fetch('/api/preferences');
+ if (!response.ok) {
+ console.error('Failed to fetch preferences');
+ return defaultPreferences;
+ }
+ const data = await response.json();
+ preferencesCache = data;
+ return data;
+ } catch (error) {
+ console.error('Error fetching preferences:', error);
+ return defaultPreferences;
}
-
- localStorage.setItem(key, JSON.stringify(data));
}
-export function setUsageForDate(date: string, count: number, substance: 'nicotine' | 'weed', userId?: string): void {
- if (typeof window === 'undefined') return;
- const key = getStorageKey('quit_smoking_usage', userId);
- const data = getUsageData(userId);
- const existingIndex = data.findIndex(
- (e) => e.date === date && e.substance === substance
- );
-
- if (existingIndex >= 0) {
- data[existingIndex].count = count;
- } else {
- data.push({ date, count, substance });
+export async function savePreferencesAsync(preferences: UserPreferences): Promise {
+ try {
+ const response = await fetch('/api/preferences', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(preferences),
+ });
+ if (response.ok) {
+ preferencesCache = preferences;
+ }
+ } catch (error) {
+ console.error('Error saving preferences:', error);
}
-
- localStorage.setItem(key, JSON.stringify(data));
}
-export function getUsageForDate(date: string, substance: 'nicotine' | 'weed', userId?: string): number {
- const data = getUsageData(userId);
+export async function fetchUsageData(): Promise {
+ try {
+ const response = await fetch('/api/usage');
+ if (!response.ok) {
+ console.error('Failed to fetch usage data');
+ return [];
+ }
+ const data = await response.json();
+ usageDataCache = data;
+ return data;
+ } catch (error) {
+ console.error('Error fetching usage data:', error);
+ return [];
+ }
+}
+
+export async function saveUsageEntryAsync(entry: UsageEntry): Promise {
+ try {
+ await fetch('/api/usage', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(entry),
+ });
+ usageDataCache = null; // Invalidate cache
+ } catch (error) {
+ console.error('Error saving usage entry:', error);
+ }
+}
+
+export async function setUsageForDateAsync(
+ date: string,
+ count: number,
+ substance: 'nicotine' | 'weed'
+): Promise {
+ try {
+ await fetch('/api/usage', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ date, count, substance }),
+ });
+ usageDataCache = null; // Invalidate cache
+ } catch (error) {
+ console.error('Error setting usage for date:', error);
+ }
+}
+
+export async function clearDayDataAsync(
+ date: string,
+ substance: 'nicotine' | 'weed'
+): Promise {
+ try {
+ await fetch(`/api/usage?date=${date}&substance=${substance}`, {
+ method: 'DELETE',
+ });
+ usageDataCache = null; // Invalidate cache
+ } catch (error) {
+ console.error('Error clearing day data:', error);
+ }
+}
+
+// Synchronous functions that use cache (for backwards compatibility)
+// These should be replaced with async versions in components
+
+export function getPreferences(_userId?: string): UserPreferences {
+ return preferencesCache || defaultPreferences;
+}
+
+export function getUsageData(_userId?: string): UsageEntry[] {
+ return usageDataCache || [];
+}
+
+export function savePreferences(preferences: UserPreferences, _userId?: string): void {
+ preferencesCache = preferences;
+ savePreferencesAsync(preferences);
+}
+
+export function saveUsageEntry(entry: UsageEntry, _userId?: string): void {
+ saveUsageEntryAsync(entry);
+}
+
+export function setUsageForDate(
+ date: string,
+ count: number,
+ substance: 'nicotine' | 'weed',
+ _userId?: string
+): void {
+ setUsageForDateAsync(date, count, substance);
+}
+
+export function getUsageForDate(
+ date: string,
+ substance: 'nicotine' | 'weed',
+ _userId?: string
+): number {
+ const data = usageDataCache || [];
const entry = data.find((e) => e.date === date && e.substance === substance);
return entry?.count ?? 0;
}
-export function getPreferences(userId?: string): UserPreferences {
- const defaultPrefs: UserPreferences = {
- substance: 'nicotine',
- trackingStartDate: null,
- hasCompletedSetup: false,
- dailyGoal: null,
- quitPlan: null,
- userName: null,
- userAge: null,
- };
-
- if (typeof window === 'undefined') {
- return defaultPrefs;
- }
-
- const key = getStorageKey('quit_smoking_preferences', userId);
- const data = localStorage.getItem(key);
- if (!data) {
- return defaultPrefs;
- }
-
- return { ...defaultPrefs, ...JSON.parse(data) };
-}
-
-export function savePreferences(preferences: UserPreferences, userId?: string): void {
- if (typeof window === 'undefined') return;
- const key = getStorageKey('quit_smoking_preferences', userId);
- localStorage.setItem(key, JSON.stringify(preferences));
-}
-
-export function getLastPromptDate(userId?: string): string | null {
- if (typeof window === 'undefined') return null;
- const key = getStorageKey('quit_smoking_last_prompt', userId);
- return localStorage.getItem(key);
-}
-
-export function setLastPromptDate(date: string, userId?: string): void {
- if (typeof window === 'undefined') return;
- const key = getStorageKey('quit_smoking_last_prompt', userId);
- localStorage.setItem(key, date);
+export function clearDayData(
+ date: string,
+ substance: 'nicotine' | 'weed',
+ _userId?: string
+): void {
+ clearDayDataAsync(date, substance);
}
export function shouldShowUsagePrompt(): boolean {
- // Always show the prompt - users can log multiple times throughout the day
return true;
}
-export function getWeeklyData(substance: 'nicotine' | 'weed', userId?: string): UsageEntry[] {
- const data = getUsageData(userId);
+export function getWeeklyData(substance: 'nicotine' | 'weed', _userId?: string): UsageEntry[] {
+ const data = usageDataCache || [];
const today = new Date();
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
@@ -148,17 +207,17 @@ export function getWeeklyData(substance: 'nicotine' | 'weed', userId?: string):
});
}
-export function calculateWeeklyAverage(substance: 'nicotine' | 'weed', userId?: string): number {
- const weeklyData = getWeeklyData(substance, userId);
+export function calculateWeeklyAverage(substance: 'nicotine' | 'weed', _userId?: string): number {
+ const weeklyData = getWeeklyData(substance);
if (weeklyData.length === 0) return 0;
const total = weeklyData.reduce((sum, entry) => sum + entry.count, 0);
return Math.round(total / weeklyData.length);
}
-export function hasOneWeekOfData(substance: 'nicotine' | 'weed', userId?: string): boolean {
- const prefs = getPreferences(userId);
- if (!prefs.trackingStartDate) return false;
+export function hasOneWeekOfData(substance: 'nicotine' | 'weed', _userId?: string): boolean {
+ const prefs = preferencesCache;
+ if (!prefs?.trackingStartDate) return false;
const startDate = new Date(prefs.trackingStartDate);
const today = new Date();
@@ -167,8 +226,8 @@ export function hasOneWeekOfData(substance: 'nicotine' | 'weed', userId?: string
return daysDiff >= 7;
}
-export function generateQuitPlan(substance: 'nicotine' | 'weed', userId?: string): QuitPlan {
- const baseline = calculateWeeklyAverage(substance, userId);
+export function generateQuitPlan(substance: 'nicotine' | 'weed', _userId?: string): QuitPlan {
+ const baseline = calculateWeeklyAverage(substance);
const today = new Date();
const startDate = today.toISOString().split('T')[0];
@@ -192,9 +251,9 @@ export function generateQuitPlan(substance: 'nicotine' | 'weed', userId?: string
};
}
-export function getCurrentWeekTarget(userId?: string): number | null {
- const prefs = getPreferences(userId);
- if (!prefs.quitPlan) return null;
+export function getCurrentWeekTarget(_userId?: string): number | null {
+ const prefs = preferencesCache;
+ if (!prefs?.quitPlan) return null;
const startDate = new Date(prefs.quitPlan.startDate);
const today = new Date();
@@ -207,20 +266,11 @@ export function getCurrentWeekTarget(userId?: string): number | null {
return prefs.quitPlan.weeklyTargets[weekNumber];
}
-export function clearDayData(date: string, substance: 'nicotine' | 'weed', userId?: string): void {
- if (typeof window === 'undefined') return;
- const key = getStorageKey('quit_smoking_usage', userId);
- const data = getUsageData(userId);
- const filtered = data.filter((e) => !(e.date === date && e.substance === substance));
- localStorage.setItem(key, JSON.stringify(filtered));
+export async function clearAllDataAsync(): Promise {
+ // This would need a dedicated API endpoint
+ console.warn('clearAllData not implemented for API-based storage');
}
-export function clearAllData(userId?: string): void {
- if (typeof window === 'undefined') return;
- const usageKey = getStorageKey('quit_smoking_usage', userId);
- const prefsKey = getStorageKey('quit_smoking_preferences', userId);
- const promptKey = getStorageKey('quit_smoking_last_prompt', userId);
- localStorage.removeItem(usageKey);
- localStorage.removeItem(prefsKey);
- localStorage.removeItem(promptKey);
+export function clearAllData(_userId?: string): void {
+ clearAllDataAsync();
}