mirror of
https://github.com/nagaoo0/HabbitGrid.git
synced 2026-01-11 23:44:55 +00:00
Add Icon Options
This commit is contained in:
@@ -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