mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-20 22:24:28 +01:00
Fix emojipicker (#152)
This commit is contained in:
@@ -15,6 +15,7 @@ import { Zap } from 'lucide-react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Habit, SafeUser } from '@/lib/types'
|
||||
import EmojiPickerButton from './EmojiPickerButton'
|
||||
import ModalOverlay from './ModalOverlay' // Import the new component
|
||||
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 { DateTime } from 'luxon'
|
||||
@@ -87,249 +88,252 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{habit
|
||||
? t(isTask ? 'editTaskTitle' : 'editHabitTitle')
|
||||
: t(isTask ? 'addNewTaskTitle' : 'addNewHabitTitle')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
{t('nameLabel')}
|
||||
</Label>
|
||||
<div className='flex col-span-3 gap-2'>
|
||||
<Input
|
||||
id="name"
|
||||
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">
|
||||
<>
|
||||
<ModalOverlay />
|
||||
<Dialog open={true} onOpenChange={onClose} modal={false}>
|
||||
<DialogContent> {/* DialogContent from shadcn/ui is typically z-50, ModalOverlay is z-40 */}
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{habit
|
||||
? t(isTask ? 'editTaskTitle' : 'editHabitTitle')
|
||||
: t(isTask ? 'addNewTaskTitle' : 'addNewHabitTitle')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
{t('nameLabel')}
|
||||
</Label>
|
||||
<div className='flex col-span-3 gap-2'>
|
||||
<Input
|
||||
id="recurrence"
|
||||
value={ruleText}
|
||||
onChange={(e) => setRuleText(e.target.value)}
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(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>
|
||||
)}
|
||||
<EmojiPickerButton
|
||||
inputIdToFocus="name"
|
||||
onEmojiSelect={(emoji) => {
|
||||
setName(prev => {
|
||||
const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
|
||||
return `${prev}${space}${emoji}`;
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</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="flex items-center justify-end gap-2">
|
||||
<Label htmlFor="sharing-toggle">{t('shareLabel')}</Label>
|
||||
<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
|
||||
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 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]
|
||||
)
|
||||
}}
|
||||
<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"
|
||||
>
|
||||
<AvatarImage src={user?.avatarPath && `/api/avatars/${user.avatarPath.split('/').pop()}` || ""} />
|
||||
<AvatarFallback>{user.username[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
-
|
||||
</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>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={!!errorMessage}>
|
||||
{habit
|
||||
? t('saveChangesButton')
|
||||
: t(isTask ? 'addTaskButton' : 'addHabitButton')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<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="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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user