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/");
}