Skip to content

Commit

Permalink
feat: add payment pages
Browse files Browse the repository at this point in the history
  • Loading branch information
conganhhcmus committed Mar 15, 2024
1 parent f627cc5 commit 54f4ae0
Show file tree
Hide file tree
Showing 27 changed files with 633 additions and 45 deletions.
Binary file added public/100k.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/20k.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/50k.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions public/language/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,12 @@
"show-less": "Show less",
"total-views": "Lượt xem",
"total-followers": "Theo dõi",
"deposit-account": "Nạp tiền vào tài khoản",
"completed": "Đã hoàn thành",
"amount": "Số tiền",
"type": "Loại",
"pending": "Đang xử lý",
"success": "Thành công",
"accept":"Chấp nhận",
"description_0": "Web đọc truyện tranh online lớn nhất được cập nhật liên tục mỗi ngày - Cùng tham gia đọc truyện và thảo luận với hơn 10 triệu thành viên 🎉 tại YouthBook ❤️💛💚"
}
28 changes: 28 additions & 0 deletions src/apis/payment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import apiClients from '@/configs/apiClients';
import { PAYMENT_PATH } from '@/constants/path';
import { TransactionData } from '@/types/payment';
import { paramOption } from '@/types/request';

const paymentApis = {
deposit(amount: number) {
const url = PAYMENT_PATH.deposit;
return apiClients.post(url, { amount });
},
getAllTransactionByUser(userId: string | undefined, option: number, status: number[], params?: paramOption) {
if (!userId) return;
const url = PAYMENT_PATH.transaction + `/${userId}`;
return apiClients.get<TransactionData>(url, { params: { ...params, option, status } });
},

getAllTransaction(option: number, status: number[], params?: paramOption) {
const url = PAYMENT_PATH.transaction;
return apiClients.get<TransactionData>(url, { params: { ...params, option, status } });
},

updateTransaction(id: string, status: number) {
const url = PAYMENT_PATH.transaction + `/${id}`;
return apiClients.post(url, { status });
},
};

