Commit ·
49e7bf6
1
Parent(s): 2460f5c
Comprehensive frontend fixes: Updated gitignore, fixed all import paths, enhanced lib utilities
Browse files- .gitignore +2 -0
- src/api-client.ts +0 -54
- src/app/(dashboard)/pico/page.tsx +1 -1
- src/app/(dashboard)/settings/page.tsx +1 -1
- src/app/layout.tsx +1 -1
- src/components/atoms/Avatar/index.tsx +1 -1
- src/components/atoms/Badge/index.tsx +1 -1
- src/components/atoms/Button/index.tsx +1 -1
- src/components/atoms/Icon/index.tsx +1 -1
- src/components/atoms/Spinner/index.tsx +1 -1
- src/components/atoms/StatCard/index.tsx +1 -5
- src/components/atoms/Tooltip/index.tsx +1 -1
- src/components/organisms/Header/index.tsx +1 -1
- src/components/organisms/Sidebar/index.tsx +1 -1
- src/components/templates/DashboardTemplate/index.tsx +1 -1
- src/constants/api.ts +0 -22
- src/lib/api-client.ts +88 -0
- src/lib/constants/api.ts +24 -0
- src/{cn.ts → lib/utils.ts} +4 -0
- src/lib/utils/index.ts +3 -0
- src/lib/utils/validators.ts +31 -0
.gitignore
CHANGED
|
@@ -15,8 +15,10 @@ dist/
|
|
| 15 |
downloads/
|
| 16 |
eggs/
|
| 17 |
.eggs/
|
|
|
|
| 18 |
lib/
|
| 19 |
lib64/
|
|
|
|
| 20 |
parts/
|
| 21 |
sdist/
|
| 22 |
var/
|
|
|
|
| 15 |
downloads/
|
| 16 |
eggs/
|
| 17 |
.eggs/
|
| 18 |
+
# Ignore system lib directories but allow our src/lib
|
| 19 |
lib/
|
| 20 |
lib64/
|
| 21 |
+
!/src/lib/
|
| 22 |
parts/
|
| 23 |
sdist/
|
| 24 |
var/
|
src/api-client.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
// API Client for RM Research Assistant
|
| 2 |
-
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
| 3 |
-
|
| 4 |
-
class ApiClient {
|
| 5 |
-
private baseURL: string;
|
| 6 |
-
|
| 7 |
-
constructor(baseURL: string = API_BASE_URL) {
|
| 8 |
-
this.baseURL = baseURL;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
async request(endpoint: string, options: RequestInit = {}) {
|
| 12 |
-
const url = `${this.baseURL}${endpoint}`;
|
| 13 |
-
const config = {
|
| 14 |
-
headers: {
|
| 15 |
-
'Content-Type': 'application/json',
|
| 16 |
-
...options.headers,
|
| 17 |
-
},
|
| 18 |
-
...options,
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
const response = await fetch(url, config);
|
| 22 |
-
|
| 23 |
-
if (!response.ok) {
|
| 24 |
-
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
return response.json();
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
async get(endpoint: string) {
|
| 31 |
-
return this.request(endpoint, { method: 'GET' });
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
async post(endpoint: string, data?: any) {
|
| 35 |
-
return this.request(endpoint, {
|
| 36 |
-
method: 'POST',
|
| 37 |
-
body: data ? JSON.stringify(data) : undefined,
|
| 38 |
-
});
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
async put(endpoint: string, data?: any) {
|
| 42 |
-
return this.request(endpoint, {
|
| 43 |
-
method: 'PUT',
|
| 44 |
-
body: data ? JSON.stringify(data) : undefined,
|
| 45 |
-
});
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
async delete(endpoint: string) {
|
| 49 |
-
return this.request(endpoint, { method: 'DELETE' });
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
export const api = new ApiClient();
|
| 54 |
-
export default api;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/(dashboard)/pico/page.tsx
CHANGED
|
@@ -26,7 +26,7 @@ import { Spinner } from "@/components/atoms/Spinner";
|
|
| 26 |
|
| 27 |
// Hooks & Utils
|
| 28 |
import { useApi } from "@/hooks/useApi";
|
| 29 |
-
import { api } from "@/api-client";
|
| 30 |
|
| 31 |
/**
|
| 32 |
* PICO Extraction Page (Final Build)
|
|
|
|
| 26 |
|
| 27 |
// Hooks & Utils
|
| 28 |
import { useApi } from "@/hooks/useApi";
|
| 29 |
+
import { api } from "@/lib/api-client";
|
| 30 |
|
| 31 |
/**
|
| 32 |
* PICO Extraction Page (Final Build)
|
src/app/(dashboard)/settings/page.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
| 14 |
import { DashboardTemplate } from "@/components/templates";
|
| 15 |
import { Icon } from "@/components/atoms/Icon";
|
| 16 |
import { Badge } from "@/components/atoms/Badge";
|
| 17 |
-
import { cn } from "@/
|
| 18 |
|
| 19 |
/**
|
| 20 |
* Utility: Safe JWT Decoder
|
|
|
|
| 14 |
import { DashboardTemplate } from "@/components/templates";
|
| 15 |
import { Icon } from "@/components/atoms/Icon";
|
| 16 |
import { Badge } from "@/components/atoms/Badge";
|
| 17 |
+
import { cn } from "@/lib/utils";
|
| 18 |
|
| 19 |
/**
|
| 20 |
* Utility: Safe JWT Decoder
|
src/app/layout.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import type { Metadata, Viewport } from "next";
|
| 2 |
import { Inter } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
-
import { cn } from "@/
|
| 5 |
|
| 6 |
// 1. Optimized Font Loading: Prevents Layout Shift (CLS)
|
| 7 |
const inter = Inter({
|
|
|
|
| 1 |
import type { Metadata, Viewport } from "next";
|
| 2 |
import { Inter } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
+
import { cn } from "@/lib/utils"; // Assumes a standard utility for class merging
|
| 5 |
|
| 6 |
// 1. Optimized Font Loading: Prevents Layout Shift (CLS)
|
| 7 |
const inter = Inter({
|
src/components/atoms/Avatar/index.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
"use client";
|
| 3 |
|
| 4 |
import React from "react";
|
| 5 |
-
import { cn } from "@/
|
| 6 |
|
| 7 |
/** Props for the Avatar root */
|
| 8 |
export interface AvatarProps {
|
|
|
|
| 2 |
"use client";
|
| 3 |
|
| 4 |
import React from "react";
|
| 5 |
+
import { cn } from "@/lib/utils";
|
| 6 |
|
| 7 |
/** Props for the Avatar root */
|
| 8 |
export interface AvatarProps {
|
src/components/atoms/Badge/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import * as React from "react";
|
| 2 |
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
-
import { cn } from "@/
|
| 4 |
|
| 5 |
// 1. Defined variants to support academic status indicators
|
| 6 |
const badgeVariants = cva(
|
|
|
|
| 1 |
import * as React from "react";
|
| 2 |
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
+
import { cn } from "@/lib/utils";
|
| 4 |
|
| 5 |
// 1. Defined variants to support academic status indicators
|
| 6 |
const badgeVariants = cva(
|
src/components/atoms/Button/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import * as React from "react";
|
| 2 |
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
-
import { cn } from "@/
|
| 4 |
|
| 5 |
// 1. Define variants using CVA for clean state management
|
| 6 |
const buttonVariants = cva(
|
|
|
|
| 1 |
import * as React from "react";
|
| 2 |
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
+
import { cn } from "@/lib/utils";
|
| 4 |
|
| 5 |
// 1. Define variants using CVA for clean state management
|
| 6 |
const buttonVariants = cva(
|
src/components/atoms/Icon/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { LucideIcon, LucideProps } from "lucide-react";
|
| 2 |
-
import { cn } from "@/
|
| 3 |
|
| 4 |
export interface IconProps extends LucideProps {
|
| 5 |
// 1. The 'icon' prop accepts any Lucide icon component
|
|
|
|
| 1 |
import { LucideIcon, LucideProps } from "lucide-react";
|
| 2 |
+
import { cn } from "@/lib/utils";
|
| 3 |
|
| 4 |
export interface IconProps extends LucideProps {
|
| 5 |
// 1. The 'icon' prop accepts any Lucide icon component
|
src/components/atoms/Spinner/index.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
import * as React from "react";
|
| 4 |
import { Loader2 } from "lucide-react";
|
| 5 |
-
import { cn } from "@/
|
| 6 |
|
| 7 |
export interface SpinnerProps extends React.ComponentPropsWithoutRef<typeof Loader2> {}
|
| 8 |
|
|
|
|
| 2 |
|
| 3 |
import * as React from "react";
|
| 4 |
import { Loader2 } from "lucide-react";
|
| 5 |
+
import { cn } from "@/lib/utils";
|
| 6 |
|
| 7 |
export interface SpinnerProps extends React.ComponentPropsWithoutRef<typeof Loader2> {}
|
| 8 |
|
src/components/atoms/StatCard/index.tsx
CHANGED
|
@@ -3,11 +3,7 @@
|
|
| 3 |
|
| 4 |
import React from "react";
|
| 5 |
import { LucideProps } from "lucide-react";
|
| 6 |
-
|
| 7 |
-
/** Utility to merge class names */
|
| 8 |
-
function cn(...classes: (string | undefined | false | null)[]) {
|
| 9 |
-
return classes.filter(Boolean).join(" ");
|
| 10 |
-
}
|
| 11 |
|
| 12 |
/** Props for StatCard */
|
| 13 |
export interface StatCardProps {
|
|
|
|
| 3 |
|
| 4 |
import React from "react";
|
| 5 |
import { LucideProps } from "lucide-react";
|
| 6 |
+
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
/** Props for StatCard */
|
| 9 |
export interface StatCardProps {
|
src/components/atoms/Tooltip/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import React, { useState, useRef } from "react";
|
| 4 |
-
import { cn } from "@/
|
| 5 |
|
| 6 |
// Custom TooltipProvider (no-op for compatibility)
|
| 7 |
const TooltipProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import React, { useState, useRef } from "react";
|
| 4 |
+
import { cn } from "@/lib/utils"; // keep your utility function
|
| 5 |
|
| 6 |
// Custom TooltipProvider (no-op for compatibility)
|
| 7 |
const TooltipProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
src/components/organisms/Header/index.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
| 8 |
TooltipContent,
|
| 9 |
TooltipProvider,
|
| 10 |
} from "@/components/atoms/Tooltip";
|
| 11 |
-
import { cn } from "@/
|
| 12 |
|
| 13 |
const Header: React.FC = () => {
|
| 14 |
return (
|
|
|
|
| 8 |
TooltipContent,
|
| 9 |
TooltipProvider,
|
| 10 |
} from "@/components/atoms/Tooltip";
|
| 11 |
+
import { cn } from "@/lib/utils"; // your utility for classnames
|
| 12 |
|
| 13 |
const Header: React.FC = () => {
|
| 14 |
return (
|
src/components/organisms/Sidebar/index.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
| 18 |
import { Avatar, AvatarFallback } from "@/components/atoms/Avatar";
|
| 19 |
import { Icon } from "@/components/atoms/Icon";
|
| 20 |
import { Spinner } from "@/components/atoms/Spinner";
|
| 21 |
-
import { cn } from "@/
|
| 22 |
|
| 23 |
const navItems = [
|
| 24 |
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
|
|
|
|
| 18 |
import { Avatar, AvatarFallback } from "@/components/atoms/Avatar";
|
| 19 |
import { Icon } from "@/components/atoms/Icon";
|
| 20 |
import { Spinner } from "@/components/atoms/Spinner";
|
| 21 |
+
import { cn } from "@/lib/utils";
|
| 22 |
|
| 23 |
const navItems = [
|
| 24 |
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
|
src/components/templates/DashboardTemplate/index.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
import * as React from "react";
|
| 4 |
import { Sidebar } from "@/components/organisms/Sidebar";
|
| 5 |
import { Header } from "@/components/organisms/Header";
|
| 6 |
-
import { cn } from "@/
|
| 7 |
|
| 8 |
interface DashboardTemplateProps {
|
| 9 |
children: React.ReactNode;
|
|
|
|
| 3 |
import * as React from "react";
|
| 4 |
import { Sidebar } from "@/components/organisms/Sidebar";
|
| 5 |
import { Header } from "@/components/organisms/Header";
|
| 6 |
+
import { cn } from "@/lib/utils";
|
| 7 |
|
| 8 |
interface DashboardTemplateProps {
|
| 9 |
children: React.ReactNode;
|
src/constants/api.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
// API Configuration Constants
|
| 2 |
-
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
| 3 |
-
|
| 4 |
-
export const API_ENDPOINTS = {
|
| 5 |
-
// Authentication
|
| 6 |
-
LOGIN: '/api/v1/auth/login',
|
| 7 |
-
REGISTER: '/api/v1/auth/register',
|
| 8 |
-
TOKEN_REFRESH: '/api/v1/auth/refresh',
|
| 9 |
-
|
| 10 |
-
// Papers
|
| 11 |
-
PAPERS: '/api/v1/papers',
|
| 12 |
-
PAPER_DETAIL: (id: string) => `/api/v1/papers/${id}`,
|
| 13 |
-
|
| 14 |
-
// PICO Analysis
|
| 15 |
-
PICO_ANALYZE: '/api/v1/pico/analyze',
|
| 16 |
-
PICO_RESULTS: (id: string) => `/api/v1/pico/results/${id}`,
|
| 17 |
-
|
| 18 |
-
// Library
|
| 19 |
-
LIBRARY: '/api/v1/library',
|
| 20 |
-
LIBRARY_ADD: '/api/v1/library/add',
|
| 21 |
-
LIBRARY_REMOVE: (id: string) => `/api/v1/library/remove/${id}`,
|
| 22 |
-
} as const;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/api-client.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Production-Grade API Client
|
| 3 |
+
* - Unified Fetch Wrapper with JWT injection.
|
| 4 |
+
* - Centralized 401 Interception (Auto-Logout).
|
| 5 |
+
* - Support for JSON and Multipart/Form-Data.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1";
|
| 9 |
+
|
| 10 |
+
export type ApiError = {
|
| 11 |
+
message: string;
|
| 12 |
+
status?: number;
|
| 13 |
+
detail?: any;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
async function request<T>(
|
| 17 |
+
endpoint: string,
|
| 18 |
+
options: RequestInit & { params?: Record<string, string> } = {}
|
| 19 |
+
): Promise<T> {
|
| 20 |
+
const { params, headers, ...config } = options;
|
| 21 |
+
|
| 22 |
+
// 1. Construct URL with Search Params
|
| 23 |
+
const url = new URL(`${BASE_URL}${endpoint}`);
|
| 24 |
+
if (params) {
|
| 25 |
+
Object.entries(params).forEach(([key, val]) => url.searchParams.append(key, val));
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// 2. Token Retrieval (Direct localStorage for non-React context utility)
|
| 29 |
+
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
| 30 |
+
|
| 31 |
+
// 3. Header Synthesis
|
| 32 |
+
const authHeader = token ? { Authorization: `Bearer ${token}` } : {};
|
| 33 |
+
|
| 34 |
+
// Don't set Content-Type if sending FormData (browser handles it)
|
| 35 |
+
const isFormData = config.body instanceof FormData;
|
| 36 |
+
const contentTypeHeader = isFormData ? {} : { "Content-Type": "application/json" };
|
| 37 |
+
|
| 38 |
+
const finalConfig: RequestInit = {
|
| 39 |
+
...config,
|
| 40 |
+
headers: {
|
| 41 |
+
...contentTypeHeader,
|
| 42 |
+
...authHeader,
|
| 43 |
+
...headers,
|
| 44 |
+
},
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
try {
|
| 48 |
+
const response = await fetch(url.toString(), finalConfig);
|
| 49 |
+
|
| 50 |
+
// 4. Global Interceptor: Handle Unauthorized
|
| 51 |
+
if (response.status === 401) {
|
| 52 |
+
if (typeof window !== "undefined") {
|
| 53 |
+
localStorage.removeItem("token");
|
| 54 |
+
// Force hard-redirect to clear state if token is dead
|
| 55 |
+
window.location.href = "/login?error=session_expired";
|
| 56 |
+
}
|
| 57 |
+
throw new Error("Unauthorized access. Please log in again.");
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// 5. Success Handlers
|
| 61 |
+
if (response.status === 204) return {} as T;
|
| 62 |
+
|
| 63 |
+
const data = await response.json();
|
| 64 |
+
|
| 65 |
+
if (!response.ok) {
|
| 66 |
+
// Return the specific backend detail if available (FastAPI style)
|
| 67 |
+
const errorMsg = data.detail || "The research server encountered an issue.";
|
| 68 |
+
return Promise.reject({ message: errorMsg, status: response.status, detail: data });
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
return data as T;
|
| 72 |
+
} catch (err: any) {
|
| 73 |
+
// Handle Network Failures
|
| 74 |
+
return Promise.reject({
|
| 75 |
+
message: err.message || "Unable to connect to the research server. Check your connection.",
|
| 76 |
+
status: err.status || 500
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
export const api = {
|
| 82 |
+
get: <T>(url: string, p?: Record<string, string>) => request<T>(url, { method: "GET", params: p }),
|
| 83 |
+
post: <T>(url: string, body: any) => request<T>(url, { method: "POST", body: JSON.stringify(body) }),
|
| 84 |
+
put: <T>(url: string, body: any) => request<T>(url, { method: "PUT", body: JSON.stringify(body) }),
|
| 85 |
+
delete: <T>(url: string) => request<T>(url, { method: "DELETE" }),
|
| 86 |
+
// For PICO/Avatar uploads later:
|
| 87 |
+
upload: <T>(url: string, formData: FormData) => request<T>(url, { method: "POST", body: formData }),
|
| 88 |
+
};
|
src/lib/constants/api.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const API_BASE_URL = 'https://example.com/api'; // Base URL for your API
|
| 2 |
+
|
| 3 |
+
// Default request timeout in milliseconds
|
| 4 |
+
export const TIMEOUT = 5000;
|
| 5 |
+
|
| 6 |
+
// Default headers for API requests
|
| 7 |
+
export const DEFAULT_HEADERS = {
|
| 8 |
+
'Content-Type': 'application/json',
|
| 9 |
+
Accept: 'application/json',
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
// Helper to build full API URLs
|
| 13 |
+
export const buildApiUrl = (path: string) => {
|
| 14 |
+
const trimmedPath = path.startsWith('/') ? path.slice(1) : path;
|
| 15 |
+
return `${API_BASE_URL}/${trimmedPath}`;
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
// Common API endpoints
|
| 19 |
+
export const ENDPOINTS = {
|
| 20 |
+
USERS: 'users',
|
| 21 |
+
POSTS: 'posts',
|
| 22 |
+
AUTH_LOGIN: 'auth/login',
|
| 23 |
+
AUTH_REGISTER: 'auth/register',
|
| 24 |
+
};
|
src/{cn.ts → lib/utils.ts}
RENAMED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
import { type ClassValue, clsx } from "clsx"
|
| 2 |
import { twMerge } from "tailwind-merge"
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
export function cn(...inputs: ClassValue[]) {
|
| 5 |
return twMerge(clsx(inputs))
|
| 6 |
}
|
|
|
|
| 1 |
import { type ClassValue, clsx } from "clsx"
|
| 2 |
import { twMerge } from "tailwind-merge"
|
| 3 |
|
| 4 |
+
/**
|
| 5 |
+
* cn - Class Names Utility
|
| 6 |
+
* Combines clsx and tailwind-merge for optimal class handling
|
| 7 |
+
*/
|
| 8 |
export function cn(...inputs: ClassValue[]) {
|
| 9 |
return twMerge(clsx(inputs))
|
| 10 |
}
|
src/lib/utils/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Re-export utils and validators for easy imports
|
| 2 |
+
export * from '../utils';
|
| 3 |
+
export * from './validators';
|
src/lib/utils/validators.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Research Data Validators
|
| 3 |
+
* Pure functions to validate scientific identifiers and form inputs.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// 1. Digital Object Identifier (DOI) Regex
|
| 7 |
+
// Matches standard DOI formats (e.g., 10.1038/s41586-021-03491-6)
|
| 8 |
+
const DOI_REGEX = /^10.\d{4,9}\/[-._;()/:A-Z0-9]+$/i;
|
| 9 |
+
|
| 10 |
+
// 2. Email Regex (Standard RFC 5322)
|
| 11 |
+
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
| 12 |
+
|
| 13 |
+
export const validators = {
|
| 14 |
+
/** Checks if a string is a valid DOI */
|
| 15 |
+
isDOI: (val: string): boolean => DOI_REGEX.test(val.trim()),
|
| 16 |
+
|
| 17 |
+
/** Checks if a string is a valid institutional email */
|
| 18 |
+
isEmail: (val: string): boolean => EMAIL_REGEX.test(val),
|
| 19 |
+
|
| 20 |
+
/** Ensures PICO instructions aren't just empty whitespace */
|
| 21 |
+
isValidInstructions: (val: string): boolean => val.trim().length >= 10,
|
| 22 |
+
|
| 23 |
+
/** Validates PICO Data object from the backend */
|
| 24 |
+
hasCompletePico: (pico: any): boolean => {
|
| 25 |
+
return !!(
|
| 26 |
+
pico?.pico_population &&
|
| 27 |
+
pico?.pico_intervention &&
|
| 28 |
+
pico?.pico_outcome
|
| 29 |
+
);
|
| 30 |
+
}
|
| 31 |
+
};
|