Skip to content

Commit 6d86876

Browse files
committed
refactor: removed usage of s3 sdk and updated components to use BE api for uploading and accessing objects
1 parent d853278 commit 6d86876

File tree

14 files changed

+4801
-4483
lines changed

14 files changed

+4801
-4483
lines changed

frontend/package-lock.json

Lines changed: 4727 additions & 4368 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"pretty": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,scss,sass,css}\""
1212
},
1313
"dependencies": {
14-
"@aws-sdk/client-s3": "^3.451.0",
1514
"@hookform/resolvers": "^3.3.2",
1615
"@radix-ui/react-accordion": "^1.1.2",
1716
"@radix-ui/react-alert-dialog": "^1.0.5",

frontend/src/api/events.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ulid } from 'ulid';
22
import { createApi } from '@/api/utils/createApi';
3-
import { Event, EventFAQs, EventStatus, FAQ } from '@/model/events';
3+
import { Event, EventFAQs, EventStatus, FAQ, UploadType } from '@/model/events';
44

55
export interface TicketType {
66
name: string;
@@ -65,6 +65,11 @@ export interface PresignedUrl {
6565
objectKey: string;
6666
}
6767

68+
export interface DownloadUrl {
69+
downloadLink: string;
70+
objectKey: string;
71+
}
72+
6873
interface EventRegCountStatus {
6974
registrationCount: number;
7075
maximumSlots: number | null;
@@ -161,14 +166,21 @@ export const deleteEvent = (entryId: string) =>
161166
output: mapEventDtoToEvent
162167
});
163168

164-
export const getPresignedUrl = (entryId: string, fileName: string, uploadType: string) =>
169+
export const getPresignedUrl = (entryId: string, fileName: string, uploadType: UploadType, headers?: object) =>
165170
createApi<PresignedUrl>({
166171
method: 'put',
167-
authorize: true,
168172
url: `/events/${entryId}/upload/${uploadType}`,
173+
headers,
169174
body: { fileName }
170175
});
171176

177+
export const getDownloadUrl = (entryId: string, objectKey: string) =>
178+
createApi<DownloadUrl>({
179+
method: 'get',
180+
url: `/events/${entryId}/download`,
181+
queryParams: { objectKey }
182+
});
183+
172184
export const getEventRegCountStatus = (entryId: string) =>
173185
createApi<EventDto, EventRegCountStatus>({
174186
method: 'get',

frontend/src/api/utils/createApi.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ import { getCookie } from 'typescript-cookie';
33
import { refreshOnIntercept } from '@/utils/refreshToken';
44
import { QueryKey } from '@tanstack/react-query';
55

6-
type SearchParamType = string | string[] | number | number[] | boolean | Record<string, any> | Date | null | undefined;
7-
8-
type SearchParams = Record<string, SearchParamType>;
9-
106
interface ErrorStringResponse {
117
message: string;
128
}
@@ -27,15 +23,16 @@ export interface CustomAxiosError extends Omit<AxiosResponse, 'data'> {
2723
errorData: ErrorResponse;
2824
}
2925

30-
export const createQueryKey = (url: string, body?: SearchParams) => [url, body];
26+
export const createQueryKey = (url: string, body?: any) => [url, body];
3127
export type ApiService = 'auth' | 'events' | 'payments';
3228
interface createApiProps<D, T = D> {
3329
method?: 'get' | 'post' | 'delete' | 'patch' | 'put';
3430
authorize?: boolean;
3531
apiService?: ApiService;
3632
url: string;
3733
queryParams?: any;
38-
body?: SearchParams;
34+
headers?: object;
35+
body?: any;
3936
timeout?: number;
4037
output?: (dto: D) => T;
4138
}
@@ -53,7 +50,7 @@ const getUrl = (apiService: ApiService) => {
5350
export type GenericReturn<T> = AxiosResponse<T> & CustomAxiosError;
5451

5552
export function createApi<D, T = D>(
56-
{ method = 'get', url, authorize = false, apiService = 'events', queryParams = {}, body, timeout = 1000 * 60, output }: createApiProps<D, T>,
53+
{ method = 'get', url, authorize = false, apiService = 'events', queryParams = {}, headers, body, timeout = 1000 * 60, output }: createApiProps<D, T>,
5754
staleTime?: number,
5855
cacheTime?: number
5956
) {
@@ -75,7 +72,8 @@ export function createApi<D, T = D>(
7572
'Content-Type': 'application/json',
7673
...(authorize && {
7774
Authorization: `Bearer ${accessToken}`
78-
})
75+
}),
76+
...headers
7977
}
8078
});
8179

