Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/components/member-status-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { getGitHubIssueUrl, formatTime } from "@/lib/utils";
import type { ActiveSession } from "@/types";
import type { getSpaceMembersWithProfiles } from "@/lib/supabase/queries";

type Members = Awaited<ReturnType<typeof getSpaceMembersWithProfiles>>;

interface MemberStatusCardProps {
members: Members | undefined;
activeSessions: ActiveSession[] | undefined;
dailyDurations: Record<string, number>;
}

export function MemberStatusCard({ members, activeSessions, dailyDurations }: MemberStatusCardProps) {
return (
<Card className="py-0">
<CardHeader>
<CardTitle>Members</CardTitle>
</CardHeader>
<CardContent className="p-0">
{members && members.length > 0 ? (
<div className="divide-y">
{members.map(({ member, profile }) => {
const activeSession = activeSessions?.find(
(s) => s?.space_member?.user_id === member.user_id
);
const duration = formatTime(dailyDurations[member.user_id] || 0);
return (
<div
key={member.id}
className="flex items-center gap-3 p-3 hover:bg-gray-50 rounded-xl"
>
<Avatar className="h-8 w-8">
<AvatarImage src={profile.avatar_url || undefined} />
<AvatarFallback>{profile.full_name?.charAt(0) || "?"}</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium leading-none">
{profile.full_name || "Unknown"}
</p>
<p className="text-xs text-muted-foreground capitalize">
{member.role}
</p>
</div>
<div className="flex flex-col items-end">
<div className="flex gap-2">
<Badge variant="secondary" className="text-xs capitalize">
{member.status || "offline"}
</Badge>
{member.status === "online" && (
<Badge variant="outline" className="text-xs capitalize">
{member.location || "remote"}
</Badge>
)}
</div>
{activeSession ? (
<a
href={getGitHubIssueUrl(activeSession.track)}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 hover:text-blue-700 hover:underline mt-1"
>
{activeSession.track.title}
</a>
) : (
<span className="text-xs text-gray-500 mt-1">No active ticket</span>
)}
<span className="text-xs text-gray-600 mt-1">{duration}</span>
</div>
</div>
);
})}
</div>
) : (
<p className="text-center text-gray-500 py-3 text-sm">No members found.</p>
)}
</CardContent>
</Card>
);
}
20 changes: 16 additions & 4 deletions src/components/search-results-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,26 @@ import remarkGfm from 'remark-gfm'
import { Avatar, AvatarImage, AvatarFallback } from './ui/avatar'
import { getTextColorForBackground } from "@/lib/utils"

import type { Track, ActiveSession, Tag } from '@/types'
import type { UseMutationResult } from '@tanstack/react-query'

