diff --git a/src/constants/translations/en/offer-page.json b/src/constants/translations/en/offer-page.json
index 023cac3c0..499fe6e4d 100644
--- a/src/constants/translations/en/offer-page.json
+++ b/src/constants/translations/en/offer-page.json
@@ -12,7 +12,12 @@
"buttonTitles": {
"tutor": "Create offer",
"student": "Create request"
- }
+ },
+ "extendedSuccessMessage": {
+ "tutor": "You successfuly created an offer!;You can review all your offers on My Offers page.",
+ "student": "You successfuly created a request!;You can review all your requests on My Requests page."
+ },
+ "seeAll": "See all"
},
"editOffer": {
"title": {
diff --git a/src/constants/translations/uk/offer-page.json b/src/constants/translations/uk/offer-page.json
index 401dd5805..51b31f16b 100644
--- a/src/constants/translations/uk/offer-page.json
+++ b/src/constants/translations/uk/offer-page.json
@@ -12,7 +12,12 @@
"buttonTitles": {
"tutor": "Створити пропозицію",
"student": "Створити запит"
- }
+ },
+ "extendedSuccessMessage": {
+ "tutor": "Ви успішно створили пропозицію!;Ви можете переглянути всі свої пропозиції на сторінці \"Мої пропозиції\".",
+ "student": "Ви успішно створили запит!;Ви можете переглянути всі свої запити на сторінці \"Мої запити\"."
+ },
+ "seeAll": "Переглянути всі"
},
"editOffer": {
"title": {
diff --git a/src/containers/layout/app-snackbar/AppSnackbar.styles.ts b/src/containers/layout/app-snackbar/AppSnackbar.styles.ts
new file mode 100644
index 000000000..db1911ea2
--- /dev/null
+++ b/src/containers/layout/app-snackbar/AppSnackbar.styles.ts
@@ -0,0 +1,13 @@
+export const styles = {
+ alert: {
+ color: 'basic.white'
+ },
+ action: {
+ p: '4px 8px 0 30px',
+ cursor: 'pointer'
+ },
+ secondMessage: {
+ fontSize: '12px',
+ fontWeight: '300'
+ }
+}
diff --git a/src/containers/layout/app-snackbar/AppSnackbar.tsx b/src/containers/layout/app-snackbar/AppSnackbar.tsx
index f8d17a690..d72bf172e 100644
--- a/src/containers/layout/app-snackbar/AppSnackbar.tsx
+++ b/src/containers/layout/app-snackbar/AppSnackbar.tsx
@@ -2,22 +2,49 @@ import { useAppDispatch, useAppSelector } from '~/hooks/use-redux'
import Snackbar from '@mui/material/Snackbar'
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
+
import { closeAlert, snackbarSelector } from '~/redux/features/snackbarSlice'
import { useTranslation } from 'react-i18next'
+import { useNavigate } from 'react-router-dom'
+import { styles } from '~/containers/layout/app-snackbar/AppSnackbar.styles'
const AppSnackbar = () => {
- const { isOpened, message, duration, severity } =
+ const { isOpened, message, duration, severity, isExtended, route } =
useAppSelector(snackbarSelector)
const { t } = useTranslation()
-
const dispatch = useAppDispatch()
+ const navigate = useNavigate()
const handleClose = () => dispatch(closeAlert())
const translatedMessage =
typeof message === 'string' ? t(message) : t(message.text, message.options)
+ const actionBody = translatedMessage
+ .split(', ')
+ .map((line) => {line})
+
+ const handleButtonClick = () => {
+ navigate(route!)
+ handleClose()
+ }
+
+ const actionButton = (
+
+ {t('offerPage.createOffer.seeAll')}
+
+ )
+
+ const [firstMessage, secondMessage] = translatedMessage.split(';')
+
+ const extendedBody = (
+ <>
+ {firstMessage}
+ {secondMessage}
+ >
+ )
+
return (
{
onClose={handleClose}
open={isOpened}
>
-
- {translatedMessage.split(', ').map((line) => (
- {line}
- ))}
+
+ {isExtended ? extendedBody : actionBody}
)
diff --git a/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.constants.ts b/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.constants.ts
index 220827126..31f8b22f9 100644
--- a/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.constants.ts
+++ b/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.constants.ts
@@ -6,6 +6,7 @@ export const getInitialValues = (offer: Offer | null) => ({
subject: offer?.subject._id ?? '',
proficiencyLevel: offer?.proficiencyLevel ?? [],
languages: offer?.languages ?? [],
+ enrolledUsers: [],
title: offer?.title ?? '',
description: offer?.description ?? '',
price: offer?.price.toString() ?? '',
diff --git a/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.tsx b/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.tsx
index 4d09093f9..551028bb2 100644
--- a/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.tsx
+++ b/src/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.tsx
@@ -26,7 +26,6 @@ import {
ButtonVariantEnum,
ComponentEnum,
CreateOrUpdateOfferData,
- ErrorResponse,
Offer,
OfferActionsEnum,
ServiceFunction,
@@ -35,7 +34,6 @@ import {
} from '~/types'
import { styles } from '~/containers/offer-page/OfferPage.styles'
import { openAlert } from '~/redux/features/snackbarSlice'
-import { getErrorKey } from '~/utils/get-error-key'
interface CreateOrUpdateOfferProps {
existingOffer?: Offer | null
@@ -61,22 +59,28 @@ const CreateOrEditOffer: FC = ({
? OfferActionsEnum.Edit
: OfferActionsEnum.Create
- const onResponseError = (error?: ErrorResponse) => {
- dispatch(
- openAlert({
- severity: snackbarVariants.error,
- message: getErrorKey(error)
- })
- )
- }
const onResponse = (response: Offer | null) => {
+ const isHash = hash === '#offer'
+
dispatch(
- openAlert({
- severity: snackbarVariants.success,
- message: `offerPage.${offerAction}.successMessage`
- })
+ openAlert(
+ isHash
+ ? {
+ severity: snackbarVariants.success,
+ message: `offerPage.createOffer.extendedSuccessMessage.${userRole}`,
+ duration: 10000,
+ isExtended: true,
+ route: authRoutes.myOffers.path
+ }
+ : {
+ severity: snackbarVariants.success,
+ message: `offerPage.${offerAction}.successMessage`
+ }
+ )
)
+
closeDrawer()
+
if (hash == '#offer') {
navigate(`${authRoutes.myProfile.path}#complete`)
updateOffer!(true)
@@ -97,8 +101,7 @@ const CreateOrEditOffer: FC = ({
service,
fetchOnMount: false,
defaultResponse: null,
- onResponse,
- onResponseError
+ onResponse
})
const {
diff --git a/src/redux/features/snackbarSlice.ts b/src/redux/features/snackbarSlice.ts
index cd02ddd62..b83988a79 100644
--- a/src/redux/features/snackbarSlice.ts
+++ b/src/redux/features/snackbarSlice.ts
@@ -16,12 +16,16 @@ interface SnackbarState {
severity: AlertColor
message: SnackbarMessage
duration: number
+ isExtended?: boolean
+ route?: string
}
interface SnackbarOpenParams {
severity: AlertColor
message: SnackbarMessage
duration?: number
+ isExtended?: boolean
+ route?: string
}
type OpenSnackbarAction = PayloadAction
@@ -30,7 +34,9 @@ const initialState: SnackbarState = {
isOpened: false,
severity: 'info',
message: '',
- duration: 0
+ duration: 0,
+ isExtended: false,
+ route: ''
}
const snackbarSlice = createSlice({
@@ -42,6 +48,8 @@ const snackbarSlice = createSlice({
state.severity = action.payload.severity
state.message = action.payload.message
state.duration = action.payload.duration || 4000
+ state.isExtended = action.payload.isExtended ?? false
+ state.route = action.payload.route ?? ''
},
closeAlert: (state) => {
state.isOpened = false
diff --git a/tests/unit/containers/layout/app-snackbar/AppSnackbarExtended.spec.jsx b/tests/unit/containers/layout/app-snackbar/AppSnackbarExtended.spec.jsx
new file mode 100644
index 000000000..7d855003e
--- /dev/null
+++ b/tests/unit/containers/layout/app-snackbar/AppSnackbarExtended.spec.jsx
@@ -0,0 +1,67 @@
+import { screen, fireEvent, waitFor } from '@testing-library/react'
+import { renderWithProviders } from '~tests/test-utils'
+import LoginDialog from '~/containers/guest-home-page/login-dialog/LoginDialog'
+import { TestSnackbar } from '~tests/test-utils'
+import { vi } from 'vitest'
+import { useAppSelector } from '~/hooks/use-redux'
+
+const preloadedState = {
+ appMain: { loading: false, authLoading: false, userRole: '', error: '' }
+}
+const unwrap = vi.fn().mockRejectedValue({ data: { code: 'error' } })
+const loginUser = vi.fn().mockReturnValue({ unwrap })
+
+vi.mock('~/containers/guest-home-page/google-button/GoogleButton', () => ({
+ __esModule: true,
+ default: function () {
+ return
+ }
+}))
+
+vi.mock('~/services/auth-service', async () => {
+ return {
+ __esModule: true,
+ authService: {
+ endpoint: { matchFulfilled: vi.fn(), matchPending: vi.fn() }
+ },
+ useLoginMutation: () => [loginUser]
+ }
+})
+
+vi.mock('~/hooks/use-redux', async () => {
+ const actual = await vi.importActual('~/hooks/use-redux')
+ return {
+ ...actual,
+ useAppSelector: vi.fn()
+ }
+})
+
+describe('snackbar with extended', () => {
+ beforeEach(async () => {
+ vi.mocked(useAppSelector).mockReturnValue({
+ isOpened: true,
+ message: 'Test message; Additional info',
+ duration: 3000,
+ severity: 'info',
+ isExtended: true,
+ route: '/test-route'
+ })
+
+ renderWithProviders(
+
+
+ ,
+ preloadedState
+ )
+ })
+
+ it('should render extended content when isExtended is true', async () => {
+ const mainMessage = await screen.findByText('Test message')
+ const additionalInfo = await screen.findByText('Additional info')
+ const actionButton = await screen.findByText('offerPage.createOffer.seeAll')
+
+ expect(mainMessage).toBeInTheDocument()
+ expect(additionalInfo).toBeInTheDocument()
+ expect(actionButton).toBeInTheDocument()
+ })
+})
diff --git a/tests/unit/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.spec.jsx b/tests/unit/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.spec.jsx
new file mode 100644
index 000000000..858f3e885
--- /dev/null
+++ b/tests/unit/containers/offer-page/create-or-edit-offer/CreateOrEditOffer.spec.jsx
@@ -0,0 +1,123 @@
+import { screen, fireEvent, waitFor } from '@testing-library/react'
+import { configureStore } from '@reduxjs/toolkit'
+
+import { renderWithProviders } from '~tests/test-utils'
+import CreateOffer from '~/containers/offer-page/create-offer/CreateOffer'
+import snackbarReducer, { openAlert } from '~/redux/features/snackbarSlice'
+import { snackbarVariants } from '~/constants'
+import { expect } from 'vitest'
+import reducer from '~/redux/reducer'
+
+const mockDispatch = vi.fn()
+const mockCloseDrawer = vi.fn()
+const mockNavigate = vi.fn()
+
+vi.mock('~/hooks/use-axios', async () => {
+ const actual = await vi.importActual('~/hooks/use-axios')
+ return {
+ ...actual,
+ useAxios: vi.fn()
+ }
+})
+
+vi.mock('~/redux/features/snackbarSlice', async () => {
+ const actual = await vi.importActual('~/redux/features/snackbarSlice')
+ return {
+ ...actual,
+ openAlert: vi.fn()
+ }
+})
+
+vi.mock('~/hooks/use-redux', async () => {
+ const actual = await vi.importActual('~/hooks/use-redux')
+ return {
+ ...actual,
+ useAppDispatch: () => mockDispatch,
+ useAppSelector: vi.fn(() => ({ userRole: 'tutor' }))
+ }
+})
+
+vi.mock('~/services/offer-service', async () => {
+ const actual = await vi.importActual('~/services/offer-service')
+ const mockCreateOffer = vi.fn()
+ return {
+ ...actual,
+ OfferService: {
+ createOffer: mockCreateOffer
+ }
+ }
+})
+
+vi.mock('react-router-dom', async () => ({
+ ...(await vi.importActual('react-router-dom')),
+ useNavigate: () => mockNavigate
+}))
+
+const store = configureStore({
+ reducer: {
+ appMain: reducer,
+ snackbar: snackbarReducer
+ }
+})
+
+describe('CreateOrEditOffer', () => {
+ beforeEach(() => {
+ renderWithProviders(, {
+ store
+ })
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ mockDispatch.mockReset()
+ })
+
+ it('should call a dispatch and navigate on successful response', async () => {
+ const { OfferService } = await import('~/services/offer-service')
+ OfferService.createOffer.mockResolvedValue({})
+
+ const saveButton = screen.getByRole('button', {
+ name: /offerPage.createOffer.buttonTitles.tutor/i
+ })
+ fireEvent.click(saveButton)
+
+ waitFor(() => {
+ expect(mockDispatch).toHaveBeenCalledWith(
+ openAlert({
+ severity: snackbarVariants.success,
+ message: 'offerPage.createOffer.successMessage'
+ })
+ )
+ expect(mockCloseDrawer).toHaveBeenCalled()
+ expect(mockNavigate).toHaveBeenCalledWith(
+ expect.stringMatching(/^\/offer-details/)
+ )
+ })
+ })
+
+ it('should call different dispatch and navigate with #offer on successful response', () => {
+ vi.mock('react-router-dom', async () => ({
+ ...(await vi.importActual('react-router-dom')),
+ useLocation: () => ({ hash: '#offer' })
+ }))
+
+ const saveButton = screen.getByRole('button', {
+ name: /offerPage.createOffer.buttonTitles.tutor/i
+ })
+ fireEvent.click(saveButton)
+
+ waitFor(() => {
+ expect(mockDispatch).toHaveBeenCalledWith(
+ openAlert({
+ severity: snackbarVariants.success,
+ message: 'offerPage.createOffer.extendedSuccessMessage.tutor',
+ duration: 10000,
+ isExtended: true,
+ route: '/my-offers'
+ })
+ )
+ expect(mockCloseDrawer).toHaveBeenCalled()
+ expect(mockNavigate).toHaveBeenCalledWith('/my-profile#complete')
+ })
+ })
+})