├── .npmrc
├── public
├── favicon.ico
├── googlea82d7709e5ffe685.html
├── icon-192x192.png
├── icon-256x256.png
├── icon-384x384.png
├── icon-512x512.png
├── images
│ ├── wallet.png
│ ├── not-found.png
│ ├── transaction.png
│ ├── wallet-dark.jpg
│ ├── finaki-graph.jpg
│ ├── mobile-preview.jpg
│ ├── preview-dark.jpg
│ ├── preview-white.jpg
│ ├── darkmode_preview.jpg
│ ├── desktop-preview.jpg
│ ├── lightmode_preview.jpg
│ ├── login_illustration.png
│ └── register_illustration.png
├── app.webmanifest
└── vercel.svg
├── postcss.config.js
├── .env.example
├── .idea
├── .gitignore
├── vcs.xml
├── modules.xml
├── finaki-frontend.iml
└── inspectionProfiles
│ └── Project_Default.xml
├── src
├── components
│ ├── Charts
│ │ ├── AreaChart
│ │ │ ├── constants.ts
│ │ │ ├── AreaTooltip.tsx
│ │ │ └── AreaChart.tsx
│ │ ├── constant.ts
│ │ ├── NoData.tsx
│ │ ├── TooltipWrapper.tsx
│ │ ├── ChartHeader.tsx
│ │ ├── ChartPlaceholder.tsx
│ │ ├── ChartWrapper.tsx
│ │ ├── PieChart
│ │ │ ├── PieTooltip.tsx
│ │ │ ├── PieLabel.tsx
│ │ │ └── PieChart.tsx
│ │ ├── ChartContainer.tsx
│ │ └── BarChart
│ │ │ ├── BarChartHeader.tsx
│ │ │ ├── BarTooltip.tsx
│ │ │ └── BarChart.tsx
│ ├── dls
│ │ ├── Modal
│ │ │ ├── index.ts
│ │ │ ├── ModalCloseTringger.tsx
│ │ │ ├── ModalTrigger.tsx
│ │ │ ├── ModalOverlay.tsx
│ │ │ ├── Modal.tsx
│ │ │ └── ModalContent.tsx
│ │ ├── TextWithIcon
│ │ │ ├── Placeholder.tsx
│ │ │ └── index.tsx
│ │ ├── Dropdown
│ │ │ ├── DropdownItem.module.scss
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownSubMenu.tsx
│ │ │ └── DropdownItem.tsx
│ │ ├── Form
│ │ │ ├── FormGroup.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Checkbox
│ │ │ │ └── Checkbox.tsx
│ │ │ ├── Radio
│ │ │ │ └── RadioButton.tsx
│ │ │ ├── TextArea.tsx
│ │ │ ├── InputWithLabel.module.scss
│ │ │ ├── CurrencyInput.tsx
│ │ │ └── InputWithLabel.tsx
│ │ ├── Divider
│ │ │ └── index.tsx
│ │ ├── ActionWrapper
│ │ │ └── OnHoverWrapper.tsx
│ │ ├── IconWrapper
│ │ │ └── index.tsx
│ │ ├── Image
│ │ │ └── index.tsx
│ │ ├── Hamburger
│ │ │ ├── Hamburger.module.scss
│ │ │ └── index.tsx
│ │ ├── IconButton
│ │ │ └── index.tsx
│ │ ├── Loading
│ │ │ └── LoadingSpinner.tsx
│ │ ├── Select
│ │ │ └── Option.tsx
│ │ ├── Heading
│ │ │ └── index.tsx
│ │ ├── Tooltip
│ │ │ └── Tooltip.tsx
│ │ └── Button
│ │ │ ├── Button.tsx
│ │ │ └── LoadingButton.tsx
│ ├── WalletCard
│ │ ├── WalletCardSkeleton.tsx
│ │ ├── constants.ts
│ │ ├── WalletTransaction.tsx
│ │ ├── WalletCard.tsx
│ │ └── WalletOption.tsx
│ ├── Transactions
│ │ ├── TransactionItem
│ │ │ ├── index.ts
│ │ │ └── Skeleton.tsx
│ │ └── AllTransactions
│ │ │ ├── TransactionHeader.tsx
│ │ │ └── TransactionList.tsx
│ ├── icons
│ │ ├── CheckIcon.tsx
│ │ ├── XmarkIcon.tsx
│ │ ├── ChevronIcon.tsx
│ │ ├── PlusIcon.tsx
│ │ ├── UserIcon.tsx
│ │ ├── ArrowRectangleIcon.tsx
│ │ ├── GridIcon.tsx
│ │ ├── ElipsisVerticalIcon.tsx
│ │ ├── PhoneIcon.tsx
│ │ ├── WalletIcon.tsx
│ │ ├── MoonIcon.tsx
│ │ ├── SunIcon.tsx
│ │ ├── ArrowsIcon.tsx
│ │ ├── DesktopIcon.tsx
│ │ ├── PencilIcon.tsx
│ │ ├── ClipboardIcon.tsx
│ │ ├── ArrowIcon.tsx
│ │ ├── TrashIcon.tsx
│ │ ├── ArrowCircleIcon.tsx
│ │ ├── GearIcon.tsx
│ │ └── EyeIcon.tsx
│ ├── Container
│ │ ├── Container.tsx
│ │ └── ContentWrapper.tsx
│ ├── AuthCard
│ │ ├── AuthCardContent.tsx
│ │ └── AuthCard.tsx
│ ├── Dashboard
│ │ ├── DashboardHeader.tsx
│ │ ├── WalletPercentage.tsx
│ │ ├── DashboardContentWrapper.tsx
│ │ ├── TransactionActivity.tsx
│ │ └── RecentTrasactions.tsx
│ ├── Placeholder
│ │ └── index.tsx
│ ├── Analytics
│ │ └── ga.tsx
│ ├── ThemeSelection
│ │ ├── ThemeToggleIcon.tsx
│ │ ├── ThemeOption.tsx
│ │ └── ThemeSelection.tsx
│ ├── Header
│ │ ├── ProfileInfo
│ │ │ ├── index.tsx
│ │ │ └── Action.tsx
│ │ └── Header.tsx
│ ├── Navigation
│ │ ├── HomeNav
│ │ │ └── HomeNavLink.tsx
│ │ └── AppNav
│ │ │ ├── AppNavLink.tsx
│ │ │ ├── AppNav.tsx
│ │ │ └── DemoNav.tsx
│ └── Account
│ │ ├── DangerZone.tsx
│ │ ├── DeviceLists
│ │ ├── index.tsx
│ │ └── Device.tsx
│ │ └── Profile.tsx
├── app
│ ├── [...not_found]
│ │ └── page.tsx
│ ├── app
│ │ ├── [...not_found]
│ │ │ └── page.tsx
│ │ ├── account
│ │ │ ├── page.tsx
│ │ │ └── MyAccount.tsx
│ │ ├── dashboard
│ │ │ ├── page.tsx
│ │ │ └── DashboardComponent.tsx
│ │ ├── wallet
│ │ │ ├── page.tsx
│ │ │ ├── [id]
│ │ │ │ └── page.tsx
│ │ │ └── SortableItem.tsx
│ │ ├── transactions
│ │ │ ├── page.tsx
│ │ │ └── ExportPDF.tsx
│ │ ├── not-found.tsx
│ │ └── layout.tsx
│ ├── robots.ts
│ ├── seo.ts
│ ├── auth
│ │ ├── login
│ │ │ └── page.tsx
│ │ ├── register
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── LoginWithGoogle.tsx
│ ├── sitemap.ts
│ ├── provider.tsx
│ ├── GetUserData.tsx
│ ├── not-found.tsx
│ ├── demo
│ │ ├── account
│ │ │ └── page.tsx
│ │ ├── transactions
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── dashboard
│ │ │ └── page.tsx
│ │ └── wallet
│ │ │ └── page.tsx
│ ├── layout.tsx
│ ├── about
│ │ └── page.tsx
│ └── globals.scss
├── types
│ ├── LayoutSegment.ts
│ ├── Theme.ts
│ ├── QueryKey.ts
│ ├── User.ts
│ ├── Wallet.ts
│ ├── IconProps.ts
│ ├── Routes.ts
│ └── Transaction.ts
├── data
│ ├── dummy-data
│ │ ├── user.json
│ │ ├── wallet.json
│ │ └── dashboard.json
│ └── transactions_example.json
├── utils
│ ├── api
│ │ ├── types
│ │ │ ├── Api.ts
│ │ │ ├── UserAPI.ts
│ │ │ ├── AuthAPI.ts
│ │ │ ├── TransactionAPI.ts
│ │ │ └── WalletAPI.ts
│ │ ├── config.ts
│ │ ├── user.ts
│ │ ├── api.ts
│ │ ├── authApi.ts
│ │ ├── transaction.ts
│ │ └── wallet.ts
│ ├── timeFormat.ts
│ ├── array.ts
│ └── currencyFormat.ts
├── hooks
│ ├── useDebounce.ts
│ ├── useHydration.ts
│ └── useTheme.ts
└── stores
│ ├── transactionStore.ts
│ └── store.ts
├── .eslintrc.json
├── next.config.js
├── .gitignore
├── tsconfig.json
├── LICENSE
├── README.md
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers=true
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/googlea82d7709e5ffe685.html:
--------------------------------------------------------------------------------
1 | google-site-verification: googlea82d7709e5ffe685.html
--------------------------------------------------------------------------------
/public/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/icon-192x192.png
--------------------------------------------------------------------------------
/public/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/icon-256x256.png
--------------------------------------------------------------------------------
/public/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/icon-384x384.png
--------------------------------------------------------------------------------
/public/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/icon-512x512.png
--------------------------------------------------------------------------------
/public/images/wallet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/wallet.png
--------------------------------------------------------------------------------
/public/images/not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/not-found.png
--------------------------------------------------------------------------------
/public/images/transaction.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/transaction.png
--------------------------------------------------------------------------------
/public/images/wallet-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/wallet-dark.jpg
--------------------------------------------------------------------------------
/public/images/finaki-graph.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/finaki-graph.jpg
--------------------------------------------------------------------------------
/public/images/mobile-preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/mobile-preview.jpg
--------------------------------------------------------------------------------
/public/images/preview-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/preview-dark.jpg
--------------------------------------------------------------------------------
/public/images/preview-white.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/preview-white.jpg
--------------------------------------------------------------------------------
/public/images/darkmode_preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/darkmode_preview.jpg
--------------------------------------------------------------------------------
/public/images/desktop-preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/desktop-preview.jpg
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/lightmode_preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/lightmode_preview.jpg
--------------------------------------------------------------------------------
/public/images/login_illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/login_illustration.png
--------------------------------------------------------------------------------
/public/images/register_illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aacmal/finaki/HEAD/public/images/register_illustration.png
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_API_PRODUCTION_URL=https://production-url/api
2 | NEXT_PUBLIC_API_DEVELOPMENT_URL=http://localhost:3001/api
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/src/components/Charts/AreaChart/constants.ts:
--------------------------------------------------------------------------------
1 | export const stopColor = {
2 | default: "#3b82f6",
3 | transparent: "#ffffff7f",
4 | };
5 |
--------------------------------------------------------------------------------
/src/app/[...not_found]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | export default function NotFoundCatchAll() {
4 | notFound();
5 | return null;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/app/[...not_found]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | export default function NotFoundCatchAll() {
4 | notFound();
5 | return null;
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/LayoutSegment.ts:
--------------------------------------------------------------------------------
1 | export enum LayoutSegment {
2 | DASHBOARD = "dashboard",
3 | WALLET = "wallet",
4 | TRANSACTIONS = "transactions",
5 | ACCOUNT = "account",
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/Theme.ts:
--------------------------------------------------------------------------------
1 | export enum Theme {
2 | Light = 'light',
3 | Dark = 'dark',
4 | }
5 |
6 |
7 | // set theme same as user preference use undefined
8 | export type ThemeState = Theme;
--------------------------------------------------------------------------------
/src/data/dummy-data/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "6451ae1759bfa320f0728f7e",
3 | "name": "Johns",
4 | "email": "john@mail.com",
5 | "token": "e4b29048d1da6a1e8cf2",
6 | "telegramAccount": {}
7 | }
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/utils/api/types/Api.ts:
--------------------------------------------------------------------------------
1 | export interface GenericResponse {
2 | message: string;
3 | data: object | null;
4 | }
5 |
6 | export interface GenericRequest {
7 | param: object;
8 | query?: object;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/dls/Modal/index.ts:
--------------------------------------------------------------------------------
1 | import ModalCloseTringger from "./ModalCloseTringger";
2 | import ModalContent from "./ModalContent";
3 | import Modal from "./Modal";
4 |
5 | export { Modal, ModalContent, ModalCloseTringger };
--------------------------------------------------------------------------------
/src/types/QueryKey.ts:
--------------------------------------------------------------------------------
1 | export enum QueryKey {
2 | RECENT_TRANSACTIONS = "recent-transactions",
3 | TRANSACTIONS = "transaction",
4 | TOTAL_TRANSACTIONS = "total-transactions",
5 | WALLETS = "wallets",
6 | USER = "user",
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/User.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: string;
3 | name: string;
4 | email: string;
5 | token?: string | null;
6 | telegramAccount?: {
7 | username: string;
8 | firstName: string;
9 | } | null;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/WalletCard/WalletCardSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import Placeholder from "../Placeholder";
2 |
3 | const WalletCardSkeleton = () => {
4 | return ;
5 | };
6 |
7 | export default WalletCardSkeleton;
8 |
--------------------------------------------------------------------------------
/src/components/Transactions/TransactionItem/index.ts:
--------------------------------------------------------------------------------
1 | import SimpleTransactionItem from "./Simple";
2 | import FullTransactionItem from "./Full";
3 | import SimpleTSkeleton from "./Skeleton";
4 |
5 | export { SimpleTransactionItem, FullTransactionItem, SimpleTSkeleton };
--------------------------------------------------------------------------------
/src/app/app/account/page.tsx:
--------------------------------------------------------------------------------
1 | import MyAccount from "./MyAccount";
2 |
3 | export const metadata = {
4 | title: "Akun Saya",
5 | description: "Akun Saya",
6 | };
7 |
8 | const Page = () => {
9 | return ;
10 | };
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/src/app/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import DashboardComponent from "./DashboardComponent";
2 |
3 | export const metadata = {
4 | title: "Dashboard",
5 | description: "Dashboard",
6 | };
7 |
8 | const Page = () => {
9 | return ;
10 | };
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/src/app/app/wallet/page.tsx:
--------------------------------------------------------------------------------
1 | import MyWallets from "./MyWallets";
2 |
3 | export const metadata = {
4 | title: "Semua Dompet",
5 | description: "Semua dompet saya",
6 | };
7 |
8 | const AllWalletsPage = () => {
9 | return ;
10 | };
11 |
12 | export default AllWalletsPage;
13 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next";
2 |
3 | export default function robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: "*",
7 | allow: "/",
8 | disallow: "/app/",
9 | },
10 | sitemap: "https://finaki.acml.me/sitemap.xml",
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/Wallet.ts:
--------------------------------------------------------------------------------
1 | export interface WalletData {
2 | _id: string;
3 | name: string;
4 | balance: number;
5 | color: string;
6 | isCredit: boolean;
7 | creadtedAt?: string;
8 | updatedAt?: Date;
9 | }
10 |
11 | export interface BalanceHistory {
12 | timestamp: string;
13 | value: number;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/app/transactions/page.tsx:
--------------------------------------------------------------------------------
1 | import AllTransactions from "./AllTransactions";
2 |
3 | export const metadata = {
4 | title: "Semua Transaksi",
5 | description: "Semua transaksi",
6 | };
7 |
8 | const TransactionsPage = () => {
9 | return ;
10 | };
11 |
12 | export default TransactionsPage;
13 |
--------------------------------------------------------------------------------
/src/app/app/wallet/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import WalletById from "./WalletById";
2 |
3 | export const metadata = {
4 | title: "Detail Dompet",
5 | description: "Detail dompet",
6 | robots: "noindex, nofollow",
7 | };
8 |
9 | const WalletDetailPage = () => {
10 | return ;
11 | };
12 |
13 | export default WalletDetailPage;
14 |
--------------------------------------------------------------------------------
/src/components/Charts/constant.ts:
--------------------------------------------------------------------------------
1 | export const COLOR = {
2 | INCOME: "#3b82f6",
3 | OUTCOME: "#f97316",
4 | };
5 |
6 | export const PIE_CHART = {
7 | red: "#ef4444",
8 | cyan: "#06b6d4",
9 | yellow: "#eab308",
10 | blue: "#3b82f6",
11 | green: "#22c55e",
12 | orange: "#f97316",
13 | purple: "#a855f7",
14 | gray: "#6b7280",
15 | };
16 |
--------------------------------------------------------------------------------
/src/data/dummy-data/wallet.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "_id": "64545d70a1a609cfd0c254b6",
5 | "name": "Gopay",
6 | "balance": 35834,
7 | "color": "cyan"
8 | },
9 | {
10 | "_id": "64545d1ccc0796bc8e2656ed",
11 | "name": "OVO",
12 | "balance": 9701,
13 | "color": "purple"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Transactions/TransactionItem/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import Placeholder from "../../Placeholder";
2 | import React from "react";
3 |
4 | type Props = {
5 | delay?: number;
6 | };
7 |
8 | const SimpleTSkeleton = (props: Props) => {
9 | return ;
10 | };
11 |
12 | export default SimpleTSkeleton;
13 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "eslint:recommended",
5 | "next/core-web-vitals",
6 | "plugin:@typescript-eslint/recommended"
7 | ],
8 | "rules": {
9 | "no-unused-vars": "off",
10 | "@typescript-eslint/no-unused-vars": ["error"],
11 | "@typescript-eslint/no-explicit-any": "off"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/dls/TextWithIcon/Placeholder.tsx:
--------------------------------------------------------------------------------
1 | import Placeholder from "../../Placeholder";
2 | import React from "react";
3 |
4 | type Props = {
5 | delay?: number;
6 | };
7 |
8 | const TextWithIconPlaceholder = (props: Props) => {
9 | return ;
10 | };
11 |
12 | export default TextWithIconPlaceholder;
13 |
--------------------------------------------------------------------------------
/src/components/dls/Dropdown/DropdownItem.module.scss:
--------------------------------------------------------------------------------
1 | .dropdownItem {
2 |
3 | .itemIndicator{
4 | width: 25px;
5 | display: inline-flex;
6 | justify-content: center;
7 | align-items: center;
8 | position: absolute;
9 | left: 0;
10 | bottom: 0;
11 | top: 0;
12 |
13 | svg{
14 | width: 20px;
15 | height: 20px;
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/utils/api/types/UserAPI.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@/types/User";
2 | import { GenericResponse } from "./Api";
3 |
4 | export interface UserResponse extends GenericResponse {
5 | data: User;
6 | }
7 |
8 | export interface UserDevicesResponse extends GenericResponse {
9 | data: {
10 | userAgent: string;
11 | createdAt: string;
12 | isCurrent: boolean;
13 | _id: string;
14 | }[];
15 | }
--------------------------------------------------------------------------------
/src/components/dls/Modal/ModalCloseTringger.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { ModalContext } from "./Modal";
3 |
4 | type Props = {
5 | children: React.ReactNode;
6 | };
7 |
8 | const ModalCloseTringger = (props: Props) => {
9 | const { close } = useContext(ModalContext);
10 |
11 | return
{props.children}
;
12 | };
13 |
14 | export default ModalCloseTringger;
15 |
--------------------------------------------------------------------------------
/src/app/seo.ts:
--------------------------------------------------------------------------------
1 | export const desciprtion =
2 | "Finaki adalah aplikasi manajemen keuangan all-in-one, yang memungkinkan Anda untuk mengatur pengeluaran, pendapatan, dan tabungan dengan mudah dan efisien.";
3 | export const keywords =
4 | "aplikasi manajemen keuangan, finaki, sistem informasi, aplikasi keuangan gratis, aplikasi pencatatan pengeluaran, aplikasi pelacakan pemasukan, apliksi keuangan online";
5 |
6 | export const title = "Finaki - Aplikasi Manajemen Keuangan";
7 |
--------------------------------------------------------------------------------
/src/components/Charts/NoData.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type Props = {
4 | className?: string;
5 | message?: string;
6 | };
7 |
8 | const NoData = (props: Props) => {
9 | return (
10 |
11 |
12 | {props.message ?? "Belum ada data yang ditampilkan."}
13 |
14 |
15 | );
16 | };
17 |
18 | export default NoData;
19 |
--------------------------------------------------------------------------------
/src/components/dls/Modal/ModalTrigger.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { ModalContext } from "./Modal";
3 |
4 | type Props = {
5 | children: React.ReactNode;
6 | className?: string;
7 | };
8 |
9 | const ModalTrigger = ({ children, className }: Props) => {
10 | const { open } = useContext(ModalContext);
11 |
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | };
18 |
19 | export default ModalTrigger;
20 |
--------------------------------------------------------------------------------
/src/utils/timeFormat.ts:
--------------------------------------------------------------------------------
1 | export const dateFormat = (timestamp: string) =>
2 | new Date(timestamp).toLocaleDateString("id-ID", {
3 | weekday: "long",
4 | day: "numeric",
5 | month: "long",
6 | year: "numeric",
7 | });
8 |
9 | export const timeFormat = (timestamp: string) =>
10 | new Date(timestamp).toLocaleTimeString(
11 | Intl.DateTimeFormat().resolvedOptions().locale,
12 | {
13 | hour: "numeric",
14 | minute: "numeric",
15 | hour12: false,
16 | }
17 | );
18 |
--------------------------------------------------------------------------------
/src/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 |
3 | export const useDebounce = (value: T, delay = 500) => {
4 | const [debouncedValue, setDebouncedValue] = useState();
5 | const timerRef = useRef();
6 |
7 | useEffect(() => {
8 | timerRef.current = setTimeout(() => setDebouncedValue(value), delay);
9 | return () => {
10 | clearTimeout(timerRef.current);
11 | };
12 | }, [value, delay]);
13 |
14 | return debouncedValue;
15 | };
16 |
--------------------------------------------------------------------------------
/.idea/finaki-frontend.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/Charts/TooltipWrapper.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | className?: string;
6 | };
7 |
8 | const TooltipWrapper = ({ children, className }: Props) => {
9 | return (
10 |
11 | {children}
12 |
13 | );
14 | };
15 |
16 | export default TooltipWrapper;
17 |
--------------------------------------------------------------------------------
/src/components/dls/Modal/ModalOverlay.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | type Props = {
4 | onClick?: () => void;
5 | className?: string;
6 | };
7 |
8 | const ModalOverlay = ({ onClick, className }: Props) => {
9 | return (
10 |
17 | );
18 | };
19 |
20 | export default ModalOverlay;
21 |
--------------------------------------------------------------------------------
/src/components/icons/CheckIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 |
3 | const CheckIcon = (props: IconProps) => {
4 | return (
5 |
11 |
16 |
17 | );
18 | };
19 |
20 | export default CheckIcon;
21 |
--------------------------------------------------------------------------------
/src/components/icons/XmarkIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 |
3 | const XmarkIcon = (props: IconProps) => {
4 | return (
5 |
11 |
16 |
17 | );
18 | };
19 |
20 | export default XmarkIcon;
21 |
--------------------------------------------------------------------------------
/src/components/Container/Container.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | type ContainerProps = {
5 | children: React.ReactNode;
6 | className?: string;
7 | };
8 |
9 | const Container = ({ children, className }: ContainerProps) => {
10 | return (
11 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export default Container;
23 |
--------------------------------------------------------------------------------
/src/components/dls/Form/FormGroup.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | // eslint-disable-next-line no-unused-vars
6 | onSubmit: (e: React.FormEvent) => void;
7 | className?: string;
8 | };
9 |
10 | const FormGroup = ({ children, onSubmit, className }: Props) => {
11 | return (
12 |
15 | );
16 | };
17 |
18 | export default FormGroup;
19 |
--------------------------------------------------------------------------------
/src/components/icons/ChevronIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 |
3 | const ChevronIcon = (props: IconProps) => {
4 | return (
5 |
11 |
16 |
17 | );
18 | };
19 |
20 | export default ChevronIcon;
21 |
--------------------------------------------------------------------------------
/src/components/Charts/ChartHeader.tsx:
--------------------------------------------------------------------------------
1 | import Heading from "../dls/Heading";
2 |
3 | type ChartHeaderProps = {
4 | title: string;
5 | children?: React.ReactNode;
6 | };
7 |
8 | const ChartHeader = ({ title, children }: ChartHeaderProps) => (
9 |
10 |
11 | {title}
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 |
19 | export default ChartHeader;
20 |
--------------------------------------------------------------------------------
/src/types/IconProps.ts:
--------------------------------------------------------------------------------
1 | export interface IconProps {
2 | className?: string | undefined;
3 | fill?: string;
4 | stroke?: string;
5 | strokeWidth?: number;
6 | width?: number;
7 | height?: number;
8 | }
9 |
10 | export interface ArrowIconProps extends IconProps {
11 | direction: 'up' | 'down' | 'left' | 'right';
12 | }
13 |
14 | export const defaultIconProps: IconProps = {
15 | fill: 'currentColor',
16 | }
17 |
18 | export const defaultOutlineIconProps: IconProps = {
19 | fill: 'none',
20 | stroke: 'currentColor',
21 | strokeWidth: 1.5,
22 | }
--------------------------------------------------------------------------------
/src/components/icons/PlusIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | const PlusIcon = (props: IconProps) => {
5 | return (
6 |
12 |
17 |
18 | );
19 | };
20 |
21 | export default PlusIcon;
22 |
--------------------------------------------------------------------------------
/src/types/Routes.ts:
--------------------------------------------------------------------------------
1 | export enum Routes {
2 | App = "/app",
3 | Dashboard = "/app/dashboard",
4 | Wallet = "/app/wallet",
5 | Transactions = "/app/transactions",
6 | Account = "/app/account",
7 | Auth = "/auth",
8 | Login = "/auth/login",
9 | Register = "/auth/register",
10 | Home = "/",
11 | Demo = "/demo",
12 | DashboardDemo = "/demo/dashboard",
13 | WalletDemo = "/demo/wallet",
14 | TransactionsDemo = "/demo/transactions",
15 | AccountDemo = "/demo/account",
16 | ForgotPassword = "/auth/forgot-password",
17 | About = "/about",
18 | }
19 |
--------------------------------------------------------------------------------
/src/types/Transaction.ts:
--------------------------------------------------------------------------------
1 | export interface Transaction {
2 | _id: string;
3 | walletId?: string;
4 | amount: number;
5 | description: string;
6 | note: string;
7 | type: string;
8 | createdAt: string;
9 | updatedAt: string;
10 | // category?: string;
11 | }
12 |
13 | export interface TransactionByDate {
14 | date: string;
15 | transactions: Transaction[];
16 | }
17 |
18 | export interface TotalTransactionByDay {
19 | _id: {
20 | day: number;
21 | };
22 | timestamp: string;
23 | in: number;
24 | out: number;
25 | totalAmount: number;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/AuthCard/AuthCardContent.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | type Props = {
5 | children: React.ReactNode;
6 | className?: string;
7 | };
8 |
9 | const AuthCardContent = ({ children, className }: Props) => {
10 | return (
11 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export default AuthCardContent;
23 |
--------------------------------------------------------------------------------
/src/components/icons/UserIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from '@/types/IconProps'
2 | import React from 'react'
3 |
4 |
5 | const UserIcon = (props: IconProps) => {
6 | return (
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default UserIcon
--------------------------------------------------------------------------------
/src/components/Charts/ChartPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type Props = {
4 | message: string;
5 | };
6 |
7 | const ChartPlaceholder = ({ message }: Props) => {
8 | return (
9 |
10 | {message}
11 |
12 | );
13 | };
14 |
15 | export const ChartLoading = () => (
16 |
17 | );
18 |
19 | export const ChartError = () => ;
20 |
21 | export default ChartPlaceholder;
22 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardHeader.tsx:
--------------------------------------------------------------------------------
1 | import Heading from "../dls/Heading";
2 |
3 | type Props = {
4 | title: string;
5 | children?: React.ReactNode;
6 | };
7 |
8 | const DashboardHeader = ({ title, children }: Props) => (
9 |
10 |
11 | {title}
12 |
13 | {children && (
14 |
15 | {children}
16 |
17 | )}
18 |
19 | );
20 |
21 | export default DashboardHeader;
22 |
--------------------------------------------------------------------------------
/src/components/Placeholder/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | type Props = {
5 | className: string;
6 | animationDelay?: number;
7 | };
8 |
9 | const Placeholder = ({ className, animationDelay = 0 }: Props) => {
10 | return (
11 |
20 | );
21 | };
22 |
23 | export default Placeholder;
24 |
--------------------------------------------------------------------------------
/src/app/app/account/MyAccount.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import DangerZone from "../../../components/Account/DangerZone";
4 | import DevicesLists from "../../../components/Account/DeviceLists";
5 | import Profile from "../../../components/Account/Profile";
6 | import ThemeSelection from "../../../components/ThemeSelection/ThemeSelection";
7 |
8 | const MyAccount = () => {
9 | return (
10 |
16 | );
17 | };
18 |
19 | export default MyAccount;
20 |
--------------------------------------------------------------------------------
/src/components/Container/ContentWrapper.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | type Props = {
5 | className?: string;
6 | children: React.ReactNode;
7 | };
8 |
9 | const ContentWrapper = ({ className, children }: Props) => {
10 | return (
11 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export default ContentWrapper;
23 |
--------------------------------------------------------------------------------
/src/components/icons/ArrowRectangleIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from '@/types/IconProps'
2 | import React from 'react'
3 |
4 |
5 | const ArrowRectangleIcon = (props: IconProps) => {
6 | return (
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default ArrowRectangleIcon
--------------------------------------------------------------------------------
/src/components/dls/Divider/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | type Props = {
5 | vertical?: boolean;
6 | horizontal?: boolean;
7 | className?: string;
8 | };
9 |
10 | const Divider = ({ vertical, horizontal, className }: Props) => {
11 | return (
12 |
24 | );
25 | };
26 |
27 | export default Divider;
28 |
--------------------------------------------------------------------------------
/src/components/dls/ActionWrapper/OnHoverWrapper.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import React from 'react'
3 |
4 | type OnHoverWrapper = {
5 | children: React.ReactNode
6 | className?: string
7 | }
8 |
9 | const OnHoverWrapper = ({
10 | children,
11 | className
12 | }: OnHoverWrapper) => {
13 | return (
14 |
15 | {children}
16 |
17 | )
18 | }
19 |
20 | export default OnHoverWrapper
--------------------------------------------------------------------------------
/src/app/auth/login/page.tsx:
--------------------------------------------------------------------------------
1 | import LoginComponent from "./LoginComponent";
2 |
3 | export const metadata = {
4 | title: "Login",
5 | description: "Login to your Finaki account",
6 | };
7 |
8 | const jsonLd = {
9 | "@context": "https://schema.org",
10 | "@type": "WebPage",
11 | name: "Login | Finaki",
12 | description: "Login to your Finaki account",
13 | };
14 |
15 | const LoginPage = () => {
16 | return (
17 | <>
18 |
22 |
23 | >
24 | );
25 | };
26 |
27 | export default LoginPage;
28 |
--------------------------------------------------------------------------------
/src/components/icons/GridIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IconProps } from '@/types/IconProps'
3 |
4 | const GridIcon = ({
5 | ...props
6 | }: IconProps) => {
7 | return (
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
15 | export default GridIcon
--------------------------------------------------------------------------------
/src/components/icons/ElipsisVerticalIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | const ElipsisVerticalIcon = (props: IconProps) => {
5 | return (
6 |
12 |
17 |
18 | );
19 | };
20 |
21 | export default ElipsisVerticalIcon;
22 |
--------------------------------------------------------------------------------
/src/components/icons/PhoneIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | const PhoneIcon = (props: IconProps) => {
5 | return (
6 |
13 |
18 |
19 | );
20 | };
21 |
22 | export default PhoneIcon;
23 |
--------------------------------------------------------------------------------
/src/components/icons/WalletIcon.tsx:
--------------------------------------------------------------------------------
1 | import { IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | const WalletIcon = ({ ...props }: IconProps) => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default WalletIcon;
13 |
--------------------------------------------------------------------------------
/src/app/auth/register/page.tsx:
--------------------------------------------------------------------------------
1 | import RegisterComponent from "./RegisterComponent";
2 |
3 | export const metadata = {
4 | title: "Register",
5 | description: "Register to create your Finaki account",
6 | };
7 |
8 | const jsonLd = {
9 | "@context": "https://schema.org",
10 | "@type": "WebPage",
11 | name: "Register | Finaki",
12 | description: "Register to create your Finaki account",
13 | };
14 |
15 | const RegisterPage = () => {
16 | return (
17 | <>
18 |
22 |
23 | >
24 | );
25 | };
26 |
27 | export default RegisterPage;
28 |
--------------------------------------------------------------------------------
/src/components/dls/IconWrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | type IconWrapperProps = {
5 | children: React.ReactNode;
6 | className?: string;
7 | withPadding?: boolean;
8 | onClick?: () => void;
9 | };
10 |
11 | const IconWrapper = ({
12 | children,
13 | className,
14 | onClick,
15 | withPadding = false,
16 | }: IconWrapperProps) => {
17 | return (
18 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | export default IconWrapper;
32 |
--------------------------------------------------------------------------------
/src/components/icons/MoonIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | const MoonIcon = (props: IconProps) => {
5 | return (
6 |
12 |
17 |
18 | );
19 | };
20 |
21 | export default MoonIcon;
22 |
--------------------------------------------------------------------------------
/src/components/Analytics/ga.tsx:
--------------------------------------------------------------------------------
1 | import Script from "next/script";
2 | import React from "react";
3 |
4 | const GoogleAnalytics = () => {
5 | if (process.env.NODE_ENV !== "production") return <>>;
6 | return (
7 | <>
8 |
12 |
21 | >
22 | );
23 | };
24 |
25 | export default GoogleAnalytics;
26 |
--------------------------------------------------------------------------------
/src/components/dls/Image/index.tsx:
--------------------------------------------------------------------------------
1 | import { default as Img } from "next/image";
2 |
3 | type Props = {
4 | src: string;
5 | alt: string;
6 | className: string;
7 | priority?: boolean;
8 | sizes?: string;
9 | };
10 |
11 | const Image = ({ src, alt, className, priority, sizes }: Props) => {
12 | return (
13 |
14 |
15 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Image;
30 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/Charts/AreaChart/AreaTooltip.tsx:
--------------------------------------------------------------------------------
1 | import { currencyFormat } from "@/utils/currencyFormat";
2 | import TooltipWrapper from "../TooltipWrapper";
3 |
4 | const renderAreaTooltip = ({ active, payload }: any) => {
5 | if (active) {
6 | const date = new Date(payload[0]?.payload.timestamp).toLocaleDateString(
7 | "id-ID",
8 | {
9 | day: "numeric",
10 | month: "short",
11 | }
12 | );
13 | return (
14 |
15 | {date}
16 |
17 | {currencyFormat(payload[0].value)}
18 |
19 |
20 | );
21 | }
22 | };
23 |
24 | export default renderAreaTooltip;
25 |
--------------------------------------------------------------------------------
/src/components/Charts/ChartWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from "react";
2 | import { ResponsiveContainer } from "recharts";
3 |
4 | type Props = {
5 | children: ReactElement;
6 | className?: string;
7 | };
8 |
9 | /**
10 | *
11 | * @param children use this to set type of chart
12 | * @param className use this to set the width and height of the chart with tailwind class
13 | * @returns
14 | *
15 | */
16 |
17 | const ChartWrapper = ({ children, className }: Props) => {
18 | return (
19 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | };
26 |
27 | export default ChartWrapper;
28 |
--------------------------------------------------------------------------------
/src/components/icons/SunIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | const SunIcon = (props: IconProps) => {
5 | return (
6 |
12 |
17 |
18 | );
19 | };
20 |
21 | export default SunIcon;
22 |
--------------------------------------------------------------------------------
/src/utils/api/types/AuthAPI.ts:
--------------------------------------------------------------------------------
1 | import { GenericResponse } from "./Api";
2 |
3 | export interface RegisterInput {
4 | name: string;
5 | email: string;
6 | password: string;
7 | }
8 |
9 | export interface LoginInput {
10 | email: string;
11 | password: string;
12 | }
13 |
14 | export interface RefreshTokenResponse extends GenericResponse {
15 | data: {
16 | accessToken: string;
17 | };
18 | }
19 |
20 | export interface LoginResponse extends GenericResponse {
21 | data: {
22 | accessToken: string;
23 | };
24 | }
25 |
26 | export interface LogoutResponse extends GenericResponse {
27 | data: null;
28 | }
29 |
30 | export interface ResetPasswordInput {
31 | token: string;
32 | password: string;
33 | }
34 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | const withPWA = require("@ducanh2912/next-pwa").default({
4 | dest: "public",
5 | });
6 | const nextConfig = {
7 | async redirects() {
8 | return [
9 | {
10 | source: '/app',
11 | destination: '/app/dashboard',
12 | permanent: true,
13 | },
14 | {
15 | source: '/auth',
16 | destination: '/auth/login',
17 | permanent: true,
18 | },
19 | {
20 | source: '/demo',
21 | destination: '/demo/dashboard',
22 | permanent: true,
23 | }
24 | ]
25 | },
26 | experimental: {
27 | appDir: true,
28 | },
29 | }
30 |
31 | module.exports = withPWA({
32 | ...nextConfig
33 | })
34 |
--------------------------------------------------------------------------------
/src/app/app/transactions/ExportPDF.tsx:
--------------------------------------------------------------------------------
1 | import ExportTransactionModal from "../../../components/Transactions/ExportTransactionModal";
2 | import Button from "../../../components/dls/Button/Button";
3 | import { useState } from "react";
4 |
5 | const ExportPDF = () => {
6 | const [isOpen, setIsOpen] = useState(false);
7 | return (
8 | <>
9 | setIsOpen(true)}
14 | >
15 | Export PDF
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default ExportPDF;
23 |
--------------------------------------------------------------------------------
/src/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next";
2 |
3 | const domain = "https://finaki.acml.me";
4 | export default function sitemap(): MetadataRoute.Sitemap {
5 | return [
6 | {
7 | url: domain,
8 | lastModified: new Date(),
9 | },
10 | {
11 | url: `${domain}/auth/login`,
12 | lastModified: new Date(),
13 | },
14 | {
15 | url: `${domain}/auth/register`,
16 | lastModified: new Date(),
17 | },
18 | {
19 | url: `${domain}/demo/transactions`,
20 | lastModified: new Date(),
21 | },
22 | {
23 | url: `${domain}/about`,
24 | lastModified: new Date(),
25 | },
26 | {
27 | url: `${domain}/telegram-integration`,
28 | lastModified: new Date(),
29 | },
30 | ];
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Transactions/AllTransactions/TransactionHeader.tsx:
--------------------------------------------------------------------------------
1 | const TransactionHeader = () => {
2 | return (
3 |
4 |
5 |
6 | Waktu
7 |
8 |
9 | Deskripsi
10 |
11 | Dompet
12 |
13 | Jumlah (Rp.)
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default TransactionHeader;
22 |
--------------------------------------------------------------------------------
/src/app/provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5 | import React from "react";
6 | import useTheme from "../hooks/useTheme";
7 |
8 | const queryClient = new QueryClient({
9 | defaultOptions: {
10 | queries: {
11 | retry: false,
12 | refetchOnWindowFocus: false,
13 | staleTime: 1000 * 60 * 5, // 5 minutes
14 | },
15 | },
16 | });
17 |
18 | const RQProvider = ({ children }: { children: React.ReactNode }) => {
19 | useTheme();
20 | return (
21 |
22 | {children}
23 |
24 |
25 | );
26 | };
27 |
28 | export default RQProvider;
29 |
--------------------------------------------------------------------------------
/src/components/icons/ArrowsIcon.tsx:
--------------------------------------------------------------------------------
1 | import { IconProps } from '@/types/IconProps'
2 | import React from 'react'
3 |
4 | const ArrowsIcon = ({
5 | strokeWidth = 1.5,
6 | stroke = 'currentColor',
7 | ...props
8 | }: IconProps) => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default ArrowsIcon
--------------------------------------------------------------------------------
/src/components/Charts/PieChart/PieTooltip.tsx:
--------------------------------------------------------------------------------
1 | import { PIE_CHART } from "../constant";
2 | import { currencyFormat } from "@/utils/currencyFormat";
3 | import TooltipWrapper from "../TooltipWrapper";
4 |
5 | const renderPieTooltip = ({ active, payload }: any) => {
6 | if (active) {
7 | return (
8 |
9 |
17 | {payload[0].name}
18 |
19 | {currencyFormat(payload[0].value)}
20 |
21 | );
22 | }
23 | };
24 | export default renderPieTooltip;
25 |
--------------------------------------------------------------------------------
/src/components/icons/DesktopIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | const DesktopIcon = (props: IconProps) => {
5 | return (
6 |
13 |
18 |
19 | );
20 | };
21 |
22 | export default DesktopIcon;
23 |
--------------------------------------------------------------------------------
/src/app/GetUserData.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { instance } from "@/api/api";
4 | import { getUserData } from "@/api/user";
5 | import { QueryKey } from "@/types/QueryKey";
6 | import { useQuery } from "@tanstack/react-query";
7 | import { useEffect } from "react";
8 | import useStore from "../stores/store";
9 |
10 | const GetUserData = () => {
11 | const setUser = useStore((state) => state.setUser);
12 | useQuery({
13 | queryKey: [QueryKey.USER],
14 | queryFn: getUserData,
15 | onSuccess: (data) => {
16 | setUser(data);
17 | },
18 | staleTime: 0,
19 | });
20 |
21 | useEffect(() => {
22 | instance.defaults.headers["Authorization"] = `Bearer ${localStorage.getItem(
23 | "access-token"
24 | )}`;
25 | }, []);
26 |
27 | return <>>;
28 | };
29 |
30 | export default GetUserData;
31 |
--------------------------------------------------------------------------------
/src/components/icons/PencilIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultIconProps, IconProps } from '@/types/IconProps'
2 | import React from 'react'
3 |
4 |
5 | const PencilIcon = ({
6 | ...props
7 | }: IconProps) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default PencilIcon
--------------------------------------------------------------------------------
/src/utils/api/config.ts:
--------------------------------------------------------------------------------
1 | // const BASE_URL = "https://finaki-backend-git-test-axcamz.vercel.app/api"; // test server
2 | export const BASE_URL =
3 | process.env.NODE_ENV === "production"
4 | ? process.env.NEXT_PUBLIC_API_PRODUCTION_URL
5 | : process.env.NEXT_PUBLIC_API_DEVELOPMENT_URL;
6 |
7 | /**
8 | *
9 | * @param path url path
10 | * @param parameters url parameters
11 | * @returns url with parameters
12 | * @example makeUrl("/transactions", { limit: 10 }) => "/transactions?limit=10"
13 | */
14 | export const makeUrl = (
15 | path: string,
16 | parameters?: Record
17 | ): string => {
18 | if (!parameters) return path;
19 |
20 | const params = Object.keys(parameters)
21 | .map((key) => {
22 | return `${key}=${parameters[key]}`;
23 | })
24 | .join("&");
25 |
26 | return `${path}?${params}`;
27 | };
28 |
--------------------------------------------------------------------------------
/src/hooks/useHydration.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import useStore from "../stores/store";
3 |
4 | const useHydration = (): boolean => {
5 | const [hydrated, setHydrated] = useState(false);
6 |
7 | useEffect(() => {
8 | // Note: This is just in case you want to take into account manual rehydration.
9 | // You can remove the following line if you don't need it.
10 | const unsubHydrate = useStore.persist.onHydrate(() => setHydrated(false));
11 |
12 | const unsubFinishHydration = useStore.persist.onFinishHydration(() =>
13 | setHydrated(true)
14 | );
15 |
16 | setHydrated(useStore.persist.hasHydrated());
17 |
18 | return () => {
19 | unsubHydrate();
20 | unsubFinishHydration();
21 | };
22 | }, []);
23 |
24 | return hydrated;
25 | };
26 |
27 | export default useHydration;
28 |
--------------------------------------------------------------------------------
/src/app/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Heading from "../../components/dls/Heading";
2 | import { Routes } from "@/types/Routes";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | const NotFound = () => {
7 | return (
8 |
9 |
15 |
16 | Ups! Ada yang salah
17 |
18 |
22 | Kembali ke Dashboard
23 |
24 |
25 | );
26 | };
27 |
28 | export default NotFound;
29 |
--------------------------------------------------------------------------------
/src/components/dls/Hamburger/Hamburger.module.scss:
--------------------------------------------------------------------------------
1 | label.hamburgerMenu {
2 | display: flex;
3 | flex-direction: column;
4 | width: 30px;
5 | cursor: pointer;
6 | }
7 | label.hamburgerMenu span {
8 | border-radius: 10px;
9 | height: 3px;
10 | margin: 3px 0;
11 | transition: 0.4s cubic-bezier(0.68, -0.6, 0.32, 1.6);
12 | }
13 |
14 | .hamburgerMenu span:nth-child(1) {
15 | width: 50%;
16 | }
17 |
18 | .hamburgerMenu span:nth-child(2) {
19 | width: 100%;
20 | }
21 |
22 | .hamburgerMenu span:nth-child(3) {
23 | width: 75%;
24 | }
25 |
26 | span.st {
27 | transform-origin: bottom;
28 | transform: rotatez(45deg) translate(3px, 0.2px);
29 | }
30 |
31 | span.nd {
32 | transform-origin: top;
33 | transform: rotatez(-45deg);
34 | }
35 |
36 | span.rd {
37 | transform-origin: bottom;
38 | width: 50%;
39 | transform: translate(7px, -7px) rotatez(45deg);
40 | }
--------------------------------------------------------------------------------
/src/utils/api/user.ts:
--------------------------------------------------------------------------------
1 | import { instance } from "./api";
2 | import { UserDevicesResponse, UserResponse } from "./types/UserAPI";
3 |
4 | export const getUserData = async () => {
5 | const response = await instance.get("/user");
6 | return response.data.data;
7 | };
8 |
9 | export const getUserDevices = async () => {
10 | const response = await instance.get("/user/devices", {
11 | withCredentials: true,
12 | });
13 | return response.data.data;
14 | }
15 |
16 | export const logoutDevices = async (deviceIds: string[]) => {
17 | const response = await instance.post("/user/devices", { deviceIds }, {
18 | withCredentials: true,
19 | });
20 | return response.data.data;
21 | }
22 |
23 | export const detachTelegramAccount = async () => {
24 | const response = await instance.delete("/user/telegram");
25 | return response.data.data;
26 | }
--------------------------------------------------------------------------------
/src/components/icons/ClipboardIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 |
5 | const ClipboardIcon = (props: IconProps) => {
6 | return (
7 |
13 |
18 |
19 | );
20 | };
21 |
22 | export default ClipboardIcon;
23 |
--------------------------------------------------------------------------------
/src/hooks/useTheme.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from "@/types/Theme";
2 | import { useEffect } from "react";
3 | import useStore from "../stores/store";
4 |
5 | export default function useTheme() {
6 | const { colorTheme, setColorTheme } = useStore((state) => ({
7 | colorTheme: state.colorTheme,
8 | setColorTheme: state.setColorTheme,
9 | }));
10 |
11 | const setMetaThemeColor = (color: string) => {
12 | document
13 | .querySelector('meta[name="theme-color"]')
14 | ?.setAttribute("content", color);
15 | };
16 |
17 | useEffect(() => {
18 | if (colorTheme === Theme.Dark) {
19 | document.documentElement.classList.add(Theme.Dark);
20 | setMetaThemeColor("#0f172a");
21 | } else {
22 | document.documentElement.classList.remove(Theme.Dark);
23 | setMetaThemeColor("#e7e5e4");
24 | }
25 | }, [colorTheme]);
26 |
27 | return { colorTheme, setColorTheme };
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/array.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | // group by day using timestamp
3 |
4 | import { Transaction } from "@/types/Transaction";
5 |
6 | export const groupByDay = (array: any[]): any[] => {
7 | const result = array.reduce((acc, item) => {
8 | const date = new Date(item.createdAt).toLocaleDateString(
9 | Intl.DateTimeFormat().resolvedOptions().locale,
10 | {
11 | year: "numeric",
12 | month: "long",
13 | day: "numeric",
14 | }
15 | );
16 | if (!acc[date]) {
17 | acc[date] = [];
18 | }
19 | acc[date].push(item);
20 | return acc;
21 | }, {});
22 | return Object.keys(result).map((date) => ({ date, data: result[date] }));
23 | };
24 |
25 | export const flatInfiniteTransaction = (array: any[]): Transaction[] => {
26 | // is already flat
27 | return array.flatMap((item) => item.transactions);
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/Dashboard/WalletPercentage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardContentWrapper from "./DashboardContentWrapper";
3 | import DashboardHeader from "./DashboardHeader";
4 | import PieChart from "../Charts/PieChart/PieChart";
5 | import { WalletData } from "@/types/Wallet";
6 |
7 | type Props = {
8 | data: WalletData[] | undefined;
9 | loading?: boolean;
10 | };
11 |
12 | const WalletPercentage = ({ data, loading }: Props) => {
13 | const pieChartData = data?.map((wallets: WalletData) => ({
14 | name: wallets.name,
15 | value: wallets.balance,
16 | color: wallets.color,
17 | }));
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default WalletPercentage;
28 |
--------------------------------------------------------------------------------
/src/components/dls/Form/Input.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React, { forwardRef, Ref } from "react";
3 |
4 | interface Props extends React.InputHTMLAttributes {
5 | transparent?: boolean;
6 | className?: string;
7 | error?: any;
8 | }
9 |
10 | // eslint-disable-next-line react/display-name
11 | const Input = forwardRef(
12 | ({ transparent, className, ...props }: Props, ref: Ref) => {
13 | return (
14 |
26 | );
27 | }
28 | );
29 |
30 | export default Input;
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # vscode
39 | .vscode
40 |
41 | # env
42 | .env.local
43 | .env
44 |
45 | # PWA
46 | **/public/precache.*.*.js
47 | **/public/sw.js
48 | **/public/workbox-*.js
49 | **/public/worker-*.js
50 | **/public/fallback-*.js
51 | **/public/precache.*.*.js.map
52 | **/public/sw.js.map
53 | **/public/workbox-*.js.map
54 | **/public/worker-*.js.map
55 | **/public/fallback-*.js
56 |
--------------------------------------------------------------------------------
/src/components/dls/Hamburger/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import style from "./Hamburger.module.scss";
3 | import classNames from "classnames";
4 |
5 | type Props = {
6 | className?: string;
7 | isOpen: boolean;
8 | };
9 |
10 | const Hamburger = ({ className, isOpen }: Props) => {
11 | return (
12 |
13 |
18 |
23 |
28 |
29 | );
30 | };
31 |
32 | export default Hamburger;
33 |
--------------------------------------------------------------------------------
/src/components/Charts/ChartContainer.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | width?: string;
6 | height?: number;
7 | className?: string;
8 | border?: boolean;
9 | theme?: "default" | "transparent";
10 | };
11 |
12 | const ChartContainer = ({
13 | border = false,
14 | className,
15 | children,
16 | theme = "default",
17 | }: Props) => {
18 | return (
19 |
31 | {children}
32 |
33 | );
34 | };
35 |
36 | export default ChartContainer;
37 |
--------------------------------------------------------------------------------
/src/components/dls/Form/Checkbox/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React, { forwardRef } from "react";
3 |
4 | interface Props extends React.HTMLAttributes {
5 | className?: string;
6 | label?: string;
7 | }
8 |
9 | const Checkbox = forwardRef(function Checkbox(
10 | { className, label, id, ...props }: Props,
11 | ref: React.Ref
12 | ) {
13 | return (
14 |
20 |
27 |
28 | {label}
29 |
30 |
31 | );
32 | });
33 |
34 | export default Checkbox;
35 |
--------------------------------------------------------------------------------
/src/components/icons/ArrowIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowIconProps, defaultOutlineIconProps } from "@/types/IconProps";
2 | import classNames from "classnames";
3 |
4 | const ArrowIcon = ({ direction, className, ...props }: ArrowIconProps) => {
5 | return (
6 |
21 |
26 |
27 | );
28 | };
29 |
30 | export default ArrowIcon;
31 |
--------------------------------------------------------------------------------
/src/components/ThemeSelection/ThemeToggleIcon.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import IconButton from "../dls/IconButton";
4 | import MoonIcon from "../icons/MoonIcon";
5 | import SunIcon from "../icons/SunIcon";
6 | import { Theme } from "@/types/Theme";
7 | import useHydration from "../../hooks/useHydration";
8 | import useTheme from "../../hooks/useTheme";
9 |
10 | const ThemeToggleIcon = () => {
11 | const { colorTheme, setColorTheme } = useTheme();
12 | const hydrated = useHydration();
13 |
14 | function toggleTheme() {
15 | if (colorTheme === Theme.Light) {
16 | setColorTheme(Theme.Dark);
17 | } else {
18 | setColorTheme(Theme.Light);
19 | }
20 | }
21 |
22 | if (!hydrated) return <>>;
23 | return (
24 |
25 | {colorTheme === Theme.Light ? : }
26 |
27 | );
28 | };
29 |
30 | export default ThemeToggleIcon;
31 |
--------------------------------------------------------------------------------
/src/components/Charts/BarChart/BarChartHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ChartHeader from "../ChartHeader";
3 |
4 | type BarChartHeaderProps = {
5 | COLOR: any;
6 | };
7 |
8 | const BarChartHeader = ({ COLOR }: BarChartHeaderProps) => (
9 |
10 |
26 |
27 | );
28 | export default BarChartHeader;
29 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Heading from "../components/dls/Heading";
2 | import { Metadata } from "next";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | export const metadata: Metadata = {
7 | title: "Not Found",
8 | description: "Halaman tidak ditemukan",
9 | };
10 |
11 | const NotFound = () => {
12 | return (
13 |
14 |
20 |
21 | Ups! Ada yang salah
22 |
23 |
27 | Kembali ke Home
28 |
29 |
30 | );
31 | };
32 |
33 | export default NotFound;
34 |
--------------------------------------------------------------------------------
/src/components/dls/TextWithIcon/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 | import Heading from "../Heading";
4 |
5 | type TextWithIconProps = {
6 | iconPosition?: "left" | "right";
7 | icon: React.ReactNode;
8 | children: React.ReactNode;
9 | className?: string;
10 | };
11 |
12 | const TextWithIcon = ({
13 | iconPosition = "left",
14 | icon,
15 | className,
16 | children,
17 | }: TextWithIconProps) => {
18 | return (
19 |
25 | {iconPosition === "left" &&
{icon}
}
26 |
27 | {children}
28 |
29 | {iconPosition === "right" &&
{icon}
}
30 |
31 | );
32 | };
33 |
34 | export default TextWithIcon;
35 |
--------------------------------------------------------------------------------
/src/components/icons/TrashIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultIconProps, IconProps } from '@/types/IconProps'
2 | import React from 'react'
3 |
4 |
5 | const TrashIcon = ({ ...props }: IconProps) => {
6 | return (
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default TrashIcon
--------------------------------------------------------------------------------
/src/components/dls/IconButton/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 | import IconWrapper from "../IconWrapper";
4 |
5 | interface Props extends React.ButtonHTMLAttributes {
6 | children: React.ReactNode;
7 | className?: string;
8 | shape?: "circle" | "square";
9 | onClick?: () => void;
10 | onMouseEnter?: () => void;
11 | onMouseLeave?: () => void;
12 | }
13 |
14 | const IconButton = ({
15 | children,
16 | className,
17 | shape = "square",
18 | onClick,
19 | ...props
20 | }: Props) => {
21 | return (
22 |
34 | {children}
35 |
36 | );
37 | };
38 |
39 | export default IconButton;
40 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardContentWrapper.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | width?: string;
6 | height?: number;
7 | className?: string;
8 | border?: boolean;
9 | theme?: "default" | "transparent";
10 | };
11 |
12 | const DashboardContentWrapper = ({
13 | border = false,
14 | className,
15 | children,
16 | theme = "default",
17 | }: Props) => {
18 | return (
19 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export default DashboardContentWrapper;
38 |
--------------------------------------------------------------------------------
/src/components/ThemeSelection/ThemeOption.tsx:
--------------------------------------------------------------------------------
1 | import Image from "../dls/Image";
2 | import classNames from "classnames";
3 | import React from "react";
4 |
5 | type ThemeOptionProps = {
6 | active: boolean;
7 | src: string;
8 | alt: string;
9 | className?: string;
10 | onClick: () => void;
11 | };
12 |
13 | const ThemeOption = ({ active, src, alt, onClick }: ThemeOptionProps) => {
14 | return (
15 |
23 |
30 |
31 | );
32 | };
33 |
34 | export default ThemeOption;
35 |
--------------------------------------------------------------------------------
/public/app.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#e7e5e4",
3 | "background_color": "#FFF",
4 | "display": "standalone",
5 | "scope": "/",
6 | "start_url": "/app",
7 | "name": "Finaki",
8 | "description": "Finaki adalah aplikasi manajemen keuangan all-in-one, yang memungkinkan Anda untuk mengatur pengeluaran, pendapatan, dan tabungan dengan mudah dan efisien.",
9 | "short_name": "Finaki",
10 | "icons": [
11 | {
12 | "src": "/icon-192x192.png",
13 | "sizes": "192x192",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "/icon-256x256.png",
18 | "sizes": "256x256",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "/icon-384x384.png",
23 | "sizes": "384x384",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "/icon-512x512.png",
28 | "sizes": "512x512",
29 | "type": "image/png"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/src/components/AuthCard/AuthCard.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 | import Image from "../dls/Image";
4 |
5 | type Props = {
6 | children: React.ReactNode;
7 | imageUrl?: string;
8 | imageAlt?: string;
9 | className?: string;
10 | };
11 |
12 | const AuthCard = ({ children, className, imageUrl, imageAlt }: Props) => {
13 | return (
14 |
20 | {imageUrl && (
21 |
27 | )}
28 |
29 | {children}
30 |
31 | );
32 | };
33 |
34 | export default AuthCard;
35 |
--------------------------------------------------------------------------------
/src/components/Header/ProfileInfo/index.tsx:
--------------------------------------------------------------------------------
1 | import IconWrapper from "../../dls/IconWrapper";
2 | import LoadingSpinner from "../../dls/Loading/LoadingSpinner";
3 | import ChevronIcon from "../../icons/ChevronIcon";
4 | import useStore from "../../../stores/store";
5 | import Action from "./Action";
6 |
7 | const ProfileInfo = () => {
8 | const { user } = useStore((state) => ({ user: state.user }));
9 |
10 | if (!user) return ;
11 | return (
12 |
13 |
14 | Hi, {user?.name.split(" ")[0]}
15 |
16 |
17 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default ProfileInfo;
30 |
--------------------------------------------------------------------------------
/src/components/dls/Loading/LoadingSpinner.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface Props {
4 | className?: string;
5 | }
6 |
7 | const LoadingSpinner = ({ className }: Props) => {
8 | return (
9 |
20 |
28 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default LoadingSpinner;
42 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/icons/*": ["src/components/icons/*"],
19 | "@/types/*": ["src/types/*"],
20 | "@/api/*": ["src/utils/api/*"],
21 | "@/dls/*": ["src/components/dls/*"],
22 | "@/components/*": ["src/components/*"],
23 | "@/utils/*": ["src/utils/*"],
24 | "@/data/*": ["src/data/*"],
25 | },
26 | "plugins": [
27 | {
28 | "name": "next"
29 | }
30 | ],
31 | "baseUrl": ".",
32 | },
33 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/src/utils/api/types/TransactionAPI.ts:
--------------------------------------------------------------------------------
1 | import { TotalTransactionByDay, Transaction } from "@/types/Transaction";
2 | import { GenericResponse } from "./Api";
3 |
4 | export enum Interval {
5 | // Daily = "daily",
6 | Weekly = "weekly",
7 | Monthly = "monthly"
8 | }
9 |
10 | export interface TransactionResponse extends GenericResponse {
11 | data: Transaction;
12 | }
13 |
14 | export interface TransactionsResponse extends GenericResponse {
15 | data: Transaction[];
16 | }
17 |
18 | export interface InfiniteTransactionResponse extends GenericResponse {
19 | data: {
20 | transactions: Transaction[];
21 | totalPages: number;
22 | currentPage: number;
23 | };
24 | }
25 |
26 | export interface TotalTransactionByDayResponse extends GenericResponse {
27 | data: TotalTransactionByDay[];
28 | }
29 |
30 | export interface TransactionInput {
31 | walletId?: string;
32 | description: string;
33 | amount: number;
34 | type: string;
35 | note: string;
36 | }
37 |
38 | export interface EditTransactionInput {
39 | id: string;
40 | transactionInput: TransactionInput;
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/icons/ArrowCircleIcon.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowIconProps, defaultIconProps } from '@/types/IconProps'
2 | import classNames from 'classnames'
3 | import React from 'react'
4 |
5 | const ArrowCircleIcon = ({
6 | fill = 'currentColor',
7 | direction,
8 | className,
9 | }: ArrowIconProps) => {
10 | return (
11 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | export default ArrowCircleIcon
--------------------------------------------------------------------------------
/src/components/dls/Modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from "react";
2 |
3 | type ModalProps = {
4 | children: React.ReactNode;
5 | defaultOpen?: boolean;
6 | stateOpen?: boolean;
7 | onClose?: () => void;
8 | };
9 |
10 | export const ModalContext = createContext(false);
11 | // create provider
12 | export const Modal = ({
13 | children,
14 | defaultOpen,
15 | stateOpen,
16 | ...props
17 | }: ModalProps) => {
18 | const [isOpen, setIsOpen] = useState(defaultOpen ?? false);
19 |
20 | function open() {
21 | setIsOpen(true);
22 | }
23 |
24 | function close() {
25 | setIsOpen(false);
26 | props.onClose && props.onClose();
27 | }
28 |
29 | if (typeof window !== "undefined") {
30 | if (isOpen === true) {
31 | document.documentElement.style.overflow = "hidden";
32 | } else {
33 | document.documentElement.style.overflow = "";
34 | }
35 | }
36 |
37 | return (
38 |
39 | {children}
40 |
41 | );
42 | };
43 |
44 | export default Modal;
45 |
--------------------------------------------------------------------------------
/src/utils/currencyFormat.ts:
--------------------------------------------------------------------------------
1 | type CurrencyFormatOptions = {
2 | style?: "currency";
3 | currency?: string;
4 | minimumFractionDigits?: number;
5 | currencyDisplay?: "symbol" | "code" | "name";
6 | }
7 |
8 | const initalOptions: CurrencyFormatOptions = {
9 | style: "currency",
10 | currency: "IDR",
11 | minimumFractionDigits: 0,
12 | };
13 |
14 |
15 | /**
16 | * Format number to currency format (1000 -> Rp 1.000)
17 | * @param value
18 | * @param options
19 | * @returns
20 | * @example
21 | * currencyFormat(1000) // Rp 1.000
22 | * currencyFormat(1000, { currency: "USD" }) // $1,000.00
23 | */
24 | export const currencyFormat = (value: number, options?: CurrencyFormatOptions) => {
25 | const number = new Intl.NumberFormat("id-ID", options || initalOptions);
26 |
27 | return number.format(value);
28 | };
29 |
30 | /**
31 | * Remove currency from string value ("Rp 1.000" -> 1000)
32 | *
33 | * @param value
34 | * @returns
35 | * @example
36 | * removeCurrencyFormat("Rp 1.000") // 1000
37 | */
38 | export const removeCurrencyFormat = (value: string) => {
39 | return Number(value.replace(/[^0-9]/g, ""));
40 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Aca M
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/app/demo/account/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ContentWrapper from "../../../components/Container/ContentWrapper";
4 | import ThemeSelection from "../../../components/ThemeSelection/ThemeSelection";
5 | import Heading from "../../../components/dls/Heading";
6 | import { Routes } from "@/types/Routes";
7 | import Link from "next/link";
8 |
9 | const Page = () => {
10 | return (
11 |
12 |
13 | Login untuk melihat profile anda
14 |
15 |
19 | Daftar
20 |
21 |
25 | Masuk
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Page;
35 |
--------------------------------------------------------------------------------
/src/components/Navigation/HomeNav/HomeNavLink.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import Link from "next/link";
3 | import React from "react";
4 |
5 | type Props = {
6 | href: string;
7 | children: React.ReactNode;
8 | isActive: boolean;
9 | type?: "primary" | "secondary";
10 | className?: string;
11 | };
12 |
13 | const HomeNavLink = ({
14 | children,
15 | isActive,
16 | href,
17 | type = "primary",
18 | className,
19 | }: Props) => {
20 | return (
21 |
37 | {children}
38 |
39 | );
40 | };
41 |
42 | export default HomeNavLink;
43 |
--------------------------------------------------------------------------------
/src/components/Navigation/AppNav/AppNavLink.tsx:
--------------------------------------------------------------------------------
1 | import IconWrapper from "../../dls/IconWrapper";
2 | import classNames from "classnames";
3 | import Link from "next/link";
4 | import React from "react";
5 |
6 | type NavLinkProps = {
7 | href: string;
8 | children: React.ReactNode;
9 | icon: React.ReactNode;
10 | active?: boolean;
11 | };
12 |
13 | const AppNavLink = ({ href, children, icon, active = false }: NavLinkProps) => {
14 | return (
15 |
16 |
29 | {icon}
30 | {children}
31 |
32 |
33 | );
34 | };
35 |
36 | export default AppNavLink;
37 |
--------------------------------------------------------------------------------
/src/components/Charts/PieChart/PieLabel.tsx:
--------------------------------------------------------------------------------
1 | const renderPieLabel = (props: any) => {
2 | const { payload } = props;
3 | let orderedPayload = payload.sort(
4 | (a: any, b: any) => b.payload.percent - a.payload.percent
5 | );
6 |
7 | if (orderedPayload.length > 6) {
8 | orderedPayload = orderedPayload.slice(0, 5);
9 | const other = orderedPayload.reduce((acc: any, curr: any) => {
10 | return acc + curr.payload.percent;
11 | }, 0);
12 |
13 | orderedPayload.push({
14 | value: "Lainnya",
15 | color: "#57C5B6",
16 | payload: {
17 | percent: 1 - other,
18 | },
19 | });
20 | }
21 |
22 | return (
23 |
24 | {orderedPayload.map((entry: any, index: number) => {
25 | const percent = ((entry.payload.percent || 0) * 100).toFixed(1);
26 | return (
27 |
28 |
29 | {percent}%
30 |
31 | {entry.value}
32 |
33 | );
34 | })}
35 |
36 | );
37 | };
38 |
39 | export default renderPieLabel;
40 |
--------------------------------------------------------------------------------
/src/components/dls/Select/Option.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React, { useContext, useEffect } from "react";
3 | import { SelectContext, SelectContextType } from "./Select";
4 |
5 | type Props = {
6 | children: React.ReactNode;
7 | value: string | null;
8 | selected?: boolean;
9 | } & React.HTMLAttributes;
10 |
11 | const Option = ({ children, value, selected, ...props }: Props) => {
12 | const { setSelected } = useContext(SelectContext) as SelectContextType;
13 |
14 | useEffect(() => {
15 | if (selected) {
16 | setTimeout(() => {
17 | setSelected({
18 | value,
19 | label: children as string,
20 | });
21 | }, 0);
22 | }
23 | // eslint-disable-next-line react-hooks/exhaustive-deps
24 | }, []);
25 |
26 | return (
27 |
32 | setSelected({
33 | value,
34 | label: children as string,
35 | })
36 | }
37 | role="listitem"
38 | {...props}
39 | >
40 | {children}
41 |
42 | );
43 | };
44 |
45 | export default Option;
46 |
--------------------------------------------------------------------------------
/src/app/auth/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Routes } from "@/types/Routes";
4 | import { usePathname, useRouter } from "next/navigation";
5 | import React, { useEffect } from "react";
6 | import {GoogleOAuthProvider} from "@react-oauth/google";
7 |
8 | type Props = {
9 | children: React.ReactNode;
10 | };
11 |
12 | const AuthLayout = ({ children }: Props) => {
13 | const router = useRouter();
14 | const pathname = usePathname();
15 |
16 | const registerPage = pathname.includes(Routes.Register);
17 | const loginPage = pathname.includes(Routes.Login);
18 |
19 | // redirect to app if already logged in or has access token
20 | useEffect(() => {
21 | const accessToken = window.localStorage.getItem("access-token");
22 |
23 | // redirect if on register or login page, otherwise do nothing
24 | if (accessToken && (registerPage || loginPage)) {
25 | router.push(Routes.App);
26 | }
27 | // eslint-disable-next-line react-hooks/exhaustive-deps
28 | }, []);
29 |
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export default AuthLayout;
38 |
--------------------------------------------------------------------------------
/src/components/Account/DangerZone.tsx:
--------------------------------------------------------------------------------
1 | import { detachTelegramAccount } from "@/api/user";
2 | import LoadingButton from "../dls/Button/LoadingButton";
3 | import { useMutation } from "@tanstack/react-query";
4 | import { toast } from "react-hot-toast";
5 | import useStore from "../../stores/store";
6 |
7 | const DangerZone = () => {
8 | const { user, setUser } = useStore((state) => ({
9 | user: state.user,
10 | setUser: state.setUser,
11 | }));
12 |
13 | const { isLoading, mutate } = useMutation({
14 | mutationFn: detachTelegramAccount,
15 | onSuccess: () => {
16 | setUser({
17 | ...user!,
18 | telegramAccount: null,
19 | });
20 | toast.success("Akun Telegram Berhasil diputuskan");
21 | },
22 | onError: () => {
23 | toast.error("Gagal memutuskan akun Telegram");
24 | },
25 | });
26 |
27 | return (
28 |
29 | {user?.telegramAccount?.username && (
30 | mutate()}
36 | className="font-bold"
37 | />
38 | )}
39 |
40 | );
41 | };
42 |
43 | export default DangerZone;
44 |
--------------------------------------------------------------------------------
/src/components/dls/Dropdown/DropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixDropdown from "@radix-ui/react-dropdown-menu";
2 |
3 | interface Props {
4 | trigger: React.ReactNode;
5 | children: React.ReactNode;
6 | align?: "start" | "center" | "end";
7 | // eslint-disable-next-line no-unused-vars
8 | onOpenChange?: (open: boolean) => void;
9 | sideOffset?: number;
10 | alignOffset?: number;
11 | }
12 |
13 | const DropdownMenu = ({
14 | trigger,
15 | children,
16 | align = "end",
17 | sideOffset = -5,
18 | alignOffset = -15,
19 | onOpenChange,
20 | }: Props) => {
21 | return (
22 |
23 |
24 | {trigger}
25 |
26 |
27 |
33 | {children}
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default DropdownMenu;
41 |
--------------------------------------------------------------------------------
/src/components/dls/Dropdown/DropdownSubMenu.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Portal,
3 | Sub,
4 | SubContent,
5 | SubTrigger,
6 | } from "@radix-ui/react-dropdown-menu";
7 |
8 | import styles from "./DropdownItem.module.scss";
9 | import classNames from "classnames";
10 |
11 | interface Props extends React.ComponentProps {
12 | trigger: React.ReactNode;
13 | iconTrigger?: React.ReactNode;
14 | className?: string
15 | }
16 |
17 | const DropdownSubMenu = ({ children, trigger, iconTrigger, className }: Props) => {
18 | return (
19 |
20 |
21 |
22 | {iconTrigger && (
23 |
{iconTrigger}
24 | )}
25 | {trigger}
26 |
27 |
28 |
29 |
30 | {children}
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default DropdownSubMenu;
38 |
--------------------------------------------------------------------------------
/src/utils/api/api.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import jwtDecode, { JwtPayload } from "jwt-decode";
3 | import { refreshAccessToken } from "./authApi";
4 | import { BASE_URL } from "./config";
5 |
6 | export const instance = axios.create({
7 | baseURL: BASE_URL,
8 | headers: {
9 | "Content-Type": "application/json",
10 | },
11 | });
12 |
13 | instance.interceptors.request.use(
14 | async (config) => {
15 | let currentToken = window?.localStorage.getItem("access-token");
16 |
17 | if (currentToken === null || currentToken === undefined) {
18 | const refreshToken = await refreshAccessToken();
19 | window?.localStorage.setItem("access-token", refreshToken.accessToken);
20 | currentToken = refreshToken.accessToken;
21 | } else {
22 | const decodedToken = jwtDecode(currentToken) as JwtPayload;
23 | const currentTime = new Date().getTime();
24 | if (!!decodedToken.exp && decodedToken.exp * 1000 < currentTime) {
25 | const refreshToken = await refreshAccessToken();
26 | window?.localStorage.setItem("access-token", refreshToken.accessToken);
27 | currentToken = refreshToken.accessToken;
28 | }
29 | }
30 |
31 | config.headers["Authorization"] = `Bearer ${currentToken}`;
32 |
33 | return config;
34 | },
35 | (error) => {
36 | return Promise.reject(error);
37 | }
38 | );
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Welcome to Finaki repository 👋
2 |
3 | 
4 |
5 | Financial record website created with NextJS .
6 |
7 |
8 |
11 |
12 | ## 💵 About
13 | A web-based application created to solve financial recording solutions, Finaki was created so that users can record finances anywhere and anytime.
14 |
15 | ## ✨ Features
16 | - 💳 Authentication
17 | - Login
18 | - Register
19 | - Forgot Password
20 | - Logout
21 | - 📝 CRUD
22 | - Transaction
23 | - Wallet
24 | - 😎 Dark Mode
25 | - 👮 Security
26 | - JWT
27 | - Password Hashing
28 | - 🤙 Responsive UI
29 | - 📱 PWA
30 | - 🏫 Export Transaction to PDF
31 |
32 |
33 | ## 🚀 Tech Stack
34 | - Front end
35 | - [NextJS](https://nextjs.org/)
36 | - [ReactJS](https://reactjs.org/)
37 | - [TypeScript](https://www.typescriptlang.org/)
38 | - [React Query](https://react-query.tanstack.com/)
39 | - [Tailwind CSS](https://tailwindcss.com/)
40 | - Back end
41 | - [ExpressJS](https://expressjs.com/)
42 | - [MongoDB](https://www.mongodb.com/)
43 |
44 |
45 | ## ⚖️ License
46 | Distributed under the MIT License. See `LICENSE` for more information.
47 |
--------------------------------------------------------------------------------
/src/components/icons/GearIcon.tsx:
--------------------------------------------------------------------------------
1 | import {IconProps} from '@/types/IconProps'
2 | import React from 'react'
3 |
4 | const GearIcon = ({
5 | ...props
6 | }: IconProps) => {
7 | return (
8 |
9 |
10 |
11 | )
12 |
13 | }
14 |
15 | export default GearIcon
--------------------------------------------------------------------------------
/src/components/dls/Form/Radio/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import IconWrapper from "../../IconWrapper";
2 | import classNames from "classnames";
3 | import { forwardRef, InputHTMLAttributes, Ref } from "react";
4 |
5 | interface Props extends InputHTMLAttributes {
6 | id: string;
7 | name: string;
8 | label: string;
9 | className?: string;
10 | icon?: React.ReactNode;
11 | checked?: boolean;
12 | }
13 |
14 | // eslint-disable-next-line react/display-name
15 | const RadioButton = forwardRef(
16 | (
17 | { id, label, className, icon, ...props }: Props,
18 | ref: Ref
19 | ) => {
20 | return (
21 |
22 |
29 |
33 | {icon && {icon} }
34 | {label}
35 |
36 |
37 | );
38 | }
39 | );
40 |
41 | export default RadioButton;
42 |
--------------------------------------------------------------------------------
/src/components/ThemeSelection/ThemeSelection.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Heading from "../dls/Heading";
4 | import { Theme } from "@/types/Theme";
5 | import useHydration from "../../hooks/useHydration";
6 | import useTheme from "../../hooks/useTheme";
7 | import ContentWrapper from "../Container/ContentWrapper";
8 | import ThemeOption from "./ThemeOption";
9 |
10 | const ThemeSelection = () => {
11 | const { colorTheme, setColorTheme } = useTheme();
12 | const hydrated = useHydration();
13 |
14 | const themeList = [
15 | {
16 | src: "/images/lightmode_preview.jpg",
17 | alt: "lightmode preview",
18 | },
19 | {
20 | src: "/images/darkmode_preview.jpg",
21 | alt: "darkmode preview",
22 | },
23 | ];
24 |
25 | if (!hydrated) return <>>;
26 | return (
27 |
28 |
29 | Tema
30 |
31 |
32 | setColorTheme(Theme.Light)}
35 | active={colorTheme === Theme.Light}
36 | />
37 | setColorTheme(Theme.Dark)}
40 | active={colorTheme === Theme.Dark}
41 | />
42 |
43 |
44 | );
45 | };
46 |
47 | export default ThemeSelection;
48 |
--------------------------------------------------------------------------------
/src/components/WalletCard/constants.ts:
--------------------------------------------------------------------------------
1 | export type WalletColor =
2 | | "blue"
3 | | "orange"
4 | | "green"
5 | | "red"
6 | | "purple"
7 | | "yellow"
8 | | "gray"
9 | | "cyan";
10 |
11 | export const walletColors = {
12 | blue: "from-blue-400 to-blue-600",
13 | orange: "from-orange-400 to-orange-600",
14 | green: "from-green-400 to-green-600",
15 | red: "from-red-400 to-red-600",
16 | purple: "from-purple-400 to-purple-600",
17 | yellow: "from-yellow-400 to-yellow-600",
18 | gray: "from-gray-400 to-gray-600",
19 | cyan: "from-cyan-400 to-cyan-600",
20 | };
21 |
22 | export const indicatorColor = {
23 | blue: "#3b82f6",
24 | orange: "#f97316",
25 | green: "#22c55e",
26 | red: "#ef4444",
27 | purple: "#a855f7",
28 | yellow: "#eab308",
29 | gray: "#6b7280",
30 | cyan: "#06b6d4",
31 | };
32 |
33 | export const hashCodeColor = {
34 | blue: "#3b82f6",
35 | orange: "#f97316",
36 | green: "#22c55e",
37 | red: "#ef4444",
38 | purple: "#a855f7",
39 | yellow: "#eab308",
40 | gray: "#6b7280",
41 | cyan: "#06b6d4",
42 | };
43 |
44 | export const walletLabelColor = {
45 | blue: "bg-blue-200 text-blue-700",
46 | orange: "bg-orange-200 text-orange-700",
47 | green: "bg-green-200 text-green-700",
48 | red: "bg-red-200 text-red-700",
49 | purple: "bg-purple-200 text-purple-700",
50 | yellow: "bg-yellow-200 text-yellow-700",
51 | gray: "bg-gray-200 text-gray-700",
52 | cyan: "bg-cyan-200 text-cyan-700",
53 | };
54 |
--------------------------------------------------------------------------------
/src/utils/api/types/WalletAPI.ts:
--------------------------------------------------------------------------------
1 | import { BalanceHistory, WalletData } from "@/types/Wallet";
2 | import { GenericRequest, GenericResponse } from "./Api";
3 |
4 | export interface WalletInput {
5 | name: string;
6 | balance?: number;
7 | color: string;
8 | isCredit: boolean;
9 | }
10 |
11 | export interface CreatedWalletResponse extends GenericResponse {
12 | data: WalletData;
13 | }
14 |
15 | export interface UpdateWalletRequest {
16 | id: string;
17 | data: WalletInput;
18 | }
19 |
20 | export interface UpdatedWalletResponse extends GenericResponse {
21 | data: WalletData;
22 | }
23 |
24 | export interface AllWalletResponse {
25 | data: WalletData[];
26 | }
27 |
28 | export interface WalletResponse extends GenericResponse {
29 | data: WalletData;
30 | }
31 |
32 | export interface UpdatedWalletColorResponse extends GenericResponse {
33 | data: {
34 | _id: string;
35 | color: string;
36 | };
37 | }
38 |
39 | export interface DeleteWalletRequest extends GenericRequest {
40 | param: {
41 | id: string;
42 | };
43 | query: {
44 | deleteTransactions: boolean;
45 | };
46 | }
47 |
48 | export interface WalletBalanceActivityResponse extends GenericResponse {
49 | data: BalanceHistory[];
50 | }
51 |
52 | export interface WalletDetailsResponse extends GenericResponse {
53 | data: WalletData;
54 | }
55 |
56 | export interface TransferBalance {
57 | sourceWallet: string;
58 | destinationWallet: string;
59 | amount: number;
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/demo/transactions/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import TransactionHeader from "../../../components/Transactions/AllTransactions/TransactionHeader";
4 | import TransactionList from "../../../components/Transactions/AllTransactions/TransactionList";
5 | import Heading from "../../../components/dls/Heading";
6 | import { QueryKey } from "@/types/QueryKey";
7 | import { Transaction } from "@/types/Transaction";
8 | import { groupByDay } from "@/utils/array";
9 | import { useQuery } from "@tanstack/react-query";
10 | import Head from "next/head";
11 |
12 | const TransactionsPage = () => {
13 | const { data, isLoading } = useQuery({
14 | queryKey: [QueryKey.TRANSACTIONS],
15 | queryFn: (): Promise =>
16 | new Promise((resolve) => {
17 | import("@/data/dummy-data/transaction.json").then((data) => {
18 | resolve(data.data as Transaction[]);
19 | });
20 | }),
21 | });
22 |
23 | if (isLoading || !data) {
24 | return (
25 |
26 | Memuat...
27 |
28 | );
29 | }
30 |
31 | const transaction = groupByDay(data);
32 |
33 | return (
34 | <>
35 |
36 | Transaksi
37 |
38 |
42 | >
43 | );
44 | };
45 |
46 | export default TransactionsPage;
47 |
--------------------------------------------------------------------------------
/src/components/Transactions/AllTransactions/TransactionList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Transaction } from "@/types/Transaction";
4 | import classNames from "classnames";
5 | import { Fragment } from "react";
6 | import { FullTransactionItem } from "../TransactionItem";
7 |
8 | type Props = {
9 | data: {
10 | date: string;
11 | data: Transaction[];
12 | }[];
13 | };
14 |
15 | const TransactionList = ({ data }: Props) => {
16 | return (
17 |
18 | {data.map((transactionData, index) => {
19 | return (
20 |
21 |
22 |
23 |
24 | {transactionData.date}
25 |
26 | 0 }
31 | )}
32 | >
33 |
34 |
35 | {transactionData.data?.map((transaction: Transaction) => (
36 |
40 | ))}
41 |
42 | );
43 | })}
44 |
45 | );
46 | };
47 |
48 | export default TransactionList;
49 |
--------------------------------------------------------------------------------
/src/data/dummy-data/dashboard.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "_id": {
5 | "day": 29
6 | },
7 | "timestamp": "2023-04-29T01:42:05.768Z",
8 | "in": 53255,
9 | "out": 53259,
10 | "totalAmount": 106514
11 | },
12 | {
13 | "_id": {
14 | "day": 30
15 | },
16 | "timestamp": "2023-04-30T01:42:05.768Z",
17 | "in": 26004,
18 | "out": 45502,
19 | "totalAmount": 71506
20 | },
21 | {
22 | "_id": {
23 | "day": 1
24 | },
25 | "timestamp": "2023-05-01T01:42:05.768Z",
26 | "in": 34147,
27 | "out": 23933,
28 | "totalAmount": 58080
29 | },
30 | {
31 | "_id": {
32 | "day": 2
33 | },
34 | "timestamp": "2023-05-02T01:42:05.768Z",
35 | "in": 42517,
36 | "out": 23709,
37 | "totalAmount": 66226
38 | },
39 | {
40 | "_id": {
41 | "day": 3
42 | },
43 | "timestamp": "2023-05-03T01:42:05.768Z",
44 | "in": 31752,
45 | "out": 17149,
46 | "totalAmount": 48901
47 | },
48 | {
49 | "_id": {
50 | "day": 4
51 | },
52 | "timestamp": "2023-05-04T01:42:05.768Z",
53 | "in": 22893,
54 | "out": 13800,
55 | "totalAmount": 36693
56 | },
57 | {
58 | "_id": {
59 | "day": 5
60 | },
61 | "timestamp": "2023-05-05T01:42:05.768Z",
62 | "in": 31101,
63 | "out": 18782,
64 | "totalAmount": 49883
65 | }
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/app/wallet/SortableItem.tsx:
--------------------------------------------------------------------------------
1 | import WalletCard from "../../../components/WalletCard/WalletCard";
2 | import { useSortable } from "@dnd-kit/sortable";
3 | import { CSS } from "@dnd-kit/utilities";
4 | import classNames from "classnames";
5 | import React, { ComponentProps } from "react";
6 | import { RxDragHandleDots2 } from "react-icons/rx";
7 |
8 | type Props = ComponentProps;
9 |
10 | export const SortableWalletCard = ({ ...props }: Props) => {
11 | const {
12 | attributes,
13 | listeners,
14 | isDragging,
15 | setNodeRef,
16 | transform,
17 | transition,
18 | } = useSortable({ id: props.id });
19 |
20 | const style: React.CSSProperties = {
21 | transform: CSS.Transform.toString(transform),
22 | transition,
23 | };
24 |
25 | return (
26 |
33 |
38 |
39 |
40 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/Charts/BarChart/BarTooltip.tsx:
--------------------------------------------------------------------------------
1 | import { currencyFormat } from "@/utils/currencyFormat";
2 | import { COLOR } from "../constant";
3 | import TooltipWrapper from "../TooltipWrapper";
4 |
5 | const renderBarTooltip = ({ active, payload }: any) => {
6 | if (active) {
7 | const date = new Date(payload[0]?.payload.timestamp).toLocaleDateString(
8 | "id-ID",
9 | {
10 | day: "numeric",
11 | month: "short",
12 | }
13 | );
14 | return (
15 |
16 | {date}
17 |
18 |
22 |
23 | {currencyFormat(payload[0].value)}
24 |
25 |
26 |
27 |
31 |
32 | {currencyFormat(payload[1].value)}
33 |
34 |
35 | {/* {date}
36 | {`Rp. ${payload[0].value}`}
*/}
37 |
38 | );
39 | }
40 | };
41 |
42 | export default renderBarTooltip;
43 |
--------------------------------------------------------------------------------
/src/components/dls/Form/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React, { forwardRef, LegacyRef } from "react";
3 |
4 | interface Props extends React.TextareaHTMLAttributes {
5 | transparent?: boolean;
6 | className?: string;
7 | id: string;
8 | label?: string;
9 | placeholder?: string;
10 | padding?: string;
11 | error?: {
12 | message: string;
13 | };
14 | }
15 |
16 | // eslint-disable-next-line react/display-name
17 | const TextArea = forwardRef(
18 | (
19 | {
20 | className,
21 | id,
22 | error,
23 | label,
24 | placeholder,
25 | padding = "px-4 py-4",
26 | ...props
27 | }: Props,
28 | ref: LegacyRef
29 | ) => {
30 | return (
31 |
37 |
45 | {label}
46 | {error && (
47 |
51 | {error.message}
52 |
53 | )}
54 |
55 | );
56 | }
57 | );
58 |
59 | export default TextArea;
60 |
--------------------------------------------------------------------------------
/src/components/dls/Modal/ModalContent.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React, { useContext } from "react";
3 | import { ModalContext } from "./Modal";
4 | import ModalOverlay from "./ModalOverlay";
5 |
6 | type Props = {
7 | children: React.ReactNode;
8 | closeOnOverlayClick?: boolean;
9 | className?: string;
10 | onClickOverlay?: () => void;
11 | };
12 |
13 | const ModalContent = ({
14 | children,
15 | closeOnOverlayClick,
16 | className,
17 | onClickOverlay,
18 | }: Props) => {
19 | const { isOpen, close } = useContext(ModalContext);
20 | return (
21 |
28 |
{
31 | if (closeOnOverlayClick) {
32 | close();
33 | }
34 | onClickOverlay && onClickOverlay();
35 | }}
36 | />
37 |
45 | {isOpen && children}
46 |
47 |
48 | );
49 | };
50 |
51 | export default ModalContent;
52 |
--------------------------------------------------------------------------------
/src/components/dls/Heading/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | type HeadingTextProps = {
4 | children: React.ReactNode;
5 | className?: string;
6 | fontWeight?: "normal" | "medium" | "bold" | "extrabold";
7 | isItalic?: boolean;
8 | isUnderline?: boolean;
9 | defaultColor?: "bright" | "dark";
10 | level: 1 | 2 | 3 | 4 | 5 | 6;
11 | gradient?: boolean
12 | };
13 |
14 | const Heading = ({
15 | children,
16 | className,
17 | defaultColor = "dark",
18 | level,
19 | fontWeight = "bold",
20 | isItalic = false,
21 | isUnderline = false,
22 | gradient = false
23 | }: HeadingTextProps) => {
24 | return (
25 |
49 | {children}
50 |
51 | );
52 | };
53 |
54 | export default Heading;
55 |
--------------------------------------------------------------------------------
/src/components/dls/Tooltip/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4 | import classNames from "classnames";
5 | import * as React from "react";
6 |
7 | const TooltipTrigger = TooltipPrimitive.Trigger;
8 |
9 | const TooltipContent = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, sideOffset = 4, ...props }, ref) => (
13 |
22 | ));
23 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
24 |
25 | const Tooltip = ({
26 | children,
27 | content,
28 | }: {
29 | children: React.ReactNode;
30 | content: string;
31 | }) => {
32 | return (
33 |
34 |
35 | {children}
36 |
37 | {content}
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default Tooltip;
45 |
--------------------------------------------------------------------------------
/src/components/icons/EyeIcon.tsx:
--------------------------------------------------------------------------------
1 | import { defaultOutlineIconProps, IconProps } from "@/types/IconProps";
2 | import React from "react";
3 |
4 | type EyeIconProps = IconProps & {
5 | isVisible?: boolean;
6 | };
7 |
8 | const EyeIcon = ({ isVisible = false, ...props }: EyeIconProps) => {
9 | return (
10 |
16 | {!isVisible ? (
17 | // Visible icon
18 | <>
19 |
24 |
29 | >
30 | ) : (
31 | // Hide icon
32 | <>
33 |
38 | >
39 | )}
40 |
41 | );
42 | };
43 |
44 | export default EyeIcon;
45 |
--------------------------------------------------------------------------------
/src/components/Header/ProfileInfo/Action.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import OnHoverWrapper from "../../dls/ActionWrapper/OnHoverWrapper";
4 | import IconWrapper from "../../dls/IconWrapper";
5 | import ArrowRectangleIcon from "../../icons/ArrowRectangleIcon";
6 | import { Routes } from "@/types/Routes";
7 | import { logoutUser } from "@/utils/api/authApi";
8 | import { useQueryClient } from "@tanstack/react-query";
9 | import { useRouter } from "next/navigation";
10 | import { toast } from "react-hot-toast";
11 | import useStore from "../../../stores/store";
12 |
13 | const Action = () => {
14 | const router = useRouter();
15 | const queryClient = useQueryClient();
16 | const setUser = useStore((state) => state.setUser);
17 |
18 | const handleLogout = () => {
19 | logoutUser()
20 | .then(() => {
21 | toast.success("Logout berhasil");
22 | })
23 | .catch(() => {
24 | toast.error("Ada kesalahan saat logout");
25 | })
26 | .finally(() => {
27 | queryClient.invalidateQueries();
28 | queryClient.removeQueries();
29 | router.push(Routes.Home);
30 | setUser(null);
31 | localStorage.removeItem("access-token");
32 | });
33 | };
34 |
35 | return (
36 |
37 |
42 |
43 |
44 |
45 | Log out
46 |
47 |
48 | );
49 | };
50 |
51 | export default Action;
52 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Inter } from "@next/font/google";
2 | import classNames from "classnames";
3 | import "./globals.scss";
4 | import HomeNav from "../components/Navigation/HomeNav/HomeNav";
5 | import { Toaster } from "react-hot-toast";
6 | import GoogleAnalytics from "../components/Analytics/ga";
7 | import RQProvider from "./provider";
8 | import * as Seo from "./seo";
9 |
10 | // font set up
11 | const font = Inter({
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata = {
16 | metadataBase: new URL("https://finaki.acml.me/"),
17 | title: "Finkai",
18 | keywords: Seo.keywords,
19 | description: Seo.desciprtion,
20 | manifest: "/app.webmanifest",
21 | openGraph: {
22 | title: Seo.title,
23 | description: Seo.desciprtion,
24 | },
25 | twitter: {
26 | title: Seo.title,
27 | description: Seo.desciprtion,
28 | },
29 | };
30 |
31 | export default function RootLayout({
32 | children,
33 | }: {
34 | children: React.ReactNode;
35 | }) {
36 | return (
37 |
38 | {/*
39 | will contain the components returned by the nearest parent
40 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
41 | */}
42 |
43 | {/* theme color */}
44 |
45 |
46 |
49 |
50 |
51 |
52 | {children}
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/about/page.tsx:
--------------------------------------------------------------------------------
1 | import Heading from "../../components/dls/Heading";
2 |
3 | export const metadata = {
4 | title: "About",
5 | description: "About Finaki project",
6 | };
7 | const AboutPage = () => {
8 | return (
9 |
10 |
11 |
12 | About this project
13 |
14 |
15 | Finaki is a web-based application for your financial management, you
16 | can record incoming and outgoing transactions. Equipped with graphs to
17 | make it easier for you to analyze your finances.
18 |
19 |
20 |
21 | - Background
22 |
23 |
24 | This application was created to assist me in recording incoming or
25 | outgoing finances so that they are more precisely controlled, and
26 | can be analyzed.
27 |
28 |
29 |
30 |
31 | - Technology
32 |
33 |
34 | This application is made with Typescript on the frontend and
35 | backend. The frontend is built with NextJS and TailwindCSS, and the
36 | backed is built with ExpressJS. For security this application is
37 | equipped with JWT (JSON Web Token).
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default AboutPage;
46 |
--------------------------------------------------------------------------------
/src/components/Navigation/AppNav/AppNav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname } from "next/navigation";
4 |
5 | import ArrowsIcon from "../../icons/ArrowsIcon";
6 | import GridIcon from "../../icons/GridIcon";
7 | import UserIcon from "../../icons/UserIcon";
8 | import WalletIcon from "../../icons/WalletIcon";
9 | import { defaultIconProps } from "@/types/IconProps";
10 | import { Routes } from "@/types/Routes";
11 | import AppNavLink from "./AppNavLink";
12 |
13 | const AppNav = () => {
14 | const pathname = usePathname();
15 |
16 | return (
17 |
18 | }
20 | href={Routes.Dashboard}
21 | active={pathname === Routes.Dashboard}
22 | >
23 | Dashboard
24 |
25 | }
27 | href={Routes.Wallet}
28 | active={pathname?.includes(Routes.Wallet)}
29 | >
30 | Dompet
31 |
32 | }
34 | href={Routes.Transactions}
35 | active={pathname === Routes.Transactions}
36 | >
37 | Transaksi
38 |
39 | }
41 | href={Routes.Account}
42 | active={pathname === Routes.Account}
43 | >
44 | Akun Saya
45 |
46 |
47 | );
48 | };
49 |
50 | export default AppNav;
51 |
--------------------------------------------------------------------------------
/src/components/Navigation/AppNav/DemoNav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname } from "next/navigation";
4 |
5 | import ArrowsIcon from "../../icons/ArrowsIcon";
6 | import GridIcon from "../../icons/GridIcon";
7 | import UserIcon from "../../icons/UserIcon";
8 | import WalletIcon from "../../icons/WalletIcon";
9 | import { defaultIconProps } from "@/types/IconProps";
10 | import { Routes } from "@/types/Routes";
11 | import AppNavLink from "./AppNavLink";
12 |
13 | const DemoNav = () => {
14 | const pathname = usePathname();
15 |
16 | return (
17 |
18 | }
20 | href={Routes.DashboardDemo}
21 | active={pathname === Routes.DashboardDemo}
22 | >
23 | Dashboard
24 |
25 | }
27 | href={Routes.WalletDemo}
28 | active={pathname?.includes(Routes.WalletDemo)}
29 | >
30 | Dompet
31 |
32 | }
34 | href={Routes.TransactionsDemo}
35 | active={pathname === Routes.TransactionsDemo}
36 | >
37 | Transaksi
38 |
39 | }
41 | href={Routes.AccountDemo}
42 | active={pathname === Routes.AccountDemo}
43 | >
44 | Akun Saya
45 |
46 |
47 | );
48 | };
49 |
50 | export default DemoNav;
51 |
--------------------------------------------------------------------------------
/src/components/dls/Form/InputWithLabel.module.scss:
--------------------------------------------------------------------------------
1 | // .inputWithLabelWrapper{
2 | // Disable this line because the module.scss file is not working with the darkmode and change the color in the global.scss file
3 |
4 |
5 | // position: relative;
6 |
7 | // label {
8 | // @apply text-gray-500;
9 | // position: absolute;
10 | // left: 30px;
11 | // top: 50%;
12 | // transform: translateY(-50%);
13 | // padding: 0 5px;
14 | // transition: all 0.2s ease-in-out;
15 | // }
16 |
17 | // input{
18 | // @apply bg-gray-100;
19 | // transition: all 0.2s ease-in-out;
20 | // &:focus + label {
21 | // @apply bg-white dark:bg-slate-900;
22 | // top: 0 !important;
23 | // left: 20px;
24 | // }
25 |
26 | // &:not(:placeholder-shown) + label {
27 | // @apply bg-white;
28 | // top: 0 !important;
29 | // left: 20px;
30 |
31 | // }
32 |
33 | // &::placeholder {
34 | // color: transparent;
35 | // }
36 |
37 | // &:focus::placeholder {
38 | // @apply text-gray-400;
39 | // transition-delay: 100ms;
40 | // }
41 |
42 | // &:not(:placeholder-shown){
43 | // @apply bg-white;
44 | // @apply ring-1 ring-gray-300 invalid:ring-pink-400;
45 | // }
46 |
47 | // &:not(:placeholder-shown) ~ .toggleHide {
48 | // display: block;
49 | // }
50 |
51 | // &:focus{
52 | // @apply bg-white ring-2 ring-blue-400;
53 | // }
54 |
55 | // &:focus:not(:placeholder-shown){
56 | // @apply ring-2 ring-blue-400 ;
57 | // }
58 | // }
59 |
60 | // .toggleHide{
61 | // @apply text-gray-500;
62 | // display: none;
63 | // position: absolute;
64 | // right: 20px;
65 | // top: 50%;
66 | // transform: translateY(-50%);
67 | // cursor: pointer;
68 | // }
69 | // }
--------------------------------------------------------------------------------
/src/components/dls/Form/CurrencyInput.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { currencyFormat } from "@/utils/currencyFormat";
3 | import { forwardRef, useState } from "react";
4 | import InputWithLabel from "./InputWithLabel";
5 |
6 | type Props = {
7 | placeholder: string;
8 | label?: string;
9 | id: string;
10 | prefixSymbol?: string;
11 | error?: any;
12 | className?: string;
13 | required?: boolean;
14 | minLength?: number;
15 | defaultValue?: string;
16 | onChange?: (e: React.ChangeEvent) => void;
17 | onBlur?: (e: React.FocusEvent) => void;
18 | value?: string;
19 | onReset?: () => void;
20 | min?: number;
21 | inputStyle?: string;
22 | };
23 |
24 | const CurrencyInput = forwardRef(function CurrencyInput(
25 | { prefixSymbol = "Rp", className, inputStyle, ...props }: Props,
26 | ref: React.Ref
27 | ) {
28 | const [value, setValue] = useState(
29 | props.value ? parseInt(props.value) : 0
30 | );
31 |
32 | return (
33 | {
43 | setValue(() => {
44 | const newValue = parseInt(e.target.value.replace(/[^0-9]/g, ""));
45 | if (isNaN(newValue)) return 0;
46 | return newValue;
47 | });
48 | props.onChange && props.onChange(e);
49 | }}
50 | value={
51 | props.value
52 | ? value
53 | ? `${prefixSymbol} ${currencyFormat(value, {})}`
54 | : ""
55 | : ""
56 | }
57 | id={props.id}
58 | />
59 | );
60 | });
61 |
62 | export default CurrencyInput;
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "finaki-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@dnd-kit/core": "^6.0.8",
13 | "@dnd-kit/sortable": "^7.0.2",
14 | "@dnd-kit/utilities": "^3.2.1",
15 | "@ducanh2912/next-pwa": "^9.5.0",
16 | "@next/font": "^13.0.7",
17 | "@radix-ui/react-dropdown-menu": "^2.0.2",
18 | "@radix-ui/react-tooltip": "^1.0.7",
19 | "@react-oauth/google": "^0.11.1",
20 | "@tanstack/react-query": "^4.22.0",
21 | "@tanstack/react-query-devtools": "^4.22.0",
22 | "@types/node": "18.11.13",
23 | "@types/react": "18.0.26",
24 | "@types/react-dom": "18.0.9",
25 | "axios": "^1.2.2",
26 | "classnames": "^2.3.2",
27 | "eslint-config-next": "^13.2.0",
28 | "jspdf": "^2.5.1",
29 | "jspdf-autotable": "^3.5.31",
30 | "jwt-decode": "^3.1.2",
31 | "next": "^13.4.9",
32 | "react": "^18.2.0",
33 | "react-dom": "^18.2.0",
34 | "react-hook-form": "^7.42.1",
35 | "react-hot-toast": "^2.4.0",
36 | "react-icons": "^4.8.0",
37 | "react-intersection-observer": "^9.4.3",
38 | "react-select": "^5.7.0",
39 | "recharts": "^2.7.2",
40 | "sass": "^1.56.2",
41 | "ua-parser-js": "^1.0.33",
42 | "zustand": "^4.3.9"
43 | },
44 | "devDependencies": {
45 | "@types/react-datepicker": "^4.15.0",
46 | "@types/ua-parser-js": "^0.7.36",
47 | "@typescript-eslint/eslint-plugin": "^6.7.0",
48 | "@typescript-eslint/parser": "^6.7.0",
49 | "autoprefixer": "^10.4.13",
50 | "eslint": "8.29.0",
51 | "postcss": "^8.4.19",
52 | "tailwindcss": "^3.2.4",
53 | "tailwindcss-animate": "^1.0.7",
54 | "typescript": "4.9.4",
55 | "webpack": "^5.88.2"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/dls/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import React from "react";
3 |
4 | interface Props extends React.ButtonHTMLAttributes {
5 | children: React.ReactNode;
6 | type: "submit" | "button";
7 | width: "full" | "auto" | "fit";
8 | className?: string;
9 | isLoading?: boolean;
10 | buttonStyle?: "primary" | "secondary" | "danger";
11 | background?: boolean;
12 | }
13 |
14 | const Button = ({
15 | children,
16 | type,
17 | width,
18 | className,
19 | buttonStyle = "primary",
20 | background = true,
21 | ...props
22 | }: Props) => {
23 | return (
24 |
51 | {children}
52 |
53 | );
54 | };
55 |
56 | export default Button;
57 |
--------------------------------------------------------------------------------
/src/components/dls/Button/LoadingButton.tsx:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 | import LoadingSpinner from "../Loading/LoadingSpinner";
3 | import Button from "./Button";
4 |
5 | type Props = {
6 | isLoading: boolean;
7 | isSuccess?: boolean;
8 | onSuccessText?: string;
9 | isError?: boolean;
10 | onErrorText?: string;
11 | onLoadingText: string;
12 | width?: "full" | "auto" | "fit";
13 | title: string;
14 | loadingOnSuccess?: boolean;
15 | styleButton?: "primary" | "secondary" | "danger";
16 | background?: boolean;
17 | stroke?: string;
18 | onClick?: () => void;
19 | className?: string;
20 | disabled?: boolean;
21 | };
22 |
23 | const LoadingButton = ({
24 | isLoading,
25 | isSuccess,
26 | width = "full",
27 | onSuccessText,
28 | onLoadingText,
29 | isError,
30 | onErrorText,
31 | title,
32 | loadingOnSuccess,
33 | styleButton,
34 | className,
35 | background,
36 | disabled,
37 | onClick,
38 | }: Props) => {
39 | return (
40 |
49 |
50 |
61 |
62 | {isSuccess && onSuccessText}
63 | {isLoading && onLoadingText}
64 | {isError && onErrorText}
65 | {!isLoading && !isSuccess && !isError && title}
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default LoadingButton;
73 |
--------------------------------------------------------------------------------
/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Divider from "../dls/Divider";
4 | import Heading from "../dls/Heading";
5 | import PlusIcon from "../icons/PlusIcon";
6 | import { LayoutSegment } from "@/types/LayoutSegment";
7 | import { useSelectedLayoutSegment } from "next/navigation";
8 | import useStore from "../../stores/store";
9 | import ProfileInfo from "./ProfileInfo";
10 |
11 | const Header = () => {
12 | const selectedLayout = useSelectedLayoutSegment();
13 | const { setOpen } = useStore((state) => state.addTransactionState);
14 |
15 | const getHeaderTitle = (selectedLayout: string | null) => {
16 | switch (selectedLayout) {
17 | case LayoutSegment.DASHBOARD:
18 | return "Dashboard";
19 | case LayoutSegment.WALLET:
20 | return "Dompet";
21 | case LayoutSegment.TRANSACTIONS:
22 | return "Transaksi";
23 | case LayoutSegment.ACCOUNT:
24 | return "Akun saya";
25 | default:
26 | return "";
27 | }
28 | };
29 | return (
30 |
31 | {getHeaderTitle(selectedLayout)}
32 |
33 |
34 |
setOpen(true)}>
35 |
Tambah Transaksi
36 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default Header;
49 |
--------------------------------------------------------------------------------
/src/components/WalletCard/WalletTransaction.tsx:
--------------------------------------------------------------------------------
1 | import Heading from "../dls/Heading";
2 | import { Transaction } from "@/types/Transaction";
3 | import { SimpleTransactionItem } from "../Transactions/TransactionItem";
4 |
5 | type Props = {
6 | transactions: Transaction[] | undefined;
7 | };
8 |
9 | const WalletTransaction = (props: Props) => {
10 | if (!props.transactions)
11 | return (
12 |
13 |
14 | Riwayat Transaksi
15 |
16 |
17 | {Array(3)
18 | .fill("")
19 | .map((_, index) => (
20 |
27 | ))}
28 |
29 |
30 | );
31 |
32 | return (
33 |
34 |
35 | Riwayat Transaksi
36 |
37 |
38 | {props.transactions.length > 0 ? (
39 | props.transactions?.map((transaction, index) => {
40 | return (
41 |
47 | );
48 | })
49 | ) : (
50 |
51 | Tidak ada transaksi
52 |
53 | )}
54 |
55 |
56 | );
57 | };
58 |
59 | export default WalletTransaction;
60 |
--------------------------------------------------------------------------------
/src/components/Charts/PieChart/PieChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Cell, Legend, Pie, PieChart as PiChart, Tooltip } from "recharts";
4 | import { ChartLoading } from "../ChartPlaceholder";
5 | import ChartWrapper from "../ChartWrapper";
6 | import { PIE_CHART } from "../constant";
7 | import renderPieLabel from "./PieLabel";
8 | import renderPieTooltip from "./PieTooltip";
9 |
10 | export type PieChartData = {
11 | name: string;
12 | value: number;
13 | color: string;
14 | };
15 |
16 | type Props = {
17 | data: PieChartData[];
18 | legend?: boolean;
19 | loading?: boolean;
20 | };
21 |
22 | const PieChart = ({ data, legend = true, loading }: Props) => {
23 | return (
24 |
25 | {loading ? (
26 |
27 | ) : data.length > 0 ? (
28 |
29 |
39 | {data?.map((entry, index) => (
40 | |
48 | ))}
49 |
50 |
57 |
58 |
59 | ) : (
60 |
61 | Belum ada Dompet yang ditambahkan
62 |
63 | )}
64 |
65 | );
66 | };
67 |
68 | export default PieChart;
69 |
--------------------------------------------------------------------------------
/src/app/auth/LoginWithGoogle.tsx:
--------------------------------------------------------------------------------
1 | import Button from "../../components/dls/Button/Button";
2 | import { FcGoogle } from "react-icons/fc";
3 | import { useGoogleLogin } from "@react-oauth/google";
4 | import { loginWithGoogleCode } from "@/api/authApi";
5 | import { Routes } from "@/types/Routes";
6 | import { useRouter } from "next/navigation";
7 | import LoadingSpinner from "../../components/dls/Loading/LoadingSpinner";
8 | import classNames from "classnames";
9 | import { useState } from "react";
10 |
11 | const LoginWithGoogle = ({
12 | onError,
13 | }: {
14 | onError: (message: string) => void;
15 | }) => {
16 | const router = useRouter();
17 | const [loading, setLoading] = useState(false);
18 |
19 | const login = useGoogleLogin({
20 | onError: (err) => {
21 | onError(err.error_description!);
22 | },
23 | onSuccess: ({ code }) => {
24 | loginWithGoogleCode(code)
25 | .then((res) => {
26 | router.replace(Routes.App);
27 | localStorage.setItem("access-token", res.data.accessToken);
28 | })
29 | .catch((err) => {
30 | onError(err.response.data.message);
31 | })
32 | .finally(() => {
33 | setLoading(false);
34 | });
35 | },
36 | flow: "auth-code",
37 | });
38 | return (
39 | {
44 | login();
45 | setLoading(true);
46 | }}
47 | >
48 |
54 |
55 | {loading ? "Memproses" : "Login dengan Google"}
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default LoginWithGoogle;
63 |
--------------------------------------------------------------------------------
/src/utils/api/authApi.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { BASE_URL } from "./config";
3 | import { GenericResponse } from "./types/Api";
4 | import {
5 | LoginInput,
6 | LoginResponse,
7 | LogoutResponse,
8 | RefreshTokenResponse,
9 | RegisterInput,
10 | ResetPasswordInput,
11 | } from "./types/AuthAPI";
12 |
13 | export const authApi = axios.create({
14 | baseURL: `${BASE_URL}/auth`,
15 | withCredentials: true,
16 | });
17 |
18 | export const refreshAccessToken = async () => {
19 | const response = await authApi.get("/refresh-token");
20 | return response.data.data;
21 | };
22 |
23 | export const loginUser = async (data: LoginInput) => {
24 | const response = await authApi.post("/sign", data);
25 | return response.data.data;
26 | };
27 |
28 | export const registerUser = async (data: RegisterInput) => {
29 | const response = await authApi.post("/register", data);
30 | return response.data.data;
31 | };
32 |
33 | export const logoutUser = async () => {
34 | const response = await authApi.delete("/logout");
35 | return response.data.message;
36 | };
37 |
38 | export const getUser = async () => {
39 | const response = await authApi.get("/user");
40 | return response.data.data;
41 | };
42 |
43 | export const forgotPassword = async (email: string) => {
44 | const response = await authApi.post("/forgot-password", {
45 | email,
46 | });
47 | return response.data;
48 | };
49 |
50 | export const verifyResetPasswordToken = async (token: string) => {
51 | const response = await authApi.get(
52 | `/reset-password?token=${token}`
53 | );
54 | return response.data.data;
55 | };
56 |
57 | export const resetPassword = async (data: ResetPasswordInput) => {
58 | const response = await authApi.post("/reset-password", data);
59 | return response.data.message;
60 | };
61 |
62 | export const loginWithGoogleCode = async (code: string)=> {
63 | const response = await authApi.post("/login-with-google", {
64 | code
65 | })
66 | return response.data
67 | }
--------------------------------------------------------------------------------
/src/app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { getUserData } from "@/api/user";
4 | import Container from "../../components/Container/Container";
5 | import Header from "../../components/Header/Header";
6 | import AppNav from "../../components/Navigation/AppNav/AppNav";
7 | import AddTransaction from "../../components/Transactions/AddTransaction";
8 | import TransactionDetail from "../../components/Transactions/TransactionDetail";
9 | import DeleteWalletDialog from "../../components/WalletCard/DeleteWalletDialog";
10 | import TransferBalanceDialog from "../../components/WalletCard/TransferBalanceDialog";
11 | import { QueryKey } from "@/types/QueryKey";
12 | import { Routes } from "@/types/Routes";
13 | import { useQuery } from "@tanstack/react-query";
14 | import { useRouter } from "next/navigation";
15 | import { useEffect } from "react";
16 | import useStore from "../../stores/store";
17 |
18 | type Props = {
19 | children: React.ReactNode;
20 | };
21 |
22 | const AppLayout = ({ children }: Props) => {
23 | const setUser = useStore((state) => state.setUser);
24 | const router = useRouter();
25 |
26 | const { isLoading, data, isError } = useQuery({
27 | queryKey: [QueryKey.USER],
28 | queryFn: getUserData,
29 | onSuccess: (data) => {
30 | setUser(data);
31 | },
32 | staleTime: 0,
33 | });
34 |
35 | useEffect(() => {
36 | if (isError) {
37 | router.push(Routes.Login);
38 |
39 | // remove access token if error
40 | window.localStorage.removeItem("access-token");
41 | }
42 | // eslint-disable-next-line react-hooks/exhaustive-deps
43 | }, [isError]);
44 |
45 | if (isLoading || !data) {
46 | return (
47 |
48 | Loading...
49 |
50 | );
51 | }
52 |
53 | return (
54 | <>
55 |
56 |
57 |
58 |
59 | {children}
60 |
61 |
62 |
63 |
64 |
65 |
66 | >
67 | );
68 | };
69 |
70 | export default AppLayout;
71 |
--------------------------------------------------------------------------------
/src/components/Dashboard/TransactionActivity.tsx:
--------------------------------------------------------------------------------
1 | import { TotalTransactionByDay } from "@/types/Transaction";
2 | import AreaChart from "../Charts/AreaChart/AreaChart";
3 | import DashboardContentWrapper from "./DashboardContentWrapper";
4 | import DashboardHeader from "./DashboardHeader";
5 | import DropdownMenu from "../dls/Dropdown/DropdownMenu";
6 | import { DropdownItem } from "../dls/Dropdown/DropdownItem";
7 | import { BsClockHistory } from "react-icons/bs";
8 | import { Interval } from "@/api/types/TransactionAPI";
9 | import useTransaction from "../../stores/transactionStore";
10 | import { shallow } from "zustand/shallow";
11 |
12 | type Props = {
13 | data: TotalTransactionByDay[] | undefined;
14 | loading?: boolean;
15 | };
16 |
17 | const TransactionActivity = ({ data, loading }: Props) => {
18 | const areaChartData = data?.map((item) => ({
19 | day: item._id.day as unknown as string,
20 | timestamp: item.timestamp,
21 | value: item.totalAmount,
22 | }));
23 |
24 | const { interval, setInterval } = useTransaction(
25 | (state) => ({
26 | interval: state.interval,
27 | setInterval: state.setInterval,
28 | }),
29 | shallow
30 | );
31 |
32 | const ButtonTrigger = () => (
33 |
37 |
38 | {interval}
39 |
40 | );
41 |
42 | return (
43 |
44 |
45 | }>
46 | {Object.values(Interval).map((val) => (
47 | setInterval(val as Interval)}
50 | className="px-3 font-medium capitalize"
51 | key={val}
52 | >
53 | {val}
54 |
55 | ))}
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default TransactionActivity;
64 |
--------------------------------------------------------------------------------
/src/utils/api/transaction.ts:
--------------------------------------------------------------------------------
1 | import { Transaction } from "@/types/Transaction";
2 | import { instance } from "./api";
3 | import { makeUrl } from "./config";
4 | import {
5 | EditTransactionInput,
6 | InfiniteTransactionResponse, Interval,
7 | TotalTransactionByDayResponse,
8 | TransactionInput,
9 | TransactionResponse,
10 | TransactionsResponse,
11 | } from "./types/TransactionAPI";
12 |
13 | export const insertNewTransaction = async (data: TransactionInput) => {
14 | const response = await instance.post(
15 | "/transactions",
16 | data
17 | );
18 | return response.data.data;
19 | };
20 |
21 | export const getTransactionsByDate = async () => {
22 | const response = await instance.get("/transactions/by-date");
23 | return response.data;
24 | };
25 |
26 | export const editTransaction = async ({
27 | id,
28 | transactionInput,
29 | }: EditTransactionInput): Promise => {
30 | const response = await instance.put(`/transactions/${id}`, transactionInput);
31 | return response.data.data;
32 | };
33 |
34 | export const deleteTransaction = async (id: string) => {
35 | const response = await instance.delete(
36 | `/transactions/${id}`
37 | );
38 | return response.data.data;
39 | };
40 |
41 | export const getTotalTransactionByPeriod = async (
42 | interval: Interval
43 | ) => {
44 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
45 | const response = await instance.get(
46 | makeUrl("/transactions/total", { interval, timezone })
47 | );
48 |
49 | return response.data.data;
50 | };
51 |
52 | export const getAllTransactions = async (query: {
53 | limit: number;
54 | page?: number;
55 | search?: string;
56 | }) => {
57 | const response = await instance.get(
58 | makeUrl("/transactions", query)
59 | );
60 |
61 | return response.data.data;
62 | };
63 |
64 | export const getAllTransactionByMonth = async (date: Date) => {
65 | const month = date.getMonth() + 1;
66 | const year = date.getFullYear();
67 | const response = await instance.get(
68 | makeUrl(`/transactions/by-month/${month}/${year}`)
69 | );
70 |
71 | return response.data.data;
72 | };
73 |
--------------------------------------------------------------------------------
/src/data/transactions_example.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": [
3 | {
4 | "date": "2018-01-01",
5 | "transactions": [
6 | {
7 | "id": 1,
8 | "amount": 100,
9 | "type": "out",
10 | "category": "food",
11 | "description": "McDonalds",
12 | "time": "18:30"
13 | },
14 | {
15 | "id": 2,
16 | "amount": 200,
17 | "type": "out",
18 | "category": "food",
19 | "description": "KFC",
20 | "time": "19:30"
21 | },
22 | {
23 | "id": 3,
24 | "amount": 300,
25 | "type": "in",
26 | "category": "food",
27 | "description": "Burger King",
28 | "time": "20:30"
29 | }
30 | ]
31 | },
32 | {
33 | "date": "2018-01-02",
34 | "transactions": [
35 | {
36 | "id": 4,
37 | "amount": 100,
38 | "type": "out",
39 | "category": "food",
40 | "description": "McDonalds",
41 | "time": "18:30"
42 | },
43 | {
44 | "id": 5,
45 | "amount": 200,
46 | "type": "out",
47 | "category": "food",
48 | "description": "KFC",
49 | "time": "19:30"
50 | },
51 | {
52 | "id": 6,
53 | "amount": 300,
54 | "type": "in",
55 | "category": "food",
56 | "description": "Burger King",
57 | "time": "20:30"
58 | }
59 | ]
60 | },
61 | {
62 | "date": "2018-01-03",
63 | "transactions": [
64 | {
65 | "id": 7,
66 | "amount": 100,
67 | "type": "out",
68 | "category": "food",
69 | "description": "McDonalds",
70 | "time": "18:30"
71 | },
72 | {
73 | "id": 8,
74 | "amount": 200,
75 | "type": "in",
76 | "category": "food",
77 | "description": "KFC",
78 | "time": "19:30"
79 | },
80 | {
81 | "id": 9,
82 | "amount": 300,
83 | "type": "out",
84 | "category": "food",
85 | "description": "Burger King",
86 | "time": "20:30"
87 | }
88 | ]
89 | }
90 | ]
91 | }
--------------------------------------------------------------------------------
/src/app/app/dashboard/DashboardComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | getAllTransactions,
5 | getTotalTransactionByPeriod,
6 | } from "@/api/transaction";
7 | import { getAllWallets } from "@/api/wallet";
8 | import Ratio from "../../../components/Dashboard/Ratio";
9 | import RecentTransactions from "../../../components/Dashboard/RecentTrasactions";
10 | import TransactionActivity from "../../../components/Dashboard/TransactionActivity";
11 | import WalletPercentage from "../../../components/Dashboard/WalletPercentage";
12 | import { QueryKey } from "@/types/QueryKey";
13 | import { useQuery } from "@tanstack/react-query";
14 | import useTransaction from "../../../stores/transactionStore";
15 | import { shallow } from "zustand/shallow";
16 |
17 | const DashboardComponent = () => {
18 | const { interval } = useTransaction(
19 | (state) => ({
20 | interval: state.interval,
21 | }),
22 | shallow
23 | );
24 |
25 | const totalTransactionQuery = useQuery({
26 | queryKey: [QueryKey.TOTAL_TRANSACTIONS, interval],
27 | queryFn: () => getTotalTransactionByPeriod(interval),
28 | });
29 |
30 | const recentTransactionsQuery = useQuery({
31 | queryKey: [QueryKey.RECENT_TRANSACTIONS],
32 | queryFn: () =>
33 | getAllTransactions({
34 | limit: 4,
35 | }),
36 | });
37 |
38 | const walletQuery = useQuery({
39 | queryKey: [QueryKey.WALLETS],
40 | queryFn: getAllWallets,
41 | });
42 |
43 | return (
44 |
45 |
49 |
53 |
54 |
58 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default DashboardComponent;
69 |
--------------------------------------------------------------------------------
/src/components/Charts/BarChart/BarChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {TotalTransactionByDay} from "@/types/Transaction";
4 | import classNames from "classnames";
5 | import {Bar, BarChart as BaChart, CartesianGrid, Tooltip, XAxis,} from "recharts";
6 | import {ChartError, ChartLoading} from "../ChartPlaceholder";
7 | import ChartWrapper from "../ChartWrapper";
8 |
9 | import renderBarTooltip from "./BarTooltip";
10 | import useTransaction from "../../../stores/transactionStore";
11 | import {shallow} from "zustand/shallow";
12 | import {Interval} from "@/api/types/TransactionAPI";
13 |
14 | type Props = {
15 | data: TotalTransactionByDay[];
16 | className?: string;
17 | color: any;
18 | size?: "medium" | "large";
19 | loading?: boolean;
20 | };
21 |
22 | const BarChart = ({ data, color, loading, className }: Props) => {
23 | const interval = useTransaction(state => state.interval, shallow)
24 | const barSize = interval === Interval.Weekly ? 8 :4
25 |
26 | return (
27 |
28 | {loading ? (
29 |
30 | ) : data ? (
31 |
32 |
37 |
41 |
48 |
55 |
63 |
64 | ) : (
65 |
66 | )}
67 |
68 | );
69 | };
70 |
71 | export default BarChart;
72 |
--------------------------------------------------------------------------------
/src/components/dls/Form/InputWithLabel.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import EyeIcon from "../../icons/EyeIcon";
4 | import classNames from "classnames";
5 | import { forwardRef, useState } from "react";
6 | import IconWrapper from "../IconWrapper";
7 |
8 | interface Props extends React.InputHTMLAttributes {
9 | type: "text" | "number" | "password" | "email";
10 | placeholder: string;
11 | value?: string | number;
12 | label?: string;
13 | id: string;
14 | defaultValue?: string;
15 | required?: boolean;
16 | minLength?: number;
17 | className?: string;
18 | inputStyle?: string;
19 | error?: {
20 | message: string;
21 | };
22 | }
23 |
24 | // eslint-disable-next-line react/display-name
25 | const InputWithLabel = forwardRef(
26 | (
27 | { id, type, label, className, inputStyle, error, ...props }: Props,
28 | ref: React.Ref
29 | ) => {
30 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
31 |
32 | return (
33 |
39 |
54 | {label}
55 | {error && (
56 |
57 | {error.message}
58 |
59 | )}
60 | {type === "password" && (
61 |
63 | setIsPasswordVisible((currentState) => !currentState)
64 | }
65 | className={"toggleHide"}
66 | >
67 |
68 |
69 | )}
70 |
71 | );
72 | }
73 | );
74 |
75 | export default InputWithLabel;
76 |
--------------------------------------------------------------------------------
/src/stores/transactionStore.ts:
--------------------------------------------------------------------------------
1 | import {Transaction} from "@/types/Transaction";
2 | import {create} from "zustand";
3 | import {Interval} from "@/api/types/TransactionAPI";
4 |
5 | interface TransactionStore {
6 | transactionDetailState: {
7 | isOpen: boolean;
8 | transaction?: Transaction;
9 | };
10 | setTransactionDetailState: ({
11 | isOpen,
12 | transaction,
13 | }: {
14 | isOpen: boolean;
15 | transaction?: Transaction;
16 | }) => void;
17 | interval: Interval;
18 | setInterval: (val: Interval) => void;
19 | transactions: Transaction[];
20 | setTransactions: (transactions: Transaction[]) => void;
21 | dispatchUpdateTransaction: (id: string, newTransaction: Transaction) => void;
22 | dispatchDeleteTransaction: (id: string) => void;
23 | dispatchAddTransaction: (transaction: Transaction) => void;
24 | pushTransactions: (transactions: Transaction[]) => void;
25 | }
26 |
27 | const useTransaction = create((set) => ({
28 | transactionDetailState: {
29 | isOpen: false,
30 | },
31 | transactions: [],
32 | interval: Interval.Weekly,
33 | setInterval: (val) => {
34 | set({ interval: val })
35 | },
36 | setTransactions: (transactions: Transaction[]) => {
37 | set({ transactions });
38 | },
39 | pushTransactions: (transactions: Transaction[]) => {
40 | set((state) => ({
41 | transactions: [...state.transactions, ...transactions],
42 | }));
43 | },
44 | dispatchUpdateTransaction: (id, newTransaction) =>
45 | set((state) => ({
46 | transactions: state.transactions.map((transaction) =>
47 | transaction._id === id ? newTransaction : transaction
48 | ),
49 | })),
50 | dispatchDeleteTransaction: (id) =>
51 | set((state) => ({
52 | transactions: state.transactions.filter(
53 | (transaction) => transaction._id !== id
54 | ),
55 | })),
56 | dispatchAddTransaction: (transaction) =>
57 | set((state) => ({
58 | transactions: [transaction, ...state.transactions],
59 | })),
60 |
61 | setTransactionDetailState: ({
62 | isOpen,
63 | transaction: transactionDetailState,
64 | }) =>
65 | set({
66 | transactionDetailState: {
67 | isOpen: isOpen,
68 | transaction: transactionDetailState,
69 | },
70 | }),
71 | }));
72 |
73 | export default useTransaction;
74 |
--------------------------------------------------------------------------------
/src/app/demo/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { getUserData } from "@/api/user";
4 | import Container from "../../components/Container/Container";
5 | import Header from "../../components/Header/Header";
6 | import DemoNav from "../../components/Navigation/AppNav/DemoNav";
7 | import AddTransaction from "../../components/Transactions/AddTransaction";
8 | import TransactionDetail from "../../components/Transactions/TransactionDetail";
9 | import DeleteWalletDialog from "../../components/WalletCard/DeleteWalletDialog";
10 | import TransferBalanceDialog from "../../components/WalletCard/TransferBalanceDialog";
11 | import { QueryKey } from "@/types/QueryKey";
12 | import { Routes } from "@/types/Routes";
13 | import { useQuery, useQueryClient } from "@tanstack/react-query";
14 | import Link from "next/link";
15 | import { useRouter } from "next/navigation";
16 | import { useEffect } from "react";
17 |
18 | type Props = {
19 | children: React.ReactNode;
20 | };
21 |
22 | const AppLayout = ({ children }: Props) => {
23 | const router = useRouter();
24 |
25 | const queryClient = useQueryClient();
26 | useQuery({
27 | queryKey: [QueryKey.USER],
28 | queryFn: getUserData,
29 | onSuccess: () => {
30 | router.push(Routes.App);
31 | },
32 | staleTime: 0,
33 | });
34 |
35 | useEffect(() => {
36 | document.title = "Demo";
37 | return () => {
38 | queryClient.removeQueries();
39 | document.title = "Finaki";
40 | };
41 | // eslint-disable-next-line react-hooks/exhaustive-deps
42 | }, []);
43 |
44 | return (
45 | <>
46 |
47 | Sekarang ini anda sedang dalam akun Demo
48 |
52 | Daftar Sekarang
53 |
54 | Untuk menggunakan seluruh fitur
55 |
56 |
57 |
58 |
59 |
60 | {children}
61 |
62 |
63 |
64 |
65 |
66 |
67 | >
68 | );
69 | };
70 |
71 | export default AppLayout;
72 |
--------------------------------------------------------------------------------
/src/app/demo/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Ratio from "../../../components/Dashboard/Ratio";
4 | import RecentTransactions from "../../../components/Dashboard/RecentTrasactions";
5 | import TransactionActivity from "../../../components/Dashboard/TransactionActivity";
6 | import WalletPercentage from "../../../components/Dashboard/WalletPercentage";
7 | import { QueryKey } from "@/types/QueryKey";
8 | import { TotalTransactionByDay, Transaction } from "@/types/Transaction";
9 | import { WalletData } from "@/types/Wallet";
10 | import { useQuery } from "@tanstack/react-query";
11 |
12 | const Page = () => {
13 | const totalTransactionQuery = useQuery({
14 | queryKey: [QueryKey.TOTAL_TRANSACTIONS],
15 | queryFn: (): Promise =>
16 | new Promise((resolve) => {
17 | import("@/data/dummy-data/dashboard.json").then((data) => {
18 | resolve(data.data);
19 | });
20 | }),
21 | });
22 |
23 | const recentTransactionsQuery = useQuery({
24 | queryKey: [QueryKey.RECENT_TRANSACTIONS],
25 | queryFn: (): Promise =>
26 | new Promise((resolve) => {
27 | import("@/data/dummy-data/transaction.json").then((data) => {
28 | resolve(data.data as unknown as Transaction[]);
29 | });
30 | }),
31 | });
32 |
33 | const walletQuery = useQuery({
34 | queryKey: [QueryKey.WALLETS],
35 | queryFn: (): Promise =>
36 | new Promise((resolve) => {
37 | import("@/data/dummy-data/wallet.json").then((data) => {
38 | resolve(data.data as WalletData[]);
39 | });
40 | }),
41 | });
42 |
43 | return (
44 |
45 |
49 |
53 |
54 |
58 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default Page;
69 |
--------------------------------------------------------------------------------
/src/components/Dashboard/RecentTrasactions.tsx:
--------------------------------------------------------------------------------
1 | import { Routes } from "@/types/Routes";
2 | import { Transaction } from "@/types/Transaction";
3 | import Link from "next/link";
4 | import { ChartError } from "../Charts/ChartPlaceholder";
5 | import {
6 | SimpleTransactionItem,
7 | SimpleTSkeleton,
8 | } from "../Transactions/TransactionItem";
9 | import DashboardContentWrapper from "./DashboardContentWrapper";
10 | import DashboardHeader from "./DashboardHeader";
11 |
12 | type Props = {
13 | data: Transaction[] | undefined;
14 | isLoading: boolean;
15 | isError: boolean;
16 | };
17 |
18 | const RecentTransactions = ({ data, isLoading, isError }: Props) => {
19 | if (isLoading || isError) {
20 | return (
21 |
22 |
23 |
24 | {isLoading &&
25 | Array(4)
26 | .fill("")
27 | .map((_, index) => (
28 |
29 | ))}
30 |
31 | {isError && }
32 |
33 |
34 | );
35 | }
36 |
37 | const slicedData = data ? data.slice(0, 4) : [];
38 | const lengthData = slicedData.length;
39 |
40 | return (
41 |
42 |
43 | {slicedData?.length > 0 && (
44 |
48 | Lihat semua
49 |
50 | )}
51 |
52 | {slicedData?.length > 0 ? (
53 |
54 | {slicedData.map((transaction, index) => {
55 | return (
56 |
61 | );
62 | })}
63 |
64 | ) : (
65 |
66 | Belum ada transaksi
67 |
68 | )}
69 |
70 | );
71 | };
72 |
73 | export default RecentTransactions;
74 |
--------------------------------------------------------------------------------
/src/utils/api/wallet.ts:
--------------------------------------------------------------------------------
1 | import { instance } from "./api";
2 | import { makeUrl } from "./config";
3 | import {
4 | AllWalletResponse,
5 | CreatedWalletResponse,
6 | DeleteWalletRequest,
7 | TransferBalance,
8 | UpdatedWalletColorResponse,
9 | UpdatedWalletResponse,
10 | UpdateWalletRequest,
11 | WalletDetailsResponse,
12 | WalletInput,
13 | } from "./types/WalletAPI";
14 |
15 | import { Transaction } from "@/types/Transaction";
16 | import { TransactionsResponse } from "./types/TransactionAPI";
17 |
18 | export const createNewWallet = async (data: WalletInput) => {
19 | const response = await instance.post("/wallets", data);
20 | return response.data.data;
21 | };
22 |
23 | export const getAllWallets = async () => {
24 | const response = await instance.get("/wallets");
25 |
26 | return response.data.data;
27 | };
28 |
29 | export const updateWallet = async ({ id, data }: UpdateWalletRequest) => {
30 | const response = await instance.put(
31 | `/wallets/${id}`,
32 | data
33 | );
34 | return response.data.data;
35 | };
36 |
37 | export const deleteWallet = async (data: DeleteWalletRequest) => {
38 | const response = await instance.delete(
39 | makeUrl(`/wallets/${data.param.id}`, data?.query)
40 | );
41 | return response.data.data;
42 | };
43 |
44 | export const getOneWallet = async (id: string) => {
45 | const response = await instance.get(`/wallets/${id}`);
46 | return response.data.data;
47 | };
48 |
49 | export const updateWalletColor = async ({
50 | id,
51 | color,
52 | }: Record) => {
53 | const response = await instance.patch(
54 | `/wallets/${id}/color`,
55 | { color }
56 | );
57 | return response.data.data;
58 | };
59 |
60 | export const transferBalance = async (data: TransferBalance) => {
61 | const response = await instance.post(`/wallets/transfer-balance`, data);
62 | return response.data.data;
63 | };
64 |
65 | export const getWalletTransactions = async (
66 | id: string
67 | ): Promise => {
68 | const response = await instance.get(
69 | `/wallets/${id}/transactions`
70 | );
71 | return response.data.data;
72 | };
73 |
74 | export const reoderWallets = async (params: { walletIds: string[] }) => {
75 | const data = {
76 | walletIds: JSON.stringify(params.walletIds),
77 | };
78 | const response = await instance.post(`/wallets/reorder`, data);
79 | return response.data;
80 | };
81 |
82 | // export const getWalletBalanceActivity = async (walletId: string) => {
83 | // const response = await instance.get(
84 | // `/wallets/${walletId}/balance-activity`
85 | // );
86 | // return response.data.data;
87 | // };
88 |
--------------------------------------------------------------------------------
/src/components/Account/DeviceLists/index.tsx:
--------------------------------------------------------------------------------
1 | import { getUserDevices, logoutDevices } from "@/api/user";
2 | import ContentWrapper from "../../Container/ContentWrapper";
3 | import LoadingButton from "../../dls/Button/LoadingButton";
4 | import Heading from "../../dls/Heading";
5 | import { QueryKey } from "@/types/QueryKey";
6 | import { Routes } from "@/types/Routes";
7 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
8 | import { useRouter } from "next/navigation";
9 | import { toast } from "react-hot-toast";
10 | import useStore from "../../../stores/store";
11 | import Device from "./Device";
12 |
13 | const DevicesLists = () => {
14 | const setUser = useStore((state) => state.setUser);
15 | const router = useRouter();
16 | const queryClient = useQueryClient();
17 |
18 | const { data, isLoading } = useQuery({
19 | queryKey: ["devices"],
20 | queryFn: getUserDevices,
21 | });
22 |
23 | const deleteDeviceMutation = useMutation({
24 | mutationFn: logoutDevices,
25 | onSuccess: () => {
26 | setUser(null);
27 | router.push(Routes.Home);
28 | localStorage.removeItem("access-token");
29 | queryClient.invalidateQueries();
30 | queryClient.removeQueries([QueryKey.USER]);
31 | toast.success("Berhasil keluar dari semua perangkat");
32 | },
33 | });
34 |
35 | const handleDeleteAllDevices = () => {
36 | const ids = data?.map((device) => device._id);
37 | if (!ids || ids!.length === 0) return;
38 | deleteDeviceMutation.mutate(ids);
39 | };
40 |
41 | if (isLoading) return <>>;
42 | if (!data) return <>>;
43 | const sortedData = data?.sort((a, b) => {
44 | if (a.isCurrent) return -1;
45 | if (b.isCurrent) return 1;
46 | return 0;
47 | });
48 | return (
49 |
50 |
51 |
52 | Daftar Perangkat
53 |
54 |
63 |
64 |
65 | {sortedData.map((device, index) => (
66 |
74 | ))}
75 |
76 |
77 | );
78 | };
79 |
80 | export default DevicesLists;
81 |
--------------------------------------------------------------------------------
/src/app/demo/wallet/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Placeholder from "../../../components/Placeholder";
4 | import AddNewWallet from "../../../components/WalletCard/AddNewWallet";
5 | import WalletCard from "../../../components/WalletCard/WalletCard";
6 | import WalletCardSkeleton from "../../../components/WalletCard/WalletCardSkeleton";
7 | import { QueryKey } from "@/types/QueryKey";
8 | import { WalletData } from "@/types/Wallet";
9 | import { currencyFormat } from "@/utils/currencyFormat";
10 | import { useQuery } from "@tanstack/react-query";
11 |
12 | const AllWalletsPage = () => {
13 | const { data, isLoading } = useQuery({
14 | queryKey: [QueryKey.WALLETS],
15 | queryFn: (): Promise =>
16 | new Promise((resolve) => {
17 | import("@/data/dummy-data/wallet.json").then((data) => {
18 | resolve(data.data as WalletData[]);
19 | });
20 | }),
21 | });
22 |
23 | if (isLoading || !data) {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | Total saldo
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | const totalBalance = data.reduce(
46 | (acc: number, curr: any) => acc + curr.balance,
47 | 0
48 | );
49 |
50 | return (
51 |
52 |
53 |
54 |
55 | Total saldo
56 |
57 | {currencyFormat(totalBalance)}
58 |
59 |
60 |
61 |
62 | {data.map((wallet: any) => (
63 |
71 | ))}
72 |
73 |
74 | );
75 | };
76 |
77 | export default AllWalletsPage;
78 |
--------------------------------------------------------------------------------
/src/components/Charts/AreaChart/AreaChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import classNames from "classnames";
4 | import {
5 | AreaChart as ArChart,
6 | Area,
7 | CartesianGrid,
8 | Tooltip,
9 | XAxis,
10 | } from "recharts";
11 | import { ChartError, ChartLoading } from "../ChartPlaceholder";
12 | import ChartWrapper from "../ChartWrapper";
13 | import renderAreaTooltip from "./AreaTooltip";
14 | import { stopColor } from "./constants";
15 |
16 | export type AreaChartData = {
17 | day?: string;
18 | timestamp: string;
19 | value: number;
20 | };
21 |
22 | type Props = {
23 | chartName?: string;
24 | data: AreaChartData[];
25 | size?: "medium" | "large";
26 | theme?: "default" | "transparent";
27 | xAxis?: boolean;
28 | horizonalLines?: boolean;
29 | loading?: boolean;
30 | };
31 |
32 | const AreaChart = ({
33 | data,
34 | size = "large",
35 | theme = "default",
36 | xAxis = true,
37 | horizonalLines = true,
38 | loading,
39 | }: Props) => {
40 | return (
41 |
47 | {loading ? (
48 |
49 | ) : data ? (
50 |
51 |
52 |
53 |
58 |
63 |
64 |
65 |
74 |
81 |
82 |
91 |
92 | ) : (
93 |
94 | )}
95 |
96 | );
97 | };
98 |
99 | export default AreaChart;
100 |
--------------------------------------------------------------------------------
/src/components/Account/Profile.tsx:
--------------------------------------------------------------------------------
1 | import Heading from "../dls/Heading";
2 | import IconWrapper from "../dls/IconWrapper";
3 | import ClipboardIcon from "../icons/ClipboardIcon";
4 | import { toast } from "react-hot-toast";
5 | import useStore from "../../stores/store";
6 | import ContentWrapper from "../Container/ContentWrapper";
7 | import Link from "next/link";
8 |
9 | type Props = {
10 | className?: string;
11 | };
12 |
13 | const Profile = (props: Props) => {
14 | const { user } = useStore((state) => ({ user: state.user }));
15 |
16 | const copyTokenToClipboard = () => {
17 | navigator.clipboard.writeText(user?.token || "");
18 | toast.success("Token berhasil disalin");
19 | };
20 |
21 | return (
22 |
23 |
24 | Informasi tentang akun
25 |
26 |
27 |
28 | Nama
29 | {user?.name}
30 |
31 |
32 | Email
33 | {user?.email}
34 |
35 |
36 |
37 | Token
38 |
39 |
40 |
41 | {user?.token}
42 |
46 |
47 |
48 |
49 |
50 |
54 | Hubungkan dengan Telegram
55 |
56 |
57 |
58 |
59 | {user?.telegramAccount?.username && (
60 | <>
61 |
62 |
63 | Akun Telegram yang terhubung
64 |
65 |
66 | Username
67 |
68 | @{user?.telegramAccount?.username}
69 |
70 |
71 |
72 | Nama
73 |
74 | {user?.telegramAccount?.firstName}
75 |
76 |
77 | >
78 | )}
79 |
80 |
81 | );
82 | };
83 |
84 | export default Profile;
85 |
--------------------------------------------------------------------------------
/src/components/dls/Dropdown/DropdownItem.tsx:
--------------------------------------------------------------------------------
1 | import CheckIcon from "../../icons/CheckIcon";
2 | import {
3 | CheckboxItem,
4 | Item,
5 | ItemIndicator,
6 | SubTrigger,
7 | } from "@radix-ui/react-dropdown-menu";
8 | import classNames from "classnames";
9 |
10 | import styles from "./DropdownItem.module.scss";
11 | import React from "react";
12 |
13 | interface ItemProps extends React.ComponentProps {
14 | indicator?: React.ReactNode;
15 | icon?: React.ReactNode;
16 | shouldCloseAfterClick?: boolean;
17 | }
18 |
19 | interface CheckboxItemProps extends React.ComponentProps {
20 | children: React.ReactNode;
21 | className?: string;
22 | shouldCloseAfterClick?: boolean;
23 | }
24 |
25 | interface SubTriggerProps extends React.ComponentProps {
26 | indicator?: React.ReactNode;
27 | }
28 |
29 | const DropdownItem = ({
30 | children,
31 | className,
32 | icon,
33 | shouldCloseAfterClick = false,
34 | onClick,
35 | ...props
36 | }: ItemProps) => {
37 | return (
38 | - {
40 | // by default Dropdown menu will close after clicking on an item
41 | // but if we want to keep it open, we can pass shouldCloseAfterClick = false
42 | if (!shouldCloseAfterClick) {
43 | e.preventDefault();
44 | }
45 | if (onClick) {
46 | onClick(e);
47 | }
48 | }}
49 | className={classNames(
50 | styles.dropdownItem,
51 | "py-1 px-2 rounded hover:bg-blue-100 dark:hover:bg-blue-500/50 relative cursor-default",
52 | { "pl-7": !!icon },
53 | className
54 | )}
55 | {...props}
56 | >
57 | {icon &&
{icon}
}
58 | {children}
59 |
60 | );
61 | };
62 |
63 | const DropdownCheckboxItem = ({
64 | children,
65 | className,
66 | shouldCloseAfterClick = false,
67 | onClick,
68 | ...props
69 | }: CheckboxItemProps) => {
70 | return (
71 | {
73 | // by default Dropdown menu will close after clicking on an item
74 | // but if we want to keep it open, we can pass shouldCloseAfterClick = false
75 | if (!shouldCloseAfterClick) {
76 | e.preventDefault();
77 | }
78 | if (onClick) {
79 | onClick(e);
80 | }
81 | }}
82 | className={classNames(styles.dropdownItem, className)}
83 | {...props}
84 | >
85 |
86 |
87 |
88 | {children}
89 |
90 | );
91 | };
92 |
93 | const DropdownSubTrigger = ({
94 | children,
95 | className,
96 | indicator,
97 | ...props
98 | }: SubTriggerProps) => {
99 | return (
100 |
104 | {indicator && (
105 |
106 | {indicator}
107 |
108 | )}
109 | {children}
110 |
111 | );
112 | };
113 |
114 | export { DropdownItem, DropdownSubTrigger, DropdownCheckboxItem };
115 |
--------------------------------------------------------------------------------
/src/components/Account/DeviceLists/Device.tsx:
--------------------------------------------------------------------------------
1 | import { logoutDevices } from "@/api/user";
2 | import LoadingButton from "../../dls/Button/LoadingButton";
3 | import DesktopIcon from "../../icons/DesktopIcon";
4 | import PhoneIcon from "../../icons/PhoneIcon";
5 | import { useMutation, useQueryClient } from "@tanstack/react-query";
6 | import classNames from "classnames";
7 | import { toast } from "react-hot-toast";
8 | import parser from "ua-parser-js";
9 |
10 | type Props = {
11 | _id: string;
12 | userAgent: string;
13 | createdAt: string;
14 | isCurrent: boolean;
15 | isLastItem: boolean;
16 | };
17 |
18 | const Device = ({
19 | _id,
20 | userAgent,
21 | createdAt,
22 | isCurrent,
23 | isLastItem,
24 | }: Props) => {
25 | const queryClient = useQueryClient();
26 |
27 | const ua = parser(userAgent);
28 | const date = new Date(createdAt).toLocaleDateString("id-ID", {
29 | day: "numeric",
30 | month: "short",
31 | year: "numeric",
32 | });
33 |
34 | const { isLoading, mutate } = useMutation({
35 | mutationFn: logoutDevices,
36 | onSuccess: () => {
37 | queryClient.setQueryData(["devices"], (oldData: any) => {
38 | return oldData.filter((device: any) => device._id !== _id);
39 | });
40 | toast.success(
41 | `Perangkat ${ua.device.model} berhasil dihapus, proses ini setidaknya membutuhkan waktu 15 menit`
42 | );
43 | },
44 | });
45 |
46 | const handleDeleteDevice = (id: string) => {
47 | mutate([id]);
48 | };
49 |
50 | return (
51 |
59 |
60 |
61 | {ua.device.type === "mobile" ? (
62 |
63 | ) : (
64 |
65 | )}
66 |
67 |
68 |
69 | {ua.device.model || ua.os.name}
70 | {isCurrent && (
71 |
72 | Sekarang
73 |
74 | )}
75 |
76 | {ua.browser.name}
77 | Login pada : {date}
78 |
79 |
80 | {!isCurrent && (
81 |
handleDeleteDevice(_id)}
83 | styleButton="danger"
84 | className={classNames(
85 | "!font-bold !px-3 !py-2 lg:invisible lg:group-hover:visible",
86 | { "lg:!visible": isLoading }
87 | )}
88 | width="fit"
89 | title="Hapus"
90 | isLoading={isLoading}
91 | onLoadingText="Menghapus"
92 | background={false}
93 | />
94 | )}
95 |
96 | );
97 | };
98 |
99 | export default Device;
100 |
--------------------------------------------------------------------------------
/src/app/globals.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | /* create custom style for Tooltop recharts component */
7 | // .recharts-tooltip-wrapper{
8 | // outline: none;
9 | // padding: 2px;
10 | // background-color: #fff;
11 | // border-radius: 10px;
12 | // .recharts-default-tooltip{
13 | // border-radius: 10px !important;
14 | // border: 1px solid blue !important;
15 | // }
16 |
17 | // }
18 |
19 | *{
20 | outline: none;
21 | -webkit-tap-highlight-color: rgba(0,0,0,0);
22 | -webkit-tap-highlight-color: transparent;
23 | }
24 |
25 | // Remove the spin button from number input
26 | /* Chrome, Safari, Edge, Opera */
27 | input::-webkit-outer-spin-button,
28 | input::-webkit-inner-spin-button {
29 | -webkit-appearance: none;
30 | margin: 0;
31 | }
32 | /* Firefox */
33 | input[type=number] {
34 | -moz-appearance: textfield;
35 | }
36 |
37 | .recharts-cartesian-axis-tick text tspan {
38 | @apply dark:text-slate-400;
39 | }
40 |
41 | // this line bellow is for the InputWithLabel component, because the module.scss file is not working with the darkmode
42 |
43 | .input-with-label-wrapper{
44 | position: relative;
45 |
46 | label {
47 | @apply text-slate-500 dark:text-slate-300;
48 | position: absolute;
49 | left: 10px;
50 | top: 27px;
51 | transform: translateY(-50%);
52 | padding: 0 5px;
53 | transition: all 0.2s ease-in-out;
54 | }
55 |
56 |
57 | input, textarea{
58 | @apply bg-gray-100 text-slate-800 dark:text-slate-100 dark:bg-slate-500 ;
59 | transition: all 0.2s ease-in-out;
60 | &:focus + label {
61 | @apply bg-white dark:bg-slate-600;
62 | top: 0 !important;
63 | left: 20px;
64 | }
65 |
66 | &:not(:placeholder-shown) + label {
67 | @apply bg-white dark:bg-slate-600;
68 | top: 0 !important;
69 | left: 20px;
70 |
71 | }
72 |
73 | &::placeholder {
74 | color: transparent;
75 | }
76 |
77 | &:focus::placeholder {
78 | @apply text-gray-400;
79 | transition-delay: 100ms;
80 | }
81 |
82 | &:not(:placeholder-shown){
83 | @apply bg-transparent;
84 | @apply ring-1 ring-gray-300 invalid:ring-pink-400;
85 | }
86 |
87 |
88 | &:focus{
89 | @apply bg-transparent ring-2 ring-blue-400;
90 | }
91 |
92 | &:focus:not(:placeholder-shown){
93 | @apply ring-2 ring-blue-400 ;
94 | }
95 |
96 | &:not(:placeholder-shown) ~ .toggleHide {
97 | display: block;
98 | }
99 | }
100 |
101 | .toggleHide{
102 | @apply text-gray-500;
103 | display: none;
104 | position: absolute;
105 | right: 20px;
106 | top: 50%;
107 | transform: translateY(-50%);
108 | cursor: pointer;
109 | }
110 |
111 | }
112 |
113 | .hero-image{
114 | transform: scaleX(1) scaleY(1) scaleZ(1) rotateX(-15deg) rotateY(32deg) rotateZ(1deg) translateX(0px) translateY(0px) translateZ(0px) skewX(0deg) skewY(0deg);
115 | border-radius: 10px;
116 | overflow: hidden;
117 | }
118 |
119 | .hero-image:nth-child(2){
120 | transform: scaleX(1) scaleY(1) scaleZ(1) rotateX(-15deg) rotateY(32deg) rotateZ(1deg) translateX(0px) translateY(0px) translateZ(0px) skewX(0deg) skewY(0deg);
121 | border-radius: 10px;
122 | overflow: hidden;
123 | position: absolute;
124 | top: 100px;
125 | left: 80px;
126 |
127 | @media screen and (max-width: 768px) {
128 | top: 50px;
129 | left: 20px;
130 | }
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/src/components/WalletCard/WalletCard.tsx:
--------------------------------------------------------------------------------
1 | import { QueryKey } from "@/types/QueryKey";
2 | import { WalletData } from "@/types/Wallet";
3 | import { updateWalletColor } from "@/utils/api/wallet";
4 | import { currencyFormat } from "@/utils/currencyFormat";
5 | import { useMutation, useQueryClient } from "@tanstack/react-query";
6 | import classNames from "classnames";
7 | import Link from "next/link";
8 | import { useState } from "react";
9 | import { WalletColor, walletColors } from "./constants";
10 | import WalletCardDropdown from "./WalletCardDropdown";
11 |
12 | type Props = {
13 | id: string;
14 | isDefault?: boolean;
15 | initColorKey: WalletColor;
16 | name: string;
17 | balance: number;
18 | link?: string;
19 | isCredit: boolean;
20 | demoMode?: boolean;
21 | };
22 |
23 | const WalletCard = ({
24 | id,
25 | isDefault = true,
26 | initColorKey,
27 | name,
28 | balance,
29 | isCredit,
30 | demoMode = false,
31 | }: Props) => {
32 | const [colorKey, setColorKey] = useState(initColorKey);
33 |
34 | const queryClient = useQueryClient();
35 | const colorMutation = useMutation({
36 | mutationFn: updateWalletColor,
37 | onSuccess: (data) => {
38 | // update colorKey in cache
39 | queryClient.setQueryData([QueryKey.WALLETS], (oldData: any) => {
40 | return oldData.map((wallet: WalletData) => {
41 | if (wallet._id === id) {
42 | return { ...wallet, color: data.color };
43 | }
44 | return wallet;
45 | });
46 | });
47 |
48 | // update single wallet color in cache
49 | if (queryClient.getQueryData([QueryKey.WALLETS, id])) {
50 | queryClient.setQueryData([QueryKey.WALLETS, id], (oldData: any) => {
51 | return { ...oldData, color: data.color };
52 | });
53 | }
54 | },
55 | onError: () => {
56 | // reset colorKey to initColorKey
57 | setColorKey(initColorKey);
58 | },
59 | });
60 |
61 | const handleUpdateColor = () => {
62 | // if colorKey is not changed, do nothing
63 | if (colorKey === initColorKey) return;
64 | colorMutation.mutate({ id, color: colorKey });
65 | };
66 |
67 | return (
68 |
77 |
78 |
85 | {name}
86 |
87 |
93 |
94 |
95 |
96 |
Saldo {isCredit && "minus"}
97 |
98 | {isCredit && "-"}
99 | {currencyFormat(balance)}
100 |
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default WalletCard;
108 |
--------------------------------------------------------------------------------
/src/stores/store.ts:
--------------------------------------------------------------------------------
1 | import { Theme, ThemeState } from "@/types/Theme";
2 | import { User } from "@/types/User";
3 | import { create } from "zustand";
4 | import { persist } from "zustand/middleware";
5 |
6 | interface Store {
7 | user: User | null;
8 | colorTheme: ThemeState;
9 | accessToken: string | null;
10 | deleteWalletId: string | null;
11 | transactionDetailState: {
12 | transactionId: string | null;
13 | setTransactionId: (id: string | null) => void;
14 | };
15 | addTransactionState: {
16 | walletId: string | null;
17 | setWalletId: (id: string | null) => void;
18 | isOpen: boolean;
19 | setOpen: (isOpen: boolean) => void;
20 | };
21 | transferBalanceState: {
22 | sourceWalletId: string | null;
23 | setSourceWalletId: (id: string | null) => void;
24 | destinationWalletId: string | null;
25 | setDestinationWalletId: (id: string | null) => void;
26 | isOpen: boolean;
27 | setOpen: (isOpen: boolean) => void;
28 | };
29 | setDeleteWalletId: (id: string | null) => void;
30 | setToken: (token: string) => void;
31 | setUser: (user: User | null) => void;
32 | setColorTheme: (theme: ThemeState) => void;
33 | }
34 |
35 | const useStore = create()(
36 | persist(
37 | (set, get) => ({
38 | user: null,
39 | colorTheme: Theme.Light,
40 | accessToken: null,
41 | deleteWalletId: null,
42 | transactionDetailState: {
43 | transactionId: null,
44 | setTransactionId: (id: string | null) =>
45 | set({
46 | transactionDetailState: {
47 | ...get().transactionDetailState,
48 | transactionId: id,
49 | },
50 | }),
51 | },
52 | addTransactionState: {
53 | walletId: null,
54 | setWalletId: (id: string | null) =>
55 | set({
56 | addTransactionState: { ...get().addTransactionState, walletId: id },
57 | }),
58 | isOpen: false,
59 | setOpen: (isOpen: boolean) =>
60 | set({
61 | addTransactionState: {
62 | ...get().addTransactionState,
63 | isOpen: isOpen,
64 | walletId: isOpen ? get().addTransactionState.walletId : null,
65 | },
66 | }),
67 | },
68 | transferBalanceState: {
69 | sourceWalletId: null,
70 | setSourceWalletId: (id: string | null) =>
71 | set({
72 | transferBalanceState: {
73 | ...get().transferBalanceState,
74 | sourceWalletId: id,
75 | },
76 | }),
77 | destinationWalletId: null,
78 | setDestinationWalletId: (id: string | null) =>
79 | set({
80 | transferBalanceState: {
81 | ...get().transferBalanceState,
82 | destinationWalletId: id,
83 | },
84 | }),
85 | isOpen: false,
86 | setOpen: (isOpen: boolean) =>
87 | set({
88 | transferBalanceState: { ...get().transferBalanceState, isOpen },
89 | }),
90 | },
91 | setDeleteWalletId: (id: string | null) => set({ deleteWalletId: id }),
92 | setToken: (token: string) => set({ accessToken: token }),
93 | setUser: (user) => set({ user }),
94 | setColorTheme: (theme: ThemeState) => set({ colorTheme: theme }),
95 | }),
96 | {
97 | name: "user-data",
98 | partialize(state) {
99 | return {
100 | colorTheme: state.colorTheme,
101 | };
102 | },
103 | }
104 | )
105 | );
106 |
107 | export default useStore;
108 |
--------------------------------------------------------------------------------
/src/components/WalletCard/WalletOption.tsx:
--------------------------------------------------------------------------------
1 | import IconButton from "../dls/IconButton";
2 | import ArrowsIcon from "../icons/ArrowsIcon";
3 | import ElipsisVerticalIcon from "../icons/ElipsisVerticalIcon";
4 | import PlusIcon from "../icons/PlusIcon";
5 | import { useCallback } from "react";
6 | import useStore from "../../stores/store";
7 | import { WalletData } from "@/types/Wallet";
8 | import DropdownMenu from "../dls/Dropdown/DropdownMenu";
9 | import { DropdownItem } from "../dls/Dropdown/DropdownItem";
10 | import PencilIcon from "../icons/PencilIcon";
11 | import TrashIcon from "../icons/TrashIcon";
12 | import XmarkIcon from "../icons/XmarkIcon";
13 | import CheckIcon from "../icons/CheckIcon";
14 |
15 | type Props = {
16 | walletData: WalletData;
17 | state: any;
18 | };
19 |
20 | const WalletOption = ({ walletData, state }: Props) => {
21 | const { setSourceWalletId, setOpen, setDeleteWalletId, setWalletId } =
22 | useStore((state) => ({
23 | setSourceWalletId: state.transferBalanceState.setSourceWalletId,
24 | setOpen: state.transferBalanceState.setOpen,
25 | setDeleteWalletId: state.setDeleteWalletId,
26 | setWalletId: state.addTransactionState.setWalletId,
27 | }));
28 |
29 | const handleTransferBalance = useCallback(() => {
30 | setOpen(true);
31 | setSourceWalletId(walletData._id);
32 | // eslint-disable-next-line react-hooks/exhaustive-deps
33 | }, []);
34 |
35 | if (state.edit) {
36 | return (
37 |
38 | state.setEdit(false)}
40 | shape="circle"
41 | className="bg-white/40 text-slate-50 !p-2"
42 | >
43 |
44 |
45 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | return (
58 |
59 |
setWalletId(walletData._id)}
61 | shape="circle"
62 | className="bg-white/40 text-slate-50 !p-2"
63 | >
64 |
65 |
66 |
71 |
72 |
73 |
78 |
82 |
83 | }
84 | >
85 | state.setEdit(true)}
88 | icon={ }
89 | >
90 | Edit Dompet
91 |
92 | }
96 | onClick={() => setDeleteWalletId(walletData._id)}
97 | >
98 | Hapus dompet
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default WalletOption;
106 |
--------------------------------------------------------------------------------