Skip to content

Commit

Permalink
fix coin stats and add transaction note (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
dohsimpson authored Jan 9, 2025
1 parent 8bbd684 commit aacf75e
Show file tree
Hide file tree
Showing 11 changed files with 437 additions and 131 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Version 0.1.17

### Added

- transactions note

### Fixed

- coin statistics

## Version 0.1.16

### Fixed
Expand Down
12 changes: 8 additions & 4 deletions app/actions/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export async function addCoins(
amount: number,
description: string,
type: TransactionType = 'MANUAL_ADJUSTMENT',
relatedItemId?: string
relatedItemId?: string,
note?: string
): Promise<CoinsData> {
const data = await loadCoinsData()
const newTransaction: CoinTransaction = {
Expand All @@ -113,7 +114,8 @@ export async function addCoins(
type,
description,
timestamp: d2t({ dateTime: getNow({}) }),
...(relatedItemId && { relatedItemId })
...(relatedItemId && { relatedItemId }),
...(note && note.trim() !== '' && { note })
}

const newData: CoinsData = {
Expand Down Expand Up @@ -144,7 +146,8 @@ export async function removeCoins(
amount: number,
description: string,
type: TransactionType = 'MANUAL_ADJUSTMENT',
relatedItemId?: string
relatedItemId?: string,
note?: string
): Promise<CoinsData> {
const data = await loadCoinsData()
const newTransaction: CoinTransaction = {
Expand All @@ -153,7 +156,8 @@ export async function removeCoins(
type,
description,
timestamp: d2t({ dateTime: getNow({}) }),
...(relatedItemId && { relatedItemId })
...(relatedItemId && { relatedItemId }),
...(note && note.trim() !== '' && { note })
}

const newData: CoinsData = {
Expand Down
102 changes: 64 additions & 38 deletions components/CoinsManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ 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'
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,
Expand All @@ -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('')
}
}

Expand All @@ -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>
<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 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>
</div>
</div>
</CardContent>
</Card>
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
)}
Expand Down
138 changes: 138 additions & 0 deletions components/TransactionNoteEditor.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
Loading

0 comments on commit aacf75e

Please sign in to comment.