From 7a0bd97c5e2fcee3648f358698afe6d21d9e9887 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 26 Sep 2024 21:48:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20ErrorBoundary=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 37 ++++++++ src/content/App.tsx | 7 +- .../{Navigation.tsx => BottomNavigation.tsx} | 2 +- src/content/components/ErrorFallback.tsx | 66 ++++++++++++++ src/content/components/MainModal.tsx | 91 +++++++++++++++++++ src/content/components/Trigger.tsx | 77 ++-------------- .../components/setting/SettingsContent.tsx | 28 +++++- .../components/task/filterActivities.ts | 6 -- src/styles/index.css | 21 +++++ vite.config.ts | 11 +++ 11 files changed, 264 insertions(+), 84 deletions(-) rename src/content/components/{Navigation.tsx => BottomNavigation.tsx} (96%) create mode 100644 src/content/components/ErrorFallback.tsx create mode 100644 src/content/components/MainModal.tsx diff --git a/package.json b/package.json index 78775fa..a5bdb57 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-easy-crop": "^5.0.8", + "react-error-boundary": "^4.0.13", + "react-hot-toast": "^2.4.1", "react-hotkeys-hook": "^4.4.1", "react-shadow": "^20.4.0", "tailwind-merge": "^2.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e90d559..e287892 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ dependencies: react-easy-crop: specifier: ^5.0.8 version: 5.0.8(react-dom@18.3.1)(react@18.3.1) + react-error-boundary: + specifier: ^4.0.13 + version: 4.0.13(react@18.3.1) + react-hot-toast: + specifier: ^2.4.1 + version: 2.4.1(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1) react-hotkeys-hook: specifier: ^4.4.1 version: 4.5.0(react-dom@18.3.1)(react@18.3.1) @@ -2547,6 +2553,14 @@ packages: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true + /goober@2.1.14(csstype@3.1.3): + resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} + peerDependencies: + csstype: ^3.0.10 + dependencies: + csstype: 3.1.3 + dev: false + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -3543,6 +3557,29 @@ packages: tslib: 2.6.3 dev: false + /react-error-boundary@4.0.13(react@18.3.1): + resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.25.0 + react: 18.3.1 + dev: false + + /react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + goober: 2.1.14(csstype@3.1.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - csstype + dev: false + /react-hotkeys-hook@4.5.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==} peerDependencies: diff --git a/src/content/App.tsx b/src/content/App.tsx index 3085b9b..e8876ca 100644 --- a/src/content/App.tsx +++ b/src/content/App.tsx @@ -1,10 +1,15 @@ +import { ErrorBoundary } from 'react-error-boundary' + +import { ErrorFallback } from './components/ErrorFallback' import { Trigger } from './components/Trigger' import { ContentThemeProvider } from '@/components/ContentThemeProvider' export function App() { return ( - + + + ) } diff --git a/src/content/components/Navigation.tsx b/src/content/components/BottomNavigation.tsx similarity index 96% rename from src/content/components/Navigation.tsx rename to src/content/components/BottomNavigation.tsx index c6ee025..7e290ea 100644 --- a/src/content/components/Navigation.tsx +++ b/src/content/components/BottomNavigation.tsx @@ -5,7 +5,7 @@ type NavigationProps = { setActiveTab: (tab: 'tasks' | 'settings') => void } -export function Navigation({ activeTab, setActiveTab }: NavigationProps) { +export function BottomNavigation({ activeTab, setActiveTab }: NavigationProps) { return (
+
+
{error.stack}
+
+
+
+ + + +
+
버전: {packageJson.version}
+ + + + ) +} diff --git a/src/content/components/MainModal.tsx b/src/content/components/MainModal.tsx new file mode 100644 index 0000000..e8a14ba --- /dev/null +++ b/src/content/components/MainModal.tsx @@ -0,0 +1,91 @@ +import { motion } from 'framer-motion' +import { useState } from 'react' +import { ToastBar, Toaster } from 'react-hot-toast' + +import { BottomNavigation } from './BottomNavigation' +import { SettingsContent } from './setting' +import { TaskContent } from './task' + +const modalVariants = { + hidden: { + opacity: 0, + scale: 0.8, + y: 20, + transition: { duration: 0.2, ease: 'easeInOut' }, + }, + visible: { + opacity: 1, + scale: 1, + y: 0, + transition: { duration: 0.3, ease: 'easeOut' }, + }, + exit: { + opacity: 0, + scale: 0.8, + y: 20, + transition: { duration: 0.2, ease: 'easeInOut' }, + }, +} + +export function MainModal() { + const [activeTab, setActiveTab] = useState<'tasks' | 'settings'>('tasks') + + return ( + +
+ {activeTab === 'tasks' ? : } + + + {t => ( + + )} + +
+
+ ) +} diff --git a/src/content/components/Trigger.tsx b/src/content/components/Trigger.tsx index 5eae62c..bd455b0 100644 --- a/src/content/components/Trigger.tsx +++ b/src/content/components/Trigger.tsx @@ -1,57 +1,13 @@ import { AnimatePresence, motion } from 'framer-motion' import { X } from 'lucide-react' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' -import { SettingsContent } from './setting' -import { Navigation } from '@/content/components/Navigation' -import { TaskContent } from '@/content/components/task' +import { MainModal } from './MainModal' import { useStorageStore } from '@/storage/useStorageStore' -import { cn } from '@/utils/cn' - -const iconVariants = { - closed: { - opacity: 0, - scale: 0, - pathLength: 0, - transition: { duration: 0.2 }, - }, - open: { - opacity: 1, - scale: 1, - pathLength: 1, - transition: { - opacity: { delay: 0.2, duration: 0.2 }, - scale: { delay: 0.2, duration: 0.2 }, - pathLength: { delay: 0.2, duration: 0.3 }, - }, - }, -} - -const modalVariants = { - hidden: { - opacity: 0, - scale: 0.9, - y: 20, - transition: { duration: 0.2, ease: 'easeInOut' }, - }, - visible: { - opacity: 1, - scale: 1, - y: 0, - transition: { duration: 0.3, ease: 'easeOut' }, - }, - exit: { - opacity: 0, - scale: 0.9, - y: 20, - transition: { duration: 0.2, ease: 'easeInOut' }, - }, -} export function Trigger() { const [isOpen, setIsOpen] = useState(false) - const [activeTab, setActiveTab] = useState<'tasks' | 'settings'>('tasks') const { settings, status } = useStorageStore() @@ -67,9 +23,7 @@ export function Trigger() { <>
setIsOpen(prev => !prev)} - className={cn( - 'd-mask d-mask-squircle fixed bottom-25px right-25px h-56px w-56px cursor-pointer bg-cover bg-center bg-no-repeat shadow-lg transition-all duration-300 ease-in-out hover:shadow-xl', - )} + className="d-mask d-mask-squircle fixed bottom-25px right-25px h-56px w-56px cursor-pointer bg-cover bg-center bg-no-repeat shadow-lg transition-all duration-300 ease-in-out hover:shadow-xl" style={ settings.trigger.type === 'color' ? { background: settings.trigger.color } @@ -85,34 +39,15 @@ export function Trigger() { transition={{ duration: 0.2 }} className="absolute inset-0 bg-black bg-opacity-30 backdrop-blur-sm" > - +
- +
)}
- - {isOpen && ( - -
- {activeTab === 'tasks' ? : } - -
-
- )} -
+ {isOpen && } ) } diff --git a/src/content/components/setting/SettingsContent.tsx b/src/content/components/setting/SettingsContent.tsx index 9ec1d2c..10129eb 100644 --- a/src/content/components/setting/SettingsContent.tsx +++ b/src/content/components/setting/SettingsContent.tsx @@ -1,7 +1,8 @@ import { AnimatePresence } from 'framer-motion' import { Camera, Palette } from 'lucide-react' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useState } from 'react' import { useDropzone } from 'react-dropzone' +import toast from 'react-hot-toast' import { ColorPickerModal } from './ColorPickerModal' import { ImageCropModal } from './ImageCropModal' @@ -21,33 +22,50 @@ const refreshIntervalOptions = [ { value: 1000 * 60 * 120, label: '2시간' }, ] +const maxImageSize = 1024 * 1024 * 4 // 4MB + export function SettingsContent() { const { settings, updateSettings } = useStorageStore() const [image, setImage] = useState(null) const [isCropModalOpen, setIsCropModalOpen] = useState(false) const [isColorPickerOpen, setIsColorPickerOpen] = useState(false) - const inputRef = useRef(null) - const onDrop = useCallback((acceptedFiles: File[]) => { const file = acceptedFiles[0] + + if (!file) { + return + } + + if (file.size > maxImageSize) { + toast.error(`이미지는 ${maxImageSize / 1024 / 1024}MB 이하로 업로드해주세요`) + return + } + setImage(URL.createObjectURL(file)) setIsCropModalOpen(true) }, []) - const { getRootProps, getInputProps, isDragActive } = useDropzone({ + const { getRootProps, getInputProps, isDragActive, inputRef } = useDropzone({ onDrop, accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'], }, noClick: true, - maxSize: 1024 * 1024 * 2, + noKeyboard: true, + maxFiles: 1, + onError: () => { + toast.error('이미지 업로드에 실패했어요') + }, }) const handleCropComplete = useCallback( async (croppedImage: string) => { updateSettings({ trigger: { type: 'image', image: croppedImage } }) setIsCropModalOpen(false) + setImage(null) + + toast.success('이미지가 성공적으로 업로드되었어요') }, [updateSettings], ) diff --git a/src/content/components/task/filterActivities.ts b/src/content/components/task/filterActivities.ts index c173b5c..ac46749 100644 --- a/src/content/components/task/filterActivities.ts +++ b/src/content/components/task/filterActivities.ts @@ -32,12 +32,6 @@ const filterBySearchQuery = (activity: Activity, searchQuery?: string): boolean } export function filterActivities(activity: Activity, options: FilterOptions): boolean { - console.log( - isValidActivity(activity), - filterByStatus(activity, options.status), - filterByCourse(activity, options.courseId), - filterBySearchQuery(activity, options.searchQuery), - ) return ( isValidActivity(activity) && filterByStatus(activity, options.status) && diff --git a/src/styles/index.css b/src/styles/index.css index caaa920..cc81d4b 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -36,3 +36,24 @@ line-height: 1.5 !important; letter-spacing: normal; } + +@keyframes fadein { + 0% { + opacity: 0.5; + transform: translateY(10px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeout { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateY(10px); + } +} diff --git a/vite.config.ts b/vite.config.ts index bf9ce68..58af72b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,4 +41,15 @@ export default defineConfig({ '@/assets': resolve(__dirname, './src/assets'), }, }, + build: { + chunkSizeWarningLimit: 1024, + rollupOptions: { + onwarn(warning, warn) { + if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { + return + } + warn(warning) + }, + }, + }, })