diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c9d92b9d..9ac0ab1b 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,20 +1,20 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. - // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp - // List of extensions which should be recommended for users of this workspace. - "recommendations": [ - "esbenp.prettier-vscode", - "rvest.vs-code-prettier-eslint", - "dbaeumer.vscode-eslint", - "stylelint.vscode-stylelint", - "hex-ci.stylelint-plus", - "formulahendry.auto-rename-tag", - "formulahendry.auto-close-tag", - "vincaslt.highlight-matching-tag", - "christian-kohler.npm-intellisense", - "christian-kohler.path-intellisense", - "burkeholland.simple-react-snippets" - ], - // List of extensions recommended by VS Code that should not be recommended for users of this workspace. - "unwantedRecommendations": [] -} \ No newline at end of file + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "esbenp.prettier-vscode", + "rvest.vs-code-prettier-eslint", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint", + "hex-ci.stylelint-plus", + "formulahendry.auto-rename-tag", + "formulahendry.auto-close-tag", + "vincaslt.highlight-matching-tag", + "christian-kohler.npm-intellisense", + "christian-kohler.path-intellisense", + "burkeholland.simple-react-snippets" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] +} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 442d1cdc..18edb199 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,16 +1,8 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "es5", - "dom" - ], - "types": [ - "cypress", - "node" - ] + "lib": ["es5", "dom"], + "types": ["cypress", "node"] }, - "include": [ - "**/*.ts" - ] -} \ No newline at end of file + "include": ["**/*.ts"] +} diff --git a/public/site.webmanifest b/public/site.webmanifest index b20abb7c..fa99de77 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,19 +1,19 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/src/components/common/Cropper/index.tsx b/src/components/common/Cropper/index.tsx index 5bed4eb9..a0248cb9 100644 --- a/src/components/common/Cropper/index.tsx +++ b/src/components/common/Cropper/index.tsx @@ -80,6 +80,7 @@ const Cropper = ({ const handleImageError = useCallback(() => { if (file !== null) { showToast('This image format is not supported.'); + onClose(); } }, [file, onClose]); diff --git a/src/components/common/VerticalFormItem/style.module.scss b/src/components/common/VerticalFormItem/style.module.scss index 9ff0cc86..48799901 100644 --- a/src/components/common/VerticalFormItem/style.module.scss +++ b/src/components/common/VerticalFormItem/style.module.scss @@ -60,11 +60,7 @@ padding: 2px; transition: 0.3s ease; width: 100%; - - } - - } .formError { diff --git a/src/lib/api/KlefkiAPI.ts b/src/lib/api/KlefkiAPI.ts index a5a1fd47..4dd7d5c0 100644 --- a/src/lib/api/KlefkiAPI.ts +++ b/src/lib/api/KlefkiAPI.ts @@ -114,3 +114,17 @@ export const generateACMURL = async (acmurlInfo: GenerateACMURLRequest): Promise }, }); }; + +export const uploadBoardPhoto = async (file: File): Promise => { + const { klefki } = config; + const formData = new FormData(); + formData.append('file', file); + const requestUrl = `${klefki.baseUrl}${klefki.endpoints.board.photoUpload}`; + const response = await axios.post(requestUrl, formData, { + headers: { + Authorization: `Bearer ${generateToken(klefki.key)}`, + }, + }); + + return response.data; +}; diff --git a/src/lib/config.ts b/src/lib/config.ts index c3b649d4..2f1ec6e6 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -73,6 +73,9 @@ const config = { acmurl: { generate: '/acmurl/generate', }, + board: { + photoUpload: '/board/uploadPhoto', + }, }, }, defaultEventImage: Cat, @@ -110,6 +113,7 @@ const config = { awardMilestone: '/admin/milestone', viewResumes: '/admin/resumes', manageUserAccess: '/admin/access', + updateProfile: '/admin/profile', store: { items: '/admin/store/items', pickup: '/admin/store/pickup', diff --git a/src/lib/types/apiResponses.ts b/src/lib/types/apiResponses.ts index 98927f1a..75ea3dfa 100644 --- a/src/lib/types/apiResponses.ts +++ b/src/lib/types/apiResponses.ts @@ -529,6 +529,7 @@ export interface DeleteResumeResponse extends ApiResponse {} export interface KlefkiAPIResponse { message: string; error: string; + url?: string; } export interface NotionEventDetails { diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index 2ba6541b..8c66fe4d 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -99,6 +99,7 @@ const AdminPage = ({ user: { accessType }, preview }: AdminProps) => { <> View User Resumes Manage User Access + Update Board Website Photo ) : ( 'Restricted Access' diff --git a/src/pages/admin/profile.tsx b/src/pages/admin/profile.tsx new file mode 100644 index 00000000..09123536 --- /dev/null +++ b/src/pages/admin/profile.tsx @@ -0,0 +1,163 @@ +import { VerticalForm, VerticalFormButton, VerticalFormTitle, Cropper } from '@/components/common'; +import { showToast, config } from '@/lib'; +import withAccessType, { GetServerSidePropsWithAuth } from '@/lib/hoc/withAccessType'; +import { PermissionService } from '@/lib/services'; +import { PrivateProfile } from '@/lib/types/apiResponses'; +import { reportError } from '@/lib/utils'; +import { KlefkiAPI } from '@/lib/api'; + +import type { NextPage } from 'next'; +import Image from 'next/image'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import { ChangeEvent, useEffect, useState, useRef } from 'react'; + +interface UploadBoardPhotoProps { + user: PrivateProfile; +} +interface FormValues { + photo: File; +} +const UpdateProfilePage: NextPage = ({ + user: { firstName, lastName }, +}: UploadBoardPhotoProps) => { + const { handleSubmit } = useForm(); + const [file, setFile] = useState(null); + const [croppedImg, setCroppedImg] = useState(null); + const [uploadedURL, setUploadedURL] = useState(null); + const fileInputRef = useRef(null); // Reference to the hidden file input + const [previewURL, setPreviewURL] = useState(''); + + const handleUpload = (e: ChangeEvent) => { + const uploadedFile = e.target.files?.[0] || null; + e.currentTarget.value = ''; + const maxFileSize = 5 * 1024 * 1024; + + if (uploadedFile) { + if (!uploadedFile.type.startsWith('image/')) { + showToast('Unsupported file type. Please upload an image.'); + return; + } + if (uploadedFile.size > maxFileSize) { + showToast('File size exceeds the 5MB limit. Please upload a smaller image.'); + return; + } + setUploadedURL(null); + setFile(uploadedFile); + } + }; + + const handleCrop = async (croppedFile: Blob) => { + setCroppedImg(croppedFile); + + setPreviewURL(URL.createObjectURL(croppedFile)); + }; + + const triggerFileInput = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); // Simulate a click on the hidden file input + } + }; + + const onSubmit: SubmitHandler = async () => { + if (!croppedImg) { + showToast('Please crop your image to submit!'); + return; + } + try { + const croppedFile = new File( + [croppedImg], + `${firstName}_${lastName}.${croppedImg.type.split('/')[1]}`, + { + type: croppedImg.type, + } + ); + + const response = await KlefkiAPI.uploadBoardPhoto(croppedFile); + showToast('Photo uploaded successfully!'); + if (response.url) { + setUploadedURL(response.url); + } + } catch (error) { + reportError('Error found!', error); + setUploadedURL(null); + } + }; + + useEffect(() => { + return () => { + if (previewURL) { + URL.revokeObjectURL(previewURL); // Clean up the blob URL + } + }; + }, [previewURL]); + + return ( + + + {uploadedURL && ( +
+

Your Image URL is:

+ {uploadedURL} +
+ )} + {!uploadedURL && croppedImg && ( +
+ preview photo +
+ )} + + + + + + setFile(null)} + /> + + {!uploadedURL && croppedImg && ( + + )} +
+ ); +}; +export default UpdateProfilePage; + +const getServerSidePropsFunc: GetServerSidePropsWithAuth = async () => { + return { + props: { + title: 'Upload Board Photo', + }, + }; +}; + +export const getServerSideProps = withAccessType( + getServerSidePropsFunc, + PermissionService.canViewAdminPage, + { redirectTo: config.admin.homeRoute } +);