mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-20 22:24:28 +01:00
Merge Tag v0.2.17.0
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## Version 0.2.17
|
||||
|
||||
### Fixed
|
||||
|
||||
* fix emoji selector (#142)
|
||||
* fix about modal (#145)
|
||||
|
||||
## Version 0.2.16
|
||||
|
||||
### Improved
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HabitTrove
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle aria-label={t('dialogArisLabel')}></DialogTitle>
|
||||
|
||||
@@ -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
|
||||
@@ -111,33 +110,15 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
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 }) => {
|
||||
<EmojiPickerButton
|
||||
inputIdToFocus="name"
|
||||
onEmojiSelect={(emoji) => {
|
||||
setName(prev => {
|
||||
// Add space before emoji if there isn't one already
|
||||
const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
|
||||
return `${prev}${space}${emoji.native}`;
|
||||
return `${prev}${space}${emoji}`;
|
||||
})
|
||||
// Focus back on input after selection
|
||||
const input = document.getElementById('name') as HTMLInputElement
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
|
||||
@@ -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 {
|
||||
@@ -139,29 +136,15 @@ export default function AddEditWishlistItemModal({
|
||||
className="flex-1"
|
||||
required
|
||||
/>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
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()
|
||||
<EmojiPickerButton
|
||||
inputIdToFocus="name"
|
||||
onEmojiSelect={(emoji) => {
|
||||
setName(prev => {
|
||||
const space = prev.length > 0 && !prev.endsWith(' ') ? ' ' : '';
|
||||
return `${prev}${space}${emoji}`;
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,6 +31,9 @@ export default function ClientWrapper({ children }: { children: ReactNode }) {
|
||||
{userSelect && (
|
||||
<UserSelectModal onClose={() => setUserSelect(false)} />
|
||||
)}
|
||||
{aboutOpen && (
|
||||
<AboutModal onClose={() => setAboutOpen(false)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
51
components/EmojiPickerButton.tsx
Normal file
51
components/EmojiPickerButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -97,7 +95,6 @@ export default function Navigation({ viewPort }: NavigationProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer px-2 py-1.5" asChild>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
<Link
|
||||
href="/settings"
|
||||
aria-label={t('settingsLink')}
|
||||
className="flex items-center w-full gap-3"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>{t('settingsLink')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer px-2 py-1.5" asChild>
|
||||
<button
|
||||
onClick={() => setShowAbout(true)}
|
||||
className="flex items-center w-full gap-3"
|
||||
>
|
||||
<DropdownMenuItem className="cursor-pointer px-2 py-1.5" onClick={() => {
|
||||
setOpen(false); // Close the dropdown
|
||||
setAboutOpen(true); // Open the about modal
|
||||
}}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="h-4 w-4" />
|
||||
<span>{t('aboutButton')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<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 gap-3">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="h-4 w-4" />
|
||||
<span>{t('themeLabel')}</span>
|
||||
</div>
|
||||
@@ -169,8 +173,6 @@ export function Profile() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
|
||||
|
||||
{/* Add the UserForm dialog */}
|
||||
{isEditing && user && (
|
||||
<Dialog open={isEditing} onOpenChange={() => setIsEditing(false)}>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -99,6 +99,7 @@ export const pomodoroAtom = atom<PomodoroAtom>({
|
||||
})
|
||||
|
||||
export const userSelectAtom = atom<boolean>(false)
|
||||
export const aboutOpenAtom = atom<boolean>(false)
|
||||
|
||||
// Derived atom for completion cache
|
||||
export const completionCacheAtom = atom((get) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habittrove",
|
||||
"version": "0.2.16",
|
||||
"version": "0.2.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
|
||||
Reference in New Issue
Block a user