@@ -103,7 +101,7 @@ export function createApi<D, T = D>(
103101
authorize && refreshOnIntercept(api);
104102

105103
return {
106-
queryKey: createQueryKey(url, body) as unknown as QueryKey,
104+
queryKey: createQueryKey(url, body ?? queryParams) as unknown as QueryKey,
107105
queryFn,
108106
staleTime,
109107
cacheTime

frontend/src/components/EventCard/EventCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ interface CardHeaderProps {
2222
}
2323

2424
const EventCardHeader: React.FC<CardHeaderProps> = ({ event, isDeleteEnabled, isDeletingEvent, setDeleteModalOpen }) => {
25-
const { fileUrl: imageUrl, isFetching } = useFileUrl(event.bannerLink!);
25+
const { fileUrl, isFetching } = useFileUrl(event.eventId, event.bannerLink);
2626

2727
return (
28-
<div className="h-1/2 group-hover:opacity-70 transition" style={{ backgroundImage: `url(${imageUrl})`, backgroundSize: 'cover' }}>
28+
<div className="h-1/2 group-hover:opacity-70 transition" style={{ backgroundImage: `url(${fileUrl})`, backgroundSize: 'cover' }}>
2929
{isFetching && <Skeleton className="w-full h-full rounded-b-none" />}
3030
{isDeleteEnabled && (
3131
<div className="w-full flex p-2 justify-end">

frontend/src/components/FileUpload.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { forwardRef, useMemo } from 'react';
2+
import { UploadType } from '@/model/events';
23
import { useFileUpload } from '@/hooks/useFileUpload';
34
import { CardContainer, CardFooter, CardHeader } from './Card';
45
import ImageViewer from './ImageViewer';
@@ -9,7 +10,7 @@ import Progress from './Progress';
910
interface FileUploadProps {
1011
name?: string;
1112
eventId: string;
12-
uploadType: string;
13+
uploadType: UploadType;
1314
value: string;
1415
onChange: (value: string) => void;
1516
}
@@ -36,7 +37,7 @@ const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(({ name, eventI
3637
return (
3738
<CardContainer className="p-0 border-none shadow-none">
3839
<CardHeader className="items-center">
39-
<ImageViewer objectKey={value} className="h-40 w-min object-cover" alt="" />
40+
<ImageViewer eventId={eventId} objectKey={value} className="h-40 w-min object-cover" alt="" />
4041
</CardHeader>
4142
<CardFooter className="flex flex-wrap px-0 pb-2 gap-2 items-center w-full">
4243
<Input id={`upload-custom-${uploadType}`} ref={ref} onChange={onFileChange} type="file" accept="image/*" className="hidden" />

frontend/src/components/ImageViewer.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { useFileUrl } from '@/hooks/useFileUrl';
44
import Skeleton from './Skeleton';
55

66
interface FileViewerProps {
7+
eventId: string;
78
objectKey: string | null;
89
className?: string;
910
alt?: string;
1011
}
1112

12-
const ImageViewer: FC<FileViewerProps> = ({ objectKey, className, alt }) => {
13-
const { fileUrl, isFetching } = useFileUrl(objectKey);
14-
13+
const ImageViewer: FC<FileViewerProps> = ({ eventId, objectKey, className, alt }) => {
14+
const { fileUrl, isFetching } = useFileUrl(eventId, objectKey);
1515
if (isFetching) {
1616
return <Skeleton className={cn('h-full', className)} />;
1717
}
@@ -20,7 +20,7 @@ const ImageViewer: FC<FileViewerProps> = ({ objectKey, className, alt }) => {
2020
return <div className={cn('min-w-full h-full', className)} />;
2121
}
2222

23-
return <img src={fileUrl ?? undefined} className={cn('w-full h-full', className)} alt={alt} />;
23+
return <img src={fileUrl} className={cn('w-full h-full', className)} alt={alt} />;
2424
};
2525

2626
export default ImageViewer;

frontend/src/hooks/useFileUpload.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ChangeEvent, useState } from 'react';
22
import { useFormContext } from 'react-hook-form';
33
import { getPresignedUrl } from '@/api/events';
4+
import { createApi } from '@/api/utils/createApi';
5+
import { UploadType } from '@/model/events';
46
import { useApi } from './useApi';
57
import { useNotifyToast } from './useNotifyToast';
6-
import { S3Client, PutObjectCommand, PutObjectCommandInput } from '@aws-sdk/client-s3';
78

89
const MAX_FILE_UPLOAD_SIZE = 1e7; // 10MB
910

@@ -26,15 +27,15 @@ const isExtensionAllowed = (fileName: string) => {
2627
}
2728
};
2829

29-
export const useFileUpload = (eventId: string, uploadType: string, onChange: (key: string) => void, name?: string) => {
30+
export const useFileUpload = (eventId: string, uploadType: UploadType, onChange: (key: string) => void, name?: string) => {
3031
const api = useApi();
3132
const { successToast, errorToast } = useNotifyToast();
3233
const formContext = useFormContext();
3334
const [isUploading, setIsUploading] = useState(false);
3435
const [uploadProgress, setUploadProgress] = useState(0);
3536

36-
const getPresignedUrlTrigger = async (entryId: string, fileName: string, fileType: string) => {
37-
const response = await api.execute(getPresignedUrl(entryId, fileName, fileType));
37+
const getPresignedUrlTrigger = async (entryId: string, file: File, uploadType: UploadType) => {
38+
const response = await api.execute(getPresignedUrl(entryId, file.name, uploadType));
3839
return response.data;
3940
};
4041

@@ -77,33 +78,29 @@ export const useFileUpload = (eventId: string, uploadType: string, onChange: (ke
7778
return;
7879
}
7980

80-
setIsUploading(true);
81-
await uploadFile(selectedFileObj);
82-
setIsUploading(false);
83-
setUploadProgress(0);
81+
try {
82+
setIsUploading(true);
83+
await uploadFile(selectedFileObj);
84+
} catch (error) {
85+
console.error(error);
86+
} finally {
87+
setIsUploading(false);
88+
setUploadProgress(0);
89+
}
8490
};
8591

8692
const uploadFile = async (file: File) => {
87-
const s3Client = new S3Client({
88-
region: 'ap-southeast-1',
89-
credentials: {
90-
accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID!,
91-
secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY!
92-
}
93-
});
93+
const { uploadLink, objectKey } = await getPresignedUrlTrigger(eventId, file, uploadType);
9494

95-
const { objectKey } = await getPresignedUrlTrigger(eventId, file.name, uploadType);
96-
97-
// Function to upload a file to S3
98-
const uploadParams: PutObjectCommandInput = {
99-
Bucket: import.meta.env.VITE_S3_BUCKET!,
100-
Key: objectKey,
101-
Body: file
102-
};
95+
const uploadApi = createApi({
96+
method: 'put',
97+
url: uploadLink,
98+
headers: { 'Content-Type': file.type },
99+
body: file
100+
});
103101

104102
try {
105-
const command = new PutObjectCommand(uploadParams);
106-
await s3Client.send(command);
103+
await api.execute(uploadApi);
107104
setUploadProgress(100);
108105

109106
successToast({
@@ -113,6 +110,7 @@ export const useFileUpload = (eventId: string, uploadType: string, onChange: (ke
113110

114111
onChange(objectKey);
115112
} catch (err) {
113+
setIsUploading(false);
116114
console.error('Error', err);
117115
errorToast({
118116
title: 'File Upload Failed',

frontend/src/hooks/useFileUrl.ts

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,11 @@
1-
import { useCallback } from 'react';
2-
import { createQueryKey } from '@/api/utils/createApi';
3-
import { S3Client, GetObjectCommand, GetObjectAclCommandInput } from '@aws-sdk/client-s3';
4-
import { useQuery } from '@tanstack/react-query';
1+
import { getDownloadUrl } from '@/api/events';
2+
import { useApiQuery } from './useApi';
53

6-
const s3Client = new S3Client({
7-
region: 'ap-southeast-1',
8-
credentials: {
9-
accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID!,
10-
secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY!
11-
}
12-
});
13-
14-
export const useFileUrl = (key: string | null) => {
15-
const getFileUrl = useCallback(async () => {
16-
const params: GetObjectAclCommandInput = {
17-
Bucket: import.meta.env.VITE_S3_BUCKET!,
18-
Key: key!
19-
};
20-
21-
const { Body } = await s3Client.send(new GetObjectCommand(params));
22-
const blob = await new Response(Body as ReadableStream).blob();
23-
return URL.createObjectURL(blob);
24-
}, [key]);
25-
26-
const { data, isFetching } = useQuery({
27-
queryKey: createQueryKey('getFileUrl', { key }),
28-
queryFn: async () => getFileUrl(),
29-
enabled: !!key,
30-
refetchOnWindowFocus: false,
31-
refetchOnMount: false,
32-
refetchOnReconnect: false
33-
});
4+
export const useFileUrl = (eventId: string, key: string | null) => {
5+
const { data, isFetching } = useApiQuery(getDownloadUrl(eventId, key!), { active: !!key });
346

357
return {
36-
fileUrl: data,
8+
fileUrl: data?.data.downloadLink,
379
isFetching
3810
};
3911
};

frontend/src/model/events.ts

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -78,40 +78,15 @@ export const EVENT_STATUSES: EventStatusItem[] = [
7878
}
7979
];
8080

81-
export const EVENT_UPLOAD_TYPES = {
81+
export type UploadType = 'banner' | 'logo' | 'certificateTemplate' | 'proofOfPayment' | 'gcashQRCode';
82+
83+
export const EVENT_UPLOAD_TYPE: Record<string, UploadType> = {
8284
BANNER: 'banner',
8385
LOGO: 'logo',
8486
CERTIFICATE_TEMPLATE: 'certificateTemplate',
8587
PROOF_OF_PAYMENT: 'proofOfPayment',
8688
GCASH_QR: 'gcashQRCode'
8789
};
88-
89-
export const EVENT_OBJECT_KEY_MAPS = {
90-
BANNER: 'bannerLink',
91-
LOGO: 'logoLink',
92-
CERTIFICATE_TEMPLATE: 'certificateTemplate',
93-
PROOF_OF_PAYMENT: 'proofOfPayment',
94-
GCASH_QR: 'gcashQRCode'
95-
};
96-
97-
export type UploadType = keyof typeof EVENT_UPLOAD_TYPE;
98-
99-
export const enum EVENT_UPLOAD_TYPE {
100-
BANNER = 'banner',
101-
LOGO = 'logo',
102-
CERTIFICATE_TEMPLATE = 'certificateTemplate',
103-
PROOF_OF_PAYMENT = 'proofOfPayment',
104-
GCASH_QR = 'gcashQRCode'
105-
}
106-
107-
export const enum EVENT_OBJECT_KEY_MAP {
108-
BANNER = 'bannerLink',
109-
LOGO = 'logoLink',
110-
CERTIFICATE_TEMPLATE = 'certificateTemplate',
111-
GCASH_PAYMENT = 'gcashPayment',
112-
GCASH_QR = 'gcashQRCode'
113-
}
114-
11590
export type EventFAQs = {
11691
isActive: boolean;
11792
faqs: FAQ[];

0 commit comments

Comments
 (0)