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
BIN
View File
Binary file not shown.
+2
View File
@@ -17,6 +17,7 @@
"@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "^2.0.1",
"appwrite": "^13.0.1", "appwrite": "^13.0.1",
"entgamers-database": "^0.0.5", "entgamers-database": "^0.0.5",
"entgamers-panda-preset": "0.1.1", "entgamers-panda-preset": "0.1.1",
@@ -27,6 +28,7 @@
"node-appwrite": "^11.1.0", "node-appwrite": "^11.1.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-redux": "^9.0.4",
"sharp": "^0.33.1", "sharp": "^0.33.1",
"yup": "^1.3.3" "yup": "^1.3.3"
}, },
+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
+5
View File
@@ -10,6 +10,8 @@ import { config } from '@fortawesome/fontawesome-svg-core'
import '@fortawesome/fontawesome-svg-core/styles.css' import '@fortawesome/fontawesome-svg-core/styles.css'
import { type Metadata } from 'next' import { type Metadata } from 'next'
import { type FC, type ReactNode } from 'react' import { type FC, type ReactNode } from 'react'
import FeedbackConsumer from './FeedbackConsumer'
import StateProvider from './StateProvider'
config.autoAddCss = false config.autoAddCss = false
@@ -26,6 +28,7 @@ const RootLayout: FC<RootLayoutProps> = ({ children }) => {
return ( return (
<html lang="en"> <html lang="en">
<body> <body>
<StateProvider>
<Header /> <Header />
<main <main
className={css({ className={css({
@@ -36,6 +39,8 @@ const RootLayout: FC<RootLayoutProps> = ({ children }) => {
{children} {children}
</main> </main>
<Footer /> <Footer />
<FeedbackConsumer />
</StateProvider>
</body> </body>
</html> </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 { export interface Alert {
id: string
title: string title: string
message: string message: string
severity: 'success' | 'info' | 'warning' | 'error' severity: 'success' | 'info' | 'warning' | 'error'