diff --git a/app/reducers/surveys.ts b/app/reducers/surveys.ts index de3abce123..3155f5442c 100644 --- a/app/reducers/surveys.ts +++ b/app/reducers/surveys.ts @@ -40,14 +40,17 @@ const surveysSlice = createSlice({ export default surveysSlice.reducer; export const { - selectAll: selectAllSurveys, + selectAll, selectById: selectSurveyById, selectByField: selectSurveysByField, } = legoAdapter.getSelectors((state: RootState) => state.surveys); -export const selectSurveyTemplates = createSelector( - selectAllSurveys, - (surveys) => surveys.filter((survey) => survey.templateType), +export const selectAllSurveys = createSelector(selectAll, (surveys) => + surveys.filter((survey) => !survey.templateType), +); + +export const selectSurveyTemplates = createSelector(selectAll, (surveys) => + surveys.filter((survey) => survey.templateType), ); export type TransformedSurveyTemplate = Overwrite< diff --git a/app/routes/events/components/Calendar.tsx b/app/routes/events/components/Calendar.tsx index 449499e8fe..a9b207e9d3 100644 --- a/app/routes/events/components/Calendar.tsx +++ b/app/routes/events/components/Calendar.tsx @@ -1,16 +1,15 @@ -import { Icon, LinkButton, Page } from '@webkom/lego-bricks'; +import { Icon } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import moment, { type Moment } from 'moment-timezone'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; import { fetchEvents } from 'app/actions/EventActions'; import { useCurrentUser } from 'app/reducers/auth'; -import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { useAppDispatch } from 'app/store/hooks'; import createMonthlyCalendar from 'app/utils/createMonthlyCalendar'; import styles from './Calendar.module.css'; import CalendarCell from './CalendarCell'; import EventFooter from './EventFooter'; -import EventsTabs from './EventsTabs'; const WEEKDAYS = ['Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør', 'Søn']; @@ -35,7 +34,6 @@ const Calendar = () => { const { month, year } = useParams<{ month: string; year: string }>(); const date = getDate(month, year); - const actionGrant = useAppSelector((state) => state.events.actionGrant); const currentUser = useCurrentUser(); const icalToken = currentUser?.icalToken; @@ -63,15 +61,7 @@ const Calendar = () => { ); return ( - Lag nytt - ) - } - tabs={} - > + <>

@@ -100,8 +90,9 @@ const Calendar = () => { /> ))} + {icalToken && } - + ); }; diff --git a/app/routes/events/components/EventList.tsx b/app/routes/events/components/EventList.tsx index d0027586f9..f1bec1ab04 100644 --- a/app/routes/events/components/EventList.tsx +++ b/app/routes/events/components/EventList.tsx @@ -1,26 +1,15 @@ -import { - Button, - FilterSection, - filterSidebar, - Flex, - Icon, - LinkButton, - Page, - Skeleton, -} from '@webkom/lego-bricks'; +import { Button, Flex, Icon, Skeleton } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import { isEmpty, orderBy } from 'lodash'; import { FilterX, FolderOpen } from 'lucide-react'; import moment from 'moment-timezone'; import { useState, useEffect } from 'react'; import { Helmet } from 'react-helmet-async'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { fetchEvents } from 'app/actions/EventActions'; import EmptyState from 'app/components/EmptyState'; import EventItem from 'app/components/EventItem'; import eventItemStyles from 'app/components/EventItem/styles.module.css'; -import { CheckBox, RadioButton } from 'app/components/Form/'; -import ToggleSwitch from 'app/components/Form/ToggleSwitch'; import { EventTime } from 'app/models'; import { useCurrentUser, useIsLoggedIn } from 'app/reducers/auth'; import { selectAllEvents } from 'app/reducers/events'; @@ -28,21 +17,11 @@ import { selectPaginationNext } from 'app/reducers/selectors'; import joblistingListStyles from 'app/routes/joblistings/components/JoblistingList.module.css'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { EntityType } from 'app/store/models/entities'; -import useQuery from 'app/utils/useQuery'; import EventFooter from './EventFooter'; import styles from './EventList.module.css'; -import EventsTabs from './EventsTabs'; +import type { EventsOutletContext } from './EventsOverview'; import type { ListEvent } from 'app/store/models/Event'; -type FilterEventType = 'company_presentation' | 'course' | 'social' | 'other'; -type FilterRegistrationsType = 'all' | 'open' | 'future'; - -export const eventListDefaultQuery = { - eventTypes: [] as FilterEventType[], - registrations: 'all' as FilterRegistrationsType, - showPrevious: '' as '' | 'true' | 'false', -}; - type GroupedEvents = { currentWeek?: ListEvent[]; nextWeek?: ListEvent[]; @@ -96,54 +75,18 @@ const EventListGroup = ({ ); }; -type Option = { - filterRegDateFunc: (event: ListEvent) => boolean; - label: string; - value: FilterRegistrationsType; - field: EventTime; -}; -const filterRegDateOptions: Option[] = [ - { - filterRegDateFunc: (event) => !!event, - label: 'Vis alle', - value: 'all', - field: EventTime.start, - }, - { - filterRegDateFunc: (event) => - event.activationTime != null && - moment(event.activationTime).isBefore(moment()), - label: 'Påmelding åpnet', - value: 'open', - field: EventTime.start, - }, - { - filterRegDateFunc: (event) => - event.activationTime != null && - moment(event.activationTime).isAfter(moment()), - label: 'Åpner i fremtiden', - value: 'future', - field: EventTime.activate, - }, -]; - const EventList = () => { - const { query, setQueryValue } = useQuery(eventListDefaultQuery); - - const regDateFilter = - filterRegDateOptions.find( - (option) => option.value === query.registrations, - ) || filterRegDateOptions[0]; + const { + query, + regDateFilter, + showCourse, + showSocial, + showOther, + showCompanyPresentation, + } = useOutletContext(); const { field, filterRegDateFunc } = regDateFilter; - const showCourse = query.eventTypes.includes('course'); - const showSocial = query.eventTypes.includes('social'); - const showOther = query.eventTypes.includes('other'); - const showCompanyPresentation = query.eventTypes.includes( - 'company_presentation', - ); - const icalToken = useCurrentUser()?.icalToken; const loggedIn = useIsLoggedIn(); @@ -180,7 +123,6 @@ const EventList = () => { }), ); - const actionGrant = useAppSelector((state) => state.events.actionGrant); const events = useAppSelector((state) => selectAllEvents(state, { pagination }), ); @@ -286,80 +228,8 @@ const EventList = () => { navigate(pathname); }; - const toggleEventType = - (type: 'company_presentation' | 'course' | 'social' | 'other') => () => { - setQueryValue('eventTypes')( - query.eventTypes.includes(type) - ? query.eventTypes.filter((t) => t !== type) - : [...query.eventTypes, type], - ); - }; - return ( - - - - setQueryValue('showPrevious')(checked ? 'true' : 'false') - } - /> - - - - - - - - - {filterRegDateOptions.map((option) => ( - { - setQueryValue('registrations')(option.value); - }} - /> - ))} - - - ), - })} - actionButtons={ - actionGrant?.includes('create') && ( - Lag nytt - ) - } - tabs={} - > + <> @@ -420,7 +290,7 @@ const EventList = () => { )}
{icalToken && } - + ); }; diff --git a/app/routes/events/components/EventsOverview.tsx b/app/routes/events/components/EventsOverview.tsx new file mode 100644 index 0000000000..360d661616 --- /dev/null +++ b/app/routes/events/components/EventsOverview.tsx @@ -0,0 +1,189 @@ +import { + LinkButton, + Page, + filterSidebar, + FilterSection, +} from '@webkom/lego-bricks'; +import moment from 'moment-timezone'; +import { Outlet, useLocation } from 'react-router-dom'; +import { CheckBox, RadioButton } from 'app/components/Form'; +import ToggleSwitch from 'app/components/Form/ToggleSwitch'; +import { NavigationTab } from 'app/components/NavigationTab/NavigationTab'; +import { EventTime } from 'app/models'; +import { useAppSelector } from 'app/store/hooks'; +import useQuery from 'app/utils/useQuery'; +import type { ListEvent } from 'app/store/models/Event'; +import type { ParsedQs } from 'qs'; + +type FilterEventType = 'company_presentation' | 'course' | 'social' | 'other'; +type FilterRegistrationsType = 'all' | 'open' | 'future'; + +export const eventListDefaultQuery = { + eventTypes: [] as FilterEventType[], + registrations: 'all' as FilterRegistrationsType, + showPrevious: '' as '' | 'true' | 'false', +}; + +type Option = { + filterRegDateFunc: (event: ListEvent) => boolean; + label: string; + value: FilterRegistrationsType; + field: EventTime; +}; +const filterRegDateOptions: Option[] = [ + { + filterRegDateFunc: (event) => !!event, + label: 'Vis alle', + value: 'all', + field: EventTime.start, + }, + { + filterRegDateFunc: (event) => + event.activationTime != null && + moment(event.activationTime).isBefore(moment()), + label: 'Påmelding åpnet', + value: 'open', + field: EventTime.start, + }, + { + filterRegDateFunc: (event) => + event.activationTime != null && + moment(event.activationTime).isAfter(moment()), + label: 'Åpner i fremtiden', + value: 'future', + field: EventTime.activate, + }, +]; + +type EventsOutletContext = { + query: ParsedQs; + regDateFilter: Option; + showCourse: boolean; + showSocial: boolean; + showOther: boolean; + showCompanyPresentation: boolean; +}; + +const EventsOverview = () => { + const actionGrant = useAppSelector((state) => state.events.actionGrant); + + const location = useLocation(); + const showFilters = location.pathname === '/events'; + + const { query, setQueryValue } = useQuery(eventListDefaultQuery); + + const showCourse = query.eventTypes.includes('course'); + const showSocial = query.eventTypes.includes('social'); + const showOther = query.eventTypes.includes('other'); + const showCompanyPresentation = query.eventTypes.includes( + 'company_presentation', + ); + + const toggleEventType = + (type: 'company_presentation' | 'course' | 'social' | 'other') => () => { + setQueryValue('eventTypes')( + query.eventTypes.includes(type) + ? query.eventTypes.filter((t) => t !== type) + : [...query.eventTypes, type], + ); + }; + + const regDateFilter = + filterRegDateOptions.find( + (option) => option.value === query.registrations, + ) || filterRegDateOptions[0]; + + return ( + + + + setQueryValue('showPrevious')( + checked ? 'true' : 'false', + ) + } + /> + + + + + + + + + {filterRegDateOptions.map((option) => ( + { + setQueryValue('registrations')(option.value); + }} + /> + ))} + + + ), + }) + : undefined + } + actionButtons={ + actionGrant.includes('create') && ( + Lag nytt + ) + } + tabs={ + <> + Oversikt + + Kalender + + + } + > + + + ); +}; + +export default EventsOverview; +export type { EventsOutletContext }; diff --git a/app/routes/events/components/EventsTabs.tsx b/app/routes/events/components/EventsTabs.tsx deleted file mode 100644 index d57c29cd78..0000000000 --- a/app/routes/events/components/EventsTabs.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { NavigationTab } from 'app/components/NavigationTab/NavigationTab'; - -const EventsTabs = () => ( - <> - Oversikt - - Kalender - - -); - -export default EventsTabs; diff --git a/app/routes/events/index.tsx b/app/routes/events/index.tsx index a6cbb2022e..311aedda01 100644 --- a/app/routes/events/index.tsx +++ b/app/routes/events/index.tsx @@ -7,10 +7,17 @@ const Calendar = loadable(() => import('./components/Calendar')); const EventDetail = loadable(() => import('./components/EventDetail')); const EventEditor = loadable(() => import('./components/EventEditor')); const EventList = loadable(() => import('./components/EventList')); +const EventsOverview = loadable(() => import('./components/EventsOverview')); const eventsRoute: RouteObject[] = [ - { index: true, Component: EventList }, - { path: 'calendar/:year?/:month?', Component: Calendar }, + { + path: '', + Component: EventsOverview, + children: [ + { index: true, Component: EventList }, + { path: 'calendar/:year?/:month?', Component: Calendar }, + ], + }, { path: 'create', Component: EventEditor }, { path: ':eventIdOrSlug', Component: EventDetail }, { path: ':eventIdOrSlug/edit', Component: EventEditor }, diff --git a/app/routes/frontpage/components/CompactEvents.tsx b/app/routes/frontpage/components/CompactEvents.tsx index fcc59cfae2..8c6a250830 100644 --- a/app/routes/frontpage/components/CompactEvents.tsx +++ b/app/routes/frontpage/components/CompactEvents.tsx @@ -7,7 +7,7 @@ import EmptyState from 'app/components/EmptyState'; import Time from 'app/components/Time'; import Tooltip from 'app/components/Tooltip'; import { selectAllEvents } from 'app/reducers/events'; -import { eventListDefaultQuery } from 'app/routes/events/components/EventList'; +import { eventListDefaultQuery } from 'app/routes/events/components/EventsOverview'; import { colorForEventType } from 'app/routes/events/utils'; import { useAppSelector } from 'app/store/hooks'; import { EventType } from 'app/store/models/Event'; diff --git a/app/routes/surveys/components/Submissions/SubmissionsPage.tsx b/app/routes/surveys/components/Submissions/SubmissionsPage.tsx index 0e036a55f0..f57c1cbc9a 100644 --- a/app/routes/surveys/components/Submissions/SubmissionsPage.tsx +++ b/app/routes/surveys/components/Submissions/SubmissionsPage.tsx @@ -1,18 +1,18 @@ -import { LoadingIndicator, Page, PageCover } from '@webkom/lego-bricks'; -import { useParams } from 'react-router-dom'; +import { useOutletContext } from 'react-router-dom'; import { ContentSection, ContentMain } from 'app/components/Content'; -import { useFetchedSurveySubmissions } from 'app/reducers/surveySubmissions'; -import { useFetchedSurvey } from 'app/reducers/surveys'; import { useAppSelector } from 'app/store/hooks'; import { guardLogin } from 'app/utils/replaceUnlessLoggedIn'; -import { SurveyDetailTabs, getCsvUrl, getPdfUrl } from '../../utils'; +import { getCsvUrl, getPdfUrl } from '../../utils'; import AdminSideBar from '../AdminSideBar'; +import type { SurveysRouteContext } from 'app/routes/surveys'; +import type { EventForSurvey } from 'app/store/models/Event'; import type { DetailedSurvey } from 'app/store/models/Survey'; import type { SurveySubmission } from 'app/store/models/SurveySubmission'; import type { ComponentType } from 'react'; type ChildProps = { survey: DetailedSurvey; + event: EventForSurvey; submissions: SurveySubmission[]; }; export type SubmissionsPageChild = ComponentType; @@ -21,73 +21,41 @@ type Props = { children: SubmissionsPageChild; }; -type SubmissionsPageParams = { - surveyId: string; -}; - const SubmissionsPage = ({ children: Children }: Props) => { - const { surveyId } = - useParams() as SubmissionsPageParams; - const { survey, event } = useFetchedSurvey('surveySubmissions', surveyId); - const submissions = useFetchedSurveySubmissions( - 'surveySubmissions', - surveyId, - ); - const fetching = useAppSelector( - (state) => state.surveys.fetching || state.surveySubmissions.fetching, - ); + const { survey, event, submissions } = + useOutletContext(); const authToken = useAppSelector((state) => state.auth.token); - if (!survey) { - return ; - } - return ( - - } - title={survey.title} - back={{ href: '/surveys' }} - tabs={} - > - - - - - - { - const blob = await fetch(getCsvUrl(survey.id), { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }).then((response) => response.blob()); - return { - url: URL.createObjectURL(blob), - filename: survey.title.replace(/ /g, '_') + '.csv', - }; - }} - exportSurveyPDF={async () => { - const blob = await fetch(getPdfUrl(survey.id), { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }).then((response) => response.blob()); - return { - url: URL.createObjectURL(blob), - filename: survey.title.replace(/ /g, '_') + '.pdf', - }; - }} - /> - - + + + + + + { + const blob = await fetch(getCsvUrl(survey!.id), { + headers: { Authorization: `Bearer ${authToken}` }, + }).then((response) => response.blob()); + return { + url: URL.createObjectURL(blob), + filename: survey!.title.replace(/ /g, '_') + '.csv', + }; + }} + exportSurveyPDF={async () => { + const blob = await fetch(getPdfUrl(survey!.id), { + headers: { Authorization: `Bearer ${authToken}` }, + }).then((response) => response.blob()); + return { + url: URL.createObjectURL(blob), + filename: survey!.title.replace(/ /g, '_') + '.pdf', + }; + }} + /> + ); }; diff --git a/app/routes/surveys/components/SurveyDetail.tsx b/app/routes/surveys/components/SurveyDetail.tsx index fa8ac6388d..154d54dbf6 100644 --- a/app/routes/surveys/components/SurveyDetail.tsx +++ b/app/routes/surveys/components/SurveyDetail.tsx @@ -1,16 +1,19 @@ -import { LinkButton, LoadingPage, Page, PageCover } from '@webkom/lego-bricks'; -import { Helmet } from 'react-helmet-async'; -import { Link, useNavigate, useParams } from 'react-router-dom'; +import { LinkButton, LoadingPage } from '@webkom/lego-bricks'; +import { + Link, + useNavigate, + useOutletContext, + useParams, +} from 'react-router-dom'; import { ContentSection, ContentMain } from 'app/components/Content'; import Time from 'app/components/Time'; -import { useFetchedSurvey } from 'app/reducers/surveys'; import { displayNameForEventType } from 'app/routes/events/utils'; import { useAppSelector } from 'app/store/hooks'; import { guardLogin } from 'app/utils/replaceUnlessLoggedIn'; -import { SurveyDetailTabs } from '../utils'; import AdminSideBar from './AdminSideBar'; import StaticSubmission from './StaticSubmission'; import styles from './surveys.module.css'; +import type { SurveysRouteContext } from 'app/routes/surveys'; type SurveyDetailPageParams = { surveyId: string; @@ -18,7 +21,8 @@ type SurveyDetailPageParams = { const SurveyDetailPage = () => { const { surveyId } = useParams() as SurveyDetailPageParams; - const { survey, event } = useFetchedSurvey('surveyDetail', surveyId); + const { survey, event } = useOutletContext(); + const fetching = useAppSelector((state) => state.surveys.fetching); const actionGrant = survey?.actionGrant; @@ -32,61 +36,43 @@ const SurveyDetailPage = () => { navigate(`/surveys/${surveyId}/answer`); } - const isTemplate = !!survey.templateType; - return ( - - ) - } - title={survey.title} - back={{ href: `/surveys/${isTemplate ? 'templates' : ''}` }} - tabs={!isTemplate && } - > - - - - - {survey.templateType ? ( -

- Dette er malen for arrangementer av type{' '} - {displayNameForEventType(survey.templateType)} -

- ) : ( - <> -
- Spørreundersøkelse for{' '} - {event.title} -
+ + + {survey.templateType ? ( +

+ Dette er malen for arrangementer av type{' '} + {displayNameForEventType(survey.templateType)} +

+ ) : ( + <> +
+ Spørreundersøkelse for{' '} + {event.title} +
-
- Aktiv fra
+
+ Aktiv fra
- - Svar på undersøkelsen - - - )} - -
+ + Svar på undersøkelsen + + + )} + +
- -
-
+ + ); }; diff --git a/app/routes/surveys/components/SurveyList/SurveyList.tsx b/app/routes/surveys/components/SurveyList/SurveyList.tsx index 2f82223a59..ba5e1ca6c4 100644 --- a/app/routes/surveys/components/SurveyList/SurveyList.tsx +++ b/app/routes/surveys/components/SurveyList/SurveyList.tsx @@ -7,14 +7,15 @@ import type { DetailedSurvey } from 'app/store/models/Survey'; type Props = { surveys: DetailedSurvey[]; fetching: boolean; + isTemplates?: boolean; }; -const SurveyList = ({ surveys, fetching }: Props) => { +const SurveyList = ({ surveys, fetching, isTemplates }: Props) => { if (isEmpty(surveys) && !fetching) { return ( } - body="Ingen spørreundersøkelser funnet" + body={`Ingen ${isTemplates ? 'maler' : 'spørreundersøkelser'} funnet`} /> ); } diff --git a/app/routes/surveys/components/SurveyList/SurveyListPage.tsx b/app/routes/surveys/components/SurveyList/SurveyListPage.tsx index 89fc6347b0..aced6e5892 100644 --- a/app/routes/surveys/components/SurveyList/SurveyListPage.tsx +++ b/app/routes/surveys/components/SurveyList/SurveyListPage.tsx @@ -1,56 +1,32 @@ -import { LinkButton, Page } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; import { Helmet } from 'react-helmet-async'; -import { - fetchAll as fetchSurveys, - fetchTemplates, -} from 'app/actions/SurveyActions'; -import { NavigationTab } from 'app/components/NavigationTab/NavigationTab'; +import { fetchAll as fetchSurveys } from 'app/actions/SurveyActions'; import Paginator from 'app/components/Paginator'; import { selectPaginationNext } from 'app/reducers/selectors'; -import { selectAllSurveys, selectSurveyTemplates } from 'app/reducers/surveys'; +import { selectAllSurveys } from 'app/reducers/surveys'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { EntityType } from 'app/store/models/entities'; import { guardLogin } from 'app/utils/replaceUnlessLoggedIn'; import SurveyList from './SurveyList'; import type { DetailedSurvey } from 'app/store/models/Survey'; -type Props = { - templates?: boolean; -}; -const SurveyListPage = ({ templates }: Props) => { +const SurveyListPage = () => { const dispatch = useAppDispatch(); - const fetchAll = templates ? fetchTemplates : fetchSurveys; - usePreparedEffect('fetchSurveys', () => dispatch(fetchAll()), [templates]); + usePreparedEffect('fetchSurveys', () => dispatch(fetchSurveys()), []); - const surveys = useAppSelector((state) => - templates - ? selectSurveyTemplates(state) - : selectAllSurveys(state).filter((survey) => !survey.templateType), - ) as DetailedSurvey[]; + const surveys = useAppSelector(selectAllSurveys) as DetailedSurvey[]; const { pagination } = useAppSelector( selectPaginationNext({ - endpoint: templates ? '/survey-templates/' : '/surveys/', + endpoint: '/surveys/', entity: EntityType.Surveys, query: {}, }), ); return ( - Ny undersøkelse - } - tabs={ - <> - Undersøkelser - Maler - - } - > + <> { fetching={pagination.fetching} fetchNext={() => { dispatch( - fetchAll({ + fetchSurveys({ next: true, }), ); @@ -66,7 +42,7 @@ const SurveyListPage = ({ templates }: Props) => { > - + ); }; diff --git a/app/routes/surveys/components/SurveyList/SurveyTemplatesListPage.tsx b/app/routes/surveys/components/SurveyList/SurveyTemplatesListPage.tsx new file mode 100644 index 0000000000..2f29d729bc --- /dev/null +++ b/app/routes/surveys/components/SurveyList/SurveyTemplatesListPage.tsx @@ -0,0 +1,57 @@ +import { usePreparedEffect } from '@webkom/react-prepare'; +import { Helmet } from 'react-helmet-async'; +import { fetchTemplates } from 'app/actions/SurveyActions'; +import Paginator from 'app/components/Paginator'; +import { selectPaginationNext } from 'app/reducers/selectors'; +import { selectSurveyTemplates } from 'app/reducers/surveys'; +import { useAppDispatch, useAppSelector } from 'app/store/hooks'; +import { EntityType } from 'app/store/models/entities'; +import { guardLogin } from 'app/utils/replaceUnlessLoggedIn'; +import SurveyList from './SurveyList'; +import type { DetailedSurvey } from 'app/store/models/Survey'; + +const SurveyTemplatesListPage = () => { + const dispatch = useAppDispatch(); + + usePreparedEffect( + 'fetchSurveyTemplates', + () => dispatch(fetchTemplates()), + [], + ); + + const surveys = useAppSelector(selectSurveyTemplates) as DetailedSurvey[]; + + const { pagination } = useAppSelector( + selectPaginationNext({ + endpoint: '/survey-templates/', + entity: EntityType.Surveys, + query: {}, + }), + ); + + return ( + <> + + + { + dispatch( + fetchTemplates({ + next: true, + }), + ); + }} + > + + + + ); +}; + +export default guardLogin(SurveyTemplatesListPage); diff --git a/app/routes/surveys/components/SurveysWrapper.tsx b/app/routes/surveys/components/SurveysWrapper.tsx new file mode 100644 index 0000000000..f5fa4e2d9b --- /dev/null +++ b/app/routes/surveys/components/SurveysWrapper.tsx @@ -0,0 +1,49 @@ +import { LoadingPage, Page, PageCover } from '@webkom/lego-bricks'; +import { Helmet } from 'react-helmet-async'; +import { Outlet, useParams } from 'react-router-dom'; +import { useFetchedSurveySubmissions } from 'app/reducers/surveySubmissions'; +import { useFetchedSurvey } from 'app/reducers/surveys'; +import { useAppSelector } from 'app/store/hooks'; +import { SurveyDetailTabs } from '../utils'; +import type { SurveysRouteContext } from 'app/routes/surveys'; + +type Params = { + surveyId: string; +}; + +const SurveysWrapper = () => { + const { surveyId } = useParams() as Params; + const { survey, event } = useFetchedSurvey('surveyWrapper', surveyId); + const submissions = useFetchedSurveySubmissions( + 'surveySubmissions', + surveyId, + ); + const fetching = useAppSelector((state) => state.surveys.fetching); + + if (!survey) { + return ; + } + + const isTemplate = !!survey.templateType; + + return ( + + ) + } + title={survey.title} + back={{ href: `/surveys/${isTemplate ? 'templates' : ''}` }} + tabs={!isTemplate && } + > + + + + ); +}; + +export default SurveysWrapper; diff --git a/app/routes/surveys/index.tsx b/app/routes/surveys/index.tsx index a28f3a7d35..093aa58496 100644 --- a/app/routes/surveys/index.tsx +++ b/app/routes/surveys/index.tsx @@ -1,12 +1,20 @@ import loadable from '@loadable/component'; +import { Page } from '@webkom/lego-bricks'; import { type RouteObject, Outlet } from 'react-router-dom'; +import { NavigationTab } from 'app/components/NavigationTab/NavigationTab'; +import { LinkButton } from 'packages/lego-bricks/src/components/Button'; import pageNotFound from '../pageNotFound'; +import type { EventForSurvey } from 'app/store/models/Event'; import type { DetailedSurvey } from 'app/store/models/Survey'; import type { SurveySubmission } from 'app/store/models/SurveySubmission'; const SurveyListPage = loadable( () => import('./components/SurveyList/SurveyListPage'), ); +const SurveyTemplatesListPage = loadable( + () => import('./components/SurveyList/SurveyTemplatesListPage'), +); +const SurveysWrapper = loadable(() => import('./components/SurveysWrapper')); const SurveyDetailPage = loadable(() => import('./components/SurveyDetail')); const AddSurveyPage = loadable( () => import('./components/SurveyEditor/AddSurveyPage'), @@ -30,32 +38,68 @@ const SubmissionPublicResultsPage = loadable( () => import('./components/Submissions/SubmissionPublicResultsPage'), ); +const SurveysOverview = () => { + return ( + Ny undersøkelse + } + tabs={ + <> + Undersøkelser + + Maler + + + } + > + + + ); +}; + export type SurveysRouteContext = { - submissions: SurveySubmission[]; survey: DetailedSurvey; + event: EventForSurvey; + submissions: SurveySubmission[]; }; const surveysRoute: RouteObject[] = [ - { index: true, Component: SurveyListPage }, - { path: 'templates', Component: () => }, + { + path: '', + Component: SurveysOverview, + children: [ + { index: true, Component: SurveyListPage }, + { path: 'templates', Component: SurveyTemplatesListPage }, + ], + }, { path: 'add', Component: AddSurveyPage }, - { path: ':surveyId', Component: SurveyDetailPage }, { path: ':surveyId/edit', Component: EditSurveyPage }, { path: ':surveyId/answer', Component: AddSubmissionPage }, { - path: ':surveyId/submissions/*', - Component: () => ( - - {({ submissions, survey }) => ( - - )} - - ), + path: ':surveyId', + Component: SurveysWrapper, children: [ - { path: 'summary', Component: SubmissionsSummary }, - { path: 'individual', Component: SubmissionsIndividual }, + { index: true, Component: SurveyDetailPage }, + { + path: 'submissions', + Component: () => ( + + {({ survey, event, submissions }) => ( + + )} + + ), + children: [ + { path: 'summary', Component: SubmissionsSummary }, + { path: 'individual', Component: SubmissionsIndividual }, + ], + }, ], }, { path: ':surveyId/results', Component: SubmissionPublicResultsPage }, diff --git a/packages/lego-bricks/src/components/Layout/Page/Page.module.css b/packages/lego-bricks/src/components/Layout/Page/Page.module.css index b05c1a40d7..84e4204736 100644 --- a/packages/lego-bricks/src/components/Layout/Page/Page.module.css +++ b/packages/lego-bricks/src/components/Layout/Page/Page.module.css @@ -11,6 +11,7 @@ align-items: center; width: fit-content; margin-bottom: var(--spacing-sm); + outline: none; } span.backLabel { @@ -22,6 +23,7 @@ transition: transform var(--easing-medium); } + .back:focus > .backIcon, .back:hover > .backIcon { transform: translateX(calc(-1 * var(--spacing-xs))); } diff --git a/packages/lego-bricks/src/components/Tabs/Tab.module.css b/packages/lego-bricks/src/components/Tabs/Tab.module.css index 5dac8e5631..67559b95e5 100644 --- a/packages/lego-bricks/src/components/Tabs/Tab.module.css +++ b/packages/lego-bricks/src/components/Tabs/Tab.module.css @@ -1,15 +1,28 @@ +.tabContainer { + position: relative; + display: inline-flex; + flex-direction: column; +} + .tab { display: inline-block; color: var(--lego-font-color); padding: var(--spacing-sm) var(--spacing-md); - border-bottom: 2px solid var(--border-gray); -} - -.tab.active { - border-bottom: 2px solid var(--color-black); + cursor: pointer; + outline: none; } .tab.disabled { - color: var(--border-gray); + opacity: 0.5; pointer-events: none; } + +.tabIndicator { + position: absolute; + bottom: -2px; + height: 2px; + background-color: var(--color-black); + transition: + transform var(--easing-medium), + width var(--easing-medium); +} diff --git a/packages/lego-bricks/src/components/Tabs/Tab.tsx b/packages/lego-bricks/src/components/Tabs/Tab.tsx index 51280577c5..b6f535b81e 100644 --- a/packages/lego-bricks/src/components/Tabs/Tab.tsx +++ b/packages/lego-bricks/src/components/Tabs/Tab.tsx @@ -13,31 +13,31 @@ type Props = { }; export const Tab = ({ active, disabled, onPress, href, children }: Props) => { - const className = cx( - styles.tab, - active && styles.active, - disabled && styles.disabled, - ); + const className = cx(styles.tab, disabled && styles.disabled); - return href ? ( - - {children} - - ) : ( - + return ( +
+ {href ? ( + + {children} + + ) : ( + + )} +
); }; diff --git a/packages/lego-bricks/src/components/Tabs/TabContainer.module.css b/packages/lego-bricks/src/components/Tabs/TabContainer.module.css index 724f2f6c8e..c1e2a3bfbb 100644 --- a/packages/lego-bricks/src/components/Tabs/TabContainer.module.css +++ b/packages/lego-bricks/src/components/Tabs/TabContainer.module.css @@ -1,8 +1,24 @@ .container { margin: var(--spacing-sm) 0 var(--spacing-md); + position: relative; + display: flex; + flex-wrap: wrap; } -.spacer { - flex-grow: 1; +.tabList { + display: flex; + position: relative; border-bottom: 2px solid var(--border-gray); + width: 100%; +} + +.indicator { + position: absolute; + bottom: -2px; + left: 0; + height: 2px; + background-color: var(--color-black); + transition: + transform var(--easing-medium), + width var(--easing-medium); } diff --git a/packages/lego-bricks/src/components/Tabs/TabContainer.tsx b/packages/lego-bricks/src/components/Tabs/TabContainer.tsx index 33117a7a2b..8c0f9a7d3d 100644 --- a/packages/lego-bricks/src/components/Tabs/TabContainer.tsx +++ b/packages/lego-bricks/src/components/Tabs/TabContainer.tsx @@ -1,5 +1,6 @@ import cx from 'classnames'; -import { Flex } from '../Layout'; +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import styles from './TabContainer.module.css'; import type { ReactNode } from 'react'; @@ -9,9 +10,28 @@ type Props = { children?: ReactNode; }; -export const TabContainer = ({ className, lineColor, children }: Props) => ( - - {children} -
- -); +export const TabContainer = ({ className, lineColor, children }: Props) => { + const [indicatorStyle, setIndicatorStyle] = useState({}); + const location = useLocation(); + + useEffect(() => { + const activeTab = document.querySelector('[data-active="true"]'); + if (activeTab) { + const width = activeTab.clientWidth; + const left = (activeTab as HTMLElement).offsetLeft; + setIndicatorStyle({ + width: `${width}px`, + transform: `translateX(${left}px)`, + }); + } + }, [children, location.pathname]); + + return ( +
+
+ {children} +
+
+
+ ); +};