mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-15 06:04:42 +00:00
Changed backend to work with supabase instead of sqlite
This commit is contained in:
parent
ef8f959f57
commit
f1ca72a782
10 changed files with 362 additions and 784 deletions
2
.env
Normal file
2
.env
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
VITE_SUPABASE_URL=https://niuilwzhthqanmkzvisb.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5pdWlsd3podGhxYW5ta3p2aXNiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzIxNDgyNTgsImV4cCI6MjA0NzcyNDI1OH0.IP0FneBf396qfS0kNZ9R6ErEuR5kKsi562cj8pPZgLw
|
||||
879
package-lock.json
generated
879
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`}>
|
||||
|
|
|
|||
|
|
@ -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
11
src/lib/supabase.ts
Normal 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);
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
48
src/types/supabase.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/utils/streakCalculator.ts
Normal file
48
src/utils/streakCalculator.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue