feat: state redux toolkit & feedback slice

This commit is contained in:
2024-01-05 15:08:11 -06:00
parent ab82d0797d
commit b7e273ae06
10 changed files with 171 additions and 10 deletions
+78
View File
@@ -0,0 +1,78 @@
'use client'
import IconButton from '@/components/ui/IconButton'
import Typography from '@/components/ui/Typography'
import { useAppDispatch } from '@/hooks/useAppDispatch'
import { useAppSelector } from '@/hooks/useAppSelector'
import { removeAlert } from '@/state/feedbackSlice'
import { css } from '@/styled-system/css'
import { alert } from '@/styled-system/recipes/alert'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { AnimatePresence, motion } from 'framer-motion'
import { type FC } from 'react'
import { createPortal } from 'react-dom'
const FeedbackConsumer: FC = () => {
const { alerts } = useAppSelector(state => state.feedback)
const dispatch = useAppDispatch()
return (
<>
{alerts.length > 0 && createPortal(
(
<AnimatePresence>
<motion.div
key="alerts"
className={css({
display: 'flex',
flexDirection: 'column',
gap: 'medium',
position: 'fixed',
bottom: 'medium',
left: 'medium',
padding: 'medium',
zIndex: 'modalBackdrop',
width: 'calc(100vw - 32px)',
maxWidth: '400px'
})}
initial={{ opacity: 0 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.3, ease: 'backIn' } }}
exit={{ opacity: 0, x: 400, transition: { duration: 0.3, ease: 'backOut' } }}
>
<AnimatePresence>
{alerts.map((currentAlert) => (
<motion.div
key={currentAlert.id}
// This is a workaround for PandaCSS to auto-generate styles and avoid Alerts with non-generated styles. See https://panda-css.com/docs/guides/dynamic-styling#runtime-conditions
className={alert({
severity: currentAlert.severity === 'success' ? 'success' : currentAlert.severity === 'info' ? 'info' : currentAlert.severity === 'warning' ? 'warning' : currentAlert.severity === 'error' ? 'error' : undefined
}).body}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.3, ease: 'backIn' } }}
exit={{ opacity: 0, x: 400, transition: { duration: 0.3, ease: 'backOut' } }}
>
<IconButton
size="small"
className={alert().closeButton}
onClick={() => dispatch(removeAlert(currentAlert.id))}
>
<FontAwesomeIcon icon={faTimes} fixedWidth size='sm'/>
</IconButton>
<Typography variant="h3" component="div">
{currentAlert.title}
</Typography>
<Typography variant="body1">
{currentAlert.message}
</Typography>
</motion.div>
))}
</AnimatePresence>
</motion.div>
</AnimatePresence>
),
document.body,
'alerts'
)}
</>
)
}
export default FeedbackConsumer
+19
View File
@@ -0,0 +1,19 @@
'use client'
import store from '@/state/store'
import { type FC, type ReactNode } from 'react'
import { Provider } from 'react-redux'
export interface StateProviderProps {
children: ReactNode
}
const StateProvider: FC<StateProviderProps> = ({ children }) => {
return (
<Provider
store={store}
>
{children}
</Provider>
)
}
export default StateProvider
+15 -10
View File
@@ -10,6 +10,8 @@ import { config } from '@fortawesome/fontawesome-svg-core'
import '@fortawesome/fontawesome-svg-core/styles.css'
import { type Metadata } from 'next'
import { type FC, type ReactNode } from 'react'
import FeedbackConsumer from './FeedbackConsumer'
import StateProvider from './StateProvider'
config.autoAddCss = false
@@ -26,16 +28,19 @@ const RootLayout: FC<RootLayoutProps> = ({ children }) => {
return (
<html lang="en">
<body>
<Header />
<main
className={css({
paddingBlock: 'medium',
minHeight: 'calc(100vh - 60px - 72px)'
})}
>
{children}
</main>
<Footer />
<StateProvider>
<Header />
<main
className={css({
paddingBlock: 'medium',
minHeight: 'calc(100vh - 60px - 72px)'
})}
>
{children}
</main>
<Footer />
<FeedbackConsumer />
</StateProvider>
</body>
</html>
)
+5
View File
@@ -0,0 +1,5 @@
import type { AppDispatch } from '@/state/store'
import { useDispatch } from 'react-redux'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
+5
View File
@@ -0,0 +1,5 @@
import type { RootState } from '@/state/store'
import type { TypedUseSelectorHook } from 'react-redux'
import { useSelector } from 'react-redux'
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
+33
View File
@@ -0,0 +1,33 @@
import { type Alert } from '@/types/feedback'
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
interface FeedbackState {
alerts: Alert[]
}
const initialState: FeedbackState = {
alerts: []
}
const feedbackSlice = createSlice({
name: 'feedback',
initialState,
reducers: {
addAlert (state, action: PayloadAction<Alert>) {
return {
...state,
alerts: [...state.alerts, action.payload]
}
},
removeAlert (state, action: PayloadAction<string>) {
return {
...state,
alerts: state.alerts.filter(alert => alert.id !== action.payload)
}
}
}
})
export const { addAlert, removeAlert } = feedbackSlice.actions
export default feedbackSlice
+13
View File
@@ -0,0 +1,13 @@
import feedbackSlice from '@/state/feedbackSlice'
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {
feedback: feedbackSlice.reducer
}
})
export default store
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
+1
View File
@@ -1,4 +1,5 @@
export interface Alert {
id: string
title: string
message: string
severity: 'success' | 'info' | 'warning' | 'error'