feat: state redux toolkit & feedback slice
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"appwrite": "^13.0.1",
|
||||
"entgamers-database": "^0.0.5",
|
||||
"entgamers-panda-preset": "0.1.1",
|
||||
@@ -27,6 +28,7 @@
|
||||
"node-appwrite": "^11.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "^9.0.4",
|
||||
"sharp": "^0.33.1",
|
||||
"yup": "^1.3.3"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,4 +1,5 @@
|
||||
export interface Alert {
|
||||
id: string
|
||||
title: string
|
||||
message: string
|
||||
severity: 'success' | 'info' | 'warning' | 'error'
|
||||
|
||||
Reference in New Issue
Block a user