diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0e7a0..d2f35bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Version 0.2.17 + +### Fixed + +* fix emoji selector (#142) +* fix about modal (#145) + ## Version 0.2.16 ### Improved diff --git a/README.md b/README.md index 19f469d..1be8ba4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # HabitTrove +![cover](https://github.com/user-attachments/assets/b63e98b4-64ae-49c7-ae7e-21ef76c04a5a) + HabitTrove is a gamified habit tracking application that helps you build and maintain positive habits by rewarding you with coins, which you can use to exchange for rewards. > **⚠️ Important:** HabitTrove is currently in beta. Please regularly backup your `data/` directory to prevent any potential data loss. diff --git a/components/AboutModal.tsx b/components/AboutModal.tsx index 8f6a697..309d633 100644 --- a/components/AboutModal.tsx +++ b/components/AboutModal.tsx @@ -11,17 +11,16 @@ import ChangelogModal from "./ChangelogModal" import { useState } from "react" interface AboutModalProps { - isOpen: boolean onClose: () => void } -export default function AboutModal({ isOpen, onClose }: AboutModalProps) { +export default function AboutModal({ onClose }: AboutModalProps) { const t = useTranslations('AboutModal') const version = packageJson.version const [changelogOpen, setChangelogOpen] = useState(false) return ( - + diff --git a/components/AddEditHabitModal.tsx b/components/AddEditHabitModal.tsx index 64f5f1c..50c1214 100644 --- a/components/AddEditHabitModal.tsx +++ b/components/AddEditHabitModal.tsx @@ -12,14 +12,13 @@ import { useHelpers } from '@/lib/client-helpers' import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, MAX_COIN_LIMIT, QUICK_DATES } from '@/lib/constants' import { Habit } from '@/lib/types' import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2t, serializeRRule } from '@/lib/utils' -import data from '@emoji-mart/data' -import Picker from '@emoji-mart/react' import { useAtom } from 'jotai' -import { SmilePlus, Zap } from 'lucide-react' +import { Zap } from 'lucide-react' import { DateTime } from 'luxon' import { useTranslations } from 'next-intl' import { useState } from 'react' import { RRule } from 'rrule' +import EmojiPickerButton from './EmojiPickerButton' interface AddEditHabitModalProps { onClose: () => void @@ -37,9 +36,9 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad const [targetCompletions, setTargetCompletions] = useState(habit?.targetCompletions || 1) const isRecurRule = !isTask // Initialize ruleText with the actual frequency string or default, not the display text - const initialRuleText = habit?.frequency ? convertMachineReadableFrequencyToHumanReadable({ + const initialRuleText = habit?.frequency ? convertMachineReadableFrequencyToHumanReadable({ frequency: habit.frequency, - isRecurRule, + isRecurRule, timezone: settings.system.timezone }) : (isRecurRule ? INITIAL_RECURRENCE_RULE : INITIAL_DUE); const [ruleText, setRuleText] = useState(initialRuleText) @@ -111,33 +110,15 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad onChange={(e) => setName(e.target.value)} required /> - - - - - - { - setName(prev => { - // Add space before emoji if there isn't one already - const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : ''; - return `${prev}${space}${emoji.native}`; - }) - // Focus back on input after selection - const input = document.getElementById('name') as HTMLInputElement - input?.focus() - }} - /> - - + { + setName(prev => { + const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : ''; + return `${prev}${space}${emoji}`; + }) + }} + />
diff --git a/components/AddEditWishlistItemModal.tsx b/components/AddEditWishlistItemModal.tsx index 254b1cc..4cdf82c 100644 --- a/components/AddEditWishlistItemModal.tsx +++ b/components/AddEditWishlistItemModal.tsx @@ -3,18 +3,15 @@ import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Textarea } from '@/components/ui/textarea' import { usersAtom } from '@/lib/atoms' import { useHelpers } from '@/lib/client-helpers' import { MAX_COIN_LIMIT } from '@/lib/constants' import { WishlistItemType } from '@/lib/types' -import data from '@emoji-mart/data' -import Picker from '@emoji-mart/react' import { useAtom } from 'jotai' -import { SmilePlus } from 'lucide-react' import { useTranslations } from 'next-intl' import { useEffect, useState } from 'react' +import EmojiPickerButton from './EmojiPickerButton' import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar' interface AddEditWishlistItemModalProps { @@ -114,7 +111,7 @@ export default function AddEditWishlistItemModal({ } else { addWishlistItem(itemData) } - + setIsOpen(false) setEditingItem(null) } @@ -139,29 +136,15 @@ export default function AddEditWishlistItemModal({ className="flex-1" required /> - - - - - - { - setName(prev => `${prev}${emoji.native}`) - // Focus back on input after selection - const input = document.getElementById('name') as HTMLInputElement - input?.focus() - }} - /> - - + { + setName(prev => { + const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : ''; + return `${prev}${space}${emoji}`; + }) + }} + />
@@ -296,13 +279,13 @@ export default function AddEditWishlistItemModal({ { - setSelectedUserIds(prev => + setSelectedUserIds(prev => prev.includes(user.id) ? prev.filter(id => id !== user.id) : [...prev, user.id] diff --git a/components/ClientWrapper.tsx b/components/ClientWrapper.tsx index 9235c92..1c79740 100644 --- a/components/ClientWrapper.tsx +++ b/components/ClientWrapper.tsx @@ -2,14 +2,16 @@ import { ReactNode, useEffect } from 'react' import { useAtom } from 'jotai' -import { pomodoroAtom, userSelectAtom } from '@/lib/atoms' +import { aboutOpenAtom, pomodoroAtom, userSelectAtom } from '@/lib/atoms' import PomodoroTimer from './PomodoroTimer' import UserSelectModal from './UserSelectModal' import { useSession } from 'next-auth/react' +import AboutModal from './AboutModal' export default function ClientWrapper({ children }: { children: ReactNode }) { const [pomo] = useAtom(pomodoroAtom) const [userSelect, setUserSelect] = useAtom(userSelectAtom) + const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom) const { data: session, status } = useSession() const currentUserId = session?.user.id @@ -27,7 +29,10 @@ export default function ClientWrapper({ children }: { children: ReactNode }) { )} {userSelect && ( - setUserSelect(false)}/> + setUserSelect(false)} /> + )} + {aboutOpen && ( + setAboutOpen(false)} /> )} ) diff --git a/components/EmojiPickerButton.tsx b/components/EmojiPickerButton.tsx new file mode 100644 index 0000000..04fe7a7 --- /dev/null +++ b/components/EmojiPickerButton.tsx @@ -0,0 +1,51 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { SmilePlus } from 'lucide-react' +import data from '@emoji-mart/data' +import Picker from '@emoji-mart/react' + +interface EmojiPickerButtonProps { + onEmojiSelect: (emoji: string) => void + inputIdToFocus?: string // Optional: ID of the input to focus after selection +} + +export default function EmojiPickerButton({ onEmojiSelect, inputIdToFocus }: EmojiPickerButtonProps) { + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false) + + return ( + + + + + { + if (inputIdToFocus) { + event.preventDefault(); + const input = document.getElementById(inputIdToFocus) as HTMLInputElement; + input?.focus(); + } + }} + > + { + onEmojiSelect(emoji.native); + setIsEmojiPickerOpen(false); + // Focus is handled by onCloseAutoFocus + }} + /> + + + ) +} diff --git a/components/Navigation.tsx b/components/Navigation.tsx index a7a273c..f90f6dd 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -7,7 +7,6 @@ import { useTranslations } from 'next-intl' import Link from 'next/link' import { usePathname } from 'next/navigation' import { useEffect, useState } from 'react' -import AboutModal from './AboutModal' type ViewPort = 'main' | 'mobile' @@ -68,7 +67,6 @@ export default function Navigation({ viewPort }: NavigationProps) { ))}
- setShowAbout(false)} /> ) } @@ -97,7 +95,6 @@ export default function Navigation({ viewPort }: NavigationProps) { - setShowAbout(false)} /> ) } diff --git a/components/Profile.tsx b/components/Profile.tsx index 8f68efe..17aff77 100644 --- a/components/Profile.tsx +++ b/components/Profile.tsx @@ -5,7 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { toast } from "@/hooks/use-toast" -import { settingsAtom, userSelectAtom } from "@/lib/atoms" +import { aboutOpenAtom, settingsAtom, userSelectAtom } from "@/lib/atoms" import { useHelpers } from "@/lib/client-helpers" import { useAtom } from "jotai" import { ArrowRightLeft, Crown, Info, LogOut, Moon, Palette, Settings, Sun, User } from "lucide-react" @@ -13,7 +13,6 @@ import { useTranslations } from 'next-intl' import { useTheme } from "next-themes" import Link from "next/link" import { useState } from "react" -import AboutModal from "./AboutModal" import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog' import UserForm from './UserForm' @@ -22,7 +21,7 @@ export function Profile() { const [settings] = useAtom(settingsAtom) const [userSelect, setUserSelect] = useAtom(userSelectAtom) const [isEditing, setIsEditing] = useState(false) - const [showAbout, setShowAbout] = useState(false) + const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom) const { theme, setTheme } = useTheme() const { currentUser: user } = useHelpers() const [open, setOpen] = useState(false) @@ -111,27 +110,32 @@ export function Profile() { - - - {t('settingsLink')} - +
+
+ + + {t('settingsLink')} + +
+
- - + { + setOpen(false); // Close the dropdown + setAboutOpen(true); // Open the about modal + }}> +
+
+ + {t('aboutButton')} +
+
-
-
+
+
{t('themeLabel')}
@@ -169,8 +173,6 @@ export function Profile() { - setShowAbout(false)} /> - {/* Add the UserForm dialog */} {isEditing && user && ( setIsEditing(false)}> diff --git a/components/UserSelectModal.tsx b/components/UserSelectModal.tsx index 1850681..c01b837 100644 --- a/components/UserSelectModal.tsx +++ b/components/UserSelectModal.tsx @@ -1,17 +1,6 @@ 'use client'; import { signIn } from '@/app/actions/user'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; import { toast } from '@/hooks/use-toast'; import { usersAtom } from '@/lib/atoms'; import { useHelpers } from '@/lib/client-helpers'; @@ -19,7 +8,7 @@ import { SafeUser, User } from '@/lib/types'; import { cn } from '@/lib/utils'; import { Description } from '@radix-ui/react-dialog'; import { useAtom } from 'jotai'; -import { Crown, Plus, Trash2, User as UserIcon, UserRoundPen } from 'lucide-react'; +import { Crown, Plus, User as UserIcon, UserRoundPen } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; import PasswordEntryForm from './PasswordEntryForm'; diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 1647513..afbac43 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef< ({ }) export const userSelectAtom = atom(false) +export const aboutOpenAtom = atom(false) // Derived atom for completion cache export const completionCacheAtom = atom((get) => { diff --git a/package.json b/package.json index a9d84aa..3830fb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habittrove", - "version": "0.2.16", + "version": "0.2.17", "private": true, "scripts": { "dev": "next dev --turbopack",