diff --git a/CHANGELOG.md b/CHANGELOG.md index f5aa8de..3c51e25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Version 0.1.28 + +### Added + +- redeem link for wishlist items (#52) +- sound effect for habit / task completion (#53) + +### Fixed + +- fail habit create or edit if frequency is not set (#54) +- archive task when completed (#50) + ## Version 0.1.27 ### Added diff --git a/components/AddEditHabitModal.tsx b/components/AddEditHabitModal.tsx index acfd5e4..c55d38e 100644 --- a/components/AddEditHabitModal.tsx +++ b/components/AddEditHabitModal.tsx @@ -61,7 +61,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab
setRuleText(e.target.value)} + required // placeholder="e.g. 'every weekday' or 'every 2 weeks on Monday, Wednesday'" />
diff --git a/components/AddEditWishlistItemModal.tsx b/components/AddEditWishlistItemModal.tsx index e4bda65..79b444c 100644 --- a/components/AddEditWishlistItemModal.tsx +++ b/components/AddEditWishlistItemModal.tsx @@ -13,32 +13,44 @@ import { WishlistItemType } from '@/lib/types' interface AddEditWishlistItemModalProps { isOpen: boolean - onClose: () => void - onSave: (item: Omit) => void - item?: WishlistItemType | null + setIsOpen: (isOpen: boolean) => void + editingItem: WishlistItemType | null + setEditingItem: (item: WishlistItemType | null) => void + addWishlistItem: (item: Omit) => void + editWishlistItem: (item: WishlistItemType) => void } -export default function AddEditWishlistItemModal({ isOpen, onClose, onSave, item }: AddEditWishlistItemModalProps) { - const [name, setName] = useState(item?.name || '') - const [description, setDescription] = useState(item?.description || '') - const [coinCost, setCoinCost] = useState(item?.coinCost || 1) - const [targetCompletions, setTargetCompletions] = useState(item?.targetCompletions) +export default function AddEditWishlistItemModal({ + isOpen, + setIsOpen, + editingItem, + setEditingItem, + addWishlistItem, + editWishlistItem +}: AddEditWishlistItemModalProps) { + const [name, setName] = useState(editingItem?.name || '') + const [description, setDescription] = useState(editingItem?.description || '') + const [coinCost, setCoinCost] = useState(editingItem?.coinCost || 1) + const [targetCompletions, setTargetCompletions] = useState(editingItem?.targetCompletions) + const [link, setLink] = useState(editingItem?.link || '') const [errors, setErrors] = useState<{ [key: string]: string }>({}) useEffect(() => { - if (item) { - setName(item.name) - setDescription(item.description) - setCoinCost(item.coinCost) - setTargetCompletions(item.targetCompletions) + if (editingItem) { + setName(editingItem.name) + setDescription(editingItem.description) + setCoinCost(editingItem.coinCost) + setTargetCompletions(editingItem.targetCompletions) + setLink(editingItem.link || '') } else { setName('') setDescription('') setCoinCost(1) setTargetCompletions(undefined) + setLink('') } setErrors({}) - }, [item]) + }, [editingItem]) const validate = () => { const newErrors: { [key: string]: string } = {} @@ -51,32 +63,60 @@ export default function AddEditWishlistItemModal({ isOpen, onClose, onSave, item if (targetCompletions !== undefined && targetCompletions < 1) { newErrors.targetCompletions = 'Target completions must be at least 1' } + if (link && !isValidUrl(link)) { + newErrors.link = 'Please enter a valid URL' + } setErrors(newErrors) return Object.keys(newErrors).length === 0 } - const handleSubmit = (e: React.FormEvent) => { + const isValidUrl = (url: string) => { + try { + new URL(url) + return true + } catch { + return false + } + } + + const handleClose = () => { + setIsOpen(false) + setEditingItem(null) + } + + const handleSave = (e: React.FormEvent) => { e.preventDefault() if (!validate()) return - onSave({ + + const itemData = { name, description, coinCost, - targetCompletions: targetCompletions || undefined - }) + targetCompletions: targetCompletions || undefined, + link: link.trim() || undefined + } + + if (editingItem) { + editWishlistItem({ ...itemData, id: editingItem.id }) + } else { + addWishlistItem(itemData) + } + + setIsOpen(false) + setEditingItem(null) } return ( - + - {item ? 'Edit Reward' : 'Add New Reward'} + {editingItem ? 'Edit Reward' : 'Add New Reward'} -
+
+
+ +
+ setLink(e.target.value)} + className="col-span-3" + /> + {errors.link && ( +
+ {errors.link} +
+ )} +
+
- + diff --git a/components/PomodoroTimer.tsx b/components/PomodoroTimer.tsx index f9c2e05..d492364 100644 --- a/components/PomodoroTimer.tsx +++ b/components/PomodoroTimer.tsx @@ -148,14 +148,6 @@ export default function PomodoroTimer() { } }, [state]) - - const playSound = useCallback(() => { - const audio = new Audio('/sounds/timer-end.wav') - audio.play().catch(error => { - console.error('Error playing sound:', error) - }) - }, []) - const handleTimerEnd = async () => { setState("stopped") const currentTimerType = currentTimer.current.type @@ -165,9 +157,6 @@ export default function PomodoroTimer() { currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)] ) - // Play sound - playSound() - // update habits only after focus sessions if (selectedHabit && currentTimerType === 'focus') { await completeHabit(selectedHabit) diff --git a/components/WishlistManager.tsx b/components/WishlistManager.tsx index 37c98c1..5fead6b 100644 --- a/components/WishlistManager.tsx +++ b/components/WishlistManager.tsx @@ -137,20 +137,11 @@ export default function WishlistManager() {
{ - setIsModalOpen(false) - setEditingItem(null) - }} - onSave={(item) => { - if (editingItem) { - editWishlistItem({ ...item, id: editingItem.id }) - } else { - addWishlistItem(item) - } - setIsModalOpen(false) - setEditingItem(null) - }} - item={editingItem} + setIsOpen={setIsModalOpen} + editingItem={editingItem} + setEditingItem={setEditingItem} + addWishlistItem={addWishlistItem} + editWishlistItem={editWishlistItem} /> @@ -46,29 +59,36 @@ export function useHabits() { ) await saveHabitsData({ habits: updatedHabits }) - setHabitsData({ habits: updatedHabits }) // Check if we've now reached the target const isTargetReached = completionsToday + 1 === target if (isTargetReached) { const updatedCoins = await addCoins({ amount: habit.coinReward, - description: `Completed habit: ${habit.name}`, + description: `Completed: ${habit.name}`, type: habit.isTask ? 'TASK_COMPLETION' : 'HABIT_COMPLETION', relatedItemId: habit.id, }) + isTargetReached && playSound() + toast({ + title: "Habit completed!", + description: `You earned ${habit.coinReward} coins.`, + action: undoComplete(updatedHabit)}> + Undo + + }) setCoins(updatedCoins) + } else { + toast({ + title: "Progress!", + description: `You've completed ${completionsToday + 1}/${target} times today.`, + action: undoComplete(updatedHabit)}> + Undo + + }) } - - toast({ - title: isTargetReached ? "Habit completed!" : "Progress!", - description: isTargetReached - ? `You earned ${habit.coinReward} coins.` - : `You've completed ${completionsToday + 1}/${target} times today.`, - action: undoComplete(updatedHabit)}> - Undo - - }) + // move atom update at the end of function to improve UI responsiveness + setHabitsData({ habits: updatedHabits }) return { updatedHabits, @@ -87,12 +107,13 @@ export function useHabits() { ) if (todayCompletions.length > 0) { - // Remove the most recent completion + // Remove the most recent completion and unarchive if needed const updatedHabit = { ...habit, completions: habit.completions.filter( (_, index) => index !== habit.completions.length - 1 - ) + ), + archived: habit.isTask ? undefined : habit.archived // Unarchive if it's a task } const updatedHabits = habitsData.habits.map(h => @@ -107,7 +128,7 @@ export function useHabits() { if (todayCompletions.length === target) { const updatedCoins = await removeCoins({ amount: habit.coinReward, - description: `Undid habit completion: ${habit.name}`, + description: `Undid completion: ${habit.name}`, type: habit.isTask ? 'TASK_UNDO' : 'HABIT_UNDO', relatedItemId: habit.id, }) @@ -207,7 +228,7 @@ export function useHabits() { if (isTargetReached) { const updatedCoins = await addCoins({ amount: habit.coinReward, - description: `Completed habit: ${habit.name} on ${d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' })}`, + description: `Completed: ${habit.name} on ${d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' })}`, type: habit.isTask ? 'TASK_COMPLETION' : 'HABIT_COMPLETION', relatedItemId: habit.id, }) diff --git a/lib/types.ts b/lib/types.ts index 33e5fd1..43f7d25 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -20,6 +20,7 @@ export type WishlistItemType = { coinCost: number archived?: boolean // mark the wishlist item as archived targetCompletions?: number // Optional field, infinity when unset + link?: string // Optional URL to external resource } export type TransactionType = 'HABIT_COMPLETION' | 'HABIT_UNDO' | 'WISH_REDEMPTION' | 'MANUAL_ADJUSTMENT' | 'TASK_COMPLETION' | 'TASK_UNDO'; diff --git a/lib/utils.ts b/lib/utils.ts index e0915e2..2b7ac02 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -279,3 +279,11 @@ export function getHabitFreq(habit: Habit): Freq { default: throw new Error(`Invalid frequency: ${freq}`) } } + +// play sound (client side only, must be run in browser) +export const playSound = (soundPath: string = '/sounds/timer-end.wav') => { + const audio = new Audio(soundPath) + audio.play().catch(error => { + console.error('Error playing sound:', error) + }) +} \ No newline at end of file diff --git a/package.json b/package.json index ce63227..1bb4085 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habittrove", - "version": "0.1.27", + "version": "0.1.28", "private": true, "scripts": { "dev": "next dev --turbopack",