mirror of
https://github.com/nagaoo0/HabbitGrid.git
synced 2026-04-19 15:23:16 +00:00
Compare commits
5 Commits
1.0.0
...
6dbb690e3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dbb690e3d | ||
| af1f8a8ac0 | |||
| b6a277cabf | |||
| bb64bacd1e | |||
| 7b513bca28 |
@@ -20,7 +20,7 @@ A modern, grid-based habit tracker app inspired by GitHub contribution graphs. T
|
|||||||
### Installation
|
### Installation
|
||||||
```powershell
|
```powershell
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/yourusername/habitgrid.git
|
git clone https://github.com/nagaoo0/habitgrid.git
|
||||||
cd habitgrid
|
cd habitgrid
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
|||||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"lucide-react": "^0.285.0",
|
"lucide-react": "^0.285.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -4943,6 +4944,15 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.16",
|
"version": "2.8.16",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
|
||||||
@@ -5296,6 +5306,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -6762,6 +6781,19 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -9349,6 +9381,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -9786,6 +9827,15 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.9",
|
"version": "7.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"lucide-react": "^0.285.0",
|
"lucide-react": "^0.285.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { getColorIntensity, isToday, formatDate, getWeekdayLabel } from '../lib/utils-habit';
|
import { getColorIntensity, isToday, formatDate, getWeekdayLabel } from '../lib/utils-habit';
|
||||||
import { toggleCompletion } from '../lib/storage';
|
import { toggleCompletion } from '../lib/storage';
|
||||||
@@ -29,35 +29,30 @@ const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
|||||||
return weeksArray;
|
return weeksArray;
|
||||||
}, [fullView]);
|
}, [fullView]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Scroll to the rightmost (most recent) week on mount
|
||||||
|
const gridScroll = document.querySelector('.grid-scroll');
|
||||||
|
if (gridScroll) {
|
||||||
|
gridScroll.scrollLeft = gridScroll.scrollWidth;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCellClick = (date) => {
|
const handleCellClick = (date) => {
|
||||||
toggleCompletion(habit.id, formatDate(date));
|
toggleCompletion(habit.id, formatDate(date));
|
||||||
onUpdate();
|
onUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-700">
|
<div className="bg-white dark:bg-slate-800 rounded-2xl p-4 pt-0 shadow-sm border border-slate-200 dark:border-slate-700 flex flex-col items-center">
|
||||||
<div className="mb-4">
|
<div className="mb-2 text-center w-full">
|
||||||
<h2 className="text-lg font-semibold mb-1">Activity Calendar</h2>
|
<h2 className="text-lg font-semibold mb-1 mt-4">Activity Calendar</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Tap any day to mark it as complete
|
Tap any day to mark it as complete
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto grid-scroll">
|
<div className="overflow-x-auto grid-scroll mt-2 w-full flex justify-center">
|
||||||
<div className="inline-flex gap-1">
|
<div className="inline-flex gap-1 mb-4">
|
||||||
{/* Weekday labels: Monday (top) to Sunday (bottom) */}
|
|
||||||
<div className="flex flex-col gap-1 mr-2">
|
|
||||||
<div className="h-3" />
|
|
||||||
{[1, 2, 3, 4, 5, 6, 0].map((day) => (
|
|
||||||
<div
|
|
||||||
key={day}
|
|
||||||
className="h-3 flex items-center justify-end text-xs text-muted-foreground pr-1"
|
|
||||||
>
|
|
||||||
{getWeekdayLabel(day)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Grid: Monday (top) to Sunday (bottom) */}
|
{/* Grid: Monday (top) to Sunday (bottom) */}
|
||||||
{weeks.map((week, weekIndex) => (
|
{weeks.map((week, weekIndex) => (
|
||||||
<div key={weekIndex} className="flex flex-col gap-1">
|
<div key={weekIndex} className="flex flex-col gap-1">
|
||||||
@@ -92,6 +87,18 @@ const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{/* Weekday labels: Monday (top) to Sunday (bottom) */}
|
||||||
|
<div className="flex flex-col gap-1 ml-2">
|
||||||
|
<div className="h-1" />
|
||||||
|
{[1, 2, 3, 4, 5, 6, 0].map((day) => (
|
||||||
|
<div
|
||||||
|
key={day}
|
||||||
|
className="h-3 flex items-center justify-end text-xs text-muted-foreground pr-0"
|
||||||
|
>
|
||||||
|
{getWeekdayLabel(day)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,15 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
// Show fewer days on mobile for better aspect ratio
|
// Show fewer days on mobile for better aspect ratio
|
||||||
const isMobile = window.innerWidth < 640; // Tailwind 'sm' breakpoint
|
const isMobile = window.innerWidth < 640; // Tailwind 'sm' breakpoint
|
||||||
const numDays = isMobile ? 14 : 28;
|
const numDays = isMobile ? 11 : 28;
|
||||||
const days = [];
|
const days = [];
|
||||||
|
const scrollRef = React.useRef(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
|
||||||
|
}
|
||||||
|
}, [numDays, habit.completions]);
|
||||||
|
|
||||||
for (let i = numDays - 1; i >= 0; i--) {
|
for (let i = numDays - 1; i >= 0; i--) {
|
||||||
const date = new Date(today);
|
const date = new Date(today);
|
||||||
@@ -23,20 +30,21 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1 overflow-x-auto grid-scroll pb-2">
|
<div ref={scrollRef} className="flex gap-1 overflow-x-auto grid-scroll pb-2">
|
||||||
{days.map((date, index) => {
|
{days.map((date, index) => {
|
||||||
const dateStr = formatDate(date);
|
const dateStr = formatDate(date);
|
||||||
const isCompleted = habit.completions.includes(dateStr);
|
const isCompleted = habit.completions.includes(dateStr);
|
||||||
const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
|
const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
|
||||||
const isTodayCell = isToday(date);
|
const isTodayCell = isToday(date);
|
||||||
|
const dayLetter = date.toLocaleDateString('en-US', { weekday: 'short' })[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div key={index} className="flex flex-col items-center">
|
||||||
<motion.button
|
<motion.button
|
||||||
key={index}
|
|
||||||
whileHover={{ scale: 0.9 }}
|
whileHover={{ scale: 0.9 }}
|
||||||
whileTap={{ scale: 0.5 }}
|
whileTap={{ scale: 0.5 }}
|
||||||
onClick={(e) => handleCellClick(e, date)}
|
onClick={(e) => handleCellClick(e, date)}
|
||||||
className="habit-cell flex w-8 h-8 rounded-2xl transition-all"
|
className={`habit-cell flex w-8 h-8 transition-all ${isTodayCell ? 'rounded-md' : 'rounded-2xl'}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isCompleted
|
backgroundColor: isCompleted
|
||||||
? habit.color
|
? habit.color
|
||||||
@@ -48,6 +56,8 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
|||||||
}}
|
}}
|
||||||
title={dateStr}
|
title={dateStr}
|
||||||
/>
|
/>
|
||||||
|
<span className="text-[10px] text-muted-foreground mt-1">{dayLetter}</span>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user