support archiving habit and wishlist + wishlist redeem count (#49)

This commit is contained in:
Doh
2025-01-24 20:41:26 -05:00
committed by GitHub
parent d3502e284d
commit 6fe10d9fa5
13 changed files with 374 additions and 83 deletions

View File

@@ -1,5 +1,16 @@
# Changelog
## Version 0.1.26
### Added
- archiving habits and wishlists (#44)
- wishlist item now supports redeem count (#36)
### Fixed
- pomodoro skip should update label
## Version 0.1.25
### Added

View File

@@ -9,8 +9,6 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Info, SmilePlus } from 'lucide-react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import data from '@emoji-mart/data'

View File

@@ -5,7 +5,8 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { SmilePlus } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { SmilePlus, Info } from 'lucide-react'
import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react'
import { WishlistItemType } from '@/lib/types'
@@ -18,25 +19,51 @@ interface AddEditWishlistItemModalProps {
}
export default function AddEditWishlistItemModal({ isOpen, onClose, onSave, item }: AddEditWishlistItemModalProps) {
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [coinCost, setCoinCost] = useState(1)
const [name, setName] = useState(item?.name || '')
const [description, setDescription] = useState(item?.description || '')
const [coinCost, setCoinCost] = useState(item?.coinCost || 1)
const [targetCompletions, setTargetCompletions] = useState<number | undefined>(item?.targetCompletions)
const [errors, setErrors] = useState<{ [key: string]: string }>({})
useEffect(() => {
if (item) {
setName(item.name)
setDescription(item.description)
setCoinCost(item.coinCost)
setTargetCompletions(item.targetCompletions)
} else {
setName('')
setDescription('')
setCoinCost(1)
setTargetCompletions(undefined)
}
setErrors({})
}, [item])
const validate = () => {
const newErrors: { [key: string]: string } = {}
if (!name.trim()) {
newErrors.name = 'Name is required'
}
if (coinCost < 1) {
newErrors.coinCost = 'Coin cost must be at least 1'
}
if (targetCompletions !== undefined && targetCompletions < 1) {
newErrors.targetCompletions = 'Target completions must be at least 1'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSave({ name, description, coinCost })
if (!validate()) return
onSave({
name,
description,
coinCost,
targetCompletions: targetCompletions || undefined
})
}
return (
@@ -96,18 +123,90 @@ export default function AddEditWishlistItemModal({ isOpen, onClose, onSave, item
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="coinCost" className="text-right">
Coin Cost
<div className="flex items-center gap-2 justify-end">
<Label htmlFor="coinReward">
Cost
</Label>
</div>
<div className="col-span-3">
<div className="flex items-center gap-4">
<div className="flex items-center border rounded-lg overflow-hidden">
<button
type="button"
onClick={() => setCoinCost(prev => Math.max(0, prev - 1))}
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
>
-
</button>
<Input
id="coinCost"
id="coinReward"
type="number"
value={coinCost}
onChange={(e) => setCoinCost(parseInt(e.target.value === "" ? "0" : e.target.value))}
className="col-span-3"
min={1}
min={0}
required
className="w-20 text-center border-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
<button
type="button"
onClick={() => setCoinCost(prev => prev + 1)}
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
>
+
</button>
</div>
<span className="text-sm text-muted-foreground">
coins
</span>
</div>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center gap-2 justify-end">
<Label htmlFor="targetCompletions">
Redeemable
</Label>
</div>
<div className="col-span-3">
<div className="flex items-center gap-4">
<div className="flex items-center border rounded-lg overflow-hidden">
<button
type="button"
onClick={() => setTargetCompletions(prev => prev !== undefined && prev > 1 ? prev - 1 : undefined)}
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
>
-
</button>
<Input
id="targetCompletions"
type="number"
value={targetCompletions || ''}
onChange={(e) => {
const value = e.target.value
setTargetCompletions(value && value !== "0" ? parseInt(value) : undefined)
}}
min={0}
placeholder="∞"
className="w-20 text-center border-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
<button
type="button"
onClick={() => setTargetCompletions(prev => Math.min(10, (prev || 0) + 1))}
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
>
+
</button>
</div>
<span className="text-sm text-muted-foreground">
times
</span>
</div>
{errors.targetCompletions && (
<div className="text-sm text-red-500">
{errors.targetCompletions}
</div>
)}
</div>
</div>
</div>
<DialogFooter>

View File

@@ -4,7 +4,7 @@ import { settingsAtom, pomodoroAtom, browserSettingsAtom } from '@/lib/atoms'
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, parseNaturalLanguageRRule, parseRRule, d2s } from '@/lib/utils'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Coins, Edit, Trash2, Check, Undo2, MoreVertical, Timer } from 'lucide-react'
import { Coins, Edit, Trash2, Check, Undo2, MoreVertical, Timer, Archive, ArchiveRestore } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -24,7 +24,7 @@ interface HabitItemProps {
}
export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
const { completeHabit, undoComplete } = useHabits()
const { completeHabit, undoComplete, archiveHabit, unarchiveHabit } = useHabits()
const [settings] = useAtom(settingsAtom)
const [_, setPomo] = useAtom(pomodoroAtom)
const completionsToday = habit.completions?.filter(completion =>
@@ -59,21 +59,21 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
return (
<Card
id={`habit-${habit.id}`}
className={`h-full flex flex-col transition-all duration-500 ${isHighlighted ? 'bg-yellow-100 dark:bg-yellow-900' : ''}`}
className={`h-full flex flex-col transition-all duration-500 ${isHighlighted ? 'bg-yellow-100 dark:bg-yellow-900' : ''} ${habit.archived ? 'opacity-75' : ''}`}
>
<CardHeader className="flex-none">
<CardTitle className="line-clamp-1">{habit.name}</CardTitle>
<CardTitle className={`line-clamp-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{habit.name}</CardTitle>
{habit.description && (
<CardDescription className="whitespace-pre-line">
<CardDescription className={`whitespace-pre-line ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
{habit.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="flex-1">
<p className="text-sm text-gray-500">When: {isRecurRule ? parseRRule(habit.frequency || INITIAL_RECURRENCE_RULE).toText() : d2s({ dateTime: t2d({ timestamp: habit.frequency, timezone: settings.system.timezone }), timezone: settings.system.timezone, format: DateTime.DATE_MED_WITH_WEEKDAY })}</p>
<p className={`text-sm ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500'}`}>When: {isRecurRule ? parseRRule(habit.frequency || INITIAL_RECURRENCE_RULE).toText() : d2s({ dateTime: t2d({ timestamp: habit.frequency, timezone: settings.system.timezone }), timezone: settings.system.timezone, format: DateTime.DATE_MED_WITH_WEEKDAY })}</p>
<div className="flex items-center mt-2">
<Coins className="h-4 w-4 text-yellow-400 mr-1" />
<span className="text-sm font-medium">{habit.coinReward} coins per completion</span>
<Coins className={`h-4 w-4 mr-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
<span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{habit.coinReward} coins per completion</span>
</div>
</CardContent>
<CardFooter className="flex justify-between gap-2">
@@ -83,8 +83,8 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
variant={isCompletedToday ? "secondary" : "default"}
size="sm"
onClick={async () => await completeHabit(habit)}
disabled={isCompletedToday && completionsToday >= target}
className="overflow-hidden w-24 sm:w-auto"
disabled={habit.archived || (isCompletedToday && completionsToday >= target)}
className={`overflow-hidden w-24 sm:w-auto ${habit.archived ? 'cursor-not-allowed' : ''}`}
>
<Check className="h-4 w-4 sm:mr-2" />
<span>
@@ -116,7 +116,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
)}
</Button>
</div>
{completionsToday > 0 && (
{completionsToday > 0 && !habit.archived && (
<Button
variant="outline"
size="sm"
@@ -129,6 +129,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
)}
</div>
<div className="flex gap-2">
{!habit.archived && (
<Button
variant="edit"
size="sm"
@@ -138,6 +139,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
<Edit className="h-4 w-4" />
<span className="ml-2">Edit</span>
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
@@ -145,6 +147,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!habit.archived && (
<DropdownMenuItem onClick={() => {
setPomo((prev) => ({
...prev,
@@ -155,7 +158,24 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
<Timer className="mr-2 h-4 w-4" />
<span>Start Pomodoro</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onEdit} className="sm:hidden">
)}
{!habit.archived && (
<DropdownMenuItem onClick={() => archiveHabit(habit.id)}>
<Archive className="mr-2 h-4 w-4" />
<span>Archive</span>
</DropdownMenuItem>
)}
{habit.archived && (
<DropdownMenuItem onClick={() => unarchiveHabit(habit.id)}>
<ArchiveRestore className="mr-2 h-4 w-4" />
<span>Unarchive</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={onEdit}
className="sm:hidden"
disabled={habit.archived}
>
<Edit className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>

View File

@@ -21,6 +21,8 @@ export default function HabitList() {
const habits = habitsData.habits.filter(habit =>
isTasksView ? habit.isTask : !habit.isTask
)
const activeHabits = habits.filter(h => !h.archived)
const archivedHabits = habits.filter(h => h.archived)
const [settings] = useAtom(settingsAtom)
const [isModalOpen, setIsModalOpen] = useState(false)
const [editingHabit, setEditingHabit] = useState<Habit | null>(null)
@@ -41,7 +43,7 @@ export default function HabitList() {
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
{habits.length === 0 ? (
{activeHabits.length === 0 ? (
<div className="col-span-2">
<EmptyState
icon={isTasksView ? TaskIcon : HabitIcon}
@@ -50,7 +52,7 @@ export default function HabitList() {
/>
</div>
) : (
habits.map((habit) => (
activeHabits.map((habit: Habit) => (
<HabitItem
key={habit.id}
habit={habit}
@@ -62,6 +64,27 @@ export default function HabitList() {
/>
))
)}
{archivedHabits.length > 0 && (
<>
<div className="col-span-2 relative flex items-center my-6">
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
<span className="mx-4 text-sm text-gray-500 dark:text-gray-400">Archived</span>
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
</div>
{archivedHabits.map((habit: Habit) => (
<HabitItem
key={habit.id}
habit={habit}
onEdit={() => {
setEditingHabit(habit)
setIsModalOpen(true)
}}
onDelete={() => setDeleteConfirmation({ isOpen: true, habitId: habit.id })}
/>
))}
</>
)}
</div>
{isModalOpen &&
<AddEditHabitModal

View File

@@ -184,6 +184,16 @@ export default function PomodoroTimer() {
setTimeLeft(currentTimer.current.duration)
}
const skipTimer = () => {
currentTimer.current = currentTimer.current.type === 'focus'
? PomoConfigs.break
: PomoConfigs.focus
resetTimer()
setCurrentLabel(
currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)]
)
}
const formatTime = (seconds: number) => {
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
@@ -314,12 +324,7 @@ export default function PomodoroTimer() {
</Button>
<Button
variant="outline"
onClick={() => {
currentTimer.current = currentTimer.current.type === 'focus'
? PomoConfigs.break
: PomoConfigs.focus
resetTimer()
}}
onClick={skipTimer}
disabled={state === "started"}
className="sm:px-4"
>

View File

@@ -2,7 +2,7 @@ import { WishlistItemType } from '@/lib/types'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import ReactMarkdown from 'react-markdown'
import { Button } from '@/components/ui/button'
import { Coins, Edit, Trash2, Gift, MoreVertical } from 'lucide-react'
import { Coins, Edit, Trash2, Gift, MoreVertical, Archive, ArchiveRestore } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -16,9 +16,12 @@ interface WishlistItemProps {
onEdit: () => void
onDelete: () => void
onRedeem: () => void
onArchive: () => void
onUnarchive: () => void
canRedeem: boolean
isHighlighted?: boolean
isRecentlyRedeemed?: boolean
isArchived?: boolean
}
export default function WishlistItem({
@@ -26,6 +29,8 @@ export default function WishlistItem({
onEdit,
onDelete,
onRedeem,
onArchive,
onUnarchive,
canRedeem,
isHighlighted,
isRecentlyRedeemed
@@ -35,20 +40,31 @@ export default function WishlistItem({
id={`wishlist-${item.id}`}
className={`h-full flex flex-col transition-all duration-500 ${isHighlighted ? 'bg-yellow-100 dark:bg-yellow-900' : ''
} ${isRecentlyRedeemed ? 'animate-[celebrate_1s_ease-in-out] shadow-lg ring-2 ring-primary' : ''
}`}
} ${item.archived ? 'opacity-75' : ''}`}
>
<CardHeader className="flex-none">
<CardTitle className="line-clamp-1">{item.name}</CardTitle>
<div className="flex items-center gap-2">
<CardTitle className={`line-clamp-1 ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
{item.name}
</CardTitle>
{item.targetCompletions && (
<span className="text-sm text-gray-500 dark:text-gray-400">
({item.targetCompletions} {item.targetCompletions === 1 ? 'use' : 'uses'} left)
</span>
)}
</div>
{item.description && (
<CardDescription className="whitespace-pre-line">
<CardDescription className={`whitespace-pre-line ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
{item.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="flex-1">
<div className="flex items-center">
<Coins className="h-4 w-4 text-yellow-400 mr-1" />
<span className="text-sm font-medium">{item.coinCost} coins</span>
<div className="flex items-center gap-2">
<Coins className={`h-4 w-4 ${item.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
<span className={`text-sm font-medium ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
{item.coinCost} coins
</span>
</div>
</CardContent>
<CardFooter className="flex justify-between gap-2">
@@ -57,8 +73,8 @@ export default function WishlistItem({
variant={canRedeem ? "default" : "secondary"}
size="sm"
onClick={onRedeem}
disabled={!canRedeem}
className={`transition-all duration-300 w-24 sm:w-auto ${isRecentlyRedeemed ? 'bg-green-500 hover:bg-green-600' : ''}`}
disabled={!canRedeem || item.archived}
className={`transition-all duration-300 w-24 sm:w-auto ${isRecentlyRedeemed ? 'bg-green-500 hover:bg-green-600' : ''} ${item.archived ? 'cursor-not-allowed' : ''}`}
>
<Gift className={`h-4 w-4 sm:mr-2 ${isRecentlyRedeemed ? 'animate-spin' : ''}`} />
<span>
@@ -77,6 +93,7 @@ export default function WishlistItem({
</Button>
</div>
<div className="flex gap-2">
{!item.archived && (
<Button
variant="edit"
size="sm"
@@ -86,6 +103,7 @@ export default function WishlistItem({
<Edit className="h-4 w-4" />
<span className="ml-2">Edit</span>
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
@@ -93,6 +111,18 @@ export default function WishlistItem({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!item.archived && (
<DropdownMenuItem onClick={onArchive}>
<Archive className="mr-2 h-4 w-4" />
<span>Archive</span>
</DropdownMenuItem>
)}
{item.archived && (
<DropdownMenuItem onClick={onUnarchive}>
<ArchiveRestore className="mr-2 h-4 w-4" />
<span>Unarchive</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onEdit} className="sm:hidden">
<Edit className="mr-2 h-4 w-4" />
Edit

View File

@@ -16,10 +16,15 @@ export default function WishlistManager() {
editWishlistItem,
deleteWishlistItem,
redeemWishlistItem,
archiveWishlistItem,
unarchiveWishlistItem,
canRedeem,
wishlistItems
} = useWishlist()
const activeItems = wishlistItems.filter(item => !item.archived)
const archivedItems = wishlistItems.filter(item => item.archived)
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(null)
const [recentlyRedeemedId, setRecentlyRedeemedId] = useState<string | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
@@ -69,7 +74,7 @@ export default function WishlistManager() {
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
{wishlistItems.length === 0 ? (
{activeItems.length === 0 ? (
<div className="col-span-2">
<EmptyState
icon={Gift}
@@ -78,7 +83,7 @@ export default function WishlistManager() {
/>
</div>
) : (
wishlistItems.map((item) => (
activeItems.map((item) => (
<div
key={item.id}
ref={(el) => {
@@ -97,11 +102,38 @@ export default function WishlistManager() {
}}
onDelete={() => setDeleteConfirmation({ isOpen: true, itemId: item.id })}
onRedeem={() => handleRedeem(item)}
onArchive={() => archiveWishlistItem(item.id)}
onUnarchive={() => unarchiveWishlistItem(item.id)}
canRedeem={canRedeem(item.coinCost)}
/>
</div>
))
)}
{archivedItems.length > 0 && (
<>
<div className="col-span-2 relative flex items-center my-6">
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
<span className="mx-4 text-sm text-gray-500 dark:text-gray-400">Archived</span>
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
</div>
{archivedItems.map((item) => (
<WishlistItem
key={item.id}
item={item}
onEdit={() => {
setEditingItem(item)
setIsModalOpen(true)
}}
onDelete={() => setDeleteConfirmation({ isOpen: true, itemId: item.id })}
onRedeem={() => handleRedeem(item)}
onArchive={() => archiveWishlistItem(item.id)}
onUnarchive={() => unarchiveWishlistItem(item.id)}
canRedeem={canRedeem(item.coinCost)}
/>
))}
</>
)}
</div>
<AddEditWishlistItemModal
isOpen={isModalOpen}

View File

@@ -231,11 +231,29 @@ export function useHabits() {
}
}
const archiveHabit = async (id: string) => {
const updatedHabits = habitsData.habits.map(h =>
h.id === id ? { ...h, archived: true } : h
)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
}
const unarchiveHabit = async (id: string) => {
const updatedHabits = habitsData.habits.map(h =>
h.id === id ? { ...h, archived: undefined } : h
)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
}
return {
completeHabit,
undoComplete,
saveHabit,
deleteHabit,
completePastHabit
completePastHabit,
archiveHabit,
unarchiveHabit
}
}

View File

@@ -33,6 +33,16 @@ export function useWishlist() {
const redeemWishlistItem = async (item: WishlistItemType) => {
if (balance >= item.coinCost) {
// Check if item has target completions and if we've reached the limit
if (item.targetCompletions && item.targetCompletions <= 0) {
toast({
title: "Redemption limit reached",
description: `You've reached the maximum redemptions for "${item.name}".`,
variant: "destructive",
})
return false
}
const data = await removeCoins({
amount: item.coinCost,
description: `Redeemed reward: ${item.name}`,
@@ -41,6 +51,30 @@ export function useWishlist() {
})
setCoins(data)
// Update target completions if set
if (item.targetCompletions !== undefined) {
const newItems = wishlist.items.map(wishlistItem => {
if (wishlistItem.id === item.id) {
const newTarget = wishlistItem.targetCompletions! - 1
// If target reaches 0, archive the item
if (newTarget <= 0) {
return {
...wishlistItem,
targetCompletions: undefined,
archived: true
}
}
return {
...wishlistItem,
targetCompletions: newTarget
}
}
return wishlistItem
})
setWishlist({ items: newItems })
await saveWishlistItems(newItems)
}
// Randomly choose a celebration effect
const celebrationEffects = [
celebrations.emojiParty
@@ -66,11 +100,29 @@ export function useWishlist() {
const canRedeem = (cost: number) => balance >= cost
const archiveWishlistItem = async (id: string) => {
const newItems = wishlist.items.map(item =>
item.id === id ? { ...item, archived: true } : item
)
setWishlist({ items: newItems })
await saveWishlistItems(newItems)
}
const unarchiveWishlistItem = async (id: string) => {
const newItems = wishlist.items.map(item =>
item.id === id ? { ...item, archived: undefined } : item
)
setWishlist({ items: newItems })
await saveWishlistItems(newItems)
}
return {
addWishlistItem,
editWishlistItem,
deleteWishlistItem,
redeemWishlistItem,
archiveWishlistItem,
unarchiveWishlistItem,
canRedeem,
wishlistItems: wishlist.items
}

View File

@@ -7,6 +7,7 @@ export type Habit = {
targetCompletions?: number // Optional field, default to 1
completions: string[] // Array of UTC ISO date strings
isTask?: boolean // mark the habit as a task
archived?: boolean // mark the habit as archived
}
@@ -17,6 +18,8 @@ export type WishlistItemType = {
name: string
description: string
coinCost: number
archived?: boolean // mark the wishlist item as archived
targetCompletions?: number // Optional field, infinity when unset
}
export type TransactionType = 'HABIT_COMPLETION' | 'HABIT_UNDO' | 'WISH_REDEMPTION' | 'MANUAL_ADJUSTMENT' | 'TASK_COMPLETION' | 'TASK_UNDO';

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "habittrove",
"version": "0.1.24",
"version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habittrove",
"version": "0.1.24",
"version": "0.1.26",
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",

View File

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