diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss index 8369434ec..2c6eb12fd 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.module.scss @@ -4,6 +4,8 @@ @use 'src/constants' as *; +@use 'src/styles/colors' as c; + .header { font-size: 2em; font-weight: 700; @@ -87,3 +89,84 @@ padding-bottom: 3.75em; gap: 0.5em; } + +.template_search_wrapper { + flex: 0 0 50%; + width: 50%; + + @include for-tablet-down { + flex: 0 0 100%; + width: 100%; + } +} + +.create_from_existing_event { + position: relative; +} + +.search_results { + position: absolute; + top: calc(100% + 0.4rem); + width: 100%; + z-index: 20; + border: 1px solid c.$gray-200; + border-radius: 6px; + background: white; + max-height: 220px; + overflow-y: auto; + box-shadow: 0 4px 12px rgb(0 0 0 / 12%); +} + +.search_result_item { + width: 100%; + text-align: left; + padding: 0.75rem 1rem; + border: none; + background: white; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.search_result_item:not(:last-child) { + border-bottom: 1px solid c.$gray-100; +} + +.search_result_item:hover { + background: c.$white-950 +} + +.search_result_title { + font-weight: 600; +} + +.search_result_meta { + font-size: 0.9rem; + color: c.$gray-600; +} + +.selected_event_info { + margin-top: 0.4rem; + padding: 0.55rem 0.75rem; + border: 1px solid c.$green-100; + border-radius: 6px; + background: c.$green-200; + display: flex; + align-items: center; + gap: 0.5rem; + box-sizing: border-box; +} + +.selected_event_text { + font-size: 0.9rem; + color: c.$gray-950; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.selected_event_icon { + color: $green; + flex-shrink: 0; +} diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index e0706e0da..ce0f01c60 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -64,7 +64,14 @@ export function EventCreatorAdminPage() { }); const stepComponentMap: Record = { - text: , + text: ( + + ), info: , payment: , graphics: , diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/components/EventTemplateSearch.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/components/EventTemplateSearch.tsx new file mode 100644 index 000000000..25e7b6c60 --- /dev/null +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/components/EventTemplateSearch.tsx @@ -0,0 +1,84 @@ +import { Icon } from '@iconify/react'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { InputField } from '~/Components/InputField/InputField'; +import type { EventDto } from '~/dto'; +import { KEY } from '~/i18n/constants'; +import { dbT } from '~/utils'; +import styles from '../EventCreatorAdminPage.module.scss'; + +type EventTemplateSearchProp = { + events: EventDto[]; + onSelectEvent: (event: EventDto) => void; +}; + +export function EventTemplateSearch({ events, onSelectEvent }: EventTemplateSearchProp) { + const { t, i18n } = useTranslation(); + const [query, setQuery] = useState(''); + const [selectedEvent, SetSelectedEvent] = useState(null); + const filteredEvents = useMemo(() => { + const normalizedSearch = query.trim().toLowerCase(); + if (normalizedSearch === '') return []; + const keywords = normalizedSearch.split(/\s+/); + return events.filter((event) => { + const title = (dbT(event, 'title', i18n.language) as string)?.toLowerCase() ?? ''; + return keywords.every((kw) => title.includes(kw)); + }); + }, [events, query, i18n.language]); + const showResults = query.trim() !== '' && selectedEvent === null && filteredEvents.length > 0; + // TODO: add translations + + function handleChange(value: string) { + setQuery(value); + + const selectedTitle = selectedEvent ? (dbT(selectedEvent, 'title', i18n.language) as string) ?? '' : ''; + if (value !== selectedTitle) { + SetSelectedEvent(null); + } + } + + return ( +
+ + {selectedEvent && ( +
+ + + {t(KEY.event_selected_existing_event)}: {dbT(selectedEvent, 'title', i18n.language)}{' '} + {new Intl.DateTimeFormat('nb-NO', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }).format(new Date(selectedEvent.start_dt))} + +
+ )} + {showResults && ( +
+ {filteredEvents.map((e) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts b/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts index bc39fa4fd..49fbad9e4 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/hooks/useEventCreatorForm.ts @@ -5,8 +5,8 @@ import type { z } from 'zod'; import type { EventDto, EventWriteDto } from '~/dto'; import type { EventCategoryValue } from '~/types'; -import { utcTimestampToLocal } from '~/utils'; import { eventSchema } from '../EventCreatorSchema'; +import { mapEventToFormValues } from '../utils'; export type FormType = z.infer; @@ -54,30 +54,11 @@ export function useEventCreatorForm(params: { const resetValues = useMemo(() => { if (!event?.id) return undefined; - const duration = computeDurationMinutes(event.start_dt, event.end_dt); - - return { - title_nb: event.title_nb || '', - title_en: event.title_en || '', - description_long_nb: event.description_long_nb || '', - description_long_en: event.description_long_en || '', - description_short_nb: event.description_short_nb || '', - description_short_en: event.description_short_en || '', - start_dt: event.start_dt ? utcTimestampToLocal(event.start_dt, false) : '', - duration: duration || undefined, - end_dt: event.end_dt ? utcTimestampToLocal(event.end_dt, false) : '', - category: event.category || defaultCategory, - host: event.host || '', - location: event.location || defaultLocation, - capacity: event.capacity || undefined, - age_restriction: event.age_restriction || 'none', - ticket_type: event.ticket_type || 'free', - custom_tickets: event.custom_tickets || [], - billig_id: event.billig?.id, - image: event.image ?? undefined, - visibility_from_dt: event.visibility_from_dt ? utcTimestampToLocal(event.visibility_from_dt, false) : '', - visibility_to_dt: event.visibility_to_dt ? utcTimestampToLocal(event.visibility_to_dt, false) : '', - }; + return mapEventToFormValues({ + event, + defaultCategory, + defaultLocation, + }); }, [event, defaultCategory, defaultLocation]); useEffect(() => { diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/TextStep.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/TextStep.tsx index 7545f6e47..68c1efcac 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/TextStep.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/steps/TextStep.tsx @@ -1,15 +1,52 @@ +import { useQuery } from '@tanstack/react-query'; import type { UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { FormControl, FormField, FormItem, FormLabel, FormMessage, Input, Textarea } from '~/Components'; +import { getEvents } from '~/api'; +import type { EventDto } from '~/dto'; import { KEY } from '~/i18n/constants'; +import type { EventCategoryValue } from '~/types'; import styles from '../EventCreatorAdminPage.module.scss'; +import { EventTemplateSearch } from '../components/EventTemplateSearch'; import type { FormType } from '../hooks/useEventCreatorForm'; +import { mapEventToFormValues } from '../utils'; -export function TextStep({ form }: { form: UseFormReturn }) { +type Props = { + form: UseFormReturn; + defaultCategory: EventCategoryValue; + defaultLocation: string; + isEditMode: boolean; +}; + +export function TextStep({ form, defaultCategory, defaultLocation, isEditMode }: Props) { const { t } = useTranslation(); + const { data: events = [] } = useQuery({ + queryKey: ['events'], + queryFn: getEvents, + enabled: !isEditMode, + }); + + function handleSelectedEvent(selectedEvent: EventDto) { + form.reset( + mapEventToFormValues({ + event: selectedEvent, + defaultCategory, + defaultLocation, + forTemplate: true, + }), + ); + } return ( <> + {!isEditMode && ( +
+
+ + +
+
+ )}
; + defaultCategory: EventCategoryValue; + defaultLocation: string; + forTemplate?: boolean; +}): FormType { + const { event, defaultCategory, defaultLocation, forTemplate = false } = params; + + const duration = computeDurationMinutes(event.start_dt, event.end_dt); + + return { + title_nb: event.title_nb || '', + title_en: event.title_en || '', + description_long_nb: event.description_long_nb || '', + description_long_en: event.description_long_en || '', + description_short_nb: event.description_short_nb || '', + description_short_en: event.description_short_en || '', + + start_dt: forTemplate ? '' : event.start_dt ? utcTimestampToLocal(event.start_dt, false) : '', + duration: forTemplate ? undefined : duration || undefined, + end_dt: forTemplate ? '' : event.end_dt ? utcTimestampToLocal(event.end_dt, false) : '', + + category: event.category || defaultCategory, + host: event.host || '', + location: event.location || defaultLocation, + capacity: event.capacity || undefined, + age_restriction: event.age_restriction || 'none', + ticket_type: event.ticket_type || 'free', + custom_tickets: event.custom_tickets || [], + billig_id: event.billig?.id, + image: event.image ?? undefined, + + visibility_from_dt: forTemplate + ? '' + : event.visibility_from_dt + ? utcTimestampToLocal(event.visibility_from_dt, false) + : '', + visibility_to_dt: forTemplate + ? '' + : event.visibility_to_dt + ? utcTimestampToLocal(event.visibility_to_dt, false) + : '', + }; +} diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index bff73255a..419a9a882 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -290,6 +290,9 @@ export const KEY = { event_registration_url: 'event_registration_url', event_add_ticket: 'event_add_ticket', event_invalid_form_error: 'event_invalid_form_error', + event_create_from_existing_event: 'event_create_from_existing_event', + event_search_for_an_existing_event: 'event_search_for_an_existing_event', + event_selected_existing_event: 'event_selected_existing_event', // Purchase Ticket Info: invalid_email_message: 'invalid_email_message', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index b2824092c..d42698b47 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -280,6 +280,9 @@ export const nb = prepareTranslations({ [KEY.event_registration_url]: 'Registreringslenke', [KEY.event_add_ticket]: 'Legg til billett', [KEY.event_invalid_form_error]: 'Skjemaet inneholder valideringsfeil. Vennligst sjekk de uthevede feltene.', + [KEY.event_create_from_existing_event]: 'Opprett fra eksisterende arrangement', + [KEY.event_search_for_an_existing_event]: 'Søk etter et eksisterende arrangement', + [KEY.event_selected_existing_event]: 'Valgte eksisterende arrangement', // Event categories [KEY.event_category_art]: 'Kunst', @@ -1007,6 +1010,9 @@ export const en = prepareTranslations({ [KEY.event_registration_url]: 'Registration URL', [KEY.event_add_ticket]: 'Add ticket', [KEY.event_invalid_form_error]: 'Form contains validation errors. Please check highlighted fields.', + [KEY.event_create_from_existing_event]: 'Create from existing event', + [KEY.event_search_for_an_existing_event]: 'Search for an existing event', + [KEY.event_selected_existing_event]: 'Selected existing event', //Purchase Ticket Info: [KEY.invalid_email_message]: 'Invalid email format',