mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
show today earned coins
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Version 0.1.14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- show today earned coins in balance and header
|
||||||
|
|
||||||
## Version 0.1.13
|
## Version 0.1.13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { Coins } from 'lucide-react'
|
|||||||
import { formatNumber } from '@/lib/utils/formatNumber'
|
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { settingsAtom } from '@/lib/atoms'
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
|
|
||||||
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
|
const { coinsEarnedToday } = useCoins()
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -14,9 +16,21 @@ export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<Coins className="h-12 w-12 text-yellow-400 mr-4" />
|
<Coins className="h-12 w-12 text-yellow-400 mr-4" />
|
||||||
|
<div className="flex flex-col">
|
||||||
<span className="text-4xl font-bold">
|
<span className="text-4xl font-bold">
|
||||||
{formatNumber({ amount: coinBalance, settings })}
|
{formatNumber({ amount: coinBalance, settings })}
|
||||||
</span>
|
</span>
|
||||||
|
{coinsEarnedToday > 0 && (
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<span className="text-md text-green-600 dark:text-green-400 font-medium">
|
||||||
|
+{formatNumber({ amount: coinsEarnedToday, settings })}
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-muted-foreground">
|
||||||
|
today
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ import { useAtom } from 'jotai'
|
|||||||
import { useCoins } from '@/hooks/useCoins'
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
|
|
||||||
export default function CoinsManager() {
|
export default function CoinsManager() {
|
||||||
const { add, remove, balance, transactions } = useCoins()
|
const {
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
balance,
|
||||||
|
transactions,
|
||||||
|
coinsEarnedToday,
|
||||||
|
totalEarned,
|
||||||
|
totalSpent,
|
||||||
|
coinsSpentToday,
|
||||||
|
transactionsToday
|
||||||
|
} = useCoins()
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const DEFAULT_AMOUNT = '0'
|
const DEFAULT_AMOUNT = '0'
|
||||||
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
||||||
@@ -40,7 +50,7 @@ export default function CoinsManager() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<span className="text-2xl animate-bounce hover:animate-none cursor-default">🪙</span>
|
<span className="text-2xl animate-bounce hover:animate-none cursor-default">💰</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-normal text-muted-foreground">Current Balance</div>
|
<div className="text-sm font-normal text-muted-foreground">Current Balance</div>
|
||||||
<div className="text-3xl font-bold">{formatNumber({ amount: balance, settings })} coins</div>
|
<div className="text-3xl font-bold">{formatNumber({ amount: balance, settings })} coins</div>
|
||||||
@@ -98,45 +108,47 @@ export default function CoinsManager() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* Top Row - Totals */}
|
||||||
<div className="p-4 rounded-lg bg-green-100 dark:bg-green-900">
|
<div className="p-4 rounded-lg bg-green-100 dark:bg-green-900">
|
||||||
<div className="text-sm text-green-800 dark:text-green-100 mb-1">Total Earned</div>
|
<div className="text-sm text-green-800 dark:text-green-100 mb-1">Total Earned</div>
|
||||||
<div className="text-2xl font-bold text-green-900 dark:text-green-50">
|
<div className="text-2xl font-bold text-green-900 dark:text-green-50">
|
||||||
{formatNumber({
|
{formatNumber({ amount: totalEarned, settings })} 🪙
|
||||||
amount: transactions
|
|
||||||
.filter(t => {
|
|
||||||
if (t.type === 'HABIT_COMPLETION' && t.relatedItemId) {
|
|
||||||
return !transactions.some(undoT =>
|
|
||||||
undoT.type === 'HABIT_UNDO' &&
|
|
||||||
undoT.relatedItemId === t.relatedItemId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return t.amount > 0 && t.type !== 'HABIT_UNDO'
|
|
||||||
})
|
|
||||||
.reduce((sum, t) => sum + t.amount, 0)
|
|
||||||
, settings
|
|
||||||
})} 🪙
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 rounded-lg bg-red-100 dark:bg-red-900">
|
<div className="p-4 rounded-lg bg-red-100 dark:bg-red-900">
|
||||||
<div className="text-sm text-red-800 dark:text-red-100 mb-1">Total Spent</div>
|
<div className="text-sm text-red-800 dark:text-red-100 mb-1">Total Spent</div>
|
||||||
<div className="text-2xl font-bold text-red-900 dark:text-red-50">
|
<div className="text-2xl font-bold text-red-900 dark:text-red-50">
|
||||||
{formatNumber({
|
{formatNumber({ amount: totalSpent, settings })} 💸
|
||||||
amount: Math.abs(
|
|
||||||
transactions
|
|
||||||
.filter(t => t.type === 'WISH_REDEMPTION' || t.type === 'MANUAL_ADJUSTMENT')
|
|
||||||
.reduce((sum, t) => sum + (t.amount < 0 ? t.amount : 0), 0)
|
|
||||||
), settings
|
|
||||||
})} 🪙
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-pink-100 dark:bg-pink-900">
|
||||||
|
<div className="text-sm text-pink-800 dark:text-pink-100 mb-1">Total Transactions</div>
|
||||||
|
<div className="text-2xl font-bold text-pink-900 dark:text-pink-50">
|
||||||
|
{transactions.length} 📈
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Row - Today */}
|
||||||
<div className="p-4 rounded-lg bg-blue-100 dark:bg-blue-900">
|
<div className="p-4 rounded-lg bg-blue-100 dark:bg-blue-900">
|
||||||
<div className="text-sm text-blue-800 dark:text-blue-100 mb-1">Today's Transactions</div>
|
<div className="text-sm text-blue-800 dark:text-blue-100 mb-1">Today's Earned</div>
|
||||||
<div className="text-2xl font-bold text-blue-900 dark:text-blue-50">
|
<div className="text-2xl font-bold text-blue-900 dark:text-blue-50">
|
||||||
{transactions.filter(t =>
|
{formatNumber({ amount: coinsEarnedToday, settings })} 🪙
|
||||||
isSameDate(getNow({ timezone: settings.system.timezone }), t2d({ timestamp: t.timestamp, timezone: settings.system.timezone }))
|
</div>
|
||||||
).length} 📊
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-purple-100 dark:bg-purple-900">
|
||||||
|
<div className="text-sm text-purple-800 dark:text-purple-100 mb-1">Today's Spent</div>
|
||||||
|
<div className="text-2xl font-bold text-purple-900 dark:text-purple-50">
|
||||||
|
{formatNumber({ amount: coinsSpentToday, settings })} 💸
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg bg-orange-100 dark:bg-orange-900">
|
||||||
|
<div className="text-sm text-orange-800 dark:text-orange-100 mb-1">Today's Transactions</div>
|
||||||
|
<div className="text-2xl font-bold text-orange-900 dark:text-orange-50">
|
||||||
|
{transactionsToday} 📊
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { settingsAtom, coinsAtom } from '@/lib/atoms'
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
import { Bell, Menu, Settings, User, Info, Coins } from 'lucide-react'
|
import { Bell, Menu, Settings, User, Info, Coins } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Logo } from '@/components/Logo'
|
import { Logo } from '@/components/Logo'
|
||||||
@@ -23,7 +24,7 @@ interface HeaderProps {
|
|||||||
export default function Header({ className }: HeaderProps) {
|
export default function Header({ className }: HeaderProps) {
|
||||||
const [showAbout, setShowAbout] = useState(false)
|
const [showAbout, setShowAbout] = useState(false)
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [coins] = useAtom(coinsAtom)
|
const { balance, coinsEarnedToday } = useCoins()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={`border-b bg-white dark:bg-gray-800 shadow-sm ${className || ''}`}>
|
<header className={`border-b bg-white dark:bg-gray-800 shadow-sm ${className || ''}`}>
|
||||||
@@ -33,11 +34,18 @@ export default function Header({ className }: HeaderProps) {
|
|||||||
<Logo />
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/coins" className="flex items-center gap-1 px-2 py-1 bg-amber-100 hover:bg-amber-200 dark:bg-amber-900 dark:hover:bg-amber-800 rounded-full transition-colors">
|
<Link href="/coins" className="flex items-center 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="text-amber-500 dark:text-amber-300" />
|
<Coins className="h-5 w-5 text-yellow-500 dark:text-yellow-400" />
|
||||||
<span className="text-amber-600 dark:text-amber-400 font-medium">
|
<div className="flex items-baseline gap-2">
|
||||||
{coins.balance}
|
<span className="text-gray-800 dark:text-gray-100 font-medium text-lg">
|
||||||
|
{balance}
|
||||||
</span>
|
</span>
|
||||||
|
{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">
|
||||||
|
+{coinsEarnedToday}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Button variant="ghost" size="icon" aria-label="Notifications">
|
<Button variant="ghost" size="icon" aria-label="Notifications">
|
||||||
<Bell className="h-5 w-5" />
|
<Bell className="h-5 w-5" />
|
||||||
|
|||||||
@@ -1,10 +1,73 @@
|
|||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { coinsAtom } from '@/lib/atoms'
|
import { coinsAtom, settingsAtom } from '@/lib/atoms'
|
||||||
import { addCoins, removeCoins } from '@/app/actions/data'
|
import { addCoins, removeCoins } from '@/app/actions/data'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
|
import { getTodayInTimezone, isSameDate, t2d } from '@/lib/utils'
|
||||||
|
|
||||||
export function useCoins() {
|
export function useCoins() {
|
||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
|
const [settings] = useAtom(settingsAtom)
|
||||||
|
|
||||||
|
const getTotalEarned = () => {
|
||||||
|
return coins.transactions
|
||||||
|
.filter(t => {
|
||||||
|
if (t.type === 'HABIT_COMPLETION' && t.relatedItemId) {
|
||||||
|
return !coins.transactions.some(undoT =>
|
||||||
|
undoT.type === 'HABIT_UNDO' &&
|
||||||
|
undoT.relatedItemId === t.relatedItemId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return t.amount > 0 && t.type !== 'HABIT_UNDO'
|
||||||
|
})
|
||||||
|
.reduce((sum, t) => sum + t.amount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTotalSpent = () => {
|
||||||
|
return Math.abs(
|
||||||
|
coins.transactions
|
||||||
|
.filter(t => t.type === 'WISH_REDEMPTION' || t.type === 'MANUAL_ADJUSTMENT')
|
||||||
|
.reduce((sum, t) => sum + (t.amount < 0 ? t.amount : 0), 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCoinsEarnedToday = () => {
|
||||||
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
|
return coins.transactions
|
||||||
|
.filter(transaction =>
|
||||||
|
isSameDate(t2d({ timestamp: transaction.timestamp, timezone: settings.system.timezone }),
|
||||||
|
t2d({ timestamp: today, timezone: settings.system.timezone }))
|
||||||
|
)
|
||||||
|
.reduce((sum, transaction) => {
|
||||||
|
if (transaction.type !== 'HABIT_UNDO' && transaction.amount > 0) {
|
||||||
|
return sum + transaction.amount
|
||||||
|
}
|
||||||
|
if (transaction.type === 'HABIT_UNDO') {
|
||||||
|
return sum - Math.abs(transaction.amount)
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCoinsSpentToday = () => {
|
||||||
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
|
return Math.abs(
|
||||||
|
coins.transactions
|
||||||
|
.filter(t =>
|
||||||
|
isSameDate(t2d({ timestamp: t.timestamp, timezone: settings.system.timezone }),
|
||||||
|
t2d({ timestamp: today, timezone: settings.system.timezone })) &&
|
||||||
|
t.amount < 0
|
||||||
|
)
|
||||||
|
.reduce((sum, t) => sum + t.amount, 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTransactionsToday = () => {
|
||||||
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
|
return coins.transactions.filter(t =>
|
||||||
|
isSameDate(t2d({ timestamp: t.timestamp, timezone: settings.system.timezone }),
|
||||||
|
t2d({ timestamp: today, timezone: settings.system.timezone }))
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
|
||||||
const add = async (amount: number, description: string) => {
|
const add = async (amount: number, description: string) => {
|
||||||
if (isNaN(amount) || amount <= 0) {
|
if (isNaN(amount) || amount <= 0) {
|
||||||
@@ -41,6 +104,11 @@ export function useCoins() {
|
|||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
balance: coins.balance,
|
balance: coins.balance,
|
||||||
transactions: coins.transactions
|
transactions: coins.transactions,
|
||||||
|
coinsEarnedToday: getCoinsEarnedToday(),
|
||||||
|
totalEarned: getTotalEarned(),
|
||||||
|
totalSpent: getTotalSpent(),
|
||||||
|
coinsSpentToday: getCoinsSpentToday(),
|
||||||
|
transactionsToday: getTransactionsToday()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "habittrove",
|
"name": "habittrove",
|
||||||
"version": "0.1.13",
|
"version": "0.1.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
|||||||
Reference in New Issue
Block a user