Fix emojipicker (#152)

This commit is contained in:
Doh
2025-05-29 08:46:08 -04:00
committed by GitHub
parent 5ae659469b
commit 9e0ae1e0da
6 changed files with 441 additions and 416 deletions

View File

@@ -1,5 +1,11 @@
# Changelog # Changelog
## Version 0.2.21
### Fixed
* emoji picker overlay issue (#150)
## Version 0.2.20 ## Version 0.2.20
### Fixed ### Fixed

View File

@@ -15,6 +15,7 @@ import { Zap } from 'lucide-react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Habit, SafeUser } from '@/lib/types' import { Habit, SafeUser } from '@/lib/types'
import EmojiPickerButton from './EmojiPickerButton' import EmojiPickerButton from './EmojiPickerButton'
import ModalOverlay from './ModalOverlay' // Import the new component
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 { DateTime } from 'luxon' import { DateTime } from 'luxon'
@@ -87,249 +88,252 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
} }
return ( return (
<Dialog open={true} onOpenChange={onClose}> <>
<DialogContent> <ModalOverlay />
<DialogHeader> <Dialog open={true} onOpenChange={onClose} modal={false}>
<DialogTitle> <DialogContent> {/* DialogContent from shadcn/ui is typically z-50, ModalOverlay is z-40 */}
{habit <DialogHeader>
? t(isTask ? 'editTaskTitle' : 'editHabitTitle') <DialogTitle>
: t(isTask ? 'addNewTaskTitle' : 'addNewHabitTitle')} {habit
</DialogTitle> ? t(isTask ? 'editTaskTitle' : 'editHabitTitle')
</DialogHeader> : t(isTask ? 'addNewTaskTitle' : 'addNewHabitTitle')}
<form onSubmit={handleSubmit}> </DialogTitle>
<div className="grid gap-4 py-4"> </DialogHeader>
<div className="grid grid-cols-4 items-center gap-4"> <form onSubmit={handleSubmit}>
<Label htmlFor="name" className="text-right"> <div className="grid gap-4 py-4">
{t('nameLabel')} <div className="grid grid-cols-4 items-center gap-4">
</Label> <Label htmlFor="name" className="text-right">
<div className='flex col-span-3 gap-2'> {t('nameLabel')}
<Input </Label>
id="name" <div className='flex col-span-3 gap-2'>
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<EmojiPickerButton
inputIdToFocus="name"
onEmojiSelect={(emoji) => {
setName(prev => {
const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
return `${prev}${space}${emoji}`;
})
}}
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
{t('descriptionLabel')}
</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="recurrence" className="text-right">
{t('whenLabel')}
</Label>
{/* date input (task) */}
<div className="col-span-3 space-y-2">
<div className="flex gap-2">
<Input <Input
id="recurrence" id="name"
value={ruleText} value={name}
onChange={(e) => setRuleText(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
/> />
{isTask && ( <EmojiPickerButton
<Popover open={isQuickDatesOpen} onOpenChange={setIsQuickDatesOpen}> inputIdToFocus="name"
<PopoverTrigger asChild> onEmojiSelect={(emoji) => {
<Button setName(prev => {
type="button" const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
variant="ghost" return `${prev}${space}${emoji}`;
size="icon" })
className="h-8 w-8" }}
> />
<Zap className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-3 w-[280px] max-h-[40vh] overflow-y-auto" align="start">
<div className="space-y-1">
<div className="grid grid-cols-2 gap-2">
{QUICK_DATES.map((date) => (
<Button
key={date.value}
variant="outline"
className="justify-start h-9 px-3 hover:bg-primary hover:text-primary-foreground transition-colors"
onClick={() => {
setRuleText(date.value);
setIsQuickDatesOpen(false);
}}
>
{date.label}
</Button>
))}
</div>
</div>
</PopoverContent>
</Popover>
)}
</div> </div>
</div> </div>
{/* rrule input (habit) */}
<div className="col-start-2 col-span-3 text-sm">
{(() => {
let displayText = '';
const { result, message } = convertHumanReadableFrequencyToMachineReadable({ text: ruleText, timezone: settings.system.timezone, isRecurring: isRecurRule });
if (message !== errorMessage) { // Only update if it changed to avoid re-renders
setErrorMessage(message);
}
displayText = convertMachineReadableFrequencyToHumanReadable({ frequency: result, isRecurRule, timezone: settings.system.timezone })
return (
<>
<span className={errorMessage ? 'text-destructive' : 'text-muted-foreground'}>
{displayText}
</span>
{errorMessage && (
<p className="text-destructive text-xs mt-1">{errorMessage}</p>
)}
</>
);
})()}
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center gap-2 justify-end">
<Label htmlFor="targetCompletions">
{t('completeLabel')}
</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 => Math.max(1, 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="targetCompletions"
type="number"
value={targetCompletions}
onChange={(e) => {
const value = parseInt(e.target.value)
setTargetCompletions(isNaN(value) ? 1 : Math.max(1, value))
}}
min={1}
max={10}
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 + 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">
{t('timesSuffix')}
</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="coinReward">
{t('rewardLabel')}
</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={() => setCoinReward(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="coinReward"
type="number"
value={coinReward}
onChange={(e) => {
const value = parseInt(e.target.value === "" ? "0" : e.target.value)
setCoinReward(Math.min(value, MAX_COIN_LIMIT))
}}
min={0}
max={MAX_COIN_LIMIT}
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={() => setCoinReward(prev => Math.min(prev + 1, MAX_COIN_LIMIT))}
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">
{t('coinsSuffix')}
</span>
</div>
</div>
</div>
{users && users.length > 1 && (
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center justify-end gap-2"> <Label htmlFor="description" className="text-right">
<Label htmlFor="sharing-toggle">{t('shareLabel')}</Label> {t('descriptionLabel')}
</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="recurrence" className="text-right">
{t('whenLabel')}
</Label>
{/* date input (task) */}
<div className="col-span-3 space-y-2">
<div className="flex gap-2">
<Input
id="recurrence"
value={ruleText}
onChange={(e) => setRuleText(e.target.value)}
required
/>
{isTask && (
<Popover open={isQuickDatesOpen} onOpenChange={setIsQuickDatesOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
>
<Zap className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-3 w-[280px] max-h-[40vh] overflow-y-auto" align="start">
<div className="space-y-1">
<div className="grid grid-cols-2 gap-2">
{QUICK_DATES.map((date) => (
<Button
key={date.value}
variant="outline"
className="justify-start h-9 px-3 hover:bg-primary hover:text-primary-foreground transition-colors"
onClick={() => {
setRuleText(date.value);
setIsQuickDatesOpen(false);
}}
>
{date.label}
</Button>
))}
</div>
</div>
</PopoverContent>
</Popover>
)}
</div>
</div>
{/* rrule input (habit) */}
<div className="col-start-2 col-span-3 text-sm">
{(() => {
let displayText = '';
const { result, message } = convertHumanReadableFrequencyToMachineReadable({ text: ruleText, timezone: settings.system.timezone, isRecurring: isRecurRule });
if (message !== errorMessage) { // Only update if it changed to avoid re-renders
setErrorMessage(message);
}
displayText = convertMachineReadableFrequencyToHumanReadable({ frequency: result, isRecurRule, timezone: settings.system.timezone })
return (
<>
<span className={errorMessage ? 'text-destructive' : 'text-muted-foreground'}>
{displayText}
</span>
{errorMessage && (
<p className="text-destructive text-xs mt-1">{errorMessage}</p>
)}
</>
);
})()}
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center gap-2 justify-end">
<Label htmlFor="targetCompletions">
{t('completeLabel')}
</Label>
</div> </div>
<div className="col-span-3"> <div className="col-span-3">
<div className="flex flex-wrap gap-2"> <div className="flex items-center gap-4">
{users.filter((u) => u.id !== currentUser?.id).map(user => ( <div className="flex items-center border rounded-lg overflow-hidden">
<Avatar <button
key={user.id} type="button"
className={`h-8 w-8 border-2 cursor-pointer onClick={() => setTargetCompletions(prev => Math.max(1, prev - 1))}
${selectedUserIds.includes(user.id) className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
? 'border-primary'
: 'border-muted'
}`}
title={user.username}
onClick={() => {
setSelectedUserIds(prev =>
prev.includes(user.id)
? prev.filter(id => id !== user.id)
: [...prev, user.id]
)
}}
> >
<AvatarImage src={user?.avatarPath && `/api/avatars/${user.avatarPath.split('/').pop()}` || ""} /> -
<AvatarFallback>{user.username[0]}</AvatarFallback> </button>
</Avatar> <Input
))} id="targetCompletions"
type="number"
value={targetCompletions}
onChange={(e) => {
const value = parseInt(e.target.value)
setTargetCompletions(isNaN(value) ? 1 : Math.max(1, value))
}}
min={1}
max={10}
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 + 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">
{t('timesSuffix')}
</span>
</div> </div>
</div> </div>
</div> </div>
)} <div className="grid grid-cols-4 items-center gap-4">
</div> <div className="flex items-center gap-2 justify-end">
<DialogFooter> <Label htmlFor="coinReward">
<Button type="submit" disabled={!!errorMessage}> {t('rewardLabel')}
{habit </Label>
? t('saveChangesButton') </div>
: t(isTask ? 'addTaskButton' : 'addHabitButton')} <div className="col-span-3">
</Button> <div className="flex items-center gap-4">
</DialogFooter> <div className="flex items-center border rounded-lg overflow-hidden">
</form> <button
</DialogContent> type="button"
</Dialog> onClick={() => setCoinReward(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="coinReward"
type="number"
value={coinReward}
onChange={(e) => {
const value = parseInt(e.target.value === "" ? "0" : e.target.value)
setCoinReward(Math.min(value, MAX_COIN_LIMIT))
}}
min={0}
max={MAX_COIN_LIMIT}
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={() => setCoinReward(prev => Math.min(prev + 1, MAX_COIN_LIMIT))}
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">
{t('coinsSuffix')}
</span>
</div>
</div>
</div>
{users && users.length > 1 && (
<div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center justify-end gap-2">
<Label htmlFor="sharing-toggle">{t('shareLabel')}</Label>
</div>
<div className="col-span-3">
<div className="flex flex-wrap gap-2">
{users.filter((u) => u.id !== currentUser?.id).map(user => (
<Avatar
key={user.id}
className={`h-8 w-8 border-2 cursor-pointer
${selectedUserIds.includes(user.id)
? 'border-primary'
: 'border-muted'
}`}
title={user.username}
onClick={() => {
setSelectedUserIds(prev =>
prev.includes(user.id)
? prev.filter(id => id !== user.id)
: [...prev, user.id]
)
}}
>
<AvatarImage src={user?.avatarPath && `/api/avatars/${user.avatarPath.split('/').pop()}` || ""} />
<AvatarFallback>{user.username[0]}</AvatarFallback>
</Avatar>
))}
</div>
</div>
</div>
)}
</div>
<DialogFooter>
<Button type="submit" disabled={!!errorMessage}>
{habit
? t('saveChangesButton')
: t(isTask ? 'addTaskButton' : 'addHabitButton')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
) )
} }

View File

@@ -10,6 +10,7 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { WishlistItemType } from '@/lib/types' import { WishlistItemType } from '@/lib/types'
import EmojiPickerButton from './EmojiPickerButton' import EmojiPickerButton from './EmojiPickerButton'
import ModalOverlay from './ModalOverlay'
import { MAX_COIN_LIMIT } from '@/lib/constants' import { MAX_COIN_LIMIT } from '@/lib/constants'
interface AddEditWishlistItemModalProps { interface AddEditWishlistItemModalProps {
@@ -115,196 +116,199 @@ export default function AddEditWishlistItemModal({
} }
return ( return (
<Dialog open={isOpen} onOpenChange={handleClose}> <>
<DialogContent> <ModalOverlay />
<DialogHeader> <Dialog open={true} onOpenChange={handleClose} modal={false}>
<DialogTitle>{editingItem ? t('editTitle') : t('addTitle')}</DialogTitle> <DialogContent> {/* DialogContent from shadcn/ui is typically z-50, ModalOverlay is z-40 */}
</DialogHeader> <DialogHeader>
<form onSubmit={handleSave}> <DialogTitle>{editingItem ? t('editTitle') : t('addTitle')}</DialogTitle>
<div className="grid gap-4 py-4"> </DialogHeader>
<div className="grid grid-cols-4 items-center gap-4"> <form onSubmit={handleSave}>
<Label htmlFor="name" className="text-right"> <div className="grid gap-4 py-4">
{t('nameLabel')} <div className="grid grid-cols-4 items-center gap-4">
</Label> <Label htmlFor="name" className="text-right">
<div className="col-span-3 flex gap-2"> {t('nameLabel')}
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="flex-1"
required
/>
<EmojiPickerButton
inputIdToFocus="name"
onEmojiSelect={(emoji) => {
setName(prev => {
const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
return `${prev}${space}${emoji}`;
})
}}
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
{t('descriptionLabel')}
</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center gap-2 justify-end">
<Label htmlFor="coinReward">
{t('costLabel')}
</Label> </Label>
</div> <div className="col-span-3 flex gap-2">
<div className="col-span-3"> <Input
<div className="flex items-center gap-4"> id="name"
<div className="flex items-center border rounded-lg overflow-hidden"> value={name}
<button onChange={(e) => setName(e.target.value)}
type="button" className="flex-1"
onClick={() => setCoinCost(prev => Math.max(0, prev - 1))} required
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors" />
> <EmojiPickerButton
- inputIdToFocus="name"
</button> onEmojiSelect={(emoji) => {
<Input setName(prev => {
id="coinReward" const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
type="number" return `${prev}${space}${emoji}`;
value={coinCost} })
onChange={(e) => { }}
const value = parseInt(e.target.value === "" ? "0" : e.target.value) />
setCoinCost(Math.min(value, MAX_COIN_LIMIT))
}}
min={0}
max={MAX_COIN_LIMIT}
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 => Math.min(prev + 1, MAX_COIN_LIMIT))}
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">
{t('coinsSuffix')}
</span>
</div> </div>
</div> </div>
</div> <div className="grid grid-cols-4 items-center gap-4">
<div className="grid grid-cols-4 items-center gap-4"> <Label htmlFor="description" className="text-right">
<div className="flex items-center gap-2 justify-end"> {t('descriptionLabel')}
<Label htmlFor="targetCompletions">
{t('redeemableLabel')}
</Label> </Label>
</div> <Textarea
<div className="col-span-3"> id="description"
<div className="flex items-center gap-4"> value={description}
<div className="flex items-center border rounded-lg overflow-hidden"> onChange={(e) => setDescription(e.target.value)}
<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">
{t('timesSuffix')}
</span>
</div>
{errors.targetCompletions && (
<div className="text-sm text-red-500">
{errors.targetCompletions}
</div>
)}
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="link" className="text-right">
{t('linkLabel')}
</Label>
<div className="col-span-3">
<Input
id="link"
type="url"
placeholder="https://..."
value={link}
onChange={(e) => setLink(e.target.value)}
className="col-span-3" className="col-span-3"
/> />
{errors.link && (
<div className="text-sm text-red-500">
{errors.link}
</div>
)}
</div> </div>
</div>
{usersData.users && usersData.users.length > 1 && (
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center gap-2 justify-end">
<Label htmlFor="sharing-toggle">{t('shareLabel')}</Label> <Label htmlFor="coinReward">
{t('costLabel')}
</Label>
</div> </div>
<div className="col-span-3"> <div className="col-span-3">
<div className="flex flex-wrap gap-2"> <div className="flex items-center gap-4">
{usersData.users.filter((u) => u.id !== currentUser?.id).map(user => ( <div className="flex items-center border rounded-lg overflow-hidden">
<Avatar <button
key={user.id} type="button"
className={`h-8 w-8 border-2 cursor-pointer onClick={() => setCoinCost(prev => Math.max(0, prev - 1))}
${selectedUserIds.includes(user.id) className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
? 'border-primary'
: 'border-muted'
}`}
title={user.username}
onClick={() => {
setSelectedUserIds(prev =>
prev.includes(user.id)
? prev.filter(id => id !== user.id)
: [...prev, user.id]
)
}}
> >
<AvatarImage src={user?.avatarPath && `/api/avatars/${user.avatarPath.split('/').pop()}` || ""} /> -
<AvatarFallback>{user.username[0]}</AvatarFallback> </button>
</Avatar> <Input
))} id="coinReward"
type="number"
value={coinCost}
onChange={(e) => {
const value = parseInt(e.target.value === "" ? "0" : e.target.value)
setCoinCost(Math.min(value, MAX_COIN_LIMIT))
}}
min={0}
max={MAX_COIN_LIMIT}
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 => Math.min(prev + 1, MAX_COIN_LIMIT))}
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">
{t('coinsSuffix')}
</span>
</div> </div>
</div> </div>
</div> </div>
)} <div className="grid grid-cols-4 items-center gap-4">
</div> <div className="flex items-center gap-2 justify-end">
<DialogFooter> <Label htmlFor="targetCompletions">
<Button type="submit">{editingItem ? t('saveButton') : t('addButton')}</Button> {t('redeemableLabel')}
</DialogFooter> </Label>
</form> </div>
</DialogContent> <div className="col-span-3">
</Dialog> <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">
{t('timesSuffix')}
</span>
</div>
{errors.targetCompletions && (
<div className="text-sm text-red-500">
{errors.targetCompletions}
</div>
)}
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="link" className="text-right">
{t('linkLabel')}
</Label>
<div className="col-span-3">
<Input
id="link"
type="url"
placeholder="https://..."
value={link}
onChange={(e) => setLink(e.target.value)}
className="col-span-3"
/>
{errors.link && (
<div className="text-sm text-red-500">
{errors.link}
</div>
)}
</div>
</div>
{usersData.users && usersData.users.length > 1 && (
<div className="grid grid-cols-4 items-center gap-4">
<div className="flex items-center justify-end gap-2">
<Label htmlFor="sharing-toggle">{t('shareLabel')}</Label>
</div>
<div className="col-span-3">
<div className="flex flex-wrap gap-2">
{usersData.users.filter((u) => u.id !== currentUser?.id).map(user => (
<Avatar
key={user.id}
className={`h-8 w-8 border-2 cursor-pointer
${selectedUserIds.includes(user.id)
? 'border-primary'
: 'border-muted'
}`}
title={user.username}
onClick={() => {
setSelectedUserIds(prev =>
prev.includes(user.id)
? prev.filter(id => id !== user.id)
: [...prev, user.id]
)
}}
>
<AvatarImage src={user?.avatarPath && `/api/avatars/${user.avatarPath.split('/').pop()}` || ""} />
<AvatarFallback>{user.username[0]}</AvatarFallback>
</Avatar>
))}
</div>
</div>
</div>
)}
</div>
<DialogFooter>
<Button type="submit">{editingItem ? t('saveButton') : t('addButton')}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
) )
} }

View File

@@ -16,7 +16,7 @@ export default function EmojiPickerButton({ onEmojiSelect, inputIdToFocus }: Emo
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false) const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false)
return ( return (
<Popover modal={true} open={isEmojiPickerOpen} onOpenChange={setIsEmojiPickerOpen}> <Popover modal={false} open={isEmojiPickerOpen} onOpenChange={setIsEmojiPickerOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
type="button" type="button"

View File

@@ -0,0 +1,9 @@
/**
* ModalOverlay provides a dimming effect for non-modal dialogs or popovers
* that need to appear modal (e.g., to prevent interaction with background elements).
* It should be rendered alongside the dialog/popover it's intended to overlay for.
* Ensure the dialog/popover has a z-index higher than this overlay (default z-40).
*/
export default function ModalOverlay() {
return <div className="fixed inset-0 z-50 bg-black/80" />
}

View File

@@ -152,14 +152,16 @@ export default function WishlistManager() {
</> </>
)} )}
</div> </div>
<AddEditWishlistItemModal {isModalOpen &&
isOpen={isModalOpen} <AddEditWishlistItemModal
setIsOpen={setIsModalOpen} isOpen={isModalOpen}
editingItem={editingItem} setIsOpen={setIsModalOpen}
setEditingItem={setEditingItem} editingItem={editingItem}
addWishlistItem={addWishlistItem} setEditingItem={setEditingItem}
editWishlistItem={editWishlistItem} addWishlistItem={addWishlistItem}
/> editWishlistItem={editWishlistItem}
/>
}
<ConfirmDialog <ConfirmDialog
isOpen={deleteConfirmation.isOpen} isOpen={deleteConfirmation.isOpen}
onClose={() => setDeleteConfirmation({ isOpen: false, itemId: null })} onClose={() => setDeleteConfirmation({ isOpen: false, itemId: null })}