Skip to content

Commit

Permalink
feat: pusher implement
Browse files Browse the repository at this point in the history
  • Loading branch information
TzuHanLiang committed Oct 9, 2024
1 parent 5d0e291 commit 25a707f
Show file tree
Hide file tree
Showing 18 changed files with 491 additions and 230 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iSunFA",
"version": "0.8.2+34",
"version": "0.8.2+35",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down
8 changes: 7 additions & 1 deletion src/components/certificate/certificate_qrcode_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const CertificateQRCodeModal: React.FC<CertificateQRCodeModalProps> = ({
<div className="text-xl font-semibold">Url</div>
<div className="text-xs font-normal text-card-text-sub">for mobile upload</div>
</h2>
<div className="mx-20 my-10">
<div className="mx-20 my-10 flex flex-col items-center">
{/* Info: (20240924 - tzuhan) 發票縮略圖 */}
<Canvas
text={`${isDev ? 'http://192.168.2.29:3000' : DOMAIN}/${ISUNFA_ROUTE.UPLOAD}?token=${token}`}
Expand All @@ -55,6 +55,12 @@ const CertificateQRCodeModal: React.FC<CertificateQRCodeModalProps> = ({
},
}}
/>
<a
className="mt-2 text-center text-xs text-card-text-sub"
href={`${isDev ? 'http://localhost:3000' : DOMAIN}/${ISUNFA_ROUTE.UPLOAD}?token=${token}`}
target="_blank"
rel="noreferrer"
>{`${isDev ? 'http://localhost:3000' : DOMAIN}/${ISUNFA_ROUTE.UPLOAD}?token=${token}`}</a>
</div>
<div className="flex justify-end gap-2 border-t border-stroke-neutral-quaternary px-4 py-3">
<Button
Expand Down
43 changes: 27 additions & 16 deletions src/components/floating_upload_popup/floating_upload_popup.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import UploadFileItem from '@/components/upload_certificate/upload_file_item';
import { ProgressStatus } from '@/constants/account';
import Image from 'next/image';
Expand All @@ -10,20 +10,21 @@ interface FloatingUploadPopupProps {

const FloatingUploadPopup: React.FC<FloatingUploadPopupProps> = ({ uploadingCertificates }) => {
const [files, setFiles] = useState<ICertificateInfo[]>(uploadingCertificates);
// const [files, setFiles] = useState<UploadFile[]>([
// { name: 'preline-ui.xls', size: 7, progress: 20, status: ProgressStatus.IN_PROGRESS },
// { name: 'preline-ui.xls', size: 7, progress: 50, status: ProgressStatus.IN_PROGRESS },
// { name: 'preline-ui.xls', size: 7, progress: 80, status: ProgressStatus.IN_PROGRESS },
// ]);
const [expanded, setExpanded] = useState(false); // Info: (20240919 - tzuhan) 控制展開/收縮狀態
const [expanded, setExpanded] = useState(false);

// Info: (20240919 - tzuhan) 計算總上傳進度和狀態
const totalFiles = files.length;
const completedFiles = files.filter((file) => file.progress === 100).length;
const isUploading = files.some(
(file) => file.progress > 0 && file.progress < 100 && file.status !== ProgressStatus.PAUSED
// Info: (20241009 - tzuhan) Memoize calculated values to avoid redundant recalculations
const totalFiles = useMemo(() => uploadingCertificates.length, [files]);
const completedFiles = useMemo(
() => uploadingCertificates.filter((file) => file.status === ProgressStatus.SUCCESS).length,
[files]
);
const isUploading = useMemo(
() =>
files.some(
(file) => file.progress > 0 && file.progress < 100 && file.status !== ProgressStatus.PAUSED
),
[files]
);

// Info: (20240919 - tzuhan) 暫停或繼續上傳
const updateFileStatus = (prevFiles: ICertificateInfo[], index: number) =>
prevFiles.map((file, i) => {
Expand All @@ -48,13 +49,21 @@ const FloatingUploadPopup: React.FC<FloatingUploadPopupProps> = ({ uploadingCert
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
};

return (
const displayed =
totalFiles > 0 ||
uploadingCertificates.filter((file) => file.status === ProgressStatus.IN_PROGRESS).length > 0;

useEffect(() => {
setFiles(uploadingCertificates);
}, [uploadingCertificates]);

const popUpBody = displayed ? (
<div className="dashboardCardShadow fixed bottom-4 right-4 w-480px overflow-hidden">
{/* Info: (20240919 - tzuhan) Header: 顯示標題與收縮/展開按鈕 */}
<div className="flex items-center justify-between p-4">
<div className="flex-auto flex-col items-center text-center">
<div className="flex items-center justify-center space-x-2 text-lg font-semibold">
<Image src="/elements/cloud_upload.svg" width={24} height={24} alt="clock" />
<Image src="/elements/cloud_upload.svg" width={24} height={24} alt="Upload icon" />
<div>Upload file</div>
</div>
{totalFiles > 0 && (
Expand Down Expand Up @@ -94,7 +103,9 @@ const FloatingUploadPopup: React.FC<FloatingUploadPopupProps> = ({ uploadingCert
</div>
)}
</div>
);
) : null;

return popUpBody;
};

export default FloatingUploadPopup;
1 change: 1 addition & 0 deletions src/constants/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Info: (20240416 - Murky) type
export enum ProgressStatus {
SUCCESS = 'success',
FAILED = 'failed',
IN_PROGRESS = 'inProgress',
NOT_FOUND = 'notFound',
ALREADY_UPLOAD = 'alreadyUpload',
Expand Down
28 changes: 28 additions & 0 deletions src/constants/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export enum APIName {
STATUS_INFO_GET = 'STATUS_INFO_GET',
ACCOUNT_LIST = 'ACCOUNT_LIST',
FILE_UPLOAD = 'FILE_UPLOAD',
PUBLIC_FILE_UPLOAD = 'PUBLIC_FILE_UPLOAD',
FILE_DELETE = 'FILE_DELETE',
FILE_GET = 'FILE_GET',
COMPANY_GET_BY_ID = 'COMPANY_GET_BY_ID',
Expand All @@ -99,6 +100,9 @@ export enum APIName {
PUBLIC_KEY_GET = 'PUBLIC_KEY_GET',
ZOD_EXAMPLE = 'ZOD_EXAMPLE', // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data.
CERTIFICATE_LIST = 'CERTIFICATE_LIST',
PUSHER = 'PUSHER',
ENCRYPT = 'ENCRYPT',
DECRYPT = 'DECRYPT',
}

export enum APIPath {
Expand Down Expand Up @@ -156,6 +160,7 @@ export enum APIPath {
STATUS_INFO_GET = `${apiPrefix}/status_info`,
ACCOUNT_LIST = `${apiPrefix}/company/:companyId/account`,
FILE_UPLOAD = `${apiPrefix}/company/:companyId/file`,
PUBLIC_FILE_UPLOAD = `${apiPrefixV2}/upload`,
FILE_DELETE = `${apiPrefix}/company/:companyId/file/:fileId`,
FILE_GET = `${apiPrefix}/company/:companyId/file/:fileId`,
COMPANY_GET_BY_ID = `${apiPrefix}/company/:companyId`,
Expand All @@ -177,6 +182,9 @@ export enum APIPath {
PUBLIC_KEY_GET = `${apiPrefix}/company/:companyId/public_key`,
ZOD_EXAMPLE = `${apiPrefix}/company/zod`, // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data.
CERTIFICATE_LIST = `${apiPrefix}/company/:companyId/certificate`,
PUSHER = `${apiPrefixV2}/pusher`,
ENCRYPT = `${apiPrefixV2}/encrypt`,
DECRYPT = `${apiPrefixV2}/decrypt`,
}
const createConfig = ({
name,
Expand Down Expand Up @@ -418,6 +426,11 @@ export const APIConfig: Record<IAPIName, IAPIConfig> = {
method: HttpMethod.POST,
path: APIPath.FILE_UPLOAD,
}),
[APIName.PUBLIC_FILE_UPLOAD]: createConfig({
name: APIName.PUBLIC_FILE_UPLOAD,
method: HttpMethod.POST,
path: APIPath.PUBLIC_FILE_UPLOAD,
}),
[APIName.FILE_DELETE]: createConfig({
name: APIName.FILE_DELETE,
method: HttpMethod.DELETE,
Expand Down Expand Up @@ -518,4 +531,19 @@ export const APIConfig: Record<IAPIName, IAPIConfig> = {
method: HttpMethod.GET,
path: APIPath.CERTIFICATE_LIST,
}),
[APIName.PUSHER]: createConfig({
name: APIName.PUSHER,
method: HttpMethod.POST,
path: APIPath.PUSHER,
}),
[APIName.ENCRYPT]: createConfig({
name: APIName.ENCRYPT,
method: HttpMethod.POST,
path: APIPath.ENCRYPT,
}),
[APIName.DECRYPT]: createConfig({
name: APIName.DECRYPT,
method: HttpMethod.POST,
path: APIPath.DECRYPT,
}),
};
3 changes: 3 additions & 0 deletions src/constants/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum FileFolder {
INVOICE = 'invoice',
KYC = 'kyc',
TMP = 'tmp',
mobile_upload = 'mobile_upload',
}

export enum UploadType {
Expand All @@ -12,6 +13,7 @@ export enum UploadType {
USER = 'user',
PROJECT = 'project',
INVOICE = 'invoice',
MOBILE_UPLOAD = 'mobile_upload',
}

export enum FileDatabaseConnectionType {
Expand All @@ -31,6 +33,7 @@ export const UPLOAD_TYPE_TO_FOLDER_MAP = {
[UploadType.USER]: FileFolder.TMP,
[UploadType.PROJECT]: FileFolder.TMP,
[UploadType.INVOICE]: FileFolder.INVOICE,
[UploadType.MOBILE_UPLOAD]: FileFolder.mobile_upload,
};

export enum UploadDocumentType {
Expand Down
13 changes: 13 additions & 0 deletions src/constants/pusher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export enum PRIVATE_CHANNEL {
NOTIFICATION = 'private-notification',
CERTIFICATE = 'certificate', // TODO: (20241009 - tzuhan) update to 'private-certificate',
VOUCHER = 'private-voucher',
REPORT = 'private-report',
}

export enum CERTIFICATE_EVENT {
UPLOAD = 'certificate-upload',
UPDATE = 'certificate-update',
ANALYSIS = 'certificate-analysis',
DELETE = 'certificate-delete',
}
9 changes: 7 additions & 2 deletions src/interfaces/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type IAPIName =
| 'STATUS_INFO_GET'
| 'ACCOUNT_LIST'
| 'FILE_UPLOAD'
| 'PUBLIC_FILE_UPLOAD'
| 'FILE_DELETE'
| 'FILE_GET'
| 'COMPANY_GET_BY_ID'
Expand All @@ -64,7 +65,10 @@ export type IAPIName =
| 'GET_PROJECT_BY_ID'
| 'UPDATE_PROJECT_BY_ID'
| 'PUBLIC_KEY_GET'
| 'CERTIFICATE_LIST';
| 'CERTIFICATE_LIST'
| 'PUSHER'
| 'ENCRYPT'
| 'DECRYPT';

export type IHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';

Expand All @@ -75,7 +79,8 @@ export type IAPIInput = {
| FormData
| IVoucher
| IFinancialReportRequest
| ICompanyKYCForm;
| ICompanyKYCForm
| string;
params?: { [key: string]: unknown };
query?: { [key: string]: unknown };
};
Expand Down
10 changes: 3 additions & 7 deletions src/interfaces/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,14 @@ export interface ICertificate {
uploader: string;
}

export interface ICertificateData {
token: string;
url: string;
status: ProgressStatus;
}

export interface ICertificateInfo {
url: string;
id: number;
name: string;
size: number;
status: ProgressStatus;
progress: number;
url: string;
file?: File;
}

export enum VIEW_TYPES {
Expand Down
19 changes: 19 additions & 0 deletions src/lib/pusher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Pusher from 'pusher';

// Info: (20241009-tzuhan) 初始化 Pusher
const pusherConfig = {
appId: process.env.PUSHER_APP_ID!,
key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
secret: process.env.PUSHER_SECRET!,
host: process.env.NEXT_PUBLIC_PUSHER_HOST!,
useTLS: process.env.PUSHER_USE_TLS === 'true',
};

let pusherInstance: Pusher | null = null;

export const getPusherInstance = (): Pusher => {
if (!pusherInstance) {
pusherInstance = new Pusher(pusherConfig);
}
return pusherInstance;
};
26 changes: 26 additions & 0 deletions src/lib/pusherClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Pusher from 'pusher-js';

let pusherInstance: Pusher | null = null;

const pusherConfig = {
appKey: process.env.NEXT_PUBLIC_PUSHER_KEY!,
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER || '',
wsHost: process.env.NEXT_PUBLIC_PUSHER_HOST!,
wsPort: parseFloat(process.env.NEXT_PUBLIC_PUSHER_PORT!),
};

export const getPusherInstance = (): Pusher => {
if (!pusherInstance) {
pusherInstance = new Pusher(pusherConfig.appKey, {
cluster: pusherConfig.cluster,
wsHost: pusherConfig.wsHost,
wsPort: pusherConfig.wsPort,
channelAuthorization: {
transport: 'jsonp',
endpoint: `${pusherConfig.wsHost}/api/pusher/auth`,
headers: {},
},
});
}
return pusherInstance;
};
40 changes: 37 additions & 3 deletions src/lib/utils/parse_image_form.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import { IncomingForm, Fields, Files } from 'formidable';
import formidable, { IncomingForm, Fields, Files } from 'formidable';
import { promises as fs } from 'fs';
import path from 'path';
import { NextApiRequest } from 'next';
import { FORMIDABLE_OPTIONS } from '@/constants/config';
import { FileFolder, getFileFolder } from '@/constants/file';
import loggerBack from '@/lib/utils/logger_back';

export const extractKeyAndIvFromFields = (fields: formidable.Fields) => {
const { encryptedSymmetricKey, iv } = fields;
const keyStr =
encryptedSymmetricKey && encryptedSymmetricKey.length ? encryptedSymmetricKey[0] : '';
const ivStr = iv ? iv[0] : '';

const ivUnit8: Uint8Array = new Uint8Array(ivStr.split(',').map((num) => parseInt(num, 10)));

const isEncrypted = !!(keyStr && ivUnit8.length > 0);

return {
isEncrypted,
encryptedSymmetricKey: keyStr,
iv: ivUnit8,
};
};

export const parseForm = async (
req: NextApiRequest,
subDir: FileFolder = FileFolder.TMP // Info: (20240726 - Jacky) 預設子資料夾名稱為tmp
subDir: FileFolder = FileFolder.TMP, // Info: (20240726 - Jacky) 預設子資料夾名稱為tmp
subSubDir?: string // Info: (202410008 - Tzuhan) 如果有傳入subSubDir,則使用subSubDir
) => {
const uploadDir = getFileFolder(subDir);
let uploadDir = getFileFolder(subDir);
// Deprecated: (20241011-tzuhan) Debugging purpose
// eslint-disable-next-line no-console
console.log(`parseForm (subDir: ${subDir}, subSubDir: ${subSubDir}), req`, req);

// Info: (202410008 - Tzuhan) 如果有傳入subSubDir,更新 uploadDir
if (subSubDir) {
uploadDir = path.join(uploadDir, subSubDir);
await fs.mkdir(uploadDir, { recursive: true }); // Info: (202410008 - Tzuhan) 確保該目錄存在
}

const options = {
...FORMIDABLE_OPTIONS,
uploadDir,
Expand All @@ -20,8 +48,14 @@ export const parseForm = async (
const parsePromise = new Promise<{ fields: Fields; files: Files<string> }>((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) {
// Deprecated: (20241011-tzuhan) Debugging purpose
// eslint-disable-next-line no-console
console.error(`form.parse err:`, err);
reject(err);
} else {
// Deprecated: (20241011-tzuhan) Debugging purpose
// eslint-disable-next-line no-console
console.log(`form.parse fields, files:`, fields, files);
resolve({ fields, files });
}
});
Expand Down
Loading

0 comments on commit 25a707f

Please sign in to comment.