diff --git a/backend/root/constants.py b/backend/root/constants.py index f4d9aab9a..f3a8198c3 100644 --- a/backend/root/constants.py +++ b/backend/root/constants.py @@ -58,6 +58,7 @@ class WebFeatures: ROLES = 'roles' GANGS = 'gangs' INFORMATION = 'information' + INFOBOX = 'infobox' DOCUMENTS = 'documents' RECRUITMENT = 'recruitment' SULTEN = 'sulten' @@ -79,6 +80,7 @@ class WebFeatures: WebFeatures.ROLES, WebFeatures.GANGS, WebFeatures.INFORMATION, + WebFeatures.INFOBOX, WebFeatures.DOCUMENTS, WebFeatures.RECRUITMENT, WebFeatures.SULTEN, diff --git a/backend/root/settings/base.py b/backend/root/settings/base.py index 12f4ff937..dcba6ffcc 100644 --- a/backend/root/settings/base.py +++ b/backend/root/settings/base.py @@ -327,4 +327,4 @@ DEFAULT_FROM_EMAIL = 'mg-web@samfundet.no' # For enabled features in the control panel -CP_ENABLED = {s.strip() for s in os.getenv('CP_ENABLED', 'events,images,opening_hours,closed_hours,venue').split(',') if s.strip()} & CP_FEATURES_ALL +CP_ENABLED = {s.strip() for s in os.getenv('CP_ENABLED', 'events,images,opening_hours,closed_hours,venue,infobox').split(',') if s.strip()} & CP_FEATURES_ALL diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 6f7cc6f0a..cd5ced9f1 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -163,6 +163,15 @@ admin__samfundet_closedperiod_delete = 'admin:samfundet_closedperiod_delete' admin__samfundet_closedperiod_change = 'admin:samfundet_closedperiod_change' adminsamfundetclosedperiod__objectId = '' +admin__samfundet_infobox_permissions = 'admin:samfundet_infobox_permissions' +admin__samfundet_infobox_permissions_manage_user = 'admin:samfundet_infobox_permissions_manage_user' +admin__samfundet_infobox_permissions_manage_group = 'admin:samfundet_infobox_permissions_manage_group' +admin__samfundet_infobox_changelist = 'admin:samfundet_infobox_changelist' +admin__samfundet_infobox_add = 'admin:samfundet_infobox_add' +admin__samfundet_infobox_history = 'admin:samfundet_infobox_history' +admin__samfundet_infobox_delete = 'admin:samfundet_infobox_delete' +admin__samfundet_infobox_change = 'admin:samfundet_infobox_change' +adminsamfundetinfobox__objectId = '' admin__samfundet_textitem_permissions = 'admin:samfundet_textitem_permissions' admin__samfundet_textitem_permissions_manage_user = 'admin:samfundet_textitem_permissions_manage_user' admin__samfundet_textitem_permissions_manage_group = 'admin:samfundet_textitem_permissions_manage_group' diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index 8189cfb7c..bc80ba465 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -584,7 +584,7 @@ class ClosedPeriodAdmin(CustomBaseAdmin): # list_select_related = True -@register_if_feature_enabled(WebFeatures.INFORMATION, Infobox) +@register_if_feature_enabled(WebFeatures.INFOBOX, Infobox) class InfoboxAdmin(CustomBaseAdmin): # ordering = [] sortable_by = ['id', 'title_nb'] diff --git a/backend/samfundet/view/general_views.py b/backend/samfundet/view/general_views.py index 929aa22cb..d3e78a17f 100644 --- a/backend/samfundet/view/general_views.py +++ b/backend/samfundet/view/general_views.py @@ -236,7 +236,11 @@ class InformationPageView(ModelViewSet): class InfoboxView(ModelViewSet): - permission_classes = (RoleProtectedOrAnonReadOnlyObjectPermissions,) + feature_key = WebFeatures.INFOBOX + permission_classes = ( + RoleProtectedOrAnonReadOnlyObjectPermissions, + FeatureEnabled, + ) serializer_class = InfoboxSerializer queryset = Infobox.objects.all() diff --git a/frontend/src/Pages/AdminPage/applets.ts b/frontend/src/Pages/AdminPage/applets.ts index 9a5c0461f..a6afe0eea 100644 --- a/frontend/src/Pages/AdminPage/applets.ts +++ b/frontend/src/Pages/AdminPage/applets.ts @@ -23,6 +23,14 @@ export const appletCategories: AdminAppletCategory[] = [ url: ROUTES.frontend.admin_information, feature: 'information', }, + { + title_nb: 'Infobokser', + title_en: 'Infoboxes', + perm: PERM.SAMFUNDET_ADD_INFOBOX, + icon: 'mdi:message-badge-outline', + url: ROUTES.frontend.admin_infobox, + feature: 'infobox', + }, { title_nb: 'Åpningstider', title_en: 'Opening hours', diff --git a/frontend/src/Pages/ComponentPage/ComponentPage.tsx b/frontend/src/Pages/ComponentPage/ComponentPage.tsx index 7250ffe1f..874910916 100644 --- a/frontend/src/Pages/ComponentPage/ComponentPage.tsx +++ b/frontend/src/Pages/ComponentPage/ComponentPage.tsx @@ -65,7 +65,7 @@ export function ComponentPage() { visibility_from_dt: new Date().toISOString(), visibility_to_dt: '', start_dt: new Date().toISOString(), - status: 'active', + status: 'public', ticket_type: 'free', title_en: 'Von August with a very long title just like this // 23:59', title_nb: 'Von August // 23:59', diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss index 8369434ec..5916c83b3 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss @@ -87,3 +87,92 @@ padding-bottom: 3.75em; gap: 0.5em; } + +.status_label_row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5em; +} + +.status_info_button { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: none; + border-radius: 999px; + padding: 0; + width: 1.35rem; + height: 1.35rem; + background: transparent; + color: $blue; + + &:hover { + background: rgba($blue, 0.12); + } + + &:focus-visible { + outline: 2px solid $blue; + outline-offset: 2px; + } +} + +.status_help_modal { + width: min(32rem, calc(100vw - 2rem)); +} + +.status_help_modal_header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; + + h3 { + margin: 0; + } +} + +.status_help_close_button { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: none; + border-radius: 999px; + width: 2rem; + height: 2rem; + background: transparent; + color: $grey-1; + + &:hover { + background: rgba($grey-2, 0.2); + } + + @include theme-dark { + color: $grey-4; + + &:hover { + background: rgba($grey-4, 0.2); + } + } +} + +.status_help_intro { + margin: 0.5rem 0 0; +} + +.status_help_list { + margin: 0.75rem 0 0; + padding-left: 1.2rem; + + li + li { + margin-top: 0.5rem; + } +} + +.status_help_actions { + display: flex; + justify-content: flex-end; + margin-top: 1.25rem; +} diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index e0706e0da..50ea114c2 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -13,8 +13,22 @@ import type { EventDto } from '~/dto'; import { usePrevious, useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; import { venueKeys } from '~/queryKeys'; -import { EventAgeRestriction, type EventAgeRestrictionValue, EventCategory, type EventCategoryValue } from '~/types'; -import { dbT, getAgeRestrictionKey, getEventCategoryKey, lowerCapitalize } from '~/utils'; +import { + EventAgeRestriction, + type EventAgeRestrictionValue, + EventCategory, + type EventCategoryValue, + type EventStatus, + EventStatusChoice, +} from '~/types'; +import { + dbT, + getAgeRestrictionKey, + getEventCategoryKey, + getEventStatusDescriptionTranslationKey, + getEventStatusTranslationKey, + lowerCapitalize, +} from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './EventCreatorAdminPage.module.scss'; import { type FormType, useEventCreatorForm } from './hooks/useEventCreatorForm'; @@ -30,6 +44,7 @@ import { InfoStep } from './steps/InfoStep'; import { PaymentStep } from './steps/PaymentStep'; import { SummaryStep } from './steps/SummaryStep'; import { TextStep } from './steps/TextStep'; +import type { EventStatusOption } from './types'; export function EventCreatorAdminPage() { const { t } = useTranslation(); @@ -57,6 +72,16 @@ export function EventCreatorAdminPage() { label: t(getAgeRestrictionKey(age)), })); + const availableEventStatuses: EventStatus[] = id + ? Object.values(EventStatusChoice) + : [EventStatusChoice.PUBLIC, EventStatusChoice.PRIVATE]; + + const eventStatusOptions: EventStatusOption[] = availableEventStatuses.map((status) => ({ + value: status, + label: t(getEventStatusTranslationKey(status)), + description: t(getEventStatusDescriptionTranslationKey(status)), + })); + const { form, watchedValues, buildPayload } = useEventCreatorForm({ event, defaultCategory: eventCategoryOptions[0]?.value ?? EventCategory.ART, @@ -68,7 +93,7 @@ export function EventCreatorAdminPage() { info: , payment: , graphics: , - summary: , + summary: , }; // Fetch event data using the event ID diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts index 6a9ca71a6..2add0a854 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorSchema.ts @@ -12,6 +12,7 @@ import { EVENT_LOCATION, EVENT_REGISTRATION_URL, EVENT_START_DT, + EVENT_STATUS, EVENT_TICKET_TYPE, EVENT_TITLE, EVENT_VISIBILITY_FROM_DT, @@ -51,6 +52,7 @@ export const eventSchema = z.object({ // Graphics image: OPTIONAL_IMAGE, // Summary/Publication date + status: EVENT_STATUS, visibility_from_dt: EVENT_VISIBILITY_FROM_DT, visibility_to_dt: EVENT_VISIBILITY_TO_DT, }); diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts b/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts index bc39fa4fd..69d85e4b7 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts @@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'; import type { z } from 'zod'; import type { EventDto, EventWriteDto } from '~/dto'; -import type { EventCategoryValue } from '~/types'; +import { type EventCategoryValue, EventStatusChoice } from '~/types'; import { utcTimestampToLocal } from '~/utils'; import { eventSchema } from '../EventCreatorSchema'; @@ -46,6 +46,7 @@ export function useEventCreatorForm(params: { custom_tickets: [], billig_id: undefined, image: undefined, + status: EventStatusChoice.PUBLIC, visibility_from_dt: '', visibility_to_dt: '', }, @@ -75,6 +76,7 @@ export function useEventCreatorForm(params: { custom_tickets: event.custom_tickets || [], billig_id: event.billig?.id, image: event.image ?? undefined, + status: event.status || EventStatusChoice.PUBLIC, visibility_from_dt: event.visibility_from_dt ? utcTimestampToLocal(event.visibility_from_dt, false) : '', visibility_to_dt: event.visibility_to_dt ? utcTimestampToLocal(event.visibility_to_dt, false) : '', }; diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/SummaryStep.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/SummaryStep.tsx index 3f95cc438..304e32f12 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/SummaryStep.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/SummaryStep.tsx @@ -1,27 +1,113 @@ +import { Icon } from '@iconify/react'; +import { useState } from 'react'; import type { UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from '~/Components'; +import { + Button, + Dropdown, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + Modal, + Text, +} from '~/Components'; import { KEY } from '~/i18n/constants'; import styles from '../EventCreatorAdminPage.module.scss'; import type { FormType } from '../hooks/useEventCreatorForm'; +import type { EventStatusOption } from '../types'; -export function SummaryStep({ form }: { form: UseFormReturn }) { +type Props = { + form: UseFormReturn; + eventStatusOptions: EventStatusOption[]; +}; + +export function SummaryStep({ form, eventStatusOptions }: Props) { const { t } = useTranslation(); + const [isStatusInfoOpen, setIsStatusInfoOpen] = useState(false); return ( - ( - - {t(KEY.saksdokumentpage_publication_date) ?? ''} - - - - - - )} - /> + <> +
+ ( + + {t(KEY.saksdokumentpage_publication_date) ?? ''} + + + + + + )} + /> + + ( + +
+ {t(KEY.event_status)} + +
+ + + + +
+ )} + /> +
+ + setIsStatusInfoOpen(false)} + className={styles.status_help_modal} + > +
+ + {t(KEY.event_status_help_title)} + + +
+ + {t(KEY.event_status_help_intro)} + +
    + {eventStatusOptions.map((option) => ( +
  • + {option.label}: {option.description} +
  • + ))} +
