fix emoji picker and about modal (#146)

This commit is contained in:
Doh
2025-05-25 20:33:08 -04:00
committed by GitHub
parent 3ac311c3fd
commit 42c8d14d6d
12 changed files with 122 additions and 107 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## Version 0.2.17
### Fixed
* fix emoji selector (#142)
* fix about modal (#145)
## Version 0.2.16 ## Version 0.2.16
### Improved ### Improved

View File

@@ -11,17 +11,16 @@ import ChangelogModal from "./ChangelogModal"
import { useState } from "react" import { useState } from "react"
interface AboutModalProps { interface AboutModalProps {
isOpen: boolean
onClose: () => void onClose: () => void
} }
export default function AboutModal({ isOpen, onClose }: AboutModalProps) { export default function AboutModal({ onClose }: AboutModalProps) {
const t = useTranslations('AboutModal') const t = useTranslations('AboutModal')
const version = packageJson.version const version = packageJson.version
const [changelogOpen, setChangelogOpen] = useState(false) const [changelogOpen, setChangelogOpen] = useState(false)
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={true} onOpenChange={onClose}>
<DialogContent className="max-w-sm"> <DialogContent className="max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle aria-label={t('dialogArisLabel')}></DialogTitle> <DialogTitle aria-label={t('dialogArisLabel')}></DialogTitle>

View File

@@ -8,26 +8,16 @@ import { settingsAtom, browserSettingsAtom, usersAtom } from '@/lib/atoms'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Switch } from '@/components/ui/switch'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Info, SmilePlus, Zap } from 'lucide-react' import { Zap } from 'lucide-react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react'
import { Habit, SafeUser } from '@/lib/types' import { Habit, SafeUser } from '@/lib/types'
import EmojiPickerButton from './EmojiPickerButton'
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2s, d2t, serializeRRule } from '@/lib/utils' import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2s, d2t, serializeRRule } from '@/lib/utils'
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP, MAX_COIN_LIMIT } from '@/lib/constants' import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP, MAX_COIN_LIMIT } from '@/lib/constants'
import * as chrono from 'chrono-node';
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useHelpers } from '@/lib/client-helpers' import { useHelpers } from '@/lib/client-helpers'
interface AddEditHabitModalProps { interface AddEditHabitModalProps {
@@ -46,9 +36,9 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
const [targetCompletions, setTargetCompletions] = useState(habit?.targetCompletions || 1) const [targetCompletions, setTargetCompletions] = useState(habit?.targetCompletions || 1)
const isRecurRule = !isTask const isRecurRule = !isTask
// Initialize ruleText with the actual frequency string or default, not the display text // 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, frequency: habit.frequency,
isRecurRule, isRecurRule,
timezone: settings.system.timezone timezone: settings.system.timezone
}) : (isRecurRule ? INITIAL_RECURRENCE_RULE : INITIAL_DUE); }) : (isRecurRule ? INITIAL_RECURRENCE_RULE : INITIAL_DUE);
const [ruleText, setRuleText] = useState<string>(initialRuleText) const [ruleText, setRuleText] = useState<string>(initialRuleText)
@@ -119,33 +109,15 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
/> />
<Popover> <EmojiPickerButton
<PopoverTrigger asChild> inputIdToFocus="name"
<Button onEmojiSelect={(emoji) => {
type="button" setName(prev => {
variant="ghost" const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
size="icon" return `${prev}${space}${emoji}`;
className="h-8 w-8" })
> }}
<SmilePlus className="h-8 w-8" /> />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Picker
data={data}
onEmojiSelect={(emoji: { native: string }) => {
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()
}}
/>
</PopoverContent>
</Popover>
</div> </div>
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">

View File

