diff --git a/.env.example b/.env.example index a9e706e..9a1a680 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,4 @@ VITE_CLERK_PUBLISHABLE_KEY=pk_test_ZXRlcm5hbC1rcmlsbC03Ny5jbGVyay5hY2NvdW50cy5kZ # VITE_SITE_URL=https://example.com # Backend API base URL for portal/auth/application endpoints -# VITE_API_BASE_URL=http://localhost:8080 +VITE_API_BASE_URL=https://forestial-afocal-rex.ngrok-free.dev diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 931339c..7f55a5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 20 cache: pnpm - name: Install dependencies @@ -56,11 +56,12 @@ jobs: needs: lint env: - # Expose required VITE_ secrets as env vars so Vite can embed them + # All VITE_ secrets must be set in GitHub repo → Settings → Secrets → Actions + VITE_GOOGLE_CLIENT_ID: ${{ secrets.VITE_GOOGLE_CLIENT_ID }} + VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} VITE_EMAILJS_SERVICE_ID: ${{ secrets.VITE_EMAILJS_SERVICE_ID }} VITE_EMAILJS_TEMPLATE_ID: ${{ secrets.VITE_EMAILJS_TEMPLATE_ID }} VITE_EMAILJS_PUBLIC_KEY: ${{ secrets.VITE_EMAILJS_PUBLIC_KEY }} - VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_CLERK_PUBLISHABLE_KEY }} steps: - name: Checkout repository @@ -75,7 +76,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 20 cache: pnpm - name: Install dependencies @@ -92,56 +93,25 @@ jobs: retention-days: 7 # ───────────────────────────────────────────── - # Job 3 – Deploy to Vercel (main/master only) + # Job 3 – Summary (cPanel deploy is manual) + # Upload the dist artifact from the build job, + # then download it locally and upload to cPanel. # ───────────────────────────────────────────── - deploy: - name: 🚀 Deploy to Vercel + summary: + name: ✅ Build Ready for cPanel runs-on: ubuntu-latest needs: build if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') - environment: - name: production - url: ${{ steps.deploy.outputs.url }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - name: Download dist artifact + uses: actions/download-artifact@v4 with: - version: 10 - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Install Vercel CLI - run: pnpm add -g vercel@latest - - - name: Pull Vercel environment - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - - - name: Build via Vercel - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + name: dist + path: dist/ - - name: Deploy to Vercel - id: deploy + - name: Confirm build output run: | - url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}) - echo "url=$url" >> $GITHUB_OUTPUT - - - name: Comment deployment URL on PR (if applicable) - if: github.event.pull_request.number != '' - uses: actions/github-script@v7 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `✅ Deployed to production: ${{ steps.deploy.outputs.url }}` - }) + echo "✅ Build complete. dist/ is ready for cPanel upload." + echo "Files in dist/:" + ls -lah dist/ diff --git a/index.html b/index.html index 3fa2876..d059c78 100644 --- a/index.html +++ b/index.html @@ -1,31 +1,55 @@ - + Knowledge Center - - + + - - + + - + - - - - + + + + - - - - + + + + diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx index 6ee312c..e063600 100644 --- a/src/components/Contact.tsx +++ b/src/components/Contact.tsx @@ -86,7 +86,9 @@ const Contact: React.FC = () => { message: formData.message, reply_to: formData.email, }, - emailjsPublicKey + { + publicKey: emailjsPublicKey, + } ); if (res.status === 200) { @@ -99,17 +101,30 @@ const Contact: React.FC = () => { } else { throw new Error(res.text || "Unknown error"); } - } catch { - toast({ - title: "Open your email app", - description: "The form could not send directly, so we will open your email client instead.", - }); + } catch (err: any) { + console.error("[KC] EmailJS send failed:", err); + + // Copy email to clipboard as a helpful fallback for users without a local mail client + try { + await navigator.clipboard.writeText(supportEmail); + toast({ + title: "Form submission failed", + description: `We've copied our email (${supportEmail}) to your clipboard so you can write to us directly.`, + variant: "destructive", + }); + } catch { + toast({ + title: "Form submission failed", + description: `Please write to us directly at ${supportEmail}.`, + variant: "destructive", + }); + } + openMailClient(); } finally { setIsSubmitting(false); } }; - const contactInfo = [ { icon: MapPin, diff --git a/src/contexts/GspAuthContext.tsx b/src/contexts/GspAuthContext.tsx index e78e578..a2fcb73 100644 --- a/src/contexts/GspAuthContext.tsx +++ b/src/contexts/GspAuthContext.tsx @@ -5,9 +5,8 @@ import { getCurrentUser, hasAuthToken, loginGsp, - saveAuthToken, + persistAuthTokens, } from "@/services/gspApi"; -import { getToken } from "@/services/gspApi"; type AuthContextValue = { user: GspUser | null; @@ -50,7 +49,7 @@ export const GspAuthProvider: React.FC<{ children: React.ReactNode }> = ({ child const signIn = async (email: string, password: string) => { const data = await loginGsp({ email, password }); - saveAuthToken(data.token); + persistAuthTokens(data); setUser(data.user); }; diff --git a/src/main.tsx b/src/main.tsx index 3dab689..05bd535 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,9 +4,12 @@ import './index.css' import ErrorBoundary from './ErrorBoundary' import { GoogleOAuthProvider } from "@react-oauth/google" -const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID; +const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID || ""; if (!googleClientId) { - throw new Error("VITE_GOOGLE_CLIENT_ID is not set in environment variables"); + console.warn( + "[KC] VITE_GOOGLE_CLIENT_ID is not set. Google sign-in will not work. " + + "Add it to your .env file or GitHub Actions secrets." + ); } createRoot(document.getElementById("root")!).render( diff --git a/src/pages/AuthPage.tsx b/src/pages/AuthPage.tsx index f8204d7..5fb9c4f 100644 --- a/src/pages/AuthPage.tsx +++ b/src/pages/AuthPage.tsx @@ -9,10 +9,10 @@ import { useToast } from "@/components/ui/use-toast"; import { createPortalAccount, forgotPassword, + persistAuthTokens, registerGsp, resendVerificationCode, resetPassword, - saveAuthToken, verifyEmailCode, } from "@/services/gspApi"; import { useGspAuth } from "@/contexts/GspAuthContext"; @@ -75,12 +75,10 @@ const AuthPage: React.FC = () => { username: profile.name || profile.email, email: profile.email, }); - const token = data?.access || data?.token; - if (!data?.success || !token) { + if (!data?.success || !persistAuthTokens(data)) { throw new Error("Unable to register with Google account."); } - saveAuthToken(token); if (data.user) { setAuthenticatedUser(data.user); } else { @@ -165,8 +163,7 @@ const AuthPage: React.FC = () => { email: form.email, password: form.password, }); - if (data.token) { - saveAuthToken(data.token); + if (persistAuthTokens(data)) { if (data.user) { setAuthenticatedUser(data.user); } else { @@ -186,7 +183,7 @@ const AuthPage: React.FC = () => { email: form.email, code: form.verificationCode, }); - saveAuthToken(data.token); + persistAuthTokens(data); if (data.user) { setAuthenticatedUser(data.user); } else { diff --git a/src/services/api.ts b/src/services/api.ts index 6dc4087..c6c30b5 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -12,7 +12,9 @@ export type TimelineEvent = { }; const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); -const BASE_URL = (import.meta as any).env?.VITE_API_BASE_URL || "http://localhost:8080"; +const BASE_URL = + (import.meta as any).env?.VITE_API_BASE_URL || + "https://forestial-afocal-rex.ngrok-free.dev"; // Newsletter subscription stub export async function subscribeEmail(email: string): Promise { diff --git a/src/services/blogApi.ts b/src/services/blogApi.ts index 43f7a6e..4b6111f 100644 --- a/src/services/blogApi.ts +++ b/src/services/blogApi.ts @@ -3,7 +3,7 @@ import { getToken } from "./gspApi"; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || - "https://kcbackend-production-7ae5.up.railway.app"; + "https://forestial-afocal-rex.ngrok-free.dev"; const LOCAL_LIKES_KEY = "kc_blog_likes_v2"; const LOCAL_COMMENTS_KEY = "kc_blog_comments_v2"; diff --git a/src/services/eventsApi.ts b/src/services/eventsApi.ts index a8792b0..68606ad 100644 --- a/src/services/eventsApi.ts +++ b/src/services/eventsApi.ts @@ -3,7 +3,7 @@ import { getToken } from "./gspApi"; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || - "https://kcbackend-production-7ae5.up.railway.app"; + "https://forestial-afocal-rex.ngrok-free.dev"; function authHeaders() { const token = getToken(); @@ -11,11 +11,11 @@ function authHeaders() { } export interface KCEvent { - id: string; // UUID from Django + id: string; // UUID from Django title: string; - date: string; // e.g. "Feb 9, 2026" - date_iso: string; // ISO date string for sorting - time: string; // e.g. "10:00 - 14:00" + date: string; // e.g. "Feb 9, 2026" + date_iso: string; // ISO date string for sorting + time: string; // e.g. "10:00 - 14:00" location: string; description: string; badge?: string; @@ -23,7 +23,10 @@ export interface KCEvent { updated_at?: string; } -export type CreateEventPayload = Omit; +export type CreateEventPayload = Omit< + KCEvent, + "id" | "created_at" | "updated_at" +>; /* ---- Local Storage Fallback ---- */ const EVENTS_KEY = "kc_events_v1"; @@ -59,7 +62,7 @@ export async function getEvents(): Promise { /** Admin: Create a new event */ export async function adminCreateEvent( - payload: CreateEventPayload + payload: CreateEventPayload, ): Promise { try { const resp = await fetch(`${API_BASE_URL}/api/admin/events`, { @@ -72,7 +75,9 @@ export async function adminCreateEvent( }); if (!resp.ok) { const errData = await resp.json().catch(() => ({})); - throw new Error(errData.error || errData.message || "Create event failed"); + throw new Error( + errData.error || errData.message || "Create event failed", + ); } const data = await resp.json(); const event = data.event || data; @@ -99,7 +104,7 @@ export async function adminCreateEvent( /** Admin: Update an event */ export async function adminUpdateEvent( eventId: string, - payload: Partial + payload: Partial, ): Promise { try { const resp = await fetch(`${API_BASE_URL}/api/admin/events/${eventId}`, { @@ -112,7 +117,9 @@ export async function adminUpdateEvent( }); if (!resp.ok) { const errData = await resp.json().catch(() => ({})); - throw new Error(errData.error || errData.message || "Update event failed"); + throw new Error( + errData.error || errData.message || "Update event failed", + ); } const data = await resp.json(); return data.event || data; @@ -121,7 +128,11 @@ export async function adminUpdateEvent( const local = readLocalEvents(); const idx = local.findIndex((e) => e._id === eventId); if (idx !== -1) { - local[idx] = { ...local[idx], ...payload, updatedAt: new Date().toISOString() }; + local[idx] = { + ...local[idx], + ...payload, + updatedAt: new Date().toISOString(), + }; writeLocalEvents(local); return local[idx]; } @@ -130,7 +141,9 @@ export async function adminUpdateEvent( } /** Admin: Delete an event */ -export async function adminDeleteEvent(eventId: string): Promise<{ deleted: boolean }> { +export async function adminDeleteEvent( + eventId: string, +): Promise<{ deleted: boolean }> { try { const resp = await fetch(`${API_BASE_URL}/api/admin/events/${eventId}`, { method: "DELETE", diff --git a/src/services/gspApi.ts b/src/services/gspApi.ts index 8342325..484a5f8 100644 --- a/src/services/gspApi.ts +++ b/src/services/gspApi.ts @@ -1,7 +1,14 @@ import { toast } from "sonner"; -const BASE_URL = (import.meta as any).env?.VITE_API_BASE_URL || "https://kcbackend-production-7ae5.up.railway.app"; -const TOKEN_KEY = "kc_gsp_token"; +const BASE_URL = + (import.meta as any).env?.VITE_API_BASE_URL || + "https://forestial-afocal-rex.ngrok-free.dev"; +const JSON_HEADERS = { + "Content-Type": "application/json", +}; +const ACCESS_TOKEN_KEY = "kc_gsp_access_token"; +const REFRESH_TOKEN_KEY = "kc_gsp_refresh_token"; +const LEGACY_TOKEN_KEY = "kc_gsp_token"; export type GspUser = { id: string; @@ -20,10 +27,21 @@ export type UploadedDocument = { originalFilename: string; }; -export type GspDecisionStatus = "pending" | "accepted" | "waitlisted" | "not_admitted"; +export type GspDecisionStatus = + | "pending" + | "accepted" + | "waitlisted" + | "not_admitted"; export function getToken() { - return localStorage.getItem(TOKEN_KEY); + return ( + localStorage.getItem(ACCESS_TOKEN_KEY) || + localStorage.getItem(LEGACY_TOKEN_KEY) + ); +} + +function getRefreshToken() { + return localStorage.getItem(REFRESH_TOKEN_KEY); } function authHeaders() { @@ -31,16 +49,45 @@ function authHeaders() { return token ? { Authorization: `Bearer ${token}` } : {}; } -async function apiRequest(path: string, init?: RequestInit): Promise { +async function refreshAccessToken() { + const refresh = getRefreshToken(); + if (!refresh) return null; + + const res = await fetch(`${BASE_URL}/api/v2/auth/token/refresh/`, { + method: "POST", + headers: JSON_HEADERS, + body: JSON.stringify({ refresh }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok || !data?.access) { + clearAuthToken(); + return null; + } + + saveAuthToken(data.access, refresh); + return data.access as string; +} + +async function apiRequest( + path: string, + init?: RequestInit, + hasRetried = false, +): Promise { const res = await fetch(`${BASE_URL}${path}`, { ...init, headers: { - "Content-Type": "application/json", + ...JSON_HEADERS, ...authHeaders(), ...(init?.headers || {}), }, }); const data = await res.json().catch(() => ({})); + if (res.status === 401 && !hasRetried && getRefreshToken()) { + const nextToken = await refreshAccessToken(); + if (nextToken) { + return apiRequest(path, init, true); + } + } if (!res.ok) { const message = data?.error || data?.message || "Request failed"; throw new Error(message); @@ -48,20 +95,41 @@ async function apiRequest(path: string, init?: RequestInit): Promise { return data as T; } -export function saveAuthToken(token: string) { - localStorage.setItem(TOKEN_KEY, token); +export function saveAuthToken(accessToken: string, refreshToken?: string) { + localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); + localStorage.removeItem(LEGACY_TOKEN_KEY); + if (refreshToken) { + localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); + } } export function clearAuthToken() { - localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(ACCESS_TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); + localStorage.removeItem(LEGACY_TOKEN_KEY); } export function hasAuthToken() { return Boolean(getToken()); } -export async function registerGsp(payload: { google_id: string; username: string; email: string }) { +export function persistAuthTokens(data: { + access?: string; + refresh?: string; + token?: string; +}) { + const accessToken = data.access || data.token; + if (!accessToken) return false; + saveAuthToken(accessToken, data.refresh); + return true; +} + +export async function registerGsp(payload: { + google_id: string; + username: string; + email: string; +}) { const res = await fetch(`${BASE_URL}/api/v2/auth/google-login/`, { headers: { - "Content-Type": "application/json", + ...JSON_HEADERS, }, method: "POST", body: JSON.stringify(payload), @@ -70,39 +138,65 @@ export async function registerGsp(payload: { google_id: string; username: string if (!res.ok) { throw new Error(data?.error || data?.message || "Google sign-in failed"); } - return data as { success: boolean; refresh?: string; access?: string; token?: string; user: GspUser }; + return data as { + success: boolean; + refresh?: string; + access?: string; + token?: string; + user: GspUser; + }; } -export async function verifyEmailCode(payload: { email: string; code: string }) { - return apiRequest<{ success: boolean; message: string; token: string; user: GspUser }>("/api/v2/auth/verify-email/", { +export async function verifyEmailCode(payload: { + email: string; + code: string; +}) { + return apiRequest<{ + success: boolean; + message: string; + token: string; + user: GspUser; + }>("/api/v2/auth/verify-email/", { method: "POST", body: JSON.stringify(payload), }); } export async function resendVerificationCode(email: string) { - return apiRequest<{ success: boolean; message: string; debugVerificationCode?: string }>("/api/v2/auth/resend-verification-code/", { + return apiRequest<{ + success: boolean; + message: string; + debugVerificationCode?: string; + }>("/api/v2/auth/resend-verification-code/", { method: "POST", body: JSON.stringify({ email }), }); } export async function loginGsp(payload: { email: string; password: string }) { - return apiRequest<{ token: string; user: GspUser }>("/api/v2/auth/google-login/", { - method: "POST", - body: JSON.stringify(payload), - }); + return apiRequest<{ + success: boolean; + token: string; + refresh?: string; + user: GspUser; + }>( + "/api/v2/auth/login/", + { + method: "POST", + body: JSON.stringify(payload), + }, + ); } export async function forgotPassword(email: string) { - return apiRequest<{ success: boolean }>("/api/auth/forgot-password", { + return apiRequest<{ success: boolean }>("/api/v2/auth/forgot-password/", { method: "POST", body: JSON.stringify({ email }), }); } export async function resetPassword(token: string, newPassword: string) { - return apiRequest<{ success: boolean }>("/api/auth/reset-password", { + return apiRequest<{ success: boolean }>("/api/v2/auth/reset-password/", { method: "POST", body: JSON.stringify({ token, newPassword }), }); @@ -112,8 +206,17 @@ export async function getCurrentUser() { return apiRequest<{ user: GspUser }>("/api/v2/auth/me/"); } -export async function createPortalAccount(payload: { name: string; email: string; password: string }) { - return apiRequest<{ success: boolean; message?: string; token?: string; user?: GspUser }>("/api/v2/auth/register/", { +export async function createPortalAccount(payload: { + name: string; + email: string; + password: string; +}) { + return apiRequest<{ + success: boolean; + message?: string; + token?: string; + user?: GspUser; + }>("/api/v2/auth/register/", { method: "POST", body: JSON.stringify(payload), }); @@ -155,94 +258,153 @@ function buildGspApplicationPayload( }; } -export async function saveGspDraft(data: any, sectionState: Record, r_id?: string) { +export async function saveGspDraft( + data: any, + sectionState: Record, + r_id?: string, +) { const payload = buildGspApplicationPayload(data, sectionState); if (r_id) { - return apiRequest<{ success: boolean; application: any }>(`/api/v2/gsp/registration/${r_id}/`, { - method: "PATCH", - body: JSON.stringify(payload), - }); + return apiRequest<{ success: boolean; application: any }>( + `/api/v2/gsp/registration/${r_id}/`, + { + method: "PATCH", + body: JSON.stringify(payload), + }, + ); } else { - return apiRequest<{ success: boolean; application: any }>("/api/v2/gsp/registration/", { - method: "POST", - body: JSON.stringify(payload), - }); + return apiRequest<{ success: boolean; application: any }>( + "/api/v2/gsp/registration/", + { + method: "POST", + body: JSON.stringify(payload), + }, + ); } -} +} -export async function submitGspApplication(data: any, sectionState: Record, r_id: string) { +export async function submitGspApplication( + data: any, + sectionState: Record, + r_id: string, +) { const payload = buildGspApplicationPayload(data, sectionState, { submitted: true, status: "submitted", }); - return apiRequest<{ success: boolean; reference: string; application: any }>(`/api/v2/gsp/registration/${r_id}/`, { - method: "PATCH", - body: JSON.stringify(payload), - }); + return apiRequest<{ success: boolean; reference: string; application: any }>( + `/api/v2/gsp/registration/${r_id}/`, + { + method: "PATCH", + body: JSON.stringify(payload), + }, + ); } +// NOTE: Trailing slash is required — Django APPEND_SLASH will 301-redirect +// slashless PATCH/POST requests and the body is dropped. export async function getGspDecision() { - return apiRequest<{ released: boolean; decisionStatus: GspDecisionStatus | null; reference?: string; lowerSixthPathwayChoice?: string | null }>("/api/gsp/application/decision"); + return apiRequest<{ + released: boolean; + decisionStatus: GspDecisionStatus | null; + reference?: string; + lowerSixthPathwayChoice?: string | null; + }>("/api/gsp/application/decision/"); } -export async function uploadGspDocument({ file, application, field }: { file: File; application: string; field: "reportCard" | "olSlip" | "alSlip" }) { +export async function uploadGspDocument({ + file, + application, + field, +}: { + file: File; + application: string; + field: "reportCard" | "olSlip" | "alSlip"; +}) { const form = new FormData(); form.append(field, file); - // return apiRequest(`/api/v2/gsp/registration/${application}`, { - // method: "PATCH", - // body: form - // }); - return await fetch(`${BASE_URL}/api/v2/gsp/registration/${application}/`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${getToken()}`, - }, - body: form, - }).then(async (res) => { - const data = await res.json().catch(() => ({})); - if (!res.ok) { - throw new Error(data?.error || data?.message || "Failed to upload document"); - } - return data; - }) - .then(data => {return data}) - .catch(e => { - console.error(e); - toast.error("Failed to upload document. Please try again."); - throw e; - }); + + const upload = () => + fetch(`${BASE_URL}/api/v2/gsp/registration/${application}/`, { + method: "PATCH", + headers: { + ...authHeaders(), + }, + body: form, + }); + + return await upload() + .then(async (res) => { + if (res.status === 401 && getRefreshToken()) { + const nextToken = await refreshAccessToken(); + if (nextToken) return upload(); + } + return res; + }) + .then(async (res) => { + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error( + data?.error || data?.message || "Failed to upload document", + ); + } + return data; + }) + .then((data) => { + return data; + }) + .catch((e) => { + console.error(e); + toast.error("Failed to upload document. Please try again."); + throw e; + }); } export async function adminGetApplications(query?: string) { const params = new URLSearchParams(); if (query) params.set("query", query); - return apiRequest<{ applications: any[] }>(`/api/admin/gsp/applications${params.toString() ? `?${params.toString()}` : ""}`); + return apiRequest<{ applications: any[] }>( + `/api/admin/gsp/applications/${params.toString() ? `?${params.toString()}` : ""}`, + ); } export async function adminGetApplication(applicationId: string) { - const res = await apiRequest(`/api/admin/gsp/applications/${applicationId}`); + const res = await apiRequest( + `/api/admin/gsp/applications/${applicationId}/`, + ); return { application: res.application || res }; } -export async function adminSetDecision(applicationId: string, decisionStatus: GspDecisionStatus) { - return apiRequest<{ success: boolean; application: any }>(`/api/admin/gsp/applications/${applicationId}/decision`, { - method: "PATCH", - body: JSON.stringify({ decisionStatus }), - }); +export async function adminSetDecision( + applicationId: string, + decisionStatus: GspDecisionStatus, +) { + return apiRequest<{ success: boolean; application: any }>( + `/api/admin/gsp/applications/${applicationId}/decision/`, + { + method: "PATCH", + body: JSON.stringify({ decisionStatus }), + }, + ); } export async function adminGetUsers() { - return apiRequest<{ users: GspUser[] }>("/api/admin/gsp/users"); + return apiRequest<{ users: GspUser[] }>("/api/admin/gsp/users/"); } export async function adminToggleRelease(isReleased: boolean) { - return apiRequest<{ success: boolean; release: { isReleased: boolean; releasedAt: string | null } }>("/api/admin/gsp/release", { + return apiRequest<{ + success: boolean; + release: { isReleased: boolean; releasedAt: string | null }; + }>("/api/admin/gsp/release/", { method: "PATCH", body: JSON.stringify({ isReleased }), }); } export async function adminGetRelease() { - return apiRequest<{ release: { isReleased: boolean; releasedAt: string | null } }>("/api/admin/gsp/release"); + return apiRequest<{ + release: { isReleased: boolean; releasedAt: string | null }; + }>("/api/admin/gsp/release/"); }