max coin limit (#140)

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Doh
2025-05-22 22:05:49 -04:00
committed by GitHub
parent a3d2b1ef96
commit 82f45343ae
14 changed files with 121 additions and 56 deletions

View File

@@ -1,6 +1,12 @@
# Changelog # Changelog
## Version 0.2.13 ## Version 0.2.15
### Improved
* max coins set to 9999, to prevent js large number precision issue (#137)
## Version 0.2.14
### Added ### Added

View File

@@ -18,7 +18,7 @@ import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react' import Picker from '@emoji-mart/react'
import { Habit, SafeUser } from '@/lib/types' import { Habit, SafeUser } from '@/lib/types'
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 } from '@/lib/constants' import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP, MAX_COIN_LIMIT } from '@/lib/constants'
import * as chrono from 'chrono-node'; import * as chrono from 'chrono-node';
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import { import {
@@ -291,14 +291,18 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
id="coinReward" id="coinReward"
type="number" type="number"
value={coinReward} value={coinReward}
onChange={(e) => setCoinReward(parseInt(e.target.value === "" ? "0" : e.target.value))} onChange={(e) => {
const value = parseInt(e.target.value === "" ? "0" : e.target.value)
setCoinReward(Math.min(value, MAX_COIN_LIMIT))
}}
min={0} min={0}
max={MAX_COIN_LIMIT}
required required
className="w-20 text-center border-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className="w-20 text-center border-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/> />
<button <button
type="button" type="button"
onClick={() => setCoinReward(prev => prev + 1)} 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" className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
> >
+ +

View File

@@ -15,6 +15,7 @@ import { SmilePlus, Info } from 'lucide-react'
import data from '@emoji-mart/data' import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react' import Picker from '@emoji-mart/react'
import { WishlistItemType } from '@/lib/types' import { WishlistItemType } from '@/lib/types'
import { MAX_COIN_LIMIT } from '@/lib/constants'
interface AddEditWishlistItemModalProps { interface AddEditWishlistItemModalProps {
isOpen: boolean isOpen: boolean
@@ -68,6 +69,8 @@ export default function AddEditWishlistItemModal({
} }
if (coinCost < 1) { if (coinCost < 1) {
newErrors.coinCost = t('errorCoinCostMin') newErrors.coinCost = t('errorCoinCostMin')
} else if (coinCost > MAX_COIN_LIMIT) {
newErrors.coinCost = t('errorCoinCostMax', { max: MAX_COIN_LIMIT })
} }
if (targetCompletions !== undefined && targetCompletions < 1) { if (targetCompletions !== undefined && targetCompletions < 1) {
newErrors.targetCompletions = t('errorTargetCompletionsMin') newErrors.targetCompletions = t('errorTargetCompletionsMin')
@@ -192,14 +195,18 @@ export default function AddEditWishlistItemModal({
id="coinReward" id="coinReward"
type="number" type="number"
value={coinCost} value={coinCost}
onChange={(e) => setCoinCost(parseInt(e.target.value === "" ? "0" : e.target.value))} onChange={(e) => {
const value = parseInt(e.target.value === "" ? "0" : e.target.value)
setCoinCost(Math.min(value, MAX_COIN_LIMIT))
}}
min={0} min={0}
max={MAX_COIN_LIMIT}
required required
className="w-20 text-center border-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className="w-20 text-center border-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/> />
<button <button
type="button" type="button"
onClick={() => setCoinCost(prev => prev + 1)} 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" className="px-3 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors"
> >
+ +

View File

@@ -15,6 +15,7 @@ import Link from 'next/link'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
import { useCoins } from '@/hooks/useCoins' import { useCoins } from '@/hooks/useCoins'
import { MAX_COIN_LIMIT } from '@/lib/constants'
import { TransactionNoteEditor } from './TransactionNoteEditor' import { TransactionNoteEditor } from './TransactionNoteEditor'
import { useHelpers } from '@/lib/client-helpers' import { useHelpers } from '@/lib/client-helpers'
import { TransactionType } from '@/lib/types' import { TransactionType } from '@/lib/types'
@@ -138,7 +139,11 @@ export default function CoinsManager() {
variant="outline" variant="outline"
size="icon" size="icon"
className="h-10 w-10 text-lg" className="h-10 w-10 text-lg"
onClick={() => setAmount(prev => (Number(prev) - 1).toString())} onClick={() => setAmount(prev => {
const current = Number(prev);
const next = current - 1;
return (Math.abs(next) > MAX_COIN_LIMIT ? (next < 0 ? -MAX_COIN_LIMIT : MAX_COIN_LIMIT) : next).toString();
})}
> >
- -
</Button> </Button>
@@ -146,7 +151,22 @@ export default function CoinsManager() {
<Input <Input
type="number" type="number"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={(e) => {
const rawValue = e.target.value;
if (rawValue === '' || rawValue === '-') {
setAmount(rawValue);
return;
}
let numericValue = Number(rawValue); // Changed const to let
if (isNaN(numericValue)) return; // Or handle error
if (Math.abs(numericValue) > MAX_COIN_LIMIT) {
numericValue = numericValue < 0 ? -MAX_COIN_LIMIT : MAX_COIN_LIMIT;
}
setAmount(numericValue.toString());
}}
min={-MAX_COIN_LIMIT}
max={MAX_COIN_LIMIT}
className="text-center text-xl font-medium h-12" 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="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
@@ -157,7 +177,11 @@ export default function CoinsManager() {
variant="outline" variant="outline"
size="icon" size="icon"
className="h-10 w-10 text-lg" className="h-10 w-10 text-lg"
onClick={() => setAmount(prev => (Number(prev) + 1).toString())} onClick={() => setAmount(prev => {
const current = Number(prev);
const next = current + 1;
return (Math.abs(next) > MAX_COIN_LIMIT ? (next < 0 ? -MAX_COIN_LIMIT : MAX_COIN_LIMIT) : next).toString();
})}
> >
+ +
</Button> </Button>

View File

@@ -16,6 +16,7 @@ import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data'
import { CoinsData, User } from '@/lib/types' import { CoinsData, User } from '@/lib/types'
import { toast } from '@/hooks/use-toast' import { toast } from '@/hooks/use-toast'
import { useHelpers } from '@/lib/client-helpers' import { useHelpers } from '@/lib/client-helpers'
import { MAX_COIN_LIMIT } from '@/lib/constants'
function handlePermissionCheck( function handlePermissionCheck(
user: User | undefined, user: User | undefined,
@@ -77,6 +78,13 @@ export function useCoins(options?: { selectedUser?: string }) {
}) })
return null return null
} }
if (amount > MAX_COIN_LIMIT) {
toast({
title: t("invalidAmountTitle"),
description: t("maxAmountExceededDescription", { max: MAX_COIN_LIMIT })
})
return null
}
const data = await addCoins({ const data = await addCoins({
amount, amount,
@@ -100,6 +108,13 @@ export function useCoins(options?: { selectedUser?: string }) {
}) })
return null return null
} }
if (numAmount > MAX_COIN_LIMIT) {
toast({
title: t("invalidAmountTitle"),
description: t("maxAmountExceededDescription", { max: MAX_COIN_LIMIT })
})
return null
}
const data = await removeCoins({ const data = await removeCoins({
amount: numAmount, amount: numAmount,

View File

@@ -29,4 +29,6 @@ export const QUICK_DATES = [
{ label: 'Friday', value: 'this friday' }, { label: 'Friday', value: 'this friday' },
{ label: 'Saturday', value: 'this saturday' }, { label: 'Saturday', value: 'this saturday' },
{ label: 'Sunday', value: 'this sunday' }, { label: 'Sunday', value: 'this sunday' },
] as const ] as const
export const MAX_COIN_LIMIT = 9999

View File

@@ -406,16 +406,17 @@
"notEnoughCoinsTitle": "Nicht genug Münzen", "notEnoughCoinsTitle": "Nicht genug Münzen",
"notEnoughCoinsDescription": "Sie benötigen {coinsNeeded} Münzen mehr, um diese Belohnung einzulösen." "notEnoughCoinsDescription": "Sie benötigen {coinsNeeded} Münzen mehr, um diese Belohnung einzulösen."
}, },
"Warning": {
"areYouSure": "Sind Sie sicher?",
"cancel": "Abbrechen"
},
"useCoins": { "useCoins": {
"invalidAmountTitle": "Ungültiger Betrag", "invalidAmountTitle": "Ungültiger Betrag",
"invalidAmountDescription": "Bitte geben Sie eine gültige positive Zahl ein", "invalidAmountDescription": "Bitte geben Sie eine gültige positive Zahl ein",
"successTitle": "Erfolg", "successTitle": "Erfolg",
"addedCoinsDescription": "{amount} Münzen hinzugefügt", "transactionNotFoundDescription": "Transaktion nicht gefunden",
"removedCoinsDescription": "{amount} Münzen entfernt", "maxAmountExceededDescription": "Der Betrag darf {max} nicht überschreiten.",
"transactionNotFoundDescription": "Transaktion nicht gefunden" "transactionNotFoundDescription": "Transaktion nicht gefunden",
}, "maxAmountExceededDescription": "Der Betrag darf {max} nicht überschreiten."
"Warning": {
"areYouSure": "Sind Sie sicher?",
"cancel": "Abbrechen"
} }
} }

View File

@@ -410,9 +410,10 @@
"invalidAmountTitle": "Invalid amount", "invalidAmountTitle": "Invalid amount",
"invalidAmountDescription": "Please enter a valid positive number", "invalidAmountDescription": "Please enter a valid positive number",
"successTitle": "Success", "successTitle": "Success",
"addedCoinsDescription": "Added {amount} coins", "transactionNotFoundDescription": "Transaction not found",
"removedCoinsDescription": "Removed {amount} coins", "maxAmountExceededDescription": "The amount cannot exceed {max}.",
"transactionNotFoundDescription": "Transaction not found" "transactionNotFoundDescription": "Transaction not found",
"maxAmountExceededDescription": "The amount cannot exceed {max}."
}, },
"Warning": { "Warning": {
"areYouSure": "Are you sure?", "areYouSure": "Are you sure?",

View File

@@ -406,16 +406,17 @@
"notEnoughCoinsTitle": "No hay suficientes monedas", "notEnoughCoinsTitle": "No hay suficientes monedas",
"notEnoughCoinsDescription": "Necesitas {coinsNeeded} monedas más para canjear esta recompensa." "notEnoughCoinsDescription": "Necesitas {coinsNeeded} monedas más para canjear esta recompensa."
}, },
"Warning": {
"areYouSure": "¿Estás seguro?",
"cancel": "Cancelar"
},
"useCoins": { "useCoins": {
"invalidAmountTitle": "Cantidad inválida", "invalidAmountTitle": "Cantidad inválida",
"invalidAmountDescription": "Por favor ingresa un número positivo válido", "invalidAmountDescription": "Por favor ingresa un número positivo válido",
"successTitle": "Éxito", "successTitle": "Éxito",
"addedCoinsDescription": "Añadidas {amount} monedas", "transactionNotFoundDescription": "Transacción no encontrada",
"removedCoinsDescription": "Quitadas {amount} monedas", "maxAmountExceededDescription": "La cantidad no puede exceder {max}.",
"transactionNotFoundDescription": "Transacción no encontrada" "transactionNotFoundDescription": "Transacción no encontrada",
}, "maxAmountExceededDescription": "La cantidad no puede exceder {max}."
"Warning": {
"areYouSure": "¿Estás seguro?",
"cancel": "Cancelar"
} }
} }

View File

@@ -406,16 +406,17 @@
"notEnoughCoinsTitle": "Pas assez de pièces", "notEnoughCoinsTitle": "Pas assez de pièces",
"notEnoughCoinsDescription": "Il vous manque {coinsNeeded} pièces pour échanger cette récompense." "notEnoughCoinsDescription": "Il vous manque {coinsNeeded} pièces pour échanger cette récompense."
}, },
"Warning": {
"areYouSure": "Êtes-vous sûr ?",
"cancel": "Annuler"
},
"useCoins": { "useCoins": {
"invalidAmountTitle": "Montant invalide", "invalidAmountTitle": "Montant invalide",
"invalidAmountDescription": "Veuillez entrer un nombre positif valide", "invalidAmountDescription": "Veuillez entrer un nombre positif valide",
"successTitle": "Succès", "successTitle": "Succès",
"addedCoinsDescription": "Ajouté {amount} pièces", "transactionNotFoundDescription": "Transaction non trouvée",
"removedCoinsDescription": "Retiré {amount} pièces", "maxAmountExceededDescription": "Le montant ne peut pas dépasser {max}.",
"transactionNotFoundDescription": "Transaction non trouvée" "transactionNotFoundDescription": "Transaction non trouvée",
}, "maxAmountExceededDescription": "Le montant ne peut pas dépasser {max}."
"Warning": {
"areYouSure": "Êtes-vous sûr ?",
"cancel": "Annuler"
} }
} }

