Former-commit-id: d9ad6a54db75ba160fd2fceb4fdcc107d785603c [formerly 10734618a783fab90e38202d95eb42bde6f570f6] [formerly db810a6126969e2598de30b207967ce42866d21b [formerly cde19d38d100d14e77744b8b37093eb1d45ee05a]]
Former-commit-id: 0cd9223971314d2159e4c05ac7129a86b3088def [formerly e88a40fea5b7ccfbbbea5fafef25f53b4b2242fa]
Former-commit-id: 2a40542295fd872a91c0a00431732b014c2aeb04
This commit is contained in:
Melvin Chia
2025-07-04 14:11:30 +08:00
parent 775224af6b
commit 798a9ddd8d
12 changed files with 270 additions and 45 deletions

View File

@@ -5,10 +5,12 @@
- **Books Library**: Renamed `category` to `collection` for better clarity.
- **Books Library**: Fixed a bug where the sidebar content is not updated when stuff got deleted.
- **Books Library**: Download process is now handled by the task pool mechanism.
- **Books Library**: User can now download books from different mirrors of Libgen.
- **Guitar Tabs**: Download process is now handled by the task pool mechanism.
- **API**: Implemented task pool mechanism using socketIO to handle long-running tasks like downloading books from Libgen.
- **UI**: Added inline style for the `preloader` component so that styling will be applied when tailwind is not loaded yet.
- **UI**: Completely revamped the `ModalStore`, now you can pass in the modal component directly into the `open` function instead of registering it first. This allow facilitate the type safety of the modal component.
- **UI**: Fixed a bug in combobox input where the input will not be focused when the user click the area outside of the input itself in the container.
## 📌 **dev 25w26 (6/23/2025 - 6/30/2025)**
- **Movies**: Added tab selector to separate watched and unwatched movies.

View File

