Changed backend to work with supabase instead of sqlite

This commit is contained in:
Harivansh Rathi 2024-11-20 19:56:48 -05:00
parent ef8f959f57
commit f1ca72a782
10 changed files with 362 additions and 784 deletions

2
.env Normal file
View file

@ -0,0 +1,2 @@
VITE_SUPABASE_URL=https://niuilwzhthqanmkzvisb.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5pdWlsd3podGhxYW5ta3p2aXNiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzIxNDgyNTgsImV4cCI6MjA0NzcyNDI1OH0.IP0FneBf396qfS0kNZ9R6ErEuR5kKsi562cj8pPZgLw

879
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "concurrently \"vite\" \"node server.js\"",
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
@ -13,16 +13,14 @@
"deploy": "gh-pages -d dist"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.3",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"sql.js": "^1.10.2"
"react-router-dom": "^6.28.0"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@supabase/supabase-js": "^2.46.1",
"@types/express": "^4.17.21",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",

View file

@ -35,7 +35,9 @@ export const Calendar: React.FC<CalendarProps> = ({
};
// Get today's date in YYYY-MM-DD format
const today = formatDate(new Date());
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayStr = formatDate(today);
const handleToggleHabit = async (e: React.MouseEvent, habitId: number, date: string) => {
e.stopPropagation();
@ -115,7 +117,7 @@ export const Calendar: React.FC<CalendarProps> = ({
return days.map(({ date, dayNumber, isCurrentMonth }) => {
const completedHabits = getCompletedHabitsForDate(date);
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
const isToday = date === today;
const isToday = date === todayStr;
return (
<div
@ -124,7 +126,13 @@ export const Calendar: React.FC<CalendarProps> = ({
border rounded-lg p-3 min-h-[80px] relative
${theme.border}
${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth}
${isToday ? `border-2 ${theme.calendar.day.today}` : ''}
${isToday ? `
border-2
${theme.calendar.day.today}
ring-2 ring-offset-2 ring-blue-500 dark:ring-blue-400
bg-blue-50 dark:bg-blue-900/20
shadow-sm
` : ''}
`}
>
<span className={`font-medium ${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}`}>

View file

@ -1,14 +1,36 @@
import { useState } from 'react';
import { supabase } from '../lib/supabase';
import { Habit } from '../types';
import { calculateStreak } from '../utils/streakCalculator';
export const useHabits = () => {
const [habits, setHabits] = useState<Habit[]>([]);
const fetchHabits = async () => {
try {
const response = await fetch('http://localhost:5000/api/habits');
const data = await response.json();
setHabits(data);
const { data, error } = await supabase
.from('habits')
.select(`
id,
name,
created_at,
best_streak,
habit_completions (completion_date)
`);
if (error) throw error;
const formattedHabits = data.map(habit => ({
id: habit.id,
name: habit.name,
created_at: habit.created_at,
best_streak: habit.best_streak,
completedDates: habit.habit_completions.map(
(completion: { completion_date: string }) => completion.completion_date
)
}));
setHabits(formattedHabits);
} catch (error) {
console.error('Error fetching habits:', error);
setHabits([]);
@ -17,16 +39,19 @@ export const useHabits = () => {
const addHabit = async (name: string) => {
try {
const response = await fetch('http://localhost:5000/api/habits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
if (response.ok) {
await fetchHabits();
return true;
}
return false;
const { error } = await supabase
.from('habits')
.insert([{
name,
best_streak: 0,
created_at: new Date().toISOString()
}])
.select()
.single();
if (error) throw error;
await fetchHabits();
return true;
} catch (error) {
console.error('Error adding habit:', error);
return false;
@ -35,11 +60,43 @@ export const useHabits = () => {
const toggleHabit = async (id: number, date: string) => {
try {
await fetch(`http://localhost:5000/api/habits/${id}/toggle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date }),
});
const { data: existing } = await supabase
.from('habit_completions')
.select()
.eq('habit_id', id)
.eq('completion_date', date)
.single();
if (existing) {
await supabase
.from('habit_completions')
.delete()
.eq('habit_id', id)
.eq('completion_date', date);
} else {
await supabase
.from('habit_completions')
.insert([{ habit_id: id, completion_date: date }]);
}
// After toggling, recalculate streak
const habit = habits.find(h => h.id === id);
if (habit) {
const newCompletedDates = existing
? habit.completedDates.filter(d => d !== date)
: [...habit.completedDates, date];
const { bestStreak } = calculateStreak(newCompletedDates);
// Update best_streak if the new streak is higher
if (bestStreak > habit.best_streak) {
await supabase
.from('habits')
.update({ best_streak: bestStreak })
.eq('id', id);
}
}
await fetchHabits();
} catch (error) {
console.error('Error toggling habit:', error);
@ -48,11 +105,10 @@ export const useHabits = () => {
const updateHabit = async (id: number, name: string) => {
try {
await fetch(`http://localhost:5000/api/habits/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
await supabase
.from('habits')
.update({ name })
.eq('id', id);
await fetchHabits();
} catch (error) {
console.error('Error updating habit:', error);
@ -61,39 +117,22 @@ export const useHabits = () => {
const deleteHabit = async (id: number) => {
try {
await fetch(`http://localhost:5000/api/habits/${id}`, {
method: 'DELETE',
});
await supabase
.from('habits')
.delete()
.eq('id', id);
await fetchHabits();
} catch (error) {
console.error('Error deleting habit:', error);
}
};
const updateStreak = async (id: number, newStreak: number) => {
if (newStreak < 0) return;
try {
await fetch(`http://localhost:5000/api/habits/${id}/streak`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ streak: newStreak }),
});
setHabits(habits.map(habit =>
habit.id === id ? { ...habit, manualStreak: newStreak } : habit
));
} catch (error) {
console.error('Error updating streak:', error);
}
};
return {
habits,
fetchHabits,
addHabit,
toggleHabit,
updateHabit,
deleteHabit,
updateStreak
deleteHabit
};
};

11
src/lib/supabase.ts Normal file
View file

@ -0,0 +1,11 @@
import { createClient } from '@supabase/supabase-js';
import { Database } from '../types/supabase';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Missing Supabase environment variables');
}
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);

View file

@ -37,7 +37,7 @@ export const lightTheme = {
day: {
default: 'bg-[#ffffff] hover:bg-[#f1f1ef] text-[#37352f] shadow-sm',
selected: 'bg-[#37352f] text-white',
today: 'border-[#37352f]',
today: 'border border-black/20 bg-blue-50/10',
otherMonth: 'text-[#787774] bg-[#fafafa]'
},
navigation: {
@ -97,7 +97,7 @@ export const darkTheme = {
day: {
default: 'bg-[#2f2f2f] hover:bg-[#363636] text-[#ffffff] shadow-md shadow-[#00000030]',
selected: 'bg-[#ffffff] text-[#191919]',
today: 'border-[#ffffff]',
today: 'border border-white/20 bg-blue-900/5',
otherMonth: 'text-[#666666] bg-[#242424]'
},
navigation: {

View file

@ -1,6 +1,7 @@
export interface Habit {
id: number;
name: string;
created_at: string;
best_streak: number;
completedDates: string[];
bestStreak: number;
}

48
src/types/supabase.ts Normal file
View file

@ -0,0 +1,48 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
public: {
Tables: {
habits: {
Row: {
id: number
name: string
created_at: string
best_streak: number
}
Insert: {
id?: number
name: string
created_at?: string
best_streak?: number
}
Update: {
id?: number
name?: string
created_at?: string
best_streak?: number
}
}
habit_completions: {
Row: {
habit_id: number
completion_date: string
}
Insert: {
habit_id: number
completion_date: string
}
Update: {
habit_id?: number
completion_date?: string
}
}
}
}
}

View file

@ -0,0 +1,48 @@
export function calculateStreak(completedDates: string[]) {
if (!completedDates.length) return { currentStreak: 0, bestStreak: 0 };
const sortedDates = [...completedDates].sort((a, b) =>
new Date(a).getTime() - new Date(b).getTime()
);
const today = new Date();
today.setHours(0, 0, 0, 0);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
let currentStreak = 0;
let bestStreak = 0;
let tempStreak = 0;
let lastDate: Date | null = null;
for (const dateStr of sortedDates) {
const currentDate = new Date(dateStr);
currentDate.setHours(0, 0, 0, 0);
if (lastDate) {
const dayDifference = (currentDate.getTime() - lastDate.getTime()) / (1000 * 3600 * 24);
if (dayDifference === 1) {
tempStreak++;
} else {
tempStreak = 1;
}
} else {
tempStreak = 1;
}
bestStreak = Math.max(bestStreak, tempStreak);
lastDate = currentDate;
}
if (lastDate) {
const lastDateTime = lastDate.getTime();
const isToday = lastDateTime === today.getTime();
const isYesterday = lastDateTime === yesterday.getTime();
if (isToday || isYesterday) {
currentStreak = tempStreak;
}
}
return { currentStreak, bestStreak };
}