repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Pan-African Youth Internship,…
stars
latest
clone command
git clone gitlawb://did:key:z6MkmNFz...XP9B/pan-african-you...git clone gitlawb://did:key:z6MkmNFz.../pan-african-you...53f59357sync from playground10m ago| #1 | import { useState, useEffect } from "react"; |
| #2 | import { supabase } from "./supabaseClient"; |
| #3 | |
| #4 | // ---- Types ---- |
| #5 | |
| #6 | interface AdminStats { |
| #7 | totalYouth: number; |
| #8 | totalOrgs: number; |
| #9 | totalOpportunities: number; |
| #10 | pendingVerifications: number; |
| #11 | } |
| #12 | |
| #13 | interface OrgProfile { |
| #14 | id: string; |
| #15 | user_id: string; |
| #16 | org_name: string; |
| #17 | org_type: string; |
| #18 | org_email: string; |
| #19 | logo_url: string; |
| #20 | country: string; |
| #21 | region_city: string; |
| #22 | contact_name: string; |
| #23 | contact_email: string; |
| #24 | contact_phone: string; |
| #25 | website: string; |
| #26 | verified: boolean | null; |
| #27 | verification_note: string; |
| #28 | created_at: string; |
| #29 | } |
| #30 | |
| #31 | interface YouthProfile { |
| #32 | id: string; |
| #33 | user_id: string; |
| #34 | full_name: string; |
| #35 | email: string; |
| #36 | university: string; |
| #37 | programme: string; |
| #38 | country_of_residence: string; |
| #39 | city: string; |
| #40 | premium: boolean | null; |
| #41 | created_at: string; |
| #42 | } |
| #43 | |
| #44 | interface Opportunity { |
| #45 | id: string; |
| #46 | title: string; |
| #47 | org_name: string; |
| #48 | type: string; |
| #49 | location: string; |
| #50 | status: string; |
| #51 | external_link: string; |
| #52 | duration: string; |
| #53 | paid_status: string; |
| #54 | stipend_amount: string; |
| #55 | responsibilities: string; |
| #56 | learning_outcomes: string; |
| #57 | expectations: string; |
| #58 | benefits: string; |
| #59 | description: string; |
| #60 | deadline: string; |
| #61 | created_at: string; |
| #62 | } |
| #63 | |
| #64 | // ---- Stat Card ---- |
| #65 | |
| #66 | function StatCard({ icon, value, label, color }: { icon: string; value: number | string; label: string; color: string }) { |
| #67 | return ( |
| #68 | <div className="dash-stat-card"> |
| #69 | <div className="dash-stat-icon" style={{ background: color }}>{icon}</div> |
| #70 | <div className="dash-stat-info"> |
| #71 | <span className="dash-stat-value">{value}</span> |
| #72 | <span className="dash-stat-label">{label}</span> |
| #73 | </div> |
| #74 | </div> |
| #75 | ); |
| #76 | } |
| #77 | |
| #78 | // ---- Main Component ---- |
| #79 | |
| #80 | interface AdminDashboardProps { |
| #81 | userId: string | null; |
| #82 | onBack: () => void; |
| #83 | } |
| #84 | |
| #85 | export default function AdminDashboard({ onBack }: AdminDashboardProps) { |
| #86 | const [activeTab, setActiveTab] = useState<"overview" | "verifications" | "youth" | "organisations" | "opportunities">("overview"); |
| #87 | const [stats, setStats] = useState<AdminStats>({ totalYouth: 0, totalOrgs: 0, totalOpportunities: 0, pendingVerifications: 0 }); |
| #88 | const [pendingOrgs, setPendingOrgs] = useState<OrgProfile[]>([]); |
| #89 | const [allOrgs, setAllOrgs] = useState<OrgProfile[]>([]); |
| #90 | const [allYouth, setAllYouth] = useState<YouthProfile[]>([]); |
| #91 | const [allOpportunities, setAllOpportunities] = useState<Opportunity[]>([]); |
| #92 | const [loading, setLoading] = useState(true); |
| #93 | const [sidebarOpen, setSidebarOpen] = useState(false); |
| #94 | const [youthCountry, setYouthCountry] = useState("All"); |
| #95 | const [youthRegion, setYouthRegion] = useState(""); |
| #96 | const [orgCountry, setOrgCountry] = useState("All"); |
| #97 | const [orgRegion, setOrgRegion] = useState(""); |
| #98 | const [oppCountry, setOppCountry] = useState("All"); |
| #99 | const [oppRegion, setOppRegion] = useState(""); |
| #100 | const [verificationNote, setVerificationNote] = useState(""); |
| #101 | const [verificationFilter, setVerificationFilter] = useState("All"); |
| #102 | const [selectedOrg, setSelectedOrg] = useState<OrgProfile | null>(null); |
| #103 | const [showAddOpp, setShowAddOpp] = useState(false); |
| #104 | const [editOpp, setEditOpp] = useState<Opportunity | null>(null); |
| #105 | const [newOpp, setNewOpp] = useState({ |
| #106 | title: "", |
| #107 | org_name: "", |
| #108 | type: "Internship", |
| #109 | location: "", |
| #110 | description: "", |
| #111 | external_link: "", |
| #112 | deadline: "", |
| #113 | paid_status: "Paid", |
| #114 | duration: "", |
| #115 | }); |
| #116 | |
| #117 | // Filtered data |
| #118 | const filteredYouth = allYouth.filter((y) => { |
| #119 | const matchesCountry = youthCountry === "All" || (y.country_of_residence || "").toLowerCase() === youthCountry.toLowerCase(); |
| #120 | const matchesRegion = !youthRegion || (y.city || "").toLowerCase().includes(youthRegion.toLowerCase()); |
| #121 | return matchesCountry && matchesRegion; |
| #122 | }); |
| #123 | |
| #124 | const filteredOrgs = allOrgs.filter((o) => { |
| #125 | const matchesCountry = orgCountry === "All" || (o.country || "").toLowerCase() === orgCountry.toLowerCase(); |
| #126 | const matchesRegion = !orgRegion || (o.region_city || "").toLowerCase().includes(orgRegion.toLowerCase()); |
| #127 | const matchesVerification = verificationFilter === "All" || |
| #128 | (verificationFilter === "verified" && o.verified === true) || |
| #129 | (verificationFilter === "unverified" && o.verified !== true); |
| #130 | return matchesCountry && matchesRegion && matchesVerification; |
| #131 | }); |
| #132 | |
| #133 | const filteredOpps = allOpportunities.filter((o) => { |
| #134 | const locParts = (o.location || "").split(",").map((s) => s.trim()); |
| #135 | const oppCity = locParts[0] || ""; |
| #136 | const oppCountry = locParts.length > 1 ? locParts[locParts.length - 1] : o.location || ""; |
| #137 | const matchesCountry = oppCountry === "All" || oppCountry.toLowerCase() === oppCountry.toLowerCase(); |
| #138 | const matchesRegion = !oppRegion || oppCity.toLowerCase().includes(oppRegion.toLowerCase()) || (o.location || "").toLowerCase().includes(oppRegion.toLowerCase()); |
| #139 | return matchesCountry && matchesRegion; |
| #140 | }); |
| #141 | |
| #142 | // Extract unique countries for filter dropdowns |
| #143 | const youthCountries = [...new Set(allYouth.map((y) => y.country_of_residence).filter(Boolean))]; |
| #144 | const orgCountries = [...new Set(allOrgs.map((o) => o.country).filter(Boolean))]; |
| #145 | |
| #146 | useEffect(() => { |
| #147 | async function fetchData() { |
| #148 | try { |
| #149 | const [youthRes, orgRes, oppRes, pendingRes] = await Promise.all([ |
| #150 | supabase.from("youth_profiles").select("*", { count: "exact", head: true }), |
| #151 | supabase.from("organisation_profiles").select("*", { count: "exact", head: true }), |
| #152 | supabase.from("opportunities").select("*", { count: "exact", head: true }), |
| #153 | supabase.from("organisation_profiles").select("*").neq("verified", true), |
| #154 | ]); |
| #155 | |
| #156 | setStats({ |
| #157 | totalYouth: youthRes.count || 0, |
| #158 | totalOrgs: orgRes.count || 0, |
| #159 | totalOpportunities: oppRes.count || 0, |
| #160 | pendingVerifications: pendingRes.data?.length || 0, |
| #161 | }); |
| #162 | |
| #163 | if (pendingRes.data) { |
| #164 | setPendingOrgs(pendingRes.data); |
| #165 | } |
| #166 | |
| #167 | // Fetch all orgs |
| #168 | const { data: orgsData, error: orgErr } = await supabase |
| #169 | .from("organisation_profiles") |
| #170 | .select("*") |
| #171 | .order("created_at", { ascending: false }); |
| #172 | |
| #173 | console.log("Admin orgs fetch:", { orgsData, orgErr, count: orgsData?.length }); |
| #174 | if (orgsData) setAllOrgs(orgsData); |
| #175 | if (orgErr) console.error("Org fetch error:", orgErr); |
| #176 | |
| #177 | // Fetch all youth |
| #178 | const { data: youthData, error: youthErr } = await supabase |
| #179 | .from("youth_profiles") |
| #180 | .select("*") |
| #181 | .order("created_at", { ascending: false }); |
| #182 | |
| #183 | console.log("Admin youth fetch:", { youthData, youthErr, count: youthData?.length }); |
| #184 | if (youthData) setAllYouth(youthData); |
| #185 | if (youthErr) console.error("Youth fetch error:", youthErr); |
| #186 | |
| #187 | // Fetch all opportunities |
| #188 | const { data: oppsData, error: oppErr } = await supabase |
| #189 | .from("opportunities") |
| #190 | .select("*") |
| #191 | .order("created_at", { ascending: false }); |
| #192 | |
| #193 | console.log("Admin opps fetch:", { oppsData, oppErr, count: oppsData?.length }); |
| #194 | if (oppsData) setAllOpportunities(oppsData); |
| #195 | if (oppErr) console.error("Opps fetch error:", oppErr); |
| #196 | |
| #197 | } catch (err) { |
| #198 | console.error("Error fetching admin data:", err); |
| #199 | } finally { |
| #200 | setLoading(false); |
| #201 | } |
| #202 | } |
| #203 | |
| #204 | fetchData(); |
| #205 | }, []); |
| #206 | |
| #207 | const handleVerifyOrg = async (orgUserId: string, status: "verified" | "rejected" | "pending", note?: string) => { |
| #208 | const verifiedValue = status === "verified"; |
| #209 | const updateData: Record<string, unknown> = { verified: verifiedValue }; |
| #210 | if (note) updateData.verification_note = note; |
| #211 | |
| #212 | console.log("Verifying org:", orgUserId, "to status:", status, "with data:", updateData); |
| #213 | |
| #214 | const { data, error } = await supabase |
| #215 | .from("organisation_profiles") |
| #216 | .update(updateData) |
| #217 | .eq("user_id", orgUserId) |
| #218 | .select(); |
| #219 | |
| #220 | console.log("Verify result:", { data, error }); |
| #221 | |
| #222 | if (error) { |
| #223 | console.error("Failed to verify org:", error); |
| #224 | alert("Failed to verify: " + error.message); |
| #225 | return; |
| #226 | } |
| #227 | |
| #228 | if (!data || data.length === 0) { |
| #229 | console.error("No rows updated for org:", orgUserId); |
| #230 | alert("No rows updated. Check that the user_id exists."); |
| #231 | return; |
| #232 | } |
| #233 | |
| #234 | setPendingOrgs((prev) => prev.filter((o) => o.user_id !== orgUserId)); |
| #235 | setAllOrgs((prev) => |
| #236 | prev.map((o) => (o.user_id === orgUserId ? { ...o, verified: verifiedValue, verification_note: note || o.verification_note } : o)) |
| #237 | ); |
| #238 | setStats((prev) => ({ |
| #239 | ...prev, |
| #240 | pendingVerifications: Math.max(0, prev.pendingVerifications - 1), |
| #241 | })); |
| #242 | }; |
| #243 | |
| #244 | const handleDeleteOrg = async (orgUserId: string) => { |
| #245 | if (!confirm("Are you sure you want to delete this organisation profile? This action cannot be undone.")) return; |
| #246 | |
| #247 | const { error } = await supabase |
| #248 | .from("organisation_profiles") |
| #249 | .delete() |
| #250 | .eq("user_id", orgUserId); |
| #251 | |
| #252 | if (error) { |
| #253 | console.error("Failed to delete org:", error); |
| #254 | alert("Failed to delete: " + error.message); |
| #255 | } else { |
| #256 | setAllOrgs((prev) => prev.filter((o) => o.user_id !== orgUserId)); |
| #257 | setPendingOrgs((prev) => prev.filter((o) => o.user_id !== orgUserId)); |
| #258 | } |
| #259 | }; |
| #260 | |
| #261 | const handleDeleteOpportunity = async (oppId: string) => { |
| #262 | const { error } = await supabase |
| #263 | .from("opportunities") |
| #264 | .delete() |
| #265 | .eq("id", oppId); |
| #266 | |
| #267 | if (!error) { |
| #268 | setAllOpportunities((prev) => prev.filter((o) => o.id !== oppId)); |
| #269 | setStats((prev) => ({ |
| #270 | ...prev, |
| #271 | totalOpportunities: Math.max(0, prev.totalOpportunities - 1), |
| #272 | })); |
| #273 | } |
| #274 | }; |
| #275 | |
| #276 | const handleTogglePremium = async (youthUserId: string, currentPremium: boolean | null) => { |
| #277 | const newPremium = !currentPremium; |
| #278 | const { error } = await supabase |
| #279 | .from("youth_profiles") |
| #280 | .update({ premium: newPremium }) |
| #281 | .eq("user_id", youthUserId); |
| #282 | |
| #283 | if (!error) { |
| #284 | setAllYouth((prev) => |
| #285 | prev.map((y) => (y.user_id === youthUserId ? { ...y, premium: newPremium } : y)) |
| #286 | ); |
| #287 | } else { |
| #288 | alert("Failed to update premium: " + error.message); |
| #289 | } |
| #290 | }; |
| #291 | |
| #292 | const handleAddOpportunity = async () => { |
| #293 | if (!newOpp.title || !newOpp.org_name || !newOpp.location) { |
| #294 | alert("Please fill in Title, Organisation, and Location."); |
| #295 | return; |
| #296 | } |
| #297 | |
| #298 | const { data, error } = await supabase.from("opportunities").insert({ |
| #299 | title: newOpp.title, |
| #300 | org_name: newOpp.org_name, |
| #301 | type: newOpp.type, |
| #302 | location: newOpp.location, |
| #303 | description: newOpp.description, |
| #304 | external_link: newOpp.external_link, |
| #305 | deadline: newOpp.deadline || null, |
| #306 | paid_status: newOpp.paid_status, |
| #307 | duration: newOpp.duration, |
| #308 | status: "Active", |
| #309 | org_user_id: null, |
| #310 | }).select(); |
| #311 | |
| #312 | if (error) { |
| #313 | alert("Failed to add opportunity: " + error.message); |
| #314 | return; |
| #315 | } |
| #316 | |
| #317 | if (data && data[0]) { |
| #318 | setAllOpportunities((prev) => [data[0] as unknown as Opportunity, ...prev]); |
| #319 | setStats((prev) => ({ ...prev, totalOpportunities: prev.totalOpportunities + 1 })); |
| #320 | } |
| #321 | |
| #322 | setShowAddOpp(false); |
| #323 | setNewOpp({ title: "", org_name: "", type: "Internship", location: "", description: "", external_link: "", deadline: "", paid_status: "Paid", duration: "" }); |
| #324 | }; |
| #325 | |
| #326 | const handleSaveOpportunity = async () => { |
| #327 | if (!editOpp) return; |
| #328 | const { error } = await supabase.from("opportunities").update({ |
| #329 | title: editOpp.title, |
| #330 | type: editOpp.type, |
| #331 | location: editOpp.location, |
| #332 | status: editOpp.status, |
| #333 | external_link: editOpp.external_link, |
| #334 | deadline: editOpp.deadline || null, |
| #335 | paid_status: editOpp.paid_status, |
| #336 | stipend_amount: editOpp.stipend_amount, |
| #337 | duration: editOpp.duration, |
| #338 | responsibilities: editOpp.responsibilities, |
| #339 | learning_outcomes: editOpp.learning_outcomes, |
| #340 | expectations: editOpp.expectations, |
| #341 | benefits: editOpp.benefits, |
| #342 | description: editOpp.description, |
| #343 | }).eq("id", editOpp.id); |
| #344 | |
| #345 | if (error) { |
| #346 | alert("Failed to update opportunity: " + error.message); |
| #347 | } else { |
| #348 | setAllOpportunities((prev) => |
| #349 | prev.map((o) => (o.id === editOpp.id ? editOpp : o)) |
| #350 | ); |
| #351 | setEditOpp(null); |
| #352 | } |
| #353 | }; |
| #354 | |
| #355 | if (loading) { |
| #356 | return ( |
| #357 | <div className="dash-modal-overlay" style={{ display: "flex", alignItems: "center", justifyContent: "center" }}> |
| #358 | <div style={{ textAlign: "center", color: "white" }}> |
| #359 | <div style={{ fontSize: "2rem", marginBottom: "1rem" }}>⏳</div> |
| #360 | <p>Loading admin dashboard...</p> |
| #361 | </div> |
| #362 | </div> |
| #363 | ); |
| #364 | } |
| #365 | |
| #366 | return ( |
| #367 | <div className="dash-page"> |
| #368 | {/* Top Nav */} |
| #369 | <nav className="nav"> |
| #370 | <div className="nav-inner"> |
| #371 | <div className="dash-nav-left"> |
| #372 | <button type="button" className="dash-sidebar-toggle" onClick={() => setSidebarOpen(!sidebarOpen)} aria-label="Toggle menu"> |
| #373 | {sidebarOpen ? "✕" : "☰"} |
| #374 | </button> |
| #375 | <a href="#" className="logo" onClick={(e) => { e.preventDefault(); onBack(); }}> |
| #376 | <span className="logo-icon"><img src="https://i.imgur.com/FT8aHGw.png" alt="FursaLink" /></span> |
| #377 | <span className="logo-text">Fursa<span className="logo-highlight">Link</span></span> |
| #378 | </a> |
| #379 | <span style={{ background: "#fee2e2", color: "#991b1b", padding: "0.25rem 0.75rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600, marginLeft: "0.5rem" }}>ADMIN</span> |
| #380 | </div> |
| #381 | <div className="dash-nav-tabs"> |
| #382 | {[ |
| #383 | { key: "overview" as const, label: "Overview" }, |
| #384 | { key: "verifications" as const, label: `Verifications${stats.pendingVerifications > 0 ? ` (${stats.pendingVerifications})` : ""}` }, |
| #385 | { key: "youth" as const, label: "Youth" }, |
| #386 | { key: "organisations" as const, label: "Organisations" }, |
| #387 | { key: "opportunities" as const, label: "Opportunities" }, |
| #388 | ].map((tab) => ( |
| #389 | <button |
| #390 | key={tab.key} |
| #391 | className={`dash-nav-tab ${activeTab === tab.key ? "dash-nav-tab-active" : ""}`} |
| #392 | onClick={() => setActiveTab(tab.key)} |
| #393 | > |
| #394 | {tab.label} |
| #395 | </button> |
| #396 | ))} |
| #397 | </div> |
| #398 | </div> |
| #399 | </nav> |
| #400 | |
| #401 | {/* Sidebar */} |
| #402 | <aside className={`dash-sidebar ${sidebarOpen ? "dash-sidebar-open" : ""}`}> |
| #403 | <div className="dash-sidebar-header"> |
| #404 | <div className="dash-sidebar-user"> |
| #405 | <div style={{ width: "3rem", height: "3rem", borderRadius: "50%", background: "#fee2e2", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 700, color: "#991b1b" }}>A</div> |
| #406 | <div> |
| #407 | <span className="dash-sidebar-name">Admin User</span> |
| #408 | <span className="dash-sidebar-sub">System Administrator</span> |
| #409 | </div> |
| #410 | </div> |
| #411 | </div> |
| #412 | <nav className="dash-sidebar-nav"> |
| #413 | {[ |
| #414 | { key: "overview" as const, label: "Overview", icon: "📊" }, |
| #415 | { key: "verifications" as const, label: "Verifications", icon: "✅" }, |
| #416 | { key: "youth" as const, label: "Youth Users", icon: "👤" }, |
| #417 | { key: "organisations" as const, label: "Organisations", icon: "🏢" }, |
| #418 | { key: "opportunities" as const, label: "Opportunities", icon: "📋" }, |
| #419 | ].map((tab) => ( |
| #420 | <button |
| #421 | key={tab.key} |
| #422 | className={`dash-sidebar-link ${activeTab === tab.key ? "dash-sidebar-link-active" : ""}`} |
| #423 | onClick={() => { setActiveTab(tab.key); setSidebarOpen(false); }} |
| #424 | > |
| #425 | <span className="dash-sidebar-icon">{tab.icon}</span> |
| #426 | <span>{tab.label}</span> |
| #427 | </button> |
| #428 | ))} |
| #429 | </nav> |
| #430 | <div className="dash-sidebar-footer"> |
| #431 | <button type="button" className="dash-sidebar-link" onClick={onBack}> |
| #432 | <span className="dash-sidebar-icon">🚪</span> |
| #433 | <span>Sign Out</span> |
| #434 | </button> |
| #435 | </div> |
| #436 | </aside> |
| #437 | |
| #438 | <div className="dash-container"> |
| #439 | {/* Overview Tab */} |
| #440 | {activeTab === "overview" && ( |
| #441 | <div className="dash-content"> |
| #442 | <h2 className="dash-section-title">System Overview</h2> |
| #443 | <div className="dash-stats-grid"> |
| #444 | <StatCard icon="👤" value={stats.totalYouth} label="Youth Registered" color="var(--green-100)" /> |
| #445 | <StatCard icon="🏢" value={stats.totalOrgs} label="Organisations" color="var(--blue-100, #dbeafe)" /> |
| #446 | <StatCard icon="📋" value={stats.totalOpportunities} label="Opportunities" color="var(--gold-100, #fef9c3)" /> |
| #447 | <StatCard icon="⏳" value={stats.pendingVerifications} label="Pending Verifications" color="var(--purple-100, #f3e8ff)" /> |
| #448 | </div> |
| #449 | |
| #450 | {stats.pendingVerifications > 0 && ( |
| #451 | <div className="dash-card" style={{ marginTop: "1.5rem" }}> |
| #452 | <h3 className="dash-card-title">Organisations Awaiting Verification</h3> |
| #453 | <div className="dash-table-wrap"> |
| #454 | <table className="dash-table"> |
| #455 | <thead> |
| #456 | <tr> |
| #457 | <th>Organisation</th> |
| #458 | <th>Type</th> |
| #459 | <th>Location</th> |
| #460 | <th>Actions</th> |
| #461 | </tr> |
| #462 | </thead> |
| #463 | <tbody> |
| #464 | {pendingOrgs.slice(0, 5).map((org) => ( |
| #465 | <tr key={org.user_id}> |
| #466 | <td className="dash-table-title">{org.org_name}</td> |
| #467 | <td>{org.org_type}</td> |
| #468 | <td>{org.region_city}, {org.country}</td> |
| #469 | <td> |
| #470 | <div className="dash-actions"> |
| #471 | <button type="button" className="btn btn-primary btn-sm" onClick={() => handleVerifyOrg(org.user_id, "verified")}>Approve</button> |
| #472 | <button type="button" className="btn btn-outline btn-sm" onClick={() => handleVerifyOrg(org.user_id, "rejected")}>Reject</button> |
| #473 | </div> |
| #474 | </td> |
| #475 | </tr> |
| #476 | ))} |
| #477 | </tbody> |
| #478 | </table> |
| #479 | </div> |
| #480 | {pendingOrgs.length > 5 && ( |
| #481 | <button type="button" className="btn btn-outline btn-sm" style={{ marginTop: "1rem" }} onClick={() => setActiveTab("verifications")}> |
| #482 | View All ({pendingOrgs.length}) |
| #483 | </button> |
| #484 | )} |
| #485 | </div> |
| #486 | )} |
| #487 | </div> |
| #488 | )} |
| #489 | |
| #490 | {/* Verifications Tab */} |
| #491 | {activeTab === "verifications" && ( |
| #492 | <div className="dash-content"> |
| #493 | <h2 className="dash-section-title">Organisation Verifications</h2> |
| #494 | <div className="dash-card"> |
| #495 | <div className="dash-card-header"> |
| #496 | <h3 className="dash-card-title">Pending Verifications ({pendingOrgs.length})</h3> |
| #497 | </div> |
| #498 | {pendingOrgs.length === 0 ? ( |
| #499 | <p style={{ color: "var(--gray-500)", padding: "2rem", textAlign: "center" }}>No organisations pending verification</p> |
| #500 | ) : ( |
| #501 | <div className="dash-table-wrap"> |
| #502 | <table className="dash-table"> |
| #503 | <thead> |
| #504 | <tr> |
| #505 | <th>Organisation</th> |
| #506 | <th>Type</th> |
| #507 | <th>Sector</th> |
| #508 | <th>Location</th> |
| #509 | <th>Submitted</th> |
| #510 | <th>Actions</th> |
| #511 | </tr> |
| #512 | </thead> |
| #513 | <tbody> |
| #514 | {pendingOrgs.map((org) => ( |
| #515 | <tr key={org.user_id}> |
| #516 | <td className="dash-table-title">{org.org_name}</td> |
| #517 | <td>{org.org_type}</td> |
| #518 | <td>{org.region_city}</td> |
| #519 | <td>{org.country}</td> |
| #520 | <td>{new Date(org.created_at).toLocaleDateString()}</td> |
| #521 | <td> |
| #522 | <div className="dash-actions"> |
| #523 | <button type="button" className="btn btn-primary btn-sm" onClick={() => handleVerifyOrg(org.user_id, "verified")}>✓ Approve</button> |
| #524 | <button type="button" className="btn btn-outline btn-sm" style={{ color: "#dc2626", borderColor: "#fca5a5" }} onClick={() => handleVerifyOrg(org.user_id, "rejected")}>✕ Reject</button> |
| #525 | </div> |
| #526 | </td> |
| #527 | </tr> |
| #528 | ))} |
| #529 | </tbody> |
| #530 | </table> |
| #531 | </div> |
| #532 | )} |
| #533 | </div> |
| #534 | </div> |
| #535 | )} |
| #536 | |
| #537 | {/* Youth Tab */} |
| #538 | {activeTab === "youth" && ( |
| #539 | <div className="dash-content"> |
| #540 | <h2 className="dash-section-title">Youth Users ({filteredYouth.length})</h2> |
| #541 | <div className="dash-card"> |
| #542 | <div style={{ display: "flex", gap: "1rem", marginBottom: "1rem", flexWrap: "wrap" }}> |
| #543 | <div> |
| #544 | <label style={{ fontSize: "0.75rem", color: "#6b7280", display: "block", marginBottom: "0.25rem" }}>Country</label> |
| #545 | <select style={{ padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minWidth: "10rem" }} value={youthCountry} onChange={(e) => setYouthCountry(e.target.value)}> |
| #546 | <option value="All">All Countries</option> |
| #547 | {youthCountries.map((c) => <option key={c} value={c}>{c}</option>)} |
| #548 | </select> |
| #549 | </div> |
| #550 | <div> |
| #551 | <label style={{ fontSize: "0.75rem", color: "#6b7280", display: "block", marginBottom: "0.25rem" }}>Region / City</label> |
| #552 | <input style={{ padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minWidth: "10rem" }} placeholder="e.g. Lagos" value={youthRegion} onChange={(e) => setYouthRegion(e.target.value)} /> |
| #553 | </div> |
| #554 | </div> |
| #555 | <div className="dash-table-wrap"> |
| #556 | <table className="dash-table"> |
| #557 | <thead> |
| #558 | <tr> |
| #559 | <th>Name</th> |
| #560 | <th>Email</th> |
| #561 | <th>University</th> |
| #562 | <th>Programme</th> |
| #563 | <th>Country</th> |
| #564 | <th>City</th> |
| #565 | <th>Premium</th> |
| #566 | <th>Actions</th> |
| #567 | <th>Registered</th> |
| #568 | </tr> |
| #569 | </thead> |
| #570 | <tbody> |
| #571 | {filteredYouth.map((youth) => ( |
| #572 | <tr key={youth.user_id}> |
| #573 | <td className="dash-table-title">{youth.full_name}</td> |
| #574 | <td>{youth.email}</td> |
| #575 | <td>{youth.university}</td> |
| #576 | <td>{youth.programme}</td> |
| #577 | <td>{youth.country_of_residence}</td> |
| #578 | <td>{youth.city || "—"}</td> |
| #579 | <td> |
| #580 | {youth.premium ? ( |
| #581 | <span style={{ background: "#d1fae5", color: "#065f46", padding: "0.25rem 0.75rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>Premium</span> |
| #582 | ) : ( |
| #583 | <span style={{ background: "#f3f4f6", color: "#6b7280", padding: "0.25rem 0.75rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>Free</span> |
| #584 | )} |
| #585 | </td> |
| #586 | <td> |
| #587 | <button |
| #588 | type="button" |
| #589 | className="btn btn-outline btn-sm" |
| #590 | style={youth.premium ? { color: "#dc2626", borderColor: "#fca5a5" } : { color: "#16a34a", borderColor: "#86efac" }} |
| #591 | onClick={() => handleTogglePremium(youth.user_id, youth.premium)} |
| #592 | > |
| #593 | {youth.premium ? "Revoke Premium" : "Approve Premium"} |
| #594 | </button> |
| #595 | </td> |
| #596 | <td>{new Date(youth.created_at).toLocaleDateString()}</td> |
| #597 | </tr> |
| #598 | ))} |
| #599 | </tbody> |
| #600 | </table> |
| #601 | </div> |
| #602 | </div> |
| #603 | </div> |
| #604 | )} |
| #605 | |
| #606 | {/* Organisations Tab */} |
| #607 | {activeTab === "organisations" && ( |
| #608 | <div className="dash-content"> |
| #609 | <h2 className="dash-section-title">Organisations ({filteredOrgs.length})</h2> |
| #610 | <div className="dash-card"> |
| #611 | <div style={{ display: "flex", gap: "1rem", marginBottom: "1rem", flexWrap: "wrap", alignItems: "flex-end" }}> |
| #612 | <div> |
| #613 | <label style={{ fontSize: "0.75rem", color: "#6b7280", display: "block", marginBottom: "0.25rem" }}>Country</label> |
| #614 | <select style={{ padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minWidth: "10rem" }} value={orgCountry} onChange={(e) => setOrgCountry(e.target.value)}> |
| #615 | <option value="All">All Countries</option> |
| #616 | {orgCountries.map((c) => <option key={c} value={c}>{c}</option>)} |
| #617 | </select> |
| #618 | </div> |
| #619 | <div> |
| #620 | <label style={{ fontSize: "0.75rem", color: "#6b7280", display: "block", marginBottom: "0.25rem" }}>Region / City</label> |
| #621 | <input style={{ padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minWidth: "10rem" }} placeholder="e.g. Nairobi" value={orgRegion} onChange={(e) => setOrgRegion(e.target.value)} /> |
| #622 | </div> |
| #623 | <div> |
| #624 | <label style={{ fontSize: "0.75rem", color: "#6b7280", display: "block", marginBottom: "0.25rem" }}>Verification Status</label> |
| #625 | <select style={{ padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minWidth: "10rem" }} value={verificationFilter} onChange={(e) => setVerificationFilter(e.target.value)}> |
| #626 | <option value="All">All</option> |
| #627 | <option value="verified">Verified</option> |
| #628 | <option value="unverified">Unverified</option> |
| #629 | </select> |
| #630 | </div> |
| #631 | </div> |
| #632 | <div className="dash-table-wrap"> |
| #633 | <table className="dash-table"> |
| #634 | <thead> |
| #635 | <tr> |
| #636 | <th>Organisation</th> |
| #637 | <th>Email</th> |
| #638 | <th>Domain</th> |
| #639 | <th>Country</th> |
| #640 | <th>Type</th> |
| #641 | <th>Status</th> |
| #642 | <th>Note</th> |
| #643 | <th>Actions</th> |
| #644 | </tr> |
| #645 | </thead> |
| #646 | <tbody> |
| #647 | {filteredOrgs.map((org) => ( |
| #648 | <tr key={org.user_id}> |
| #649 | <td className="dash-table-title"> |
| #650 | <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}> |
| #651 | {org.logo_url ? ( |
| #652 | <img src={org.logo_url} alt="" style={{ width: "2rem", height: "2rem", borderRadius: "50%", objectFit: "cover" }} /> |
| #653 | ) : ( |
| #654 | <span style={{ width: "2rem", height: "2rem", borderRadius: "50%", background: "#e5e7eb", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: "0.75rem", fontWeight: 700 }}> |
| #655 | {(org.org_name || "O")[0]} |
| #656 | </span> |
| #657 | )} |
| #658 | {org.org_name} |
| #659 | </div> |
| #660 | </td> |
| #661 | <td>{org.org_email || "—"}</td> |
| #662 | <td style={{ fontSize: "0.8rem", color: "#6b7280" }}>{org.org_email?.split("@")[1] || "—"}</td> |
| #663 | <td>{org.country}</td> |
| #664 | <td>{org.org_type}</td> |
| #665 | <td> |
| #666 | {org.verified === true && ( |
| #667 | <span style={{ background: "#d1fae5", color: "#065f46", padding: "0.25rem 0.75rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>✓ Verified</span> |
| #668 | )} |
| #669 | {org.verified === false && ( |
| #670 | <span style={{ background: "#fee2e2", color: "#991b1b", padding: "0.25rem 0.75rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>✗ Rejected</span> |
| #671 | )} |
| #672 | {org.verified === null && ( |
| #673 | <span style={{ background: "#fef3c7", color: "#92400e", padding: "0.25rem 0.75rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>⏳ Pending</span> |
| #674 | )} |
| #675 | </td> |
| #676 | <td style={{ fontSize: "0.8rem", color: "#6b7280", maxWidth: "10rem" }}>{org.verification_note || "—"}</td> |
| #677 | <td> |
| #678 | <div className="dash-actions"> |
| #679 | <button type="button" className="btn btn-outline btn-sm" onClick={() => setSelectedOrg(org)}>Details</button> |
| #680 | {org.verified !== true && ( |
| #681 | <button type="button" className="btn btn-primary btn-sm" onClick={() => handleVerifyOrg(org.user_id, "verified")}>Approve</button> |
| #682 | )} |
| #683 | {org.verified !== false && ( |
| #684 | <button type="button" className="btn btn-outline btn-sm" style={{ color: "#dc2626", borderColor: "#fca5a5" }} onClick={() => handleVerifyOrg(org.user_id, "rejected")}>Reject</button> |
| #685 | )} |
| #686 | {org.verified !== true && ( |
| #687 | <button |
| #688 | type="button" |
| #689 | className="btn btn-outline btn-sm" |
| #690 | style={{ color: "#0369a1", borderColor: "#7dd3fc" }} |
| #691 | title="Use this for organisations using non-office emails that you have manually confirmed" |
| #692 | onClick={() => { |
| #693 | const note = verificationNote || "Manually verified by admin"; |
| #694 | handleVerifyOrg(org.user_id, "verified", note); |
| #695 | setVerificationNote(""); |
| #696 | }} |
| #697 | > |
| #698 | ✉️ Manual Verify |
| #699 | </button> |
| #700 | )} |
| #701 | <button type="button" className="btn btn-outline btn-sm" style={{ color: "#dc2626", borderColor: "#fca5a5" }} onClick={() => handleDeleteOrg(org.user_id)}>Delete</button> |
| #702 | </div> |
| #703 | </td> |
| #704 | </tr> |
| #705 | ))} |
| #706 | </tbody> |
| #707 | </table> |
| #708 | </div> |
| #709 | </div> |
| #710 | </div> |
| #711 | )} |
| #712 | |
| #713 | {/* Opportunities Tab */} |
| #714 | {activeTab === "opportunities" && ( |
| #715 | <div className="dash-content"> |
| #716 | <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1rem" }}> |
| #717 | <h2 className="dash-section-title" style={{ margin: 0 }}>Opportunities ({filteredOpps.length})</h2> |
| #718 | <button type="button" className="btn btn-primary" onClick={() => setShowAddOpp(true)}>+ Add Opportunity</button> |
| #719 | </div> |
| #720 | <div className="dash-card"> |
| #721 | <div style={{ display: "flex", gap: "1rem", marginBottom: "1rem", flexWrap: "wrap" }}> |
| #722 | <div> |
| #723 | <label style={{ fontSize: "0.75rem", color: "#6b7280", display: "block", marginBottom: "0.25rem" }}>Country</label> |
| #724 | <select style={{ padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minWidth: "10rem" }} value={oppCountry} onChange={(e) => setOppCountry(e.target.value)}> |
| #725 | <option value="All">All Countries</option> |
| #726 | <option value="Kenya">Kenya</option> |
| #727 | <option value="Nigeria">Nigeria</option> |
| #728 | <option value="Ghana">Ghana</option> |
| #729 | <option value="South Africa">South Africa</option> |
| #730 | <option value="Tanzania">Tanzania</option> |
| #731 | <option value="Uganda">Uganda</option> |
| #732 | <option value="Ethiopia">Ethiopia</option> |
| #733 | <option value="Rwanda">Rwanda</option> |
| #734 | <option value="Senegal">Senegal</option> |
| #735 | </select> |
| #736 | </div> |
| #737 | <div> |
| #738 | <label style={{ fontSize: "0.75rem", color: "#6b7280", display: "block", marginBottom: "0.25rem" }}>Region / City</label> |
| #739 | <input style={{ padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minWidth: "10rem" }} placeholder="e.g. Accra" value={oppRegion} onChange={(e) => setOppRegion(e.target.value)} /> |
| #740 | </div> |
| #741 | </div> |
| #742 | <div className="dash-table-wrap"> |
| #743 | <table className="dash-table"> |
| #744 | <thead> |
| #745 | <tr> |
| #746 | <th>Title</th> |
| #747 | <th>Organisation</th> |
| #748 | <th>Type</th> |
| #749 | <th>Location</th> |
| #750 | <th>Link</th> |
| #751 | <th>Status</th> |
| #752 | <th>Posted</th> |
| #753 | <th>Actions</th> |
| #754 | </tr> |
| #755 | </thead> |
| #756 | <tbody> |
| #757 | {filteredOpps.map((opp) => ( |
| #758 | <tr key={opp.id}> |
| #759 | <td className="dash-table-title">{opp.title}</td> |
| #760 | <td>{opp.org_name}</td> |
| #761 | <td>{opp.type}</td> |
| #762 | <td>{opp.location}</td> |
| #763 | <td> |
| #764 | {opp.external_link ? ( |
| #765 | <a href={opp.external_link} target="_blank" rel="noopener noreferrer" style={{ color: "#0369a1", fontSize: "0.8rem" }}>Apply</a> |
| #766 | ) : "—"} |
| #767 | </td> |
| #768 | <td> |
| #769 | <span className={`dash-status dash-status-${opp.status.toLowerCase()}`}>{opp.status}</span> |
| #770 | </td> |
| #771 | <td>{new Date(opp.created_at).toLocaleDateString()}</td> |
| #772 | <td> |
| #773 | <div className="dash-actions"> |
| #774 | <button |
| #775 | type="button" |
| #776 | className="btn btn-outline btn-sm" |
| #777 | onClick={() => setEditOpp(opp)} |
| #778 | > |
| #779 | Edit |
| #780 | </button> |
| #781 | <button |
| #782 | type="button" |
| #783 | className="btn btn-outline btn-sm" |
| #784 | style={{ color: "#dc2626", borderColor: "#fca5a5" }} |
| #785 | onClick={() => handleDeleteOpportunity(opp.id)} |
| #786 | > |
| #787 | Delete |
| #788 | </button> |
| #789 | </div> |
| #790 | </td> |
| #791 | </tr> |
| #792 | ))} |
| #793 | </tbody> |
| #794 | </table> |
| #795 | </div> |
| #796 | </div> |
| #797 | </div> |
| #798 | )} |
| #799 | </div> |
| #800 | |
| #801 | {/* Organisation Details Modal */} |
| #802 | {selectedOrg && ( |
| #803 | <div className="dash-modal-overlay" onClick={() => setSelectedOrg(null)}> |
| #804 | <div className="dash-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: "32rem" }}> |
| #805 | <div className="dash-modal-header"> |
| #806 | <h2>Organisation Details</h2> |
| #807 | <button type="button" className="dash-modal-close" onClick={() => setSelectedOrg(null)}>✕</button> |
| #808 | </div> |
| #809 | <div className="dash-modal-body"> |
| #810 | <div style={{ display: "flex", alignItems: "center", gap: "1rem", marginBottom: "1.5rem" }}> |
| #811 | {selectedOrg.logo_url ? ( |
| #812 | <img src={selectedOrg.logo_url} alt="" style={{ width: "3rem", height: "3rem", borderRadius: "50%", objectFit: "cover" }} /> |
| #813 | ) : ( |
| #814 | <span style={{ width: "3rem", height: "3rem", borderRadius: "50%", background: "#e5e7eb", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: "1.25rem", fontWeight: 700 }}> |
| #815 | {(selectedOrg.org_name || "O")[0]} |
| #816 | </span> |
| #817 | )} |
| #818 | <div> |
| #819 | <h3 style={{ margin: 0 }}>{selectedOrg.org_name}</h3> |
| #820 | <p style={{ margin: 0, color: "#6b7280", fontSize: "0.875rem" }}>{selectedOrg.org_type}</p> |
| #821 | </div> |
| #822 | </div> |
| #823 | <div style={{ display: "grid", gap: "0.75rem", fontSize: "0.875rem" }}> |
| #824 | <div><span style={{ color: "#6b7280" }}>Email:</span> <strong>{selectedOrg.org_email || "—"}</strong></div> |
| #825 | <div><span style={{ color: "#6b7280" }}>Domain:</span> {selectedOrg.org_email?.split("@")[1] || "—"}</div> |
| #826 | <div><span style={{ color: "#6b7280" }}>Country:</span> {selectedOrg.country || "—"}</div> |
| #827 | <div><span style={{ color: "#6b7280" }}>Region / City:</span> {selectedOrg.region_city || "—"}</div> |
| #828 | <div><span style={{ color: "#6b7280" }}>Type:</span> {selectedOrg.org_type || "—"}</div> |
| #829 | <div style={{ borderTop: "1px solid #e5e7eb", paddingTop: "0.75rem", marginTop: "0.25rem" }}> |
| #830 | <span style={{ color: "#6b7280", fontWeight: 600 }}>Contact Person</span> |
| #831 | </div> |
| #832 | <div><span style={{ color: "#6b7280" }}>Name:</span> {selectedOrg.contact_name || "—"}</div> |
| #833 | <div><span style={{ color: "#6b7280" }}>Email:</span> {selectedOrg.contact_email || "—"}</div> |
| #834 | <div><span style={{ color: "#6b7280" }}>Phone:</span> {selectedOrg.contact_phone || "—"}</div> |
| #835 | <div style={{ borderTop: "1px solid #e5e7eb", paddingTop: "0.75rem", marginTop: "0.25rem" }}> |
| #836 | <span style={{ color: "#6b7280" }}>Website:</span> {selectedOrg.website ? <a href={selectedOrg.website} target="_blank" rel="noopener noreferrer" style={{ color: "#0369a1" }}>{selectedOrg.website}</a> : "—"} |
| #837 | </div> |
| #838 | <div> |
| #839 | <span style={{ color: "#6b7280" }}>Status: </span> |
| #840 | {selectedOrg.verified === true && <span style={{ background: "#d1fae5", color: "#065f46", padding: "0.15rem 0.5rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>✓ Verified</span>} |
| #841 | {selectedOrg.verified === false && <span style={{ background: "#fee2e2", color: "#991b1b", padding: "0.15rem 0.5rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>✗ Rejected</span>} |
| #842 | {selectedOrg.verified === null && <span style={{ background: "#fef3c7", color: "#92400e", padding: "0.15rem 0.5rem", borderRadius: "9999px", fontSize: "0.75rem", fontWeight: 600 }}>⏳ Pending</span>} |
| #843 | </div> |
| #844 | {selectedOrg.verification_note && ( |
| #845 | <div><span style={{ color: "#6b7280" }}>Note:</span> {selectedOrg.verification_note}</div> |
| #846 | )} |
| #847 | <div><span style={{ color: "#6b7280" }}>Registered:</span> {new Date(selectedOrg.created_at).toLocaleDateString()}</div> |
| #848 | </div> |
| #849 | <div className="reg-form-actions" style={{ marginTop: "1.5rem" }}> |
| #850 | <button type="button" className="btn btn-outline" onClick={() => setSelectedOrg(null)}>Close</button> |
| #851 | {selectedOrg.verified !== true && ( |
| #852 | <button type="button" className="btn btn-primary" onClick={() => { handleVerifyOrg(selectedOrg.user_id, "verified"); setSelectedOrg(null); }}>Approve</button> |
| #853 | )} |
| #854 | <button type="button" className="btn btn-outline" style={{ color: "#dc2626", borderColor: "#fca5a5" }} onClick={() => { handleDeleteOrg(selectedOrg.user_id); setSelectedOrg(null); }}>Delete Profile</button> |
| #855 | </div> |
| #856 | </div> |
| #857 | </div> |
| #858 | </div> |
| #859 | )} |
| #860 | |
| #861 | {/* Add Opportunity Modal */} |
| #862 | {showAddOpp && ( |
| #863 | <div className="dash-modal-overlay" onClick={() => setShowAddOpp(false)}> |
| #864 | <div className="dash-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: "40rem" }}> |
| #865 | <div className="dash-modal-header"> |
| #866 | <h2>Add Opportunity</h2> |
| #867 | <button type="button" className="dash-modal-close" onClick={() => setShowAddOpp(false)}>✕</button> |
| #868 | </div> |
| #869 | <div className="dash-modal-body"> |
| #870 | <div style={{ display: "grid", gap: "1rem" }}> |
| #871 | <div> |
| #872 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Title *</label> |
| #873 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} placeholder="e.g. Software Engineering Intern" value={newOpp.title} onChange={(e) => setNewOpp({ ...newOpp, title: e.target.value })} /> |
| #874 | </div> |
| #875 | <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}> |
| #876 | <div> |
| #877 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Organisation *</label> |
| #878 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} placeholder="e.g. Safaricom" value={newOpp.org_name} onChange={(e) => setNewOpp({ ...newOpp, org_name: e.target.value })} /> |
| #879 | </div> |
| #880 | <div> |
| #881 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Type</label> |
| #882 | <select style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={newOpp.type} onChange={(e) => setNewOpp({ ...newOpp, type: e.target.value })}> |
| #883 | <option>Internship</option> |
| #884 | <option>Volunteering</option> |
| #885 | <option>Graduate Trainee</option> |
| #886 | <option>Research</option> |
| #887 | <option>Part-time</option> |
| #888 | <option>Full-time</option> |
| #889 | </select> |
| #890 | </div> |
| #891 | </div> |
| #892 | <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}> |
| #893 | <div> |
| #894 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Location *</label> |
| #895 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} placeholder="e.g. Nairobi, Kenya" value={newOpp.location} onChange={(e) => setNewOpp({ ...newOpp, location: e.target.value })} /> |
| #896 | </div> |
| #897 | <div> |
| #898 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Paid Status</label> |
| #899 | <select style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={newOpp.paid_status} onChange={(e) => setNewOpp({ ...newOpp, paid_status: e.target.value })}> |
| #900 | <option>Paid</option> |
| #901 | <option>Unpaid</option> |
| #902 | <option>Stipend</option> |
| #903 | </select> |
| #904 | </div> |
| #905 | </div> |
| #906 | <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}> |
| #907 | <div> |
| #908 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Duration</label> |
| #909 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} placeholder="e.g. 3 months" value={newOpp.duration} onChange={(e) => setNewOpp({ ...newOpp, duration: e.target.value })} /> |
| #910 | </div> |
| #911 | <div> |
| #912 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Deadline</label> |
| #913 | <input type="date" style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={newOpp.deadline} onChange={(e) => setNewOpp({ ...newOpp, deadline: e.target.value })} /> |
| #914 | </div> |
| #915 | </div> |
| #916 | <div> |
| #917 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>External Link (optional)</label> |
| #918 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} placeholder="https://example.com/apply" value={newOpp.external_link} onChange={(e) => setNewOpp({ ...newOpp, external_link: e.target.value })} /> |
| #919 | <span style={{ fontSize: "0.75rem", color: "#6b7280" }}>If provided, youth will be directed to this link to apply</span> |
| #920 | </div> |
| #921 | <div> |
| #922 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Description</label> |
| #923 | <textarea style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minHeight: "5rem" }} placeholder="Describe the opportunity..." value={newOpp.description} onChange={(e) => setNewOpp({ ...newOpp, description: e.target.value })} /> |
| #924 | </div> |
| #925 | </div> |
| #926 | <div style={{ display: "flex", gap: "0.75rem", marginTop: "1.5rem", justifyContent: "flex-end" }}> |
| #927 | <button type="button" className="btn btn-outline" onClick={() => setShowAddOpp(false)}>Cancel</button> |
| #928 | <button type="button" className="btn btn-primary" onClick={handleAddOpportunity}>Add Opportunity</button> |
| #929 | </div> |
| #930 | </div> |
| #931 | </div> |
| #932 | </div> |
| #933 | )} |
| #934 | |
| #935 | {/* Edit Opportunity Modal */} |
| #936 | {editOpp && ( |
| #937 | <div className="dash-modal-overlay" onClick={() => setEditOpp(null)}> |
| #938 | <div className="dash-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: "40rem" }}> |
| #939 | <div className="dash-modal-header"> |
| #940 | <h2>Edit Opportunity</h2> |
| #941 | <button type="button" className="dash-modal-close" onClick={() => setEditOpp(null)}>✕</button> |
| #942 | </div> |
| #943 | <div className="dash-modal-body"> |
| #944 | <div style={{ display: "grid", gap: "1rem" }}> |
| #945 | <div> |
| #946 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Title *</label> |
| #947 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.title} onChange={(e) => setEditOpp({ ...editOpp, title: e.target.value })} /> |
| #948 | </div> |
| #949 | <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}> |
| #950 | <div> |
| #951 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Organisation</label> |
| #952 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.org_name} onChange={(e) => setEditOpp({ ...editOpp, org_name: e.target.value })} /> |
| #953 | </div> |
| #954 | <div> |
| #955 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Type</label> |
| #956 | <select style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.type} onChange={(e) => setEditOpp({ ...editOpp, type: e.target.value })}> |
| #957 | <option>Internship</option> |
| #958 | <option>Volunteering</option> |
| #959 | <option>Graduate Trainee</option> |
| #960 | <option>Research</option> |
| #961 | <option>Part-time</option> |
| #962 | <option>Full-time</option> |
| #963 | </select> |
| #964 | </div> |
| #965 | </div> |
| #966 | <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}> |
| #967 | <div> |
| #968 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Location</label> |
| #969 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.location} onChange={(e) => setEditOpp({ ...editOpp, location: e.target.value })} /> |
| #970 | </div> |
| #971 | <div> |
| #972 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Status</label> |
| #973 | <select style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.status} onChange={(e) => setEditOpp({ ...editOpp, status: e.target.value })}> |
| #974 | <option>Active</option> |
| #975 | <option>Closed</option> |
| #976 | <option>Draft</option> |
| #977 | </select> |
| #978 | </div> |
| #979 | </div> |
| #980 | <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}> |
| #981 | <div> |
| #982 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Duration</label> |
| #983 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.duration || ""} onChange={(e) => setEditOpp({ ...editOpp, duration: e.target.value })} /> |
| #984 | </div> |
| #985 | <div> |
| #986 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Deadline</label> |
| #987 | <input type="date" style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.deadline || ""} onChange={(e) => setEditOpp({ ...editOpp, deadline: e.target.value })} /> |
| #988 | </div> |
| #989 | </div> |
| #990 | <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}> |
| #991 | <div> |
| #992 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Payment Status</label> |
| #993 | <select style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.paid_status || ""} onChange={(e) => setEditOpp({ ...editOpp, paid_status: e.target.value })}> |
| #994 | <option value="">Select...</option> |
| #995 | <option>Paid</option> |
| #996 | <option>Unpaid</option> |
| #997 | <option>Stipend</option> |
| #998 | </select> |
| #999 | </div> |
| #1000 | <div> |
| #1001 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Stipend/Amount</label> |
| #1002 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.stipend_amount || ""} onChange={(e) => setEditOpp({ ...editOpp, stipend_amount: e.target.value })} placeholder="e.g. 500,000 TZS/month" /> |
| #1003 | </div> |
| #1004 | </div> |
| #1005 | <div> |
| #1006 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>External Link</label> |
| #1007 | <input style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem" }} value={editOpp.external_link || ""} onChange={(e) => setEditOpp({ ...editOpp, external_link: e.target.value })} placeholder="https://..." /> |
| #1008 | </div> |
| #1009 | <div> |
| #1010 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Responsibilities</label> |
| #1011 | <textarea style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minHeight: "4rem" }} value={editOpp.responsibilities || ""} onChange={(e) => setEditOpp({ ...editOpp, responsibilities: e.target.value })} /> |
| #1012 | </div> |
| #1013 | <div> |
| #1014 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Learning Outcomes</label> |
| #1015 | <textarea style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minHeight: "3rem" }} value={editOpp.learning_outcomes || ""} onChange={(e) => setEditOpp({ ...editOpp, learning_outcomes: e.target.value })} /> |
| #1016 | </div> |
| #1017 | <div> |
| #1018 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Expectations</label> |
| #1019 | <textarea style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minHeight: "3rem" }} value={editOpp.expectations || ""} onChange={(e) => setEditOpp({ ...editOpp, expectations: e.target.value })} /> |
| #1020 | </div> |
| #1021 | <div> |
| #1022 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Benefits</label> |
| #1023 | <textarea style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minHeight: "3rem" }} value={editOpp.benefits || ""} onChange={(e) => setEditOpp({ ...editOpp, benefits: e.target.value })} /> |
| #1024 | </div> |
| #1025 | <div> |
| #1026 | <label style={{ fontSize: "0.85rem", fontWeight: 600, display: "block", marginBottom: "0.25rem" }}>Description</label> |
| #1027 | <textarea style={{ width: "100%", padding: "0.5rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", minHeight: "3rem" }} value={editOpp.description || ""} onChange={(e) => setEditOpp({ ...editOpp, description: e.target.value })} /> |
| #1028 | </div> |
| #1029 | </div> |
| #1030 | <div style={{ display: "flex", gap: "0.75rem", marginTop: "1.5rem", justifyContent: "flex-end" }}> |
| #1031 | <button type="button" className="btn btn-outline" onClick={() => setEditOpp(null)}>Cancel</button> |
| #1032 | <button type="button" className="btn btn-primary" onClick={handleSaveOpportunity}>Save Changes</button> |
| #1033 | </div> |
| #1034 | </div> |
| #1035 | </div> |
| #1036 | </div> |
| #1037 | )} |
| #1038 | </div> |
| #1039 | ); |
| #1040 | } |
| #1041 |