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:
parent
ea911a67e6
commit
2ff1292bf6
3 changed files with 81 additions and 3 deletions
|
|
@ -14,6 +14,7 @@ export interface Opportunity {
|
|||
salary_max: number;
|
||||
experience_level: string;
|
||||
arm_affiliation: string;
|
||||
ecosystem?: string;
|
||||
posted_by_id: string;
|
||||
aethex_creators: OpportunityPoster;
|
||||
status: string;
|
||||
|
|
@ -40,12 +41,14 @@ export interface CreateOpportunityData {
|
|||
salary_max?: number;
|
||||
experience_level?: string;
|
||||
arm_affiliation: string;
|
||||
ecosystem?: string;
|
||||
}
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
|
||||
export async function getOpportunities(filters?: {
|
||||
arm?: string;
|
||||
ecosystem?: string;
|
||||
search?: string;
|
||||
jobType?: string;
|
||||
experienceLevel?: string;
|
||||
|
|
@ -55,6 +58,7 @@ export async function getOpportunities(filters?: {
|
|||
}): Promise<OpportunitiesResponse> {
|
||||
const params = new URLSearchParams();
|
||||
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?.jobType) params.append("jobType", filters.jobType);
|
||||
if (filters?.experienceLevel)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import Layout from "@/components/Layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 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() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [opportunities, setOpportunities] = useState<Opportunity[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
|
@ -17,6 +27,9 @@ export default function OpportunitiesHub() {
|
|||
const [selectedArm, setSelectedArm] = useState<string | undefined>(
|
||||
searchParams.get("arm") || undefined,
|
||||
);
|
||||
const [selectedEcosystem, setSelectedEcosystem] = useState<string>(
|
||||
searchParams.get("ecosystem") || "all",
|
||||
);
|
||||
const [page, setPage] = useState(parseInt(searchParams.get("page") || "1"));
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
|
||||
|
|
@ -26,6 +39,7 @@ export default function OpportunitiesHub() {
|
|||
try {
|
||||
const result = await getOpportunities({
|
||||
arm: selectedArm,
|
||||
ecosystem: selectedEcosystem !== "all" ? selectedEcosystem : undefined,
|
||||
search: search || undefined,
|
||||
page,
|
||||
limit: 12,
|
||||
|
|
@ -37,6 +51,7 @@ export default function OpportunitiesHub() {
|
|||
// Update URL params
|
||||
const params = new URLSearchParams();
|
||||
if (selectedArm) params.set("arm", selectedArm);
|
||||
if (selectedEcosystem && selectedEcosystem !== "all") params.set("ecosystem", selectedEcosystem);
|
||||
if (search) params.set("search", search);
|
||||
if (page > 1) params.set("page", String(page));
|
||||
setSearchParams(params);
|
||||
|
|
@ -49,7 +64,7 @@ export default function OpportunitiesHub() {
|
|||
};
|
||||
|
||||
fetchOpportunities();
|
||||
}, [selectedArm, search, page, setSearchParams]);
|
||||
}, [selectedArm, selectedEcosystem, search, page, setSearchParams]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -61,6 +76,11 @@ export default function OpportunitiesHub() {
|
|||
setPage(1);
|
||||
};
|
||||
|
||||
const handleEcosystemChange = (ecosystem: string) => {
|
||||
setSelectedEcosystem(ecosystem);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="relative min-h-screen bg-black text-white overflow-hidden">
|
||||
|
|
@ -98,7 +118,7 @@ export default function OpportunitiesHub() {
|
|||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
|
|
@ -115,6 +135,25 @@ export default function OpportunitiesHub() {
|
|||
</Button>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
|
@ -151,6 +190,7 @@ export default function OpportunitiesHub() {
|
|||
onClick={() => {
|
||||
setSearch("");
|
||||
setSelectedArm(undefined);
|
||||
setSelectedEcosystem("all");
|
||||
setPage(1);
|
||||
}}
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ const ARMS = [
|
|||
{ 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() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
|
@ -52,6 +61,7 @@ export default function OpportunityPostForm() {
|
|||
salary_max: undefined,
|
||||
experience_level: "Mid",
|
||||
arm_affiliation: "nexus",
|
||||
ecosystem: "web",
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
|
@ -267,6 +277,30 @@ export default function OpportunityPostForm() {
|
|||
)}
|
||||
</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 */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
|
|
|
|||
Loading…
Reference in a new issue