mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
chore: clean diagnostics and prepare for designer structural refactor (stub legacy useActiveStudy)
This commit is contained in:
@@ -1,146 +1,44 @@
|
||||
"use client";
|
||||
/**
|
||||
* @file useActiveStudy.ts
|
||||
*
|
||||
* Legacy placeholder for the deprecated `useActiveStudy` hook.
|
||||
*
|
||||
* This file exists solely to satisfy lingering TypeScript project
|
||||
* service references (e.g. editor cached import paths) after the
|
||||
* migration to the unified `useSelectedStudyDetails` hook.
|
||||
*
|
||||
* Previous responsibilities:
|
||||
* - Exposed the currently "active" study id via localStorage.
|
||||
* - Partially overlapped with a separate study context implementation.
|
||||
*
|
||||
* Migration:
|
||||
* - All consumers should now import `useSelectedStudyDetails` from:
|
||||
* `~/hooks/useSelectedStudyDetails`
|
||||
* - That hook centralizes selection, metadata, counts, and role info.
|
||||
*
|
||||
* Safe Removal:
|
||||
* - Once you are certain no editors / build artifacts reference this
|
||||
* path, you may delete this file. It is intentionally tiny and has
|
||||
* zero runtime footprint unless mistakenly invoked.
|
||||
*/
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
const ACTIVE_STUDY_KEY = "hristudio-active-study";
|
||||
|
||||
// Helper function to validate UUID format
|
||||
const isValidUUID = (id: string): boolean => {
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(id);
|
||||
};
|
||||
|
||||
export function useActiveStudy() {
|
||||
const { data: session } = useSession();
|
||||
const [activeStudyId, setActiveStudyId] = useState<string | null>(null);
|
||||
const [isSettingActiveStudy, setIsSettingActiveStudy] = useState(false);
|
||||
|
||||
// Load active study from localStorage on mount
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(ACTIVE_STUDY_KEY);
|
||||
if (stored && isValidUUID(stored)) {
|
||||
setActiveStudyId(stored);
|
||||
} else if (stored) {
|
||||
// Clear invalid UUID from localStorage
|
||||
localStorage.removeItem(ACTIVE_STUDY_KEY);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get active study details
|
||||
const { data: activeStudy, isLoading: isLoadingActiveStudy } =
|
||||
api.studies.get.useQuery(
|
||||
{ id: activeStudyId! },
|
||||
{
|
||||
enabled: !!activeStudyId && isValidUUID(activeStudyId),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: false, // Don't retry if study doesn't exist
|
||||
},
|
||||
);
|
||||
|
||||
// Clear localStorage if study doesn't exist
|
||||
useEffect(() => {
|
||||
if (activeStudyId && !activeStudy && !isLoadingActiveStudy) {
|
||||
localStorage.removeItem(ACTIVE_STUDY_KEY);
|
||||
setActiveStudyId(null);
|
||||
toast.error(
|
||||
"Selected study no longer exists. Please select a new study.",
|
||||
);
|
||||
}
|
||||
}, [activeStudy, activeStudyId, isLoadingActiveStudy]);
|
||||
|
||||
// Get user's studies for switching (always use memberOnly: true for security)
|
||||
const { data: studiesData, isLoading: isLoadingStudies } =
|
||||
api.studies.list.useQuery(
|
||||
{ limit: 20, memberOnly: true },
|
||||
{
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
enabled: !!session?.user?.id,
|
||||
},
|
||||
);
|
||||
|
||||
const userStudies = studiesData?.studies ?? [];
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const setActiveStudy = (studyId: string) => {
|
||||
if (!isValidUUID(studyId)) {
|
||||
toast.error("Invalid study ID format");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSettingActiveStudy(true);
|
||||
setActiveStudyId(studyId);
|
||||
localStorage.setItem(ACTIVE_STUDY_KEY, studyId);
|
||||
|
||||
// Invalidate all related queries when study changes
|
||||
void utils.participants.invalidate();
|
||||
void utils.trials.invalidate();
|
||||
void utils.experiments.invalidate();
|
||||
|
||||
toast.success("Active study updated");
|
||||
|
||||
// Reset loading state after a brief delay to allow queries to refetch
|
||||
setTimeout(() => {
|
||||
setIsSettingActiveStudy(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const clearActiveStudy = () => {
|
||||
setIsSettingActiveStudy(true);
|
||||
setActiveStudyId(null);
|
||||
localStorage.removeItem(ACTIVE_STUDY_KEY);
|
||||
|
||||
// Invalidate all related queries when clearing study
|
||||
void utils.participants.invalidate();
|
||||
void utils.trials.invalidate();
|
||||
void utils.experiments.invalidate();
|
||||
|
||||
toast.success("Active study cleared");
|
||||
|
||||
// Reset loading state after a brief delay
|
||||
setTimeout(() => {
|
||||
setIsSettingActiveStudy(false);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Note: Auto-selection removed to require manual study selection
|
||||
|
||||
return {
|
||||
// State
|
||||
activeStudyId,
|
||||
activeStudy:
|
||||
activeStudy && typeof activeStudy === "object"
|
||||
? {
|
||||
id: activeStudy.id,
|
||||
title: (activeStudy as { name?: string }).name ?? "",
|
||||
description:
|
||||
(activeStudy as { description?: string }).description ?? "",
|
||||
}
|
||||
: null,
|
||||
userStudies: userStudies.map(
|
||||
(study: { id: string; name: string; description?: string | null }) => ({
|
||||
id: study.id,
|
||||
title: study.name,
|
||||
description: study.description ?? "",
|
||||
}),
|
||||
),
|
||||
|
||||
// Loading states
|
||||
isLoadingActiveStudy,
|
||||
isLoadingStudies,
|
||||
isSettingActiveStudy,
|
||||
isClearingActiveStudy: false,
|
||||
|
||||
// Actions
|
||||
setActiveStudy,
|
||||
clearActiveStudy,
|
||||
|
||||
// Utilities
|
||||
hasActiveStudy: !!activeStudyId,
|
||||
hasStudies: userStudies.length > 0,
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `useSelectedStudyDetails()` instead.
|
||||
* Legacy no-op placeholder retained only to satisfy stale references.
|
||||
* Returns a neutral object so accidental invocations are harmless.
|
||||
*/
|
||||
export function useActiveStudy(): DeprecatedActiveStudyHookReturn {
|
||||
return { studyId: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Type alias maintained for backward compatibility with (now removed)
|
||||
* code that might have referenced the old hook's return type.
|
||||
* Kept minimal on purpose.
|
||||
*/
|
||||
export interface DeprecatedActiveStudyHookReturn {
|
||||
/** Previously the active study id (now: studyId in useSelectedStudyDetails) */
|
||||
studyId: string | null;
|
||||
}
|
||||
|
||||
export default useActiveStudy;
|
||||
|
||||
123
src/hooks/useSelectedStudyDetails.ts
Normal file
123
src/hooks/useSelectedStudyDetails.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
|
||||
/**
|
||||
* useSelectedStudyDetails
|
||||
*
|
||||
* Strongly typed unified source of truth for the currently selected study.
|
||||
*
|
||||
* Provides a single hook to retrieve:
|
||||
* - selected study id
|
||||
* - lightweight summary counts
|
||||
* - role + createdAt
|
||||
* - loading / fetching flags
|
||||
* - mutation helpers
|
||||
*/
|
||||
|
||||
interface StudyRelatedEntity {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface StudyMember {
|
||||
id: string;
|
||||
userId?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
interface StudyDetails {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: string;
|
||||
experiments?: StudyRelatedEntity[];
|
||||
participants?: StudyRelatedEntity[];
|
||||
members?: StudyMember[];
|
||||
userRole?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export interface StudySummary {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: string;
|
||||
experimentCount: number;
|
||||
participantCount: number;
|
||||
memberCount: number;
|
||||
userRole?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export interface UseSelectedStudyDetailsReturn {
|
||||
studyId: string | null;
|
||||
study: StudySummary | null;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
refetch: () => Promise<unknown>;
|
||||
setStudyId: (id: string | null) => void;
|
||||
clearStudy: () => void;
|
||||
hasStudy: boolean;
|
||||
}
|
||||
|
||||
export function useSelectedStudyDetails(): UseSelectedStudyDetailsReturn {
|
||||
const { selectedStudyId, setSelectedStudyId } = useStudyContext();
|
||||
|
||||
const { data, isLoading, isFetching, refetch } = api.studies.get.useQuery(
|
||||
{ id: selectedStudyId ?? "" },
|
||||
{
|
||||
enabled: !!selectedStudyId,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
},
|
||||
);
|
||||
|
||||
const study: StudySummary | null = useMemo(() => {
|
||||
if (!data || !selectedStudyId) return null;
|
||||
|
||||
// data is inferred from tRPC; we defensively narrow array fields
|
||||
const typed = data as StudyDetails;
|
||||
|
||||
const experiments = Array.isArray(typed.experiments)
|
||||
? typed.experiments
|
||||
: [];
|
||||
const participants = Array.isArray(typed.participants)
|
||||
? typed.participants
|
||||
: [];
|
||||
const members = Array.isArray(typed.members) ? typed.members : [];
|
||||
|
||||
return {
|
||||
id: typed.id,
|
||||
name: typed.name ?? "Unnamed Study",
|
||||
description: typed.description ?? "",
|
||||
status: typed.status ?? "active",
|
||||
experimentCount: experiments.length,
|
||||
participantCount: participants.length,
|
||||
memberCount: members.length,
|
||||
userRole: typed.userRole,
|
||||
createdAt: typed.createdAt,
|
||||
};
|
||||
}, [data, selectedStudyId]);
|
||||
|
||||
const setStudyId = useCallback(
|
||||
(id: string | null) => {
|
||||
void setSelectedStudyId(id);
|
||||
},
|
||||
[setSelectedStudyId],
|
||||
);
|
||||
|
||||
const clearStudy = useCallback(() => {
|
||||
void setSelectedStudyId(null);
|
||||
}, [setSelectedStudyId]);
|
||||
|
||||
return {
|
||||
studyId: selectedStudyId,
|
||||
study,
|
||||
isLoading,
|
||||
isFetching,
|
||||
refetch,
|
||||
setStudyId,
|
||||
clearStudy,
|
||||
hasStudy: !!study,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user