interface SearchResultsListProps {
searchResults: GitHubIssue[]
getTrackForIssue: (issue: GitHubIssue) => any
getTrackForIssue: (issue: GitHubIssue) => Track | null
handleCreateTrack: (issue: GitHubIssue) => void
handleStartSession: (trackId: string) => void
activeSession: any
startSessionMutation: any
endSessionMutation: any
activeSession: ActiveSession | null
startSessionMutation: UseMutationResult<unknown, unknown, string>
endSessionMutation: UseMutationResult<
unknown,
unknown,
{
sessionId: string
message: string
skipSummary: boolean
selectedTags: Tag[]
}
>
setShowEndSessionDialog: (show: boolean) => void
isCurrentSessionTrack: (trackId: string) => boolean
getSessionCount: (trackId: string) => number
Expand Down
1 change: 0 additions & 1 deletion src/components/ui/multi-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ export const MultiSelect = React.forwardRef<
animation = 0,
maxCount = 3,
modalPopover = false,
asChild = false,
className,
...props
},
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/api/use-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
if (!octokit) throw new Error("Octokit not initialized");
await octokit.rest.users.getAuthenticated();
return githubToken;
} catch (error: any) {
if (error?.status === 401) {
} catch (error: unknown) {
if (typeof error === 'object' && error && 'status' in error && (error as { status?: number }).status === 401) {
// Token is expired, trigger a new OAuth sign-in
await loginWithGitHub();
return null;
Expand Down
45 changes: 44 additions & 1 deletion src/hooks/api/use-space-members.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from "react";
import { useQuery } from "@tanstack/react-query";
import {
getSpaceMembersWithProfiles,
getActiveSession,
getClosedSessions,
} from "@/lib/supabase/queries";

export function useSpaceMembers(slug: string) {
Expand All @@ -19,7 +21,7 @@ export function useSpaceMembers(slug: string) {
try {
if (!member.user_id) return null;
return await getActiveSession(member.user_id);
} catch (error) {
} catch {
// It's better to return null and filter later than to throw
return null;
}
Expand All @@ -30,9 +32,50 @@ export function useSpaceMembers(slug: string) {
enabled: !!members,
});

const spaceId = members && members.length > 0 ? members[0].member.space_id : null;

const { data: closedSessions } = useQuery({
queryKey: ["closedSessionsToday", spaceId],
queryFn: () => getClosedSessions(spaceId || ""),
enabled: !!spaceId,
});

const dailyDurations = React.useMemo(() => {
if (!closedSessions) return {} as Record<string, number>;

const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);

const durations: Record<string, number> = {};

closedSessions.forEach((session) => {
if (!session.ended_at) return;
if (new Date(session.ended_at) < startOfDay) return;
const userId = session.space_member?.user_id;
if (!userId) return;
const start = new Date(session.started_at).getTime();
const end = new Date(session.ended_at).getTime();
durations[userId] = (durations[userId] || 0) + (end - start);
});

if (activeSessions) {
const now = Date.now();
activeSessions.forEach((session) => {
const userId = session?.space_member?.user_id;
if (!userId) return;
const start = new Date(session.started_at).getTime();
if (start < startOfDay.getTime()) return;
durations[userId] = (durations[userId] || 0) + (now - start);
});
}

return durations;
}, [closedSessions, activeSessions]);

return {
members,
isLoading,
activeSessions,
dailyDurations,
};
}
6 changes: 5 additions & 1 deletion src/lib/github/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export async function getRepo(org: string, repo: string) {
}
}

export async function searchIssues(orgs: string[] | string, query: string = '', options: {} = {}): Promise<GitHubIssue[]> {
export async function searchIssues(
orgs: string[] | string,
query: string = '',
options: Record<string, unknown> = {}
): Promise<GitHubIssue[]> {
const octokit = await getOctokitClient();
if (!octokit) throw new Error("Octokit client not initialized");
if (!orgs || orgs.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/supabase/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const getUserSpaces = async () => {
if (error) throw new Error(error.message);
// Flatten the organizations and include role
return (
data?.map((row: any) => ({
data?.map((row: { space: Space; role: string }) => ({
...row.space,
member_role: row.role,
})) || []
Expand Down
28 changes: 28 additions & 0 deletions src/routes/space/$slug/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { DashboardStats } from '@/components/dashboard-stats'
import { SessionActivityChart } from '@/components/session-activity-chart'
import { TrackStatsChart } from '@/components/track-stats-chart'
import { ActiveSessionsList } from '@/components/active-sessions-list'
import { MemberStatusCard } from '@/components/member-status-card'
import { useSpaceDashboard } from '@/hooks/api/use-space-dashboard'
import { useSpaceMembers } from '@/hooks/api/use-space-members'

export const Route = createFileRoute('/space/$slug/')({
component: SpaceHome,
Expand All @@ -26,6 +28,7 @@ function SpaceHome() {
const [openSections, setOpenSections] = useLocalStorage('space-sections', {
stats: true,
activeSessions: true,
members: true,
sessionActivity: true,
trackStats: true
})
Expand All @@ -42,6 +45,12 @@ function SpaceHome() {
trackStatsData,
} = useSpaceDashboard(slug);

const {
members,
activeSessions: memberActiveSessions,
dailyDurations,
} = useSpaceMembers(slug);

return (
<div className='space-y-6'>
<div className="flex items-center justify-between space-y-2">
Expand Down Expand Up @@ -99,6 +108,25 @@ function SpaceHome() {
<ActiveSessionsList sessions={spaceActiveSessions || []} />
</CollapsibleContent>
</Collapsible>

{/* Members Widget */}
<Collapsible
open={openSections.members}
onOpenChange={(open) => setOpenSections(prev => ({ ...prev, members: open }))}
className="space-y-2"
>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Members</h3>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-8 h-8 p-0">
<ChevronDown className={cn("h-4 w-4 transition-transform", openSections.members && "rotate-180")} />
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<MemberStatusCard members={members} activeSessions={memberActiveSessions} dailyDurations={dailyDurations} />
</CollapsibleContent>
</Collapsible>
</div>

{/* Charts Section */}
Expand Down