@@ -9,12 +9,8 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
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' import { WishlistItemType } from '@/lib/types'
import EmojiPickerButton from './EmojiPickerButton'
import { MAX_COIN_LIMIT } from '@/lib/constants' import { MAX_COIN_LIMIT } from '@/lib/constants'
interface AddEditWishlistItemModalProps { interface AddEditWishlistItemModalProps {
@@ -114,7 +110,7 @@ export default function AddEditWishlistItemModal({
} else { } else {
addWishlistItem(itemData) addWishlistItem(itemData)
} }
setIsOpen(false) setIsOpen(false)
setEditingItem(null) setEditingItem(null)
} }
@@ -139,29 +135,15 @@ export default function AddEditWishlistItemModal({
className="flex-1" className="flex-1"
required required
/> />
<Popover> <EmojiPickerButton
<PopoverTrigger asChild> inputIdToFocus="name"
<Button onEmojiSelect={(emoji) => {
type="button" setName(prev => {
variant="ghost" const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
size="icon" return `${prev}${space}${emoji}`;
className="h-8 w-8" })
> }}
<SmilePlus className="h-4 w-4" /> />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Picker
data={data}
onEmojiSelect={(emoji: { native: string }) => {
setName(prev => `${prev}${emoji.native}`)
// Focus back on input after selection
const input = document.getElementById('name') as HTMLInputElement
input?.focus()
}}
/>
</PopoverContent>
</Popover>
</div> </div>
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
@@ -296,13 +278,13 @@ export default function AddEditWishlistItemModal({
<Avatar <Avatar
key={user.id} key={user.id}
className={`h-8 w-8 border-2 cursor-pointer className={`h-8 w-8 border-2 cursor-pointer
${selectedUserIds.includes(user.id) ${selectedUserIds.includes(user.id)
? 'border-primary' ? 'border-primary'
: 'border-muted' : 'border-muted'
}`} }`}
title={user.username} title={user.username}
onClick={() => { onClick={() => {
setSelectedUserIds(prev => setSelectedUserIds(prev =>
prev.includes(user.id) prev.includes(user.id)
? prev.filter(id => id !== user.id) ? prev.filter(id => id !== user.id)
: [...prev, user.id] : [...prev, user.id]

View File

@@ -2,14 +2,16 @@
import { ReactNode, useEffect } from 'react' import { ReactNode, useEffect } from 'react'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { pomodoroAtom, userSelectAtom } from '@/lib/atoms' import { aboutOpenAtom, pomodoroAtom, userSelectAtom } from '@/lib/atoms'
import PomodoroTimer from './PomodoroTimer' import PomodoroTimer from './PomodoroTimer'
import UserSelectModal from './UserSelectModal' import UserSelectModal from './UserSelectModal'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import AboutModal from './AboutModal'
export default function ClientWrapper({ children }: { children: ReactNode }) { export default function ClientWrapper({ children }: { children: ReactNode }) {
const [pomo] = useAtom(pomodoroAtom) const [pomo] = useAtom(pomodoroAtom)
const [userSelect, setUserSelect] = useAtom(userSelectAtom) const [userSelect, setUserSelect] = useAtom(userSelectAtom)
const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom)
const { data: session, status } = useSession() const { data: session, status } = useSession()
const currentUserId = session?.user.id const currentUserId = session?.user.id
@@ -27,7 +29,10 @@ export default function ClientWrapper({ children }: { children: ReactNode }) {
<PomodoroTimer /> <PomodoroTimer />
)} )}
{userSelect && ( {userSelect && (
<UserSelectModal onClose={() => setUserSelect(false)}/> <UserSelectModal onClose={() => setUserSelect(false)} />
)}
{aboutOpen && (
<AboutModal onClose={() => setAboutOpen(false)} />
)} )}
</> </>
) )

View File

@@ -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 (
<Popover modal={true} open={isEmojiPickerOpen} onOpenChange={setIsEmojiPickerOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8" // Consistent sizing
>
<SmilePlus className="h-4 w-4" /> {/* Consistent icon size */}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[300px] p-0"
onCloseAutoFocus={(event) => {
if (inputIdToFocus) {
event.preventDefault();
const input = document.getElementById(inputIdToFocus) as HTMLInputElement;
input?.focus();
}
}}
>
<Picker
data={data}
onEmojiSelect={(emoji: { native: string }) => {
onEmojiSelect(emoji.native);
setIsEmojiPickerOpen(false);
// Focus is handled by onCloseAutoFocus
}}
/>
</PopoverContent>
</Popover>
)
}

View File

@@ -16,7 +16,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import AboutModal from './AboutModal'
import Link from 'next/link' import Link from 'next/link'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { Profile } from './Profile' import { Profile } from './Profile'

View File

@@ -6,7 +6,6 @@ import { useAtom } from 'jotai'
import { browserSettingsAtom } from '@/lib/atoms' import { browserSettingsAtom } from '@/lib/atoms'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
import AboutModal from './AboutModal'
import { HabitIcon, TaskIcon } from '@/lib/constants' import { HabitIcon, TaskIcon } from '@/lib/constants'
import { useHelpers } from '@/lib/client-helpers' import { useHelpers } from '@/lib/client-helpers'
@@ -71,7 +70,6 @@ export default function Navigation({ className, viewPort }: NavigationProps) {
))} ))}
</div> </div>
</nav> </nav>
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
</> </>
) )
} }
@@ -97,7 +95,6 @@ export default function Navigation({ className, viewPort }: NavigationProps) {
</div> </div>
</div> </div>
</div> </div>
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
</div> </div>
) )
} }

