Skip to content

Commit 640bb28

Browse files
kanekoshoyuclaude
andcommitted
feat: add Orbit Signal dashboard page
- Add new /dashboard/orbit-signal page to display hiring intent data - Create HiringIntentDashboard component with space filtering - Add getHiringIntentsBySpace API utility function - Display orbit signals with category badges, confidence scores, and company details - Include responsive grid layout with loading, error, and empty states 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 8d4b7c5 commit 640bb28

File tree

3 files changed

+300
-0
lines changed

3 files changed

+300
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5+
import { Badge } from "@/components/ui/badge";
6+
import SpaceSelector from "@/components/interactive/SpaceSelector";
7+
import { getHiringIntentsBySpace, type HiringIntent } from "@/lib/utils";
8+
import { EXTERNAL } from "@/constant";
9+
10+
export default function HiringIntentDashboard() {
11+
const [hiringIntents, setHiringIntents] = useState<HiringIntent[]>([]);
12+
const [selectedSpaceId, setSelectedSpaceId] = useState<string | null>(null);
13+
const [isLoading, setIsLoading] = useState(true);
14+
const [error, setError] = useState<string | null>(null);
15+
16+
useEffect(() => {
17+
fetchHiringIntents();
18+
}, [selectedSpaceId]);
19+
20+
const fetchHiringIntents = async () => {
21+
setIsLoading(true);
22+
setError(null);
23+
24+
try {
25+
const spaceIdNumber = selectedSpaceId && selectedSpaceId !== "all" ? parseInt(selectedSpaceId) : null;
26+
const result = await getHiringIntentsBySpace(spaceIdNumber, EXTERNAL.directus_url);
27+
28+
if (result.success && result.hiringIntents) {
29+
setHiringIntents(result.hiringIntents);
30+
} else {
31+
setError(result.error || "Failed to fetch orbit signals");
32+
}
33+
} catch (err) {
34+
setError("An error occurred while fetching orbit signals");
35+
console.error("Error fetching orbit signals:", err);
36+
} finally {
37+
setIsLoading(false);
38+
}
39+
};
40+
41+
const getCategoryColor = (category?: string) => {
42+
switch (category) {
43+
case "funding":
44+
return "bg-blue-100 text-blue-800";
45+
case "growth":
46+
return "bg-green-100 text-green-800";
47+
case "replacement":
48+
return "bg-amber-100 text-amber-800";
49+
default:
50+
return "bg-gray-100 text-gray-800";
51+
}
52+
};
53+
54+
const getConfidenceColor = (confidence?: number) => {
55+
if (!confidence) return "bg-gray-100 text-gray-800";
56+
if (confidence >= 80) return "bg-green-100 text-green-800";
57+
if (confidence >= 50) return "bg-yellow-100 text-yellow-800";
58+
return "bg-red-100 text-red-800";
59+
};
60+
61+
const formatDate = (dateString?: string) => {
62+
if (!dateString) return "N/A";
63+
const date = new Date(dateString);
64+
return date.toLocaleDateString("en-US", {
65+
year: "numeric",
66+
month: "short",
67+
day: "numeric",
68+
});
69+
};
70+
71+
const handleSpaceChange = (spaceId: string | null) => {
72+
setSelectedSpaceId(spaceId);
73+
};
74+
75+
return (
76+
<div className="space-y-6">
77+
{/* Space Selector */}
78+
<div className="flex items-center justify-between">
79+
<div className="flex items-center gap-3">
80+
<label className="text-sm font-medium text-gray-700">Filter by Space:</label>
81+
<SpaceSelector
82+
onSpaceChange={handleSpaceChange}
83+
selectedSpaceId={selectedSpaceId}
84+
showAllOption={true}
85+
className="w-auto"
86+
/>
87+
</div>
88+
</div>
89+
90+
{/* Loading State */}
91+
{isLoading && (
92+
<div className="flex items-center justify-center py-12">
93+
<div className="flex items-center gap-2">
94+
<div className="w-6 h-6 animate-spin rounded-full border-4 border-gray-200 border-t-blue-600"></div>
95+
<span className="text-gray-600">Loading orbit signals...</span>
96+
</div>
97+
</div>
98+
)}
99+
100+
{/* Error State */}
101+
{error && !isLoading && (
102+
<Card className="border-red-200 bg-red-50">
103+
<CardContent className="pt-6">
104+
<p className="text-red-600">{error}</p>
105+
</CardContent>
106+
</Card>
107+
)}
108+
109+
{/* Empty State */}
110+
{!isLoading && !error && hiringIntents.length === 0 && (
111+
<Card>
112+
<CardContent className="pt-6">
113+
<div className="text-center py-8">
114+
<p className="text-gray-500 text-lg">No orbit signals found</p>
115+
<p className="text-gray-400 text-sm mt-2">
116+
{selectedSpaceId && selectedSpaceId !== "all"
117+
? "Try selecting a different space or 'All' to see all orbit signals."
118+
: "Orbit signals will appear here once they are created."}
119+
</p>
120+
</div>
121+
</CardContent>
122+
</Card>
123+
)}
124+
125+
{/* Orbit Signals Grid */}
126+
{!isLoading && !error && hiringIntents.length > 0 && (
127+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
128+
{hiringIntents.map((intent) => (
129+
<Card key={intent.id} className="hover:shadow-lg transition-shadow">
130+
<CardHeader>
131+
<div className="flex items-start justify-between">
132+
<CardTitle className="text-lg">
133+
{intent.company_profile?.name || "Unknown Company"}
134+
</CardTitle>
135+
{intent.category && (
136+
<Badge className={getCategoryColor(intent.category)}>
137+
{intent.category}
138+
</Badge>
139+
)}
140+
</div>
141+
</CardHeader>
142+
<CardContent className="space-y-3">
143+
{/* Reason */}
144+
{intent.reason && (
145+
<div>
146+
<p className="text-xs font-medium text-gray-500 mb-1">Reason</p>
147+
<p className="text-sm text-gray-700 line-clamp-3">{intent.reason}</p>
148+
</div>
149+
)}
150+
151+
{/* Potential Role */}
152+
{intent.potential_role && (
153+
<div>
154+
<p className="text-xs font-medium text-gray-500 mb-1">Potential Role</p>
155+
<p className="text-sm text-gray-700">
156+
{typeof intent.potential_role === "string"
157+
? intent.potential_role
158+
: JSON.stringify(intent.potential_role)}
159+
</p>
160+
</div>
161+
)}
162+
163+
{/* Skills */}
164+
{intent.skill && (
165+
<div>
166+
<p className="text-xs font-medium text-gray-500 mb-1">Skills</p>
167+
<p className="text-sm text-gray-700">
168+
{typeof intent.skill === "string"
169+
? intent.skill
170+
: JSON.stringify(intent.skill)}
171+
</p>
172+
</div>
173+
)}
174+
175+
{/* Confidence Score */}
176+
{intent.confidence !== undefined && intent.confidence !== null && (
177+
<div className="flex items-center justify-between">
178+
<p className="text-xs font-medium text-gray-500">Confidence</p>
179+
<Badge className={getConfidenceColor(intent.confidence)}>
180+
{intent.confidence}%
181+
</Badge>
182+
</div>
183+
)}
184+
185+
{/* Date Created */}
186+
<div className="pt-2 border-t border-gray-100">
187+
<p className="text-xs text-gray-400">
188+
Created {formatDate(intent.date_created)}
189+
</p>
190+
</div>
191+
</CardContent>
192+
</Card>
193+
))}
194+
</div>
195+
)}
196+
</div>
197+
);
198+
}

