mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 06:34:44 -05:00
chore(deps): Update dependencies and refactor API routes for improved error handling
- Updated various dependencies in package.json and pnpm-lock.yaml, including '@clerk/nextjs' to version 6.7.1 and several others for better performance and security. - Refactored API routes to use Promise.resolve for context parameters, enhancing reliability in asynchronous contexts. - Improved error handling in the dashboard and studies components, ensuring better user experience during data fetching. - Removed unused favicon.ico file to clean up the project structure. - Enhanced the dashboard components to utilize a new utility function for API URL fetching, promoting code reusability.
This commit is contained in:
29
package.json
29
package.json
@@ -16,7 +16,7 @@
|
||||
"test:email": "tsx src/scripts/test-email.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^6.4.0",
|
||||
"@clerk/nextjs": "^6.7.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
@@ -29,31 +29,32 @@
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@vercel/analytics": "^1.4.1",
|
||||
"@vercel/postgres": "^0.10.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.36.3",
|
||||
"lucide-react": "^0.454.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-orm": "^0.37.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "15.0.3",
|
||||
"ngrok": "5.0.0-beta.2",
|
||||
"nodemailer": "^6.9.16",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"svix": "^1.41.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"svix": "^1.42.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.9.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^18.3.13",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"drizzle-kit": "^0.27.2",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"drizzle-kit": "^0.29.1",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
1084
pnpm-lock.yaml
generated
1084
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -10,7 +10,7 @@ export async function GET(
|
||||
context: { params: { id: string } }
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
const { id } = await context.params;
|
||||
const { id } = await Promise.resolve(context.params);
|
||||
|
||||
if (!userId) {
|
||||
return ApiError.Unauthorized();
|
||||
@@ -52,7 +52,7 @@ export async function POST(
|
||||
context: { params: { id: string } }
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
const { id } = await context.params;
|
||||
const { id } = await Promise.resolve(context.params);
|
||||
|
||||
if (!userId) {
|
||||
return ApiError.Unauthorized();
|
||||
@@ -98,7 +98,7 @@ export async function DELETE(
|
||||
context: { params: { id: string } }
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
const { id } = await context.params;
|
||||
const { id } = await Promise.resolve(context.params);
|
||||
|
||||
if (!userId) {
|
||||
return ApiError.Unauthorized();
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function GET(
|
||||
context: { params: { id: string } }
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
const { id } = context.params;
|
||||
const { id } = await Promise.resolve(context.params);
|
||||
|
||||
if (!userId) {
|
||||
return ApiError.Unauthorized();
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function PUT(
|
||||
context: { params: { id: string; userId: string } }
|
||||
) {
|
||||
try {
|
||||
const { id, userId } = await context.params;
|
||||
const { id, userId } = await Promise.resolve(context.params);
|
||||
const studyId = parseInt(id);
|
||||
|
||||
if (isNaN(studyId)) {
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function GET(
|
||||
context: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const { id } = await Promise.resolve(context.params);
|
||||
const studyId = parseInt(id);
|
||||
|
||||
if (isNaN(studyId)) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { BookOpen, Settings2 } from "lucide-react";
|
||||
import { useToast } from "~/hooks/use-toast";
|
||||
import { Breadcrumb } from "~/components/breadcrumb";
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
|
||||
interface DashboardStats {
|
||||
studyCount: number;
|
||||
@@ -22,15 +23,11 @@ export default function Dashboard() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardStats();
|
||||
}, []);
|
||||
|
||||
const fetchDashboardStats = async () => {
|
||||
const fetchDashboardStats = useCallback(async () => {
|
||||
try {
|
||||
const studiesRes = await fetch('/api/studies');
|
||||
const studiesRes = await fetch(getApiUrl('/api/studies'));
|
||||
const studies = await studiesRes.json();
|
||||
|
||||
|
||||
// For now, just show study count
|
||||
setStats({
|
||||
studyCount: studies.data.length,
|
||||
@@ -46,7 +43,12 @@ export default function Dashboard() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardStats();
|
||||
}, [fetchDashboardStats]);
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -59,7 +61,7 @@ export default function Dashboard() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Breadcrumb />
|
||||
|
||||
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<p className="text-muted-foreground">Overview of your research studies</p>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
SelectValue
|
||||
} from "~/components/ui/select";
|
||||
import { usePermissions } from "~/hooks/usePermissions";
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
|
||||
interface Study {
|
||||
id: number;
|
||||
@@ -47,7 +48,7 @@ export default function Participants() {
|
||||
|
||||
const fetchStudies = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/studies');
|
||||
const response = await fetch(getApiUrl('/api/studies'));
|
||||
const data = await response.json();
|
||||
setStudies(data);
|
||||
} catch (error) {
|
||||
@@ -60,7 +61,7 @@ export default function Participants() {
|
||||
const fetchParticipants = async (studyId: number) => {
|
||||
try {
|
||||
console.log(`Fetching participants for studyId: ${studyId}`);
|
||||
const response = await fetch(`/api/participants?studyId=${studyId}`);
|
||||
const response = await fetch(getApiUrl(`/api/participants?studyId=${studyId}`));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
@@ -84,7 +85,7 @@ export default function Participants() {
|
||||
if (!selectedStudyId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/participants`, {
|
||||
const response = await fetch(getApiUrl('/api/participants'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -109,7 +110,7 @@ export default function Participants() {
|
||||
|
||||
const deleteParticipant = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/participants/${id}`, {
|
||||
const response = await fetch(getApiUrl('/api/participants/${id}'), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useToast } from "~/hooks/use-toast";
|
||||
import Link from "next/link";
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
|
||||
interface StudyStats {
|
||||
participantCount: number;
|
||||
@@ -31,13 +32,9 @@ export default function StudyDashboard() {
|
||||
const { id } = useParams();
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
fetchStudyStats();
|
||||
}, [id]);
|
||||
|
||||
const fetchStudyStats = async () => {
|
||||
const fetchStudyStats = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${id}/stats`);
|
||||
const response = await fetch(getApiUrl(`/api/studies/${id}/stats`));
|
||||
if (!response.ok) throw new Error("Failed to fetch study statistics");
|
||||
const data = await response.json();
|
||||
setStats(data.data);
|
||||
@@ -51,7 +48,11 @@ export default function StudyDashboard() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [toast, id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStudyStats();
|
||||
}, [fetchStudyStats]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,7 @@ import Link from "next/link";
|
||||
import { useActiveStudy } from "~/context/active-study";
|
||||
import { hasPermission } from "~/lib/permissions-client";
|
||||
import { PERMISSIONS } from "~/lib/permissions";
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
|
||||
export default function NewParticipant() {
|
||||
const [name, setName] = useState("");
|
||||
@@ -36,7 +37,7 @@ export default function NewParticipant() {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${id}/participants`, {
|
||||
const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
interface Participant {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -48,13 +48,9 @@ export default function ParticipantsList() {
|
||||
const canDeleteParticipant = activeStudy && hasPermission(activeStudy.permissions, PERMISSIONS.DELETE_PARTICIPANT);
|
||||
const canViewNames = activeStudy && hasPermission(activeStudy.permissions, PERMISSIONS.VIEW_PARTICIPANT_NAMES);
|
||||
|
||||
useEffect(() => {
|
||||
fetchParticipants();
|
||||
}, [id]);
|
||||
|
||||
const fetchParticipants = async () => {
|
||||
const fetchParticipants = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${id}/participants`, {
|
||||
const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -74,11 +70,15 @@ export default function ParticipantsList() {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [toast, id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchParticipants();
|
||||
}, [fetchParticipants]);
|
||||
|
||||
const handleDelete = async (participantId: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${id}/participants`, {
|
||||
const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PERMISSIONS } from "~/lib/permissions-client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Settings2Icon, UsersIcon, UserIcon } from "lucide-react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
|
||||
interface Study {
|
||||
id: number;
|
||||
@@ -29,7 +30,7 @@ export default function StudySettings() {
|
||||
useEffect(() => {
|
||||
const fetchStudy = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${id}`);
|
||||
const response = await fetch(getApiUrl(`/api/studies/${id}`));
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
router.push('/dashboard/studies');
|
||||
|
||||
@@ -13,7 +13,7 @@ import Link from "next/link";
|
||||
import { useActiveStudy } from "~/context/active-study";
|
||||
import { hasPermission } from "~/lib/permissions-client";
|
||||
import { PERMISSIONS } from "~/lib/permissions";
|
||||
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
export default function NewStudy() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
@@ -27,7 +27,7 @@ export default function NewStudy() {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/studies', {
|
||||
const response = await fetch(getApiUrl('/api/studies'), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
AlertDialogFooter
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import { ROLES } from "~/lib/roles";
|
||||
import { getApiUrl } from "~/lib/fetch-utils";
|
||||
|
||||
interface Study {
|
||||
id: number;
|
||||
@@ -50,7 +51,7 @@ export default function Studies() {
|
||||
|
||||
const fetchStudies = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/studies");
|
||||
const response = await fetch(getApiUrl("/api/studies"));
|
||||
if (!response.ok) throw new Error("Failed to fetch studies");
|
||||
const data = await response.json();
|
||||
setStudies(data.data || []);
|
||||
@@ -67,7 +68,7 @@ export default function Studies() {
|
||||
const createStudy = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch("/api/studies", {
|
||||
const response = await fetch(getApiUrl("/api/studies"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -99,7 +100,7 @@ export default function Studies() {
|
||||
|
||||
const deleteStudy = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${id}`, {
|
||||
const response = await fetch(getApiUrl(`/api/studies/${id}`), {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -187,15 +187,19 @@ export function Sidebar() {
|
||||
<div className="border-t border-[hsl(var(--sidebar-separator))]">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<UserButton />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[hsl(var(--sidebar-foreground))]">
|
||||
{user?.fullName ?? user?.username ?? 'User'}
|
||||
</p>
|
||||
<p className="text-xs text-[hsl(var(--sidebar-muted))]">
|
||||
{user?.primaryEmailAddress?.emailAddress ?? 'user@example.com'}
|
||||
</p>
|
||||
<div className="w-8 h-8">
|
||||
<UserButton afterSignOutUrl="/" />
|
||||
</div>
|
||||
{user && (
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-[hsl(var(--sidebar-foreground))] truncate">
|
||||
{user.fullName ?? user.username ?? 'User'}
|
||||
</p>
|
||||
<p className="text-xs text-[hsl(var(--sidebar-muted))] truncate">
|
||||
{user.primaryEmailAddress?.emailAddress ?? 'user@example.com'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { useToast } from "~/hooks/use-toast";
|
||||
@@ -28,11 +28,7 @@ export function InvitationsTab({ studyId, permissions }: InvitationsTabProps) {
|
||||
const hasPermission = (permission: string) => permissions.includes(permission);
|
||||
const canManageRoles = hasPermission(PERMISSIONS.MANAGE_ROLES);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInvitations();
|
||||
}, [studyId]);
|
||||
|
||||
const fetchInvitations = async () => {
|
||||
const fetchInvitations = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/invitations?studyId=${studyId}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch invitations");
|
||||
@@ -48,7 +44,12 @@ export function InvitationsTab({ studyId, permissions }: InvitationsTabProps) {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [studyId, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInvitations();
|
||||
}, [fetchInvitations]);
|
||||
|
||||
|
||||
const handleDeleteInvitation = async (invitationId: string) => {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -41,14 +41,7 @@ export function InviteUserDialog({ studyId, onInviteSent }: InviteUserDialogProp
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Fetch available roles when dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchRoles();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const fetchRoles = async () => {
|
||||
const fetchRoles = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/roles");
|
||||
if (!response.ok) {
|
||||
@@ -67,7 +60,12 @@ export function InviteUserDialog({ studyId, onInviteSent }: InviteUserDialogProp
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
// Fetch available roles when dialog opens
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
}, [fetchRoles]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
@@ -43,11 +43,7 @@ export function ParticipantsTab({ studyId, permissions }: ParticipantsTabProps)
|
||||
const canDeleteParticipant = hasPermission(PERMISSIONS.DELETE_PARTICIPANT);
|
||||
const canViewNames = hasPermission(PERMISSIONS.VIEW_PARTICIPANT_NAMES);
|
||||
|
||||
useEffect(() => {
|
||||
fetchParticipants();
|
||||
}, [studyId]);
|
||||
|
||||
const fetchParticipants = async () => {
|
||||
const fetchParticipants = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${studyId}/participants`);
|
||||
if (!response.ok) throw new Error("Failed to fetch participants");
|
||||
@@ -63,7 +59,11 @@ export function ParticipantsTab({ studyId, permissions }: ParticipantsTabProps)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [toast, studyId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchParticipants();
|
||||
}, [fetchParticipants]);
|
||||
|
||||
const createParticipant = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -72,7 +72,7 @@ export function SettingsTab({ study }: SettingsTabProps) {
|
||||
<Card>
|
||||
<CardContent className="py-8">
|
||||
<p className="text-center text-muted-foreground">
|
||||
You don't have permission to edit this study.
|
||||
You don't have permission to edit this study.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { UserAvatar } from "~/components/user-avatar";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
@@ -38,8 +38,7 @@ import {
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
name: string | null;
|
||||
roles: Array<{ id: number; name: string }>;
|
||||
}
|
||||
|
||||
@@ -72,23 +71,7 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
||||
const hasPermission = (permission: string) => permissions.includes(permission);
|
||||
const canManageRoles = hasPermission(PERMISSIONS.MANAGE_ROLES);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [studyId]);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchUsers(),
|
||||
fetchInvitations(),
|
||||
fetchRoles(),
|
||||
]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUsers = async () => {
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/studies/${studyId}/users`);
|
||||
if (!response.ok) throw new Error("Failed to fetch users");
|
||||
@@ -102,9 +85,9 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [studyId, toast]);
|
||||
|
||||
const fetchInvitations = async () => {
|
||||
const fetchInvitations = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/invitations?studyId=${studyId}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch invitations");
|
||||
@@ -118,9 +101,9 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [studyId, toast]);
|
||||
|
||||
const fetchRoles = async () => {
|
||||
const fetchRoles = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/roles");
|
||||
if (!response.ok) throw new Error("Failed to fetch roles");
|
||||
@@ -136,7 +119,24 @@ export function UsersTab({ studyId, permissions }: UsersTabProps) {
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchUsers(),
|
||||
fetchInvitations(),
|
||||
fetchRoles(),
|
||||
]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [fetchUsers, fetchInvitations, fetchRoles]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleRoleChange = async (userId: string, newRoleId: string) => {
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
interface Study {
|
||||
id: number;
|
||||
@@ -30,10 +30,10 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
|
||||
const [studies, setStudies] = useState<Study[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const fetchStudies = async () => {
|
||||
const fetchStudies = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/studies', {
|
||||
method: 'GET',
|
||||
@@ -55,6 +55,7 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
|
||||
|
||||
if (studiesWithDates.length === 1 && !activeStudy) {
|
||||
setActiveStudy(studiesWithDates[0]);
|
||||
router.push(`/dashboard/studies/${studiesWithDates[0].id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching studies:', error);
|
||||
@@ -62,11 +63,11 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStudies();
|
||||
}, []);
|
||||
}, [fetchStudies]);
|
||||
|
||||
useEffect(() => {
|
||||
const studyIdMatch = pathname.match(/\/dashboard\/studies\/(\d+)/);
|
||||
@@ -77,7 +78,9 @@ export function ActiveStudyProvider({ children }: { children: React.ReactNode })
|
||||
setActiveStudy(study);
|
||||
}
|
||||
} else if (!pathname.includes('/studies/new')) {
|
||||
setActiveStudy(null);
|
||||
if (activeStudy) {
|
||||
setActiveStudy(null);
|
||||
}
|
||||
}
|
||||
}, [pathname, studies]);
|
||||
|
||||
|
||||
@@ -3,7 +3,14 @@ import { eq } from "drizzle-orm";
|
||||
import { db } from "./index";
|
||||
import { PERMISSIONS } from "~/lib/permissions";
|
||||
import { ROLES, ROLE_PERMISSIONS } from "~/lib/roles";
|
||||
import { permissionsTable, rolesTable, rolePermissionsTable, userRolesTable, usersTable, studyTable } from "./schema";
|
||||
import {
|
||||
permissionsTable,
|
||||
rolesTable,
|
||||
rolePermissionsTable,
|
||||
userRolesTable,
|
||||
usersTable,
|
||||
studyTable,
|
||||
} from "~/db/schema";
|
||||
|
||||
// Load environment variables from .env.local
|
||||
config({ path: ".env.local" });
|
||||
@@ -14,7 +21,8 @@ async function seed() {
|
||||
// Insert roles
|
||||
console.log("Inserting roles...");
|
||||
for (const [roleKey, roleName] of Object.entries(ROLES)) {
|
||||
await db.insert(rolesTable)
|
||||
await db
|
||||
.insert(rolesTable)
|
||||
.values({
|
||||
name: roleName,
|
||||
description: getRoleDescription(roleKey),
|
||||
@@ -25,7 +33,8 @@ async function seed() {
|
||||
// Insert permissions
|
||||
console.log("Inserting permissions...");
|
||||
for (const [permKey, permCode] of Object.entries(PERMISSIONS)) {
|
||||
await db.insert(permissionsTable)
|
||||
await db
|
||||
.insert(permissionsTable)
|
||||
.values({
|
||||
name: formatPermissionName(permKey),
|
||||
code: permCode,
|
||||
@@ -41,14 +50,19 @@ async function seed() {
|
||||
// Insert role permissions
|
||||
console.log("Inserting role permissions...");
|
||||
for (const [roleKey, permissionCodes] of Object.entries(ROLE_PERMISSIONS)) {
|
||||
const role = roles.find(r => r.name === ROLES[roleKey as keyof typeof ROLES]);
|
||||
const role = roles.find(
|
||||
(r) => r.name === ROLES[roleKey as keyof typeof ROLES]
|
||||
);
|
||||
if (!role) continue;
|
||||
|
||||
for (const permissionCode of permissionCodes) {
|
||||
const permission = permissions.find(p => p.code === PERMISSIONS[permissionCode]);
|
||||
const permission = permissions.find(
|
||||
(p) => p.code === PERMISSIONS[permissionCode]
|
||||
);
|
||||
if (!permission) continue;
|
||||
|
||||
await db.insert(rolePermissionsTable)
|
||||
await db
|
||||
.insert(rolePermissionsTable)
|
||||
.values({
|
||||
roleId: role.id,
|
||||
permissionId: permission.id,
|
||||
@@ -61,7 +75,7 @@ async function seed() {
|
||||
console.log("Setting up initial user roles...");
|
||||
const users = await db.select().from(usersTable);
|
||||
if (users.length > 0) {
|
||||
const piRole = roles.find(r => r.name === ROLES.PRINCIPAL_INVESTIGATOR);
|
||||
const piRole = roles.find((r) => r.name === ROLES.PRINCIPAL_INVESTIGATOR);
|
||||
if (piRole) {
|
||||
// Get all studies owned by the first user
|
||||
const userStudies = await db
|
||||
@@ -71,7 +85,8 @@ async function seed() {
|
||||
|
||||
// Assign PI role for each study
|
||||
for (const study of userStudies) {
|
||||
await db.insert(userRolesTable)
|
||||
await db
|
||||
.insert(userRolesTable)
|
||||
.values({
|
||||
userId: users[0].id,
|
||||
roleId: piRole.id,
|
||||
@@ -88,8 +103,10 @@ async function seed() {
|
||||
function getRoleDescription(roleKey: string): string {
|
||||
const descriptions: Record<string, string> = {
|
||||
ADMIN: "Full system administrator with all permissions",
|
||||
PRINCIPAL_INVESTIGATOR: "Lead researcher responsible for study design and oversight",
|
||||
RESEARCHER: "Study team member with data collection and analysis capabilities",
|
||||
PRINCIPAL_INVESTIGATOR:
|
||||
"Lead researcher responsible for study design and oversight",
|
||||
RESEARCHER:
|
||||
"Study team member with data collection and analysis capabilities",
|
||||
WIZARD: "Operator controlling robot behavior during experiments",
|
||||
OBSERVER: "Team member observing and annotating experiments",
|
||||
ASSISTANT: "Support staff with limited view access",
|
||||
@@ -122,10 +139,11 @@ function getPermissionDescription(permKey: string): string {
|
||||
}
|
||||
|
||||
function formatPermissionName(permKey: string): string {
|
||||
return permKey.toLowerCase()
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return permKey
|
||||
.toLowerCase()
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
seed().catch(console.error);
|
||||
|
||||
9
src/lib/fetch-utils.ts
Normal file
9
src/lib/fetch-utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== 'undefined') return ''; // browser should use relative url
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||
}
|
||||
|
||||
export function getApiUrl(path: string) {
|
||||
return `${getBaseUrl()}${path}`;
|
||||
}
|
||||
Reference in New Issue
Block a user