View File

@@ -8,8 +8,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'
import UserForm from './UserForm' import UserForm from './UserForm'
import Link from "next/link" import Link from "next/link"
import { useAtom } from "jotai" import { useAtom } from "jotai"
import { settingsAtom, userSelectAtom } from "@/lib/atoms" import { aboutOpenAtom, settingsAtom, userSelectAtom } from "@/lib/atoms"
import AboutModal from "./AboutModal"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { signOut } from "@/app/actions/user" import { signOut } from "@/app/actions/user"
@@ -22,7 +21,7 @@ export function Profile() {
const [settings] = useAtom(settingsAtom) const [settings] = useAtom(settingsAtom)
const [userSelect, setUserSelect] = useAtom(userSelectAtom) const [userSelect, setUserSelect] = useAtom(userSelectAtom)
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [showAbout, setShowAbout] = useState(false) const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom)
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const { currentUser: user } = useHelpers() const { currentUser: user } = useHelpers()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -111,27 +110,32 @@ export function Profile() {
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer px-2 py-1.5" asChild> <DropdownMenuItem className="cursor-pointer px-2 py-1.5" asChild>
<Link <div className="flex items-center justify-between w-full">
href="/settings" <div className="flex items-center gap-2">
aria-label={t('settingsLink')} <Settings className="h-4 w-4" />
className="flex items-center w-full gap-3" <Link
> href="/settings"
<Settings className="h-4 w-4" /> aria-label={t('settingsLink')}
<span>{t('settingsLink')}</span> >
</Link> <span>{t('settingsLink')}</span>
</Link>
</div>
</div>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer px-2 py-1.5" asChild> <DropdownMenuItem className="cursor-pointer px-2 py-1.5" onClick={() => {
<button setOpen(false); // Close the dropdown
onClick={() => setShowAbout(true)} setAboutOpen(true); // Open the about modal
className="flex items-center w-full gap-3" }}>
> <div className="flex items-center justify-between w-full">
<Info className="h-4 w-4" /> <div className="flex items-center gap-2">
<span>{t('aboutButton')}</span> <Info className="h-4 w-4" />
</button> <span>{t('aboutButton')}</span>
</div>
</div>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer px-2 py-1.5"> <DropdownMenuItem className="cursor-pointer px-2 py-1.5">
<div className="flex items-center justify-between w-full gap-3"> <div className="flex items-center justify-between w-full">
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<Palette className="h-4 w-4" /> <Palette className="h-4 w-4" />
<span>{t('themeLabel')}</span> <span>{t('themeLabel')}</span>
</div> </div>
@@ -169,8 +173,6 @@ export function Profile() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
{/* Add the UserForm dialog */} {/* Add the UserForm dialog */}
{isEditing && user && ( {isEditing && user && (
<Dialog open={isEditing} onOpenChange={() => setIsEditing(false)}> <Dialog open={isEditing} onOpenChange={() => setIsEditing(false)}>

View File

@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:pointer-events-none",
className className
)} )}
{...props} {...props}

View File

@@ -107,6 +107,7 @@ export const pomodoroAtom = atom<PomodoroAtom>({
}) })
export const userSelectAtom = atom<boolean>(false) export const userSelectAtom = atom<boolean>(false)
export const aboutOpenAtom = atom<boolean>(false)
// Derived atom for completion cache // Derived atom for completion cache
export const completionCacheAtom = atom((get) => { export const completionCacheAtom = atom((get) => {

View File

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