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