View File

@@ -406,16 +406,17 @@
"notEnoughCoinsTitle": "コインが不足しています", "notEnoughCoinsTitle": "コインが不足しています",
"notEnoughCoinsDescription": "この報酬を使用するにはあと{coinsNeeded}コイン必要です。" "notEnoughCoinsDescription": "この報酬を使用するにはあと{coinsNeeded}コイン必要です。"
}, },
"Warning": {
"areYouSure": "本当によろしいですか?",
"cancel": "キャンセル"
},
"useCoins": { "useCoins": {
"invalidAmountTitle": "無効な値です", "invalidAmountTitle": "無効な値です",
"invalidAmountDescription": "有効な正の数を入力してください", "invalidAmountDescription": "有効な正の数を入力してください",
"successTitle": "成功しました", "successTitle": "成功しました",
"addedCoinsDescription": "{amount}コインを追加しました", "transactionNotFoundDescription": "取引が見つかりません",
"removedCoinsDescription": "{amount}コインを削除しました", "maxAmountExceededDescription": "金額は{max}を超えることはできません。",
"transactionNotFoundDescription": "取引が見つかりません" "transactionNotFoundDescription": "取引が見つかりません",
}, "maxAmountExceededDescription": "金額は{max}を超えることはできません。"
"Warning": {
"areYouSure": "本当によろしいですか?",
"cancel": "キャンセル"
} }
} }

