add support for habit pinning (#105)

This commit is contained in:
Doh
2025-04-10 16:47:59 -04:00
committed by GitHub
parent f1e3ee5747
commit 685cb80321
6 changed files with 342 additions and 412 deletions

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
## Version 0.2.7
### Added
* visual pin indicators for pinned habits/tasks
* pin/unpin options in context menus
* support click and right-click context menu in dailyoverview
## Version 0.2.6 ## Version 0.2.6
### Added ### Added

View File

@@ -1,4 +1,4 @@
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp, Timer, Plus } from 'lucide-react' import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp, Timer, Plus, Pin, Calendar } from 'lucide-react'
import CompletionCountBadge from './CompletionCountBadge' import CompletionCountBadge from './CompletionCountBadge'
import { import {
ContextMenu, ContextMenu,
@@ -28,6 +28,283 @@ interface UpcomingItemsProps {
coinBalance: number coinBalance: number
} }
interface ItemSectionProps {
title: string;
items: Habit[];
emptyMessage: string;
isTask: boolean;
viewLink: string;
expanded: boolean;
setExpanded: (value: boolean) => void;
addNewItem: () => void;
badgeType: "tasks" | "habits";
todayCompletions: Habit[];
settings: any;
setBrowserSettings: (value: React.SetStateAction<BrowserSettings>) => void;
}
const ItemSection = ({
title,
items,
emptyMessage,
isTask,
viewLink,
expanded,
setExpanded,
addNewItem,
badgeType,
todayCompletions,
settings,
setBrowserSettings,
}: ItemSectionProps) => {
const { completeHabit, undoComplete, saveHabit } = useHabits();
const [_, setPomo] = useAtom(pomodoroAtom);
if (items.length === 0) {
return (
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold">{title}</h3>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-full hover:bg-primary/10 hover:text-primary"
onClick={addNewItem}
>
<Plus className="h-4 w-4" />
<span className="sr-only">Add {isTask ? "Task" : "Habit"}</span>
</Button>
</div>
<div className="text-center text-muted-foreground text-sm py-4">
{emptyMessage}
</div>
</div>
);
}
return (
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold">{title}</h3>
</div>
<div className="flex items-center gap-2">
<CompletionCountBadge type={badgeType} />
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-full hover:bg-primary/10 hover:text-primary"
onClick={addNewItem}
>
<Plus className="h-4 w-4" />
<span className="sr-only">Add {isTask ? "Task" : "Habit"}</span>
</Button>
</div>
</div>
<ul className={`grid gap-2 transition-all duration-300 ease-in-out ${expanded ? 'max-h-none' : 'max-h-[200px]'} overflow-hidden`}>
{items
.sort((a, b) => {
// First by pinned status
if (a.pinned !== b.pinned) {
return a.pinned ? -1 : 1;
}
// Then by completion status
const aCompleted = todayCompletions.includes(a);
const bCompleted = todayCompletions.includes(b);
if (aCompleted !== bCompleted) {
return aCompleted ? 1 : -1;
}
// Then by frequency (daily first)
const aFreq = getHabitFreq(a);
const bFreq = getHabitFreq(b);
const freqOrder = ['daily', 'weekly', 'monthly', 'yearly'];
if (freqOrder.indexOf(aFreq) !== freqOrder.indexOf(bFreq)) {
return freqOrder.indexOf(aFreq) - freqOrder.indexOf(bFreq);
}
// Then by coin reward (higher first)
if (a.coinReward !== b.coinReward) {
return b.coinReward - a.coinReward;
}
// Finally by target completions (higher first)
const aTarget = a.targetCompletions || 1;
const bTarget = b.targetCompletions || 1;
return bTarget - aTarget;
})
.slice(0, expanded ? undefined : 5)
.map((habit) => {
const completionsToday = habit.completions.filter(completion =>
isSameDate(t2d({ timestamp: completion, timezone: settings.system.timezone }), t2d({ timestamp: d2t({ dateTime: getNow({ timezone: settings.system.timezone }) }), timezone: settings.system.timezone }))
).length
const target = habit.targetCompletions || 1
const isCompleted = completionsToday >= target || (isTask && habit.archived)
return (
<li
className={`flex items-center justify-between text-sm p-2 rounded-md
${isCompleted ? 'bg-secondary/50' : 'bg-secondary/20'}`}
key={habit.id}
>
<span className="flex items-center gap-2 flex-1 min-w-0">
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="flex items-center gap-2 cursor-pointer flex-1 min-w-0">
<div className="flex-shrink-0">
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (isCompleted) {
undoComplete(habit);
} else {
completeHabit(habit);
}
}}
className="relative hover:opacity-70 transition-opacity w-4 h-4"
>
{isCompleted ? (
<CircleCheck className="h-4 w-4 text-green-500" />
) : (
<div className="relative h-4 w-4">
<Circle className="absolute h-4 w-4 text-muted-foreground" />
<div
className="absolute h-4 w-4 rounded-full overflow-hidden"
style={{
background: `conic-gradient(
currentColor ${(completionsToday / target) * 360}deg,
transparent ${(completionsToday / target) * 360}deg 360deg
)`,
mask: 'radial-gradient(transparent 50%, black 51%)',
WebkitMask: 'radial-gradient(transparent 50%, black 51%)'
}}
/>
</div>
)}
</button>
</div>
<span className="flex items-center gap-1">
{habit.pinned && (
<Pin className="h-4 w-4 text-yellow-500" />
)}
<Link
href={`/habits?highlight=${habit.id}`}
className={cn(
isCompleted ? 'line-through' : '',
'break-all hover:text-primary transition-colors'
)}
>
{habit.name}
</Link>
</span>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-64">
<ContextMenuItem onClick={() => {
setPomo((prev) => ({
...prev,
show: true,
selectedHabitId: habit.id
}))
}}>
<Timer className="mr-2 h-4 w-4" />
<span>Start Pomodoro</span>
</ContextMenuItem>
{habit.isTask && (
<ContextMenuItem onClick={() => {
saveHabit({...habit, frequency: d2t({ dateTime: getNow({ timezone: settings.system.timezone })})})
}}>
<Calendar className="mr-2 h-4 w-4" />
<span>Move to Today</span>
</ContextMenuItem>
)}
<ContextMenuItem onClick={() => {
saveHabit({ ...habit, pinned: !habit.pinned })
}}>
{habit.pinned ? (
<>
<Pin className="mr-2 h-4 w-4" />
<span>Unpin</span>
</>
) : (
<>
<Pin className="mr-2 h-4 w-4" />
<span>Pin</span>
</>
)}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</span>
<span className="flex items-center gap-2 text-xs text-muted-foreground flex-shrink-0">
{habit.targetCompletions && (
<span className="bg-secondary px-1.5 py-0.5 rounded-full">
{completionsToday}/{target}
</span>
)}
{getHabitFreq(habit) !== 'daily' && (
<Badge variant="outline" className="text-xs">
{getHabitFreq(habit)}
</Badge>
)}
<span className="flex items-center">
<Coins className={cn(
"h-3 w-3 mr-1 transition-all",
isCompleted
? "text-yellow-500 drop-shadow-[0_0_2px_rgba(234,179,8,0.3)]"
: "text-gray-400"
)} />
<span className={cn(
"transition-all",
isCompleted
? "text-yellow-500 font-medium"
: "text-gray-400"
)}>
{habit.coinReward}
</span>
</span>
</span>
</li>
)
})}
</ul>
<div className="flex items-center justify-between">
<button
onClick={() => setExpanded(!expanded)}
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
>
{expanded ? (
<>
Show less
<ChevronUp className="h-3 w-3" />
</>
) : (
<>
Show all
<ChevronDown className="h-3 w-3" />
</>
)}
</button>
<Link
href={viewLink}
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
onClick={() => {
if (isTask) {
setBrowserSettings(prev => ({ ...prev, viewType: 'tasks' }));
} else {
setBrowserSettings(prev => ({ ...prev, viewType: 'habits' }));
}
}}
>
View
<ArrowRight className="h-3 w-3" />
</Link>
</div>
</div>
);
};
export default function DailyOverview({ export default function DailyOverview({
habits, habits,
wishlistItems, wishlistItems,
@@ -37,12 +314,12 @@ export default function DailyOverview({
const [settings] = useAtom(settingsAtom) const [settings] = useAtom(settingsAtom)
const [completedHabitsMap] = useAtom(completedHabitsMapAtom) const [completedHabitsMap] = useAtom(completedHabitsMapAtom)
const [dailyItems] = useAtom(dailyHabitsAtom) const [dailyItems] = useAtom(dailyHabitsAtom)
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom)
const dailyTasks = dailyItems.filter(habit => habit.isTask) const dailyTasks = dailyItems.filter(habit => habit.isTask)
const dailyHabits = dailyItems.filter(habit => !habit.isTask) const dailyHabits = dailyItems.filter(habit => !habit.isTask)
const today = getTodayInTimezone(settings.system.timezone) const today = getTodayInTimezone(settings.system.timezone)
const todayCompletions = completedHabitsMap.get(today) || [] const todayCompletions = completedHabitsMap.get(today) || []
const { saveHabit } = useHabits() const { saveHabit } = useHabits()
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom)
// Get all wishlist items sorted by redeemable status (non-redeemable first) then by coin cost // Get all wishlist items sorted by redeemable status (non-redeemable first) then by coin cost
// Filter out archived wishlist items // Filter out archived wishlist items
@@ -80,414 +357,38 @@ export default function DailyOverview({
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
{/* Tasks Section */} {/* Tasks Section */}
{hasTasks && dailyTasks.length === 0 ? ( {hasTasks && (
<div> <ItemSection
<div className="flex items-center justify-between mb-2"> title="Daily Tasks"
<h3 className="font-semibold">Daily Tasks</h3> items={dailyTasks}
<Button emptyMessage="No tasks due today. Add some tasks to get started!"
variant="ghost" isTask={true}
size="sm" viewLink="/habits?view=tasks"
className="h-7 w-7 rounded-full hover:bg-primary/10 hover:text-primary" expanded={browserSettings.expandedTasks}
onClick={() => { setExpanded={(value) => setBrowserSettings(prev => ({ ...prev, expandedTasks: value }))}
setModalConfig({ addNewItem={() => setModalConfig({ isOpen: true, isTask: true })}
isOpen: true, badgeType="tasks"
isTask: true todayCompletions={todayCompletions}
}); settings={settings}
}} setBrowserSettings={setBrowserSettings}
>
<Plus className="h-4 w-4" />
<span className="sr-only">Add Task</span>
</Button>
</div>
<div className="text-center text-muted-foreground text-sm py-4">
No tasks due today. Add some tasks to get started!
</div>
</div>
) : hasTasks && (
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold">Daily Tasks</h3>
</div>
<div className="flex items-center gap-2">
<CompletionCountBadge type="tasks" />
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-full hover:bg-primary/10 hover:text-primary"
onClick={() => {
setModalConfig({
isOpen: true,
isTask: true
});
}}
>
<Plus className="h-4 w-4" />
<span className="sr-only">Add Task</span>
</Button>
</div>
</div>
<ul className={`grid gap-2 transition-all duration-300 ease-in-out ${browserSettings.expandedTasks ? 'max-h-none' : 'max-h-[200px]'} overflow-hidden`}>
{dailyTasks
.sort((a, b) => {
// First by completion status
const aCompleted = todayCompletions.includes(a);
const bCompleted = todayCompletions.includes(b);
if (aCompleted !== bCompleted) {
return aCompleted ? 1 : -1;
}
// Then by frequency (daily first)
const aFreq = getHabitFreq(a);
const bFreq = getHabitFreq(b);
const freqOrder = ['daily', 'weekly', 'monthly', 'yearly'];
if (freqOrder.indexOf(aFreq) !== freqOrder.indexOf(bFreq)) {
return freqOrder.indexOf(aFreq) - freqOrder.indexOf(bFreq);
}
// Then by coin reward (higher first)
if (a.coinReward !== b.coinReward) {
return b.coinReward - a.coinReward;
}
// Finally by target completions (higher first)
const aTarget = a.targetCompletions || 1;
const bTarget = b.targetCompletions || 1;
return bTarget - aTarget;
})
.slice(0, browserSettings.expandedTasks ? undefined : 5)
.map((habit) => {
const completionsToday = habit.completions.filter(completion =>
isSameDate(t2d({ timestamp: completion, timezone: settings.system.timezone }), t2d({ timestamp: d2t({ dateTime: getNow({ timezone: settings.system.timezone }) }), timezone: settings.system.timezone }))
).length
const target = habit.targetCompletions || 1
const isCompleted = completionsToday >= target || (habit.isTask && habit.archived)
return (
<li
className={`flex items-center justify-between text-sm p-2 rounded-md
${isCompleted ? 'bg-secondary/50' : 'bg-secondary/20'}`}
key={habit.id}
>
<span className="flex items-center gap-2 flex-1 min-w-0">
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="flex-shrink-0">
<button
onClick={(e) => {
e.preventDefault();
if (isCompleted) {
undoComplete(habit);
} else {
completeHabit(habit);
}
}}
className="relative hover:opacity-70 transition-opacity w-4 h-4"
>
{isCompleted ? (
<CircleCheck className="h-4 w-4 text-green-500" />
) : (
<div className="relative h-4 w-4">
<Circle className="absolute h-4 w-4 text-muted-foreground" />
<div
className="absolute h-4 w-4 rounded-full overflow-hidden"
style={{
background: `conic-gradient(
currentColor ${(completionsToday / target) * 360}deg,
transparent ${(completionsToday / target) * 360}deg 360deg
)`,
mask: 'radial-gradient(transparent 50%, black 51%)',
WebkitMask: 'radial-gradient(transparent 50%, black 51%)'
}}
/> />
</div>
)}
</button>
</div>
</ContextMenuTrigger>
<span className={cn(isCompleted ? 'line-through' : '', 'break-all')}>
<Linkify>
{habit.name}
</Linkify>
</span>
<ContextMenuContent className="w-64">
<ContextMenuItem onClick={() => {
setPomo((prev) => ({
...prev,
show: true,
selectedHabitId: habit.id
}))
}}>
<Timer className="mr-2 h-4 w-4" />
<span>Start Pomodoro</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</span>
<span className="flex items-center gap-2 text-xs text-muted-foreground flex-shrink-0">
{habit.targetCompletions && (
<span className="bg-secondary px-1.5 py-0.5 rounded-full">
{completionsToday}/{target}
</span>
)}
{getHabitFreq(habit) !== 'daily' && (
<Badge variant="outline" className="text-xs">
{getHabitFreq(habit)}
</Badge>
)}
<span className="flex items-center">
<Coins className={cn(
"h-3 w-3 mr-1 transition-all",
isCompleted
? "text-yellow-500 drop-shadow-[0_0_2px_rgba(234,179,8,0.3)]"
: "text-gray-400"
)} />
<span className={cn(
"transition-all",
isCompleted
? "text-yellow-500 font-medium"
: "text-gray-400"
)}>
{habit.coinReward}
</span>
</span>
</span>
</li>
)
})}
</ul>
<div className="flex items-center justify-between">
<button
onClick={() => setBrowserSettings(prev => ({ ...prev, expandedTasks: !prev.expandedTasks }))}
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
>
{browserSettings.expandedTasks ? (
<>
Show less
<ChevronUp className="h-3 w-3" />
</>
) : (
<>
Show all
<ChevronDown className="h-3 w-3" />
</>
)}
</button>
<Link
href="/habits?view=tasks"
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
onClick={() => setBrowserSettings(prev => ({ ...prev, viewType: 'tasks' }))}
>
View
<ArrowRight className="h-3 w-3" />
</Link>
</div>
</div>
)} )}
{/* Habits Section */} {/* Habits Section */}
{dailyHabits.length === 0 ? ( <ItemSection
<div> title="Daily Habits"
<div className="flex items-center justify-between mb-2"> items={dailyHabits}
<h3 className="font-semibold">Daily Habits</h3> emptyMessage="No habits due today. Add some habits to get started!"
<Button isTask={false}
variant="ghost" viewLink="/habits"
size="sm" expanded={browserSettings.expandedHabits}
className="h-7 w-7 rounded-full hover:bg-primary/10 hover:text-primary" setExpanded={(value) => setBrowserSettings(prev => ({ ...prev, expandedHabits: value }))}
onClick={() => { addNewItem={() => setModalConfig({ isOpen: true, isTask: false })}
setModalConfig({ badgeType="habits"
isOpen: true, todayCompletions={todayCompletions}
isTask: false settings={settings}
}); setBrowserSettings={setBrowserSettings}
}}
>
<Plus className="h-4 w-4" />
<span className="sr-only">Add Habit</span>
</Button>
</div>
<div className="text-center text-muted-foreground text-sm py-4">
No habits due today. Add some habits to get started!
</div>
</div>
) : (
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<h3 className="font-semibold">Daily Habits</h3>
</div>
<div className="flex items-center gap-2">
<CompletionCountBadge type="habits" />
<Button
variant="ghost"
size="sm"
className="h-7 w-7 rounded-full hover:bg-primary/10 hover:text-primary"
onClick={() => {
setModalConfig({
isOpen: true,
isTask: false
});
}}
>
<Plus className="h-4 w-4" />
<span className="sr-only">Add Habit</span>
</Button>
</div>
</div>
<ul className={`grid gap-2 transition-all duration-300 ease-in-out ${browserSettings.expandedHabits ? 'max-h-none' : 'max-h-[200px]'} overflow-hidden`}>
{dailyHabits
.sort((a, b) => {
// First by completion status
const aCompleted = todayCompletions.includes(a);
const bCompleted = todayCompletions.includes(b);
if (aCompleted !== bCompleted) {
return aCompleted ? 1 : -1;
}
// Then by frequency (daily first)
const aFreq = getHabitFreq(a);
const bFreq = getHabitFreq(b);
const freqOrder = ['daily', 'weekly', 'monthly', 'yearly'];
if (freqOrder.indexOf(aFreq) !== freqOrder.indexOf(bFreq)) {
return freqOrder.indexOf(aFreq) - freqOrder.indexOf(bFreq);
}
// Then by coin reward (higher first)
if (a.coinReward !== b.coinReward) {
return b.coinReward - a.coinReward;
}
// Finally by target completions (higher first)
const aTarget = a.targetCompletions || 1;
const bTarget = b.targetCompletions || 1;
return bTarget - aTarget;
})
.slice(0, browserSettings.expandedHabits ? undefined : 5)
.map((habit) => {
const completionsToday = habit.completions.filter(completion =>
isSameDate(t2d({ timestamp: completion, timezone: settings.system.timezone }), t2d({ timestamp: d2t({ dateTime: getNow({ timezone: settings.system.timezone }) }), timezone: settings.system.timezone }))
).length
const target = habit.targetCompletions || 1
const isCompleted = completionsToday >= target
return (
<li
className={`flex items-center justify-between text-sm p-2 rounded-md
${isCompleted ? 'bg-secondary/50' : 'bg-secondary/20'}`}
key={habit.id}
>
<span className="flex items-center gap-2 flex-1 min-w-0">
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="flex-shrink-0">
<button
onClick={(e) => {
e.preventDefault();
if (isCompleted) {
undoComplete(habit);
} else {
completeHabit(habit);
}
}}
className="relative hover:opacity-70 transition-opacity w-4 h-4"
>
{isCompleted ? (
<CircleCheck className="h-4 w-4 text-green-500" />
) : (
<div className="relative h-4 w-4">
<Circle className="absolute h-4 w-4 text-muted-foreground" />
<div
className="absolute h-4 w-4 rounded-full overflow-hidden"
style={{
background: `conic-gradient(
currentColor ${(completionsToday / target) * 360}deg,
transparent ${(completionsToday / target) * 360}deg 360deg
)`,
mask: 'radial-gradient(transparent 50%, black 51%)',
WebkitMask: 'radial-gradient(transparent 50%, black 51%)'
}}
/> />
</div>
)}
</button>
</div>
</ContextMenuTrigger>
<span className={cn(isCompleted ? 'line-through' : '', 'break-all')}>
<Linkify>
{habit.name}
</Linkify>
</span>
<ContextMenuContent className="w-64">
<ContextMenuItem onClick={() => {
setPomo((prev) => ({
...prev,
show: true,
selectedHabitId: habit.id
}))
}}>
<Timer className="mr-2 h-4 w-4" />
<span>Start Pomodoro</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</span>
<span className="flex items-center gap-2 text-xs text-muted-foreground flex-shrink-0">
{habit.targetCompletions && (
<span className="bg-secondary px-1.5 py-0.5 rounded-full">
{completionsToday}/{target}
</span>
)}
{getHabitFreq(habit) !== 'daily' && (
<Badge variant="outline" className="text-xs">
{getHabitFreq(habit)}
</Badge>
)}
<span className="flex items-center">
<Coins className={cn(
"h-3 w-3 mr-1 transition-all",
isCompleted
? "text-yellow-500 drop-shadow-[0_0_2px_rgba(234,179,8,0.3)]"
: "text-gray-400"
)} />
<span className={cn(
"transition-all",
isCompleted
? "text-yellow-500 font-medium"
: "text-gray-400"
)}>
{habit.coinReward}
</span>
</span>
</span>
</li>
)
})}
</ul>
<div className="flex items-center justify-between">
<button
onClick={() => setBrowserSettings(prev => ({ ...prev, expandedHabits: !prev.expandedHabits }))}
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
>
{browserSettings.expandedHabits ? (
<>
Show less
<ChevronUp className="h-3 w-3" />
</>
) : (
<>
Show all
<ChevronDown className="h-3 w-3" />
</>
)}
</button>
<Link
href="/habits"
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
onClick={() => setBrowserSettings(prev => ({ ...prev, viewType: 'habits' }))}
>
View
<ArrowRight className="h-3 w-3" />
</Link>
</div>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">

View File

@@ -4,7 +4,7 @@ import { settingsAtom, pomodoroAtom, browserSettingsAtom, usersAtom } from '@/li
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, d2s, getCompletionsForToday, isTaskOverdue, convertMachineReadableFrequencyToHumanReadable } from '@/lib/utils' import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, d2s, getCompletionsForToday, isTaskOverdue, convertMachineReadableFrequencyToHumanReadable } from '@/lib/utils'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Coins, Edit, Trash2, Check, Undo2, MoreVertical, Timer, Archive, ArchiveRestore, Calendar } from 'lucide-react' import { Coins, Edit, Trash2, Check, Undo2, MoreVertical, Timer, Archive, ArchiveRestore, Calendar, Pin } from 'lucide-react'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -88,7 +88,12 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
<CardHeader className="flex-none"> <CardHeader className="flex-none">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<CardTitle className={`line-clamp-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''} flex items-center ${isTasksView ? 'w-full' : ''} justify-between`}> <CardTitle className={`line-clamp-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''} flex items-center ${isTasksView ? 'w-full' : ''} justify-between`}>
<div className="flex items-center gap-1">
{habit.pinned && (
<Pin className="h-4 w-4 text-yellow-500" />
)}
<span>{habit.name}</span> <span>{habit.name}</span>
</div>
{isTaskOverdue(habit, settings.system.timezone) && ( {isTaskOverdue(habit, settings.system.timezone) && (
<span className="ml-2 inline-flex items-center rounded-md bg-red-50 dark:bg-red-900/30 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-600/10 dark:ring-red-500/20"> <span className="ml-2 inline-flex items-center rounded-md bg-red-50 dark:bg-red-900/30 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-600/10 dark:ring-red-500/20">
Overdue Overdue
@@ -212,6 +217,19 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
<span>Move to Today</span> <span>Move to Today</span>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuItem disabled={!canWrite} onClick={() => saveHabit({...habit, pinned: !habit.pinned})}>
{habit.pinned ? (
<>
<Archive className="mr-2 h-4 w-4" />
<span>Unpin</span>
</>
) : (
<>
<Archive className="mr-2 h-4 w-4" />
<span>Pin</span>
</>
)}
</DropdownMenuItem>
<DropdownMenuItem disabled={!canWrite} onClick={() => archiveHabit(habit.id)}> <DropdownMenuItem disabled={!canWrite} onClick={() => archiveHabit(habit.id)}>
<Archive className="mr-2 h-4 w-4" /> <Archive className="mr-2 h-4 w-4" />
<span>Archive</span> <span>Archive</span>

View File

@@ -22,7 +22,9 @@ export default function HabitList() {
const habits = habitsData.habits.filter(habit => const habits = habitsData.habits.filter(habit =>
isTasksView ? habit.isTask : !habit.isTask isTasksView ? habit.isTask : !habit.isTask
) )
const activeHabits = habits.filter(h => !h.archived) const activeHabits = habits
.filter(h => !h.archived)
.sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0))
const archivedHabits = habits.filter(h => h.archived) const archivedHabits = habits.filter(h => h.archived)
const [settings] = useAtom(settingsAtom) const [settings] = useAtom(settingsAtom)
const [modalConfig, setModalConfig] = useState<{ const [modalConfig, setModalConfig] = useState<{

View File

@@ -44,6 +44,7 @@ export type Habit = {
completions: string[] // Array of UTC ISO date strings completions: string[] // Array of UTC ISO date strings
isTask?: boolean // mark the habit as a task isTask?: boolean // mark the habit as a task
archived?: boolean // mark the habit as archived archived?: boolean // mark the habit as archived
pinned?: boolean // mark the habit as pinned
userIds?: UserId[] userIds?: UserId[]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "habittrove", "name": "habittrove",
"version": "0.2.6", "version": "0.2.7",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",