@@ -8,7 +8,7 @@
"@headlessui/react": "^2.2.2",
"@iconify/react": "^5.2.1",
"@iconify/types": "^2.0.0",
"@lifeforge/ui": "^0.25.27-4",
"@lifeforge/ui": "^0.25.27-8",
"@million/lint": "^1.0.14",
"@tabler/icons-react": "^3.31.0",
"@tailwindcss/postcss": "^4.1.5",
@@ -17,7 +17,7 @@
"@tanstack/react-query-devtools": "^5.75.1",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/react-color": "^2.5.3",
"@vis.gl/react-google-maps": "^1.5.2",
"@vis.gl/react-google-maps": "^1.5.3",
"@wavesurfer/react": "^1.0.11",
"babel-plugin-react-compiler": "^19.0.0-beta-ebf51a3-20250411",
"chart.js": "^4.4.9",
@@ -300,7 +300,7 @@
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@lifeforge/ui": ["@lifeforge/ui@0.25.27-4", "", { "dependencies": { "@headlessui/react": "^2.2.2", "@iconify/collections": "^1.0.544", "@iconify/react": "^5.2.1", "@million/lint": "^1.0.14", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.75.4", "@uidotdev/usehooks": "^2.4.1", "@uiw/react-color": "^2.5.3", "@vitejs/plugin-react": "^4.4.1", "@yudiel/react-qr-scanner": "^2.3.0", "clsx": "^2.1.1", "color-sorter": "^6.2.0", "crypto-js": "^4.2.0", "daisyui": "^5.0.37", "dayjs": "^1.11.13", "file-type-mime": "^0.4.6", "i18next-http-backend": "^3.0.2", "javascript-color-gradient": "^2.5.0", "lodash": "^4.17.21", "pocketbase": "^0.25.2", "radix-ui": "^1.3.4", "react-currency-input-field": "^3.10.0", "react-custom-scrollbars": "^4.2.1", "react-date-picker": "^11.0.0", "react-datepicker": "^8.4.0", "react-datetime-picker": "^6.0.1", "react-dropzone": "^14.3.8", "react-i18next": "^15.5.1", "react-medium-image-zoom": "^5.2.14", "react-otp-input": "^3.1.1", "react-photo-album": "^2.4.1", "react-router": "^7.5.3", "react-toastify": "^10.0.6", "react-tooltip": "^5.28.1", "react-virtualized": "^9.22.6", "tailwindcss": "^4.1.5", "tinycolor2": "^1.6.0", "zustand": "^5.0.4" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-zGjwOvLPBcviQ/cDruPaJb+F1Mhj6m9NkPNCVt4R/lxmCLrhz46EZD6kE9Er6SToTQcuyk2AooBhtSk1SKgEKw=="],
"@lifeforge/ui": ["@lifeforge/ui@0.25.27-8", "", { "dependencies": { "@headlessui/react": "^2.2.2", "@iconify/collections": "^1.0.544", "@iconify/react": "^5.2.1", "@million/lint": "^1.0.14", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.75.4", "@uidotdev/usehooks": "^2.4.1", "@uiw/react-color": "^2.5.3", "@vitejs/plugin-react": "^4.4.1", "@yudiel/react-qr-scanner": "^2.3.0", "clsx": "^2.1.1", "color-sorter": "^6.2.0", "crypto-js": "^4.2.0", "daisyui": "^5.0.37", "dayjs": "^1.11.13", "file-type-mime": "^0.4.6", "i18next-http-backend": "^3.0.2", "javascript-color-gradient": "^2.5.0", "lodash": "^4.17.21", "pocketbase": "^0.25.2", "radix-ui": "^1.3.4", "react-currency-input-field": "^3.10.0", "react-custom-scrollbars": "^4.2.1", "react-date-picker": "^11.0.0", "react-datepicker": "^8.4.0", "react-datetime-picker": "^6.0.1", "react-dropzone": "^14.3.8", "react-i18next": "^15.5.1", "react-medium-image-zoom": "^5.2.14", "react-otp-input": "^3.1.1", "react-photo-album": "^2.4.1", "react-router": "^7.5.3", "react-toastify": "^10.0.6", "react-tooltip": "^5.28.1", "react-virtualized": "^9.22.6", "tailwindcss": "^4.1.5", "tinycolor2": "^1.6.0", "zustand": "^5.0.4" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-ZG9Djqp9aN6aGqrXUp04lKb5KOMI6BpIijdLQpHK+aN800chF+HGlxdX581HT2WpzhrFOUFAG3oOZ44ImeTZfA=="],
"@million/install": ["@million/install@1.0.14", "", { "dependencies": { "@antfu/ni": "^0.21.12", "@axiomhq/js": "1.0.0-rc.3", "@babel/parser": "^7.25.3", "@babel/types": "7.26.0", "@clack/prompts": "^0.7.0", "ast-types": "^0.14.2", "cli-high": "^0.4.2", "diff": "^5.1.0", "effect": "^3.8.4", "nanoid": "^5.0.7", "recast": "^0.23.9", "xycolors": "^0.1.2" }, "bin": { "install": "bin/index.js" } }, "sha512-xZvj4AEHc5hyn8RCiLl9dYNqggj2fa0lgNvUkCiJyhRJPNE2hZrUa/Ka0Weu82VpBaO//zujG0YErk7osjNXPA=="],

3
env/.env.example vendored
View File

@@ -1 +1,2 @@
VITE_API_HOST=
VITE_API_HOST=
VITE_GOOGLE_MAPS_API_KEY=

View File

@@ -17,7 +17,7 @@
"@headlessui/react": "^2.2.2",
"@iconify/react": "^5.2.1",
"@iconify/types": "^2.0.0",
"@lifeforge/ui": "^0.25.27-4",
"@lifeforge/ui": "^0.25.27-8",
"@million/lint": "^1.0.14",
"@tabler/icons-react": "^3.31.0",
"@tailwindcss/postcss": "^4.1.5",
@@ -26,7 +26,7 @@
"@tanstack/react-query-devtools": "^5.75.1",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/react-color": "^2.5.3",
"@vis.gl/react-google-maps": "^1.5.2",
"@vis.gl/react-google-maps": "^1.5.3",
"@wavesurfer/react": "^1.0.11",
"babel-plugin-react-compiler": "^19.0.0-beta-ebf51a3-20250411",
"chart.js": "^4.4.9",

View File

@@ -45,7 +45,7 @@ function ModifyEventModal({
use_google_map: false,
category: '',
calendar: '',
location: '',
location: null,
reference_link: '',
description: '',
recurring_rrule: '',

View File

@@ -1,5 +1,7 @@
import type { RecordModel } from 'pocketbase'
import { ILocationEntry } from '@lifeforge/ui'
interface ICalendarEvent extends RecordModel {
type: 'single' | 'recurring'
title: string
@@ -24,7 +26,7 @@ type ICalendarEventFormState = {
use_google_map: boolean
category: string
calendar: string
location: string
location: string | ILocationEntry | null
reference_link: string
description: string
type: 'single' | 'recurring'

View File

@@ -44,7 +44,11 @@ interface IWalletTransaction extends RecordModel {
side: 'debit' | 'credit'
particulars: string
amount: number
location: string
location_name: string
location_coords: {
lat: number
lon: number
}
date: string
category: string
asset: string

View File

@@ -92,6 +92,7 @@ function AssetItem({ asset }: { asset: IWalletAsset }) {
>
<MenuItem
icon="tabler:chart-line"
namespace="apps.wallet"
text="View Balance Chart"
onClick={handleOpenBalanceChart}
/>

View File

@@ -137,7 +137,9 @@ function BalanceChartModal({
return (
<div className="min-w-[50vw]">
<ModalHeader
appendTitle={<> - {existedData.name}</>}
appendTitle={
<p className="hidden truncate sm:block"> - {existedData.name}</p>
}
icon="tabler:chart-line"
namespace="apps.wallet"
title="assetsBalanceChart"

View File

@@ -9,6 +9,7 @@ import {
Button,
CurrencyInput,
DateInput,
ILocationEntry,
ImageAndFileInput,
LocationInput,
ModalHeader,
@@ -43,7 +44,7 @@ function ModifyTransactionsModal({
const [transactionDate, setTransactionDate] = useState<Date | null>(null)
const [amount, setAmount] = useState<string>()
const [location, setLocation] = useState<string | null>(null)
const [location, setLocation] = useState<ILocationEntry | null>(null)
const [category, setCategory] = useState<string | null>(null)
const [transactionAsset, setTransactionAsset] = useState<string | null>(null)
const [ledger, setLedger] = useState<string | null>(null)
@@ -80,7 +81,26 @@ function ModifyTransactionsModal({
)
setAmount(`${existedData.amount}`)
setCategory(existedData.category || '')
setLocation(existedData.location || '')
setLocation(
existedData.location_name
? {
displayName: {
text: existedData.location_name,
languageCode: ''
},
location: existedData.location_coords
? {
latitude: existedData.location_coords.lat,
longitude: existedData.location_coords.lon
}
: {
latitude: 0,
longitude: 0
},
formattedAddress: ''
}
: null
)
setTransactionAsset(existedData.asset || '')
setLedger(existedData.ledger || '')
setReceipt(
@@ -104,7 +124,7 @@ function ModifyTransactionsModal({
setTransactionType('income')
setTransactionDate(dayjs().toDate())
setAmount(undefined)
setLocation('')
setLocation(null)
setCategory(null)
setTransactionAsset(null)
setLedger(null)
@@ -140,8 +160,8 @@ function ModifyTransactionsModal({
data.append('date', dayjs(transactionDate).format('YYYY-MM-DD'))
data.append('amount', parseFloat(`${amount}` || '0').toString())
data.append('category', category ?? '')
data.append('location', location ?? '')
data.append('location_name', location?.displayName.text ?? '')
data.append('location_coords', JSON.stringify(location?.location))
data.append('asset', transactionAsset ?? '')
data.append('ledger', ledger ?? '')
data.append('type', transactionType)

View File

@@ -1,7 +1,18 @@
import { ModalHeader } from '@lifeforge/ui'
import { Icon } from '@iconify/react/dist/iconify.js'
import { APIProvider, AdvancedMarker, Map } from '@vis.gl/react-google-maps'
import clsx from 'clsx'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { Button, ModalHeader, useModalStore } from '@lifeforge/ui'
import { useWalletData } from '@apps/Wallet/hooks/useWalletData'
import { IWalletTransaction } from '@apps/Wallet/interfaces/wallet_interfaces'
import useComponentBg from '@hooks/useComponentBg'
import ViewReceiptModal from '../../views/ListView/components/ViewReceiptModal'
function ViewTransactionModal({
data: { transaction },
onClose
@@ -11,19 +22,221 @@ function ViewTransactionModal({
}
onClose: () => void
}) {
//TODO
const { t } = useTranslation('apps.wallet')
const open = useModalStore(state => state.open)
const { assetsQuery, categoriesQuery, ledgersQuery } = useWalletData()
const { componentBgLighter } = useComponentBg()
const asset = assetsQuery.data?.find(asset => asset.id === transaction.asset)
const category = categoriesQuery.data?.find(
category => category.id === transaction.category
)
const ledger = ledgersQuery.data?.find(
ledger => ledger.id === transaction.ledger
)
return (
<div className="min-w-[30vw] space-y-4">
<ModalHeader
icon="tabler:eye"
title="View Transaction"
namespace="apps.wallet"
title="transactions.view"
onClose={onClose}
/>
<pre>
<code className="break-words whitespace-pre-wrap">
{JSON.stringify(transaction, null, 2)}
</code>
</pre>
<div className="flex-center flex flex-col">
{category && (
<div
className="shadow-custom mb-6 w-min rounded-lg p-4"
style={{
backgroundColor: category.color + (category.color ? '50' : ''),
color: category.color
}}
>
<Icon className="size-8" icon={category.icon ?? ''} />
</div>
)}
{transaction.type === 'transfer' && (
<div className="mb-6 w-min rounded-lg bg-blue-500/20 p-4">
<Icon
className="size-8 text-blue-500"
icon="tabler:arrows-exchange"
/>
</div>
)}
<div className="mb-2 text-center text-4xl font-medium">
<span className="text-bg-500 mr-2">
{transaction.side === 'debit' ? '+' : '-'}
</span>
RM{' '}
{Intl.NumberFormat('en-MY', {
maximumFractionDigits: 2,
minimumFractionDigits: 2
}).format(transaction.amount)}
</div>
<p className="text-center text-lg">{transaction.particulars}</p>
<p className="text-bg-500 mt-2 text-center">
{dayjs(transaction.date).format('dddd, D MMM YYYY')}
</p>
</div>
<div className="space-y-3">
<div
className={clsx(
'flex-between mt-6 rounded-lg p-4',
componentBgLighter
)}
>
<div className="text-bg-500 flex items-center gap-3">
<Icon className="size-6" icon="tabler:exchange" />
<h3 className="text-lg font-medium">
{t('inputs.transactionType')}
</h3>
</div>
<p className="flex items-center gap-1">
<Icon
className={clsx('size-5', {
'text-green-500': transaction.type === 'income',
'text-red-500': transaction.type === 'expenses',
'text-blue-500': transaction.type === 'transfer'
})}
icon={
{
income: 'tabler:login-2',
expenses: 'tabler:logout',
transfer: 'tabler:arrows-exchange'
}[transaction.type]
}
/>
{transaction.type.charAt(0).toUpperCase() +
transaction.type.slice(1)}
</p>
</div>
<div
className={clsx('flex-between rounded-lg p-4', componentBgLighter)}
>
<div className="text-bg-500 flex items-center gap-3">
<Icon className="size-6" icon="tabler:calendar" />
<h3 className="text-lg font-medium">{t('inputs.date')}</h3>
</div>
<p className="text-center">
{dayjs(transaction.date).format('dddd, D MMM YYYY')}
</p>
</div>
<div
className={clsx(
'flex-between mt-1 rounded-lg p-4',
componentBgLighter
)}
>
<div className="text-bg-500 flex items-center gap-3">
<Icon className="size-6" icon="tabler:category" />
<h3 className="text-lg font-medium">{t('inputs.category')}</h3>
</div>
<p className="flex items-center gap-1">
<Icon
className="size-6"
icon={category!.icon}
style={{
color: category!.color
}}
/>
{category!.name}
</p>
</div>
<div
className={clsx(
'flex-between mt-1 rounded-lg p-4',
componentBgLighter
)}
>
<div className="text-bg-500 flex items-center gap-3">
<Icon className="size-6" icon="tabler:wallet" />
<h3 className="text-lg font-medium">{t('inputs.asset')}</h3>
</div>
<p className="flex items-center gap-1">
<Icon className="size-6" icon={asset!.icon} />
{asset!.name}
</p>
</div>
<div
className={clsx(
'flex-between mt-1 rounded-lg p-4',
componentBgLighter
)}
>
<div className="text-bg-500 flex items-center gap-3">
<Icon className="size-6" icon="tabler:book" />
<h3 className="text-lg font-medium">{t('inputs.ledger')}</h3>
</div>
{ledger ? (
<p className="flex items-center gap-1">
<Icon
className="size-6"
icon={ledger.icon}
style={{
color: ledger.color
}}
/>
{ledger.name}
</p>
) : (
<span className="text-bg-500">No Ledger</span>
)}
</div>
<div
className={clsx('mt-1 space-y-4 rounded-lg p-4', componentBgLighter)}
>
<div className="text-bg-500 flex items-center gap-3">
<Icon className="size-6" icon="tabler:receipt" />
<h3 className="text-lg font-medium">{t('inputs.receipt')}</h3>
</div>
{transaction.receipt ? (
<Button
className="w-full"
icon="tabler:eye"
namespace="apps.wallet"
variant="secondary"
onClick={() => {
open(ViewReceiptModal, {
src: `${import.meta.env.VITE_API_HOST}/media/${transaction.collectionId}/${transaction.id}/${transaction.receipt}`
})
}}
>
View Receipt
</Button>
) : (
<p className="text-bg-500 mb-2 text-center">No Receipt</p>
)}
</div>
<div
className={clsx('mt-1 space-y-4 rounded-lg p-4', componentBgLighter)}
>
<div className="text-bg-500 flex items-center gap-3">
<Icon className="size-6" icon="tabler:map-pin" />
<h3 className="text-lg font-medium">{t('inputs.location')}</h3>
</div>
{transaction.location_name ? (
<APIProvider apiKey={import.meta.env.VITE_GOOGLE_MAPS_API_KEY}>
<Map
className="h-96 overflow-hidden rounded-md"
defaultCenter={{
lat: transaction.location_coords?.lat || 0,
lng: transaction.location_coords?.lon || 0
}}
defaultZoom={15}
mapId="LocationMap"
>
<AdvancedMarker
position={{
lat: transaction.location_coords?.lat || 0,
lng: transaction.location_coords?.lon || 0
}}
/>
</Map>
</APIProvider>
) : (
<p className="text-bg-500 mb-2 text-center">No Location</p>
)}
</div>
</div>
</div>
)
}

View File

@@ -3,7 +3,6 @@ import { useQueryClient } from '@tanstack/react-query'
import clsx from 'clsx'
import dayjs from 'dayjs'
import { useCallback } from 'react'
import { Tooltip } from 'react-tooltip'
import { DeleteConfirmationModal, HamburgerMenu, MenuItem } from '@lifeforge/ui'
import { useModalStore } from '@lifeforge/ui'
@@ -108,10 +107,10 @@ function TransactionListItem({
<div className="flex w-full min-w-0 items-center gap-2">
<div className="min-w-0 truncate text-lg font-medium">
{transaction.particulars}{' '}
{transaction.location !== '' && (
{transaction.location_name !== '' && (
<>
<span className="text-bg-500">@</span>{' '}
{`${transaction.location.split(',')[0]}`}
{transaction.location_name}
</>
)}
</div>
@@ -120,25 +119,6 @@ function TransactionListItem({
<Icon className="text-bg-500 size-5" icon="tabler:file-text" />
</button>
)}
{transaction.location !== '' && (
<>
<span data-tooltip-id={`tooltip-location-${transaction.id}`}>
<Icon className="text-bg-500 size-5" icon="tabler:map-pin" />
</span>
<Tooltip
className="bg-bg-50 text-bg-800 shadow-custom dark:bg-bg-800 dark:text-bg-50 z-9999 rounded-md! p-4! text-base!"
classNameArrow="size-6!"
id={`tooltip-location-${transaction.id}`}
opacity={1}
place="top-start"
positionStrategy="fixed"
>
<div className="relative z-10 max-w-sm">
{transaction.location}
</div>
</Tooltip>
</>
)}
</div>
<div className="text-bg-500 flex items-center gap-2 text-sm font-medium">
<span className="block sm:hidden">