Skip to content

Commit

Permalink
feat: ErrorBoundary 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
kangju2000 committed Sep 26, 2024
1 parent aaccb9c commit 7a0bd97
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 84 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/content/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ContentThemeProvider>
<Trigger />
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Trigger />
</ErrorBoundary>
</ContentThemeProvider>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type NavigationProps = {
setActiveTab: (tab: 'tasks' | 'settings') => void
}

export function Navigation({ activeTab, setActiveTab }: NavigationProps) {
export function BottomNavigation({ activeTab, setActiveTab }: NavigationProps) {
return (
<div className="flex justify-around border-t border-gray-200 bg-white bg-opacity-80">
<button
Expand Down
66 changes: 66 additions & 0 deletions src/content/components/ErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Copy } from 'lucide-react'

import packageJson from '../../../package.json'
import { chromeStorageClient } from '@/storage/chromeStorageClient'
import { useStorageStore } from '@/storage/useStorageStore'

import type { FallbackProps } from 'react-error-boundary'

export function ErrorFallback({ error }: FallbackProps) {
const { resetStore } = useStorageStore()

return (
<div>
<div className="d-mask d-mask-squircle fixed bottom-25px right-25px h-56px w-56px cursor-pointer bg-rose-400 bg-cover bg-center bg-no-repeat shadow-lg transition-all duration-300 ease-in-out hover:shadow-xl">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-2xl font-bold text-white">!</div>
</div>
</div>
<div className="fixed bottom-96px right-25px h-600px w-350px overflow-hidden rounded-36px bg-slate-100 p-16px shadow-[0_0_100px_0_rgba(0,0,0,0.2)] backdrop-blur-sm">
<div className="flex h-full flex-col items-center justify-center text-center">
<div className="whitespace-pre-line break-words text-xl font-bold text-gray-800">
확장 프로그램에 문제가 발생했어요 😢
</div>
<div className="relative mt-4 h-124px w-full overflow-hidden whitespace-pre-wrap rounded-lg bg-white p-4 text-left text-12px text-gray-500">
<button
onClick={() => navigator.clipboard.writeText(error.stack)}
className="absolute right-4 top-4 rounded-lg bg-white bg-opacity-50 p-2 backdrop-blur-sm"
>
<Copy size={16} />
</button>
<div className="h-full overflow-auto">
<pre className="whitespace-pre-wrap break-words">{error.stack}</pre>
</div>
</div>
<div className="flex gap-16px">
<button
onClick={() => {
resetStore()
window.location.reload()
}}
className="mt-16px rounded-lg bg-rose-400 px-12px py-8px font-bold text-white"
>
다시 시작하기
</button>

<button
onClick={async e => {
const data = await chromeStorageClient.getData()
navigator.clipboard.writeText(JSON.stringify(data, null, 2))
;(e.target as HTMLButtonElement).innerText = '복사 완료!'

setTimeout(() => {
;(e.target as HTMLButtonElement).innerText = '데이터 복사하기'
}, 2000)
}}
className="mt-16px rounded-lg bg-gray-500 px-12px py-8px font-bold text-white"
>
데이터 복사하기
</button>
</div>
<div className="mt-16px text-sm text-gray-500">버전: {packageJson.version}</div>
</div>
</div>
</div>
)
}
91 changes: 91 additions & 0 deletions src/content/components/MainModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<motion.div
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
className="fixed bottom-96px right-25px h-600px w-350px origin-bottom-right overflow-hidden rounded-36px bg-slate-100 shadow-[0_0_100px_0_rgba(0,0,0,0.2)] backdrop-blur-sm"
>
<div className="flex h-full flex-col">
{activeTab === 'tasks' ? <TaskContent /> : <SettingsContent />}
<BottomNavigation activeTab={activeTab} setActiveTab={setActiveTab} />
<Toaster
containerStyle={{ bottom: 100 }}
toastOptions={{
position: 'bottom-center',
success: {
duration: 3000,
style: {
backgroundColor: 'rgba(133, 239, 133, 0.5)',
border: '1px solid rgba(133, 239, 133, 0.5)',
},
},
error: {
duration: 3000,
style: {
backgroundColor: 'rgba(239, 133, 133, 0.5)',
border: '1px solid rgba(239, 133, 133, 0.5)',
},
},
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px 8px',
height: '40px',
width: '200px',
maxWidth: '200px',
overflow: 'hidden',
borderRadius: '24px',
fontSize: '11px',
boxShadow: '0 0 100px rgba(0, 0, 0, 0.1)',
backdropFilter: 'blur(10px)',
},
}}
>
{t => (
<ToastBar
toast={t}
style={{
...t.style,
animation: t.visible ? 'fadein 0.5s' : 'fadeout 1s',
}}
/>
)}
</Toaster>
</div>
</motion.div>
)
}
77 changes: 6 additions & 71 deletions src/content/components/Trigger.tsx
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -67,9 +23,7 @@ export function Trigger() {
<>
<div
onClick={() => 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 }
Expand All @@ -85,34 +39,15 @@ export function Trigger() {
transition={{ duration: 0.2 }}
className="absolute inset-0 bg-black bg-opacity-30 backdrop-blur-sm"
>
<motion.div
animate={isOpen ? 'open' : 'closed'}
variants={iconVariants}
className="absolute inset-0 flex items-center justify-center text-white"
>
<div className="absolute inset-0 flex items-center justify-center text-white">
<X size={24} />
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>

<AnimatePresence>
{isOpen && (
<motion.div
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
className="fixed bottom-96px right-25px h-600px w-350px overflow-hidden rounded-36px border border-solid border-slate-200 border-opacity-50 bg-slate-100 shadow-[0_0_100px_0_rgba(0,0,0,0.2)] backdrop-blur-sm"
>
<div className="flex h-full flex-col">
{activeTab === 'tasks' ? <TaskContent /> : <SettingsContent />}
<Navigation activeTab={activeTab} setActiveTab={setActiveTab} />
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>{isOpen && <MainModal />}</AnimatePresence>
</>
)
}
Loading

0 comments on commit 7a0bd97

Please sign in to comment.