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;
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue