diff --git a/.npmrc b/.npmrc index 7f96873..5ea33b8 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,5 @@ public-hoist-pattern[]=*@nextui-org/* engine-strict=true package-manager-strict-version=true -manage-package-manager-versions=true \ No newline at end of file +manage-package-manager-versions=true +auto-install-peers=true \ No newline at end of file diff --git a/app/error.tsx b/app/error.tsx index 6c1169a..cd4a77e 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -21,7 +21,7 @@ export default function Error({ [reset()]} + onPress={() => [reset()]} /> ) diff --git a/app/feedback/page.tsx b/app/feedback/page.tsx index bde13f1..14e08ed 100644 --- a/app/feedback/page.tsx +++ b/app/feedback/page.tsx @@ -14,8 +14,8 @@ export const metadata: Metadata = { title: NAV_TITLE.FEEDBACK, } -export default function Page() { - const cookieStore = cookies() +export default async function Page() { + const cookieStore = await cookies() const feedbackCookie = cookieStore.get(FEEDBACK.NAME) const confettiCookie = cookieStore.get(CONFETTI.NAME) const isSentFeedback = feedbackCookie?.value === FEEDBACK.VALUE diff --git a/app/layout.tsx b/app/layout.tsx index 5c7fe57..c9cf652 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,8 +10,7 @@ import { CUSTOM_DARK } from '@/config/constants/colors' import { APP_NAME, APP_URL, - AUTHOR_NAME, - AUTHOR_URL, + AUTHOR, DEFAULT_DIR, DEFAULT_LANG, } from '@/config/constants/main' @@ -72,8 +71,8 @@ export const metadata: Metadata = { }, authors: [ { - name: AUTHOR_NAME, - url: AUTHOR_URL, + name: AUTHOR.NAME, + url: AUTHOR.URL, }, ], } diff --git a/app/lib/actions.ts b/app/lib/actions.ts index 646582f..f84b2c3 100644 --- a/app/lib/actions.ts +++ b/app/lib/actions.ts @@ -227,7 +227,8 @@ export async function setCookie( value: TCookie['VALUE'], maxAge: TCookie['MAX_AGE'], ) { - cookies().set(name, value, { + const cookieStore = await cookies() + cookieStore.set(name, value, { maxAge, httpOnly: true, secure: true, diff --git a/app/lib/data.ts b/app/lib/data.ts index 3cfc623..d6eaaad 100644 --- a/app/lib/data.ts +++ b/app/lib/data.ts @@ -8,6 +8,13 @@ import type { TTransaction, } from './types' +export const calculateTotalAmount = (transactions: TTransaction[]) => { + return transactions.reduce( + (total, { amount }) => total + parseFloat(amount), + 0, + ) +} + export const filterTransactions = (transactions: TTransaction[]) => ({ income: transactions.filter((t) => t.isIncome), expense: transactions.filter((t) => !t.isIncome), @@ -55,13 +62,6 @@ export const calculateChartData = ( return chartData } -export const calculateTotalAmount = (transactions: TTransaction[]) => { - return transactions.reduce( - (total, { amount }) => total + parseFloat(amount), - 0, - ) -} - export const calculateTotalsByCategory = ( transactions: TTransaction[], categoryWithoutEmoji: boolean = false, diff --git a/app/lib/helpers.ts b/app/lib/helpers.ts index 9f32160..a601cc2 100644 --- a/app/lib/helpers.ts +++ b/app/lib/helpers.ts @@ -177,7 +177,7 @@ export const calculateEntryRange = ( export const copyToClipboard = async ( successTitle: string, errorTitle: string, - ref?: React.RefObject, + ref?: React.RefObject, text?: string, ): Promise => { const content = text || ref?.current?.textContent || '' diff --git a/app/lib/types.ts b/app/lib/types.ts index f98f5cb..6cb7e23 100644 --- a/app/lib/types.ts +++ b/app/lib/types.ts @@ -1,6 +1,8 @@ +import type { JSX } from 'react' + import type { DefaultSession, Session, User } from 'next-auth' -import type { ObjectId } from 'mongoose' +import { type ObjectId } from 'mongoose' import { CURRENCY_CODE, diff --git a/app/loading.tsx b/app/loading.tsx index bdaec69..937f8ca 100644 --- a/app/loading.tsx +++ b/app/loading.tsx @@ -1,23 +1,24 @@ import { Spinner, SpinnerProps } from '@nextui-org/react' +import { ClassValue } from 'clsx' import { cn } from './lib/helpers' type TProps = { size?: SpinnerProps['size'] inline?: boolean - wrapperCN?: string + wrapperClassName?: ClassValue } export default function Loading({ size = 'lg', inline = false, - wrapperCN, + wrapperClassName, }: TProps) { return (
}) { + const searchParams = await props.searchParams const session = await getCachedAuthSession() const userId = session?.user?.email const query = searchParams?.[SEARCH_PARAM.QUERY] || '' @@ -209,18 +208,18 @@ export default async function Home({

Searched Totals

- {} - Income:{' '} + + Income:{' '} {getFormattedCurrency( getTransactionsTotals(searchedTransactionsByQuery).income, )}{' '} {currency?.code}

- { - - } - Expense:{' '} + + + Expense: + {' '} {getFormattedCurrency( getTransactionsTotals(searchedTransactionsByQuery).expense, )}{' '} diff --git a/app/providers.tsx b/app/providers.tsx index 93f3da6..08150f2 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -4,12 +4,13 @@ import { useEffect, useState } from 'react' import { Toaster } from 'react-hot-toast' import { Next13ProgressBar } from 'next13-progressbar' -import { ThemeProvider as NextThemesProvider } from 'next-themes' +import dynamic from 'next/dynamic' import { useRouter } from 'next/navigation' import { NextUIProvider } from '@nextui-org/react' import { SUCCESS } from '@/config/constants/colors' +import { DEFAULT_THEME } from '@/config/constants/main' import { DARK_TOAST_OPTS, LIGHT_TOAST_OPTS, @@ -19,6 +20,13 @@ import { import { userLocale } from './lib/helpers' import { TTheme } from './lib/types' +const NextThemesProvider = dynamic( + () => import('next-themes').then((e) => e.ThemeProvider), + { + ssr: false, + }, +) + export default function Providers({ children }: { children: React.ReactNode }) { const router = useRouter() const [theme, setTheme] = useState(undefined) @@ -29,7 +37,7 @@ export default function Providers({ children }: { children: React.ReactNode }) { return ( - + }) { + const params = await props.params const { id } = params const [transaction, session] = await Promise.all([ findTransactionById(id), diff --git a/app/ui/balance-card.tsx b/app/ui/balance-card.tsx index d979834..61bd374 100644 --- a/app/ui/balance-card.tsx +++ b/app/ui/balance-card.tsx @@ -4,15 +4,18 @@ import { useCallback, useEffect, useState } from 'react' import { PiArrowCircleDownFill, PiArrowCircleUpFill } from 'react-icons/pi' import { Card, CardHeader } from '@nextui-org/react' +import { motion } from 'framer-motion' import { DEFAULT_CURRENCY_CODE, DEFAULT_TIME_ZONE, } from '@/config/constants/main' +import { DIV } from '@/config/constants/motion' import { getAllTransactions } from '../lib/actions' import { getTransactionsTotals } from '../lib/data' import { + cn, getFormattedBalance, getFormattedCurrency, getGreeting, @@ -27,24 +30,19 @@ type TProps = { } function BalanceCard({ balance, currency, user }: TProps) { - const [isChangeInfo, setIsChangeInfo] = useState(false) + const [isShowTotals, setIsChangeInfo] = useState(false) const [isLoading, setIsLoading] = useState(false) const [total, setTotal] = useState<{ - income: string - expense: string + income: number + expense: number }>({ - income: '', - expense: '', + income: 0, + expense: 0, }) - // const [isFocused, setIsFocused] = useState(false) - // const [position, setPosition] = useState({ x: 0, y: 0 }) - // const [opacity, setOpacity] = useState(0) - // const divRef = useRef(null) - // const isMd = useMedia(getBreakpointWidth('md'), false) - // const isPositiveBalance = Number(balance) > 0 const userId = user?.email const isTotalLoaded = total.income || total.income + const isPositiveBalance = Number(balance) > 0 const currentTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone const greetingMsg = `${getGreeting(currentTimeZone || DEFAULT_TIME_ZONE)}, ${user?.name} 👋🏼` @@ -57,17 +55,13 @@ function BalanceCard({ balance, currency, user }: TProps) { try { const transactions = await getAllTransactions(userId) setTotal({ - income: getFormattedCurrency( - getTransactionsTotals(transactions).income, - ), - expense: getFormattedCurrency( - getTransactionsTotals(transactions).expense, - ), + income: getTransactionsTotals(transactions).income, + expense: getTransactionsTotals(transactions).expense, }) } catch (err) { setTotal({ - income: '', - expense: '', + income: 0, + expense: 0, }) throw err } finally { @@ -76,97 +70,83 @@ function BalanceCard({ balance, currency, user }: TProps) { }, [userId]) useEffect(() => { - if (isChangeInfo && !isTotalLoaded) { + if (isShowTotals && !isTotalLoaded) { getTotal() } - }, [getTotal, isChangeInfo, isTotalLoaded]) - - // const onMouseMove = (e: React.MouseEvent) => { - // if (!isMd) return - // if (!divRef.current || isFocused) return - // const div = divRef.current - // const rect = div.getBoundingClientRect() - // setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }) - // } - - // const onFocus = () => { - // if (!isMd) return - // setIsFocused(true) - // setOpacity(1) - // } - - // const onBlur = () => { - // if (!isMd) return - // setIsFocused(false) - // setOpacity(0) - // } - - // const onMouseEnter = () => { - // if (!isMd) return - // setOpacity(1) - // } - - // const onMouseLeave = () => { - // if (!isMd) return - // setOpacity(0) - // divRef.current?.blur() - // } + }, [getTotal, isShowTotals, isTotalLoaded]) return ( -

+
diff --git a/app/ui/categories/categories.tsx b/app/ui/categories/categories.tsx index 367ac5d..fc4511c 100644 --- a/app/ui/categories/categories.tsx +++ b/app/ui/categories/categories.tsx @@ -194,34 +194,37 @@ function Categories({ userId, userCategories }: TProps) { return ( <> - {categories.map((category, index) => ( - - ))} + {categories.map((category, index) => { + return ( + + ) + })}
)}
    - {category.items?.map((item, itemIndex) => ( - - ))} + {category.items?.map((item, itemIndex) => { + return ( + + ) + })}
) diff --git a/app/ui/chart/custom-legend.tsx b/app/ui/chart/custom-legend.tsx index bacc717..9b9f002 100644 --- a/app/ui/chart/custom-legend.tsx +++ b/app/ui/chart/custom-legend.tsx @@ -8,14 +8,15 @@ import { capitalizeFirstLetter } from '@/app/lib/helpers' function CustomLegend({ payload }: LegendProps) { return ( -
    - {payload?.map((entry, index) => { +
      + {payload?.map((entry) => { const Icon = entry.dataKey === 'income' ? PiArrowCircleUpFill : PiArrowCircleDownFill + return ( -
    • +
    • diff --git a/app/ui/chart/custom-tooltip.tsx b/app/ui/chart/custom-tooltip.tsx index 4f6962d..cc30de8 100644 --- a/app/ui/chart/custom-tooltip.tsx +++ b/app/ui/chart/custom-tooltip.tsx @@ -1,6 +1,12 @@ import { PiArrowCircleDownFill, PiArrowCircleUpFill } from 'react-icons/pi' -import { capitalizeFirstLetter, getFormattedCurrency } from '@/app/lib/helpers' +import { DEFAULT_ICON_SIZE } from '@/config/constants/main' + +import { + capitalizeFirstLetter, + cn, + getFormattedCurrency, +} from '@/app/lib/helpers' import type { TTransaction } from '@/app/lib/types' type TProps = { @@ -14,35 +20,34 @@ function CustomTooltip({ active, payload, label, currency }: TProps) { if (!active && !payload?.length) return null return ( -
      - <> -

      {label}

      - {payload?.map((item, index) => ( - <> - {item.dataKey === 'income' ? ( - <> - {item.payload.income > 0 && ( -

      - {' '} - {capitalizeFirstLetter(item.dataKey)}:{' '} - {getFormattedCurrency(item.value)} {currency?.code} -

      - )} - - ) : ( - <> - {item.payload.expense > 0 && ( -

      - {' '} - {capitalizeFirstLetter(item.dataKey)}:{' '} - {getFormattedCurrency(item.value)} {currency?.code} -

      - )} - - )} - - ))} - +
      +

      {label}

      + {payload?.map((item, idx) => { + const isIncome = item.dataKey === 'income' + const isPositive = isIncome + ? item.payload?.income > 0 + : item.payload?.expense > 0 + + if (!isPositive) return null + + const Icon = isIncome ? PiArrowCircleUpFill : PiArrowCircleDownFill + const iconClassName = isIncome ? 'fill-success' : 'fill-danger' + + return ( +

      + {' '} + + {capitalizeFirstLetter(item.dataKey)}:{' '} + + + {getFormattedCurrency(item.value)} {currency?.code} + +

      + ) + })}
      ) } diff --git a/app/ui/chart/radar-chart.tsx b/app/ui/chart/radar-chart.tsx index 041871e..89f39d7 100644 --- a/app/ui/chart/radar-chart.tsx +++ b/app/ui/chart/radar-chart.tsx @@ -13,8 +13,16 @@ import { import { DANGER, SUCCESS } from '@/config/constants/colors' -import { calculateChartData, filterTransactions } from '@/app/lib/data' -import { capitalizeFirstLetter, getFormattedCurrency } from '@/app/lib/helpers' +import { + calculateChartData, + filterTransactions, + getTransactionsTotals, +} from '@/app/lib/data' +import { + capitalizeFirstLetter, + cn, + getFormattedCurrency, +} from '@/app/lib/helpers' import { TTransaction } from '@/app/lib/types' import CustomLegend from './custom-legend' @@ -28,12 +36,30 @@ type TProps = { function RadarChart({ transactions, currency }: TProps) { const { income, expense } = filterTransactions(transactions) const chartData = calculateChartData(income, expense) + const transactionsTotals = getTransactionsTotals([...income, ...expense]) + const isPositiveBalance = + transactionsTotals.income > transactionsTotals.expense return ( - - + + ( + + {props.payload.value} + + )} + /> getFormattedCurrency(value)} + className='text-sm md:text-medium' /> diff --git a/app/ui/home/transaction-form.tsx b/app/ui/home/transaction-form.tsx index e8bb567..3763861 100644 --- a/app/ui/home/transaction-form.tsx +++ b/app/ui/home/transaction-form.tsx @@ -94,7 +94,7 @@ function TransactionForm({ currency, userCategories }: TProps) { const resetAllStates = () => { setIsSwitchedOn(false) - setIsExpanded(false) + // setIsExpanded(false) setAmount('') setDescription('') setIsLoadingAIData(false) @@ -329,6 +329,7 @@ function TransactionForm({ currency, userCategories }: TProps) { }} > } hoveredElement={ @@ -522,6 +526,7 @@ function Limits({ userId, currency, transactions, userCategories }: TProps) { color='danger' startContent={ } hoveredElement={ diff --git a/app/ui/monthly-report/month-picker.tsx b/app/ui/monthly-report/month-picker.tsx index 308bdc3..8117669 100644 --- a/app/ui/monthly-report/month-picker.tsx +++ b/app/ui/monthly-report/month-picker.tsx @@ -20,8 +20,9 @@ function MonthPicker({ minTransaction, maxTransaction, }: TProps) { - const [dateRange, setDateRange] = - useState>(selectedDate) + const [dateRange, setDateRange] = useState | null>( + selectedDate, + ) const daysInMonth = getDaysInMonth(new Date()) const maxTransactionDayOfMonth = toCalendarDate( @@ -34,9 +35,11 @@ function MonthPicker({ }) // Docs https://github.com/streamich/react-use/blob/master/docs/useDebounce.md - const [isReady, cancel] = useDebounce(() => onDateSelection(dateRange), 300, [ - dateRange, - ]) + const [isReady, cancel] = useDebounce( + () => dateRange && onDateSelection(dateRange), + 300, + [dateRange], + ) useEffect(() => { if (!isReady()) cancel() @@ -51,7 +54,9 @@ function MonthPicker({ value={dateRange} minValue={minTransactionValue} maxValue={maxTransactionValue} - onChange={(range) => setDateRange(range)} + onChange={(range: RangeValue | null) => + range && setDateRange(range) + } />
      diff --git a/app/ui/monthly-report/monthly-report.tsx b/app/ui/monthly-report/monthly-report.tsx index 138ab29..619217c 100644 --- a/app/ui/monthly-report/monthly-report.tsx +++ b/app/ui/monthly-report/monthly-report.tsx @@ -225,7 +225,7 @@ function MonthlyReport({ transactions, currency }: TProps) {