src/lib/utils.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,72 @@ export async function deleteSpace(
14691469
}
14701470
}
14711471

1472+
// Hiring Intent types
1473+
export type HiringIntent = {
1474+
id: number;
1475+
user_created?: string;
1476+
date_created?: string;
1477+
user_updated?: string;
1478+
date_updated?: string;
1479+
company_profile?: any;
1480+
reason?: string;
1481+
potential_role?: any;
1482+
skill?: any;
1483+
category?: 'funding' | 'growth' | 'replacement';
1484+
space?: number;
1485+
confidence?: number;
1486+
}
1487+
1488+
// Fetch hiring intents by space
1489+
export async function getHiringIntentsBySpace(
1490+
spaceId: number | null,
1491+
directusUrl: string
1492+
): Promise<{ success: boolean; hiringIntents?: HiringIntent[]; error?: string }> {
1493+
try {
1494+
const user = await getUserProfile(directusUrl);
1495+
if (!user) {
1496+
return {
1497+
success: false,
1498+
error: "User not authenticated"
1499+
};
1500+
}
1501+
1502+
// Build URL with optional space filter
1503+
let url = `${directusUrl}/items/hiring_intent?sort[]=-date_created&limit=100&fields=id,date_created,date_updated,company_profile.*,reason,potential_role,skill,category,space,confidence`;
1504+
1505+
if (spaceId) {
1506+
url += `&filter[space][_eq]=${spaceId}`;
1507+
}
1508+
1509+
const response = await fetch(url, {
1510+
credentials: 'include',
1511+
headers: {
1512+
'Accept': 'application/json'
1513+
}
1514+
});
1515+
1516+
if (!response.ok) {
1517+
const errorText = await response.text().catch(() => 'Unknown error');
1518+
return {
1519+
success: false,
1520+
error: `HTTP ${response.status}: ${errorText}`
1521+
};
1522+
}
1523+
1524+
const result = await response.json();
1525+
return {
1526+
success: true,
1527+
hiringIntents: result.data || []
1528+
};
1529+
} catch (error) {
1530+
console.error('Error fetching hiring intents:', error);
1531+
return {
1532+
success: false,
1533+
error: error instanceof Error ? error.message : 'Unknown error occurred'
1534+
};
1535+
}
1536+
}
1537+
14721538
// Get package version from package.json
14731539
export async function getPackageVersion(): Promise<string> {
14741540
try {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
import Layout from "@/layouts/Layout.astro";
3+
import { Sidebar } from "@/components/interactive/Sidebar";
4+
import HiringIntentDashboard from "@/components/interactive/HiringIntentDashboard";
5+
import AuthGuard from "@/components/interactive/AuthGuard";
6+
---
7+
8+
<Layout
9+
name="Bounteer"
10+
title="Orbit Signal - Bounteer"
11+
description="Manage and review orbit signals from your organization."
12+
url="https://bounteer.com/dashboard/orbit-signal"
13+
image_url="https://bounteer.com/og.png"
14+
>
15+
<AuthGuard client:load>
16+
<main class="bg-gray-50 min-h-screen flex flex-col lg:flex-row">
17+
<!-- Sidebar handles both mobile topbar + desktop sidebar -->
18+
<Sidebar client:load />
19+
20+
<!-- Main content -->
21+
<section class="flex-1 px-4 py-6 md:px-6 md:py-10">
22+
<!-- Page Header -->
23+
<div class="mb-6">
24+
<h1 class="text-xl md:text-2xl font-semibold tracking-tight mb-2">
25+
Orbit Signal
26+
</h1>
27+
<p class="text-xs md:text-sm text-muted-foreground">
28+
Manage and review orbit signals from your organization.
29+
</p>
30+
</div>
31+
32+
<HiringIntentDashboard client:load />
33+
</section>
34+
</main>
35+
</AuthGuard>
36+
</Layout>

0 commit comments

Comments
 (0)