From 29efa129b75efef5f6e509028ae7334e95920f2a Mon Sep 17 00:00:00 2001 From: Tor Madsen Date: Thu, 26 Feb 2026 21:49:07 +0100 Subject: [PATCH 01/79] added functionallity for replacing images in admin images page. Deleting image functionallity not implemented yet. --- backend/samfundet/serializers.py | 47 +++++ .../AdminImage/AdminEditImage.module.scss | 75 +++++++ .../components/AdminImage/AdminEditImage.tsx | 188 ++++++++++++++++++ .../components/AdminImage/AdminImage.tsx | 25 ++- .../src/PagesAdmin/ImageAdminPage/index.ts | 1 + frontend/src/api.ts | 11 + frontend/src/i18n/constants.ts | 2 + frontend/src/i18n/translations.ts | 4 + frontend/src/router/router.tsx | 12 ++ frontend/src/routes/frontend.ts | 1 + 10 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss create mode 100644 frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 7ea8f8ffd..987a0d849 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -149,6 +149,53 @@ def create(self, validated_data: dict) -> Image: image.save() return image + def update(self, instance: Image, validated_data: dict) -> Image: + """ + Updates an image with a new file. + Deletes the old image file from storage before saving the new one. + """ + # Delete old image file if a new file is being uploaded + if 'file' in validated_data: + file = validated_data.pop('file') + + # Validate the image file + try: + img = PilImage.open(file) + img.verify() + except (UnidentifiedImageError, Exception) as e: + raise ValidationError(f'Invalid image file: {str(e)}') + + # Delete the old image file from storage + if instance.image: + instance.image.delete(save=False) + # Preserve original filename if available; fallback to title + original_name = getattr(file, 'name', None) or validated_data.get('title', instance.title) + instance.image = ImageFile(file, original_name) + + # Handle tag updates if provided + raw_tag_string = validated_data.pop('tag_string', None) + if raw_tag_string is not None: + # Split on comma, strip whitespace and drop empties + tag_names = [name.strip() for name in raw_tag_string.split(',')] + tag_names = [name for name in tag_names if name] + + # De-duplicate while preserving order + seen: set[str] = set() + unique_tag_names = [] + for name in tag_names: + if name not in seen: + seen.add(name) + unique_tag_names.append(name) + tags = [Tag.objects.get_or_create(name=name)[0] for name in unique_tag_names] + instance.tags.set(tags) + + # Update other fields + for attr, value in validated_data.items(): + setattr(instance, attr, value) + + instance.save() + return instance + def get_url(self, image: Image) -> str: return image.image.url diff --git a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss new file mode 100644 index 000000000..93380858a --- /dev/null +++ b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss @@ -0,0 +1,75 @@ +@use 'src/constants' as *; + +@use 'src/mixins' as *; + +.editContainer { + display: flex; + flex-direction: column; + gap: 2rem; + padding: 2rem; +} + +.imageSection { + @include rounded; + @include shadow-light; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.image { + width: 100%; + max-height: 400px; + object-fit: cover; +} + +.imageInfo { + padding: 1rem; + background-color: $black; + color: $white; + + h3 { + margin: 0 0 0.5rem 0; + font-size: 1.2em; + } +} + +.uploadSection { + @include rounded; + @include shadow-light; + padding: 2rem; + + h4 { + margin: 0 0 1rem 0; + font-size: 1.1em; + } +} + +.uploadForm { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.fileInput { + padding: 0.5rem; + border: 1px solid gray; + border-radius: 4px; + cursor: pointer; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.fileName { + font-size: 0.9em; + color: gray; + margin: 0; +} + +.container { + padding: 2rem; + text-align: center; +} diff --git a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx new file mode 100644 index 000000000..01194f35c --- /dev/null +++ b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx @@ -0,0 +1,188 @@ +import { AxiosError } from 'axios'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { Button } from '~/Components/Button/Button'; +import { getImage, replaceImageFile } from '~/api'; +import { BACKEND_DOMAIN } from '~/constants'; +import type { ImageDto } from '~/dto'; +import { useCustomNavigate, useTitle } from '~/hooks'; +import { STATUS } from '~/http_status_codes'; +import { KEY } from '~/i18n/constants'; +import { ROUTES } from '~/routes'; +import { lowerCapitalize } from '~/utils'; +import { AdminPageLayout } from '../../../AdminPageLayout/AdminPageLayout'; +import styles from './AdminEditImage.module.scss'; + +type AdminEditImageProps = { + id?: number; +}; + +export function AdminEditImage({ id }: AdminEditImageProps) { + const { t } = useTranslation(); + const navigate = useCustomNavigate(); + const { id: paramsId } = useParams<{ id: string }>(); + const fileInputRef = useRef(null); + + // Use prop if provided, otherwise fall back to URL params + const imageID = id || paramsId; + + const [image, setImage] = useState(null); + const [loading, setLoading] = useState(true); + const [uploading, setUploading] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [notFound, setNotFound] = useState(false); + const [fetchError, setFetchError] = useState(false); + + const title = lowerCapitalize(`${t(KEY.common_edit)} ${t(KEY.common_image)}`); + useTitle(title); + + // Fetch the image data on mount + useEffect(() => { + if (!imageID) { + setLoading(false); + return; + } + + let isMounted = true; + + const fetchImage = async () => { + try { + setLoading(true); + const data = await getImage(imageID); + if (isMounted) { + setImage(data); + } + } catch (error: unknown) { + if (!isMounted) return; + + if (error instanceof AxiosError && error.response?.status === STATUS.HTTP_404_NOT_FOUND) { + if (isMounted) { + setNotFound(true); + } + } else { + if (isMounted) { + setFetchError(true); + } + console.error('Failed to fetch image:', error instanceof AxiosError ? error.message : error); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + fetchImage(); + + return () => { + isMounted = false; + }; + }, [imageID]); + + // Handle 404 redirect + useEffect(() => { + if (notFound) { + navigate({ url: ROUTES.frontend.admin_images, replace: true }); + } + }, [notFound, navigate]); + + // Handle fetch errors + useEffect(() => { + if (fetchError) { + toast.error(t(KEY.common_something_went_wrong)); + } + }, [fetchError, t]); + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + setSelectedFile(file); + } + }; + + const handleUpload = async () => { + if (!selectedFile || !image) { + toast.error(t(KEY.common_something_went_wrong)); + return; + } + + // Validate file size (5MB max) + const MAX_FILE_SIZE = 5 * 1024 * 1024; + if (selectedFile.size > MAX_FILE_SIZE) { + toast.error('File size must be less than 5MB'); + return; + } + + try { + setUploading(true); + await replaceImageFile(image.id, selectedFile, image.title); + + // Refetch the image to get the latest version from the server + const refreshedImage = await getImage(image.id); + setImage(refreshedImage); + + setSelectedFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + toast.success(t(KEY.common_creation_successful)); + } catch (error: unknown) { + toast.error(t(KEY.common_something_went_wrong)); + console.error('Failed to upload image:', error); + } finally { + setUploading(false); + } + }; + + if (loading) { + return ( + +

Loading...