View File

@@ -406,16 +406,17 @@
"notEnoughCoinsTitle": "Недостаточно монет", "notEnoughCoinsTitle": "Недостаточно монет",
"notEnoughCoinsDescription": "Вам нужно еще {coinsNeeded} монет, чтобы получить эту награду." "notEnoughCoinsDescription": "Вам нужно еще {coinsNeeded} монет, чтобы получить эту награду."
}, },
"Warning": {
"areYouSure": "Вы уверены?",
"cancel": "Отмена"
},
"useCoins": { "useCoins": {
"invalidAmountTitle": "Неверная сумма", "invalidAmountTitle": "Неверная сумма",
"invalidAmountDescription": "Пожалуйста, введите положительное число", "invalidAmountDescription": "Пожалуйста, введите положительное число",
"successTitle": "Успех", "successTitle": "Успех",
"addedCoinsDescription": "Добавлено {amount} монет", "transactionNotFoundDescription": "Транзакция не найдена",
"removedCoinsDescription": "Удалено {amount} монет", "maxAmountExceededDescription": "Сумма не может превышать {max}.",
"transactionNotFoundDescription": "Транзакция не найдена" "transactionNotFoundDescription": "Транзакция не найдена",
}, "maxAmountExceededDescription": "Сумма не может превышать {max}."
"Warning": {
"areYouSure": "Вы уверены?",
"cancel": "Отмена"
} }
} }

