Add ecosystem filtering to opportunities and update posting form

Adds an 'ecosystem' field to opportunities, enabling filtering on the OpportunitiesHub page and inclusion in the OpportunityPostForm. Also resolves a navigation import error in OpportunitiesHub.tsx.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: c9ce4106-de8c-4aae-ad20-8c89a4901395
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/aPpJgbb
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-13 02:25:12 +00:00
parent ea911a67e6
commit 2ff1292bf6
3 changed files with 81 additions and 3 deletions

View file

@ -14,6 +14,7 @@ export interface Opportunity {
salary_max: number; salary_max: number;
experience_level: string; experience_level: string;
arm_affiliation: string; arm_affiliation: string;
ecosystem?: string;
posted_by_id: string; posted_by_id: string;
aethex_creators: OpportunityPoster; aethex_creators: OpportunityPoster;
status: string; status: string;
@ -40,12 +41,14 @@ export interface CreateOpportunityData {
salary_max?: number; salary_max?: number;
experience_level?: string; experience_level?: string;
arm_affiliation: string; arm_affiliation: string;
ecosystem?: string;
} }
const API_BASE = import.meta.env.VITE_API_BASE || ""; const API_BASE = import.meta.env.VITE_API_BASE || "";
export async function getOpportunities(filters?: { export async function getOpportunities(filters?: {
arm?: string; arm?: string;
ecosystem?: string;
search?: string; search?: string;
jobType?: string; jobType?: string;
experienceLevel?: string; experienceLevel?: string;
@ -55,6 +58,7 @@ export async function getOpportunities(filters?: {
}): Promise<OpportunitiesResponse> { }): Promise<OpportunitiesResponse> {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filters?.arm) params.append("arm", filters.arm); if (filters?.arm) params.append("arm", filters.arm);
if (filters?.ecosystem) params.append("ecosystem", filters.ecosystem);
if (filters?.search) params.append("search", filters.search); if (filters?.search) params.append("search", filters.search);
if (filters?.jobType) params.append("jobType", filters.jobType); if (filters?.jobType) params.append("jobType", filters.jobType);
if (filters?.experienceLevel) if (filters?.experienceLevel)

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams, useNavigate } from "react-router-dom";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -9,7 +9,17 @@ import { OpportunityCard } from "@/components/creator-network/OpportunityCard";
import { ArmFilter } from "@/components/creator-network/ArmFilter"; import { ArmFilter } from "@/components/creator-network/ArmFilter";
import type { Opportunity } from "@/api/opportunities"; import type { Opportunity } from "@/api/opportunities";
const ECOSYSTEMS = [
{ value: "all", label: "All" },
{ value: "roblox", label: "Roblox" },
{ value: "unity", label: "Unity" },
{ value: "web", label: "Web" },
{ value: "audio", label: "Audio" },
{ value: "design", label: "Design" },
];
export default function OpportunitiesHub() { export default function OpportunitiesHub() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [opportunities, setOpportunities] = useState<Opportunity[]>([]); const [opportunities, setOpportunities] = useState<Opportunity[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -17,6 +27,9 @@ export default function OpportunitiesHub() {
const [selectedArm, setSelectedArm] = useState<string | undefined>( const [selectedArm, setSelectedArm] = useState<string | undefined>(
searchParams.get("arm") || undefined, searchParams.get("arm") || undefined,
); );
const [selectedEcosystem, setSelectedEcosystem] = useState<string>(
searchParams.get("ecosystem") || "all",
);
const [page, setPage] = useState(parseInt(searchParams.get("page") || "1")); const [page, setPage] = useState(parseInt(searchParams.get("page") || "1"));
const [totalPages, setTotalPages] = useState(0); const [totalPages, setTotalPages] = useState(0);
@ -26,6 +39,7 @@ export default function OpportunitiesHub() {
try { try {
const result = await getOpportunities({ const result = await getOpportunities({
arm: selectedArm, arm: selectedArm,
ecosystem: selectedEcosystem !== "all" ? selectedEcosystem : undefined,
search: search || undefined, search: search || undefined,
page, page,
limit: 12, limit: 12,
@ -37,6 +51,7 @@ export default function OpportunitiesHub() {
// Update URL params // Update URL params
const params = new URLSearchParams(); const params = new URLSearchParams();
if (selectedArm) params.set("arm", selectedArm); if (selectedArm) params.set("arm", selectedArm);
if (selectedEcosystem && selectedEcosystem !== "all") params.set("ecosystem", selectedEcosystem);
if (search) params.set("search", search); if (search) params.set("search", search);
if (page > 1) params.set("page", String(page)); if (page > 1) params.set("page", String(page));
setSearchParams(params); setSearchParams(params);
@ -49,7 +64,7 @@ export default function OpportunitiesHub() {
}; };
fetchOpportunities(); fetchOpportunities();
}, [selectedArm, search, page, setSearchParams]); }, [selectedArm, selectedEcosystem, search, page, setSearchParams]);
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -61,6 +76,11 @@ export default function OpportunitiesHub() {
setPage(1); setPage(1);
}; };
const handleEcosystemChange = (ecosystem: string) => {
setSelectedEcosystem(ecosystem);
setPage(1);
};
return ( return (
<Layout> <Layout>
<div className="relative min-h-screen bg-black text-white overflow-hidden"> <div className="relative min-h-screen bg-black text-white overflow-hidden">
@ -98,7 +118,7 @@ export default function OpportunitiesHub() {
</div> </div>
{/* Search Bar */} {/* Search Bar */}
<form onSubmit={handleSearch} className="max-w-2xl mx-auto"> <form onSubmit={handleSearch} className="max-w-2xl mx-auto mb-6">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<Input <Input
@ -115,6 +135,25 @@ export default function OpportunitiesHub() {
</Button> </Button>
</div> </div>
</form> </form>
{/* Ecosystem Filter Tabs */}
<div className="flex flex-wrap justify-center gap-2">
{ECOSYSTEMS.map((eco) => (
<Button
key={eco.value}
variant={selectedEcosystem === eco.value ? "default" : "outline"}
size="sm"
onClick={() => handleEcosystemChange(eco.value)}
className={
selectedEcosystem === eco.value
? "bg-cyan-500 text-black hover:bg-cyan-400"
: "border-slate-600 text-slate-300 hover:bg-slate-800"
}
>
{eco.label}
</Button>
))}
</div>
</div> </div>
</section> </section>
@ -151,6 +190,7 @@ export default function OpportunitiesHub() {
onClick={() => { onClick={() => {
setSearch(""); setSearch("");
setSelectedArm(undefined); setSelectedArm(undefined);
setSelectedEcosystem("all");
setPage(1); setPage(1);
}} }}
variant="outline" variant="outline"

View file

@ -37,6 +37,15 @@ const ARMS = [
{ value: "nexus", label: "Nexus (Talent Marketplace)" }, { value: "nexus", label: "Nexus (Talent Marketplace)" },
]; ];
const ECOSYSTEMS = [
{ value: "roblox", label: "Roblox" },
{ value: "unity", label: "Unity" },
{ value: "web", label: "Web Development" },
{ value: "audio", label: "Audio / Music" },
{ value: "design", label: "Design / Art" },
{ value: "other", label: "Other" },
];
export default function OpportunityPostForm() { export default function OpportunityPostForm() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
@ -52,6 +61,7 @@ export default function OpportunityPostForm() {
salary_max: undefined, salary_max: undefined,
experience_level: "Mid", experience_level: "Mid",
arm_affiliation: "nexus", arm_affiliation: "nexus",
ecosystem: "web",
}); });
if (!user) { if (!user) {
@ -267,6 +277,30 @@ export default function OpportunityPostForm() {
)} )}
</div> </div>
{/* Ecosystem */}
<div className="space-y-2">
<label className="block text-sm font-medium text-white">
Ecosystem / Platform
</label>
<Select
value={formData.ecosystem || "web"}
onValueChange={(value) =>
setFormData({ ...formData, ecosystem: value })
}
>
<SelectTrigger className="bg-slate-800 border-slate-600 text-white">
<SelectValue placeholder="Select ecosystem" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-600 text-white">
{ECOSYSTEMS.map((eco) => (
<SelectItem key={eco.value} value={eco.value}>
{eco.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Job Type */} {/* Job Type */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-white"> <label className="block text-sm font-medium text-white">