+
+ ); + } + + if (notFound) { + return null; + } + + if (!image) { + return null; + } + + return ( + +
+ {/* Display current image */} +
+ {image.title} +
+

{image.title}

+

{image.tags.map((tag) => tag.name).join(', ')}

+
+
+ + {/* Upload form */} +
+

{lowerCapitalize(`${t(KEY.common_replace)} ${t(KEY.common_image)}`)}

+
+ + {selectedFile &&

{selectedFile.name}

} + +
+
+
+
+ ); +} diff --git a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminImage.tsx b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminImage.tsx index 4b33c6968..8c474ea60 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminImage.tsx +++ b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminImage.tsx @@ -1,7 +1,13 @@ import classNames from 'classnames'; +import { t } from 'i18next'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '~/Components/Button/Button'; import { BACKEND_DOMAIN } from '~/constants'; import type { ImageDto } from '~/dto'; -import { backgroundImageFromUrl } from '~/utils'; +import { KEY } from '~/i18n/constants'; +import { reverse } from '~/named-urls'; +import { ROUTES } from '~/routes'; +import { backgroundImageFromUrl, lowerCapitalize } from '~/utils'; import styles from './AdminImage.module.scss'; type AdminImageProps = { @@ -10,11 +16,27 @@ type AdminImageProps = { }; export function AdminImage({ image, className }: AdminImageProps) { + const navigate = useNavigate(); + + const editImageUrl = reverse({ + pattern: ROUTES.frontend.admin_images_edit, + urlParams: { id: image.id }, + }); + + const header = ( + <> + + + ); + const TAGS = image.tags .map((tag) => { return ` ${tag.name}`; }) .toString(); + return (

{image.title}

{TAGS}

+ {header}
); diff --git a/frontend/src/PagesAdmin/ImageAdminPage/index.ts b/frontend/src/PagesAdmin/ImageAdminPage/index.ts index 4c81d916d..855db3976 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/index.ts +++ b/frontend/src/PagesAdmin/ImageAdminPage/index.ts @@ -1 +1,2 @@ export { ImageAdminPage } from './ImageAdminPage'; +export { AdminEditImage } from './components/AdminImage/AdminEditImage'; diff --git a/frontend/src/api.ts b/frontend/src/api.ts index cf4793f97..0d712eb66 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -587,6 +587,17 @@ export async function putImage(id: string | number, data: Partial): Pr return response; } +export async function replaceImageFile(id: string | number, file: File, title?: string): Promise { + const url = BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__images_detail, urlParams: { pk: id } }); + const formData = new FormData(); + formData.append('file', file); + if (title) { + formData.append('title', title); + } + const response = await axios.putForm(url, formData, { withCredentials: true }); + return response.data; +} + /** Fetch all KeyValues from backend. */ export function getKeyValues(): Promise> { const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__key_value_list; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 0f55ba6e0..5fe11df75 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -202,6 +202,8 @@ export const KEY = { common_available: 'common_available', common_comment: 'common_comment', common_capacity: 'common_capacity', + common_replace: 'common_replace', + common_upload: 'common_upload', common_membership_number: 'common_membership_number', common_to_payment: 'common_to_payment', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index b74d9ae15..f72b3c086 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -186,6 +186,8 @@ export const nb = prepareTranslations({ [KEY.common_available]: 'Tilgjengelig', [KEY.common_comment]: 'Kommentar', [KEY.common_capacity]: 'Kapasitet', + [KEY.common_upload]: 'Last opp', + [KEY.common_replace]: 'Erstatt', [KEY.common_membership_number]: 'Medlemsnummer', [KEY.common_to_payment]: 'Til betaling', @@ -846,6 +848,8 @@ export const en = prepareTranslations({ [KEY.common_available]: 'Available', [KEY.common_comment]: 'Comment', [KEY.common_capacity]: 'Capacity', + [KEY.common_replace]: 'Replace', + [KEY.common_upload]: 'Upload', [KEY.common_membership_number]: 'Membership number', [KEY.common_to_payment]: 'To payment', diff --git a/frontend/src/router/router.tsx b/frontend/src/router/router.tsx index 3900e394a..fa5be24d2 100644 --- a/frontend/src/router/router.tsx +++ b/frontend/src/router/router.tsx @@ -87,6 +87,7 @@ import { ROUTES } from '~/routes'; import { t } from 'i18next'; import { App } from '~/App'; import { DynamicOrgOutlet } from '~/Components/DynamicOrgOutlet/DynamicOrgOutlet'; +import { AdminEditImage } from '~/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage'; import { RecruitmentRecruiterDashboardPage } from '~/PagesAdmin/RecruitmentRecruiterDashboardPage/RecruitmentRecruiterDashboardPage'; import { KEY } from '~/i18n/constants'; import { reverse } from '~/named-urls'; @@ -452,6 +453,17 @@ export const router = createBrowserRouter( /> } /> + {t(KEY.common_edit)} }} + element={ + } + resolveWithRolePermissions={true} + /> + } + /> {/* Saksdokumenter */} Date: Tue, 3 Mar 2026 19:56:24 +0100 Subject: [PATCH 02/79] added list of events that are tied to an image, not interactable yet --- backend/samfundet/view/general_views.py | 8 +++++ .../AdminImage/AdminEditImage.module.scss | 32 +++++++++++++++++++ .../components/AdminImage/AdminEditImage.tsx | 26 ++++++++++++++- frontend/src/api.ts | 16 ++++++++++ frontend/src/i18n/constants.ts | 2 ++ frontend/src/i18n/translations.ts | 4 +++ 6 files changed, 87 insertions(+), 1 deletion(-) diff --git a/backend/samfundet/view/general_views.py b/backend/samfundet/view/general_views.py index 5d82a972a..43dc7f89e 100644 --- a/backend/samfundet/view/general_views.py +++ b/backend/samfundet/view/general_views.py @@ -46,6 +46,7 @@ InformationPageSerializer, UserGangSectionRoleSerializer, ) +from samfundet.models.event import Event from samfundet.models.general import ( Tag, Gang, @@ -105,6 +106,13 @@ class ImageView(ModelViewSet): serializer_class = ImageSerializer queryset = Image.objects.all().order_by('-pk') + @action(detail=True, methods=['get']) + def linked_events(self, request: Request, pk: int | None = None) -> Response: + """Get all events that use this image.""" + image = self.get_object() + events = Event.objects.filter(image=image).values('id', 'title_en', 'title_nb', 'start_dt') + return Response(data=list(events)) + # Image tags class TagView(ModelViewSet): diff --git a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss index 93380858a..6104e8715 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss +++ b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss @@ -73,3 +73,35 @@ padding: 2rem; text-align: center; } + +.linkedEventsSection { + @include rounded; + @include shadow-light; + padding: 2rem; + background-color: #fff3cd; + border-left: 4px solid #ffc107; + + h4 { + margin: 0 0 1rem 0; + font-size: 1.1em; + color: #333; + } +} + +.eventsList { + margin: 0 0 1rem 0; + padding-left: 2rem; + list-style-type: disc; + + li { + margin-bottom: 0.5rem; + color: #555; + } +} + +.warningText { + margin: 1rem 0 0 0; + font-size: 0.95em; + color: #855a00; + font-style: italic; +} diff --git a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx index 01194f35c..d94b7973d 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx +++ b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx @@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import { Button } from '~/Components/Button/Button'; -import { getImage, replaceImageFile } from '~/api'; +import type { LinkedEventDto } from '~/api'; +import { getImage, getImageLinkedEvents, replaceImageFile } from '~/api'; import { BACKEND_DOMAIN } from '~/constants'; import type { ImageDto } from '~/dto'; import { useCustomNavigate, useTitle } from '~/hooks'; @@ -34,6 +35,7 @@ export function AdminEditImage({ id }: AdminEditImageProps) { const [selectedFile, setSelectedFile] = useState(null); const [notFound, setNotFound] = useState(false); const [fetchError, setFetchError] = useState(false); + const [linkedEvents, setLinkedEvents] = useState([]); const title = lowerCapitalize(`${t(KEY.common_edit)} ${t(KEY.common_image)}`); useTitle(title); @@ -53,6 +55,11 @@ export function AdminEditImage({ id }: AdminEditImageProps) { const data = await getImage(imageID); if (isMounted) { setImage(data); + // Fetch linked events + const events = await getImageLinkedEvents(imageID); + if (isMounted) { + setLinkedEvents(events); + } } } catch (error: unknown) { if (!isMounted) return; @@ -164,6 +171,23 @@ export function AdminEditImage({ id }: AdminEditImageProps) { + {/* Linked events warning */} + {linkedEvents.length > 0 && ( +
+

{t(KEY.common_image)} {t(KEY.common_linked_to_events)}:

+
    + {linkedEvents.map((event) => ( +
  • + {event.title_nb || event.title_en} ({new Date(event.start_dt).toLocaleDateString()}) +
  • + ))} +
+

+ {t(KEY.common_cannot_delete_image)} +

+
+ )} + {/* Upload form */}

{lowerCapitalize(`${t(KEY.common_replace)} ${t(KEY.common_image)}`)}

diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 0d712eb66..f644fd873 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -598,6 +598,22 @@ export async function replaceImageFile(id: string | number, file: File, title?: return response.data; } +export type LinkedEventDto = { + id: number; + title_en: string; + title_nb: string; + start_dt: string; +}; + +export async function getImageLinkedEvents(id: string | number): Promise { + const url = + BACKEND_DOMAIN + + reverse({ pattern: ROUTES.backend.samfundet__images_detail, urlParams: { pk: id } }) + + 'linked_events/'; + const response = await axios.get(url, { withCredentials: true }); + return response.data; +} + /** Fetch all KeyValues from backend. */ export function getKeyValues(): Promise> { const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__key_value_list; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 5fe11df75..f2982c2a0 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -204,6 +204,8 @@ export const KEY = { common_capacity: 'common_capacity', common_replace: 'common_replace', common_upload: 'common_upload', + common_linked_to_events: 'common_linked_to_events', + common_cannot_delete_image: 'common_cannot_delete_image', common_membership_number: 'common_membership_number', common_to_payment: 'common_to_payment', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index f72b3c086..05a98dafb 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -188,6 +188,8 @@ export const nb = prepareTranslations({ [KEY.common_capacity]: 'Kapasitet', [KEY.common_upload]: 'Last opp', [KEY.common_replace]: 'Erstatt', + [KEY.common_linked_to_events]: 'brukt i disse arrangementene', + [KEY.common_cannot_delete_image]: 'Bildet kan ikke slettes før alle disse arrangementene er slettet eller bildet erstattes.', [KEY.common_membership_number]: 'Medlemsnummer', [KEY.common_to_payment]: 'Til betaling', @@ -850,6 +852,8 @@ export const en = prepareTranslations({ [KEY.common_capacity]: 'Capacity', [KEY.common_replace]: 'Replace', [KEY.common_upload]: 'Upload', + [KEY.common_linked_to_events]: 'used in these events', + [KEY.common_cannot_delete_image]: 'The image cannot be deleted until all these events are deleted or the image is replaced.', [KEY.common_membership_number]: 'Membership number', [KEY.common_to_payment]: 'To payment', From 3cd059c82e52ee704ff0541c47af02990338ecd7 Mon Sep 17 00:00:00 2001 From: Tor Madsen Date: Tue, 3 Mar 2026 20:59:48 +0100 Subject: [PATCH 03/79] added link to events that is listed under image. Waiting for Edit Image PR to be merged into main before continuing --- .../AdminImage/AdminEditImage.module.scss | 14 ++++++++++++++ .../components/AdminImage/AdminEditImage.tsx | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss index 6104e8715..568ac1f2d 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss +++ b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.module.scss @@ -99,6 +99,20 @@ } } +.eventItem { + cursor: pointer; + transition: all 0.2s ease; + padding: 0.25rem 0.5rem; + margin-left: -0.5rem; + border-radius: 3px; + + &:hover { + background-color: #e8f4f8; + color: #003d5c; + font-weight: 500; + } +} + .warningText { margin: 1rem 0 0 0; font-size: 0.95em; diff --git a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx index d94b7973d..47bf5b546 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx +++ b/frontend/src/PagesAdmin/ImageAdminPage/components/AdminImage/AdminEditImage.tsx @@ -11,6 +11,7 @@ import type { ImageDto } from '~/dto'; 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 { lowerCapitalize } from '~/utils'; import { AdminPageLayout } from '../../../AdminPageLayout/AdminPageLayout'; @@ -177,7 +178,11 @@ export function AdminEditImage({ id }: AdminEditImageProps) {

{t(KEY.common_image)} {t(KEY.common_linked_to_events)}:

    {linkedEvents.map((event) => ( -
  • +
  • navigate({ url: reverse({ pattern: ROUTES.frontend.admin_events_edit, urlParams: { id: event.id } }) })} + > {event.title_nb || event.title_en} ({new Date(event.start_dt).toLocaleDateString()})
  • ))} From fe2f876f2e834a719c50ee0ca50d6622536e076a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:12:46 +0100 Subject: [PATCH 04/79] Bump debugpy from 1.8.17 to 1.8.20 in /backend (#2053) Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.17 to 1.8.20. - [Release notes](https://github.com/microsoft/debugpy/releases) - [Commits](https://github.com/microsoft/debugpy/compare/v1.8.17...v1.8.20) --- updated-dependencies: - dependency-name: debugpy dependency-version: 1.8.20 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 62 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 616b43165..cb0c2ce12 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -205,42 +205,42 @@ typing-inspect = ">=0.4.0,<1" [[package]] name = "debugpy" -version = "1.8.17" +version = "1.8.20" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542"}, - {file = "debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3"}, - {file = "debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4"}, - {file = "debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a"}, - {file = "debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840"}, - {file = "debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f"}, - {file = "debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da"}, - {file = "debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4"}, - {file = "debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d"}, - {file = "debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc"}, - {file = "debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf"}, - {file = "debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464"}, - {file = "debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464"}, - {file = "debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088"}, - {file = "debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83"}, - {file = "debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420"}, - {file = "debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1"}, - {file = "debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f"}, - {file = "debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670"}, - {file = "debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c"}, - {file = "debugpy-1.8.17-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:8deb4e31cd575c9f9370042876e078ca118117c1b5e1f22c32befcfbb6955f0c"}, - {file = "debugpy-1.8.17-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:b75868b675949a96ab51abc114c7163f40ff0d8f7d6d5fd63f8932fd38e9c6d7"}, - {file = "debugpy-1.8.17-cp38-cp38-win32.whl", hash = "sha256:17e456da14848d618662354e1dccfd5e5fb75deec3d1d48dc0aa0baacda55860"}, - {file = "debugpy-1.8.17-cp38-cp38-win_amd64.whl", hash = "sha256:e851beb536a427b5df8aa7d0c7835b29a13812f41e46292ff80b2ef77327355a"}, - {file = "debugpy-1.8.17-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e"}, - {file = "debugpy-1.8.17-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7"}, - {file = "debugpy-1.8.17-cp39-cp39-win32.whl", hash = "sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1"}, - {file = "debugpy-1.8.17-cp39-cp39-win_amd64.whl", hash = "sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f"}, - {file = "debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef"}, - {file = "debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e"}, + {file = "debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64"}, + {file = "debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642"}, + {file = "debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2"}, + {file = "debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893"}, + {file = "debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b"}, + {file = "debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344"}, + {file = "debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec"}, + {file = "debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb"}, + {file = "debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d"}, + {file = "debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b"}, + {file = "debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390"}, + {file = "debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3"}, + {file = "debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a"}, + {file = "debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf"}, + {file = "debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393"}, + {file = "debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7"}, + {file = "debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173"}, + {file = "debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad"}, + {file = "debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f"}, + {file = "debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be"}, + {file = "debugpy-1.8.20-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:b773eb026a043e4d9c76265742bc846f2f347da7e27edf7fe97716ea19d6bfc5"}, + {file = "debugpy-1.8.20-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:20d6e64ea177ab6732bffd3ce8fc6fb8879c60484ce14c3b3fe183b1761459ca"}, + {file = "debugpy-1.8.20-cp38-cp38-win32.whl", hash = "sha256:0dfd9adb4b3c7005e9c33df430bcdd4e4ebba70be533e0066e3a34d210041b66"}, + {file = "debugpy-1.8.20-cp38-cp38-win_amd64.whl", hash = "sha256:60f89411a6c6afb89f18e72e9091c3dfbcfe3edc1066b2043a1f80a3bbb3e11f"}, + {file = "debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6"}, + {file = "debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5"}, + {file = "debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee"}, + {file = "debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8"}, + {file = "debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7"}, + {file = "debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33"}, ] [[package]] From a8061c624df666ccdaf1719c31970f5db729a1c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:13:16 +0100 Subject: [PATCH 05/79] Bump django from 5.2.10 to 5.2.11 in /backend (#2055) Bumps [django](https://github.com/django/django) from 5.2.10 to 5.2.11. - [Commits](https://github.com/django/django/compare/5.2.10...5.2.11) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index cb0c2ce12..8388b5aaa 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -245,14 +245,14 @@ files = [ [[package]] name = "django" -version = "5.2.10" +version = "5.2.11" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "django-5.2.10-py3-none-any.whl", hash = "sha256:cf85067a64250c95d5f9067b056c5eaa80591929f7e16fbcd997746e40d6c45c"}, - {file = "django-5.2.10.tar.gz", hash = "sha256:74df100784c288c50a2b5cad59631d71214f40f72051d5af3fdf220c20bdbbbe"}, + {file = "django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0"}, + {file = "django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3"}, ] [package.dependencies] From 1b6308e98719fc214d513d2d13546b49dbd168f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:13:52 +0100 Subject: [PATCH 06/79] Bump bandit from 1.8.6 to 1.9.3 in /backend (#2047) Bumps [bandit](https://github.com/PyCQA/bandit) from 1.8.6 to 1.9.3. - [Release notes](https://github.com/PyCQA/bandit/releases) - [Commits](https://github.com/PyCQA/bandit/compare/1.8.6...1.9.3) --- updated-dependencies: - dependency-name: bandit dependency-version: 1.9.3 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 8388b5aaa..75be2ca4b 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -37,14 +37,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a [[package]] name = "bandit" -version = "1.8.6" +version = "1.9.3" description = "Security oriented static analyser for python code." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"}, - {file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"}, + {file = "bandit-1.9.3-py3-none-any.whl", hash = "sha256:4745917c88d2246def79748bde5e08b9d5e9b92f877863d43fab70cd8814ce6a"}, + {file = "bandit-1.9.3.tar.gz", hash = "sha256:ade4b9b7786f89ef6fc7344a52b34558caec5da74cb90373aed01de88472f774"}, ] [package.dependencies] From c3824f7337454964f4126ea55201b056227c6893 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:17:00 +0100 Subject: [PATCH 07/79] Bump tar from 7.5.6 to 7.5.7 in /frontend (#2043) Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.6 to 7.5.7. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.6...v7.5.7) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 5b3c8b485..d39fd4489 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -8303,15 +8303,15 @@ __metadata: linkType: hard "tar@npm:^7.4.3": - version: 7.5.6 - resolution: "tar@npm:7.5.6" + version: 7.5.7 + resolution: "tar@npm:7.5.7" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/08af3807035957650ad5f2a300c49ca4fe0566ac0ea5a23741a5b5103c6da42891a9eeaed39bc1fbcf21c5cac4dc846828a004727fb08b9d946322d3144d1fd2 + checksum: 10c0/51f261afc437e1112c3e7919478d6176ea83f7f7727864d8c2cce10f0b03a631d1911644a567348c3063c45abdae39718ba97abb073d22aa3538b9a53ae1e31c languageName: node linkType: hard From c646a7d4f918177b7f73b6d7b2a0879dbd77fff2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:19:21 +0100 Subject: [PATCH 08/79] Bump mypy from 1.19.0 to 1.19.1 in /backend (#2050) Bumps [mypy](https://github.com/python/mypy) from 1.19.0 to 1.19.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.19.0...v1.19.1) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.19.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 81 +++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 75be2ca4b..b650984e2 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -501,6 +501,7 @@ description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1"}, {file = "librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf"}, @@ -639,54 +640,54 @@ files = [ [[package]] name = "mypy" -version = "1.19.0" +version = "1.19.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8"}, - {file = "mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39"}, - {file = "mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab"}, - {file = "mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e"}, - {file = "mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3"}, - {file = "mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134"}, - {file = "mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106"}, - {file = "mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7"}, - {file = "mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7"}, - {file = "mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b"}, - {file = "mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7"}, - {file = "mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e"}, - {file = "mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d"}, - {file = "mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760"}, - {file = "mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6"}, - {file = "mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2"}, - {file = "mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431"}, - {file = "mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018"}, - {file = "mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e"}, - {file = "mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d"}, - {file = "mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba"}, - {file = "mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364"}, - {file = "mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee"}, - {file = "mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53"}, - {file = "mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d"}, - {file = "mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18"}, - {file = "mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7"}, - {file = "mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f"}, - {file = "mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835"}, - {file = "mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1"}, - {file = "mypy-1.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0dde5cb375cb94deff0d4b548b993bec52859d1651e073d63a1386d392a95495"}, - {file = "mypy-1.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1cf9c59398db1c68a134b0b5354a09a1e124523f00bacd68e553b8bd16ff3299"}, - {file = "mypy-1.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3210d87b30e6af9c8faed61be2642fcbe60ef77cec64fa1ef810a630a4cf671c"}, - {file = "mypy-1.19.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2c1101ab41d01303103ab6ef82cbbfedb81c1a060c868fa7cc013d573d37ab5"}, - {file = "mypy-1.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ea4fd21bb48f0da49e6d3b37ef6bd7e8228b9fe41bbf4d80d9364d11adbd43c"}, - {file = "mypy-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:16f76ff3f3fd8137aadf593cb4607d82634fca675e8211ad75c43d86033ee6c6"}, - {file = "mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9"}, - {file = "mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, ] [package.dependencies] -librt = ">=0.6.2" +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=0.9.0" typing_extensions = ">=4.6.0" From 4ba24077c445d7cbc2f7cc3be97f51712fdf7397 Mon Sep 17 00:00:00 2001 From: Eilif Hjermann Lindblad Date: Tue, 3 Feb 2026 22:52:32 +0100 Subject: [PATCH 09/79] Fix positon of Events Header (#2045) * Fix positon of events header on HiDpi devices * lint --- .../EventsPage/components/EventsList/EventsList.module.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.module.scss b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.module.scss index 607e883c5..167ed6bd7 100644 --- a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.module.scss +++ b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.module.scss @@ -42,6 +42,8 @@ $event-header-bg-dark: rgba(31, 31, 31, 0.9); // Position top: $navbar-height; z-index: 1; + left: 0; + right: 0; @include for-mobile-only { position: absolute; From 052fd8914266f1ddedcb3afa0a1f22fc6a604f23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:59:34 +0100 Subject: [PATCH 10/79] Bump axios from 1.13.2 to 1.13.5 in /frontend (#2062) Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.13.5. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.13.5) --- updated-dependencies: - dependency-name: axios dependency-version: 1.13.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 37 +++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index bc15163b0..c0e8d4671 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,7 @@ "@radix-ui/react-slot": "^1.2.2", "@tanstack/react-query": "^5.84.0", "@tanstack/react-query-devtools": "^5.59.0", - "axios": "^1.13.2", + "axios": "^1.13.5", "classnames": "^2.3.2", "cmdk": "^1.1.1", "cypress-fail-on-console-error": "^5.0.1", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d39fd4489..6ca5a3a72 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2981,14 +2981,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.13.2": - version: 1.13.2 - resolution: "axios@npm:1.13.2" +"axios@npm:^1.13.5": + version: 1.13.5 + resolution: "axios@npm:1.13.5" dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.4" + follow-redirects: "npm:^1.15.11" + form-data: "npm:^4.0.5" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b + checksum: 10c0/abf468c34f2d145f3dc7dbc0f1be67e520630624307bda69a41bbe8d386bd672d87b4405c4ee77f9ff54b235ab02f96a9968fb00e75b13ce64706e352a3068fd languageName: node linkType: hard @@ -4511,13 +4511,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.6": - version: 1.15.9 - resolution: "follow-redirects@npm:1.15.9" +"follow-redirects@npm:^1.15.11": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" peerDependenciesMeta: debug: optional: true - checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f + checksum: 10c0/d301f430542520a54058d4aeeb453233c564aaccac835d29d15e050beb33f339ad67d9bddbce01739c5dc46a6716dbe3d9d0d5134b1ca203effa11a7ef092343 languageName: node linkType: hard @@ -4547,7 +4547,20 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.4, form-data@npm:~4.0.0": +"form-data@npm:^4.0.5": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10c0/dd6b767ee0bbd6d84039db12a0fa5a2028160ffbfaba1800695713b46ae974a5f6e08b3356c3195137f8530dcd9dfcb5d5ae1eeff53d0db1e5aad863b619ce3b + languageName: node + linkType: hard + +"form-data@npm:~4.0.0": version: 4.0.4 resolution: "form-data@npm:4.0.4" dependencies: @@ -4595,7 +4608,7 @@ __metadata: "@types/react-modal": "npm:^3.16.3" "@vitejs/plugin-react-swc": "npm:^4.2.2" autoprefixer: "npm:^10.4.21" - axios: "npm:^1.13.2" + axios: "npm:^1.13.5" babel-plugin-named-exports-order: "npm:^0.0.2" chromatic: "npm:^7.4.0" classnames: "npm:^2.3.2" From 127b1ce3fc6447fe7433bd9fc56dab35aa8614c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:02:08 +0100 Subject: [PATCH 11/79] Bump pillow from 11.3.0 to 12.1.1 in /backend (#2059) Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.3.0 to 12.1.1. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.3.0...12.1.1) --- updated-dependencies: - dependency-name: pillow dependency-version: 12.1.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 210 +++++++++++++++++++---------------------- backend/pyproject.toml | 4 +- 2 files changed, 99 insertions(+), 115 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index b650984e2..c25d95d25 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -752,127 +752,111 @@ setuptools = "*" [[package]] name = "pillow" -version = "11.3.0" -description = "Python Imaging Library (Fork)" +version = "12.1.1" +description = "Python Imaging Library (fork)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, - {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}, - {file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}, - {file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}, - {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}, - {file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}, - {file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}, - {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}, - {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}, - {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}, - {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}, - {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}, - {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}, - {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}, - {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}, - {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}, - {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}, - {file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}, - {file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}, - {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}, - {file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}, - {file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}, - {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"}, - {file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"}, - {file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"}, - {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}, - {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, + {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, + {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, + {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, + {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, + {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, + {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, + {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, + {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, + {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, + {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, + {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, + {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, + {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, + {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, + {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, + {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, + {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, + {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, + {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, + {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, + {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, + {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -test-arrow = ["pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] [[package]] @@ -1409,4 +1393,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" python-versions = "3.11.2" -content-hash = "70cd0cb35d37937fe985f6ae774d255b0feedd8a37aef20d35e5f2b5a3e2a847" +content-hash = "e4f3f45d0679a22c5fd8a9bbcebb6d2f889cafd2f604f7171a13112566d7aa55" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index eb569e73d..299c874ea 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -150,7 +150,7 @@ djangorestframework = "3.*" django-cors-headers = "4.*" dataclasses-json = "0.*" django-guardian = "2.*" -pillow = "11.*" +pillow = "12.*" gunicorn = "23.*" django-admin-autocomplete-filter = "0.*" psycopg = { extras = ["c"], version = "*" } @@ -160,7 +160,7 @@ psycopg = { extras = ["c"], version = "*" } mypy = "1.*" ruff = "0.*" bandit = "1.*" -pillow = "11.*" +pillow = "12.*" debugpy = "1.*" requests = "2.*" pytest = "8.*" From b1248b9eeef32ce21f8142d5acda3165adb6348a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:08:27 +0100 Subject: [PATCH 12/79] Bump i18next from 24.2.3 to 25.8.0 in /frontend (#2048) Bumps [i18next](https://github.com/i18next/i18next) from 24.2.3 to 25.8.0. - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v24.2.3...v25.8.0) --- updated-dependencies: - dependency-name: i18next dependency-version: 25.8.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index c0e8d4671..326827e1e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,7 +52,7 @@ "cmdk": "^1.1.1", "cypress-fail-on-console-error": "^5.0.1", "date-fns": "^2.30.0", - "i18next": "^24.2.3", + "i18next": "^25.8.5", "i18next-browser-languagedetector": "^8.0.4", "path": "^0.12.7", "path-to-regexp": "^8.0.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6ca5a3a72..6ae297a80 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -207,7 +207,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.26.10": +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2": version: 7.26.10 resolution: "@babel/runtime@npm:7.26.10" dependencies: @@ -216,6 +216,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.28.4": + version: 7.28.6 + resolution: "@babel/runtime@npm:7.28.6" + checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d + languageName: node + linkType: hard + "@babel/template@npm:^7.26.9": version: 7.26.9 resolution: "@babel/template@npm:7.26.9" @@ -4617,7 +4624,7 @@ __metadata: cypress-fail-on-console-error: "npm:^5.0.1" cypress-vite: "npm:^1.4.2" date-fns: "npm:^2.30.0" - i18next: "npm:^24.2.3" + i18next: "npm:^25.8.5" i18next-browser-languagedetector: "npm:^8.0.4" path: "npm:^0.12.7" path-to-regexp: "npm:^8.0.0" @@ -5066,17 +5073,17 @@ __metadata: languageName: node linkType: hard -"i18next@npm:^24.2.3": - version: 24.2.3 - resolution: "i18next@npm:24.2.3" +"i18next@npm:^25.8.5": + version: 25.8.5 + resolution: "i18next@npm:25.8.5" dependencies: - "@babel/runtime": "npm:^7.26.10" + "@babel/runtime": "npm:^7.28.4" peerDependencies: typescript: ^5 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/7ac11a67d618ec714beef303aa497c1249bf5f1977dd3ebe9ca2673dfa6cadbba9e2d39ec1337688903ae3866ce9c1bc22cd6b265e66cce54c5db3a9bbedd390 + checksum: 10c0/6f4e52bcfd667715be60bdf93f5e7ae5ef37c3be328b26680ac8edb5477a8ef443dc4fb349d11556e607a47674de049ea72c50fbddd870c7ad2c7c6fa0172728 languageName: node linkType: hard From 8e0809e3ae90f2eab4e6b74cd119ad071b8d142e Mon Sep 17 00:00:00 2001 From: BragonSB Date: Thu, 12 Feb 2026 19:15:16 +0100 Subject: [PATCH 13/79] docs: add contribution (brage) --- frontend/src/Pages/ContributorsPage/ContributorsPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx b/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx index 3dbaeb49a..666b3ce1b 100644 --- a/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx +++ b/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx @@ -54,6 +54,8 @@ const CONTRIBUTORS: Contributor[] = [ // H25 { name: 'Sten Oskar Halse', github: 'StenOskar', from: 'H25' }, { name: 'Tor Madsen', github: 'madt2', from: 'H25' }, + // V26 + { name: 'Brage Sebastian Brevik', github: 'BragonSB', from: 'V26'}, ]; // @formatter:on From d48213288b9d573cfdb2db12ecadc987e81ff3cd Mon Sep 17 00:00:00 2001 From: Eilif Hjermann Lindblad Date: Thu, 12 Feb 2026 23:04:25 +0100 Subject: [PATCH 14/79] Alternative Login Picker (#2058) * Implement alternative login picker * Linter * Add backlink * Use arrow icon instead of plain css (for readability) * Use color constants * Center select dot * Use button to pass biome * Add caption "innlogging for interne" * Change hint text * Rephrase samf3 hint * STYLEEEELIIIINT!!!!!!!! GRRRRRR * Add boxes for easier mobile use * biome * Add samf colors to boxes * Add darkmode support * change heading color * Increase circle size, justify fortsett right * Change bg color of selected * Remove transition * Create breadcrumbs for new-login * Remove form, add buttons, add arrows * Biome * Adjust font weights * Use anchor tag * Add nav * Change underline of visited link, color of hover * Shorten titles (remove "logg inn") --- frontend/src/Pages/LoginPage/LoginPage.tsx | 5 +- .../LoginPickerPage.module.scss | 181 +++++++++++++----- .../Pages/LoginPickerPage/LoginPickerPage.tsx | 50 +++-- frontend/src/router/router.tsx | 10 +- 4 files changed, 176 insertions(+), 70 deletions(-) diff --git a/frontend/src/Pages/LoginPage/LoginPage.tsx b/frontend/src/Pages/LoginPage/LoginPage.tsx index 68bf8f10f..23239a969 100644 --- a/frontend/src/Pages/LoginPage/LoginPage.tsx +++ b/frontend/src/Pages/LoginPage/LoginPage.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router'; -import { Page } from '~/Components'; +import { Breadcrumb, Page } from '~/Components'; import { SamfForm } from '~/Forms/SamfForm'; import { SamfFormField } from '~/Forms/SamfFormField'; import { getUser, login } from '~/api'; @@ -57,6 +57,9 @@ export function LoginPage() {
    +
    + +

    {t(KEY.loginpage_internal_login)}

    diff --git a/frontend/src/Pages/LoginPickerPage/LoginPickerPage.module.scss b/frontend/src/Pages/LoginPickerPage/LoginPickerPage.module.scss index a852bd119..e9da59589 100644 --- a/frontend/src/Pages/LoginPickerPage/LoginPickerPage.module.scss +++ b/frontend/src/Pages/LoginPickerPage/LoginPickerPage.module.scss @@ -1,81 +1,168 @@ +@use 'sass:color'; + @use 'src/constants' as *; @use 'src/mixins' as *; .container { - min-height: 100%; + max-width: 960px; + margin: 0 auto; + padding: 2rem 1rem; +} + +.formWrapper { + max-width: 640px; display: flex; flex-direction: column; - align-items: center; - padding: 2rem 1rem; } -.header { - text-align: center; - margin-bottom: 2rem; +.caption { + display: block; + font-size: 1.25rem; + color: $grey-1; + margin-bottom: 5px; + font-weight: 400; + + @include theme-dark { + color: $grey-3; + } } .headerTitle { - font-size: 2rem; - margin: 0 0 .25rem 0; -} + color: $black-1; + font-size: 3rem; + font-weight: 700; + margin-top: 0; + margin-bottom: 30px; + line-height: 1.1; -.headerSubtitle { - font-size: 1.25rem; - opacity: 0.9; - margin: 0; + @include theme-dark { + color: $white; + } } .picker { display: flex; - justify-content: center; - align-items: flex-start; - gap: 3rem; - flex-wrap: wrap; + flex-direction: column; + margin-bottom: 30px; } .choiceWrapper { + position: relative; display: flex; - flex-direction: column; align-items: center; - width: 280px; - text-align: center; - min-height: 200px; justify-content: space-between; + width: 100%; + padding: 22px 30px; + border: 2px solid $grey-35; + background-color: $white; + cursor: pointer; + margin-bottom: 15px; + text-align: left; + font-family: inherit; + text-decoration: none; + + @include theme-dark { + background-color: $theme_dark_input_bg; + border-color: $grey-1; + } + + &:link, + &:visited { + color: inherit; + } + + @include theme-dark { + color: $white; + } + + &:hover { + border-color: $red_samf_hover; + background-color: $grey-5; + + @include theme-dark { + background-color: $black-2; + border-color: $red_samf; + } + } } -.description { - margin-bottom: 1rem; - line-height: 1.4; - min-height: 3.5rem; +.textWrapper { display: flex; - align-items: center; - justify-content: center; + flex-direction: column; } -.choice { - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - font-weight: 700; +.radioLabel { + display: block; + color: $black-1; font-size: 1.5rem; - padding: 1rem; - border-radius: 6px; - background: $red-samf; - color: $white; - box-shadow: 0 1px 2px rgba(0,0,0,.08); - min-height: 100px; - transition: transform .05s ease, filter .15s ease, box-shadow .15s ease; - - &:hover { filter: brightness(0.96); } - &:active { transform: translateY(1px); } - &:focus-visible { - outline: 3px solid rgba(0,0,0,.4); - outline-offset: 3px; + font-weight: 600; + cursor: pointer; + margin-bottom: 4px; + + @include theme-dark { + color: $white; } } -.choiceTitle { +.description { + color: $grey-1; + font-size: 1.1rem; + font-weight: 300; margin: 0; + line-height: 1.5; + + @include theme-dark { + color: $grey-3; + } +} + +.arrowIcon { + font-size: 2.5rem; + color: $black-1; + margin-left: 20px; + flex-shrink: 0; + + @include theme-dark { + color: $white; + } +} + +.backLink { + background: none; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + display: inline-flex; + align-items: center; + margin-top: 15px; + margin-bottom: 25px; + color: $black-1; + text-decoration: underline; + text-underline-offset: 3px; + + @include theme-dark { + color: $theme_dark_color; + } + + &:hover { + color: $grey-1; + text-decoration-thickness: 2px; + + @include theme-dark { + color: $white; + } + } + + &:focus-visible { + outline: 3px solid $orange-light; + outline-offset: 2px; + } +} + +.backIcon { + margin-right: 4px; + font-size: 1.2rem; + text-decoration: none; } diff --git a/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx b/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx index 82d2557b2..3fbb1ff10 100644 --- a/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx +++ b/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx @@ -1,12 +1,11 @@ +import { Icon } from '@iconify/react'; import type { FC } from 'react'; -import { Link } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { Page } from '~/Components'; import { SAMF3_LOGIN_URL } from '~/routes/samf-three'; import styles from './LoginPickerPage.module.scss'; -type Props = { - newRoute: string; -}; +type Props = { newRoute: string }; /** * A page that allows users to choose between the old and new samf login. @@ -15,29 +14,38 @@ type Props = { * @constructor */ export const LoginPickerPage: FC = ({ newRoute }) => { + const navigate = useNavigate(); + return (
    -
    -

    Logg inn som intern

    -

    Velg hvilket system du vil logge inn på

    -
    + - + +
    + Eldre plattform (samf3) +

    Gruppeadministrasjon og andre administrative oppgaver

    +
    + +
    + +
    ); diff --git a/frontend/src/router/router.tsx b/frontend/src/router/router.tsx index fa5be24d2..a0b03b4b0 100644 --- a/frontend/src/router/router.tsx +++ b/frontend/src/router/router.tsx @@ -124,7 +124,15 @@ export const router = createBrowserRouter( } />}> } /> } /> - } /> + {t(KEY.common_login)} }}> + } + handle={{ + crumb: ({ pathname }: UIMatch) => {t(KEY.loginpage_internal_login)}, + }} + /> + } /> } /> From e7c2496c92be23eb7b2c1da6df5e8d7ecef6148f Mon Sep 17 00:00:00 2001 From: BragonSB Date: Fri, 13 Feb 2026 12:18:12 +0100 Subject: [PATCH 15/79] brage-feat: add ability to sort by buy in /events * feat: add ability to sort by buy in /events * fix: Biome really wants some space --- .../src/Pages/EventsPage/components/EventsList/EventsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx index 0e9d70564..aae3c01ea 100644 --- a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx +++ b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx @@ -33,7 +33,7 @@ export function EventsList({ events }: EventsListProps) { { content: t(KEY.common_venue), sortable: true }, { content: t(KEY.category), sortable: true }, { content: t(KEY.admin_organizer), sortable: true }, - t(KEY.common_buy), + { content: t(KEY.common_buy), sortable: true }, ]; // TODO debounce and move header/filtering stuff to a separate component From 5955acf1d9aaa1823ca581eb0f9231510fa745d7 Mon Sep 17 00:00:00 2001 From: Robin <16273164+robines@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:27:03 +0100 Subject: [PATCH 16/79] Add mdb_medlem_id field to User (#2060) --- .../migrations/0004_user_mdb_medlem_id.py | 18 ++++++++++++++++++ backend/samfundet/models/general.py | 2 ++ 2 files changed, 20 insertions(+) create mode 100644 backend/samfundet/migrations/0004_user_mdb_medlem_id.py diff --git a/backend/samfundet/migrations/0004_user_mdb_medlem_id.py b/backend/samfundet/migrations/0004_user_mdb_medlem_id.py new file mode 100644 index 000000000..6ddb665f6 --- /dev/null +++ b/backend/samfundet/migrations/0004_user_mdb_medlem_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.9 on 2026-02-11 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('samfundet', '0003_alter_event_category'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='mdb_medlem_id', + field=models.PositiveIntegerField(null=True, unique=True, verbose_name='medlem_id in mdb2'), + ), + ] diff --git a/backend/samfundet/models/general.py b/backend/samfundet/models/general.py index 84f9461ca..073b212da 100644 --- a/backend/samfundet/models/general.py +++ b/backend/samfundet/models/general.py @@ -128,6 +128,8 @@ class User(AbstractUser): on_delete=models.PROTECT, ) + mdb_medlem_id = models.PositiveIntegerField(null=True, blank=False, unique=True, verbose_name='medlem_id in mdb2') + class Meta: permissions = [ ('debug', 'Can view debug mode'), From 737fe4e312be2e08dff44be0a21e0806d178837f Mon Sep 17 00:00:00 2001 From: Robin <16273164+robines@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:34:13 +0100 Subject: [PATCH 17/79] Create mdb database (#2061) * Set up fake mdb2 service. Create seed. Create unmanaged MedlemsInfo model * Rename to mdb_dev * ruff * Simplify disallowed models check * Fix port in github workflow * ruff * Update migration --- .env.example | 12 ++- .github/workflows/verify.yml | 16 +++ backend/root/db_router.py | 16 ++- .../commands/seed_scripts/__init__.py | 2 + .../management/commands/seed_scripts/mdb.py | 100 ++++++++++++++++++ .../commands/seed_scripts/seed_mdb/schema.sql | 21 ++++ backend/root/settings/dev.py | 8 ++ backend/root/settings/prod.py | 9 ++ .../samfundet/migrations/0005_medlemsinfo.py | 32 ++++++ backend/samfundet/models/mdb.py | 23 ++++ docker-compose.yml | 25 ++++- 11 files changed, 257 insertions(+), 7 deletions(-) create mode 100755 backend/root/management/commands/seed_scripts/mdb.py create mode 100644 backend/root/management/commands/seed_scripts/seed_mdb/schema.sql create mode 100644 backend/samfundet/migrations/0005_medlemsinfo.py create mode 100644 backend/samfundet/models/mdb.py diff --git a/.env.example b/.env.example index f0c714e45..e8e6610e5 100644 --- a/.env.example +++ b/.env.example @@ -7,14 +7,20 @@ COMMON_HEALTHCHECK_INTERVAL=10s COMMON_HEALTHCHECK_TIMEOUT=5s COMMON_HEALTHCHECK_RETRIES=5 -# must be the same as the servicename in docker-compose.yml +# must be the same as the service name in docker-compose.yml SM4_DEV_SERVICE_NAME=sm4_dev_database SM4_DEV_DB_IMAGE=postgres:15.13 SM4_DEV_CREDENTIAL=sm4DB -# must be the same as the servicename in docker-compose.yml +# must be the same as the service name in docker-compose.yml BILLIG_DEV_SERVICE_NAME=billig_dev_database BILLIG_DEV_DB_IMAGE=postgres:15.13 -BILLIG_DEV_CREDENTIAL=billigDB \ No newline at end of file +BILLIG_DEV_CREDENTIAL=billigDB + +# must be the same as the service name in docker-compose.yml +MDB_DEV_SERVICE_NAME=mdb_dev + +MDB_DEV_DB_IMAGE=postgres:15.13 +MDB_DEV_CREDENTIAL=mdb2 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5ef27f467..cd0206649 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -46,6 +46,19 @@ jobs: --health-retries 5 ports: - 5433:5432 + mdb_dev: + image: postgres:15 + env: + POSTGRES_DB: mdb2 + POSTGRES_USER: mdb2 + POSTGRES_PASSWORD: mdb2 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5434:5432 defaults: run: working-directory: ./backend @@ -59,10 +72,13 @@ jobs: DJANGO_SUPERUSER_EMAIL: admin@example.com SM4_DEV_CREDENTIAL: sm4DB BILLIG_DEV_CREDENTIAL: billigDB + MDB_DEV_CREDENTIAL: mdb2 SM4_DEV_HOST: localhost BILLIG_DEV_HOST: localhost + MDB_DEV_HOST: localhost SM4_DEV_PORT: 5432 BILLIG_DEV_PORT: 5433 + MDB_DEV_PORT: 5434 steps: - name: Checkout code diff --git a/backend/root/db_router.py b/backend/root/db_router.py index f4f47950f..b30a6b408 100644 --- a/backend/root/db_router.py +++ b/backend/root/db_router.py @@ -1,6 +1,6 @@ """ Handles routing for databases (which database should be used for which model). -All models use the default database except billig models. +All models use the default database except billig and mdb models. """ from __future__ import annotations @@ -9,6 +9,7 @@ from django.db import models +from samfundet.models.mdb import MedlemsInfo from samfundet.models.billig import ( BilligEvent, BilligPriceGroup, @@ -22,16 +23,27 @@ BilligPriceGroup, ] +# List of models routed to mdb database. +MDB_MODELS: list[type[models.Model]] = [ + MedlemsInfo, +] + +DISALLOWED_MODELS = {m._meta.model_name for m in BILLIG_MODELS + MDB_MODELS} + class SamfundetDatabaseRouter: def db_for_read(self, model: type[models.Model], **hints: dict[str, Any]) -> str | None: if model in BILLIG_MODELS: return 'billig' + if model in MDB_MODELS: + return 'mdb' return None def db_for_write(self, model: type[models.Model], **hints: dict[str, Any]) -> str | None: if model in BILLIG_MODELS: return 'billig' + if model in MDB_MODELS: + return 'mdb' return None def allow_migrate( @@ -41,4 +53,4 @@ def allow_migrate( model_name: str | None = None, **hints: dict[str, Any], ) -> bool: - return model_name not in [m._meta.model_name for m in BILLIG_MODELS] + return model_name not in DISALLOWED_MODELS diff --git a/backend/root/management/commands/seed_scripts/__init__.py b/backend/root/management/commands/seed_scripts/__init__.py index 70ae54aa9..a3881458b 100755 --- a/backend/root/management/commands/seed_scripts/__init__.py +++ b/backend/root/management/commands/seed_scripts/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations from . import ( + mdb, menu, gangs, merch, @@ -38,6 +39,7 @@ # It can also yield a text description of the current state (see example.py) SEED_SCRIPTS = [ + ('mdb', mdb.seed), ('campus', campus.seed), ('users', superusers.seed), ('images', images.seed), diff --git a/backend/root/management/commands/seed_scripts/mdb.py b/backend/root/management/commands/seed_scripts/mdb.py new file mode 100755 index 000000000..646ea5dc0 --- /dev/null +++ b/backend/root/management/commands/seed_scripts/mdb.py @@ -0,0 +1,100 @@ +# +# Creates and seeds the mdb database. +# +# This seed script is a bit different because the local mdb +# is not managed by django, but uses a different postgres +# database simulating the real mdb in prod (cirkus). +# +# +from __future__ import annotations + +import os +from collections.abc import Iterable + +import django +from django import db +from django.db import transaction + +from samfundet.models.mdb import MedlemsInfo + +# Get django database name from config (see database setup in dev.py) +DB_NAME = db.connections.databases['mdb']['NAME'] +SEED_DIRECTORY = os.path.join(os.path.dirname(__file__), 'seed_mdb') + +# ======================== # +# Create Database # +# ======================== # + + +def get_schema() -> str: + # Generate schema (pass schema.sql to postgres) + seed_schema = os.path.join(SEED_DIRECTORY, 'schema.sql') + with open(seed_schema) as f: + schema = f.read() + return schema + + +def create_db() -> tuple[bool, str]: + """Creates a new postgres database with schema using shell scripts""" + + schema = get_schema() + schema_queries = schema.split(';') + with django.db.connections['mdb'].cursor() as cursor, transaction.atomic(): + for query in schema_queries: + cursor.execute(query) + + return True, 'Created database and schema' + + +# ======================== # +# Seeding # +# ======================== # + +MEMBERS = [ + MedlemsInfo( + medlem_id=10123, + fornavn='Robin', + etternavn='Espinosa Jelle', + fodselsdato='1970-01-01', + telefon='12345678', + mail='robin@example.com', + skole='NTNU', + studie='', + brukernavn='robinej', + ), + MedlemsInfo( + medlem_id=20569, + fornavn='Sanctus', + etternavn='Richardus', + fodselsdato='1970-01-01', + telefon='87654321', + mail='sanctus.richardus@example.com', + skole='NTNU', + studie='', + brukernavn='sanctusr', + ), +] + + +def seed_tables() -> Iterable[tuple[int, str]]: + # Create a few fake members + with transaction.atomic(): + for m in MEMBERS: + m.save() + + yield 100, f'Created {len(MEMBERS)} fake mdb members' + + +# Main seed script entry point +def seed() -> Iterable[tuple[int, str]]: + # Create database and schema + yield 0, 'Creating mdb database...' + ok, message = create_db() + if not ok: + # Failed to create DB + yield 100, message + return + + # Seed mdb database + for percent, message in seed_tables(): + yield percent, message diff --git a/backend/root/management/commands/seed_scripts/seed_mdb/schema.sql b/backend/root/management/commands/seed_scripts/seed_mdb/schema.sql new file mode 100644 index 000000000..ebe04058d --- /dev/null +++ b/backend/root/management/commands/seed_scripts/seed_mdb/schema.sql @@ -0,0 +1,21 @@ +/* + Generates a mdb dummy database schema for local development + + This schema should be identical to the schema for mdb on the ITK servers + for all fields used by the mdb models (see queries in samfundet/models/mdb.py). +*/ + +DROP TABLE IF EXISTS "lim_medlemsinfo"; + +CREATE TABLE "lim_medlemsinfo" ( + medlem_id int NOT NULL, + fornavn varchar(512) DEFAULT NULL, + etternavn varchar(512) DEFAULT NULL, + fodselsdato date DEFAULT NULL, + telefon varchar(32) DEFAULT NULL, + mail varchar(512) DEFAULT NULL, + skole varchar(128) DEFAULT NULL, + studie varchar(128) DEFAULT NULL, + brukernavn varchar(14) NOT NULL, + PRIMARY KEY (medlem_id) +); diff --git a/backend/root/settings/dev.py b/backend/root/settings/dev.py index 8c1d399c4..4ac2affce 100644 --- a/backend/root/settings/dev.py +++ b/backend/root/settings/dev.py @@ -80,6 +80,14 @@ 'HOST': os.environ.get('BILLIG_DEV_HOST', 'billig_dev_database'), # Docker service name or CI host 'PORT': os.environ.get('BILLIG_DEV_PORT', '5432'), # Docker internal port or CI host port }, + 'mdb': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['MDB_DEV_CREDENTIAL'], + 'USER': os.environ['MDB_DEV_CREDENTIAL'], + 'PASSWORD': os.environ['MDB_DEV_CREDENTIAL'], + 'HOST': os.environ.get('MDB_DEV_HOST', 'mdb_dev'), # Docker service name or CI host + 'PORT': os.environ.get('MDB_DEV_PORT', '5432'), # Docker internal port or CI host port + }, } # ======================== # diff --git a/backend/root/settings/prod.py b/backend/root/settings/prod.py index cd712e696..f5cff9244 100644 --- a/backend/root/settings/prod.py +++ b/backend/root/settings/prod.py @@ -44,4 +44,13 @@ 'HOST': os.environ['BILLIG_DB_HOST'], 'PORT': os.environ['BILLIG_DB_PORT'], }, + # The database for the membership database (mdb) + 'mdb': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ['MDB_DB_NAME'], + 'USER': os.environ['MDB_DB_USER'], + 'PASSWORD': os.environ['MDB_DB_PASSWORD'], + 'HOST': os.environ['MDB_DB_HOST'], + 'PORT': os.environ['MDB_DB_PORT'], + }, } diff --git a/backend/samfundet/migrations/0005_medlemsinfo.py b/backend/samfundet/migrations/0005_medlemsinfo.py new file mode 100644 index 000000000..bf47b00fd --- /dev/null +++ b/backend/samfundet/migrations/0005_medlemsinfo.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.11 on 2026-02-14 14:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('samfundet', '0004_user_mdb_medlem_id'), + ] + + operations = [ + migrations.CreateModel( + name='MedlemsInfo', + fields=[ + ('medlem_id', models.PositiveIntegerField(primary_key=True, serialize=False)), + ('fornavn', models.CharField(blank=True, null=True)), + ('etternavn', models.CharField(blank=True, null=True)), + ('fodselsdato', models.DateField(blank=True, null=True)), + ('telefon', models.CharField(blank=True, null=True)), + ('mail', models.CharField(blank=True, null=True)), + ('skole', models.CharField(blank=True, null=True)), + ('studie', models.CharField(blank=True, null=True)), + ('brukernavn', models.CharField(max_length=14)), + ], + options={ + 'verbose_name': 'MedlemsInfo', + 'db_table': 'lim_medlemsinfo', + 'managed': False, + }, + ), + ] diff --git a/backend/samfundet/models/mdb.py b/backend/samfundet/models/mdb.py new file mode 100644 index 000000000..58de86892 --- /dev/null +++ b/backend/samfundet/models/mdb.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from django.db import models + + +class MedlemsInfo(models.Model): + medlem_id = models.PositiveIntegerField(null=False, blank=False, primary_key=True) + fornavn = models.CharField(null=True, blank=True) + etternavn = models.CharField(null=True, blank=True) + fodselsdato = models.DateField(null=True, blank=True) + telefon = models.CharField(null=True, blank=True) + mail = models.CharField(null=True, blank=True) + skole = models.CharField(null=True, blank=True) + studie = models.CharField(null=True, blank=True) + brukernavn = models.CharField(max_length=14, null=False, blank=False) + + class Meta: + managed = False + verbose_name = 'MedlemsInfo' + db_table = 'lim_medlemsinfo' + + def __str__(self) -> str: + return self.medlem_id diff --git a/docker-compose.yml b/docker-compose.yml index bb7e0749b..48dccc936 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: POSTGRES_USER: ${SM4_DEV_CREDENTIAL} POSTGRES_PASSWORD: ${SM4_DEV_CREDENTIAL} ports: - - 5432:5432 + - "5432:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready -U ${SM4_DEV_CREDENTIAL}" ] interval: ${COMMON_HEALTHCHECK_INTERVAL} @@ -26,12 +26,27 @@ services: POSTGRES_USER: ${BILLIG_DEV_CREDENTIAL} POSTGRES_PASSWORD: ${BILLIG_DEV_CREDENTIAL} ports: - - 5433:5432 + - "5433:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready -U ${BILLIG_DEV_CREDENTIAL}" ] interval: ${COMMON_HEALTHCHECK_INTERVAL} timeout: ${COMMON_HEALTHCHECK_TIMEOUT} retries: ${COMMON_HEALTHCHECK_RETRIES} + mdb_dev: + image: ${MDB_DEV_DB_IMAGE} + volumes: + - mdb_dev:/var/lib/postgresql/data + environment: + POSTGRES_DB: ${MDB_DEV_CREDENTIAL} + POSTGRES_USER: ${MDB_DEV_CREDENTIAL} + POSTGRES_PASSWORD: ${MDB_DEV_CREDENTIAL} + ports: + - "5434:5432" + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${MDB_DEV_CREDENTIAL}" ] + interval: ${COMMON_HEALTHCHECK_INTERVAL} + timeout: ${COMMON_HEALTHCHECK_TIMEOUT} + retries: ${COMMON_HEALTHCHECK_RETRIES} backend: build: ./backend # Build in the context of this directory (meaning: use Dockerfile, .dockerignore etc.) image: samfundet-backend @@ -40,6 +55,8 @@ services: condition: service_healthy billig_dev_database: condition: service_healthy + mdb_dev: + condition: service_healthy volumes: - ./backend:/app # Mount backend folder to docker image - /app/.venv/ # Ignore local virtual environment @@ -50,6 +67,9 @@ services: - IS_DOCKER=yes - SM4_DEV_CREDENTIAL=${SM4_DEV_CREDENTIAL} - BILLIG_DEV_CREDENTIAL=${BILLIG_DEV_CREDENTIAL} + - MDB_DEV_CREDENTIAL=${MDB_DEV_CREDENTIAL} + - BILLIG_DEV_HOST=billig_dev_database + - MDB_DEV_HOST=mdb_dev ports: - '8000:8000' # Quotes are required. django - '5678:5678' # Quotes are required. debugpy @@ -134,3 +154,4 @@ services: volumes: sm4_dev_database: billig_dev_database: + mdb_dev: From c395350829f4e3ee13bee117cc9aa44066ed71ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:04:39 +0100 Subject: [PATCH 18/79] Bump sqlparse from 0.5.3 to 0.5.4 in /backend (#2069) Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.5.3 to 0.5.4. - [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG) - [Commits](https://github.com/andialbrecht/sqlparse/compare/0.5.3...0.5.4) --- updated-dependencies: - dependency-name: sqlparse dependency-version: 0.5.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index c25d95d25..75d7a954f 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1290,18 +1290,18 @@ files = [ [[package]] name = "sqlparse" -version = "0.5.3" +version = "0.5.4" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, - {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, + {file = "sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb"}, + {file = "sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e"}, ] [package.extras] -dev = ["build", "hatch"] +dev = ["build"] doc = ["sphinx"] [[package]] From b9dbff0f00ce38b89df770b0b6be4b49cd17b384 Mon Sep 17 00:00:00 2001 From: Robin <16273164+robines@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:35:51 +0100 Subject: [PATCH 19/79] Add endpoint for calling sett_lim_utvidet_medlemsinfo (#2070) * Add mock sett_lim_utvidet_medlemsinfo function * Add endpoint for connecting user to mdb * Update function comment * Update routes * Move serializer to own file under serializer package, validate email/member_id --- .../management/commands/seed_scripts/mdb.py | 4 +- .../commands/seed_scripts/seed_mdb/schema.sql | 33 +++++++++++++++ backend/root/utils/routes.py | 1 + backend/samfundet/models/mdb.py | 19 ++++++++- .../samfundet/serializer/mdb_serializers.py | 20 +++++++++ backend/samfundet/urls.py | 3 ++ backend/samfundet/view/mdb_views.py | 41 +++++++++++++++++++ frontend/src/routes/backend.ts | 1 + 8 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 backend/samfundet/serializer/mdb_serializers.py create mode 100644 backend/samfundet/view/mdb_views.py diff --git a/backend/root/management/commands/seed_scripts/mdb.py b/backend/root/management/commands/seed_scripts/mdb.py index 646ea5dc0..ce4198715 100755 --- a/backend/root/management/commands/seed_scripts/mdb.py +++ b/backend/root/management/commands/seed_scripts/mdb.py @@ -38,10 +38,8 @@ def create_db() -> tuple[bool, str]: """Creates a new postgres database with schema using shell scripts""" schema = get_schema() - schema_queries = schema.split(';') with django.db.connections['mdb'].cursor() as cursor, transaction.atomic(): - for query in schema_queries: - cursor.execute(query) + cursor.execute(schema) return True, 'Created database and schema' diff --git a/backend/root/management/commands/seed_scripts/seed_mdb/schema.sql b/backend/root/management/commands/seed_scripts/seed_mdb/schema.sql index ebe04058d..6cd5e5a69 100644 --- a/backend/root/management/commands/seed_scripts/seed_mdb/schema.sql +++ b/backend/root/management/commands/seed_scripts/seed_mdb/schema.sql @@ -19,3 +19,36 @@ CREATE TABLE "lim_medlemsinfo" ( brukernavn varchar(14) NOT NULL, PRIMARY KEY (medlem_id) ); + +/* + This is a mock-function of the sett_lim_utvidet_medlemsinfo function. + Returns medlem_id on success, otherwise no result. + Password must be 'password' + */ +CREATE OR REPLACE FUNCTION sett_lim_utvidet_medlemsinfo( + p_email_or_id TEXT, + p_password TEXT +) +RETURNS SETOF INTEGER AS $$ +BEGIN + IF p_password <> 'password' THEN + RETURN; + END IF; + + IF p_email_or_id ~ '^\d+$' THEN + RETURN QUERY + SELECT medlem_id + FROM lim_medlemsinfo + WHERE medlem_id = p_email_or_id::INTEGER + LIMIT 1; + ELSIF p_email_or_id LIKE '%@%' THEN + RETURN QUERY + SELECT medlem_id + FROM lim_medlemsinfo + WHERE mail = p_email_or_id + LIMIT 1; + END IF; + + RETURN; +END; +$$ LANGUAGE plpgsql; diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 5a14f4ed1..931d5f52f 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -302,6 +302,7 @@ samfundet__gangsorganized = 'samfundet:gangsorganized' samfundet__check_reservation = 'samfundet:check_reservation' samfundet__reservation_create = 'samfundet:reservation-create' +samfundet__mdb_connect = 'samfundet:mdb_connect' samfundet__active_recruitments = 'samfundet:active_recruitments' samfundet__recruitment_positions = 'samfundet:recruitment_positions' samfundet__recruitment_show_unprocessed_applicants = 'samfundet:recruitment_show_unprocessed_applicants' diff --git a/backend/samfundet/models/mdb.py b/backend/samfundet/models/mdb.py index 58de86892..0224dfc31 100644 --- a/backend/samfundet/models/mdb.py +++ b/backend/samfundet/models/mdb.py @@ -1,6 +1,6 @@ from __future__ import annotations -from django.db import models +from django.db import models, connections class MedlemsInfo(models.Model): @@ -21,3 +21,20 @@ class Meta: def __str__(self) -> str: return self.medlem_id + + +def sett_lim_utvidet_medlemsinfo(member_login: str, password: str) -> int | None: + """ + Calls the sett_lim_utvidet_medlemsinfo database function. + + This function checks if the email/member_id and password are valid. If they + are, a flag is set in the `member` table allowing the user to appear in the + MedlemsInfo database view, and the function returns the member's ID. + """ + + with connections['mdb'].cursor() as cursor: + cursor.execute('SELECT * FROM sett_lim_utvidet_medlemsinfo(%s, %s)', (member_login, password)) + row = cursor.fetchone() + if row: + return row[0] + return None diff --git a/backend/samfundet/serializer/mdb_serializers.py b/backend/samfundet/serializer/mdb_serializers.py new file mode 100644 index 000000000..964aa698c --- /dev/null +++ b/backend/samfundet/serializer/mdb_serializers.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from rest_framework import serializers + +from django.core.validators import validate_email + + +class ConnectToMDBSerializer(serializers.Serializer): + member_login = serializers.CharField(required=True) + password = serializers.CharField(required=True, min_length=1) + + def validate_member_login(self, value: str) -> str: + if not (value.isdigit() or '@' in value): + raise serializers.ValidationError('Must be a valid email address or numeric member ID') + if '@' in value: + try: + validate_email(value) + except Exception as e: + raise serializers.ValidationError('Invalid email address') from e + return value diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index 7151964bf..5bd564529 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -7,6 +7,7 @@ from django.urls import path, include +import samfundet.view.mdb_views import samfundet.view.user_views import samfundet.view.event_views import samfundet.view.sulten_views @@ -93,6 +94,8 @@ ########## Lyche ########## path('check-reservation/', samfundet.view.sulten_views.ReservationCheckAvailabilityView.as_view(), name='check_reservation'), path('reservations/', samfundet.view.sulten_views.ReservationCreateView.as_view(), name='reservation-create'), + ######## MDB ######## + path('mdb/connect', samfundet.view.mdb_views.ConnectToMDBView.as_view(), name='mdb_connect'), ########## Recruitment ########## path('active-recruitments/', views.ActiveRecruitmentsView.as_view(), name='active_recruitments'), path('recruitment-positions/', views.RecruitmentPositionsPerRecruitmentView.as_view(), name='recruitment_positions'), diff --git a/backend/samfundet/view/mdb_views.py b/backend/samfundet/view/mdb_views.py new file mode 100644 index 000000000..215d1d21e --- /dev/null +++ b/backend/samfundet/view/mdb_views.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import logging + +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from samfundet.models.mdb import sett_lim_utvidet_medlemsinfo +from samfundet.serializer.mdb_serializers import ConnectToMDBSerializer + +logger = logging.getLogger('samfundet.views.mdb_views') + + +class ConnectToMDBView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request: Request) -> Response: + user = request.user + + if user.mdb_medlem_id: + return Response({'message': 'You have already connected to MDB'}, status=status.HTTP_400_BAD_REQUEST) + + serializer = ConnectToMDBSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + member_login = serializer.validated_data['member_login'] + password = serializer.validated_data['password'] + + logger.info(f'User #{user.id} attempting to connect to MDB with email {member_login}...') + + medlem_id = sett_lim_utvidet_medlemsinfo(member_login, password) + if medlem_id: + user.mdb_medlem_id = medlem_id + user.save() + logger.info(f'Connected user #{user.id} to MDB with medlem_id {medlem_id}') + return Response({'message': 'Successfully connected account to MDB!'}) + + return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index 7aa60c6e4..1c73f75ff 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -300,6 +300,7 @@ export const ROUTES_BACKEND = { samfundet__gangsorganized: '/gangtypes/:organization/', samfundet__check_reservation: '/check-reservation/', samfundet__reservation_create: '/reservations/', + samfundet__mdb_connect: '/mdb/connect', samfundet__active_recruitments: '/active-recruitments/', samfundet__recruitment_positions: '/recruitment-positions/', samfundet__recruitment_show_unprocessed_applicants: '/recruitment-show-unprocessed-applicants/', From 41c5855f2ed8321f1652e0b9223e785a3d65a635 Mon Sep 17 00:00:00 2001 From: Lidavic <148764396+Lidavic@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:24:27 +0100 Subject: [PATCH 20/79] Fix event search for control panel and events page (#2063) * search field for eventsadminpage and updated storybook for eventquery * implements search for events * biome --- .../EventQuery/EventQuery.stories.tsx | 3 +++ .../src/Components/EventQuery/EventQuery.tsx | 15 ++++++++++----- .../components/EventsList/EventsList.tsx | 11 +++++++++-- .../EventsAdminPage/EventsAdminPage.tsx | 18 +++++++++++++++++- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/frontend/src/Components/EventQuery/EventQuery.stories.tsx b/frontend/src/Components/EventQuery/EventQuery.stories.tsx index ff85d1c01..b93dc6dc0 100644 --- a/frontend/src/Components/EventQuery/EventQuery.stories.tsx +++ b/frontend/src/Components/EventQuery/EventQuery.stories.tsx @@ -26,6 +26,7 @@ export const Basic: Story = { render: (args) => { const [selectedVenue, setSelectedVenue] = useState(null); const [selectedCategory, setSelectedCategory] = useState(null); + const [search, setSearch] = useState(''); return ( ); }, diff --git a/frontend/src/Components/EventQuery/EventQuery.tsx b/frontend/src/Components/EventQuery/EventQuery.tsx index 73ea37973..91bd56e3b 100644 --- a/frontend/src/Components/EventQuery/EventQuery.tsx +++ b/frontend/src/Components/EventQuery/EventQuery.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { InputField } from '~/Components'; import { KEY } from '~/i18n/constants'; @@ -15,14 +14,20 @@ type EventQueryProps = { setSelectedVenue: SetState; selectedCategory: EventCategoryValue | null; setSelectedCategory: SetState; + search: string; + setSearch: SetState; }; -export function EventQuery({ venues, categories, setSelectedVenue, setSelectedCategory }: EventQueryProps) { +export function EventQuery({ + venues, + categories, + setSelectedVenue, + setSelectedCategory, + search, + setSearch, +}: EventQueryProps) { const { t } = useTranslation(); - // Search - const [search, setSearch] = useState(''); - const venueOptions: DropdownOption[] = (venues ?? []).map((venue) => { return { label: venue ?? '', value: venue } as DropdownOption; }); diff --git a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx index aae3c01ea..2a4688970 100644 --- a/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx +++ b/frontend/src/Pages/EventsPage/components/EventsList/EventsList.tsx @@ -20,7 +20,7 @@ type EventsListProps = { }; export function EventsList({ events }: EventsListProps) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [tableView, setTableView] = useState(false); const [query, setQuery] = useState(''); const isDesktop = useDesktop(); @@ -39,7 +39,14 @@ export function EventsList({ events }: EventsListProps) { // TODO debounce and move header/filtering stuff to a separate component function filteredEvents() { const allEvents = Object.keys(events).flatMap((k: string) => events[k]); - return eventQuery(allEvents, query); + const normalizedSearch = query.trim().toLowerCase(); + const keywords = normalizedSearch.split(' '); + + if (query === '') return eventQuery(allEvents, query); + return allEvents.filter((event) => { + const title = (dbT(event, 'title', i18n.language) as string)?.toLowerCase() ?? ''; + return keywords.every((kw) => title.includes(kw)); + }); } // TODO improve table view for events diff --git a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx index bd860190c..65312b2ba 100644 --- a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx @@ -29,6 +29,7 @@ export function EventsAdminPage() { const [categories, setCategories] = useState([]); const [selectedVenue, setSelectedVenue] = useState(null); const [selectedCategory, setSelectedCategory] = useState(null); + const [search, setSearch] = useState(''); function getEvents(venue?: string | null, category?: EventCategoryValue | null) { getEventsUpcomming({ venue: venue ?? undefined, category: category ?? undefined }) @@ -45,6 +46,17 @@ export function EventsAdminPage() { }); } + function filterEvents(): EventDto[] { + const normalizedSearch = search.trim().toLowerCase(); + if (search === '') return events; + const keywords = normalizedSearch.split(' '); + + return events.filter((event: EventDto) => { + const title = (dbT(event, 'title', i18n.language) as string)?.toLowerCase() ?? ''; + return keywords.every((kw) => title.includes(kw)); + }); + } + // Stuff to do on first render. // TODO add permissions on render // biome-ignore lint/correctness/useExhaustiveDependencies: @@ -74,7 +86,9 @@ export function EventsAdminPage() { '', // Buttons ]; - const data = events.map((event: EventDto) => ({ + const filteredEvents = filterEvents(); + + const data = filteredEvents.map((event: EventDto) => ({ cells: [ dbT(event, 'title', i18n.language) as string, { content: , value: event.start_dt }, @@ -150,6 +164,8 @@ export function EventsAdminPage() { setSelectedVenue={setSelectedVenue} selectedCategory={selectedCategory} setSelectedCategory={setSelectedCategory} + search={search} + setSearch={setSearch} />
    From 56e2c2cc4a7eada8edec25e43d81b40ee90907cd Mon Sep 17 00:00:00 2001 From: Eilif Hjermann Lindblad Date: Mon, 16 Feb 2026 22:14:55 +0100 Subject: [PATCH 21/79] Improve styling of ExpandableHeader (#2067) * Implement bbc-style dropdown * Use global constants * Shorten transitions * Change dark mode contrasts, shorten transitions * Change text color in dark mode * Redhat-style border accent * Change bg color in dark * Make hover bg color prioritized * Change light mode bg color --- .../ExpandableHeader.module.scss | 120 +++++++++++------- .../ExpandableHeader/ExpandableHeader.tsx | 29 +++-- 2 files changed, 90 insertions(+), 59 deletions(-) diff --git a/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss b/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss index b8d1e3099..3664a2c92 100644 --- a/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss +++ b/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss @@ -4,86 +4,110 @@ .container { width: 100%; - border: 2px solid $grey-3; - overflow: hidden; + border-radius: 0; + margin-bottom: 1rem; + position: relative; + border: 1px solid $grey-35; @include theme-dark { border: 1px solid $grey-0; } } -.parent { - border-radius: 10px; - border-color: $grey-4; - background-color: $grey-4; - - @include theme-dark { - background-color: #1d0809; - border-color: transparent; - } -} - -.child { - border: 1px solid transparent; - border-radius: 10px; - margin: 10px; - width: auto; - - @include theme-dark { - border-color: transparent; - } +// accent border +.with_branding.open::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -1px; + width: 5px; + background-color: $red-samf; + z-index: 10; } .extendable_header_wrapper { + appearance: none; display: flex; flex-direction: row; justify-content: space-between; align-items: center; - font-size: 20px; - font-weight: 400; - background: $grey_4; - padding: 0.5em 1.5em 0.5em 1em; - color: $black; + width: 100%; + padding: 1rem 1.25rem; + background-color: $white; + color: $black-2; + border: none; + border-bottom: 1px solid transparent; cursor: pointer; - border: 0; outline: none; text-align: left; - width: 100%; + transition: background-color 0.2s ease; - @include theme-dark { - background-color: #1d0809; - color: white; + &:hover { + background-color: $grey-35; } -} -.extendable_header_wrapper:hover { - background-color: #dbdbdb; + &.open { + border-bottom-color: $grey-35; + } @include theme-dark { - background-color: #1d0809; + background-color: $theme-dark-input-bg; + color: $white; + + &:hover { + background-color: $grey-1; + } + + &.open { + border-bottom-color: $grey-0; + } } } .extendable_header_title { - display: block; - flex-basis: 95%; - padding: 0; margin: 0; + font-weight: 700; + font-size: 1.125rem; + line-height: 1.2; } .expandable_header_arrow { - flex-basis: 5%; - height: 35px; - width: 35px; - pointer-events: none; - padding: 0; display: flex; - justify-content: center; align-items: center; - font-size: medium; - font-weight: 400; + justify-content: center; + width: 24px; + height: 24px; + transition: transform 0.2s ease; + transform-origin: center; +} + +.chevron { + display: inline-block; + width: 0.6em; + height: 0.6em; + border-style: solid; + border-width: 0.15em 0.15em 0 0; + border-color: currentcolor; + vertical-align: middle; + transform: rotate(135deg); + margin-top: -4px; } .expandable_header_arrow.open { - transform: rotate(-180deg); + transform: rotate(180deg); +} + +.expandable_header_arrow.closed { + transform: rotate(0deg); +} + +.content_wrapper { + padding: 1rem; + background-color: $white; + + @include theme-dark { + background-color: $theme-dark-input-bg; + color: $white; + } } diff --git a/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx b/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx index 30c3aa695..1a8a658ea 100644 --- a/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx +++ b/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx @@ -10,6 +10,7 @@ type ExpandableHeaderProps = { children?: ReactNode; showByDefault?: boolean; theme?: HeaderThemes; + borderBranding?: boolean; }; export function ExpandableHeader({ @@ -18,23 +19,29 @@ export function ExpandableHeader({ children, theme = 'parent', showByDefault = false, + borderBranding = false, }: ExpandableHeaderProps) { - const [showChildren, setShowChildren] = useState(showByDefault); - const classNames = classnames(className, styles.extendable_header_wrapper); - const containerClassNames = classnames(styles.container, { - [styles.parent]: theme === 'parent', - [styles.child]: theme === 'child', + const [isOpen, setIsOpen] = useState(showByDefault); + + const containerClasses = classnames(styles.container, className, { + [styles.open]: isOpen, + [styles.with_branding]: borderBranding, }); + const buttonClasses = classnames(styles.extendable_header_wrapper, isOpen ? styles.open : styles.closed); + + const arrowClasses = classnames(styles.expandable_header_arrow, isOpen ? styles.open : styles.closed); + return ( -
    - - {showChildren && children} + + {isOpen &&
    {children}
    }
    ); } From d1249c282fc8ed334f176349ce7a7f5bde7cf744 Mon Sep 17 00:00:00 2001 From: Erik Hoff <142814286+aTrueYety@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:36:09 +0100 Subject: [PATCH 22/79] Remove white text color from dark mode dropdown component (#2056) Co-authored-by: Erik --- frontend/src/Components/Dropdown/Dropdown.module.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/Components/Dropdown/Dropdown.module.scss b/frontend/src/Components/Dropdown/Dropdown.module.scss index 3957e918c..fbf0bf902 100644 --- a/frontend/src/Components/Dropdown/Dropdown.module.scss +++ b/frontend/src/Components/Dropdown/Dropdown.module.scss @@ -35,7 +35,6 @@ } @include theme-dark { - color: white; border-color: $grey-0; &:focus { border-color: $grey-1; From c4db7a0d5d25f0a3d4cc947f4ec06ea1363c9b28 Mon Sep 17 00:00:00 2001 From: Erik Hoff <142814286+aTrueYety@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:40:59 +0100 Subject: [PATCH 23/79] disable body scroll when modal component is active (#2066) * disable body scroll when modal component is active * correct body overflow handling when modal is inactive --------- Co-authored-by: Erik --- frontend/src/Components/Modal/Modal.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/Components/Modal/Modal.tsx b/frontend/src/Components/Modal/Modal.tsx index c2a3a1ea0..ba593456c 100644 --- a/frontend/src/Components/Modal/Modal.tsx +++ b/frontend/src/Components/Modal/Modal.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { useEffect } from 'react'; import ReactModal from 'react-modal'; import styles from './Modal.module.scss'; @@ -9,11 +10,24 @@ import styles from './Modal.module.scss'; interface ModalProps extends ReactModal.Props { className?: string; } -export function Modal({ children, className, ...props }: ModalProps) { +export function Modal({ children, className, isOpen, ...props }: ModalProps) { + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + return ( // @ts-ignore Date: Thu, 19 Feb 2026 21:49:21 +0100 Subject: [PATCH 24/79] added my name as contributor (#2076) slayyyy --- frontend/src/Pages/ContributorsPage/ContributorsPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx b/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx index 666b3ce1b..30454b6f2 100644 --- a/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx +++ b/frontend/src/Pages/ContributorsPage/ContributorsPage.tsx @@ -56,6 +56,7 @@ const CONTRIBUTORS: Contributor[] = [ { name: 'Tor Madsen', github: 'madt2', from: 'H25' }, // V26 { name: 'Brage Sebastian Brevik', github: 'BragonSB', from: 'V26'}, + { name: 'Lea Emilie Schjoll', github: 'leaesc', from: 'V26'}, ]; // @formatter:on From 2da85f1d9ae5f35f8a6f286ced5f6c914f25ad98 Mon Sep 17 00:00:00 2001 From: Specter <121578250+0xSpecter@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:15:23 +0100 Subject: [PATCH 25/79] breadcrum linked to profile page which was disabled, just reanabled it (#2077) --- frontend/src/constants/site-features.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/constants/site-features.ts b/frontend/src/constants/site-features.ts index 3e212ed20..3dbb6af45 100644 --- a/frontend/src/constants/site-features.ts +++ b/frontend/src/constants/site-features.ts @@ -3,7 +3,7 @@ import { ROUTES_FRONTEND } from '~/routes/frontend'; import type { SiteFeature } from '~/types'; const SITE_FEATURES: Record = { - profile: false, + profile: true, changePassword: false, events: true, images: true, From aefbf44ff32fe32ee5ecd7f9b5bb31d3a11b8a70 Mon Sep 17 00:00:00 2001 From: Tor Madsen <69582484+Madt2@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:37:34 +0100 Subject: [PATCH 26/79] Fixed labels for categories on EventsAdminPage (#2044) --- frontend/src/Components/EventQuery/EventQuery.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Components/EventQuery/EventQuery.tsx b/frontend/src/Components/EventQuery/EventQuery.tsx index 91bd56e3b..48ce31dd7 100644 --- a/frontend/src/Components/EventQuery/EventQuery.tsx +++ b/frontend/src/Components/EventQuery/EventQuery.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { InputField } from '~/Components'; import { KEY } from '~/i18n/constants'; import type { EventCategoryValue, SetState } from '~/types'; -import { lowerCapitalize } from '~/utils'; +import { getEventCategoryKey, lowerCapitalize } from '~/utils'; import { Dropdown } from '../Dropdown'; import type { DropdownOption } from '../Dropdown/Dropdown'; import styles from './EventQuery.module.scss'; @@ -33,7 +33,7 @@ export function EventQuery({ }); const categoryOptions: DropdownOption[] = (categories ?? []).map((category) => { - return { label: category, value: category } as DropdownOption; + return { label: t(getEventCategoryKey(category)), value: category } as DropdownOption; }); return ( From baaaa8a723fd4fbfd65ea3d37355ac84b391a462 Mon Sep 17 00:00:00 2001 From: Erik Hoff <142814286+aTrueYety@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:11:34 +0100 Subject: [PATCH 27/79] Add pagination to control panel ImageAdminPage (#2075) * Add pagination to ImageAdminPage * Add utility for making paginated urls * Add pagination support for images in API * Add pagination and search functionality to ImageView * Refactorize ImageAdminPage to use backend pagination and filtering * biome fix * fix ruff * biome fix * boime pls.. * fix to generate routes duplicate route bug * use react querry * boime fix * Refactor into query keys --------- Co-authored-by: Erik --- .../management/commands/generate_routes.py | 10 ++- backend/root/utils/routes.py | 1 - backend/samfundet/utils.py | 3 +- backend/samfundet/view/general_views.py | 5 ++ .../ImageAdminPage/ImageAdminPage.tsx | 80 +++++++++++++------ frontend/src/api.ts | 16 ++++ frontend/src/queryKeys.ts | 8 ++ frontend/src/routes/backend.ts | 2 +- frontend/src/utils.ts | 35 ++++++++ 9 files changed, 130 insertions(+), 30 deletions(-) diff --git a/backend/root/management/commands/generate_routes.py b/backend/root/management/commands/generate_routes.py index 3a6f7778d..8ab438a49 100644 --- a/backend/root/management/commands/generate_routes.py +++ b/backend/root/management/commands/generate_routes.py @@ -149,9 +149,12 @@ def handle(self, *args, **options) -> None: # type: ignore frontend_file.write('export const ROUTES_BACKEND = {') + # Track seen names to avoid duplicates + seen_names: set[str] = set() + # Parse all urls to frontend routes. for url in urls: - if '' in url.url: + if '' in url.url or ' None: # type: ignore parsed_url = parse_url(url.url) parsed_name = parse_name(url.name or parsed_url) # 'samfundet:new_tjeneste' -> 'samfundet__new_tjeneste' + # Skip duplicates + if parsed_name in seen_names: + continue + seen_names.add(parsed_name) + # Write to file. frontend_file.write(NEWLINE + f" {parsed_name}: '{parsed_url}',") backend_file.write(NEWLINE + f"{parsed_name} = '{url.name}'") diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 931d5f52f..6f7cc6f0a 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -279,7 +279,6 @@ samfundet__billig_ticket_group_list = 'samfundet:billig_ticket_group-list' samfundet__billig_ticket_group_detail = 'samfundet:billig_ticket_group-detail' samfundet__api_root = 'samfundet:api-root' -samfundet__api_root = 'samfundet:api-root' samfundet__schema = 'samfundet:schema' samfundet__swagger_ui = 'samfundet:swagger_ui' samfundet__redoc = 'samfundet:redoc' diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index 8e586140a..de4f01be3 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -1,7 +1,6 @@ from __future__ import annotations import datetime -from typing import Optional from operator import or_ from functools import reduce from collections.abc import Callable @@ -39,7 +38,7 @@ } -def event_query(*, query: QueryDict, events: Optional[QuerySet[Event]] = None) -> QuerySet[Event]: +def event_query(*, query: QueryDict, events: QuerySet[Event] | None = None) -> QuerySet[Event]: qs = events if events is not None else Event.objects.all() search = query.get('search') diff --git a/backend/samfundet/view/general_views.py b/backend/samfundet/view/general_views.py index 43dc7f89e..dee2cf647 100644 --- a/backend/samfundet/view/general_views.py +++ b/backend/samfundet/view/general_views.py @@ -9,6 +9,7 @@ from rest_framework import status from rest_framework.views import APIView +from rest_framework.filters import SearchFilter from rest_framework.request import Request from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.response import Response @@ -24,6 +25,7 @@ from root.custom_classes.permission_classes import FeatureEnabled, RoleProtectedOrAnonReadOnlyObjectPermissions from samfundet.homepage import homepage +from samfundet.pagination import CustomPageNumberPagination from samfundet.models.role import Role, UserOrgRole, UserGangRole, UserGangSectionRole from samfundet.serializers import ( TagSerializer, @@ -105,6 +107,9 @@ class ImageView(ModelViewSet): ) serializer_class = ImageSerializer queryset = Image.objects.all().order_by('-pk') + pagination_class = CustomPageNumberPagination + filter_backends = [SearchFilter] + search_fields = ['title'] @action(detail=True, methods=['get']) def linked_events(self, request: Request, pk: int | None = None) -> Response: diff --git a/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx b/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx index b140757fc..ad6fbcfc3 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx +++ b/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx @@ -1,34 +1,53 @@ -import { useEffect, useState } from 'react'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, ImageQuery } from '~/Components'; -import { getImages } from '~/api'; -import type { ImageDto } from '~/dto'; +import { Button, InputField } from '~/Components'; +import { PagedPagination } from '~/Components/Pagination'; +import { getImagesPaginated } from '~/api'; import { useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; +import { imageKeys } from '~/queryKeys'; import { ROUTES } from '~/routes'; import { lowerCapitalize } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './ImageAdminPage.module.scss'; import { AdminImage } from './components'; +const PAGE_SIZE = 20; + export function ImageAdminPage() { - const [images, setImages] = useState([]); - const [allImages, setAllImages] = useState([]); - const [showSpinner, setShowSpinner] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [searchInput, setSearchInput] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const debounceTimeout = useRef(); const { t } = useTranslation(); useTitle(t(KEY.admin_images_title)); - // Stuff to do on first render. - // TODO add permissions on render + // Debounce search input useEffect(() => { - getImages() - .then((data) => { - setImages(data); - setAllImages(data); - setShowSpinner(false); - }) - .catch(console.error); - }, []); + clearTimeout(debounceTimeout.current); + debounceTimeout.current = setTimeout(() => { + setDebouncedSearch(searchInput); + }, 300); + + return () => clearTimeout(debounceTimeout.current); + }, [searchInput]); + + // Reset to page 1 when search changes + // biome-ignore lint/correctness/useExhaustiveDependencies: Need to trigger on debouncedSearch change + useEffect(() => { + setCurrentPage(1); + }, [debouncedSearch]); + + // Fetch images using React Query + const { data, isLoading } = useQuery({ + queryKey: imageKeys.list(currentPage, debouncedSearch || undefined), + queryFn: () => getImagesPaginated(currentPage, PAGE_SIZE, debouncedSearch || undefined), + placeholderData: keepPreviousData, + }); + + const images = data?.results ?? []; + const totalCount = data?.count ?? 0; const title = t(KEY.admin_images_title); const backendUrl = ROUTES.backend.admin__samfundet_image_changelist; @@ -38,21 +57,32 @@ export function ImageAdminPage() { ); - // Limit maximum number of rendered images - // TODO pagination & lazy load - const displayImages = images.slice(0, Math.min(images.length, 64)); + const handleSearchChange = useCallback((value: string) => { + setSearchInput(value); + }, []); return ( - +
    - +
    - {displayImages.map((element) => ( + {images.map((element) => ( ))} - {/* TODO pagination or translation */} - {images.length > displayImages.length && And {images.length - displayImages.length} more...} +
    +
    +
    ); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index f644fd873..bd6998b46 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -58,6 +58,7 @@ import { ROUTES } from '~/routes'; import type { BilligEventDto } from './apis/billig/billigDtos'; import { BACKEND_DOMAIN } from './constants'; import type { PageNumberPaginationType } from './types'; +import { buildPaginatedUrl } from './utils'; export async function getCsrfToken(): Promise { const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__csrf; @@ -569,6 +570,21 @@ export async function getImages(): Promise { return response.data; } +export async function getImagesPaginated( + page: number, + pageSize?: number, + search?: string, +): Promise> { + const url = buildPaginatedUrl( + BACKEND_DOMAIN + ROUTES.backend.samfundet__images_list, + page, + pageSize, + search ? { search } : undefined, + ); + const response = await axios.get>(url, { withCredentials: true }); + return response.data; +} + export async function getImage(id: string | number): Promise { const url = BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__images_detail, urlParams: { pk: id } }); const response = await axios.get(url, { withCredentials: true }); diff --git a/frontend/src/queryKeys.ts b/frontend/src/queryKeys.ts index aed311870..add88d9e5 100644 --- a/frontend/src/queryKeys.ts +++ b/frontend/src/queryKeys.ts @@ -82,3 +82,11 @@ export const venueKeys = { detail: (slug: string) => [...venueKeys.details(), slug] as const, open: () => [...venueKeys.list(['open'])] as const, }; + +export const imageKeys = { + all: ['images'] as const, + lists: () => [...imageKeys.all, 'list'] as const, + list: (page: number, search?: string) => [...imageKeys.lists(), { page, search }] as const, + details: () => [...imageKeys.all, 'detail'] as const, + detail: (id: number) => [...imageKeys.details(), id] as const, +}; diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index 1c73f75ff..c994d2db4 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -336,4 +336,4 @@ export const ROUTES_BACKEND = { samfundet__recruitment_all_applications: '/recruitment/all-applications/', static__path: '/static/:path', media__path: '/media/:path', -} as const; +} as const; \ No newline at end of file diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 48771da8a..9e5c6f944 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -499,3 +499,38 @@ export function handleServerFormErrors( toast.error(i18next.t(KEY.error_generic_description)); } } + +/** + * Build URL with pagination query parameters + * @param baseUrl - The base URL for the API endpoint + * @param page - Current page number (1-indexed) + * @param pageSize - Number of items per page (optional, defaults to backend default) + * @param additionalParams - Additional query parameters as key-value pairs + * @returns Complete URL with pagination parameters + * @example + * buildPaginatedUrl('/api/images', 1, 20, { search: 'test' }) + * // Returns: '/api/images?page=1&page_size=20&search=test' + */ +export function buildPaginatedUrl( + baseUrl: string, + page: number, + pageSize?: number, + additionalParams?: Record, +): string { + const params = new URLSearchParams(); + params.append('page', page.toString()); + + if (pageSize !== undefined) { + params.append('page_size', pageSize.toString()); + } + + if (additionalParams) { + for (const [key, value] of Object.entries(additionalParams)) { + if (value !== undefined) { + params.append(key, value.toString()); + } + } + } + + return `${baseUrl}?${params.toString()}`; +} From 3bb25a14014db1d8fb7abaae1117232502d2895e Mon Sep 17 00:00:00 2001 From: Erik Hoff <142814286+aTrueYety@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:38:08 +0100 Subject: [PATCH 28/79] Add pagination and filtering to ImagePicker component (#2073) * Add pagination and filtering to ImagePicker component * Add pagination and filtering backend in ImagePicker component * remove old getImages endpoint * Move styling in imape picker to sass --------- Co-authored-by: Erik --- .../ImagePicker/ImagePicker.module.scss | 12 ++++ .../Components/ImagePicker/ImagePicker.tsx | 66 ++++++++++++++++--- frontend/src/api.ts | 6 -- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/frontend/src/Components/ImagePicker/ImagePicker.module.scss b/frontend/src/Components/ImagePicker/ImagePicker.module.scss index 1995fc242..b0c1ba438 100644 --- a/frontend/src/Components/ImagePicker/ImagePicker.module.scss +++ b/frontend/src/Components/ImagePicker/ImagePicker.module.scss @@ -4,6 +4,7 @@ .container { @include flex-row-center; + justify-content: space-evenly; gap: 0.5em; } @@ -66,3 +67,14 @@ transform: scale(1); } } + +.search_wrapper { + display: flex; + flex-direction: column; +} + +.pagination_wrapper { + display: flex; + justify-content: center; + margin-top: 1rem; +} diff --git a/frontend/src/Components/ImagePicker/ImagePicker.tsx b/frontend/src/Components/ImagePicker/ImagePicker.tsx index 89d1445e5..16f58b078 100644 --- a/frontend/src/Components/ImagePicker/ImagePicker.tsx +++ b/frontend/src/Components/ImagePicker/ImagePicker.tsx @@ -1,14 +1,20 @@ import { Icon } from '@iconify/react'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; import classNames from 'classnames'; -import { type ReactNode, useEffect, useState } from 'react'; +import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { getImages } from '~/api'; +import { getImagesPaginated } from '~/api'; import { BACKEND_DOMAIN } from '~/constants'; import type { ImageDto } from '~/dto'; import { KEY } from '~/i18n/constants'; +import { imageKeys } from '~/queryKeys'; import { backgroundImageFromUrl } from '~/utils'; +import { InputField } from '../InputField'; +import { PagedPagination } from '../Pagination'; import styles from './ImagePicker.module.scss'; +const PAGE_SIZE = 12; + export type ImagePickerProps = { onSelected?(image: ImageDto): void; selectedImage?: ImageDto; @@ -17,13 +23,36 @@ export type ImagePickerProps = { export function ImagePicker({ onSelected, selectedImage }: ImagePickerProps) { const { t } = useTranslation(); const [selected, setSelected] = useState(selectedImage); - const [images, setImages] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [searchInput, setSearchInput] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const debounceTimeout = useRef(); + // Debounce search input useEffect(() => { - getImages() - .then((imgs) => setImages(imgs)) - .catch(() => console.error); - }, []); + clearTimeout(debounceTimeout.current); + debounceTimeout.current = setTimeout(() => { + setDebouncedSearch(searchInput); + }, 300); + + return () => clearTimeout(debounceTimeout.current); + }, [searchInput]); + + // Reset to page 1 when search changes + // biome-ignore lint/correctness/useExhaustiveDependencies: Need to trigger on debouncedSearch change + useEffect(() => { + setCurrentPage(1); + }, [debouncedSearch]); + + // Fetch images using React Query + const { data } = useQuery({ + queryKey: imageKeys.list(currentPage, debouncedSearch || undefined), + queryFn: () => getImagesPaginated(currentPage, PAGE_SIZE, debouncedSearch || undefined), + placeholderData: keepPreviousData, + }); + + const images = data?.results ?? []; + const totalCount = data?.count ?? 0; function select(image: ImageDto) { setSelected(image); @@ -43,6 +72,10 @@ export function ImagePicker({ onSelected, selectedImage }: ImagePickerProps) { ); } + const handleSearchChange = useCallback((value: string) => { + setSearchInput(value); + }, []); + return (
    @@ -55,9 +88,24 @@ export function ImagePicker({ onSelected, selectedImage }: ImagePickerProps) { )}
    - {/* TODO tags and other metadata */}
    -
    {images.map((image) => renderImage(image))}
    +
    + +
    {images.map((image) => renderImage(image))}
    +
    + +
    +
    ); } diff --git a/frontend/src/api.ts b/frontend/src/api.ts index bd6998b46..7215af6e6 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -564,12 +564,6 @@ export async function deleteClosedPeriod(id: string | number): Promise { - const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__images_list; - const response = await axios.get(url, { withCredentials: true }); - return response.data; -} - export async function getImagesPaginated( page: number, pageSize?: number, From 654f19940a44a91333950c6c111fa790f1511a5b Mon Sep 17 00:00:00 2001 From: BragonSB Date: Thu, 26 Feb 2026 21:52:55 +0100 Subject: [PATCH 29/79] feat: add Choose as the default value in the Dropdowns (#2084) --- .../EventCreatorAdminPage/EventCreatorAdminPage.tsx | 13 +++++++------ .../components/PaymentForm.tsx | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index f95eb2c30..eb42ad5ba 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -90,12 +90,12 @@ export function EventCreatorAdminPage() { start_dt: '', duration: 0, end_dt: '', - category: eventCategoryOptions[0].value, + category: undefined, host: '', - location: locationOptions.length > 0 ? locationOptions[0].value : '', + location: undefined, capacity: undefined, - age_restriction: 'none', - ticket_type: 'free', + age_restriction: undefined, + ticket_type: undefined, custom_tickets: [], billig_id: undefined, image: undefined, @@ -335,7 +335,7 @@ export function EventCreatorAdminPage() { {t(KEY.category)} - + @@ -369,6 +369,7 @@ export function EventCreatorAdminPage() { ({ value: venue.name, label: venue.name }))} + nullOption={{ label: t(KEY.common_choose) }} {...field} /> @@ -420,7 +421,7 @@ export function EventCreatorAdminPage() { {t(KEY.common_age_limit)} - + diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/components/PaymentForm.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/components/PaymentForm.tsx index 2016ca4bf..b9dfaf575 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/components/PaymentForm.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/components/PaymentForm.tsx @@ -53,7 +53,7 @@ export function PaymentForm({ event, onChange }: PaymentFormProps) { {t(KEY.common_the_ticket_type)} - + From 4c665ff8ba1a1e8f06674ca38c2d437bfca5d929 Mon Sep 17 00:00:00 2001 From: Tor Madsen <69582484+Madt2@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:53:44 +0100 Subject: [PATCH 30/79] 1962 recreate samf3 navbar in samf 4 (#1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactored Navbar to have same menu items as samf3 navbar. Styling not done * Corrected vulunteer link * Copied navbar items and css, and removed unused styling for samf 3 navbar * Renamed css to samf3 naming scheme, and updated navbar item ref to samf 3 navbar items. Also split recruitment from the rest of the navbar links / items. * Fixed unupdated navbar item ref * Removed theme switch, samf 3 navbar does not have theme switch * Removed transparancy check, samf3 navbar does not have transparancy * Added missing recruitment button * Added missing export for navbarItems for samf 3 navbar * Fixed missing translations * Fixed unsafe use of variables in route links * Fixed suggested feedback from PR. * Updated comments * Merged master into branch * Fixed biome errors * Fixed navbar going over front page image. * Fixed comments from pr * Biome onclick issue solved, and navbar links are tabbable * Removed unused hook, and fixed biome feedback * Add myself to the list of contributors for H25 (#1959) * Bump tmp from 0.2.3 to 0.2.4 in /frontend (#1916) Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.3 to 0.2.4. - [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md) - [Commits](https://github.com/raszi/node-tmp/compare/v0.2.3...v0.2.4) --- updated-dependencies: - dependency-name: tmp dependency-version: 0.2.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lidavic <148764396+Lidavic@users.noreply.github.com> * Add member button link to ITK Samf member site (#1983) * Add member button link to ITK Samf member site New member button in the Navbar that links to the Samfundet member site using a constant defined in samf-three.ts. Also refactors cookie property check for impersonation to use Object.prototype.hasOwnProperty because it's safer and more reliable * Update frontend/src/Components/Navbar/Navbar.tsx Co-authored-by: Robin <16273164+robines@users.noreply.github.com> --------- Co-authored-by: Robin <16273164+robines@users.noreply.github.com> * Bump django-cors-headers from 4.7.0 to 4.9.0 in /backend (#1974) Bumps [django-cors-headers](https://github.com/adamchainz/django-cors-headers) from 4.7.0 to 4.9.0. - [Changelog](https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst) - [Commits](https://github.com/adamchainz/django-cors-headers/compare/4.7.0...4.9.0) --- updated-dependencies: - dependency-name: django-cors-headers dependency-version: 4.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump djangorestframework from 3.16.0 to 3.16.1 in /backend (#1973) Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.16.0 to 3.16.1. - [Release notes](https://github.com/encode/django-rest-framework/releases) - [Commits](https://github.com/encode/django-rest-framework/compare/3.16.0...3.16.1) --- updated-dependencies: - dependency-name: djangorestframework dependency-version: 3.16.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump bandit from 1.8.3 to 1.8.6 in /backend (#1990) Bumps [bandit](https://github.com/PyCQA/bandit) from 1.8.3 to 1.8.6. - [Release notes](https://github.com/PyCQA/bandit/releases) - [Commits](https://github.com/PyCQA/bandit/compare/1.8.3...1.8.6) --- updated-dependencies: - dependency-name: bandit dependency-version: 1.8.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add new font via adobe fonts (#1970) * Add new font via adobe fonts * biome * Update contributors (#1992) * Bump requests from 2.32.4 to 2.32.5 in /backend (#1988) Bumps [requests](https://github.com/psf/requests) from 2.32.4 to 2.32.5. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.4...v2.32.5) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump js-yaml from 4.1.0 to 4.1.1 in /frontend (#1995) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add .sqlite3 files to .gitignore (#1997) * Remove inactive codeowners, remove lockfile codeowners (#1999) * Bump django from 5.1.9 to 5.1.14 in /backend (#1996) Bumps [django](https://github.com/django/django) from 5.1.9 to 5.1.14. - [Commits](https://github.com/django/django/compare/5.1.9...5.1.14) --- updated-dependencies: - dependency-name: django dependency-version: 5.1.14 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump @tanstack/react-query from 5.69.0 to 5.84.0 in /frontend (#1912) Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.69.0 to 5.84.0. - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.84.0/packages/react-query) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-version: 5.84.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eilif Hjermann Lindblad * Bump mypy from 1.15.0 to 1.17.1 in /backend (#1911) Bumps [mypy](https://github.com/python/mypy) from 1.15.0 to 1.17.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.15.0...v1.17.1) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.17.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eilif Hjermann Lindblad * Bump axios from 1.8.4 to 1.12.0 in /frontend (#1964) Bumps [axios](https://github.com/axios/axios) from 1.8.4 to 1.12.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.8.4...v1.12.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump axios from 1.8.4 to 1.12.2 in /frontend (#1975) Bumps [axios](https://github.com/axios/axios) from 1.8.4 to 1.12.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.8.4...v1.12.2) --- updated-dependencies: - dependency-name: axios dependency-version: 1.12.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Disable everything in control panel except for events, images and opening hours (#1967) * Add ControlPanelFeature type * Add ControlPanelFeature const and check if feature is enabled * Add ControlPanelFeatureGate to reroute disabled features to an enabled one * Remove backend urls unrelated to next release * Fix after failed checks * Fix after failed checks * Fix after failed checks * Add FeatureEnabled permission class * Remove tests for disabled features * Remove unused imports * Fix ruff format * Add tests for FeatureEnabled * Add skip for tests for tests not relevant for release * Fix ruff * Fix based on pr comments * Add decorator function for registering disabling features not for first release * Add decorator to admin.py * Fix ruff and biome * Fix test after updating admin routes * Fix isControlPanelFeatureEnabled to not be nullable * Upgrade to Vite 6 (#2001) * Update from vite v4 to v5 * Upgrade to vite 6 * Set module resolution for browser and node to bundler * Sass: migrate from JS API to Modern API (#2002) * Refactor feature gate (#2004) * Rename: ControlPanelFeature -> SiteFeature Also move from config to constants folder * Rename "Gate" alias from router for clarity on what kind of Gate it is * Redirect to home instead of first admin page * biome * Disable Sulten/Lyche frontend pages (#2003) * Disable Sulten/Lyche frontend pages * biome * Fix from merge * Wrap (and disable some) public pages in SiteFeatureGate (#2005) * Disable public recruitment pages * Add site feature check to recruitmentLoader * biome * Disable info pages * don't format site feature gate wrapper with biome * Wrap gangs, events, and saksdokumenter in SiteFeatureGates * Add venues as SiteFeature, wrap venues page with gate * Don't unnecessarily wrap single-route Routes with Outlets * Wrap and disable membership page * ignore formatting * Bump drf-spectacular from 0.28.0 to 0.29.0 in /backend (#2007) Bumps [drf-spectacular](https://github.com/tfranzel/drf-spectacular) from 0.28.0 to 0.29.0. - [Release notes](https://github.com/tfranzel/drf-spectacular/releases) - [Changelog](https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tfranzel/drf-spectacular/compare/0.28.0...0.29.0) --- updated-dependencies: - dependency-name: drf-spectacular dependency-version: 0.29.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump debugpy from 1.8.14 to 1.8.17 in /backend (#2011) Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.14 to 1.8.17. - [Release notes](https://github.com/microsoft/debugpy/releases) - [Commits](https://github.com/microsoft/debugpy/compare/v1.8.14...v1.8.17) --- updated-dependencies: - dependency-name: debugpy dependency-version: 1.8.17 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add SCSS color palette from redesign (#2017) * Add color palette * Add basic variables for text colors * Create NewBadge component (#2018) * Create NewBadge component * biome * Add santa hat to navbar logo during December (#2019) * Delete unused EventCard component (was replaced by ImageCard) (#2020) * Increase stylelint selector-max-type to 1 (#2022) * Create Block composite components (#2021) * Create Block composite components * Fix descending specificity * Decrease intensity of header/footer gradients slightly * Add storybook file * Remove storybook file (our new font isnt loading) * Remove confusing custom ReactNode Children alias (#2023) * Add missing event categories from samf3, and add translations for all categories (#2024) * Event: Make capacity optional (#2025) * Create new EventCard component (#2000) * Begin creating new event card component * Set all anchor tags' text-decoration to none * Use NewBadge. biome * adjust z-index * Add box shadow to event badge * Use placeholder image * Start badge and CTA logic, display location. Tweak styling Also: * Add blue badge theme * Fix and adjust TimeDisplay event formatting * Rename NewEventCard to EventCard * Make Badge inline-flex, add outline-white theme * Remove a tag color specification in themes * Fix merge: add back Block example in ComponentPage * Block: slightly decrease opacity of footer gradient and decrease header * Block: Improve gradients through easing * BlockImage: Use background-image instead of img element * Use Block components in EventCard * Block: Add white theme * Add text shadow to bottom text and show event category * Update location class name * biome * Show translated event category * Simplify fake events creation in ComponentPage * Set letter-spacing for BlockTitle * Update component page * Change "free" badge to green theme * Round edges more, sharpen on hover * Nesten utsolgt -> Få billetter igjen Matches samf3 * Switch out Buy ticket arrow icon * Create EventCardContainer. Allow more customization of EventCard/Block * Fix biome & stylelint * Don't show CTA if event is free * Fixed Tsc error * biome fix * Added theme button to mobile navbar view, added membersbutton to navbar, and updated text for internal login. * removed unused function * removed unnecessary const declaration * removed duplicate const * Fixed routes_samf_tree const * Fixed samf-three-routes const * Reverted translations and reverted to "login" button instead of "intern", fixed breakpionts for navbar desktop to mobile * Clean up navbar styling comments in SCSS Removed commented TODOs regarding navbar styling. * Fix import path for NavbarSamfThree component * Moved samf three navbar to its own folder * biome... * added missing newline --------- Signed-off-by: dependabot[bot] Co-authored-by: Sten <117273647+StenOskar@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lidavic <148764396+Lidavic@users.noreply.github.com> Co-authored-by: Robin <16273164+robines@users.noreply.github.com> Co-authored-by: Eilif Hjermann Lindblad Co-authored-by: Heidi Herfindal Rasmussen <144371674+hei98@users.noreply.github.com> --- .../NavbarSamfThree/Navbar.module.scss | 324 ++++++++++++++++++ .../NavbarSamfThree/Navbar.stories.tsx | 15 + .../src/Components/NavbarSamfThree/Navbar.tsx | 312 +++++++++++++++++ .../HamburgerMenu/HamburgerMenu.module.scss | 48 +++ .../HamburgerMenu/HamburgerMenu.tsx | 27 ++ .../components/HamburgerMenu/index.ts | 1 + .../LanguageButton/LanguageButton.module.scss | 18 + .../LanguageButton/LanguageButton.tsx | 28 ++ .../components/LanguageButton/index.ts | 1 + .../components/NavbarItem/NavbarItem.tsx | 115 +++++++ .../components/NavbarItem/index.ts | 1 + .../NavbarSamfThree/components/index.ts | 3 + .../src/Components/NavbarSamfThree/index.ts | 1 + .../src/Components/SamfOutlet/SamfOutlet.tsx | 5 +- frontend/src/_constants.scss | 12 +- frontend/src/constants/constants.ts | 7 +- frontend/src/i18n/constants.ts | 3 + frontend/src/i18n/translations.ts | 6 + frontend/src/routes/samf-three.ts | 18 +- 19 files changed, 930 insertions(+), 15 deletions(-) create mode 100644 frontend/src/Components/NavbarSamfThree/Navbar.module.scss create mode 100644 frontend/src/Components/NavbarSamfThree/Navbar.stories.tsx create mode 100644 frontend/src/Components/NavbarSamfThree/Navbar.tsx create mode 100644 frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.module.scss create mode 100644 frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.tsx create mode 100644 frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/index.ts create mode 100644 frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.module.scss create mode 100644 frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.tsx create mode 100644 frontend/src/Components/NavbarSamfThree/components/LanguageButton/index.ts create mode 100644 frontend/src/Components/NavbarSamfThree/components/NavbarItem/NavbarItem.tsx create mode 100644 frontend/src/Components/NavbarSamfThree/components/NavbarItem/index.ts create mode 100644 frontend/src/Components/NavbarSamfThree/components/index.ts create mode 100644 frontend/src/Components/NavbarSamfThree/index.ts diff --git a/frontend/src/Components/NavbarSamfThree/Navbar.module.scss b/frontend/src/Components/NavbarSamfThree/Navbar.module.scss new file mode 100644 index 000000000..fbcd81d9c --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/Navbar.module.scss @@ -0,0 +1,324 @@ +/* stylelint-disable function-comma-space-after */ +/* stylelint-disable selector-max-compound-selectors */ +/* stylelint-disable selector-max-class */ +/* TODO: fix later */ + +@use 'src/mixins' as *; + +@use 'src/constants' as *; + +// Font +$navbar-font-family: 'futura-pt', 'Lato', sans-serif; + +// Background +$navbar-bg: white; +$navbar-bg-transparent: linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, transparent); +$navbar-bg-dark: black; +$navbar-bg-dark-transparent: $navbar-bg-transparent; +$navbar-dropdown-bg: rgba(255, 255, 255, 1); +$navbar-dropdown-bg-dark: rgba(0, 0, 0, 1); +$navbar-dropdown-border-radius: 0 0 0.5em 0.5em; + +// Link +$navbar-link-blur-light-bg: rgba(255, 255, 255, 0.4); +$navbar-link-blur-dark-bg: rgba(0, 0, 0, 0.4); +$navbar-link-light-hover-gradient-top: #f4f4f4; +$navbar-dropdown-link-light-hover: #202020; +$navbar-dropdown-link-dark-hover: $red-samf; +$navbar-link-light-hover-gradient-bottom: #ffffff; +$navbar-link-dark-hover-gradient-top: #151515; +$navbar-link-dark-hover-gradient-bottom: #000000; +$navbar-link-transparent-hover-gradient-top: rgba(255, 255, 255, 0.1); +$navbar-link-transparent-hover-gradient-bottom: transparent; + +/* Inner container with padding */ +.navbar_outlet { + padding-top: $navbar-height; +} + +/* Content padding */ +.navbar_padding { + margin-top: $navbar-height; +} + +/* Main navbar */ +#navbar_container { + position: fixed; + top: 0; + width: 100%; + height: $navbar-height; + background: $navbar-bg-dark; + color: $white; + display: flex; + align-items: center; + justify-content: center; + z-index: 12; + white-space: nowrap; + box-shadow: 0 0 25px 5px rgba(0, 0, 0, 0); +} + +.navbar_inner { + flex: 1; + max-width: 1200px; + display: flex; + justify-content: space-between; + align-items: center; + height: 100%; +} + +.navbar_item { + display: flex; + flex: 1; + height: 100%; + position: relative; +} + +.navbar_item_icon { + margin-right: 0.25rem; +} + +.hidden { + display: none; +} + +.navbar_mobile_item { + position: relative; +} + +.mobile_dropdown_container { + width: 100%; + display: flex; + flex-direction: column; +} + +.dropdown_container { + position: absolute; + pointer-events: none; + top: 100%; + min-width: 130%; + z-index: 1; + display: flex; + flex-direction: column; + background-color: $navbar-dropdown-bg-dark; + color: $white; + box-shadow: 0 0 25px 5px rgba(0, 0, 0, 0); + overflow: hidden; + transform: scaleY(0); + transform-origin: 50% 0%; + opacity: 0; +} + +.dropdown_container_left .dropdown_container { + right: 0; +} + +.dropdown_open { + pointer-events: all; + overflow: visible; + transform: scaleY(1); + opacity: 1; +} + +.navbar_dropdown_link, +.navbar_logout_button { + padding: 0.5rem 1rem; + text-decoration: none; + color: $white; + cursor: pointer; + font-size: inherit; + display: flex; + align-items: center; + gap: 0.3rem; + background: none; + border: none; + + @include theme-dark { + color: $white; + } + + &:hover { + background: $navbar-dropdown-link-dark-hover; + text-decoration: none; + } +} + +/* Desktop navigation items */ +.navbar_main_links { + display: flex; + justify-content: space-between; + height: 100%; + margin-right: auto; + font-size: 16px; + font-family: $navbar-font-family; + text-transform: uppercase; +} + +.navbar_main_links_mobile { + display: flex; + flex-direction: column; + align-items: center; +} + +.navbar_link { + color: inherit; + display: flex; + align-items: center; + text-decoration: none; + padding: 0 1rem; + height: 100%; + cursor: pointer; + flex: 1; + text-align: center; + justify-content: center; + z-index: 2; + font-size: inherit; + + &:hover { + text-decoration: none; + } + + @include for-tablet-down { + display: none; + } + + // This ensures navbar dropdown shadow does not lie + // on top of the navbar item + &.selected { + background-color: $navbar-bg-dark; + } + + /* Link hover for regular navbar */ +} + +/* Samf logo */ +.navbar_logo { + display: flex; + text-decoration: none; + margin: 0 1rem; + width: 250px; + cursor: pointer; +} + +#navbar_logo_img { + width: 100%; // QUESTION: Why is this needed? +} + +/* Widgets with language, theme, login */ +.navbar_widgets { + display: flex; + align-items: center; + justify-content: space-evenly; + margin-right: 24px; + margin-left: 24px; + gap: 15px; + height: 100%; + + @include for-tablet-down { + display: none; + } +} + +/* Container for profile */ +.navbar_profile_button { + display: flex; + flex-direction: row; + align-items: center; + white-space: nowrap; + cursor: pointer; + padding-left: 6px; + padding-right: 6px; + background-color: inherit; + gap: 5px; + + @include for-desktop-up { + height: 100%; + } + + .profile_icon { + width: 16px; + height: 16px; + } + + .profile_text { + text-decoration: none; + color: inherit; + } + + &:hover { + color: $red-samf; + } +} + +/* Mobile popup navigation */ +#mobile_popup_container { + position: fixed; + top: $navbar-height; + left: 0; + width: 100vw; + height: calc(100vh - $navbar-height); + min-height: 400px; + background: $navbar-bg-dark; + color: white; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + z-index: 100; + @include for-desktop-up { + display: none; + } +} + +/* A way to make the "Information" text look more centered +offsets the text by 18px, the size of the chevron-icon */ +#mobile_popup_container > .navbar_mobile_item:nth-child(2) { + margin-left: 18px; +} + +/* Navbar link mobile */ +.popup_link_mobile { + color: inherit; + font-size: 1.5em; + margin-bottom: 1em; + align-items: center; + text-decoration: none; + padding: 0 1rem; + cursor: pointer; + display: flex; +} + +.mobile_widgets { + display: flex; + align-items: center; + margin-top: 5em; + gap: 1em; +} + +.mobile_user { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5em; + max-width: 150px; + overflow: visible; + white-space: nowrap; +} + +.chevron.flip { + transform: scaleY(-1); +} + +// When this dropdown menu item is open and navbar is transparent +// we add backdrop filter to the dropdown itself as well +.selected { + background: $navbar-bg-dark; + color: $white; +} + +.active_recruitment { + font-weight: 500; + color: $white; + border-radius: 0.25rem; + padding: 0.5rem 0.75rem; +} diff --git a/frontend/src/Components/NavbarSamfThree/Navbar.stories.tsx b/frontend/src/Components/NavbarSamfThree/Navbar.stories.tsx new file mode 100644 index 000000000..d90c99338 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/Navbar.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Navbar } from './Navbar'; + +const meta: Meta = { + title: 'Components/Navbar', + component: Navbar, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: {}, +}; diff --git a/frontend/src/Components/NavbarSamfThree/Navbar.tsx b/frontend/src/Components/NavbarSamfThree/Navbar.tsx new file mode 100644 index 000000000..563e02d7a --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/Navbar.tsx @@ -0,0 +1,312 @@ +// ****************************************************************************************************************************** +// Replication of Samf3 navbar. Use until Samf4 is fully ready. +// ****************************************************************************************************************************** + +import { Icon } from '@iconify/react'; +import { default as classNames } from 'classnames'; +import { useEffect, useState } from 'react'; +import { useCookies } from 'react-cookie'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router'; +import { Button, Link, ThemeSwitch } from '~/Components'; +import { getActiveRecruitments, logout, stopImpersonatingUser } from '~/api'; +import { logoWhite } from '~/assets'; +import { useAuthContext } from '~/context/AuthContext'; +import { useGlobalContext } from '~/context/GlobalContextProvider'; +import type { RecruitmentDto } from '~/dto'; +import { useDesktop } from '~/hooks'; +import { STATUS } from '~/http_status_codes'; +import { KEY } from '~/i18n/constants'; +import { ROUTES } from '~/routes'; +import { SAMF3_MEMBER_URL } from '~/routes/samf-three'; +import styles from './Navbar.module.scss'; +import { HamburgerMenu, LanguageButton } from './components'; +import { NavbarItem } from './components/NavbarItem'; + +const scrollDistanceForOpaque = 30; + +export function Navbar() { + const { isMobileNavigation, setIsMobileNavigation } = useGlobalContext(); + const { t, i18n } = useTranslation(); + const { user, setUser } = useAuthContext(); + const [activeRecruitments, setActiveRecruitments] = useState(); + const navigate = useNavigate(); + const isDesktop = useDesktop(); + const [cookies, setCookie, removeCookie] = useCookies(); + + // Each NavbarItem can have a dropdown menu. + // We want only one of them to be extended at any time, therefore this parent component + // is given the responsibility of managing this. + // Store the label of the currently selected dropdown. + const [expandedDropdown, setExpandedDropdown] = useState(''); + + // Navbar style. + const isRootPath = useLocation().pathname === ROUTES.frontend.home; + + useEffect(() => { + // Close expanded dropdown menu whenever mobile navbar is closed, or we switch from mobile to desktop, like when + // switching from portrait to landscape on iPad. + if (!isMobileNavigation || isDesktop) { + setExpandedDropdown(''); + } + }, [isMobileNavigation, isDesktop]); + + useEffect(() => { + getActiveRecruitments().then((response) => { + setActiveRecruitments(response.data); + }); + }, []); + + const showActiveRecruitments = activeRecruitments !== undefined && activeRecruitments?.length > 0; + + // Return profile button for navbar if logged in. + const mobileProfileButton = ( +
    + + + {user?.username} + +
    + ); + + const infoLinks = ( + <> + setExpandedDropdown('')} + > + {t(KEY.common_general)} + + setExpandedDropdown('')} + > + {t(KEY.common_membership)} + + setExpandedDropdown('')} + > + {t(KEY.common_opening_hours)} + + setExpandedDropdown('')} + > + {t(KEY.navbar_photos)} + + setExpandedDropdown('')} + > + {t(KEY.common_rent_services)} + + + ); + + const venueLinks = ( + <> + setExpandedDropdown('')} + > + {t(KEY.common_restaurant)} + + setExpandedDropdown('')} + > + {t(KEY.navbar_bar)} + + setExpandedDropdown('')} + > + {t(KEY.navbar_stages)} + + setExpandedDropdown('')} + > + {t(KEY.navbar_club)} + + + ); + + const navbarHeaders = ( +
    + + + +
    + ); + + const recruitmentButton = ( +
    + +
    + ); + + // biome-ignore lint/suspicious/noPrototypeBuiltins: + const isImpersonate = cookies.hasOwnProperty('impersonated_user_id'); + + const userDropdownLinks = ( + <> + + + {t(KEY.control_panel_title)} + + {isImpersonate && ( + + )} + + + ); + + const profileButton = user && ( +
    + +
    + ); + + const loginButton = !user && ( + + ); + + const memberButton = ( + + ); + + // Show mobile popup for navigation. + const mobileNavigation = ( + <> + + + ); + + return ( + <> + + {isMobileNavigation && mobileNavigation} + + ); +} diff --git a/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.module.scss b/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.module.scss new file mode 100644 index 000000000..586bfcf56 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.module.scss @@ -0,0 +1,48 @@ +/* stylelint-disable selector-max-class */ +@use 'src/mixins' as *; + +@use 'src/constants' as *; + +/* Hamburger menu */ +#navbar_hamburger { + display: none; + border: none; + background: none; + + @include for-tablet-down { + height: 69%; + width: 55px; + display: flex; + cursor: pointer; + justify-content: space-evenly; + flex-direction: column; + } +} + +.open { + .navbar_hamburger_line.top { + transform: translateY(12px) rotate(-45deg); + } + + .navbar_hamburger_line.middle { + opacity: 0; + } + + .navbar_hamburger_line.bottom { + transform: translateY(-11px) rotate(45deg); + } +} + +.navbar_hamburger_line { + background-color: $white; + width: 35px; + height: 4px; + border-radius: 10px; + transition: transform 400ms; +} + +.transparent_background { + .navbar_hamburger_line { + background-color: white; + } +} diff --git a/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.tsx b/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.tsx new file mode 100644 index 000000000..1054c38c5 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/HamburgerMenu.tsx @@ -0,0 +1,27 @@ +import classNames from 'classnames'; +import { useGlobalContext } from '~/context/GlobalContextProvider'; +import styles from './HamburgerMenu.module.scss'; + +type HamburgerMenuProps = { + transparentBackground?: boolean; + className?: string; +}; +export function HamburgerMenu({ transparentBackground, className }: HamburgerMenuProps) { + const { isMobileNavigation, setIsMobileNavigation } = useGlobalContext(); + return ( + + ); +} diff --git a/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/index.ts b/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/index.ts new file mode 100644 index 000000000..1b81dc514 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/HamburgerMenu/index.ts @@ -0,0 +1 @@ +export { HamburgerMenu } from './HamburgerMenu'; diff --git a/frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.module.scss b/frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.module.scss new file mode 100644 index 000000000..3fa14fc03 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.module.scss @@ -0,0 +1,18 @@ +.language_flag { + width: 28px; + height: 20px; + cursor: pointer; + border-radius: 3px; + box-shadow: 1px 1px 10px 2px rgba(0, 0, 0, 0.1); + transition: 0.1s; + + &:hover { + transform: scale(1.1); + } +} + +.language_flag_button { + background-color: transparent; + border-color: transparent; +} + diff --git a/frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.tsx b/frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.tsx new file mode 100644 index 000000000..894d9bf18 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/LanguageButton/LanguageButton.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from 'react-i18next'; +import { englishFlag, norwegianFlag } from '~/assets'; +import { LOCALSTORAGE_KEY } from '~/i18n/i18n'; +import { LANGUAGES } from '~/i18n/types'; +import styles from './LanguageButton.module.scss'; + +export function LanguageButton() { + const { i18n } = useTranslation(); + + // Language + const currentLanguage = i18n.language; + const isNorwegian = currentLanguage === LANGUAGES.NB; + const otherLanguage = isNorwegian ? LANGUAGES.EN : LANGUAGES.NB; + const otherFlag = isNorwegian ? englishFlag : norwegianFlag; + + return ( + + ); +} diff --git a/frontend/src/Components/NavbarSamfThree/components/LanguageButton/index.ts b/frontend/src/Components/NavbarSamfThree/components/LanguageButton/index.ts new file mode 100644 index 000000000..aaa8a2aa1 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/LanguageButton/index.ts @@ -0,0 +1 @@ +export { LanguageButton } from './LanguageButton'; diff --git a/frontend/src/Components/NavbarSamfThree/components/NavbarItem/NavbarItem.tsx b/frontend/src/Components/NavbarSamfThree/components/NavbarItem/NavbarItem.tsx new file mode 100644 index 000000000..6f75062c6 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/NavbarItem/NavbarItem.tsx @@ -0,0 +1,115 @@ +import { Icon } from '@iconify/react'; +import classNames from 'classnames'; +import type { ReactNode } from 'react'; +import { Link } from 'react-router'; +import { useGlobalContext } from '~/context/GlobalContextProvider'; +import { useDesktop } from '~/hooks'; +import type { SetState } from '~/types'; +import styles from '../../Navbar.module.scss'; + +type NavbarItemProps = { + route: string; + label: string; + icon?: string; + labelClassName?: string; + dropdownLinks?: ReactNode; + expandedDropdown?: string; + setExpandedDropdown: SetState; +}; + +const iconDown = 'carbon:chevron-down'; + +export function NavbarItem({ + label, + route, + icon, + expandedDropdown, + setExpandedDropdown, + dropdownLinks, + labelClassName, +}: NavbarItemProps) { + const { setIsMobileNavigation } = useGlobalContext(); + const isDesktop = useDesktop(); + const isSelected = expandedDropdown === label; + const otherIsSelected = !isSelected && expandedDropdown !== ''; + + const itemClasses = classNames( + isDesktop ? styles.navbar_item : styles.navbar_mobile_item, + dropdownLinks && styles.navbar_dropdown_item, + otherIsSelected && !isDesktop && styles.hidden, + isSelected && styles.selected, + ); + + const dropdownClasses = classNames({ + [styles.dropdown_container]: isDesktop, + [styles.mobile_dropdown_container]: !isDesktop, + [styles.dropdown_open]: isSelected, + }); + + // Toggle dropdown on click for mobile + function handleOnClick() { + if (!dropdownLinks) { + // Given no dropdownLinks, the user is about to follow a link -> close the mobileNavigation. + setIsMobileNavigation(false); + } else { + // toggle dropdown + setExpandedDropdown(isSelected ? '' : label); + } + } + + // Toggle dropdown on mouse enter for desktop. MouseEnter and mouseLeave is used instead of mouseOver, as mouseOver causes a onHover bug. + function handleMouseEnter() { + if (!dropdownLinks) { + // Given no dropdownLinks, the user is about to follow a link -> close the mobileNavigation. + setIsMobileNavigation(false); + } else { + // show dropdown + setExpandedDropdown(isSelected ? '' : label); + } + } + + // Toggle dropdown on mouse leave for desktop. MouseEnter and mouseLeave is used instead of mouseOver, as mouseOver causes a onHover bug. + function handleMouseLeave() { + if (!dropdownLinks) { + // Given no dropdownLinks, the user is about to follow a link -> close the mobileNavigation. + setIsMobileNavigation(false); + } else { + // clear dropdown + setExpandedDropdown(''); + } + } + + return ( +
    { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleOnClick(); + } + }} + role="button" + tabIndex={0} + > + + {icon && } + {label} + {dropdownLinks && ( + + )} + + {/* On desktop the menu is always in the DOM to enable smooth animation slide in */} + {(isDesktop || isSelected) &&
    {dropdownLinks}
    } +
    + ); +} diff --git a/frontend/src/Components/NavbarSamfThree/components/NavbarItem/index.ts b/frontend/src/Components/NavbarSamfThree/components/NavbarItem/index.ts new file mode 100644 index 000000000..3420fdf2d --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/NavbarItem/index.ts @@ -0,0 +1 @@ +export { NavbarItem } from './NavbarItem'; diff --git a/frontend/src/Components/NavbarSamfThree/components/index.ts b/frontend/src/Components/NavbarSamfThree/components/index.ts new file mode 100644 index 000000000..c3a5888e0 --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/components/index.ts @@ -0,0 +1,3 @@ +export { HamburgerMenu } from './HamburgerMenu'; +export { LanguageButton } from './LanguageButton'; +export { NavbarItem } from './NavbarItem'; diff --git a/frontend/src/Components/NavbarSamfThree/index.ts b/frontend/src/Components/NavbarSamfThree/index.ts new file mode 100644 index 000000000..ec1cfd76d --- /dev/null +++ b/frontend/src/Components/NavbarSamfThree/index.ts @@ -0,0 +1 @@ +export { Navbar } from './Navbar'; diff --git a/frontend/src/Components/SamfOutlet/SamfOutlet.tsx b/frontend/src/Components/SamfOutlet/SamfOutlet.tsx index 8368e257b..e7ee79a4c 100644 --- a/frontend/src/Components/SamfOutlet/SamfOutlet.tsx +++ b/frontend/src/Components/SamfOutlet/SamfOutlet.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react'; import { Outlet } from 'react-router'; -import { Navbar } from '~/Components/Navbar'; +import { Navbar } from '~/Components/NavbarSamfThree/Navbar'; import { Footer } from '../Footer'; import styles from './SamfOutlet.module.scss'; @@ -15,6 +15,9 @@ export function SamfOutlet() { export function SamfLayout({ children }: { children: ReactNode }) { return ( <> + {/* TODO: Uncomment the following line when samf4 navbar is enabled */} + {/* */} + {/* TODO: Remove the following line when samf4 navbar is enabled */}
    {children}
    diff --git a/frontend/src/_constants.scss b/frontend/src/_constants.scss index 97f82cff9..5e7601397 100644 --- a/frontend/src/_constants.scss +++ b/frontend/src/_constants.scss @@ -100,15 +100,21 @@ $special-letter-spacing: -0.05rem; /* Screen sizes, breakpoint (bp) */ $large-desktop-bp-lower: 1201px; $desktop-bp-upper: 1200px; -$desktop-bp-lower: 993px; -$tablet-bp-upper: 992px; +// TODO: old breakpióints commented below, dont remove, fix breakpoints later (temparery navbar fix) +// $desktop-bp-lower: 993px; +// $tablet-bp-upper: 992px; +$desktop-bp-lower: 1200px; +$tablet-bp-upper: 1199px; $tablet-bp-lower: 769px; $mobile-bp-upper: 768px; $content-max-size: 1200px; $content-padding: calc((100vw - 1200px) / 2); // Navbar size -$navbar-height: 60px; +// TODO: Uncomment the following line when samf4 navbar is enabled +//$navbar-height: 60px; +// TODO: Remove the following line when samf4 navbar is enabled +$navbar-height: 64px; $sulten-navbar-height: 100px; $isfit-navbar-height: 80px; diff --git a/frontend/src/constants/constants.ts b/frontend/src/constants/constants.ts index 0f37fc54f..2f5e51778 100644 --- a/frontend/src/constants/constants.ts +++ b/frontend/src/constants/constants.ts @@ -44,8 +44,11 @@ export const LOCAL_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/; */ export const largeDesktopBpLower = 1201; export const desktopBpUpper = 1200; -export const desktopBpLower = 993; -export const tabletBpUpper = 992; +// TODO: old breakpióints commented below, dont remove, fix breakpoints later (temparery navbar fix) +// export const desktopBpLower = 993; +// export const tabletBpUpper = 992; +export const desktopBpLower = 1200; +export const tabletBpUpper = 1199; export const tabletBpLower = 769; export const mobileBpUpper = 768; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index f2982c2a0..7a3eb8f3d 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -342,6 +342,9 @@ export const KEY = { navbar_map: 'navbar_map', navbar_photos: 'navbar_photos', navbar_nybygg: 'navbar_nybygg', + navbar_bar: 'navbar_bar', + navbar_stages: 'navbar_stages', + navbar_club: 'navbar_club', footer_developed_by: 'footer_developed_by', footer_have_questions: 'footer_have_questions', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 05a98dafb..2911a1986 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -326,6 +326,9 @@ export const nb = prepareTranslations({ [KEY.navbar_map]: 'Kart og lokaler', [KEY.navbar_photos]: 'Foto', [KEY.navbar_nybygg]: 'Nybygg', + [KEY.navbar_bar]: 'Bar', + [KEY.navbar_stages]: 'Scener', + [KEY.navbar_club]: 'Klubb', // Common navigation - links for samf 3 (used in navbar and footer): @@ -990,6 +993,9 @@ export const en = prepareTranslations({ [KEY.navbar_photos]: 'Photos', [KEY.navbar_nybygg]: 'New building', [KEY.navbar_map]: 'Map and venues', + [KEY.navbar_bar]: 'Bar', + [KEY.navbar_stages]: 'Stages', + [KEY.navbar_club]: 'Club', // Common navigation (used in navbar and footer): diff --git a/frontend/src/routes/samf-three.ts b/frontend/src/routes/samf-three.ts index 5681c1fa9..3ac7a7066 100644 --- a/frontend/src/routes/samf-three.ts +++ b/frontend/src/routes/samf-three.ts @@ -13,15 +13,6 @@ export const CASE_DOCUMENTS = { saksdokumenter: `${BASE_URL}/saksdokumenter`, }; -// Samfundet3 innlogging -export const SAMF3_LOGIN_URL = { - login: `${BASE_URL}/logg-inn`, -}; - -export const SAMF3_MEMBER_URL = { - medlem: 'https://medlem.samfundet.no/', -}; - export const ROUTES_SAMF_THREE = { information: { general: `${BASE_URL}/informasjon`, @@ -39,3 +30,12 @@ export const ROUTES_SAMF_THREE = { }, volunteer: `${BASE_URL}/opptak`, } as const; + +// Samfundet3 innlogging +export const SAMF3_LOGIN_URL = { + login: `${BASE_URL}/logg-inn`, +}; + +export const SAMF3_MEMBER_URL = { + medlem: 'https://medlem.samfundet.no/', +}; From f83438da4ba22a8cd574623497c97bead66b6166 Mon Sep 17 00:00:00 2001 From: Erik Hoff <142814286+aTrueYety@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:36:23 +0100 Subject: [PATCH 31/79] Add translatrions to LoginPicker (#2095) Co-authored-by: Erik --- .../Pages/LoginPickerPage/LoginPickerPage.tsx | 19 +++++++++++-------- frontend/src/i18n/constants.ts | 9 +++++++++ frontend/src/i18n/translations.ts | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx b/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx index 3fbb1ff10..0022a34bc 100644 --- a/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx +++ b/frontend/src/Pages/LoginPickerPage/LoginPickerPage.tsx @@ -1,7 +1,9 @@ import { Icon } from '@iconify/react'; import type { FC } from 'react'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Page } from '~/Components'; +import { KEY } from '~/i18n/constants'; import { SAMF3_LOGIN_URL } from '~/routes/samf-three'; import styles from './LoginPickerPage.module.scss'; @@ -15,32 +17,33 @@ type Props = { newRoute: string }; */ export const LoginPickerPage: FC = ({ newRoute }) => { const navigate = useNavigate(); + const { t } = useTranslation(); return (
    - Innlogging for interne -

    Hvordan vil du logge inn?

    + {t(KEY.loginpicker_page_caption)} +

    {t(KEY.loginpicker_page_title)}

    -
    diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 81874762a..6bf601429 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -80,7 +80,7 @@ export const KEY = { common_date: 'common_date', common_show: 'common_show', common_menu: 'common_menu', - common_table: 'common_table', + common_table: 'common_table', // this one gives "bord" in Norwegian, not "tabell" common_sheet: 'common_sheet', common_max: 'common_max', common_edit: 'common_edit', From a749646a1f096eab02498e6d1f992d575d0f9a73 Mon Sep 17 00:00:00 2001 From: BragonSB Date: Tue, 17 Mar 2026 20:58:24 +0100 Subject: [PATCH 37/79] Brage/web 92 create connect to mdb form (#2068) * feat: add MDB-connect-form route and admin-page * misc: add comments documenting my confusion * fix: Biome and frontend-route * feat: add MDB connection labels to admin page translations * feat: add mdb_connection icon in AdminLayout * fix: removed PermissionRoute from mdb route in router * feat: create MDB connection form * fix: biome biome biome biome biome * style: move formprops to the left * style: remove unused styling * feat: connect to mdb backend * feat: change from SamfForm to React Hook Form * style: remove hardcoded styling * fix: you can never have enough biome commits * fix: made MDBConnectForm component self closing * fix: made form_button styling class * feat: add title * fix: biome * feat: add better error message when inputing invalid state * feat: add new schema for email and membership number and both * feat: add common error message * misc: change norwegian translation * misc: change username to member_login * fix: better translation on password error and turn off autocomplete on password field * fix: add space * fix: change membership number to membership ID, change api function to camelCase, be consistent with translations --- .../PagesAdmin/AdminLayout/AdminLayout.tsx | 5 ++ .../MDBConnectForm.tsx | 86 +++++++++++++++++++ .../MDBConnectFormAdminPage.module.scss | 16 ++++ .../MDBConnectFormAdminPage.tsx | 17 ++++ .../MDBConnectFormAdminPage/index.ts | 2 + frontend/src/PagesAdmin/index.ts | 1 + frontend/src/api.ts | 13 +++ frontend/src/constants/constants.ts | 2 + frontend/src/i18n/constants.ts | 8 ++ frontend/src/i18n/translations.ts | 22 +++++ frontend/src/router/router.tsx | 7 ++ frontend/src/routes/frontend.ts | 1 + frontend/src/schema/user.ts | 32 ++++++- 13 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectForm.tsx create mode 100644 frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.module.scss create mode 100644 frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.tsx create mode 100644 frontend/src/PagesAdmin/MDBConnectFormAdminPage/index.ts diff --git a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx index 7b2f60f99..487d682de 100644 --- a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx +++ b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx @@ -146,6 +146,11 @@ export function AdminLayout() { {t(KEY.admin_stop_impersonate)} )} + {/** Connect to MDB button */} + + + {t(KEY.adminpage_connect_mdb)} + {/* Logout */} + + + ); +} diff --git a/frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.module.scss b/frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.module.scss new file mode 100644 index 000000000..3bf349cf0 --- /dev/null +++ b/frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.module.scss @@ -0,0 +1,16 @@ +@use 'src/global.scss' as *; + +@use 'src/mixins' as *; + +@use 'src/constants' as *; + +.wrapper { + margin: 0 20vw 0 20vw; + display: flex; + flex-flow: row nowrap; +} + +.form_button { + max-width: 15vw; + margin: 5vh auto 0 auto; +} \ No newline at end of file diff --git a/frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.tsx b/frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.tsx new file mode 100644 index 000000000..f887c8e95 --- /dev/null +++ b/frontend/src/PagesAdmin/MDBConnectFormAdminPage/MDBConnectFormAdminPage.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from 'react-i18next'; +import { KEY } from '~/i18n/constants'; +import { AdminPage } from '../AdminPageLayout'; +import { MDBConnectForm } from './MDBConnectForm'; +import styles from './MDBConnectFormAdminPage.module.scss'; + +export function MDBConnectFormAdminPage() { + const { t } = useTranslation(); + + return ( + +
    + +
    +
    + ); +} diff --git a/frontend/src/PagesAdmin/MDBConnectFormAdminPage/index.ts b/frontend/src/PagesAdmin/MDBConnectFormAdminPage/index.ts new file mode 100644 index 000000000..16e810f9d --- /dev/null +++ b/frontend/src/PagesAdmin/MDBConnectFormAdminPage/index.ts @@ -0,0 +1,2 @@ +export { MDBConnectFormAdminPage } from './MDBConnectFormAdminPage'; +export { MDBConnectForm } from './MDBConnectForm'; diff --git a/frontend/src/PagesAdmin/index.ts b/frontend/src/PagesAdmin/index.ts index 3d4b2bc6c..c879d2c65 100644 --- a/frontend/src/PagesAdmin/index.ts +++ b/frontend/src/PagesAdmin/index.ts @@ -38,3 +38,4 @@ export { SultenMenuAdminPage } from './SultenMenuAdminPage'; export { SultenMenuItemFormAdminPage } from './SultenMenuItemFormAdminPage'; export { SultenReservationAdminPage } from './SultenReservationAdminPage'; export { UsersAdminPage } from './UsersAdminPage'; +export { MDBConnectFormAdminPage } from './MDBConnectFormAdminPage'; diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 7215af6e6..d6afdf1c1 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1341,3 +1341,16 @@ export async function getPositionsByTag( const response = await axios.get(url, { withCredentials: true }); return response.data; } + +export async function connectToMdb( + member_login: string, //email or member_id + password: string, +) { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__mdb_connect, + }); + const response = await axios.post(url, { member_login, password }, { withCredentials: true }); + return response.data; +} diff --git a/frontend/src/constants/constants.ts b/frontend/src/constants/constants.ts index 2f5e51778..88dafe31b 100644 --- a/frontend/src/constants/constants.ts +++ b/frontend/src/constants/constants.ts @@ -87,6 +87,8 @@ export const USERNAME_LENGTH_MIN = 2; export const USERNAME_LENGTH_MAX = 32; export const PASSWORD_LENGTH_MIN = 8; export const PASSWORD_LENGTH_MAX = 2048; +export const MEMBERSHIP_ID_LENGTH_MIN = 1; +export const MEMBERSHIP_ID_LENGTH_MAX = 30; // see pagination.py CustomPageNumberPagination export const PAGE_SIZE = 25; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 6bf601429..6dafd429d 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -57,6 +57,7 @@ export const KEY = { pick_a_date: 'pick_a_date', // No category: + common_connect: 'common_connect', common_content: 'common_content', common_url: 'common_url', common_manage: 'common_manage', @@ -290,6 +291,7 @@ export const KEY = { // Purchase Ticket Info: invalid_email_message: 'invalid_email_message', email_or_membership_number_message: 'email_or_membership_number_message', + email_or_membership_number: 'email_or_membership_number', no_tickets_selected_message: 'no_tickets_selected_message', kr_per_ticket: 'kr_per_ticket', enter_membership_number: 'enter_membership_number', @@ -589,6 +591,12 @@ export const KEY = { admin_role_page_orggangsection: 'admin_role_page_orggangsection', admin_role_page_role_since: 'admin_role_page_role_since', admin_role_page_given_by: 'admin_role_page_given_by', + adminpage_connect_mdb: 'adminpage_connect_mdb', + adminpage_connect_mdb_extended: 'adminpage_connect_mdb_extended', + adminpage_connect_mdb_succesful_toast: 'adminpage_connect_mdb_succesful_toast', + adminpage_connect_mdb_invalid_email: 'adminpage_connect_mdb_invalid_email', + adminpage_connect_mdb_invalid_membership_id: 'adminpage_connect_mdb_invalid_membership_id', + adminpage_connect_mdb_common_error: 'adminpage_connect_mdb_common_error', // CommandMenu: command_menu_label: 'command_menu_label', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index c7e52c512..33d1ce93c 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -43,6 +43,7 @@ export const nb = prepareTranslations({ [KEY.pick_a_date]: 'Velg en dato', // Other common + [KEY.common_connect]: 'Koble til', [KEY.common_content]: 'Innhold', [KEY.common_url]: 'URL', [KEY.common_manage]: 'Håndter', @@ -303,6 +304,7 @@ export const nb = prepareTranslations({ //Purchase Ticket Info: [KEY.invalid_email_message]: 'Ugyldig e-postformat', [KEY.email_or_membership_number_message]: 'Du må oppgi enten en e-post eller et medlemsnummer', + [KEY.email_or_membership_number]: 'e-post eller medlemsnummer', [KEY.no_tickets_selected_message]: 'Du må velge minst én billett', [KEY.kr_per_ticket]: 'kr per billett', [KEY.enter_membership_number]: 'Skriv inn medlemsnummer', @@ -324,6 +326,15 @@ export const nb = prepareTranslations({ [KEY.adminpage_gangs_title]: 'Administrer gjenger', [KEY.adminpage_gangs_create]: 'Opprett gjeng', + //MDB Connect AdminPage + [KEY.adminpage_connect_mdb]: 'Koble til MDB', + [KEY.adminpage_connect_mdb_extended]: 'Koble til medlemsdatabasen', + [KEY.adminpage_connect_mdb_succesful_toast]: 'Vellyket tilkobling til medlemsdatabasen', + [KEY.adminpage_connect_mdb_invalid_email]: 'ugyldig e-post', + [KEY.adminpage_connect_mdb_invalid_membership_id]: 'ugyldig medlemsdatabasenummer', + [KEY.adminpage_connect_mdb_common_error]: + 'Kunne ikke koble til medlemsdatabasen. Vennligst sjekk at alle felter er skrevet riktig', + // SaksdokumentPage: [KEY.saksdokumentpage_publication_date]: 'Publiseringsdato', @@ -721,6 +732,7 @@ export const en = prepareTranslations({ [KEY.pick_a_date]: 'Pick a date', // No category: + [KEY.common_connect]: 'Connect', [KEY.common_content]: 'Content', [KEY.common_url]: 'URL', [KEY.common_manage]: 'Manage', @@ -975,6 +987,7 @@ export const en = prepareTranslations({ //Purchase Ticket Info: [KEY.invalid_email_message]: 'Invalid email format', [KEY.email_or_membership_number_message]: 'You must provide either an email or a membership number', + [KEY.email_or_membership_number]: 'email or membership number', [KEY.no_tickets_selected_message]: 'You must select at least one ticket', [KEY.kr_per_ticket]: 'kr per ticket', [KEY.enter_membership_number]: 'Enter your membership number', @@ -1317,6 +1330,15 @@ export const en = prepareTranslations({ [KEY.feedback_your_feedback]: 'Your feedback', [KEY.feedback_thank_you_for_feedback]: 'Thank you for your feedback', + //MDB Connect AdminPage + [KEY.adminpage_connect_mdb]: 'Connect to MDB', + [KEY.adminpage_connect_mdb_extended]: 'Connect to the membership database', + [KEY.adminpage_connect_mdb_succesful_toast]: 'Succesfully connected to the membership database', + [KEY.adminpage_connect_mdb_invalid_email]: 'Invalid email', + [KEY.adminpage_connect_mdb_invalid_membership_id]: 'Invalid membership ID', + [KEY.adminpage_connect_mdb_common_error]: + "Couldn't connect to the membership database. Please check that all fields are correct", + // No category: [KEY.owner]: 'Owner', [KEY.end_time]: 'End time', diff --git a/frontend/src/router/router.tsx b/frontend/src/router/router.tsx index a0b03b4b0..f904b420e 100644 --- a/frontend/src/router/router.tsx +++ b/frontend/src/router/router.tsx @@ -53,6 +53,7 @@ import { ImageFormAdminPage, InformationAdminPage, InformationFormAdminPage, + MDBConnectFormAdminPage, OpeningHoursAdminPage, RecruitmentAdminPage, RecruitmentApplicantAdminPage, @@ -1010,6 +1011,12 @@ export const router = createBrowserRouter( /> } /> + {/* MDB Connect Form */} + {t(KEY.adminpage_connect_mdb)} }} + element={} + /> {/* diff --git a/frontend/src/routes/frontend.ts b/frontend/src/routes/frontend.ts index 630721f66..1b6068e98 100644 --- a/frontend/src/routes/frontend.ts +++ b/frontend/src/routes/frontend.ts @@ -47,6 +47,7 @@ export const ROUTES_FRONTEND = { // Admin pages // // ==================== // admin: '/control-panel/', + admin_mdb_connect_form: '/control-panel/mdb-connect-form', // Users admin_users: '/control-panel/users/', // Roles diff --git a/frontend/src/schema/user.ts b/frontend/src/schema/user.ts index dd4163e5e..c523e0244 100644 --- a/frontend/src/schema/user.ts +++ b/frontend/src/schema/user.ts @@ -1,6 +1,36 @@ +import type { TFunction } from 'i18next'; import { z } from 'zod'; -import { PASSWORD_LENGTH_MAX, PASSWORD_LENGTH_MIN, USERNAME_LENGTH_MAX, USERNAME_LENGTH_MIN } from '~/constants'; +import { + MEMBERSHIP_ID_LENGTH_MAX, + MEMBERSHIP_ID_LENGTH_MIN, + PASSWORD_LENGTH_MAX, + PASSWORD_LENGTH_MIN, + USERNAME_LENGTH_MAX, + USERNAME_LENGTH_MIN, +} from '~/constants'; +import { KEY } from '~/i18n/constants'; +import { lowerCapitalize } from '~/utils'; export const USERNAME = z.string().min(USERNAME_LENGTH_MIN).max(USERNAME_LENGTH_MAX); export const PASSWORD = z.string().min(PASSWORD_LENGTH_MIN).max(PASSWORD_LENGTH_MAX); + +export const EMAIL = z.string().email(); + +export const MEMBERSHIPNUMBER = z.string().min(MEMBERSHIP_ID_LENGTH_MIN).max(MEMBERSHIP_ID_LENGTH_MAX).regex(/^\d+$/); + +export const EMAIL_OR_MEMBERSHIP_NUMBER = (t: TFunction) => + z.string().superRefine((value, ctx) => { + const membershipNumberResult = MEMBERSHIPNUMBER.safeParse(value).success; + const emailResult = EMAIL.safeParse(value).success; + + if (!emailResult && !membershipNumberResult) { + const onlyNumbers = /^\d+$/.test(value); + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: onlyNumbers + ? lowerCapitalize(t(KEY.adminpage_connect_mdb_invalid_membership_id)) + : lowerCapitalize(t(KEY.adminpage_connect_mdb_invalid_email)), + }); + } + }); From 8128f381b8251484b503567e8a6e10b8755aaf92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:44:00 +0100 Subject: [PATCH 38/79] Bump gunicorn from 23.0.0 to 25.1.0 in /backend (#2087) Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 23.0.0 to 25.1.0. - [Release notes](https://github.com/benoitc/gunicorn/releases) - [Commits](https://github.com/benoitc/gunicorn/compare/23.0.0...25.1.0) --- updated-dependencies: - dependency-name: gunicorn dependency-version: 25.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 19 ++++++++++--------- backend/pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 75d7a954f..69fbc7915 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -398,25 +398,26 @@ python-dateutil = ">=2.7" [[package]] name = "gunicorn" -version = "23.0.0" +version = "25.1.0" description = "WSGI HTTP Server for UNIX" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, - {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, + {file = "gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b"}, + {file = "gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616"}, ] [package.dependencies] packaging = "*" [package.extras] -eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] -gevent = ["gevent (>=1.4.0)"] +eventlet = ["eventlet (>=0.40.3)"] +gevent = ["gevent (>=24.10.1)"] +http2 = ["h2 (>=4.1.0)"] setproctitle = ["setproctitle"] -testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] -tornado = ["tornado (>=0.2)"] +testing = ["coverage", "eventlet (>=0.40.3)", "gevent (>=24.10.1)", "h2 (>=4.1.0)", "httpx[http2]", "pytest", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"] +tornado = ["tornado (>=6.5.0)"] [[package]] name = "idna" @@ -1393,4 +1394,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" python-versions = "3.11.2" -content-hash = "e4f3f45d0679a22c5fd8a9bbcebb6d2f889cafd2f604f7171a13112566d7aa55" +content-hash = "69c12ee5ce9c929e3d8b609fcaa2f352c5f592bc65b157ccddfb1e1e53735383" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 299c874ea..93d67b7e0 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -151,7 +151,7 @@ django-cors-headers = "4.*" dataclasses-json = "0.*" django-guardian = "2.*" pillow = "12.*" -gunicorn = "23.*" +gunicorn = "25.*" django-admin-autocomplete-filter = "0.*" psycopg = { extras = ["c"], version = "*" } From 0b72a1c9db6c826479a6f67511f626adf68d5e97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:44:21 +0100 Subject: [PATCH 39/79] Bump psycopg from 3.3.2 to 3.3.3 in /backend (#2088) Bumps [psycopg](https://github.com/psycopg/psycopg) from 3.3.2 to 3.3.3. - [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst) - [Commits](https://github.com/psycopg/psycopg/compare/3.3.2...3.3.3) --- updated-dependencies: - dependency-name: psycopg dependency-version: 3.3.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 69fbc7915..6883eeb3c 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -878,39 +878,39 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "psycopg" -version = "3.3.2" +version = "3.3.3" description = "PostgreSQL database adapter for Python" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b"}, - {file = "psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7"}, + {file = "psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698"}, + {file = "psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9"}, ] [package.dependencies] -psycopg-c = {version = "3.3.2", optional = true, markers = "implementation_name != \"pypy\" and extra == \"c\""} +psycopg-c = {version = "3.3.3", optional = true, markers = "implementation_name != \"pypy\" and extra == \"c\""} typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] -binary = ["psycopg-binary (==3.3.2) ; implementation_name != \"pypy\""] -c = ["psycopg-c (==3.3.2) ; implementation_name != \"pypy\""] -dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "cython-lint (>=0.16)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.19.0)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "types-shapely (>=2.0)", "wheel (>=0.37)"] +binary = ["psycopg-binary (==3.3.3) ; implementation_name != \"pypy\""] +c = ["psycopg-c (==3.3.3) ; implementation_name != \"pypy\""] +dev = ["ast-comments (>=1.1.2)", "black (>=26.1.0)", "codespell (>=2.2)", "cython-lint (>=0.16)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.19.0)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "types-shapely (>=2.0)", "wheel (>=0.37)"] docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] pool = ["psycopg-pool"] test = ["anyio (>=4.0)", "mypy (>=1.19.0) ; implementation_name != \"pypy\"", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] [[package]] name = "psycopg-c" -version = "3.3.2" +version = "3.3.3" description = "PostgreSQL database adapter for Python -- C optimisation distribution" optional = false python-versions = ">=3.10" groups = ["main"] markers = "implementation_name != \"pypy\"" files = [ - {file = "psycopg_c-3.3.2.tar.gz", hash = "sha256:a65927731d394cc77bbf85d02d0311d7843616a4a627f3e816e94ad3a052ef83"}, + {file = "psycopg_c-3.3.3.tar.gz", hash = "sha256:86ef6f4424348247828e83fb0882c9f8acb33e64d0a5ce66c1b4a5107ee73edd"}, ] [[package]] From 8d1ccde385f6fd78ce01695974e972d329b17a9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:45:07 +0100 Subject: [PATCH 40/79] Bump bandit from 1.9.3 to 1.9.4 in /backend (#2089) Bumps [bandit](https://github.com/PyCQA/bandit) from 1.9.3 to 1.9.4. - [Release notes](https://github.com/PyCQA/bandit/releases) - [Commits](https://github.com/PyCQA/bandit/compare/1.9.3...1.9.4) --- updated-dependencies: - dependency-name: bandit dependency-version: 1.9.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 6883eeb3c..fcc2d84cb 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -37,14 +37,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a [[package]] name = "bandit" -version = "1.9.3" +version = "1.9.4" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "bandit-1.9.3-py3-none-any.whl", hash = "sha256:4745917c88d2246def79748bde5e08b9d5e9b92f877863d43fab70cd8814ce6a"}, - {file = "bandit-1.9.3.tar.gz", hash = "sha256:ade4b9b7786f89ef6fc7344a52b34558caec5da74cb90373aed01de88472f774"}, + {file = "bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e"}, + {file = "bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628"}, ] [package.dependencies] From ed257d8158c14c7428f59ecbda2b4f4c872c7382 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:45:40 +0100 Subject: [PATCH 41/79] Bump rollup from 4.53.2 to 4.59.0 in /frontend (#2086) Bumps [rollup](https://github.com/rollup/rollup) from 4.53.2 to 4.59.0. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.53.2...v4.59.0) --- updated-dependencies: - dependency-name: rollup dependency-version: 4.59.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 214 ++++++++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 92 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6ae297a80..132a35509 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1457,156 +1457,177 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.2" +"@rollup/rollup-android-arm-eabi@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-android-arm64@npm:4.53.2" +"@rollup/rollup-android-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm64@npm:4.59.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-darwin-arm64@npm:4.53.2" +"@rollup/rollup-darwin-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-darwin-x64@npm:4.53.2" +"@rollup/rollup-darwin-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.53.2" +"@rollup/rollup-freebsd-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-freebsd-x64@npm:4.53.2" +"@rollup/rollup-freebsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.53.2" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.53.2" +"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.53.2" +"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.53.2" +"@rollup/rollup-linux-arm64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loong64-gnu@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.53.2" +"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.53.2" +"@rollup/rollup-linux-loong64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.53.2" +"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.53.2" +"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.53.2" +"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.53.2" +"@rollup/rollup-linux-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.53.2" +"@rollup/rollup-linux-x64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-openharmony-arm64@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-openharmony-arm64@npm:4.53.2" +"@rollup/rollup-openbsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.53.2" +"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.53.2" +"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-gnu@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-win32-x64-gnu@npm:4.53.2" +"@rollup/rollup-win32-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.53.2": - version: 4.53.2 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.53.2" +"@rollup/rollup-win32-x64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -7527,31 +7548,34 @@ __metadata: linkType: hard "rollup@npm:^4.34.9": - version: 4.53.2 - resolution: "rollup@npm:4.53.2" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.53.2" - "@rollup/rollup-android-arm64": "npm:4.53.2" - "@rollup/rollup-darwin-arm64": "npm:4.53.2" - "@rollup/rollup-darwin-x64": "npm:4.53.2" - "@rollup/rollup-freebsd-arm64": "npm:4.53.2" - "@rollup/rollup-freebsd-x64": "npm:4.53.2" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.53.2" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.53.2" - "@rollup/rollup-linux-arm64-gnu": "npm:4.53.2" - "@rollup/rollup-linux-arm64-musl": "npm:4.53.2" - "@rollup/rollup-linux-loong64-gnu": "npm:4.53.2" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.53.2" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.53.2" - "@rollup/rollup-linux-riscv64-musl": "npm:4.53.2" - "@rollup/rollup-linux-s390x-gnu": "npm:4.53.2" - "@rollup/rollup-linux-x64-gnu": "npm:4.53.2" - "@rollup/rollup-linux-x64-musl": "npm:4.53.2" - "@rollup/rollup-openharmony-arm64": "npm:4.53.2" - "@rollup/rollup-win32-arm64-msvc": "npm:4.53.2" - "@rollup/rollup-win32-ia32-msvc": "npm:4.53.2" - "@rollup/rollup-win32-x64-gnu": "npm:4.53.2" - "@rollup/rollup-win32-x64-msvc": "npm:4.53.2" + version: 4.59.0 + resolution: "rollup@npm:4.59.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.59.0" + "@rollup/rollup-android-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-x64": "npm:4.59.0" + "@rollup/rollup-freebsd-arm64": "npm:4.59.0" + "@rollup/rollup-freebsd-x64": "npm:4.59.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" + "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-musl": "npm:4.59.0" + "@rollup/rollup-openbsd-x64": "npm:4.59.0" + "@rollup/rollup-openharmony-arm64": "npm:4.59.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" + "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -7577,8 +7601,12 @@ __metadata: optional: true "@rollup/rollup-linux-loong64-gnu": optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true "@rollup/rollup-linux-riscv64-musl": @@ -7589,6 +7617,8 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openbsd-x64": + optional: true "@rollup/rollup-openharmony-arm64": optional: true "@rollup/rollup-win32-arm64-msvc": @@ -7603,7 +7633,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/427216da71c1ce7fefb0bef75f94c301afd858ac27e35898e098c2da5977325fa54c2edda867caf9675c8abfa8d8d94efa99c482fa04f5cd91f3a740112d4f4f + checksum: 10c0/f38742da34cfee5e899302615fa157aa77cb6a2a1495e5e3ce4cc9c540d3262e235bbe60caa31562bbfe492b01fdb3e7a8c43c39d842d3293bcf843123b766fc languageName: node linkType: hard From 48644e876444697ba0ba5387836dd5b213e0ebc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:49:17 +0100 Subject: [PATCH 42/79] Bump immutable from 5.0.3 to 5.1.5 in /frontend (#2098) Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.0.3 to 5.1.5. - [Release notes](https://github.com/immutable-js/immutable-js/releases) - [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/immutable-js/immutable-js/compare/v5.0.3...v5.1.5) --- updated-dependencies: - dependency-name: immutable dependency-version: 5.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 132a35509..8864dfb1c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5132,9 +5132,9 @@ __metadata: linkType: hard "immutable@npm:^5.0.2": - version: 5.0.3 - resolution: "immutable@npm:5.0.3" - checksum: 10c0/3269827789e1026cd25c2ea97f0b2c19be852ffd49eda1b674b20178f73d84fa8d945ad6f5ac5bc4545c2b4170af9f6e1f77129bc1cae7974a4bf9b04a9cdfb9 + version: 5.1.5 + resolution: "immutable@npm:5.1.5" + checksum: 10c0/8017ece1578e3c5939ba3305176aee059def1b8a90c7fa2a347ef583ebbd38cbe77ce1bbd786a5fab57e2da00bbcb0493b92e4332cdc4e1fe5cfb09a4688df31 languageName: node linkType: hard From 9069d6657d7b0690a32a39858daf78cce9a6d0dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:49:59 +0100 Subject: [PATCH 43/79] Bump tar from 7.5.7 to 7.5.11 in /frontend (#2109) Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.11. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.11) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.11 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8864dfb1c..e14b13ca1 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -8353,15 +8353,15 @@ __metadata: linkType: hard "tar@npm:^7.4.3": - version: 7.5.7 - resolution: "tar@npm:7.5.7" + version: 7.5.11 + resolution: "tar@npm:7.5.11" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/51f261afc437e1112c3e7919478d6176ea83f7f7727864d8c2cce10f0b03a631d1911644a567348c3063c45abdae39718ba97abb073d22aa3538b9a53ae1e31c + checksum: 10c0/b6bb420550ef50ef23356018155e956cd83282c97b6128d8d5cfe5740c57582d806a244b2ef0bf686a74ce526babe8b8b9061527623e935e850008d86d838929 languageName: node linkType: hard From ad8b7de1637d25840617f1b3fdcc894e4ff062c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:09:03 +0100 Subject: [PATCH 44/79] Bump django from 5.2.11 to 5.2.12 in /backend (#2097) Bumps [django](https://github.com/django/django) from 5.2.11 to 5.2.12. - [Commits](https://github.com/django/django/compare/5.2.11...5.2.12) --- updated-dependencies: - dependency-name: django dependency-version: 5.2.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index fcc2d84cb..ce4244692 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -245,14 +245,14 @@ files = [ [[package]] name = "django" -version = "5.2.11" +version = "5.2.12" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0"}, - {file = "django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3"}, + {file = "django-5.2.12-py3-none-any.whl", hash = "sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7"}, + {file = "django-5.2.12.tar.gz", hash = "sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb"}, ] [package.dependencies] From 7f40bfb1d47dc13952afdfa2b308e3fa85dd3871 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:09:22 +0100 Subject: [PATCH 45/79] Bump django-environ from 0.12.0 to 0.13.0 in /backend (#2090) Bumps [django-environ](https://github.com/joke2k/django-environ) from 0.12.0 to 0.13.0. - [Release notes](https://github.com/joke2k/django-environ/releases) - [Changelog](https://github.com/joke2k/django-environ/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/joke2k/django-environ/compare/v0.12.0...v0.13.0) --- updated-dependencies: - dependency-name: django-environ dependency-version: 0.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index ce4244692..fbe678568 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -297,19 +297,19 @@ django = ">=4.2" [[package]] name = "django-environ" -version = "0.12.0" +version = "0.13.0" description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." optional = false python-versions = "<4,>=3.9" groups = ["main"] files = [ - {file = "django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca"}, - {file = "django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a"}, + {file = "django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e"}, + {file = "django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f"}, ] [package.extras] -develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)", "sphinx (>=5.0)", "sphinx-notfound-page"] -docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-notfound-page"] +develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"] +docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"] [[package]] From 06c6a1146b241e10df9ad7d2b26ef063777f515c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:09:37 +0100 Subject: [PATCH 46/79] Bump storybook from 8.6.15 to 8.6.17 in /frontend (#2083) Bumps [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) from 8.6.15 to 8.6.17. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/v8.6.17/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v8.6.17/code/core) --- updated-dependencies: - dependency-name: storybook dependency-version: 8.6.17 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 326827e1e..00e9330ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -98,7 +98,7 @@ "prop-types": "^15.8.1", "react-i18next": "^13.3.0", "sass": "^1.94.2", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "stylelint": "^15.10.3", "stylelint-config-sass-guidelines": "^10.0.0", "stylelint-config-standard": "^34.0.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e14b13ca1..cbf933da9 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1881,11 +1881,11 @@ __metadata: languageName: node linkType: hard -"@storybook/core@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/core@npm:8.6.15" +"@storybook/core@npm:8.6.18": + version: 8.6.18 + resolution: "@storybook/core@npm:8.6.18" dependencies: - "@storybook/theming": "npm:8.6.15" + "@storybook/theming": "npm:8.6.18" better-opn: "npm:^3.0.2" browser-assert: "npm:^1.2.1" esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" @@ -1901,7 +1901,7 @@ __metadata: peerDependenciesMeta: prettier: optional: true - checksum: 10c0/34f1f3927d605119c80d2a3dbd8efa69d7bcf419877c09da2fc1e091fcf6bdb09d9a114c0c51651d7058425375689865435f7e6ec2be911d4dd437629d7e31e1 + checksum: 10c0/830b8806a35f8b6a448efe5d0df47a148d6111295b0e7027bf307232f25542015fc15e9b9c3489921f614fc6695323d74194c90da97837b0df0a72eb73ec6220 languageName: node linkType: hard @@ -2051,12 +2051,12 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/theming@npm:8.6.15" +"@storybook/theming@npm:8.6.18": + version: 8.6.18 + resolution: "@storybook/theming@npm:8.6.18" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10c0/b5ef8fb7e987f7b416c986e110e4d7191bb27df3e10c4e62130f6299f0cf5cf7f1b345493b221383ca103d85e09d03a879918a5de5df1ff8916713995f1ad501 + checksum: 10c0/30667a5ed7b70ac4a19f0d4baff53f065bd6f921934d21165dbff52add118b2fe1872c7f25ff8c8a1c768a0344f5db461a5bdf2838533069b3d760b3727b52d0 languageName: node linkType: hard @@ -4664,7 +4664,7 @@ __metadata: react-router-dom: "npm:^7.2.0" react-toastify: "npm:^9.1.3" sass: "npm:^1.94.2" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" stylelint: "npm:^15.10.3" stylelint-config-sass-guidelines: "npm:^10.0.0" stylelint-config-standard: "npm:^34.0.0" @@ -8020,11 +8020,11 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^8.6.15": - version: 8.6.15 - resolution: "storybook@npm:8.6.15" +"storybook@npm:^8.6.17": + version: 8.6.18 + resolution: "storybook@npm:8.6.18" dependencies: - "@storybook/core": "npm:8.6.15" + "@storybook/core": "npm:8.6.18" peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: @@ -8034,7 +8034,7 @@ __metadata: getstorybook: ./bin/index.cjs sb: ./bin/index.cjs storybook: ./bin/index.cjs - checksum: 10c0/8d54dd81811fa3299c85667c55f7627e6f1010e86ab56babcd853987dedab4e711a2b0ee4ff545b091030a92e12becd84e4e2e7b99cd8a1c1d30697757af2545 + checksum: 10c0/0ea0c0d0226dc2e454838f33cada24be0b1060a539975b559846663d02c1eeb7a1b7a5f703208493261895142c68bf0b2ce4d6ba78d03869f434ceab16eb420d languageName: node linkType: hard From cffdf5e6ef77bed629463c6e0478ec69dcfaeb70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:10:04 +0100 Subject: [PATCH 47/79] Bump minimatch from 3.1.2 to 3.1.5 in /frontend (#2085) Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index cbf933da9..d83076358 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6325,11 +6325,11 @@ __metadata: linkType: hard "minimatch@npm:^3.1.1": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 languageName: node linkType: hard From 54ba6458238bf5581b23eefba56850c38e2c9793 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:21:22 +0100 Subject: [PATCH 48/79] Bump flatted from 3.3.3 to 3.4.2 in /frontend (#2118) Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2. - [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2) --- updated-dependencies: - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d83076358..e6acb1928 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4533,9 +4533,9 @@ __metadata: linkType: hard "flatted@npm:^3.2.9": - version: 3.3.3 - resolution: "flatted@npm:3.3.3" - checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538 + version: 3.4.2 + resolution: "flatted@npm:3.4.2" + checksum: 10c0/a65b67aae7172d6cdf63691be7de6c5cd5adbdfdfe2e9da1a09b617c9512ed794037741ee53d93114276bff3f93cd3b0d97d54f9b316e1e4885dde6e9ffdf7ed languageName: node linkType: hard From b95f4aaf27e67ae72286bf8aeea11a784fab9537 Mon Sep 17 00:00:00 2001 From: Lidavic <148764396+Lidavic@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:56:26 +0000 Subject: [PATCH 49/79] docs fix:) (#2106) --- docs/common-errors.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/common-errors.md b/docs/common-errors.md index c956deb16..777829670 100644 --- a/docs/common-errors.md +++ b/docs/common-errors.md @@ -31,7 +31,16 @@ seed exec /app/entrypoint.sh: no such file or directory ``` ### Fix - Make sure `/backend/entrypoint.sh` has `End of Line sequence set` to `LF` (Happens when running on windows). + Make sure `/backend/entrypoint.sh` has `End of Line sequence` set to `LF` (Happens when running on windows). \ +Sometimes VScode will lie, so you can double check by running the command below, and checking if any of the lines end in "^M". If they do, the file still has `CRLF` as the line ending. +``` bash +cat -v +``` +To fix this, you can force change the line ending to ´LF´: +``` bash +dos2unix +``` +``` ## Docker daemon not running ### Error message: From 790203e007782667b5df2b482dbc3344d29545d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:21:53 +0100 Subject: [PATCH 50/79] Bump ruff from 0.11.6 to 0.15.4 in /backend (#2091) * Bump ruff from 0.11.6 to 0.15.4 in /backend Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.6 to 0.15.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.6...0.15.4) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.4 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Ruff fixes --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: robines --- backend/manage.py | 2 +- backend/poetry.lock | 38 +++++++++---------- .../management/commands/seed_scripts/gangs.py | 2 +- backend/root/utils/debugpy.py | 2 +- backend/samfundet/apps.py | 5 +-- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/backend/manage.py b/backend/manage.py index 3c7872ace..cbdd160a6 100644 --- a/backend/manage.py +++ b/backend/manage.py @@ -10,7 +10,7 @@ def main() -> None: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'root.settings') try: - from django.core.management import execute_from_command_line + from django.core.management import execute_from_command_line # noqa: PLC0415 except ImportError as exc: raise ImportError( diff --git a/backend/poetry.lock b/backend/poetry.lock index fbe678568..cd110ee74 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1230,30 +1230,30 @@ files = [ [[package]] name = "ruff" -version = "0.11.6" +version = "0.15.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1"}, - {file = "ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de"}, - {file = "ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2"}, - {file = "ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b"}, - {file = "ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9"}, - {file = "ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287"}, - {file = "ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e"}, - {file = "ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79"}, - {file = "ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79"}, + {file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"}, + {file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"}, + {file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"}, + {file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"}, + {file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"}, + {file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"}, + {file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"}, ] [[package]] diff --git a/backend/root/management/commands/seed_scripts/gangs.py b/backend/root/management/commands/seed_scripts/gangs.py index de802e2b7..65069cd93 100755 --- a/backend/root/management/commands/seed_scripts/gangs.py +++ b/backend/root/management/commands/seed_scripts/gangs.py @@ -68,7 +68,7 @@ def seed(): # noqa: C901 - total_gangs = sum(len(gangs) for org_name, org_data in GANGS.items() for gang_type, gangs in org_data.items()) + total_gangs = sum(len(gangs) for org_data in GANGS.values() for gangs in org_data.values()) created_count = 0 yield 0, 'Creating gangs' diff --git a/backend/root/utils/debugpy.py b/backend/root/utils/debugpy.py index 3c5cb0a14..19a86c1a0 100644 --- a/backend/root/utils/debugpy.py +++ b/backend/root/utils/debugpy.py @@ -17,7 +17,7 @@ def initialize_debugpy() -> None: This may be called in wsgi.py if hosting with gunicorn or in manage.py::__main__. """ if os.environ.get('ENABLE_DEBUGPY') == 'yes': - import debugpy + import debugpy # noqa: PLC0415 # This is okay as long as ENABLE_DEBUGPY only is enabled during development and NOT in production. HOST = '0.0.0.0' # noqa: N806, S104 diff --git a/backend/samfundet/apps.py b/backend/samfundet/apps.py index 4c5b0f027..b9d926637 100644 --- a/backend/samfundet/apps.py +++ b/backend/samfundet/apps.py @@ -4,6 +4,7 @@ import logging from django.apps import AppConfig +from django.core import management from root.constants import Environment @@ -15,9 +16,7 @@ class SamfundetConfig(AppConfig): name = 'samfundet' def ready(self) -> None: - from django.core import management - - from . import signals # noqa: F401 # Important, this enables signals. + from . import signals # noqa: F401, PLC0415 # Important, this enables signals. if os.environ['ENV'] == Environment.DEV: try: From 6de996e7d2f81d7d3366bb889cc30e03a3eda3af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:41:23 +0100 Subject: [PATCH 51/79] Bump stylelint-config-standard from 34.0.0 to 40.0.0 in /frontend (#2046) Bumps [stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard) from 34.0.0 to 40.0.0. - [Release notes](https://github.com/stylelint/stylelint-config-standard/releases) - [Changelog](https://github.com/stylelint/stylelint-config-standard/blob/main/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint-config-standard/compare/34.0.0...40.0.0) --- updated-dependencies: - dependency-name: stylelint-config-standard dependency-version: 40.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package.json | 2 +- frontend/yarn.lock | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 00e9330ed..5baab813d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -101,7 +101,7 @@ "storybook": "^8.6.17", "stylelint": "^15.10.3", "stylelint-config-sass-guidelines": "^10.0.0", - "stylelint-config-standard": "^34.0.0", + "stylelint-config-standard": "^40.0.0", "stylelint-config-standard-scss": "^11.0.0", "stylelint-scss": "^5.2.1", "typescript": "^5.9.3", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e6acb1928..7de31e5f7 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4667,7 +4667,7 @@ __metadata: storybook: "npm:^8.6.17" stylelint: "npm:^15.10.3" stylelint-config-sass-guidelines: "npm:^10.0.0" - stylelint-config-standard: "npm:^34.0.0" + stylelint-config-standard: "npm:^40.0.0" stylelint-config-standard-scss: "npm:^11.0.0" stylelint-scss: "npm:^5.2.1" typescript: "npm:^5.9.3" @@ -8171,6 +8171,15 @@ __metadata: languageName: node linkType: hard +"stylelint-config-recommended@npm:^18.0.0": + version: 18.0.0 + resolution: "stylelint-config-recommended@npm:18.0.0" + peerDependencies: + stylelint: ^17.0.0 + checksum: 10c0/c7f8ff45c76ec23f4c8c0438894726976fd5e872c59d489f959b728d9879bba20dbf0040cd29ad3bbc00eb32befd95f5b6ca150002bb8aea74b0797bc42ccc17 + languageName: node + linkType: hard + "stylelint-config-sass-guidelines@npm:^10.0.0": version: 10.0.0 resolution: "stylelint-config-sass-guidelines@npm:10.0.0" @@ -8211,6 +8220,17 @@ __metadata: languageName: node linkType: hard +"stylelint-config-standard@npm:^40.0.0": + version: 40.0.0 + resolution: "stylelint-config-standard@npm:40.0.0" + dependencies: + stylelint-config-recommended: "npm:^18.0.0" + peerDependencies: + stylelint: ^17.0.0 + checksum: 10c0/d8942552d53a3afda59b64d0c49503bb626fe5cef39a9e8c9583fcd60869f21431125ef4480ff27a59f7f2cf0da8af810d377129ef1d670ddc5def4defe2880c + languageName: node + linkType: hard + "stylelint-scss@npm:^4.4.0": version: 4.7.0 resolution: "stylelint-scss@npm:4.7.0" From ab4f0d9ed476d6fd9d219adb33a02e9c7b90e24b Mon Sep 17 00:00:00 2001 From: Erik Hoff <142814286+aTrueYety@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:53:17 +0100 Subject: [PATCH 52/79] Use Translations in Schema Messages (#2105) * add translation support to react form message * add translations to event error messages * add react form utility to allow translatrions in dropdwons --------- Co-authored-by: Erik --- frontend/src/Components/Forms/Form.tsx | 5 ++++- frontend/src/i18n/constants.ts | 15 +++++++++++++ frontend/src/i18n/translations.ts | 30 ++++++++++++++++++++++++++ frontend/src/schema/event.ts | 26 +++++++++++----------- frontend/src/schema/utils.ts | 5 +++++ 5 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 frontend/src/schema/utils.ts diff --git a/frontend/src/Components/Forms/Form.tsx b/frontend/src/Components/Forms/Form.tsx index 012e3ed83..45a4159f1 100644 --- a/frontend/src/Components/Forms/Form.tsx +++ b/frontend/src/Components/Forms/Form.tsx @@ -8,6 +8,7 @@ import { FormProvider, useFormContext, } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import styles from './Forms.module.scss'; @@ -126,7 +127,9 @@ FormControl.displayName = 'FormControl'; export const FormMessage = React.forwardRef>( ({ className, children, ...props }, ref) => { const { error, formMessageId } = useFormField(); - const body = error ? String(error?.message) : children; + const { t, i18n } = useTranslation(); + const message = error ? String(error?.message) : children; + const body = typeof message === 'string' && i18n.exists(message) ? t(message) : message; if (!body) { return null; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 6dafd429d..c9258d818 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -670,6 +670,21 @@ export const KEY = { error_server_error_description: 'error_server_error_description', error_submitting_reservation: 'error_submitting_reservation', error_invalid_reservation_data: 'error_invalid_reservation_data', + + // Event form validation + event_form_title_required: 'event_form_title_required', + event_form_description_long_required: 'event_form_description_long_required', + event_form_description_short_required: 'event_form_description_short_required', + event_form_start_dt_required: 'event_form_start_dt_required', + event_form_duration_min: 'event_form_duration_min', + event_form_host_required: 'event_form_host_required', + event_form_location_required: 'event_form_location_required', + event_form_capacity_min: 'event_form_capacity_min', + event_form_visibility_from_required: 'event_form_visibility_from_required', + + event_form_category_required: 'event_form_category_required', + event_form_age_restriction_required: 'event_form_age_restriction_required', + event_form_ticket_type_required: 'event_form_ticket_type_required', } as const; // This will ensure that each value matches the key exactly. diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 33d1ce93c..19e50e8db 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -688,6 +688,21 @@ export const nb = prepareTranslations({ [KEY.error_server_error_description]: 'En serverfeil har opptstått', [KEY.error_submitting_reservation]: 'Det skjedde en feil ved innsending av reservasjon', [KEY.error_invalid_reservation_data]: 'Ugyldig reservasjonsdata', + + // Event form validation + [KEY.event_form_title_required]: 'Tittel er påkrevd', + [KEY.event_form_description_long_required]: 'Lang beskrivelse er påkrevd', + [KEY.event_form_description_short_required]: 'Kort beskrivelse er påkrevd', + [KEY.event_form_start_dt_required]: 'Dato og tid er påkrevd', + [KEY.event_form_duration_min]: 'Varighet må være større enn 0', + [KEY.event_form_host_required]: 'Arrangør er påkrevd', + [KEY.event_form_location_required]: 'Lokale er påkrevd', + [KEY.event_form_capacity_min]: 'Kapasitet må være større enn 0', + [KEY.event_form_visibility_from_required]: 'Synlig fra dato er påkrevd', + + [KEY.event_form_category_required]: 'Kategori er påkrevd', + [KEY.event_form_age_restriction_required]: 'Aldersgrense er påkrevd', + [KEY.event_form_ticket_type_required]: 'Billetttype er påkrevd', }); export const en = prepareTranslations({ @@ -1369,4 +1384,19 @@ export const en = prepareTranslations({ [KEY.error_server_error_description]: 'A server error has occurred', [KEY.error_submitting_reservation]: 'An error occurred while submitting the reservation', [KEY.error_invalid_reservation_data]: 'Invalid reservation data', + + // Event form validation + [KEY.event_form_title_required]: 'Title is required', + [KEY.event_form_description_long_required]: 'Long description is required', + [KEY.event_form_description_short_required]: 'Short description is required', + [KEY.event_form_start_dt_required]: 'Date and time is required', + [KEY.event_form_duration_min]: 'Duration must be greater than 0', + [KEY.event_form_host_required]: 'Host is required', + [KEY.event_form_location_required]: 'Venue is required', + [KEY.event_form_capacity_min]: 'Capacity must be greater than 0', + [KEY.event_form_visibility_from_required]: 'Visible from date is required', + + [KEY.event_form_category_required]: 'Category is required', + [KEY.event_form_age_restriction_required]: 'Age restriction is required', + [KEY.event_form_ticket_type_required]: 'Ticket type is required', }); diff --git a/frontend/src/schema/event.ts b/frontend/src/schema/event.ts index f3da2a737..5f9fdec75 100644 --- a/frontend/src/schema/event.ts +++ b/frontend/src/schema/event.ts @@ -1,16 +1,18 @@ import { z } from 'zod'; +import { KEY } from '~/i18n/constants'; import { EventAgeRestriction, EventCategory, EventTicketType } from '~/types'; +import { zodEnum } from './utils'; -export const EVENT_TITLE = z.string().min(1, { message: 'Tittel er påkrevd' }); -export const EVENT_DESCRIPTION_LONG = z.string().min(1, { message: 'Lang beskrivelse er påkrevd' }); -export const EVENT_DESCRIPTION_SHORT = z.string().min(1, { message: 'Kort beskrivelse er påkrevd' }); -export const EVENT_START_DT = z.string().min(1, { message: 'Dato og tid er påkrevd' }); -export const EVENT_DURATION = z.number().min(1, { message: 'Varighet må være større enn 0' }); +export const EVENT_TITLE = z.string().min(1, { message: KEY.event_form_title_required }); +export const EVENT_DESCRIPTION_LONG = z.string().min(1, { message: KEY.event_form_description_long_required }); +export const EVENT_DESCRIPTION_SHORT = z.string().min(1, { message: KEY.event_form_description_short_required }); +export const EVENT_START_DT = z.string().min(1, { message: KEY.event_form_start_dt_required }); +export const EVENT_DURATION = z.number().min(1, { message: KEY.event_form_duration_min }); export const EVENT_END_DT = z.string().optional(); -export const EVENT_HOST = z.string().min(1, { message: 'Arrangør er påkrevd' }); -export const EVENT_LOCATION = z.string().min(1, { message: 'Lokale er påkrevd' }); -export const EVENT_CAPACITY = z.number().min(1, { message: 'Kapasitet må være større enn 0' }).optional(); -export const EVENT_VISIBILITY_FROM_DT = z.string().min(1, { message: 'Synlig fra dato er påkrevd' }); +export const EVENT_HOST = z.string().min(1, { message: KEY.event_form_host_required }); +export const EVENT_LOCATION = z.string().min(1, { message: KEY.event_form_location_required }); +export const EVENT_CAPACITY = z.number().min(1, { message: KEY.event_form_capacity_min }).optional(); +export const EVENT_VISIBILITY_FROM_DT = z.string().min(1, { message: KEY.event_form_visibility_from_required }); export const EVENT_VISIBILITY_TO_DT = z.string().optional(); export const EVENT_PAID_OPTION = z.string().url().optional(); export const EVENT_BILLIG_ID = z.number().optional(); @@ -21,6 +23,6 @@ export const EVENT_INSTAGRAM_LINK = z.string().url().optional(); export const EVENT_FACEBOOK_LINK = z.string().url().optional(); export const EVENT_X_LINK = z.string().url().optional(); -export const EVENT_CATEGORY = z.nativeEnum(EventCategory); -export const EVENT_AGE_RESTRICTION = z.nativeEnum(EventAgeRestriction); -export const EVENT_TICKET_TYPE = z.nativeEnum(EventTicketType); +export const EVENT_CATEGORY = zodEnum(EventCategory, KEY.event_form_category_required); +export const EVENT_AGE_RESTRICTION = zodEnum(EventAgeRestriction, KEY.event_form_age_restriction_required); +export const EVENT_TICKET_TYPE = zodEnum(EventTicketType, KEY.event_form_ticket_type_required); diff --git a/frontend/src/schema/utils.ts b/frontend/src/schema/utils.ts new file mode 100644 index 000000000..bd45a7b0c --- /dev/null +++ b/frontend/src/schema/utils.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export function zodEnum(enumObj: T, message: string) { + return z.nativeEnum(enumObj, { errorMap: () => ({ message }) }); +} From bc2d8a82c99e42e2d5f65f9a9b4d68a4d89138da Mon Sep 17 00:00:00 2001 From: Erik Hoff <142814286+aTrueYety@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:01:50 +0100 Subject: [PATCH 53/79] Resolve "Add pagination to event admin page" (#2072) * Add pagination to EventsAdminPage * Fix translations of ticket type * Add pagination to upcomming event endpoint and view * Add ticket type as filter to event admin page * update event querry keys to fit new pagination * Connect frontend to new backend pagination * fix typing error in EventsUpcomingView * Fix typo in translations * Rename incorrect translation key and add a correct one * Remove carousel form event admin page --------- Co-authored-by: Erik --- backend/samfundet/utils.py | 1 + backend/samfundet/view/event_views.py | 49 ++++-- .../src/Components/EventQuery/EventQuery.tsx | 24 ++- .../TicketTypeRow/TicketTypeRow.tsx | 2 +- .../HomePage/components/Splash/Splash.tsx | 2 +- .../EventsAdminPage/EventsAdminPage.tsx | 160 ++++++++++-------- frontend/src/api.ts | 22 ++- frontend/src/i18n/constants.ts | 1 + frontend/src/i18n/translations.ts | 6 +- frontend/src/queryKeys.ts | 6 + frontend/src/types.ts | 6 + 11 files changed, 183 insertions(+), 96 deletions(-) diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index de4f01be3..82b3598ed 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -35,6 +35,7 @@ 'event_group': 'event_group__id', 'category': 'category__icontains', 'venue': 'location__icontains', + 'ticket_type': 'ticket_type', } diff --git a/backend/samfundet/view/event_views.py b/backend/samfundet/view/event_views.py index fe319935f..488f38bc1 100644 --- a/backend/samfundet/view/event_views.py +++ b/backend/samfundet/view/event_views.py @@ -3,10 +3,13 @@ # =============================== # from __future__ import annotations +from typing import Any + from rest_framework import status from rest_framework.views import APIView +from rest_framework.filters import SearchFilter from rest_framework.request import Request -from rest_framework.generics import CreateAPIView +from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework.permissions import AllowAny, IsAuthenticated @@ -17,6 +20,7 @@ from root.custom_classes.permission_classes import FeatureEnabled, RoleProtectedOrAnonReadOnlyObjectPermissions from samfundet.utils import event_query +from samfundet.pagination import CustomPageNumberPagination from samfundet.serializers import ( EventSerializer, EventGroupSerializer, @@ -66,24 +70,33 @@ def get(self, request: Request) -> Response: return Response(data=events_per_day) -class EventsUpcomingView(APIView): +class EventsUpcomingView(ListAPIView): permission_classes = [AllowAny] - - def get(self, request: Request) -> Response: - events = event_query(query=request.query_params) - events = events.filter(start_dt__gt=timezone.now()).order_by('start_dt') - serialized_events = EventSerializer(events, many=True).data - - # Fetch all venue names - venue_names = list(Venue.objects.values_list('name', flat=True)) - - response_data = { - 'events': serialized_events, - 'categories': Event._meta.get_field('category').choices if Event._meta.get_field('category').choices else [], - 'locations': venue_names if venue_names else [], - } - - return Response(data=response_data, status=status.HTTP_200_OK) + serializer_class = EventSerializer + pagination_class = CustomPageNumberPagination + filter_backends = [SearchFilter] + search_fields = ['title_en', 'title_nb'] + + def get_queryset(self) -> Response: + queryset = event_query(query=self.request.query_params) + queryset = queryset.filter(start_dt__gt=timezone.now()).order_by('start_dt') + return queryset + + def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: + # Get the paginated response from parent + response = super().list(request, *args, **kwargs) + + # Add categories, locations, organizers, and ticket types to the response + if isinstance(response.data, dict): + venue_names = list(Venue.objects.values_list('name', flat=True)) + categories = Event._meta.get_field('category').choices if Event._meta.get_field('category').choices else [] + ticket_types = Event._meta.get_field('ticket_type').choices if Event._meta.get_field('ticket_type').choices else [] + + response.data['categories'] = categories + response.data['locations'] = venue_names if venue_names else [] + response.data['ticket_types'] = ticket_types + + return response class EventGroupView(ModelViewSet): diff --git a/frontend/src/Components/EventQuery/EventQuery.tsx b/frontend/src/Components/EventQuery/EventQuery.tsx index ec7ec3f61..51c0e85fe 100644 --- a/frontend/src/Components/EventQuery/EventQuery.tsx +++ b/frontend/src/Components/EventQuery/EventQuery.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next'; import { InputField } from '~/Components'; import { KEY } from '~/i18n/constants'; -import type { EventCategoryValue, SetState } from '~/types'; -import { getEventCategoryKey, lowerCapitalize } from '~/utils'; +import type { EventCategoryValue, EventTicketTypeValue, SetState } from '~/types'; +import { getEventCategoryKey, getTicketTypeKey, lowerCapitalize } from '~/utils'; import { Dropdown } from '../Dropdown'; import type { DropdownOption } from '../Dropdown/Dropdown'; import styles from './EventQuery.module.scss'; @@ -10,10 +10,13 @@ import styles from './EventQuery.module.scss'; type EventQueryProps = { venues: string[] | null; categories: EventCategoryValue[] | null; + ticketTypes?: string[] | null; selectedVenue: string | null; setSelectedVenue: SetState; selectedCategory: EventCategoryValue | null; setSelectedCategory: SetState; + selectedTicketType?: string | null; + setSelectedTicketType?: SetState; search: string; setSearch: SetState; }; @@ -21,8 +24,10 @@ type EventQueryProps = { export function EventQuery({ venues, categories, + ticketTypes, setSelectedVenue, setSelectedCategory, + setSelectedTicketType, search, setSearch, }: EventQueryProps) { @@ -36,6 +41,10 @@ export function EventQuery({ return { label: t(getEventCategoryKey(category)), value: category } as DropdownOption; }); + const ticketTypeOptions: DropdownOption[] = (ticketTypes ?? []).map((tt) => { + return { label: t(getTicketTypeKey(tt as EventTicketTypeValue)), value: tt } as DropdownOption; + }); + return (
    setSelectedCategory(val as EventCategoryValue)} className={styles.element} - nullOption={{ label: lowerCapitalize(`${t(KEY.common_choose)} ${t(KEY.event_type)}`) }} + nullOption={{ label: lowerCapitalize(`${t(KEY.common_choose)} ${t(KEY.category)}`) }} /> + + {ticketTypes !== undefined && setSelectedTicketType && ( + setSelectedTicketType(val as string)} + className={styles.element} + nullOption={{ label: lowerCapitalize(`${t(KEY.common_choose)} ${t(KEY.common_ticket)}`) }} + /> + )}
    ); } diff --git a/frontend/src/Pages/EventPage/components/TicketTypeRow/TicketTypeRow.tsx b/frontend/src/Pages/EventPage/components/TicketTypeRow/TicketTypeRow.tsx index 2d10054c7..8d9f93035 100644 --- a/frontend/src/Pages/EventPage/components/TicketTypeRow/TicketTypeRow.tsx +++ b/frontend/src/Pages/EventPage/components/TicketTypeRow/TicketTypeRow.tsx @@ -15,7 +15,7 @@ export function TicketTypeRow({ event }: TicketTypeRowProps) { if (event.ticket_type !== EventTicketType.CUSTOM) { return (
    - + ); diff --git a/frontend/src/Pages/HomePage/components/Splash/Splash.tsx b/frontend/src/Pages/HomePage/components/Splash/Splash.tsx index e5559f248..5c5857082 100644 --- a/frontend/src/Pages/HomePage/components/Splash/Splash.tsx +++ b/frontend/src/Pages/HomePage/components/Splash/Splash.tsx @@ -40,7 +40,7 @@ export function Splash({ events, showInfo }: SplashProps) { const ticketButton = isPaid ? ( ) : ( <> diff --git a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx index 65312b2ba..dbf74fab1 100644 --- a/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventsAdminPage/EventsAdminPage.tsx @@ -1,74 +1,103 @@ -import { useEffect, useState } from 'react'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import { toast } from 'react-toastify'; -import { Button, Carousel, EventQuery, ImageCard, TimeDisplay } from '~/Components'; +import { Button, EventQuery, TimeDisplay } from '~/Components'; import { CrudButtons } from '~/Components/CrudButtons/CrudButtons'; +import { PagedPagination } from '~/Components/Pagination'; import { Table } from '~/Components/Table'; -import { deleteEvent, getEventsUpcomming } from '~/api'; -import { BACKEND_DOMAIN } from '~/constants'; +import { deleteEvent, getEventsUpcommingPaginated } from '~/api'; import type { EventDto } from '~/dto'; import { useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; import { reverse } from '~/named-urls'; +import { eventKeys } from '~/queryKeys'; import { ROUTES } from '~/routes'; import type { EventCategoryValue } from '~/types'; import { dbT, getTicketTypeKey, lowerCapitalize } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './EventsAdminPage.module.scss'; +const PAGE_SIZE = 20; + export function EventsAdminPage() { const navigate = useNavigate(); - const [events, setEvents] = useState([]); - const [allEvents, setAllEvents] = useState([]); - const [showSpinner, setShowSpinner] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [searchInput, setSearchInput] = useState(''); + const [debouncedSearch, setDebouncedSearch] = useState(''); + const [selectedVenue, setSelectedVenue] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedTicketType, setSelectedTicketType] = useState(null); + const debounceTimeout = useRef(); const { t, i18n } = useTranslation(); useTitle(t(KEY.admin_events_administrate)); const [venues, setVenues] = useState(null); const [categories, setCategories] = useState([]); - const [selectedVenue, setSelectedVenue] = useState(null); - const [selectedCategory, setSelectedCategory] = useState(null); - const [search, setSearch] = useState(''); + const [ticketTypes, setTicketTypes] = useState([]); - function getEvents(venue?: string | null, category?: EventCategoryValue | null) { - getEventsUpcomming({ venue: venue ?? undefined, category: category ?? undefined }) - .then((data) => { - setCategories(data.categories as EventCategoryValue[]); - setVenues(data.locations); - setEvents(data.events); - setAllEvents(data.events); - setShowSpinner(false); - }) - .catch((error) => { - toast.error(t(KEY.common_something_went_wrong)); - console.error(error); - }); - } + // Debounce search input + useEffect(() => { + clearTimeout(debounceTimeout.current); + debounceTimeout.current = setTimeout(() => { + setDebouncedSearch(searchInput); + }, 300); - function filterEvents(): EventDto[] { - const normalizedSearch = search.trim().toLowerCase(); - if (search === '') return events; - const keywords = normalizedSearch.split(' '); + return () => clearTimeout(debounceTimeout.current); + }, [searchInput]); - return events.filter((event: EventDto) => { - const title = (dbT(event, 'title', i18n.language) as string)?.toLowerCase() ?? ''; - return keywords.every((kw) => title.includes(kw)); - }); - } + // Reset to page 1 when filters change + // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally reset page when any filter changes + useEffect(() => { + setCurrentPage(1); + }, [debouncedSearch, selectedVenue, selectedCategory, selectedTicketType]); - // Stuff to do on first render. - // TODO add permissions on render - // biome-ignore lint/correctness/useExhaustiveDependencies: + // Fetch paginated events + const { data, isLoading } = useQuery({ + queryKey: eventKeys.paginatedList(currentPage, PAGE_SIZE, { + search: debouncedSearch || undefined, + venue: selectedVenue || undefined, + category: selectedCategory || undefined, + ticket_type: selectedTicketType || undefined, + }), + queryFn: () => + getEventsUpcommingPaginated(currentPage, PAGE_SIZE, { + search: debouncedSearch || undefined, + venue: selectedVenue || undefined, + category: selectedCategory || undefined, + ticket_type: selectedTicketType || undefined, + }), + placeholderData: keepPreviousData, + }); + + const events = data?.results ?? []; + const totalCount = data?.count ?? 0; + + // Extract metadata from API response useEffect(() => { - getEvents(selectedVenue, selectedCategory); - }, [selectedVenue, selectedCategory]); + if (data) { + if (data.categories) { + const rawCats = data.categories as (string | [string, string])[]; + setCategories(rawCats.map((cat) => (Array.isArray(cat) ? cat[0] : cat)) as EventCategoryValue[]); + } + if (data.locations) { + setVenues(data.locations as string[]); + } + if (data.ticket_types) { + const rawTts = data.ticket_types as (string | [string, string])[]; + setTicketTypes(rawTts.map((tt) => (Array.isArray(tt) ? tt[0] : tt))); + } + } + }, [data]); function deleteSelectedEvent(id: number) { deleteEvent(id) .then(() => { - getEvents(); + // Refetch the current page toast.success(t(KEY.eventsadminpage_successful_delete_toast)); + // Force a refetch by triggering the query again + navigate(ROUTES.frontend.admin_events); }) .catch((error) => { toast.error(t(KEY.common_something_went_wrong)); @@ -77,18 +106,16 @@ export function EventsAdminPage() { } const tableColumns = [ - { content: t(KEY.common_title), sortable: true }, - { content: t(KEY.start_time), sortable: true }, - { content: t(KEY.category), sortable: true }, - { content: t(KEY.admin_organizer), sortable: true }, - { content: t(KEY.common_venue), sortable: true }, - { content: t(KEY.common_ticket_type), sortable: true }, + { content: t(KEY.common_title) }, + { content: t(KEY.start_time) }, + { content: t(KEY.category) }, + { content: t(KEY.admin_organizer) }, + { content: t(KEY.common_venue) }, + { content: t(KEY.common_ticket_type) }, '', // Buttons ]; - const filteredEvents = filterEvents(); - - const data = filteredEvents.map((event: EventDto) => ({ + const tableData = events.map((event: EventDto) => ({ cells: [ dbT(event, 'title', i18n.language) as string, { content: , value: event.start_dt }, @@ -116,10 +143,8 @@ export function EventsAdminPage() { ); }} onDelete={() => { - // TODO custom modal confirm const msg = lowerCapitalize(`${t(KEY.form_confirm)} ${t(KEY.common_delete)}`); if (window.confirm(`${msg} ${dbT(event, 'title')}`)) { - // TODO toast component? A bit too easy to delete events deleteSelectedEvent(event.id); } }} @@ -140,35 +165,30 @@ export function EventsAdminPage() { ); return ( - - - {allEvents.slice(0, Math.min(allEvents.length, 10)).map((event) => { - return ( - - ); - })} - +
    -
    {t(KEY.common_ticket_type).toUpperCase()} {t(KEY.common_ticket).toUpperCase()} {t(getTicketTypeKey(event.ticket_type))}
    +
    + +
    +
    ); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index d6afdf1c1..b156d9cd3 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -57,7 +57,7 @@ import { reverse } from '~/named-urls'; import { ROUTES } from '~/routes'; import type { BilligEventDto } from './apis/billig/billigDtos'; import { BACKEND_DOMAIN } from './constants'; -import type { PageNumberPaginationType } from './types'; +import type { EventsPaginationType, PageNumberPaginationType } from './types'; import { buildPaginatedUrl } from './utils'; export async function getCsrfToken(): Promise { @@ -286,6 +286,26 @@ export async function getEventsUpcomming(params: { }; } +export async function getEventsUpcommingPaginated( + page: number, + pageSize?: number, + params?: { + search?: string; + venue?: string; + category?: string; + ticket_type?: string; + }, +): Promise> { + const url = buildPaginatedUrl(BACKEND_DOMAIN + ROUTES.backend.samfundet__eventsupcomming, page, pageSize, { + ...(params?.search ? { search: params.search } : {}), + ...(params?.venue ? { venue: params.venue } : {}), + ...(params?.category ? { category: params.category } : {}), + ...(params?.ticket_type ? { ticket_type: params.ticket_type } : {}), + }); + const response = await axios.get>(url, { withCredentials: true }); + return response.data; +} + export async function getEvents(): Promise { const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__events_list; const response = await axios.get(url, { withCredentials: true }); diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index c9258d818..215dba196 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -231,6 +231,7 @@ export const KEY = { common_contact_information: 'common_contact_information', common_about_samfundet: 'common_about_samfundet', // Price groups: + common_ticket: 'common_ticket', common_ticket_type: 'common_ticket_type', common_ticket_type_free: 'common_ticket_type_free', common_ticket_type_free_with_registration: 'common_ticket_type_free_with_registration', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 19e50e8db..ea44e45f3 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -215,7 +215,8 @@ export const nb = prepareTranslations({ [KEY.common_about_samfundet]: 'Om Samfundet', // Price groups: - [KEY.common_ticket_type]: 'Billett', + [KEY.common_ticket]: 'Billett', + [KEY.common_ticket_type]: 'Billettype', [KEY.common_ticket_type_free]: 'Gratis', [KEY.common_ticket_type_free_with_registration]: 'Gratis med registrering', [KEY.common_ticket_type_custom]: 'Tilpasset', @@ -919,7 +920,8 @@ export const en = prepareTranslations({ [KEY.common_administrate]: 'Administrate', [KEY.common_administration]: 'Administration', // Price groups: - [KEY.common_ticket_type]: 'Ticket', + [KEY.common_ticket]: 'Ticket', + [KEY.common_ticket_type]: 'Ticket type', [KEY.common_ticket_type_free]: 'Free', [KEY.common_ticket_type_free_with_registration]: 'Free with registration', [KEY.common_ticket_type_billig]: 'Paid (billig)', diff --git a/frontend/src/queryKeys.ts b/frontend/src/queryKeys.ts index add88d9e5..f2f53b650 100644 --- a/frontend/src/queryKeys.ts +++ b/frontend/src/queryKeys.ts @@ -36,6 +36,12 @@ export const eventKeys = { all: ['events'] as const, lists: () => [...eventKeys.all, 'list'] as const, list: (filters: unknown[]) => [...eventKeys.lists(), { filters }] as const, + paginatedLists: () => [...eventKeys.all, 'paginated'] as const, + paginatedList: ( + page: number, + pageSize: number, + filters?: { search?: string; venue?: string; category?: string; ticket_type?: string }, + ) => [...eventKeys.paginatedLists(), { page, pageSize, ...filters }] as const, details: () => [...eventKeys.all, 'detail'] as const, detail: (id: number) => [...eventKeys.details(), id] as const, }; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2fbb1396b..cbba76575 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -270,6 +270,12 @@ export interface PageNumberPaginationType { results: T[]; } +export interface EventsPaginationType extends PageNumberPaginationType { + categories?: Array<[string, string]> | string[]; + locations?: string[]; + ticket_types?: Array<[string, string]> | string[]; +} + /* For DRF pagination, see pagination.py */ export interface CursorPaginatedResponse { next: string | null; // URL or cursor for next page From 37780186efd7a49f476c4d76e88c129ed9e64b8f Mon Sep 17 00:00:00 2001 From: Heidi Herfindal Rasmussen <144371674+hei98@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:32:38 +0100 Subject: [PATCH 54/79] Fix editing event (#2054) * Fix editing event * Remove unused code * Fix edit events * Fix for checks * Fix checks * Add step-based structure and form hook to EventCreator * Fxi imagepicker * Fix edit event * Fix edit events * Remove unused variable * Fix graphics step not working after changes to backend * Add pagination to ImagePicker for it to work * Fix biome * Fix biome * Fix biome * Add translation * Remove unused variable and update icon variable * Fix biome * Update frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx Co-authored-by: Robin <16273164+robines@users.noreply.github.com> --------- Co-authored-by: Robin <16273164+robines@users.noreply.github.com> --- backend/samfundet/serializers.py | 28 +- .../ImagePicker/ImagePicker.module.scss | 22 +- .../Components/ImagePicker/ImagePicker.tsx | 8 +- .../EventCreatorAdminPage.tsx | 631 +++--------------- .../components/EventPreviewCard.tsx | 39 ++ .../hooks/useEventCreatorForm.ts | 103 +++ .../hooks/useEventMutations.ts | 45 ++ .../steps/GraphicsStep.tsx | 35 + .../EventCreatorAdminPage/steps/InfoStep.tsx | 156 +++++ .../steps/PaymentStep.tsx | 49 ++ .../steps/SummaryStep.tsx | 27 + .../EventCreatorAdminPage/steps/TextStep.tsx | 117 ++++ .../EventCreatorAdminPage/steps/stepConfig.ts | 64 ++ frontend/src/api.ts | 8 +- frontend/src/dto.ts | 48 +- frontend/src/i18n/constants.ts | 1 + frontend/src/i18n/translations.ts | 2 + frontend/src/schema/event.ts | 2 +- frontend/src/schema/samfImage.ts | 14 +- 19 files changed, 808 insertions(+), 591 deletions(-) create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/components/EventPreviewCard.tsx create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventMutations.ts create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/steps/GraphicsStep.tsx create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/steps/InfoStep.tsx create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/steps/PaymentStep.tsx create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/steps/SummaryStep.tsx create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/steps/TextStep.tsx create mode 100644 frontend/src/PagesAdmin/EventCreatorAdminPage/steps/stepConfig.ts diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 987a0d849..8ce371523 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -3,7 +3,7 @@ import re import datetime import itertools -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from collections import defaultdict from PIL import Image as PilImage @@ -271,18 +271,37 @@ class Meta: model = Event list_serializer_class = EventListSerializer # Warning: registration object contains sensitive data, don't include it! - exclude = ['image', 'registration', 'event_group', 'billig_id'] + exclude = ['registration', 'event_group', 'billig_id'] # Read only properties (computed property, foreign model). total_registrations = serializers.IntegerField(read_only=True) image_url = serializers.CharField(read_only=True) + image = serializers.SerializerMethodField(read_only=True) # Custom tickets/billig custom_tickets = EventCustomTicketSerializer(many=True, read_only=True) billig = BilligEventSerializer(read_only=True) # For post/put (change image by id). - image_id = serializers.IntegerField(write_only=True) + image_id = serializers.IntegerField(write_only=True, required=True) + + def get_image(self, obj: Event) -> dict: + img = obj.image + return {'id': img.id, 'url': img.image.url, 'title': img.title, 'tags': list(img.tags.values_list('id', flat=True))} + + def update(self, instance: Event, validated_data: dict[str, Any]) -> Event: + image_id = validated_data.pop('image_id', None) + if image_id is not None: + try: + instance.image = Image.objects.get(pk=image_id) + except Image.DoesNotExist as err: + raise serializers.ValidationError({'image_id': 'Invalid image id'}) from err + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + + instance.save() + return instance def validate(self, data: dict) -> dict: # Check if all required fields are present for validation @@ -307,7 +326,8 @@ def create(self, validated_data: dict) -> Event: and sets it in the new event. Read/write only fields enable us to use the same serializer for both reading and writing. """ - validated_data['image'] = Image.objects.get(pk=validated_data['image_id']) + image_id = validated_data.pop('image_id') + validated_data['image'] = Image.objects.get(pk=image_id) event = Event(**validated_data) event.save() return event diff --git a/frontend/src/Components/ImagePicker/ImagePicker.module.scss b/frontend/src/Components/ImagePicker/ImagePicker.module.scss index b0c1ba438..1305ae0d4 100644 --- a/frontend/src/Components/ImagePicker/ImagePicker.module.scss +++ b/frontend/src/Components/ImagePicker/ImagePicker.module.scss @@ -35,20 +35,20 @@ .image_container { max-height: 25em; - overflow-y: scroll; - display: flex; - flex-direction: row; - justify-content: center; - flex-wrap: wrap; + overflow-y: auto; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10em, 1fr)); gap: 0.5em; - margin: 0 auto; + width: 100%; + align-content: start; + padding-right: 0.25rem; } .image { @include rounded; @include shadow-light; - width: 10em; - height: 8em; + width: 100%; + aspect-ratio: 5 / 4; background-color: $grey-1; background-size: cover; background-position: center; @@ -71,6 +71,8 @@ .search_wrapper { display: flex; flex-direction: column; + flex: 1; + min-width: 0; } .pagination_wrapper { @@ -78,3 +80,7 @@ justify-content: center; margin-top: 1rem; } + +.error { + margin-top: 1rem; +} \ No newline at end of file diff --git a/frontend/src/Components/ImagePicker/ImagePicker.tsx b/frontend/src/Components/ImagePicker/ImagePicker.tsx index 16f58b078..8d98ae4bd 100644 --- a/frontend/src/Components/ImagePicker/ImagePicker.tsx +++ b/frontend/src/Components/ImagePicker/ImagePicker.tsx @@ -45,7 +45,7 @@ export function ImagePicker({ onSelected, selectedImage }: ImagePickerProps) { }, [debouncedSearch]); // Fetch images using React Query - const { data } = useQuery({ + const { data, isError } = useQuery({ queryKey: imageKeys.list(currentPage, debouncedSearch || undefined), queryFn: () => getImagesPaginated(currentPage, PAGE_SIZE, debouncedSearch || undefined), placeholderData: keepPreviousData, @@ -80,7 +80,10 @@ export function ImagePicker({ onSelected, selectedImage }: ImagePickerProps) {
    {selected &&

    {selected.title}

    } -
    +
    {selected === undefined && ( <> @@ -88,6 +91,7 @@ export function ImagePicker({ onSelected, selectedImage }: ImagePickerProps) { )}
    + {isError &&

    {t(KEY.common_something_went_wrong)}

    }
    ; - -type EventCreatorStep = { - key: string; // Unique key. - title_nb: string; // Tab title norwegian. - title_en: string; // Tab title english. - customIcon?: string; // Custom icon in tab bar. - template: ReactElement; - validate: (data: FormType) => boolean; -}; +import type { FieldErrors } from 'react-hook-form'; +import { EventPreviewCard } from './components/EventPreviewCard'; +import { GraphicsStep } from './steps/GraphicsStep'; +import { InfoStep } from './steps/InfoStep'; +import { PaymentStep } from './steps/PaymentStep'; +import { SummaryStep } from './steps/SummaryStep'; +import { TextStep } from './steps/TextStep'; export function EventCreatorAdminPage() { const { t } = useTranslation(); - const navigate = useCustomNavigate(); const [event, setEvent] = useState>(); const [showSpinner, setShowSpinner] = useState(true); const { id } = useParams(); + const { createEventMutation, editEventMutation } = useEventMutations(); - const { data: venues = [], isLoading } = useQuery({ + const { data: venues = [] } = useQuery({ queryKey: venueKeys.all, queryFn: getVenues, }); @@ -77,69 +56,26 @@ export function EventCreatorAdminPage() { label: t(getAgeRestrictionKey(age)), })); - // Setup React Hook Form - const form = useForm({ - resolver: zodResolver(eventSchema), - mode: 'onChange', - defaultValues: { - title_nb: '', - title_en: '', - description_long_nb: '', - description_long_en: '', - description_short_nb: '', - description_short_en: '', - start_dt: '', - duration: undefined, - end_dt: '', - category: undefined, - host: '', - location: undefined, - capacity: undefined, - age_restriction: undefined, - ticket_type: undefined, - custom_tickets: [], - billig_id: undefined, - image: undefined, - visibility_from_dt: '', - visibility_to_dt: '', - }, + const { form, watchedValues, buildPayload } = useEventCreatorForm({ + event, + defaultCategory: eventCategoryOptions[0]?.value ?? EventCategory.ART, + defaultLocation: locationOptions[0]?.value ?? '', }); + const stepComponentMap: Record = { + text: , + info: , + payment: , + graphics: , + summary: , + }; + // Fetch event data using the event ID useEffect(() => { if (id) { getEvent(id) .then((eventData) => { - const eventDuration = Math.round( - (new Date(eventData.end_dt).getTime() - new Date(eventData.start_dt).getTime()) / 60000, - ); - const imageObject: ImageDto | undefined = eventData.image_url - ? { id: eventData.id, title: '', url: eventData.image_url, tags: [] } - : undefined; setEvent(eventData); - form.reset({ - title_nb: eventData.title_nb || '', - title_en: eventData.title_en || '', - description_long_nb: eventData.description_long_nb || '', - description_long_en: eventData.description_long_en || '', - description_short_nb: eventData.description_short_nb || '', - description_short_en: eventData.description_short_en || '', - start_dt: eventData.start_dt ? utcTimestampToLocal(eventData.start_dt, false) : '', - duration: eventDuration || undefined, - end_dt: eventData.end_dt ? utcTimestampToLocal(eventData.end_dt, false) : '', - category: eventData.category || '', - host: eventData.host || '', - location: eventData.location || '', - capacity: eventData.capacity || undefined, - age_restriction: eventData.age_restriction || 'none', - ticket_type: eventData.ticket_type || 'free', - custom_tickets: eventData.custom_tickets || [], - billig_id: eventData.billig?.id, - image: imageObject, - visibility_from_dt: eventData.visibility_from_dt - ? utcTimestampToLocal(eventData.visibility_from_dt, false) - : '', - }); setShowSpinner(false); }) .catch((error) => { @@ -148,430 +84,28 @@ export function EventCreatorAdminPage() { } else { setShowSpinner(false); } - }, [id, t, form]); + }, [id, t]); // ================================== // // Creation Steps // // ================================== // - const [completedSteps, setCompletedSteps] = useState>({}); - - const createSteps: EventCreatorStep[] = [ - // Name and text descriptions. - { - key: 'text', - title_nb: 'Tittel/beskrivelse', - title_en: 'Text & description', - validate: (data) => { - return !!( - data.title_nb && - data.title_en && - data.description_short_nb && - data.description_short_en && - data.description_long_nb && - data.description_long_en - ); - }, - template: ( - <> -
    - ( - - - {t(KEY.common_title)} ({t(KEY.common_norwegian)}) - - - - - - - )} - /> - ( - - - {t(KEY.common_title)} ({t(KEY.common_english)}) - - - - - - - )} - /> -
    -
    - ( - - - {t(KEY.common_short_description)} ({t(KEY.common_norwegian)}) - - - - - - - )} - /> - ( - - - {t(KEY.common_short_description)} ({t(KEY.common_english)}) - - - - - - - )} - /> -
    -
    - ( - - - {t(KEY.common_long_description)} ({t(KEY.common_norwegian)}) - - -