diff --git a/package-lock.json b/package-lock.json index 16dc1cd51..f62aaca19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "react-dom": "^18.1.0", "react-error-boundary": "^6.0.0", "react-gtm-module": "^2.0.11", - "react-i18next": "^15.0.1", + "react-i18next": "^16.0.0", "react-image-crop": "^11.0.6", "react-range-slider-input": "^3.2.1", "react-router": "^7.5.2", @@ -1868,22 +1868,14 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", @@ -8605,9 +8597,9 @@ } }, "node_modules/i18next": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.2.tgz", - "integrity": "sha512-xWxgK8GAaPYkV9ia2tdgbtdM+qiC+ysVTBPvXhpCORU/+QkeQe3BSI7Crr+c4ZXULN1PfnXG/HY2n7HGx4KKBg==", + "version": "25.5.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz", + "integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==", "funding": [ { "type": "individual", @@ -8624,7 +8616,7 @@ ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.10" + "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" @@ -11139,16 +11131,18 @@ "integrity": "sha512-8gyj4TTxeP7eEyc2QKawEuQoAZdjKvMY4pgWfycGmqGByhs17fR+zEBs0JUDq4US/l+vbTl+6zvUIx27iDo/Vw==" }, "node_modules/react-i18next": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.2.tgz", - "integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz", + "integrity": "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.0", + "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0" + "i18next": ">= 25.5.2", + "react": ">= 16.8.0", + "typescript": "^5" }, "peerDependenciesMeta": { "react-dom": { @@ -11156,6 +11150,9 @@ }, "react-native": { "optional": true + }, + "typescript": { + "optional": true } } }, diff --git a/package.json b/package.json index 44fbe8b7a..dc03e0e0f 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "react-dom": "^18.1.0", "react-error-boundary": "^6.0.0", "react-gtm-module": "^2.0.11", - "react-i18next": "^15.0.1", + "react-i18next": "^16.0.0", "react-image-crop": "^11.0.6", "react-range-slider-input": "^3.2.1", "react-router": "^7.5.2", diff --git a/react/assets/forus-webshop/scss/includes/blocks/block-faq.scss b/react/assets/forus-webshop/scss/includes/blocks/block-faq.scss index 1310e94db..bac9f4c14 100644 --- a/react/assets/forus-webshop/scss/includes/blocks/block-faq.scss +++ b/react/assets/forus-webshop/scss/includes/blocks/block-faq.scss @@ -115,10 +115,19 @@ } } - @media screen and (max-width: 1000px) { + @media screen and (max-width: 1200px) { + .block-faq-header, + .block-faq-group, + .faq-item { + width: 800px; + } + } + + @media screen and (max-width: 850px) { gap: 20px; .block-faq-header { + width: 100%; margin-bottom: 5px; .block-faq-title { @@ -130,7 +139,13 @@ } } + .block-faq-group { + width: 100%; + } + .faq-item { + width: 100%; + .faq-item-header { padding: 15px 14px 15px 15px; diff --git a/react/src/dashboard/components/modals/ModalVouchersUpload.tsx b/react/src/dashboard/components/modals/ModalVouchersUpload.tsx index 79b423398..a36112490 100644 --- a/react/src/dashboard/components/modals/ModalVouchersUpload.tsx +++ b/react/src/dashboard/components/modals/ModalVouchersUpload.tsx @@ -48,7 +48,7 @@ type CSVErrorProp = { type RowDataProp = { _uid?: string; amount?: number; - expires_at?: string; + expire_at?: string; note?: string; bsn?: string; email?: string; diff --git a/react/src/dashboard/services/VoucherService.ts b/react/src/dashboard/services/VoucherService.ts index 6d66b6416..18afcdcec 100644 --- a/react/src/dashboard/services/VoucherService.ts +++ b/react/src/dashboard/services/VoucherService.ts @@ -128,16 +128,16 @@ export class VoucherService { return this.apiRequest.post(`${this.prefix}/${organizationId}/sponsor/transactions`, data); } - public sampleCSVBudgetVoucher(expires_at = '2020-02-20'): string { - const headers = ['amount', 'expires_at', 'note', 'email', 'activate', 'activation_code', 'client_uid']; - const values = [10, expires_at, 'voorbeeld notitie', 'test@example.com', 0, 0, '']; + public sampleCSVBudgetVoucher(expire_at = '2020-02-20'): string { + const headers = ['amount', 'expire_at', 'note', 'email', 'activate', 'activation_code', 'client_uid']; + const values = [10, expire_at, 'voorbeeld notitie', 'test@example.com', 0, 0, '']; return Papa.unparse([headers, values]); } - public sampleCSVProductVoucher(product_id = null, expires_at = '2020-02-20'): string { - const headers = ['product_id', 'expires_at', 'note', 'email', 'activate', 'activation_code', 'client_uid']; - const values = [product_id, expires_at, 'voorbeeld notitie', 'test@example.com', 0, 0, '']; + public sampleCSVProductVoucher(product_id = null, expire_at = '2020-02-20'): string { + const headers = ['product_id', 'expire_at', 'note', 'email', 'activate', 'activation_code', 'client_uid']; + const values = [product_id, expire_at, 'voorbeeld notitie', 'test@example.com', 0, 0, '']; return Papa.unparse([headers, values]); } diff --git a/react/src/webshop/components/elements/top-navbar/TopNavbarSearch.tsx b/react/src/webshop/components/elements/top-navbar/TopNavbarSearch.tsx index e6959006c..29e6c47bd 100644 --- a/react/src/webshop/components/elements/top-navbar/TopNavbarSearch.tsx +++ b/react/src/webshop/components/elements/top-navbar/TopNavbarSearch.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useNavigateState, useStateRoutes } from '../../../modules/state_router/Router'; import useAppConfigs from '../../../hooks/useAppConfigs'; import { mainContext } from '../../../contexts/MainContext'; @@ -21,6 +21,8 @@ import TopNavbarSearchResultItem from './TopNavbarSearchResultItem'; import useSetProgress from '../../../../dashboard/hooks/useSetProgress'; import { clickOnKeyEnter } from '../../../../dashboard/helpers/wcag'; import classNames from 'classnames'; +import { ResponseError } from '../../../../dashboard/props/ApiResponses'; +import usePushDanger from '../../../../dashboard/hooks/usePushDanger'; export type SearchResultGroupLocal = SearchResultGroup & { shown?: boolean; @@ -34,18 +36,22 @@ export type SearchResultLocal = { export default function TopNavbarSearch({ autoFocus = false }: { autoFocus?: boolean }) { const appConfigs = useAppConfigs(); - const { route } = useStateRoutes(); - const { setShowSearchBox, searchFilter } = useContext(mainContext); + const { setShowSearchBox } = useContext(mainContext); const inputRef = useRef(null); const translate = useTranslate(); const navigateState = useNavigateState(); const searchService = useSearchService(); + const pushDanger = usePushDanger(); const setProgress = useSetProgress(); + const hideSearchDropdown = useRef(false); + const searchingForDropdown = useRef(false); + const [dropdown, setDropdown] = useState(false); const [searchFocused, setSearchFocused] = useState(false); + const { route: currentState } = useStateRoutes(); const [results, setResults] = useState(null); const [resultsAll, setResultsAll] = useState>(null); @@ -59,14 +65,7 @@ export default function TopNavbarSearch({ autoFocus = false }: { autoFocus?: boo q: '', }); - const { resetFilters, update: filterUpdate } = filters; - const { update: updateSearchFilters } = searchFilter; - - const globalQuery = useMemo(() => searchFilter?.values?.q, [searchFilter?.values?.q]); - - const isSearchResultPage = useMemo(() => { - return route.state.name === 'search-result'; - }, [route?.state?.name]); + const { resetFilters } = filters; const hideDropDown = useCallback(() => { setDropdown(false); @@ -123,34 +122,44 @@ export default function TopNavbarSearch({ autoFocus = false }: { autoFocus?: boo useEffect(() => { setLastQuery(filters.activeValues.q); - if (isSearchResultPage) { - return; - } - if (!filters.activeValues.q || filters.activeValues.q?.length == 0) { return clearSearch(); } + setProgress(0); + searchingForDropdown.current = true; searchService .searchWithOverview({ q: filters.activeValues.q, with_external: 1, take: 9 }) - .then((res) => updateResults(res.data.data)) - .finally(() => setProgress(100)); - }, [filters.activeValues.q, isSearchResultPage, searchService, clearSearch, updateResults, setProgress]); + .then((res) => { + updateResults(res.data.data); + + if (hideSearchDropdown.current) { + hideDropDown(); + } + }) + .catch((err: ResponseError) => { + pushDanger(translate('push.error'), err.data?.message); + }) + .finally(() => { + setProgress(100); + hideSearchDropdown.current = false; + searchingForDropdown.current = false; + }); + }, [ + filters.activeValues.q, + searchService, + clearSearch, + updateResults, + setProgress, + hideDropDown, + pushDanger, + translate, + ]); useEffect(() => { - let timer: number; - - if (isSearchResultPage) { - timer = window.setTimeout(() => updateSearchFilters({ q: filters.values.q })); - } - - return () => window.clearTimeout(timer); - }, [filters.values.q, isSearchResultPage, updateSearchFilters]); - - useEffect(() => { - filterUpdate({ q: globalQuery }); - }, [filterUpdate, globalQuery]); + clearSearch(); + }, [currentState?.state?.name, clearSearch]); return (
@@ -159,11 +168,13 @@ export default function TopNavbarSearch({ autoFocus = false }: { autoFocus?: boo e?.preventDefault(); e?.stopPropagation(); - hideSearchBox(); - - if (!isSearchResultPage) { - navigateState('search-result', {}, { q: filters.values.q }); + clearSearch(); + if (searchingForDropdown.current) { + hideSearchDropdown.current = true; } + + navigateState('search-result', {}, { q: filters.values.q }); + document.querySelector('#main_search')?.focus(); }} className={`search-form form ${resultsAll?.length > 0 ? 'search-form-found' : ''}`}> diff --git a/react/src/webshop/components/pages/search/Search.tsx b/react/src/webshop/components/pages/search/Search.tsx index 9d77a2cf9..5f209e854 100644 --- a/react/src/webshop/components/pages/search/Search.tsx +++ b/react/src/webshop/components/pages/search/Search.tsx @@ -1,7 +1,6 @@ -import React, { Fragment, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import useTranslate from '../../../../dashboard/hooks/useTranslate'; import useAuthIdentity from '../../../hooks/useAuthIdentity'; -import { mainContext } from '../../../contexts/MainContext'; import { SearchItem, useSearchService } from '../../../services/SearchService'; import { useFundService } from '../../../services/FundService'; import { useOrganizationService } from '../../../../dashboard/services/OrganizationService'; @@ -23,6 +22,7 @@ import { clickOnKeyEnter, clickOnKeyEnterOrSpace } from '../../../../dashboard/h import { PaginationData } from '../../../../dashboard/props/ApiResponses'; import PayoutTransaction from '../../../../dashboard/props/models/PayoutTransaction'; import usePayoutTransactionService from '../../../services/PayoutTransactionService'; +import UIControlText from '../../../../dashboard/components/elements/forms/ui-controls/UIControlText'; export default function Search() { const authIdentity = useAuthIdentity(); @@ -37,14 +37,9 @@ export default function Search() { const productCategoryService = useProductCategoryService(); const payoutTransactionService = usePayoutTransactionService(); - const { searchFilter } = useContext(mainContext); - const [displayType, setDisplayType] = useState<'list' | 'grid'>('list'); const [searchItems, setSearchItems] = useState>(null); - const globalQuery = useMemo(() => searchFilter?.values?.q, [searchFilter?.values?.q]); - const [globalInitialized, setGlobalInitialized] = useState(false); - // Search direction const [sortByOptions] = useState< Array<{ @@ -248,18 +243,6 @@ export default function Search() { ); }, [doSearch, filterValuesActive, sortByOptions]); - useEffect(() => { - setGlobalInitialized(true); - - if (!globalInitialized && filterValues?.q) { - setTimeout(() => searchFilter.update({ q: filterValues.q }), 150); - } - }, [filterValues.q, globalInitialized, searchFilter]); - - useEffect(() => { - filterUpdate({ q: globalQuery }); - }, [filterUpdate, globalQuery]); - return ( +
+ + filterUpdate({ q })} + ariaLabel={translate('search.filters.search')} + id="main_search" + /> +
+
{translate('search.filters.highlighted')}
{searchItemTypes?.map((itemType) => (
@@ -358,10 +354,9 @@ export default function Search() {
- {translate('search.title')} {filterValuesActive.q && ( - {' ' + translate('search.filters.found_for', { query: filterValuesActive.q })} + {translate('search.filters.found_for', { query: filterValuesActive.q })} )}
diff --git a/react/src/webshop/contexts/MainContext.tsx b/react/src/webshop/contexts/MainContext.tsx index f0d0646b9..b950d3d8c 100644 --- a/react/src/webshop/contexts/MainContext.tsx +++ b/react/src/webshop/contexts/MainContext.tsx @@ -2,9 +2,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { createContext } from 'react'; import { AppConfigProp, useConfigService } from '../../dashboard/services/ConfigService'; import EnvDataWebshopProp from '../../props/EnvDataWebshopProp'; -import useFilter from '../../dashboard/hooks/useFilter'; -import FilterScope from '../../dashboard/types/FilterScope'; -import { useStateRoutes } from '../modules/state_router/Router'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import Language from '../../dashboard/props/models/Language'; @@ -20,10 +17,7 @@ interface AuthMemoProps { setMobileMenuOpened?: React.Dispatch>; userMenuOpened?: boolean; setUserMenuOpened?: React.Dispatch>; - searchQuery?: string; cookiesAccepted?: boolean; - setSearchQuery?: React.Dispatch>; - searchFilter?: FilterScope<{ q: string }>; language?: string; setLanguage?: React.Dispatch>; changeLanguage?: (locale: string) => void; @@ -43,7 +37,6 @@ const MainProvider = ({ children, cookiesAccepted }: { children: React.ReactElem const [showSearchBox, setShowSearchBox] = useState(false); const [mobileMenuOpened, setMobileMenuOpened] = useState(false); const [userMenuOpened, setUserMenuOpened] = useState(false); - const { route } = useStateRoutes(); const { i18n } = useTranslation(); const [language, setLanguage] = useState(i18n.language); @@ -51,12 +44,6 @@ const MainProvider = ({ children, cookiesAccepted }: { children: React.ReactElem return appConfigs?.languages; }, [appConfigs?.languages]); - const searchFilter = useFilter({ - q: '', - }); - - const { update: searchFilterUpdate } = searchFilter; - const changeLanguage = useCallback( (lang: string) => { setLanguage(lang); @@ -82,10 +69,6 @@ const MainProvider = ({ children, cookiesAccepted }: { children: React.ReactElem }); }, [configService, envData?.type]); - useEffect(() => { - searchFilterUpdate({ q: '' }); - }, [route.pathname, route?.state?.name, searchFilterUpdate]); - return (