Skip to content
Merged
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
47 changes: 47 additions & 0 deletions frontend/app/api/events/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Route: GET /api/events/:id
* Purpose: Return one event by its identifier.
* Behavior:
* - Validates Supabase service-role credentials.
* - Reads the id from route params and fetches a single record.
* - Returns 404 when the event is not found.
*/
import { NextRequest, NextResponse } from "next/server";
import { fetchEventById } from "../service";
import {
getSupabaseCredentials,
hasMissingServiceCredentials,
} from "../../utils/supabase";

type RouteContext = {
params: Promise<{
id: string;
}>;
};

export async function GET(_request: NextRequest, context: RouteContext) {
try {
const params = await context.params;
const credentials = getSupabaseCredentials();

if (hasMissingServiceCredentials(credentials)) {
return NextResponse.json(
{ error: "Supabase credentials missing" },
{ status: 500 },
);
}

const result = await fetchEventById(credentials, params.id);
if (!result.ok) {
Comment thread
kr1shap marked this conversation as resolved.
return NextResponse.json(
{ error: result.error },
{ status: result.status },
);
}

return NextResponse.json({ event: result.event });
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}
40 changes: 40 additions & 0 deletions frontend/app/api/events/past/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Route: GET /api/events/past
* Purpose: Return past events only.
* Behavior:
* - Validates Supabase anon credentials.
* - Proxies to Supabase Edge Function events/past.
* - Returns normalized events array with optional debug/error metadata.
*/
import { NextResponse } from "next/server";
import { fetchPastEvents } from "../service";
import {
getSupabaseCredentials,
hasMissingAnonCredentials,
} from "../../utils/supabase";

export async function GET() {
try {
const credentials = getSupabaseCredentials();

if (hasMissingAnonCredentials(credentials)) {
return NextResponse.json(
{ events: [], error: "Supabase credentials missing" },
{ status: 500 },
);
}

const result = await fetchPastEvents(credentials);
return NextResponse.json(
{
events: result.events || [],
// debug: result.debug,
error: result.error,
},
{ status: result.ok ? 200 : result.status },
);
Comment thread
kr1shap marked this conversation as resolved.
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json({ events: [], error: message }, { status: 500 });
}
}
54 changes: 54 additions & 0 deletions frontend/app/api/events/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Route: GET /api/events
* Purpose: Return both upcoming and past events in a single response.
* Behavior:
* - Validates required Supabase anon credentials.
* - Fetches upcoming and past datasets in parallel.
* - Returns combined payload with optional debug/error metadata.
*/
import { NextResponse } from "next/server";
import { fetchPastEvents, fetchUpcomingEvents } from "./service";
import {
getSupabaseCredentials,
hasMissingAnonCredentials,
} from "../utils/supabase";

export async function GET() {
try {
const credentials = getSupabaseCredentials();

if (hasMissingAnonCredentials(credentials)) {
return NextResponse.json(
{ upcoming: [], past: [], error: "Supabase credentials missing" },
{ status: 500 },
);
}

const [upcomingResult, pastResult] = await Promise.all([
fetchUpcomingEvents(credentials),
fetchPastEvents(credentials),
]);

const hasError = !upcomingResult.ok || !pastResult.ok;
return NextResponse.json(
{
upcoming: upcomingResult.events || [],
past: pastResult.events || [],
// upcomingDebug: upcomingResult.debug,
// pastDebug: pastResult.debug,
error: upcomingResult.error || pastResult.error,
},
{ status: hasError ? 500 : 200 },
);
Comment thread
kr1shap marked this conversation as resolved.
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json(
{
upcoming: [],
past: [],
error: message,
},
{ status: 500 },
);
}
}
117 changes: 117 additions & 0 deletions frontend/app/api/events/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Events service layer.
*
* Purpose:
* - Centralize all event data-access calls used by API routes.
* - Keep route files focused on HTTP orchestration and status mapping.
*
* Notes:
* - This service intentionally uses Supabase Edge Functions for event reads
* (`events/upcoming`, `events/past`, `events/:id`) to keep behavior consistent
* with backend function logic.
* - Response parsing is defensive to support minor payload-shape differences
* between edge function handlers.
*/
import {
requestSupabaseEdgeFunction,
type SupabaseCredentials,
} from "../utils/supabase";

type EventsTimeline = "upcoming" | "past";

/**
* Fetches a single event by id through the Edge Function route.
*
* Expected successful payloads supported:
* - `{ event: {...} }`
* - `{ events: [{...}] }`
* - direct object payload
*
* Returns a normalized result with `ok`, `status`, and either `event` or `error`.
*/
export async function fetchEventById(
credentials: SupabaseCredentials,
eventId: string,
) {
const encodedId = encodeURIComponent(eventId);
const { response, body } = await requestSupabaseEdgeFunction(
credentials.url,
`events/${encodedId}`,
credentials.anonKey,
{ method: "GET" },
);

if (!response.ok) {
return {
ok: false as const,
status: response.status,
error:
(body as { error?: string; message?: string })?.error ||
(body as { error?: string; message?: string })?.message ||
"Failed to fetch event",
};
}

const maybeObject = body as {
event?: unknown;
events?: unknown[];
};

const event =
maybeObject?.event ||
(Array.isArray(maybeObject?.events) ? maybeObject.events[0] : null) ||
(Array.isArray(body) ? body[0] : body);

if (!event) {
return {
ok: false as const,
status: 404,
error: "Event not found",
};
}

return {
ok: true as const,
event,
};
}

/**
* Fetches events from one timeline bucket (`upcoming` or `past`) through
* the Edge Function route and returns a normalized response contract.
*/
export async function fetchEventsByTimeline(
credentials: SupabaseCredentials,
timeline: EventsTimeline,
) {
const { response, body } = await requestSupabaseEdgeFunction(
credentials.url,
`events/${timeline}`,
credentials.anonKey,
);

return {
ok: response.ok,
status: response.status,
events: body.events || [],
debug: {
status: response.status,
body,
},
error: (body as { error?: string })?.error,
};
}

/**
* Convenience wrapper for fetching upcoming events.
*/
export async function fetchUpcomingEvents(credentials: SupabaseCredentials) {
return fetchEventsByTimeline(credentials, "upcoming");
}

/**
* Convenience wrapper for fetching past events.
*/
export async function fetchPastEvents(credentials: SupabaseCredentials) {
return fetchEventsByTimeline(credentials, "past");
}
40 changes: 40 additions & 0 deletions frontend/app/api/events/upcoming/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Route: GET /api/events/upcoming
* Purpose: Return upcoming events only.
* Behavior:
* - Validates Supabase anon credentials.
* - Proxies to Supabase Edge Function events/upcoming.
* - Returns normalized events array with optional debug/error metadata.
*/
import { NextResponse } from "next/server";
import { fetchUpcomingEvents } from "../service";
import {
getSupabaseCredentials,
hasMissingAnonCredentials,
} from "../../utils/supabase";

export async function GET() {
try {
const credentials = getSupabaseCredentials();

if (hasMissingAnonCredentials(credentials)) {
return NextResponse.json(
{ events: [], error: "Supabase credentials missing" },
{ status: 500 },
);
}

const result = await fetchUpcomingEvents(credentials);
return NextResponse.json(
{
events: result.events || [],
// debug: result.debug,
error: result.error,
},
{ status: result.ok ? 200 : result.status },
);
Comment thread
kr1shap marked this conversation as resolved.
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json({ events: [], error: message }, { status: 500 });
}
}
90 changes: 90 additions & 0 deletions frontend/app/api/utils/supabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
export type SupabaseCredentials = {
url: string;
serviceKey: string;
anonKey: string;
};

export type EdgeRequestOptions = {
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
body?: unknown;
};

export function getSupabaseCredentials(): SupabaseCredentials {
return {
url: process.env.SUPABASE_URL || "",
serviceKey: process.env.SUPABASE_SERVICE_ROLE_KEY || "",
anonKey: process.env.SUPABASE_ANON_KEY || "",
};
}

export function hasMissingAnonCredentials(
credentials: SupabaseCredentials,
): boolean {
return !credentials.url || !credentials.anonKey;
}

export function hasMissingServiceCredentials(
credentials: SupabaseCredentials,
): boolean {
return !credentials.url || !credentials.serviceKey;
}

function getSupabaseAnonHeaders(anonKey: string) {
return {
Authorization: `Bearer ${anonKey}`,
"Content-Type": "application/json",
};
}

function getSupabaseServiceHeaders(serviceKey: string) {
return {
Authorization: `Bearer ${serviceKey}`,
apikey: serviceKey,
"Content-Type": "application/json",
};
}

/**
* Core network helper for Supabase Edge Functions.
* Use this for calls to /functions/v1/* endpoints with anon-key authorization.
* Returns the raw response plus a safely parsed JSON payload (falls back to {}).
*/
export async function requestSupabaseEdgeFunction(
baseUrl: string,
path: string,
anonKey: string,
options: EdgeRequestOptions = {},
) {
const { method = "GET", body } = options;

const response = await fetch(`${baseUrl}/functions/v1/${path}`, {
method,
headers: getSupabaseAnonHeaders(anonKey),
body: body !== undefined ? JSON.stringify(body) : undefined,
});

const payload = await response.json().catch(() => ({}));
return { response, body: payload };
}

/**
* Core network helper for Supabase REST API.
* Use this for calls to /rest/v1/* endpoints with service-role authorization.
* Returns the raw response plus a safely parsed JSON payload (falls back to []).
*/
export async function requestSupabaseRest(
credentials: SupabaseCredentials,
path: string,
options: EdgeRequestOptions = {},
) {
const { method = "GET", body } = options;

const response = await fetch(`${credentials.url}/rest/v1/${path}`, {
method,
headers: getSupabaseServiceHeaders(credentials.serviceKey),
body: body !== undefined ? JSON.stringify(body) : undefined,
});

const payload = await response.json().catch(() => []);
return { response, body: payload };
}
Loading