mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
fix coin stats and add transaction note (#31)
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState } from 'react'
|
||||
import { t2d, d2s, getNow, isSameDate } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { History } from 'lucide-react'
|
||||
import { History, Pencil } from 'lucide-react'
|
||||
import EmptyState from './EmptyState'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -12,11 +12,13 @@ import { settingsAtom } from '@/lib/atoms'
|
||||
import Link from 'next/link'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { TransactionNoteEditor } from './TransactionNoteEditor'
|
||||
|
||||
export default function CoinsManager() {
|
||||
const {
|
||||
add,
|
||||
remove,
|
||||
updateNote,
|
||||
balance,
|
||||
transactions,
|
||||
coinsEarnedToday,
|
||||
@@ -31,14 +33,26 @@ export default function CoinsManager() {
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
||||
const [note, setNote] = useState('')
|
||||
|
||||
const handleSaveNote = async (transactionId: string, note: string) => {
|
||||
await updateNote(transactionId, note)
|
||||
}
|
||||
|
||||
const handleDeleteNote = async (transactionId: string) => {
|
||||
await updateNote(transactionId, '')
|
||||
}
|
||||
|
||||
const handleAddRemoveCoins = async () => {
|
||||
const numAmount = Number(amount)
|
||||
if (numAmount > 0) {
|
||||
await add(numAmount, "Manual addition")
|
||||
await add(numAmount, "Manual addition", note)
|
||||
setAmount(DEFAULT_AMOUNT)
|
||||
setNote('')
|
||||
} else if (numAmount < 0) {
|
||||
await remove(Math.abs(numAmount), "Manual removal")
|
||||
await remove(Math.abs(numAmount), "Manual removal", note)
|
||||
setAmount(DEFAULT_AMOUNT)
|
||||
setNote('')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,45 +73,51 @@ export default function CoinsManager() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-10 w-10 text-lg"
|
||||
onClick={() => setAmount(prev => (Number(prev) - 1).toString())}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<div className="relative w-32">
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="text-center text-xl font-medium h-12"
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||
🪙
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-10 w-10 text-lg"
|
||||
onClick={() => setAmount(prev => (Number(prev) - 1).toString())}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<div className="relative w-32">
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="text-center text-xl font-medium h-12"
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||
🪙
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-10 w-10 text-lg"
|
||||
onClick={() => setAmount(prev => (Number(prev) + 1).toString())}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleAddRemoveCoins}
|
||||
className="flex-1 h-14 transition-colors flex items-center justify-center font-medium"
|
||||
variant="default"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{Number(amount) >= 0 ? 'Add Coins' : 'Remove Coins'}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-10 w-10 text-lg"
|
||||
onClick={() => setAmount(prev => (Number(prev) + 1).toString())}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleAddRemoveCoins}
|
||||
className="w-full h-14 transition-colors flex items-center justify-center font-medium"
|
||||
variant="default"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{Number(amount) >= 0 ? 'Add Coins' : 'Remove Coins'}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -236,6 +256,12 @@ export default function CoinsManager() {
|
||||
<p className="text-sm text-gray-500">
|
||||
{d2s({ dateTime: t2d({ timestamp: transaction.timestamp, timezone: settings.system.timezone }), timezone: settings.system.timezone })}
|
||||
</p>
|
||||
<TransactionNoteEditor
|
||||
transactionId={transaction.id}
|
||||
initialNote={transaction.note}
|
||||
onSave={handleSaveNote}
|
||||
onDelete={handleDeleteNote}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={`font-mono ${transaction.amount >= 0
|
||||
|
||||
@@ -38,17 +38,17 @@ export default function Header({ className }: HeaderProps) {
|
||||
<Link href="/coins" className="flex items-center gap-1 sm:gap-2 px-3 py-1.5 bg-white hover:bg-gray-50 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-full transition-colors border border-gray-200 dark:border-gray-600">
|
||||
<Coins className="h-5 w-5 text-yellow-500 dark:text-yellow-400" />
|
||||
<div className="flex items-baseline gap-1 sm:gap-2">
|
||||
<FormattedNumber
|
||||
amount={balance}
|
||||
settings={settings}
|
||||
className="text-gray-800 dark:text-gray-100 font-medium text-lg"
|
||||
<FormattedNumber
|
||||
amount={balance}
|
||||
settings={settings}
|
||||
className="text-gray-800 dark:text-gray-100 font-medium text-lg"
|
||||
/>
|
||||
{coinsEarnedToday > 0 && (
|
||||
<span className="text-sm bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-full border border-green-100 dark:border-green-800">
|
||||
<FormattedNumber
|
||||
amount={coinsEarnedToday}
|
||||
settings={settings}
|
||||
className="inline"
|
||||
<FormattedNumber
|
||||
amount={coinsEarnedToday}
|
||||
settings={settings}
|
||||
className="inline"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
138
components/TransactionNoteEditor.tsx
Normal file
138
components/TransactionNoteEditor.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Check, Loader2, Pencil, Trash2, X } from 'lucide-react'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
||||
interface TransactionNoteEditorProps {
|
||||
transactionId: string
|
||||
initialNote?: string
|
||||
onSave: (id: string, note: string) => Promise<void>
|
||||
onDelete: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function TransactionNoteEditor({
|
||||
transactionId,
|
||||
initialNote = '',
|
||||
onSave,
|
||||
onDelete
|
||||
}: TransactionNoteEditorProps) {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [noteText, setNoteText] = useState(initialNote)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const handleSave = async () => {
|
||||
const trimmedNote = noteText.trim()
|
||||
if (trimmedNote.length > 200) {
|
||||
toast({
|
||||
title: 'Note too long',
|
||||
description: 'Notes must be less than 200 characters',
|
||||
variant: 'destructive'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await onSave(transactionId, trimmedNote)
|
||||
setIsEditing(false)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error saving note',
|
||||
description: 'Please try again',
|
||||
variant: 'destructive'
|
||||
})
|
||||
// Revert to initial value on error
|
||||
setNoteText(initialNote)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await onDelete(transactionId)
|
||||
setNoteText(initialNote)
|
||||
setIsEditing(false)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error deleting note',
|
||||
description: 'Please try again',
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Input
|
||||
value={noteText}
|
||||
onChange={(e) => setNoteText(e.target.value)}
|
||||
placeholder="Add a note..."
|
||||
className="w-64"
|
||||
maxLength={200}
|
||||
/>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="text-green-600 dark:text-green-500 hover:text-green-700 dark:hover:text-green-400 transition-colors"
|
||||
title="Save note"
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setNoteText(initialNote)
|
||||
setIsEditing(false)
|
||||
}}
|
||||
disabled={isSaving}
|
||||
className="text-red-600 dark:text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors"
|
||||
title="Cancel"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
{initialNote && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={isSaving}
|
||||
className="text-gray-600 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-400 transition-colors"
|
||||
title="Delete note"
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-2 mt-1">
|
||||
{noteText && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{noteText}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
aria-label="Edit note"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user