diff --git a/backend/samfundet/migrations/0001_initial.py b/backend/samfundet/migrations/0001_initial.py index 0ea8f1fdf..f43b5adae 100644 --- a/backend/samfundet/migrations/0001_initial.py +++ b/backend/samfundet/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.9 on 2025-08-09 16:11 +# Generated by Django 5.2.10 on 2026-01-22 13:43 import datetime import django.contrib.auth.models @@ -138,8 +138,6 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(blank=True, editable=False, null=True)), ('message_nb', models.TextField(blank=True, null=True, verbose_name='Melding (norsk)')), ('message_en', models.TextField(blank=True, null=True, verbose_name='Melding (engelsk)')), - ('description_nb', models.TextField(blank=True, null=True, verbose_name='Beskrivelse (norsk)')), - ('description_en', models.TextField(blank=True, null=True, verbose_name='Beskrivelse (engelsk)')), ('start_dt', models.DateField(blank=True, verbose_name='Start dato')), ('end_dt', models.DateField(blank=True, verbose_name='Slutt dato')), ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index 84f9461ca..f85a12df2 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -258,8 +258,8 @@ class ClosedPeriod(CustomBaseModel): message_nb = models.TextField(blank=True, null=True, verbose_name='Melding (norsk)') message_en = models.TextField(blank=True, null=True, verbose_name='Melding (engelsk)') - description_nb = models.TextField(blank=True, null=True, verbose_name='Beskrivelse (norsk)') - description_en = models.TextField(blank=True, null=True, verbose_name='Beskrivelse (engelsk)') + # description_no = models.TextField(blank=True, null=True, verbose_name='Beskrivelse (norsk)') + # description_en = models.TextField(blank=True, null=True, verbose_name='Beskrivelse (engelsk)') start_dt = models.DateField(blank=True, null=False, verbose_name='Start dato') end_dt = models.DateField(blank=True, null=False, verbose_name='Slutt dato') diff --git a/frontend/src/Components/EventEditButtons/EventEditButtons.module.scss b/frontend/src/Components/EventEditButtons/EventEditButtons.module.scss new file mode 100644 index 000000000..82ec1687c --- /dev/null +++ b/frontend/src/Components/EventEditButtons/EventEditButtons.module.scss @@ -0,0 +1,46 @@ +.edit_icon { + color: white; + filter: brightness(1000%); + transition: all 200ms ease-in-out; +} + + +.edit_button { + pointer-events:all; + z-index: 10; + border-radius: 30%; + width: fit-content; + padding: 4px; + color: white; + cursor: pointer; + transition: all 200ms ease-in-out; + box-shadow: rgba(0, 0, 0, 0.24) 0 3px 8px; + text-decoration: none; +} + +.edit_button:hover { + .edit_icon { + rotate: 15deg; + } + scale: 1.1; + filter: brightness(110%); +} + +.default_edit { + @extend .edit_button; + background: #3498db; +} + +.detail_edit { + @extend .edit_button; + background: teal; +} + +.delete_edit { + @extend .edit_button; + background: crimson; + appearance: none; + border: none; +} + + diff --git a/frontend/src/Components/EventEditButtons/EventEditButtons.stories.tsx b/frontend/src/Components/EventEditButtons/EventEditButtons.stories.tsx new file mode 100644 index 000000000..cc29d29cd --- /dev/null +++ b/frontend/src/Components/EventEditButtons/EventEditButtons.stories.tsx @@ -0,0 +1,12 @@ +import type { Meta } from '@storybook/react'; +import { EventEditButtons } from './EventEditButtons'; + +const meta: Meta = { + title: 'Components/EventEditButtons', + component: EventEditButtons, + args: { + title: 'EditButtons', + }, +}; + +export default meta; diff --git a/frontend/src/Components/EventEditButtons/EventEditButtons.tsx b/frontend/src/Components/EventEditButtons/EventEditButtons.tsx new file mode 100644 index 000000000..e518e4ec9 --- /dev/null +++ b/frontend/src/Components/EventEditButtons/EventEditButtons.tsx @@ -0,0 +1,63 @@ +import { Icon } from '@iconify/react'; +import type { ReactNode } from 'react'; +import { deleteEvent } from '~/api'; +import { useAuthContext } from '~/context/AuthContext'; +import { reverse } from '~/named-urls'; +import { PERM } from '~/permissions'; +import { ROUTES } from '~/routes'; +import { hasPerm } from '~/utils'; +import { Link } from '../Link'; +import styles from './EventEditButtons.module.scss'; + +type EventEditButtons = { + title?: ReactNode; + id?: string; + icon_size?: number; +}; + +export function EventEditButtons({ title = 'event', id, icon_size = 17 }: EventEditButtons) { + const { user } = useAuthContext(); + const isStaff = user?.is_staff; + const canChangeEvent = hasPerm({ user: user, permission: PERM.SAMFUNDET_CHANGE_EVENT, obj: id }); + + const editUrl = reverse({ pattern: ROUTES.frontend.admin_events_edit, urlParams: { id: id } }); + const detailUrl = reverse({ + pattern: ROUTES.backend.admin__samfundet_event_change, + urlParams: { objectId: id }, + }); + + return ( + <> + {canChangeEvent && ( + + + + )} + {canChangeEvent && ( + + )} + {isStaff && canChangeEvent && ( + + + + )} + + ); +} diff --git a/frontend/src/Components/EventEditButtons/index.ts b/frontend/src/Components/EventEditButtons/index.ts new file mode 100644 index 000000000..fff6f9987 --- /dev/null +++ b/frontend/src/Components/EventEditButtons/index.ts @@ -0,0 +1 @@ +export { EventEditButtons } from './EventEditButtons'; diff --git a/frontend/src/Components/ImageCard/ImageCard.module.scss b/frontend/src/Components/ImageCard/ImageCard.module.scss index 4c017eece..8c1f11dfd 100644 --- a/frontend/src/Components/ImageCard/ImageCard.module.scss +++ b/frontend/src/Components/ImageCard/ImageCard.module.scss @@ -19,6 +19,7 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); // TODO color variables .container { + position: relative; display: flex; flex-direction: column; align-items: stretch; @@ -150,6 +151,23 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); float: right; } +.edit_bar { + opacity: 0%; + position: absolute; + display: flex; + flex-direction: row; + justify-content: end; + gap: 10px; + padding-right: 13px; + align-items: end; + width: 100%; + height: 55%; + pointer-events: none; + margin-left: 20px; + transition: all 200ms ease-in-out; + z-index: 10; +} + // Styling to container and children when it is hovered. .container:hover { .edit_button { @@ -173,6 +191,10 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); opacity: 1; max-height: 3.5em; } + .edit_bar { + opacity: 100%; + margin-left: 0; + } } // Compact doesn't show description on hover @@ -186,3 +208,11 @@ $card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); max-height: 0; } } + +.badges { + display: flex; + align-items: center; + justify-content: space-between; +} + + diff --git a/frontend/src/Components/ImageCard/ImageCard.tsx b/frontend/src/Components/ImageCard/ImageCard.tsx index 6ef891988..6b1924253 100644 --- a/frontend/src/Components/ImageCard/ImageCard.tsx +++ b/frontend/src/Components/ImageCard/ImageCard.tsx @@ -3,6 +3,7 @@ import { t } from 'i18next'; import type { ReactNode } from 'react'; import { useEffect, useState } from 'react'; import { Skeleton } from '~/Components'; +import { EventEditButtons } from '~/Components'; import { KEY } from '~/i18n/constants'; import { EventTicketType } from '~/types'; import { backgroundImageFromUrl } from '~/utils'; @@ -16,6 +17,7 @@ type ImageCardProps = { title?: ReactNode; subtitle?: ReactNode; description?: ReactNode; + id?: string; date?: string | Date; url?: string; imageUrl?: string; @@ -33,6 +35,7 @@ export function ImageCard({ subtitle = , description = , date, + id, url = '#', imageUrl, compact, @@ -48,6 +51,8 @@ export function ImageCard({ const [displayTicketType, setTicketType] = useState(''); const [showTicket, setShowTicket] = useState(false); + const icon_size = compact ? 14 : 17; + useEffect(() => { if (ticket_type === EventTicketType.FREE || ticket_type === EventTicketType.REGISTRATION) { setTicketType(t(KEY.common_ticket_type_free)); @@ -68,9 +73,10 @@ export function ImageCard({ return (
+
{id && }
-
+
{showTicket && }
diff --git a/frontend/src/Components/OpeningHours/OpeningHours.module.scss b/frontend/src/Components/OpeningHours/OpeningHours.module.scss index 25bf61013..f0b7d6f8a 100644 --- a/frontend/src/Components/OpeningHours/OpeningHours.module.scss +++ b/frontend/src/Components/OpeningHours/OpeningHours.module.scss @@ -26,3 +26,11 @@ margin: 0 0.2em; } +.closedBox { + border: 2px $red_samf solid; + border-radius: 10px; + padding: 20px; + margin-bottom: 2rem; + margin-top: 1rem; +} + diff --git a/frontend/src/Components/OpeningHours/OpeningHours.tsx b/frontend/src/Components/OpeningHours/OpeningHours.tsx index 6c4e9b57e..9c2a7f1ae 100644 --- a/frontend/src/Components/OpeningHours/OpeningHours.tsx +++ b/frontend/src/Components/OpeningHours/OpeningHours.tsx @@ -1,9 +1,13 @@ +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { TimeDuration } from '~/Components'; import { Link } from '~/Components/Link/Link'; import { Text } from '~/Components/Text/Text'; +import { getClosedPeriods } from '~/api'; +import { useGlobalContext } from '~/context/GlobalContextProvider'; import type { VenueDto } from '~/dto'; import { KEY } from '~/i18n/constants'; +import { dbT } from '~/utils'; import styles from './OpeningHours.module.scss'; type OpeningHoursProps = { @@ -14,6 +18,30 @@ type OpeningHoursProps = { export function OpeningHours({ venues, isLoading, isError }: OpeningHoursProps) { const { t } = useTranslation(); + const [isClosed, setIsClosed] = useState(false); + const [closedText, setClosedText] = useState('Samf is closed'); + const globalContext = useGlobalContext(); + + useEffect(() => { + if (globalContext.isClosed !== 'default') { + if (globalContext.isClosed === 'closed') { + setIsClosed(true); + setClosedText('Samfundet is closed'); + } + return; + } + getClosedPeriods().then((periods) => { + const now = new Date(); + for (const period of periods) { + if (new Date(period.start_dt) < now && now < new Date(period.end_dt)) { + setIsClosed(true); + console.log(period); + setClosedText(dbT(period, 'message')); + break; + } + } + }); + }); if (isLoading) { return {t(KEY.common_loading)}; @@ -25,34 +53,37 @@ export function OpeningHours({ venues, isLoading, isError }: OpeningHoursProps) const today = new Date().toISOString().split('T')[0]; const day = new Date().toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase(); - return (
{t(KEY.common_opening_hours)} - - {venues.map((venue) => { - const openingTime = venue[`opening_${day}` as keyof VenueDto] as string; - const closingTime = venue[`closing_${day}` as keyof VenueDto] as string; - return ( - - - - - ); - })} -
- -

{venue.name}

- -
- -
+ {isClosed ? ( +
{closedText}
+ ) : ( + + {venues.map((venue) => { + const openingTime = venue[`opening_${day}` as keyof VenueDto] as string; + const closingTime = venue[`closing_${day}` as keyof VenueDto] as string; + return ( + + + + + ); + })} +
+ +

{venue.name}

+ +
+ +
+ )}
); } diff --git a/frontend/src/Components/OpeningHours/OpeningHoursContainer.tsx b/frontend/src/Components/OpeningHours/OpeningHoursContainer.tsx index 4caba5795..65bbb7c46 100644 --- a/frontend/src/Components/OpeningHours/OpeningHoursContainer.tsx +++ b/frontend/src/Components/OpeningHours/OpeningHoursContainer.tsx @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { getOpenVenues } from '~/api'; import type { VenueDto } from '~/dto'; -import { venueKeys } from '~/queryKeys'; import { OpeningHours } from './OpeningHours'; export function OpeningHoursContainer() { @@ -10,7 +9,7 @@ export function OpeningHoursContainer() { isLoading, isError, } = useQuery({ - queryKey: venueKeys.open(), + queryKey: ['openVenues'], queryFn: getOpenVenues, }); diff --git a/frontend/src/Components/OpeningHours/index.ts b/frontend/src/Components/OpeningHours/index.ts index beb1448d2..37a62d505 100644 --- a/frontend/src/Components/OpeningHours/index.ts +++ b/frontend/src/Components/OpeningHours/index.ts @@ -1 +1,2 @@ export { OpeningHours } from './OpeningHours'; +export { OpeningHoursContainer } from './OpeningHoursContainer'; diff --git a/frontend/src/Components/index.ts b/frontend/src/Components/index.ts index 1ffd6c92e..a3118cc75 100644 --- a/frontend/src/Components/index.ts +++ b/frontend/src/Components/index.ts @@ -51,7 +51,7 @@ export { Navbar } from './Navbar'; export { NotificationBadge } from './NotificationBadge'; export { NumberInput } from './NumberInput'; export { OccupiedForm, OccupiedFormModal } from './OccupiedForm'; -export { OpeningHours } from './OpeningHours'; +export { OpeningHours, OpeningHoursContainer } from './OpeningHours'; export { Page } from './Page'; export { PagedPagination } from './Pagination'; export { PermissionRoute } from './PermissionRoute'; @@ -93,6 +93,7 @@ export { ToolTip } from './ToolTip'; export { UkaOutlet } from './UkaOutlet'; export { UserFeedback } from './UserFeedback'; export { Video } from './Video'; +export { EventEditButtons } from './EventEditButtons'; // Props export type { ButtonProps } from './Button'; export type { CheckboxProps } from './Checkbox'; diff --git a/frontend/src/Pages/EventPage/EventPage.module.scss b/frontend/src/Pages/EventPage/EventPage.module.scss index 7d3abf0d8..accc5c3cd 100644 --- a/frontend/src/Pages/EventPage/EventPage.module.scss +++ b/frontend/src/Pages/EventPage/EventPage.module.scss @@ -2,6 +2,14 @@ @use 'src/constants' as *; +.admin_panel { + display: flex; + flex-direction: row; + gap: 20px; + margin: 15px 0 10px 0; + color: white; +} + .container { width: 100%; height: 100%; @@ -74,4 +82,4 @@ padding: 0 0.15rem 0 0.25rem; border: 1px solid $black; border-radius: 2rem; -} \ No newline at end of file +} diff --git a/frontend/src/Pages/EventPage/EventPage.tsx b/frontend/src/Pages/EventPage/EventPage.tsx index b47e3f85b..4b818832c 100644 --- a/frontend/src/Pages/EventPage/EventPage.tsx +++ b/frontend/src/Pages/EventPage/EventPage.tsx @@ -2,14 +2,18 @@ import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import { ExpandableHeader, ExternalHostBox, H1, Image, Page } from '~/Components'; +import { EventEditButtons } from '~/Components'; import { BuyEventTicket } from '~/Components/BuyEventTicket/BuyEventTicket'; import { SamfMarkdown } from '~/Components/SamfMarkdown'; import { getEvent } from '~/api'; import { BACKEND_DOMAIN } from '~/constants'; +import { useAuthContext } from '~/context/AuthContext'; import { useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; +import { PERM } from '~/permissions'; import { eventKeys } from '~/queryKeys'; import { dbT } from '~/utils'; +import { hasPerm } from '~/utils'; import styles from './EventPage.module.scss'; import { EventInformation } from './components/EventInformation/EventInformation'; import { EventTable } from './components/EventTable'; @@ -17,6 +21,8 @@ import { EventTable } from './components/EventTable'; export function EventPage() { const { t } = useTranslation(); const { id } = useParams(); + const { user } = useAuthContext(); + const canChangeEvent = hasPerm({ user: user, permission: PERM.SAMFUNDET_CHANGE_EVENT, obj: id }); const { data: event, isLoading } = useQuery({ queryKey: id ? eventKeys.detail(Number(id)) : ['events', 'no-id'], @@ -31,6 +37,12 @@ export function EventPage() { {event && }
+ {canChangeEvent && ( +
+ +
+ )} +

{dbT(event, 'title')}

{event && } diff --git a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx index 0e9d70564..b6d2030b8 100644 --- a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx +++ b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx @@ -83,6 +83,7 @@ export function EventsList({ events }: EventsListProps) { subtitle={time_display} description={dbT(event, 'description_short') ?? ''} compact={true} + id={event.id.toString()} url={reverse({ pattern: ROUTES.frontend.event, urlParams: { id: event.id } })} ticket_type={event.ticket_type} host={event.host} diff --git a/frontend/src/Pages/HomePage/HomePage.tsx b/frontend/src/Pages/HomePage/HomePage.tsx index 7e14ecc40..74d88c513 100644 --- a/frontend/src/Pages/HomePage/HomePage.tsx +++ b/frontend/src/Pages/HomePage/HomePage.tsx @@ -1,6 +1,7 @@ import { type ReactNode, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; +import { OpeningHoursContainer } from '~/Components'; import { EventCarousel, LargeCard } from '~/Pages/HomePage/components'; import { getHomeData } from '~/api'; import type { HomePageDto, HomePageElementDto } from '~/dto'; @@ -50,6 +51,7 @@ export function HomePage() { return ( <> +
{/**/} {isLoading && skeleton} diff --git a/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx b/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx index 28bd7f3a8..0a8d04ff6 100644 --- a/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx +++ b/frontend/src/Pages/HomePage/components/EventCarousel/EventCarousel.tsx @@ -1,13 +1,11 @@ -import { Carousel, IconButton, ImageCard } from '~/Components'; +import { Carousel, ImageCard } from '~/Components'; import { BuyEventTicket } from '~/Components/BuyEventTicket/BuyEventTicket'; import { BACKEND_DOMAIN } from '~/constants'; import { useAuthContext } from '~/context/AuthContext'; import type { EventDto, HomePageElementDto } from '~/dto'; import { reverse } from '~/named-urls'; -import { PERM } from '~/permissions'; import { ROUTES } from '~/routes'; -import { COLORS } from '~/types'; -import { dbT, hasPerm } from '~/utils'; +import { dbT } from '~/utils'; import styles from './EventCarousel.module.scss'; type EventCarouselProps = { @@ -37,18 +35,13 @@ export function EventCarousel({ element, skeletonCount = 0 }: EventCarouselProps {element.events.map((event: EventDto) => { const url = reverse({ pattern: ROUTES.frontend.event, urlParams: { id: event.id } }); - const editUrl = reverse({ pattern: ROUTES.frontend.admin_events_edit, urlParams: { id: event.id } }); - const detailurl = reverse({ - pattern: ROUTES.backend.admin__samfundet_event_change, - urlParams: { objectId: event.id }, - }); - const canChangeEvent = hasPerm({ user: user, permission: PERM.SAMFUNDET_CHANGE_EVENT, obj: event.id }); const event_title = dbT(event, 'title') ?? ''; const event_short_dsc = dbT(event, 'description_short') ?? ''; return ( {event.billig && } - -
- {canChangeEvent && ( - - )} - {isStaff && canChangeEvent && ( - - )} -
); })} diff --git a/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss b/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss index 92b3009ab..eb91dd4b8 100644 --- a/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss +++ b/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss @@ -36,6 +36,7 @@ $header-bg-dark: #111111; flex-direction: row; padding-bottom: 1.5em; gap: 1em; + align-items: center; } .spinner_container { diff --git a/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.module.scss b/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.module.scss index 2a8635da1..02ae74981 100644 --- a/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.module.scss +++ b/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.module.scss @@ -1,3 +1,14 @@ @use 'src/mixins' as *; @use 'src/constants' as *; + +.admin_closed_override_container { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.admin_closed_radio { + margin-top: 10px; +} diff --git a/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.tsx b/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.tsx index 9c20e233b..62f5c268b 100644 --- a/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.tsx +++ b/frontend/src/PagesAdmin/ClosedPeriodAdminPage/ClosedPeriodAdminPage.tsx @@ -1,14 +1,16 @@ import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; -import { Button, TimeDisplay } from '~/Components'; +import { Button, RadioButton, TimeDisplay } from '~/Components'; import { Table } from '~/Components/Table'; import { deleteClosedPeriod, getClosedPeriods } from '~/api'; +import { useGlobalContext } from '~/context/GlobalContextProvider'; import type { ClosedPeriodDto } from '~/dto'; import { useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; import { reverse } from '~/named-urls'; import { ROUTES } from '~/routes'; +import { dbT } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './ClosedPeriodAdminPage.module.scss'; @@ -16,6 +18,7 @@ export function ClosedPeriodAdminPage() { const [closedPeriods, setClosedPeriods] = useState([]); const [showSpinner, setShowSpinner] = useState(true); const { t } = useTranslation(); + const globalContext = useGlobalContext(); useTitle(t(KEY.command_menu_shortcut_closed)); const getAllClosedPeriods = useCallback(() => { @@ -51,9 +54,37 @@ export function ClosedPeriodAdminPage() { } const header = ( - + <> + + + + {dbT({ text_nb: 'Stenging status', text_en: 'Closing status' }, 'text')} + + + {dbT({ text_nb: 'standar', text_en: 'default' }, 'text')} + + globalContext.setIsClosed('default')} + /> + + + {dbT({ text_nb: 'stengt', text_en: 'closed' }, 'text')} + + globalContext.setIsClosed('closed')} + /> + + + {dbT({ text_nb: 'åpent', text_en: 'open' }, 'text')} + + globalContext.setIsClosed('open')} /> + + + ); const backendUrl = ROUTES.backend.admin__samfundet_closedperiod_changelist; @@ -75,8 +106,7 @@ export function ClosedPeriodAdminPage() { ]} data={closedPeriods.map((element) => ({ cells: [ - element.message_no, - element.description_no, + element.message_nb, { content: }, { content: }, { @@ -98,7 +128,7 @@ export function ClosedPeriodAdminPage() { display="block" className={styles.smallButtons} onClick={() => { - if (window.confirm(`${t(KEY.form_confirm)} ${t(KEY.common_delete)} ${element.message_no}`)) { + if (window.confirm(`${t(KEY.form_confirm)} ${t(KEY.common_delete)} ${element.message_nb}`)) { deleteSelectedEvent(element.id); } }} diff --git a/frontend/src/PagesAdmin/ClosedPeriodFormAdminPage/ClosedPeriodFormAdminPage.tsx b/frontend/src/PagesAdmin/ClosedPeriodFormAdminPage/ClosedPeriodFormAdminPage.tsx index caaf9f9e2..99b48f9de 100644 --- a/frontend/src/PagesAdmin/ClosedPeriodFormAdminPage/ClosedPeriodFormAdminPage.tsx +++ b/frontend/src/PagesAdmin/ClosedPeriodFormAdminPage/ClosedPeriodFormAdminPage.tsx @@ -5,21 +5,20 @@ import { useParams } from 'react-router'; import { toast } from 'react-toastify'; import { SamfForm } from '~/Forms/SamfForm'; import { SamfFormField } from '~/Forms/SamfFormField'; -import { getClosedPeriod } from '~/api'; +import { getClosedPeriod, postClosedPeriod, putClosedPeriod } from '~/api'; import { useCustomNavigate, useTitle } from '~/hooks'; import { STATUS } from '~/http_status_codes'; import { KEY } from '~/i18n/constants'; +import { reverse } from '~/named-urls'; import { ROUTES } from '~/routes'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './ClosedPeriodFormAdminPage.module.scss'; type formType = { - message_no: string; + message_nb: string; message_en: string; - description_no: string; - description_en: string; - start_dt: Date; - end_dt: Date; + start_dt: string; + end_dt: string; }; export function ClosedPeriodFormAdminPage() { @@ -33,6 +32,8 @@ export function ClosedPeriodFormAdminPage() { // If form has a id, check if it exists, and then load that item. const { id } = useParams(); + const min_length_message = 10; + // Stuff to do on first render. // TODO add permissions on render // biome-ignore lint/correctness/useExhaustiveDependencies: t and navigate do not need to be in deplist @@ -46,13 +47,12 @@ export function ClosedPeriodFormAdminPage() { getClosedPeriod(id) .then((data) => { // setClosedPeriod(data); For posting + console.log(data); setInitialData({ - message_no: data.message_no, + message_nb: data.message_nb, message_en: data.message_en, - description_no: data.description_no, - description_en: data.description_en, - start_dt: new Date(data.start_dt), - end_dt: new Date(data.end_dt), + start_dt: data.start_dt, + end_dt: data.end_dt, }); setShowSpinner(false); }) @@ -68,41 +68,61 @@ export function ClosedPeriodFormAdminPage() { function handleOnSubmit(data: formType) { if (id !== undefined) { - // TODO patch data + putClosedPeriod(id, data) + .then() + .catch((err) => { + alert(err); + }); } else { - // TODO post data + postClosedPeriod(data) + .then(() => { + toast.success(t(KEY.common_creation_successful)); + navigate({ url: reverse({ pattern: ROUTES.frontend.admin_closed }) }); + }) + .catch(() => { + toast.error(t(KEY.common_something_went_wrong)); + }); } - alert('TODO Submit'); - console.log(JSON.stringify(data)); } const labelMessage = `${t(KEY.common_message)} under '${t(KEY.common_opening_hours)}'`; - const labelDescription = `${t(KEY.common_description)} under '${t(KEY.common_whatsup)}'`; const title = id ? t(KEY.admin_closed_period_edit_period) : t(KEY.admin_closed_period_new_period); useTitle(title); return ( - -
- - -
+ onSubmit={handleOnSubmit} initialData={initialData}>
state.message_nb.length > min_length_message} + field="message_nb" + required={true} type="text_long" - label={`${labelDescription} (${t(KEY.common_norwegian)})`} + label={`${labelMessage} (${t(KEY.common_norwegian)})`} /> state.message_en.length > min_length_message} + field="message_en" + required={true} type="text_long" - label={`${labelDescription} (${t(KEY.common_english)})`} + label={`${labelMessage} (${t(KEY.common_english)})`} />
- - + (state.end_dt ? new Date(state.start_dt) <= new Date(state.end_dt) : true)} + field="start_dt" + type="date" + label={`${t(KEY.start_time)}`} + /> + + state.start_dt ? new Date(state.start_dt) <= new Date(state.end_dt) : true + } + field="end_dt" + type="date" + label={`${t(KEY.end_time)}`} + />
diff --git a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx index bd860190c..c81e6aef7 100644 --- a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx @@ -132,6 +132,7 @@ export function EventsAdminPage() { return ( { +export async function postClosedPeriod(data: Partial): Promise { const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__closedperiods_list; const response = await axios.post(url, data, { withCredentials: true }); return response.data; diff --git a/frontend/src/context/GlobalContextProvider.tsx b/frontend/src/context/GlobalContextProvider.tsx index cd52db431..51bfdb5ee 100644 --- a/frontend/src/context/GlobalContextProvider.tsx +++ b/frontend/src/context/GlobalContextProvider.tsx @@ -30,6 +30,10 @@ type GlobalContextProps = { setIsMobileNavigation: SetState; keyValues: KeyValueMap; + + // AdminToggels + isClosed: string; + setIsClosed: React.Dispatch>; }; /** @@ -76,6 +80,8 @@ export function GlobalContextProvider({ children, enabled = true }: GlobalContex const [mirrorDimension, setMirrorDimension] = useState(false); const { isMouseTrail, setIsMouseTrail, toggleMouseTrail } = useMouseTrail(); + const [isClosed, setIsClosed] = useState('default'); + // =================================== // // Effects // // =================================== // @@ -158,6 +164,8 @@ export function GlobalContextProvider({ children, enabled = true }: GlobalContex toggleMouseTrail, toggleMirrorDimension, keyValues, + isClosed, + setIsClosed, }; return {children}; diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 4ecd509da..6b692d9a2 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -357,12 +357,10 @@ export type GangSectionDto = { export type ClosedPeriodDto = { id: number; - message_no: string; - description_no: string; + message_nb: string; message_en: string; - description_en: string; - start_dt: Date; - end_dt: Date; + start_dt: string; + end_dt: string; }; export type TagDto = {