mirror of
https://github.com/nagaoo0/HabbitGrid.git
synced 2026-01-11 15:34:54 +00:00
Add Icon Options
This commit is contained in:
21
package-lock.json
generated
21
package-lock.json
generated
@@ -93,7 +93,6 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -691,7 +690,6 @@
|
||||
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1536,7 +1534,6 @@
|
||||
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
@@ -4135,7 +4132,6 @@
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -4160,7 +4156,6 @@
|
||||
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -4172,7 +4167,6 @@
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
@@ -4264,7 +4258,6 @@
|
||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
@@ -4504,7 +4497,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5049,7 +5041,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
@@ -5834,7 +5825,6 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -7384,7 +7374,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -8127,7 +8116,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8285,7 +8273,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -8298,7 +8285,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -8502,8 +8488,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
@@ -9354,7 +9339,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
|
||||
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -9548,7 +9532,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -9932,7 +9915,6 @@
|
||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -10026,7 +10008,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -6,6 +6,14 @@ import { Button } from './ui/button';
|
||||
import MiniGrid from './MiniGrid';
|
||||
import AnimatedCounter from './AnimatedCounter';
|
||||
|
||||
|
||||
// Helper to get streak icon from localStorage or fallback
|
||||
function getStreakIcon() {
|
||||
const icon = typeof window !== 'undefined' ? localStorage.getItem('streakIcon') : null;
|
||||
if (!icon || icon === 'flame') return <Flame className="w-4 h-4 text-orange-500" />;
|
||||
return <span className="w-4 h-4 text-lg align-text-bottom" role="img" aria-label="Streak Icon">{icon}</span>;
|
||||
}
|
||||
|
||||
const HabitCard = ({ habit, onUpdate }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -27,11 +35,11 @@ const HabitCard = ({ habit, onUpdate }) => {
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Flame className="w-4 h-4 text-orange-500" />
|
||||
<span><AnimatedCounter value={habit.currentStreak || 0} duration={800} /> day streak</span>
|
||||
{getStreakIcon()}
|
||||
<span><AnimatedCounter value={habit.currentStreak || 0} duration={800} /> day streak</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<span>Personal Record: <AnimatedCounter value={habit.longestStreak || 0} duration={800} /> days</span>
|
||||
<span>Personal Record: <AnimatedCounter value={habit.longestStreak || 0} duration={800} /> days</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -13,6 +13,30 @@ function lightenColor(hex, percent) {
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
import { Flame } from 'lucide-react';
|
||||
// Helpers to get custom icons from localStorage or fallback
|
||||
function getStreakIcon() {
|
||||
if (typeof window === 'undefined') return (
|
||||
<span className="flex items-center justify-center w-full h-full">
|
||||
<Flame className="w-4 h-4 drop-shadow-lg" />
|
||||
</span>
|
||||
);
|
||||
const icon = localStorage.getItem('streakIcon');
|
||||
if (!icon || icon === 'flame') return (
|
||||
<span className="flex items-center justify-center w-full h-full">
|
||||
<Flame className="w-4 h-4 drop-shadow-lg" />
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span className="flex items-center justify-center w-full h-full">
|
||||
<span className="text-lg" role="img" aria-label="Streak Icon">{icon}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
function getFreezeIcon() {
|
||||
if (typeof window === 'undefined') return '❄️';
|
||||
const icon = localStorage.getItem('freezeIcon');
|
||||
return icon || '❄️';
|
||||
}
|
||||
import { motion } from 'framer-motion';
|
||||
import { getColorIntensity, isToday, formatDate } from '../lib/utils-habit';
|
||||
import { getFrozenDays } from '../lib/utils-habit';
|
||||
@@ -122,7 +146,7 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
||||
}}
|
||||
transition={{ duration: 0.7, ease: 'easeInOut' }}
|
||||
>
|
||||
❄️
|
||||
{getFreezeIcon()}
|
||||
</motion.span>
|
||||
)}
|
||||
{/* Flame icon for full streak days */}
|
||||
@@ -147,6 +171,7 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
||||
whileTap={{ scale: 1.2, rotate: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
className="flex items-center justify-center w-full h-full"
|
||||
animate={{ rotate: [0, 12, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
@@ -154,12 +179,8 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
||||
duration: 2,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}
|
||||
>
|
||||
<Flame
|
||||
className="w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5 drop-shadow-lg"
|
||||
style={{ color: lightenColor(habit.color, 0.4), filter: 'brightness(1.3) drop-shadow(0 0 6px white)' }}
|
||||
/>
|
||||
{getStreakIcon()}
|
||||
</motion.div>
|
||||
</motion.span>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, Moon, Sun, Bell, Download, Upload, Trash2, Plus, Trash, GitBranch } from 'lucide-react';
|
||||
import { ArrowLeft, Moon, Sun, Bell, Download, Upload, Trash2, Plus, Trash, GitBranch, Flame } from 'lucide-react';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Switch } from '../components/ui/switch';
|
||||
import { Label } from '../components/ui/label';
|
||||
@@ -9,7 +9,42 @@ import { useToast } from '../components/ui/use-toast';
|
||||
import { exportData, importData, clearAllData } from '../lib/storage';
|
||||
import { addIntegration, getIntegrations, removeIntegration, getGitEnabled, setGitEnabled, fetchAllGitActivity, getCachedGitActivity } from '../lib/git';
|
||||
|
||||
const DEFAULT_STREAK_ICON = 'flame';
|
||||
const DEFAULT_FREEZE_ICON = '❄️';
|
||||
|
||||
const ICON_OPTIONS = [
|
||||
{ label: 'Flame', value: 'flame', icon: <Flame className="inline w-5 h-5 text-orange-500 align-text-bottom" /> },
|
||||
{ label: 'Fire (emoji)', value: '🔥', icon: <span role="img" aria-label="Fire" className="inline text-lg align-text-bottom">🔥</span> },
|
||||
{ label: 'Star', value: '⭐', icon: <span role="img" aria-label="Star" className="inline text-lg align-text-bottom">⭐</span> },
|
||||
{ label: 'Trophy', value: '🏆', icon: <span role="img" aria-label="Trophy" className="inline text-lg align-text-bottom">🏆</span> },
|
||||
{ label: 'Rocket', value: '🚀', icon: <span role="img" aria-label="Rocket" className="inline text-lg align-text-bottom">🚀</span> },
|
||||
{ label: 'Rose', value: '🌹', icon: <span role="img" aria-label="Rose" className="inline text-lg align-text-bottom">🌹</span> },
|
||||
];
|
||||
const FREEZE_OPTIONS = [
|
||||
{ label: 'Snowflake', value: '❄️', icon: <span role="img" aria-label="Snowflake" className="inline text-lg align-text-bottom">❄️</span> },
|
||||
{ label: 'Ice', value: '🧊', icon: <span role="img" aria-label="Ice" className="inline text-lg align-text-bottom">🧊</span> },
|
||||
{ label: 'Snowman', value: '☃️', icon: <span role="img" aria-label="Snowman" className="inline text-lg align-text-bottom">☃️</span> },
|
||||
{ label: 'Cloud', value: '☁️', icon: <span role="img" aria-label="Cloud" className="inline text-lg align-text-bottom">☁️</span> },
|
||||
{ label: 'Withered Flower', value: '🥀', icon: <span role="img" aria-label="Withered Flower" className="inline text-lg align-text-bottom">🥀</span> },
|
||||
];
|
||||
|
||||
const SettingsPage = () => {
|
||||
// Appearance customization state
|
||||
const [streakIcon, setStreakIcon] = useState(() => localStorage.getItem('streakIcon') || DEFAULT_STREAK_ICON);
|
||||
const [freezeIcon, setFreezeIcon] = useState(() => localStorage.getItem('freezeIcon') || DEFAULT_FREEZE_ICON);
|
||||
// Save icon selections to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem('streakIcon', streakIcon);
|
||||
}, [streakIcon]);
|
||||
useEffect(() => {
|
||||
localStorage.setItem('freezeIcon', freezeIcon);
|
||||
}, [freezeIcon]);
|
||||
// Render icon for preview
|
||||
const renderStreakIcon = (icon) => {
|
||||
if (icon === 'flame') return <Flame className="inline w-5 h-5 text-orange-500 align-text-bottom" />;
|
||||
return <span className="inline text-lg align-text-bottom">{icon}</span>;
|
||||
};
|
||||
const renderFreezeIcon = (icon) => <span className="inline text-lg align-text-bottom">{icon}</span>;
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [darkMode, setDarkMode] = useState(() => {
|
||||
@@ -150,6 +185,7 @@ const SettingsPage = () => {
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
|
||||
{/* Appearance */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -158,19 +194,63 @@ const SettingsPage = () => {
|
||||
className="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-700"
|
||||
>
|
||||
<h2 className="text-lg font-semibold mb-4">Appearance</h2>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{darkMode ? <Moon className="w-5 h-5" /> : <Sun className="w-5 h-5" />}
|
||||
<div>
|
||||
<Label htmlFor="dark-mode" className="text-base">Dark Mode</Label>
|
||||
<p className="text-sm text-muted-foreground">Toggle dark theme</p>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{darkMode ? <Moon className="w-5 h-5" /> : <Sun className="w-5 h-5" />}
|
||||
<div>
|
||||
<Label htmlFor="dark-mode" className="text-base">Dark Mode</Label>
|
||||
<p className="text-sm text-muted-foreground">Toggle dark theme</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="dark-mode"
|
||||
checked={darkMode}
|
||||
onCheckedChange={toggleDarkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Streak Icon Picker */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{renderStreakIcon(streakIcon)}
|
||||
<div>
|
||||
<Label htmlFor="streak-icon" className="text-base">Streak Icon</Label>
|
||||
<p className="text-sm text-muted-foreground">Choose your streak icon</p>
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
id="streak-icon"
|
||||
className="border rounded-md p-2 bg-white dark:bg-slate-900 text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors"
|
||||
value={streakIcon}
|
||||
onChange={e => setStreakIcon(e.target.value)}
|
||||
>
|
||||
{ICON_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Freeze Icon Picker */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{renderFreezeIcon(freezeIcon)}
|
||||
<div>
|
||||
<Label htmlFor="freeze-icon" className="text-base">Freeze Icon</Label>
|
||||
<p className="text-sm text-muted-foreground">Choose your freeze icon</p>
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
id="freeze-icon"
|
||||
className="border rounded-md p-2 bg-white dark:bg-slate-900 text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors"
|
||||
value={freezeIcon}
|
||||
onChange={e => setFreezeIcon(e.target.value)}
|
||||
>
|
||||
{FREEZE_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Switch
|
||||
id="dark-mode"
|
||||
checked={darkMode}
|
||||
onCheckedChange={toggleDarkMode}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -254,7 +334,7 @@ const SettingsPage = () => {
|
||||
<div className="grid sm:grid-cols-4 gap-2 mb-3">
|
||||
<div>
|
||||
<Label className="text-xs">Provider</Label>
|
||||
<select className="w-full bg-transparent border rounded-md p-2" value={form.provider} onChange={e => setForm({ ...form, provider: e.target.value })}>
|
||||
<select className="w-full border rounded-md p-2 bg-white dark:bg-slate-900 text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-primary/50 transition-colors" value={form.provider} onChange={e => setForm({ ...form, provider: e.target.value })}>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="gitlab">GitLab</option>
|
||||
<option value="gitea">Gitea</option>
|
||||
|
||||
Reference in New Issue
Block a user