Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { dbT, lowerCapitalize, utcTimestampToLocal } from '~/utils';
import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout';
import styles from './EventCreatorAdminPage.module.scss';
import { eventSchema } from './EventCreatorSchema';
import { CustomTicketEditor } from './components/CustomTicketEditor';

// Define the Zod schema for event validation

Expand Down Expand Up @@ -411,14 +412,10 @@ export function EventCreatorAdminPage() {
</FormItem>
)}
/>
{/* <PaymentForm
event={form.getValues()}
onChange={(partial) => {
// Update form values with payment data
const updatedValues = { ...form.getValues(), ...partial };
form.reset(updatedValues);
}}
/> */}
<div>
TODO: add Billig integration, registration and actuall form implemtentation of CustomTicketEditor
<CustomTicketEditor />
</div>
</>
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Icon } from '@iconify/react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, IconButton, InputField } from '~/Components';
import { Table, type TableRow } from '~/Components/Table';
Expand All @@ -8,73 +8,97 @@ import { KEY } from '~/i18n/constants';
import { COLORS } from '~/types';
import styles from './CustomTicketEditor.module.scss';

type PaymentFormProps = {
customTickets?: EventCustomTicketDto[];
onSetCustomTickets?(customTickets: EventCustomTicketDto[]): void;
type CustomTicketEditorProps = {
initialTickets?: EventCustomTicketDto[];
onTicketsChange?: (tickets: EventCustomTicketDto[]) => void;
};

export function CustomTicketEditor({ customTickets = [], onSetCustomTickets }: PaymentFormProps) {
// Default values for new tickets
const DEFAULT_TICKET_PRICE = 100;
const DEFAULT_FIRST_TICKET = {
id: 0,
name_nb: 'Billett 1',
name_en: 'Ticket 1',
price: DEFAULT_TICKET_PRICE,
};

export function CustomTicketEditor({ initialTickets = [], onTicketsChange }: CustomTicketEditorProps) {
// Internal state to manage tickets - makes the component more self-contained
const [tickets, setTickets] = useState<EventCustomTicketDto[]>(initialTickets);
const { t } = useTranslation();

// Add default custom ticket
// Handle initialization and sync with parent
useEffect(() => {
if (customTickets.length === 0) {
onSetCustomTickets?.([{ id: 0, name_nb: 'Billett 1', name_en: 'Ticket 1', price: 100 }]);
// If no initial tickets, create default ticket
if (initialTickets.length === 0 && tickets.length === 0) {
const defaultTickets = [DEFAULT_FIRST_TICKET];
setTickets(defaultTickets);
onTicketsChange?.(defaultTickets);
}
// If parent passes new initialTickets, update internal state
else if (initialTickets.length > 0 && JSON.stringify(initialTickets) !== JSON.stringify(tickets)) {
setTickets(initialTickets);
}
}, [onSetCustomTickets, customTickets]);
}, [initialTickets, tickets, onTicketsChange]);

// Functions to add/edit/remove
function removeTicket(ticket: EventCustomTicketDto) {
onSetCustomTickets?.(customTickets.filter((t) => t.id !== ticket.id));
}
// Update both local state and notify parent
const updateTicketsState = (newTickets: EventCustomTicketDto[]) => {
setTickets(newTickets);
onTicketsChange?.(newTickets);
};

function newTicket() {
const lastTicket = customTickets[customTickets.length - 1];
const nextId = lastTicket.id + 1;
onSetCustomTickets?.([
...customTickets,
{ id: nextId, name_nb: 'Ny billett', name_en: 'New ticket', price: lastTicket.price },
]);
}
// Generate unique ID for new ticket
const generateUniqueId = (): number => {
if (tickets.length === 0) return 0;
// Find max ID and add 1 to ensure uniqueness
return Math.max(...tickets.map((ticket) => ticket.id)) + 1;
};

// Add a new ticket to the list
const addTicket = () => {
const nextId = generateUniqueId();
const defaultPrice = tickets.length > 0 ? tickets[tickets.length - 1].price : DEFAULT_TICKET_PRICE;

const newTicket: EventCustomTicketDto = {
id: nextId,
name_nb: `Ny billett ${nextId + 1}`,
name_en: `New ticket ${nextId + 1}`,
price: defaultPrice,
};

updateTicketsState([...tickets, newTicket]);
};

// Remove a ticket from the list
const removeTicket = (ticketId: number) => {
updateTicketsState(tickets.filter((ticket) => ticket.id !== ticketId));
};

function updateTicket(id: number, edit: Partial<EventCustomTicketDto>) {
onSetCustomTickets?.(
customTickets.map((current: EventCustomTicketDto) => {
if (current.id !== id) return current;
return {
...current,
...edit,
id: id,
};
// Update fields for a specific ticket
const updateTicket = (id: number, updates: Partial<EventCustomTicketDto>) => {
updateTicketsState(
tickets.map((ticket) => {
if (ticket.id !== id) return ticket;
return { ...ticket, ...updates };
}),
);
}
};

// Render a row for a ticket in the table
const renderTicketRow = (ticket: EventCustomTicketDto): TableRow => {
// Only show delete button if there's more than one ticket
const canDelete = tickets.length > 1;

/** UI for a custom ticket */
function ticketRow(custom_ticket: EventCustomTicketDto): TableRow {
const deleteButton =
customTickets.length > 1
? {
content: (
<IconButton
color={COLORS.red}
onClick={() => removeTicket(custom_ticket)}
title=""
icon="mdi:bin"
className={styles.delete_button}
/>
),
}
: {};
return {
cells: [
{
content: (
<InputField<string>
inputClassName={styles.custom_ticket_input}
labelClassName={styles.custom_ticket_input_label}
value={custom_ticket.name_nb}
onChange={(name) => updateTicket(custom_ticket.id, { name_nb: name })}
value={ticket.name_en || ''}
onChange={(name) => updateTicket(ticket.id, { name_en: name })}
placeholder={t(KEY.common_english) || 'English'}
/>
),
},
Expand All @@ -83,8 +107,9 @@ export function CustomTicketEditor({ customTickets = [], onSetCustomTickets }: P
<InputField<string>
inputClassName={styles.custom_ticket_input}
labelClassName={styles.custom_ticket_input_label}
value={custom_ticket.name_en}
onChange={(name) => updateTicket(custom_ticket.id, { name_en: name })}
value={ticket.name_nb || ''}
onChange={(name) => updateTicket(ticket.id, { name_nb: name })}
placeholder={t(KEY.common_norwegian) || 'Norwegian'}
/>
),
},
Expand All @@ -93,35 +118,62 @@ export function CustomTicketEditor({ customTickets = [], onSetCustomTickets }: P
<InputField<number>
inputClassName={styles.custom_ticket_input}
labelClassName={styles.custom_ticket_input_label}
value={custom_ticket.price?.toString()}
value={ticket.price?.toString() || '0'}
type="number"
onChange={(price) => updateTicket(custom_ticket.id, { price: price })}
onChange={(price) => updateTicket(ticket.id, { price: Number(price) || 0 })}
placeholder="0"
/>
),
},
deleteButton,
canDelete
? {
content: (
<IconButton
color={COLORS.red}
onClick={() => removeTicket(ticket.id)}
title={t(KEY.common_delete) || 'Delete'}
icon="mdi:bin"
className={styles.delete_button}
/>
),
}
: {},
],
};
}
};

// Table column headers
const tableColumns = [
`${t(KEY.common_name)} (${t(KEY.common_english)})`,
`${t(KEY.common_name)} (${t(KEY.common_norwegian)})`,
'Pris',
'',
t(KEY.common_name) ? `${t(KEY.common_name)} (${t(KEY.common_english)})` : 'Name (English)',
t(KEY.common_name) ? `${t(KEY.common_name)} (${t(KEY.common_norwegian)})` : 'Name (Norwegian)',
t(KEY.common_price) || 'Price',
'', // Column for delete button
];

return (
<>
// If there are no tickets, create a default one
if (tickets.length === 0) {
return (
<div className={styles.custom_ticket_container}>
<Table columns={tableColumns} data={customTickets.map((ticket) => ticketRow(ticket))} />
<div className={styles.add_custom_ticket}>
<Button rounded={true} theme="green" preventDefault={true} onClick={newTicket}>
Legg til billett
<Icon icon="mdi:plus" />
</Button>
</div>
<div className={styles.loading}>{t(KEY.common_loading) || 'Loading...'}</div>
</div>
);
}

return (
<div className={styles.custom_ticket_container}>
<Table columns={tableColumns} data={tickets.map(renderTicketRow)} />
<div className={styles.add_custom_ticket}>
<Button
rounded={true}
theme="green"
preventDefault={true}
onClick={addTicket}
disabled={tickets.length >= 20} // Optional: prevent too many tickets
>
{'Add ticket'}
<Icon icon="mdi:plus" />
</Button>
</div>
</>
</div>
);
}

This file was deleted.