+ +
+ +
+
+ ); } diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/stepConfig.ts b/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/stepConfig.ts index e5027cbde..5891ad48f 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/stepConfig.ts +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/stepConfig.ts @@ -59,6 +59,6 @@ export const steps: EventCreatorStep[] = [ title_nb: 'Oppsummering', title_en: 'Summary', customIcon: 'ic:outline-remove-red-eye', - validate: (d) => !!d.visibility_from_dt, + validate: (d) => !!d.visibility_from_dt && !!d.status, }, ]; diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/types.ts b/frontend/src/PagesAdmin/EventCreatorAdminPage/types.ts new file mode 100644 index 000000000..0bce08a15 --- /dev/null +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/types.ts @@ -0,0 +1,6 @@ +import type { DropdownOption } from '~/Components/Dropdown/Dropdown'; +import type { EventStatus } from '~/types'; + +export type EventStatusOption = DropdownOption & { + description: string; +}; diff --git a/frontend/src/PagesAdmin/InfoboxAdminPage/InfoboxAdminPage.module.scss b/frontend/src/PagesAdmin/InfoboxAdminPage/InfoboxAdminPage.module.scss new file mode 100644 index 000000000..1cb022961 --- /dev/null +++ b/frontend/src/PagesAdmin/InfoboxAdminPage/InfoboxAdminPage.module.scss @@ -0,0 +1,20 @@ +@use 'src/mixins' as *; + +.table_container { + margin-top: 1.5em; +} + +.truncated_text { + display: block; + max-width: 28ch; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.color_preview { + width: 1.75rem; + height: 1.75rem; + border: 1px solid #c9c9c9; + border-radius: 0.25rem; +} diff --git a/frontend/src/PagesAdmin/InfoboxAdminPage/InfoboxAdminPage.tsx b/frontend/src/PagesAdmin/InfoboxAdminPage/InfoboxAdminPage.tsx new file mode 100644 index 000000000..5bfe90261 --- /dev/null +++ b/frontend/src/PagesAdmin/InfoboxAdminPage/InfoboxAdminPage.tsx @@ -0,0 +1,117 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'react-toastify'; +import { Button, Link } from '~/Components'; +import { CrudButtons } from '~/Components/CrudButtons/CrudButtons'; +import { Table } from '~/Components/Table'; +import { deleteInfobox, getInfoboxes } from '~/api'; +import { useCustomNavigate, useTitle } from '~/hooks'; +import { KEY } from '~/i18n/constants'; +import { reverse } from '~/named-urls'; +import { infoboxKeys } from '~/queryKeys'; +import { ROUTES } from '~/routes'; +import { dbT, lowerCapitalize } from '~/utils'; +import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; +import styles from './InfoboxAdminPage.module.scss'; + +export function InfoboxAdminPage() { + const navigate = useCustomNavigate(); + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const pageTitle = t(KEY.admin_infoboxes_title); + + useTitle(pageTitle); + + const { data: infoboxes = [], isLoading: showSpinner } = useQuery({ + queryKey: infoboxKeys.all, + queryFn: getInfoboxes, + }); + + const deleteInfoboxMutation = useMutation({ + mutationFn: (id: number) => deleteInfobox(id), + }); + + function handleDelete(id: number) { + deleteInfoboxMutation.mutate(id, { + onSuccess: () => { + toast.success(t(KEY.common_delete_successful)); + queryClient.invalidateQueries({ queryKey: infoboxKeys.all }); + }, + onError: (error) => { + toast.error(t(KEY.common_something_went_wrong)); + console.error(error); + }, + }); + } + + const tableColumns = [ + { content: t(KEY.common_title), sortable: true }, + { content: t(KEY.common_description), sortable: true }, + { content: t(KEY.common_color), sortable: true }, + { content: t(KEY.common_url), sortable: true }, + '', + ]; + + const tableData = infoboxes.map((infobox) => ({ + cells: [ + dbT(infobox, 'title') as string, + { + content: {dbT(infobox, 'text') as string}, + value: dbT(infobox, 'text') as string, + }, + { + content: ( +
+ ), + value: infobox.color, + }, + { + content: infobox.url ? ( + + {infobox.url} + + ) : ( + '-' + ), + value: infobox.url ?? '', + }, + { + content: ( + { + navigate({ + url: reverse({ + pattern: ROUTES.frontend.admin_infobox_edit, + urlParams: { id: infobox.id }, + }), + }); + }} + onDelete={() => { + if (window.confirm(t(KEY.form_confirm) ?? '')) { + handleDelete(infobox.id); + } + }} + /> + ), + }, + ], + })); + + const header = ( + + ); + + return ( + +
+ + + + ); +} diff --git a/frontend/src/PagesAdmin/InfoboxAdminPage/index.ts b/frontend/src/PagesAdmin/InfoboxAdminPage/index.ts new file mode 100644 index 000000000..057731956 --- /dev/null +++ b/frontend/src/PagesAdmin/InfoboxAdminPage/index.ts @@ -0,0 +1 @@ +export { InfoboxAdminPage } from './InfoboxAdminPage'; diff --git a/frontend/src/PagesAdmin/InfoboxFormAdminPage/InfoboxFormAdminPage.module.scss b/frontend/src/PagesAdmin/InfoboxFormAdminPage/InfoboxFormAdminPage.module.scss new file mode 100644 index 000000000..c525a699b --- /dev/null +++ b/frontend/src/PagesAdmin/InfoboxFormAdminPage/InfoboxFormAdminPage.module.scss @@ -0,0 +1,47 @@ +@use 'src/mixins' as *; + +.wrapper { + margin: 0; + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.input_row { + display: flex; + flex-direction: column; + gap: 1em; + + @include for-tablet-up { + flex-direction: row; + } +} + +.item { + flex-grow: 1; + max-width: calc(50% - 0.5em); +} + +.color_input_row { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.color_preview { + // ish the same properties as text input field. Fuck css + padding: 0.75em; + content: ''; + height: 2.9rem; + aspect-ratio: 1; + border: 1px solid #c9c9c9; + border-radius: 0.5rem; + margin-top: 0.5em; +} + +.action_row { + display: flex; + justify-content: flex-end; + margin: 1rem 0; +} diff --git a/frontend/src/PagesAdmin/InfoboxFormAdminPage/InfoboxFormAdminPage.tsx b/frontend/src/PagesAdmin/InfoboxFormAdminPage/InfoboxFormAdminPage.tsx new file mode 100644 index 000000000..7dd57db62 --- /dev/null +++ b/frontend/src/PagesAdmin/InfoboxFormAdminPage/InfoboxFormAdminPage.tsx @@ -0,0 +1,286 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router'; +import { toast } from 'react-toastify'; +import { z } from 'zod'; +import { Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, Textarea } from '~/Components'; +import { ImagePicker } from '~/Components/ImagePicker/ImagePicker'; +import { getImage, getInfobox, postInfobox, putInfobox } from '~/api'; +import type { ImageDto, InfoboxDto } from '~/dto'; +import { useCustomNavigate, useTitle } from '~/hooks'; +import { STATUS } from '~/http_status_codes'; +import { KEY } from '~/i18n/constants'; +import { infoboxKeys } from '~/queryKeys'; +import { ROUTES } from '~/routes'; +import { WEBSITE_URL } from '~/schema/url'; +import { lowerCapitalize } from '~/utils'; +import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; +import styles from './InfoboxFormAdminPage.module.scss'; + +const schema = z.object({ + title_nb: z.string().min(1), + text_nb: z.string().min(1), + title_en: z.string().min(1), + text_en: z.string().min(1), + color: z.string().min(1), + url: WEBSITE_URL.or(z.literal('')).optional(), + image: z.custom().optional(), +}); + +type InfoboxFormType = z.infer; + +function normalizeWebsiteUrl(url?: string): string | null { + const raw = url?.trim(); + if (!raw) { + return null; + } + if (raw.startsWith('http://') || raw.startsWith('https://')) { + return raw; + } + return `https://${raw}`; +} + +export function InfoboxFormAdminPage() { + const navigate = useCustomNavigate(); + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const { id } = useParams(); + const infoboxId = id ? Number(id) : undefined; + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + title_nb: '', + text_nb: '', + title_en: '', + text_en: '', + color: '', + url: '', + image: undefined, + }, + }); + + const { + data: editData, + isLoading: isLoadingEdit, + error: editQueryError, + } = useQuery({ + enabled: infoboxId !== undefined, + queryKey: infoboxId !== undefined ? [...infoboxKeys.detail(infoboxId), 'form'] : [...infoboxKeys.all, 'new'], + queryFn: async () => { + if (infoboxId === undefined) { + return undefined; + } + + const infobox = await getInfobox(infoboxId); + const image = infobox.image ? await getImage(infobox.image).catch(() => undefined) : undefined; + return { infobox, image }; + }, + }); + + useEffect(() => { + if (!editData) { + return; + } + + form.reset({ + title_nb: editData.infobox.title_nb ?? '', + text_nb: editData.infobox.text_nb ?? '', + title_en: editData.infobox.title_en, + text_en: editData.infobox.text_en, + color: editData.infobox.color, + url: editData.infobox.url ?? '', + image: editData.image, + }); + }, [editData, form]); + + useEffect(() => { + const error = editQueryError as AxiosError | null; + if (!error) { + return; + } + + if (error.request && error.request.status === STATUS.HTTP_404_NOT_FOUND) { + navigate({ url: ROUTES.frontend.admin_infobox, replace: true }); + return; + } + + toast.error(t(KEY.common_something_went_wrong)); + }, [editQueryError, navigate, t]); + + const createMutation = useMutation({ + mutationFn: postInfobox, + onSuccess: () => { + toast.success(t(KEY.common_creation_successful)); + queryClient.invalidateQueries({ queryKey: infoboxKeys.all }); + navigate({ url: ROUTES.frontend.admin_infobox }); + }, + onError: (error) => { + toast.error(t(KEY.common_something_went_wrong)); + }, + }); + + const updateMutation = useMutation({ + mutationFn: ({ itemId, payload }: { itemId: number; payload: Partial }) => putInfobox(itemId, payload), + onSuccess: () => { + toast.success(t(KEY.common_update_successful)); + queryClient.invalidateQueries({ queryKey: infoboxKeys.all }); + navigate({ url: ROUTES.frontend.admin_infobox }); + }, + onError: (error) => { + toast.error(t(KEY.common_something_went_wrong)); + }, + }); + + const isSubmitting = createMutation.isPending || updateMutation.isPending; + const showSpinner = isLoadingEdit || isSubmitting; + + const submitText = id ? t(KEY.common_save) : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.admin_infobox)}`); + const title = id + ? lowerCapitalize(`${t(KEY.common_edit)} ${t(KEY.admin_infobox)}`) + : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.admin_infobox)}`); + useTitle(title); + + function handleOnSubmit(data: InfoboxFormType) { + const payload: Partial = { + title_nb: data.title_nb, + text_nb: data.text_nb, + title_en: data.title_en, + text_en: data.text_en, + color: data.color, + url: normalizeWebsiteUrl(data.url), + image: data.image?.id ?? null, + }; + + if (infoboxId !== undefined) { + updateMutation.mutate({ itemId: infoboxId, payload }); + return; + } + + createMutation.mutate(payload); + } + + return ( + +
+ +
+
+ ( + + {`${t(KEY.common_norwegian)} ${t(KEY.common_title)}`} + + + + + + )} + /> + ( + + {`${t(KEY.common_english)} ${t(KEY.common_title)}`} + + + + + + )} + /> +
+
+ ( + + {`${t(KEY.common_norwegian)} ${t(KEY.common_description)}`} + +