View File

@@ -406,16 +406,17 @@
"notEnoughCoinsTitle": "金币不足", "notEnoughCoinsTitle": "金币不足",
"notEnoughCoinsDescription": "您还需要{coinsNeeded}金币才能兑换此奖励。" "notEnoughCoinsDescription": "您还需要{coinsNeeded}金币才能兑换此奖励。"
}, },
"Warning": {
"areYouSure": "您确定吗?",
"cancel": "取消"
},
"useCoins": { "useCoins": {
"invalidAmountTitle": "无效金额", "invalidAmountTitle": "无效金额",
"invalidAmountDescription": "请输入有效的正数", "invalidAmountDescription": "请输入有效的正数",
"successTitle": "成功", "successTitle": "成功",
"addedCoinsDescription": "添加了{amount}金币", "transactionNotFoundDescription": "未找到交易记录",
"removedCoinsDescription": "移除了{amount}金币", "maxAmountExceededDescription": "金额不能超过 {max}。",
"transactionNotFoundDescription": "未找到交易记录" "transactionNotFoundDescription": "未找到交易记录",
}, "maxAmountExceededDescription": "金额不能超过 {max}。"
"Warning": {
"areYouSure": "您确定吗?",
"cancel": "取消"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "habittrove", "name": "habittrove",
"version": "0.2.14", "version": "0.2.15",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",