mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-19 07:03:46 +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",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"vite\" \"node server.js\"",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|
@ -13,16 +13,14 @@
|
||||||
"deploy": "gh-pages -d dist"
|
"deploy": "gh-pages -d dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
|
||||||
"express": "^4.18.3",
|
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0"
|
||||||
"sql.js": "^1.10.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
|
"@supabase/supabase-js": "^2.46.1",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@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
|
// 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) => {
|
const handleToggleHabit = async (e: React.MouseEvent, habitId: number, date: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -115,7 +117,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
return days.map(({ date, dayNumber, isCurrentMonth }) => {
|
return days.map(({ date, dayNumber, isCurrentMonth }) => {
|
||||||
const completedHabits = getCompletedHabitsForDate(date);
|
const completedHabits = getCompletedHabitsForDate(date);
|
||||||
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
|
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
|
||||||
const isToday = date === today;
|
const isToday = date === todayStr;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -124,7 +126,13 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
border rounded-lg p-3 min-h-[80px] relative
|
border rounded-lg p-3 min-h-[80px] relative
|
||||||
${theme.border}
|
${theme.border}
|
||||||
${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth}
|
${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}`}>
|
<span className={`font-medium ${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}`}>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,36 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { supabase } from '../lib/supabase';
|
||||||
import { Habit } from '../types';
|
import { Habit } from '../types';
|
||||||
|
import { calculateStreak } from '../utils/streakCalculator';
|
||||||
|
|
||||||
export const useHabits = () => {
|
export const useHabits = () => {
|
||||||
const [habits, setHabits] = useState<Habit[]>([]);
|
const [habits, setHabits] = useState<Habit[]>([]);
|
||||||
|
|
||||||
const fetchHabits = async () => {
|
const fetchHabits = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:5000/api/habits');
|
const { data, error } = await supabase
|
||||||
const data = await response.json();
|
.from('habits')
|
||||||
setHabits(data);
|
.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) {
|
} catch (error) {
|
||||||
console.error('Error fetching habits:', error);
|
console.error('Error fetching habits:', error);
|
||||||
setHabits([]);
|
setHabits([]);
|
||||||
|
|
@ -17,16 +39,19 @@ export const useHabits = () => {
|
||||||
|
|
||||||
const addHabit = async (name: string) => {
|
const addHabit = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:5000/api/habits', {
|
const { error } = await supabase
|
||||||
method: 'POST',
|
.from('habits')
|
||||||
headers: { 'Content-Type': 'application/json' },
|
.insert([{
|
||||||
body: JSON.stringify({ name }),
|
name,
|
||||||
});
|
best_streak: 0,
|
||||||
if (response.ok) {
|
created_at: new Date().toISOString()
|
||||||
await fetchHabits();
|
}])
|
||||||
return true;
|
.select()
|
||||||
}
|
.single();
|
||||||
return false;
|
|
||||||
|
if (error) throw error;
|
||||||
|
await fetchHabits();
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding habit:', error);
|
console.error('Error adding habit:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -35,11 +60,43 @@ export const useHabits = () => {
|
||||||
|
|
||||||
const toggleHabit = async (id: number, date: string) => {
|
const toggleHabit = async (id: number, date: string) => {
|
||||||
try {
|
try {
|
||||||
await fetch(`http://localhost:5000/api/habits/${id}/toggle`, {
|
const { data: existing } = await supabase
|
||||||
method: 'POST',
|
.from('habit_completions')
|
||||||
headers: { 'Content-Type': 'application/json' },
|
.select()
|
||||||
body: JSON.stringify({ date }),
|
.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();
|
await fetchHabits();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling habit:', error);
|
console.error('Error toggling habit:', error);
|
||||||
|
|
@ -48,11 +105,10 @@ export const useHabits = () => {
|
||||||
|
|
||||||
const updateHabit = async (id: number, name: string) => {
|
const updateHabit = async (id: number, name: string) => {
|
||||||
try {
|
try {
|
||||||
await fetch(`http://localhost:5000/api/habits/${id}`, {
|
await supabase
|
||||||
method: 'PUT',
|
.from('habits')
|
||||||
headers: { 'Content-Type': 'application/json' },
|
.update({ name })
|
||||||
body: JSON.stringify({ name }),
|
.eq('id', id);
|
||||||
});
|
|
||||||
await fetchHabits();
|
await fetchHabits();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating habit:', error);
|
console.error('Error updating habit:', error);
|
||||||
|
|
@ -61,39 +117,22 @@ export const useHabits = () => {
|
||||||
|
|
||||||
const deleteHabit = async (id: number) => {
|
const deleteHabit = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await fetch(`http://localhost:5000/api/habits/${id}`, {
|
await supabase
|
||||||
method: 'DELETE',
|
.from('habits')
|
||||||
});
|
.delete()
|
||||||
|
.eq('id', id);
|
||||||
await fetchHabits();
|
await fetchHabits();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting habit:', 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 {
|
return {
|
||||||
habits,
|
habits,
|
||||||
fetchHabits,
|
fetchHabits,
|
||||||
addHabit,
|
addHabit,
|
||||||
toggleHabit,
|
toggleHabit,
|
||||||
updateHabit,
|
updateHabit,
|
||||||
deleteHabit,
|
deleteHabit
|
||||||
updateStreak
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
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: {
|
day: {
|
||||||
default: 'bg-[#ffffff] hover:bg-[#f1f1ef] text-[#37352f] shadow-sm',
|
default: 'bg-[#ffffff] hover:bg-[#f1f1ef] text-[#37352f] shadow-sm',
|
||||||
selected: 'bg-[#37352f] text-white',
|
selected: 'bg-[#37352f] text-white',
|
||||||
today: 'border-[#37352f]',
|
today: 'border border-black/20 bg-blue-50/10',
|
||||||
otherMonth: 'text-[#787774] bg-[#fafafa]'
|
otherMonth: 'text-[#787774] bg-[#fafafa]'
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
|
|
@ -97,7 +97,7 @@ export const darkTheme = {
|
||||||
day: {
|
day: {
|
||||||
default: 'bg-[#2f2f2f] hover:bg-[#363636] text-[#ffffff] shadow-md shadow-[#00000030]',
|
default: 'bg-[#2f2f2f] hover:bg-[#363636] text-[#ffffff] shadow-md shadow-[#00000030]',
|
||||||
selected: 'bg-[#ffffff] text-[#191919]',
|
selected: 'bg-[#ffffff] text-[#191919]',
|
||||||
today: 'border-[#ffffff]',
|
today: 'border border-white/20 bg-blue-900/5',
|
||||||
otherMonth: 'text-[#666666] bg-[#242424]'
|
otherMonth: 'text-[#666666] bg-[#242424]'
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export interface Habit {
|
export interface Habit {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
best_streak: number;
|
||||||
completedDates: string[];
|
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