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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
66 changes: 18 additions & 48 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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/
52 changes: 38 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Knowledge Center</title>
<meta name="description" content="Knowledge Center Cameroon (KC) is a non-profit NGO dedicated to empowering young Cameroonians through STEM education, tutoring, and inspiring scientific curiosity, creativity, and love." />
<meta name="keywords" content="STEM education, Cameroon, tutoring, science, technology, engineering, mathematics, GCE, education, non-profit, NGO" />
<meta
name="description"
content="Knowledge Center Cameroon (KC) — We are building the generation who will transform Africa into the greatest hub by 2060. Enjoy the journey."
/>
<meta
name="keywords"
content="Leadership, Excellence, STEM education, Cameroon, tutoring, GCE, education."
/>
<meta name="author" content="Knowledge Center Cameroon" />
<link rel="canonical" href="https://kccameroon.com" />
<link rel="icon" href="logo.png" type="image/x-icon">
<link rel="canonical" href="https://kcedu.org" />
<link rel="icon" href="logo.png" type="image/x-icon" />

<!-- Preload self-hosted Montserrat variable fonts -->
<link rel="preload" as="font" type="font/woff2" href="/fonts/Montserrat.woff2" crossorigin>
<link
rel="preload"
as="font"
type="font/woff2"
href="/fonts/Montserrat.woff2"
crossorigin
/>

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://knowledgecentercmr.org" />
<meta property="og:title" content="Knowledge Center Cameroon | Empowering Young Scientists Through STEM Education" />
<meta property="og:description" content="Knowledge Center Cameroon (KC) is a non-profit NGO dedicated to empowering young Cameroonians through STEM education, tutoring, and inspiring scientific curiosity, creativity, and love." />
<meta property="og:image" content="https://knowledgecentercmr.org/og-image.jpg" />
<meta property="og:url" content="https://kcedu.org" />
<meta
property="og:title"
content="Knowledge Center (KC)| Building a strong nexus of future-ready next generation African STEM Leaders"
/>
<meta
property="og:description"
content="We are building the generation who will transform Africa into the greatest hub by 2060. Enjoy the journey."
/>
<meta property="og:image" content="https://kcedu.org/logo.jpeg" />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://kccameroon.com/" />
<meta property="twitter:title" content="Knowledge Center Cameroon | Empowering Young Scientists Through STEM Education" />
<meta property="twitter:description" content="Knowledge Center Cameroon (KC) is a non-profit NGO dedicated to empowering young Cameroonians through STEM education, tutoring, and inspiring scientific curiosity, creativity, and love." />
<meta property="twitter:image" content="https://kccameroon.com/og-image.jpg" />
<meta property="twitter:url" content="https://kcedu.org/" />
<meta
property="twitter:title"
content="Knowledge Center (KC)| Building a strong nexus of future-ready next generation African STEM Leaders"
/>
<meta
property="twitter:description"
content="We are building the generation who will transform Africa into the greatest hub by 2069. Enjoy the journey."
/>
<meta property="twitter:image" content="https://kcedu.org/logo.jpeg" />

<!-- Additional SEO Meta Tags -->
<meta name="robots" content="index, follow" />
Expand Down
29 changes: 22 additions & 7 deletions src/components/Contact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@
message: formData.message,
reply_to: formData.email,
},
emailjsPublicKey
{
publicKey: emailjsPublicKey,
}
);

if (res.status === 200) {
Expand All @@ -99,17 +101,30 @@
} 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) {

Check failure on line 104 in src/components/Contact.tsx

View workflow job for this annotation

GitHub Actions / 🔍 Lint & Type-check

Unexpected any. Specify a different type
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,
Expand Down
5 changes: 2 additions & 3 deletions src/contexts/GspAuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
getCurrentUser,
hasAuthToken,
loginGsp,
saveAuthToken,
persistAuthTokens,
} from "@/services/gspApi";
import { getToken } from "@/services/gspApi";

type AuthContextValue = {
user: GspUser | null;
Expand Down Expand Up @@ -50,7 +49,7 @@

const signIn = async (email: string, password: string) => {
const data = await loginGsp({ email, password });
saveAuthToken(data.token);
persistAuthTokens(data);
setUser(data.user);
};

Expand All @@ -66,7 +65,7 @@
);
};

export function useGspAuth() {

Check warning on line 68 in src/contexts/GspAuthContext.tsx

View workflow job for this annotation

GitHub Actions / 🔍 Lint & Type-check

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const ctx = React.useContext(GspAuthContext);
if (!ctx) throw new Error("useGspAuth must be used within GspAuthProvider");
return ctx;
Expand Down
7 changes: 5 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 4 additions & 7 deletions src/pages/AuthPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand Down
2 changes: 1 addition & 1 deletion src/services/blogApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Loading
Loading