first commit
This commit is contained in:
72
src/features/event/api/__tests__/eventService.api.test.ts
Normal file
72
src/features/event/api/__tests__/eventService.api.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Deterministic base URL
|
||||
vi.mock('@/lib/config', () => ({ getWebhookUrl: () => 'https://api.test' }));
|
||||
|
||||
// apiFetch mock
|
||||
const apiFetchMock = vi.fn();
|
||||
vi.mock('@/utils/apiFetch', () => ({
|
||||
apiFetch: (...args: any[]) => apiFetchMock(...args),
|
||||
}));
|
||||
|
||||
import { fetchEvents, fetchCurrentEvents, createEventFromUrl } from '@/features/event/api/eventService';
|
||||
|
||||
function okJson(data: any) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
function status(code: number) {
|
||||
return new Response('', { status: code });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
apiFetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe('eventService API', () => {
|
||||
it('fetchEvents returns array data', async () => {
|
||||
apiFetchMock.mockResolvedValueOnce(okJson([{ id: 'e1' }, { id: 'e2' }]));
|
||||
|
||||
const list = await fetchEvents(async (...args: any[]) => apiFetchMock(...args));
|
||||
|
||||
expect(apiFetchMock).toHaveBeenCalledWith('https://api.test/events', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
expect(Array.isArray(list)).toBe(true);
|
||||
expect(list).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('fetchCurrentEvents returns normalized array when API returns single object', async () => {
|
||||
apiFetchMock.mockResolvedValueOnce(okJson({ id: 'single' }));
|
||||
|
||||
const list = await fetchCurrentEvents(async (...args: any[]) => apiFetchMock(...args));
|
||||
|
||||
expect(apiFetchMock).toHaveBeenCalledWith('https://api.test/events/get_current', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
expect(Array.isArray(list)).toBe(true);
|
||||
expect(list).toHaveLength(1);
|
||||
expect(list[0]).toMatchObject({ id: 'single' });
|
||||
});
|
||||
|
||||
it('createEventFromUrl posts URL and returns parsed data.message_from_ai when present', async () => {
|
||||
apiFetchMock.mockResolvedValueOnce(okJson({ message_from_ai: { name: 'AI Event' } }));
|
||||
|
||||
const result = await createEventFromUrl('https://example.com');
|
||||
|
||||
const [url, init] = apiFetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe('https://api.test/event/new_fromurl');
|
||||
expect((init?.headers as any)['Content-Type']).toBe('application/json');
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ url: 'https://example.com', command: 'eventmodus_extract_from_url' });
|
||||
expect(result).toEqual({ name: 'AI Event' });
|
||||
});
|
||||
|
||||
it('fetchEvents throws on non-ok response', async () => {
|
||||
apiFetchMock.mockResolvedValueOnce(status(500));
|
||||
await expect(fetchEvents(async (...args: any[]) => apiFetchMock(...args))).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
13
src/features/event/api/__tests__/eventService.test.ts
Normal file
13
src/features/event/api/__tests__/eventService.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatDateForInput } from '@/features/event/api/eventService';
|
||||
|
||||
describe('eventService.formatDateForInput', () => {
|
||||
it('formats a valid ISO date to YYYY-MM-DD', () => {
|
||||
expect(formatDateForInput('2024-12-31T10:20:30Z')).toBe('2024-12-31');
|
||||
expect(formatDateForInput('2024-01-05')).toBe('2024-01-05');
|
||||
});
|
||||
|
||||
it('returns empty string for falsy input', () => {
|
||||
expect(formatDateForInput('')).toBe('');
|
||||
});
|
||||
});
|
||||
435
src/features/event/api/eventService.ts
Normal file
435
src/features/event/api/eventService.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
import { getWebhookUrl } from '@/lib/config';
|
||||
|
||||
export interface Event {
|
||||
id?: string | number;
|
||||
user_id?: number;
|
||||
name?: string;
|
||||
title?: string; // neuer Titel-Name vom Timeline-Endpoint
|
||||
description?: string;
|
||||
location?: string;
|
||||
url?: string;
|
||||
image?: string;
|
||||
manager_name?: string;
|
||||
manager_email?: string;
|
||||
guests?: any;
|
||||
begin_date?: string; // ISO date string (YYYY-MM-DD)
|
||||
end_date?: string; // ISO date string (YYYY-MM-DD)
|
||||
eventDate?: string; // ISO date string (YYYY-MM-DD) vom Timeline-Endpoint (camelCase)
|
||||
event_date?: string; // ISO date string (YYYY-MM-DD) direkt aus SQL-Query
|
||||
status?: 'PENDING' | 'ON_AIR'; // aggregierter Status für Timeline
|
||||
overall_status?: 'PENDING' | 'ON_AIR'; // Alias-Name aus SQL-Query
|
||||
creation_date?: number;
|
||||
last_update?: number;
|
||||
}
|
||||
|
||||
// Payload-Struktur für den neuen Event-Endpunkt
|
||||
interface CreateEventPayload {
|
||||
title: string;
|
||||
description?: string;
|
||||
eventDate: string; // YYYY-MM-DD
|
||||
promotionStartDate?: string; // YYYY-MM-DD
|
||||
coverMediaUrl?: string;
|
||||
ticketUrl?: string;
|
||||
admissionTimes?: string;
|
||||
ticketPrice?: string;
|
||||
promotionSlots: Array<{
|
||||
slotIndex: number;
|
||||
slotType: 'EVENT_MAIN' | 'ARTIST';
|
||||
promoDate: string; // YYYY-MM-DD
|
||||
artistName?: string;
|
||||
artistInstagram?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface EventDetail extends CreateEventPayload {
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
export const createEvent = async (payload: CreateEventPayload): Promise<any> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/events/new`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error creating event manually:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ArtistCollabUpdateByOpPayload {
|
||||
eventId: string | number;
|
||||
slotId?: string | number;
|
||||
slotIndex?: number;
|
||||
artistDescription?: string;
|
||||
artistInstagram?: string;
|
||||
artistFacebook?: string;
|
||||
artistLinkedin?: string;
|
||||
artistTiktok?: string;
|
||||
artistYoutube?: string;
|
||||
}
|
||||
|
||||
export const updateArtistCollabDataByOp = async (
|
||||
payload: ArtistCollabUpdateByOpPayload
|
||||
): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/events/update_artist_data_byop`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const raw = await response.json();
|
||||
const data = Array.isArray(raw) ? raw[0] ?? {} : raw ?? {};
|
||||
|
||||
return {
|
||||
success: data.success !== false,
|
||||
message: data.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating artist collab data by op:', error);
|
||||
return { success: false, message: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
};
|
||||
|
||||
export const getEventInfoFromUrl = async (url: string): Promise<any> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/events/getinfofromurl`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error getting event info from URL:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchCurrentEvents = async (apiFetch: Function): Promise<Event[]> => {
|
||||
|
||||
const webhookUrl = `${getWebhookUrl()}/events/get_current`;
|
||||
|
||||
try {
|
||||
const response = await apiFetch(webhookUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch events');
|
||||
|
||||
const raw = await response.json();
|
||||
|
||||
// n8n kann die Liste ggf. als String-JSON zurückgeben
|
||||
const data =
|
||||
typeof raw === 'string'
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
})()
|
||||
: raw;
|
||||
|
||||
return Array.isArray(data) ? data : data ? [data] : [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ArtistCollabUpdatePayload {
|
||||
token: string;
|
||||
slotId?: string | number;
|
||||
eventId?: string | number;
|
||||
artistDescription?: string;
|
||||
artistInstagram?: string;
|
||||
artistFacebook?: string;
|
||||
artistLinkedin?: string;
|
||||
artistTiktok?: string;
|
||||
artistYoutube?: string;
|
||||
}
|
||||
|
||||
export const updateArtistCollabData = async (
|
||||
payload: ArtistCollabUpdatePayload
|
||||
): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/events/update_artist_data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const raw = await response.json();
|
||||
const data = Array.isArray(raw) ? raw[0] ?? {} : raw ?? {};
|
||||
|
||||
return {
|
||||
success: data.success !== false,
|
||||
message: data.message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating artist collab data:', error);
|
||||
return { success: false, message: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchEventById = async (
|
||||
apiFetch: Function,
|
||||
id: string | number
|
||||
): Promise<EventDetail> => {
|
||||
const webhookUrl = `${getWebhookUrl()}/events/getbyid?id=${id}`;
|
||||
|
||||
const response = await apiFetch(webhookUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch event');
|
||||
|
||||
const raw = await response.json();
|
||||
|
||||
// n8n kann entweder ein Objekt, ein Array mit einem Objekt
|
||||
// oder einen JSON-String zurückgeben – hier alles abfangen.
|
||||
const data =
|
||||
typeof raw === 'string'
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
})()
|
||||
: raw;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return (data[0] || {}) as EventDetail;
|
||||
}
|
||||
|
||||
return data as EventDetail;
|
||||
};
|
||||
|
||||
export const updateEvent = async (
|
||||
id: string | number,
|
||||
payload: CreateEventPayload
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/events/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id, ...payload }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error updating event:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchEvents = async (apiFetch: Function): Promise<Event[]> => {
|
||||
const webhookUrl = `${getWebhookUrl()}/events`;
|
||||
|
||||
try {
|
||||
const response = await apiFetch(webhookUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch events');
|
||||
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : data ? [data] : [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const createEventFromUrl = async (url: string): Promise<any> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/event/new_fromurl`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
command: 'eventmodus_extract_from_url'
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.message_from_ai || data;
|
||||
} catch (error) {
|
||||
console.error('Error creating event from URL:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to format dates to YYYY-MM-DD
|
||||
export const formatDateForInput = (dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
export interface CollabLinkResponse {
|
||||
token: string;
|
||||
url?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const generateCollabLink = async (
|
||||
eventId: string | number,
|
||||
slotIndex: number,
|
||||
artistName?: string,
|
||||
artistInstagram?: string
|
||||
): Promise<CollabLinkResponse> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/events/generate_collab_link`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ eventId, slotIndex, artistName, artistInstagram }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as CollabLinkResponse;
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error generating collaboration link:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface CollabMediaItem {
|
||||
id: string;
|
||||
url: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface CollabInfo {
|
||||
eventTitle: string;
|
||||
promoDate?: string;
|
||||
artistName?: string;
|
||||
artistDescription?: string;
|
||||
artistMediaUrl?: string;
|
||||
artistInstagram?: string;
|
||||
slotId?: string | number;
|
||||
eventId?: string | number;
|
||||
media?: CollabMediaItem[];
|
||||
}
|
||||
|
||||
export const fetchArtistSlotByToken = async (token: string): Promise<CollabInfo | null> => {
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(`${getWebhookUrl()}/events/slots/getartistsslot`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
||||
// Token ungültig, abgelaufen oder Slot nicht gefunden
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const raw = await response.json();
|
||||
|
||||
const data: any = (() => {
|
||||
if (!raw) return null;
|
||||
if (Array.isArray(raw)) return raw[0] ?? null;
|
||||
if (typeof raw === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed[0] ?? null : parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return raw as any;
|
||||
})();
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return {
|
||||
eventTitle: data.eventTitle ?? data.event_title ?? data.title ?? '',
|
||||
promoDate: data.promoDate ?? data.promo_date,
|
||||
artistName: data.artistName ?? data.artist_name,
|
||||
artistDescription: data.artistDescription ?? data.artist_description,
|
||||
artistMediaUrl: data.artistMediaUrl ?? data.artist_media_url,
|
||||
artistInstagram: data.artistInstagram ?? data.artist_instagram,
|
||||
slotId: data.id ?? data.slot_id,
|
||||
eventId: data.event_id ?? data.eventId,
|
||||
media: Array.isArray(data.media)
|
||||
? data.media.map((m: any) => ({
|
||||
id: String(m.id),
|
||||
url: m.url || m.file_url,
|
||||
type: m.type || m.file_type,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching artist slot by token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
51
src/features/event/api/getEventMedia.ts
Normal file
51
src/features/event/api/getEventMedia.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { getWebhookUrl } from '@/lib/config';
|
||||
|
||||
export interface EventMediaItem {
|
||||
id: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const getEventMedia = async (eventId?: string): Promise<EventMediaItem[]> => {
|
||||
if (!eventId) {
|
||||
return [];
|
||||
}
|
||||
const endpoint = `${getWebhookUrl()}/event/get_event_media?event_id=${encodeURIComponent(eventId)}`;
|
||||
try {
|
||||
const { apiFetch } = await import('@/utils/apiFetch');
|
||||
const response = await apiFetch(endpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Fehler beim Laden der Event Bilder!");
|
||||
}
|
||||
const data = await response.json();
|
||||
let normalized: EventMediaItem[] = [];
|
||||
if (
|
||||
Array.isArray(data) &&
|
||||
data.length > 0 &&
|
||||
typeof data[0].output === "string"
|
||||
) {
|
||||
try {
|
||||
const outputArr = JSON.parse(data[0].output);
|
||||
normalized = outputArr.map((item: any, idx: number) => ({
|
||||
id: item.id?.toString() || `remote-${idx}`,
|
||||
url: item.url || item.file_url,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Parsen von data[0].output:", e);
|
||||
}
|
||||
} else if (Array.isArray(data)) {
|
||||
normalized = data.map((item: any, idx: number) => ({
|
||||
id: item.id?.toString() || `remote-${idx}`,
|
||||
url: item.url || item.file_url,
|
||||
}));
|
||||
}
|
||||
return normalized;
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Laden der Event Bilder", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user