export default paymentApis;
2 changes: 1 addition & 1 deletion src/components/Comics/ListChapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const ListChapter = ({ data }: Props) => {
<div className="my-5 grid grid-cols-2 flex-wrap gap-5 text-sm font-semibold text-gray-800 dark:text-gray-200 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4">
{dataChapter.map((item) => (
<Link
to={`/${APP_PATH.comics_chapters}/${item._id}`}
to={`${APP_PATH.comics_chapters}/${item._id}`}
title={item.name}
key={item._id}
className="h-[38px] truncate rounded-sm bg-[#f6f6f6] px-4 pt-2 text-base font-normal hover:bg-primary/10 hover:text-primary dark:bg-gray-800 dark:hover:bg-primary/20">
Expand Down
10 changes: 4 additions & 6 deletions src/components/Header/AccountInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useTranslation from '@/hooks/useTranslation';
import { selectLanguage } from '@/redux/slices/settings';
import { UserJwtPayload } from '@/types/auth';
import { removeCookie } from '@/utils/cookies';
import { formatCurrency } from '@/utils/format';
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';

Expand Down Expand Up @@ -52,14 +53,11 @@ const AccountInfo = ({ userInfo }: AccountInfoProps) => {
<div className="absolute top-10 z-50 w-36 bg-transparent py-2">
<div className="items-left flex flex-col justify-center border bg-white p-1 text-black shadow-lg lg:p-2">
<div className="flex flex-col items-center justify-center gap-1">
<p className="flex items-center justify-center gap-x-1 font-bold text-primary">
{userInfo.wallet ?? 0}
<span className="font-normal text-black">pt</span>
</p>
<p className="flex items-center justify-center gap-x-1 font-bold text-primary">{formatCurrency(userInfo.wallet)}</p>
<Link
className="flex w-[80%] items-center justify-center rounded-lg border bg-gradient px-2 text-white"
title={translate('deposit')}
to={APP_PATH.comics_wishlist}>
to={APP_PATH.payment_deposit}>
{translate('deposit')}
</Link>
</div>
Expand Down Expand Up @@ -116,7 +114,7 @@ const AccountInfo = ({ userInfo }: AccountInfoProps) => {
)}
<Link
title={translate('wish-list')}
to={APP_PATH.comics_wishlist}
to={APP_PATH.account_wishlist}
className="flex min-w-[100px] items-center justify-start gap-2 px-2 py-1 capitalize hover:bg-[rgba(0,0,0,0.05)] active:scale-90 dark:hover:bg-[rgba(255,255,255,0.1)]">
<svg
className="mt-1 h-4 w-4 fill-gray-600"
Expand Down
26 changes: 17 additions & 9 deletions src/constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,30 @@ export const USERS_PATH = {
users: '/users',
} as const;

export const PAYMENT_PATH = {
deposit: '/payment/deposit',
transaction: '/transaction',
};

export const APP_PATH = {
// comics
comics: '/comics',
comics_genres: 'comics/genres',
comics_wishlist: 'comics/wishlist',
comics_history: 'comics/history',
comics_chapters: 'comics/chapters',
comics_search: 'comics/search',
comics_new: 'comics/new',
comics_top: 'comics/top',
comics_recommend: 'comics/recommend',
comics_recent: 'comics/recent',
comics_genres: '/comics/genres',
comics_history: '/comics/history',
comics_chapters: '/comics/chapters',
comics_search: '/comics/search',
comics_new: '/comics/new',
comics_top: '/comics/top',
comics_recommend: '/comics/recommend',
comics_recent: '/comics/recent',

// account
account: '/account',
account_info: '/account/info',
account_history: '/account/history',
account_billing: '/account/billing',
account_password: '/account/password',
account_wishlist: '/account/wishlist',

// app
language: '/language',
Expand All @@ -62,6 +67,9 @@ export const APP_PATH = {
management_billing: '/management/billing',
management_chapters: '/management/chapters',
management_genres: '/management/genres',

// payment
payment_deposit: '/payment/deposit',
} as const;

export const TOP_COMICS = {
Expand Down
43 changes: 43 additions & 0 deletions src/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,46 @@ export const COMIC_TYPES_LIST = [
name: 'picture',
},
];

export const DEPOSIT_TYPE = [
{
value: 20 * 1000,
name: '20.000',
img: '/20k.jpg',
},
{
value: 50 * 1000,
name: '50.000',
img: '/50k.jpg',
},
{
value: 100 * 1000,
name: '100.000',
img: '/100k.jpg',
},
];

export const FILTER_OPTIONS = [
{
name: 'All',
value: 0,
},
{
name: '1 Year',
value: 12,
},
{
name: '3 Months',
value: 3,
},
{
name: '1 Month',
value: 1,
},
];

export const STATUS_OPTIONS = [
{ name: 'pending', value: 0 },
{ name: 'success', value: 1 },
{ name: 'cancel', value: -1 },
];
16 changes: 15 additions & 1 deletion src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@ import { Footer } from '@/components';
import { Header, Navbar } from '@/components/Header';
import ScrollToTop from '@/components/ScrollTop';
import { APP_PATH } from '@/constants/path';
import { COOKIE_KEYS } from '@/constants/settings';
import { getCookie } from '@/utils/cookies';
import { decodeJWTToken } from '@/utils/token';
import classNames from 'classnames';
import { Outlet, useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';

const MainLayout = () => {
const location = useLocation();
const navigate = useNavigate();
const token = getCookie(COOKIE_KEYS.token);
const userInfoPayload = decodeJWTToken(token);

useEffect(() => {
if (!userInfoPayload) {
navigate(APP_PATH.home);
}
}, [navigate, userInfoPayload]);

return (
<div className="grid h-full min-h-screen grid-cols-1 place-content-between">
<header
Expand Down
9 changes: 0 additions & 9 deletions src/pages/Account/BillingHistory.tsx

This file was deleted.

166 changes: 166 additions & 0 deletions src/pages/Account/PaymentHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import paymentApis from '@/apis/payment';
import { APP_PATH } from '@/constants/path';
import { COOKIE_KEYS, FILTER_OPTIONS, STATUS_OPTIONS } from '@/constants/settings';
import { useAppSelector } from '@/hooks/reduxHook';
import useRequestParams from '@/hooks/useRequestParams';
import useTranslation from '@/hooks/useTranslation';
import { selectLanguage } from '@/redux/slices/settings';
import { getCookie } from '@/utils/cookies';
import { formatCurrency } from '@/utils/format';
import { decodeJWTToken } from '@/utils/token';
import { getTransactionStatusName, getTransactionTypeName } from '@/utils/transaction';
import classNames from 'classnames';
import moment from 'moment';
import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useNavigate } from 'react-router-dom';

const PaymentHistory: React.FC = () => {
const lang = useAppSelector((state) => selectLanguage(state.settings));
const translate = useTranslation(lang);
const { queryParams } = useRequestParams();
const navigate = useNavigate();

const [filterOptions, setFilterOptions] = useState<number>(0);
const [statusOptions, setStatusOptions] = useState<number[]>(STATUS_OPTIONS.map((x) => x.value));

const token = getCookie(COOKIE_KEYS.token);
const userInfoPayload = decodeJWTToken(token);

useEffect(() => {
if (!userInfoPayload) {
navigate(APP_PATH.home);
}
}, [navigate, userInfoPayload]);

const { data: transactionDataResult } = useQuery({
queryKey: ['getAllTransactionByUser', queryParams, filterOptions, statusOptions],
queryFn: () => paymentApis.getAllTransactionByUser(userInfoPayload?._id, filterOptions, statusOptions, queryParams),
staleTime: 60 * 1000,
enabled: !!userInfoPayload,
});

const resultData = transactionDataResult?.data;
const transactionList = resultData?.data;

const onStatusChange = (event: React.MouseEvent<HTMLInputElement>, value: number) => {
let temp = [...statusOptions, value];

if (!event.currentTarget.checked) {
temp = temp.filter((v) => v !== value);
}

const uniqueValue = temp.filter(function (item, pos) {
return temp.indexOf(item) == pos;
});

setStatusOptions(uniqueValue);
};

return (
<div className="relative h-full w-full overflow-x-auto border-2 p-8 sm:rounded-lg">
<div className="flex-column flex flex-wrap items-center justify-between space-y-4 bg-white pb-4 dark:bg-gray-700 md:flex-row md:space-y-0">
<div className="inline-flex items-center rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-gray-500 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:focus:ring-gray-700">
{FILTER_OPTIONS.map((option) => (
<>
<div
key={option.value}
className="mb-[0.125rem] mr-8 inline-block min-h-[1.5rem] pl-[1.5rem]">
<input
className="relative float-left -ml-[1.5rem] mr-1 mt-0.5 h-5 w-5 appearance-none rounded-full border-2 border-solid border-neutral-300 before:pointer-events-none before:absolute before:h-4 before:w-4 before:scale-0 before:rounded-full before:bg-transparent before:opacity-0 before:shadow-[0px_0px_0px_13px_transparent] before:content-[''] after:absolute after:z-[1] after:block after:h-4 after:w-4 after:rounded-full after:content-[''] checked:border-primary checked:before:opacity-[0.16] checked:after:absolute checked:after:left-1/2 checked:after:top-1/2 checked:after:h-[0.625rem] checked:after:w-[0.625rem] checked:after:rounded-full checked:after:border-primary checked:after:bg-primary checked:after:content-[''] checked:after:[transform:translate(-50%,-50%)] hover:cursor-pointer hover:before:opacity-[0.04] hover:before:shadow-[0px_0px_0px_13px_rgba(0,0,0,0.6)] focus:shadow-none focus:outline-none focus:ring-0 focus:before:scale-100 focus:before:opacity-[0.12] focus:before:shadow-[0px_0px_0px_13px_rgba(0,0,0,0.6)] focus:before:transition-[box-shadow_0.2s,transform_0.2s] checked:focus:border-primary checked:focus:before:scale-100 checked:focus:before:shadow-[0px_0px_0px_13px_#3b71ca] checked:focus:before:transition-[box-shadow_0.2s,transform_0.2s] dark:border-neutral-600 dark:checked:border-primary dark:checked:after:border-primary dark:checked:after:bg-primary dark:focus:before:shadow-[0px_0px_0px_13px_rgba(255,255,255,0.4)] dark:checked:focus:border-primary dark:checked:focus:before:shadow-[0px_0px_0px_13px_#3b71ca]"
type="radio"
name="inlineRadioOptions"
id={option.name}
value={option.value}
checked={option.value == filterOptions}
onChange={() => setFilterOptions(option.value)}
/>
<label
className="mt-px inline-block pl-[0.15rem] hover:cursor-pointer"
htmlFor={option.name}>
{translate(option.name)}
</label>
</div>
</>
))}
</div>
<div className="inline-flex items-center rounded-lg bg-white px-3 py-1.5 text-sm font-medium text-gray-500 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:focus:ring-gray-700">
{STATUS_OPTIONS.map((option) => (
<>
<input
onClick={(e) => onStatusChange(e, option.value)}
id={option.name}
type="checkbox"
checked={statusOptions.includes(option.value)}
className="focus:ring-3 h-4 w-4 rounded border border-gray-300 bg-gray-50 focus:ring-primary dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-primary"
/>
<label
htmlFor={option.name}
className="w-30 mb-2 ml-2 mr-6 mt-2 text-sm font-medium capitalize text-gray-900 dark:text-white">
{translate(option.name)}
</label>
</>
))}
</div>
</div>
<div className="relative h-96 w-full overflow-y-auto sm:rounded-lg">
<table className="w-full text-left text-sm text-gray-500 dark:text-gray-400 rtl:text-right">
<thead className="sticky top-0 bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th
scope="col"
className="px-6 py-3">
{translate('type')}
</th>
<th
scope="col"
className="px-6 py-3">
{translate('amount')}
</th>
<th
scope="col"
className="px-6 py-3">
{translate('status')}
</th>
<th
scope="col"
className="px-6 py-3">
{translate('create-at')}
</th>
<th
scope="col"
className="px-6 py-3">
{translate('update-at')}
</th>
</tr>
</thead>
<tbody>
{transactionList &&
transactionList.map((transaction) => (
<tr
key={transaction._id}
className="border-b bg-white hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-600">
<td className="px-6 py-4">{translate(getTransactionTypeName(transaction.type))}</td>
<td className="px-6 py-4 font-bold text-primary">{formatCurrency(transaction.amount)}</td>
<td className="px-6 py-4">
<p
className={classNames('capitalize', {
'text-gray-700': transaction.status == 0,
'text-red-700': transaction.status == -1,
'text-primary': transaction.status == 1,
})}>
{translate(getTransactionStatusName(transaction.status))}
</p>
</td>
<td className="px-6 py-4">{moment(transaction.createTime).format('hh:mm:ss DD/MM/YYYY')}</td>
<td className="px-6 py-4">{moment(transaction.updateTime).format('hh:mm:ss DD/MM/YYYY')}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};

export default PaymentHistory;
Loading

0 comments on commit 54f4ae0

Please sign in to comment.