wip 10
This commit is contained in:
@@ -9,10 +9,8 @@ import eslintConfigPrettier from "eslint-config-prettier";
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
reactPlugin.configs.flat.recommended,
|
|
||||||
reactHooksPlugin.configs.flat.recommended,
|
|
||||||
{
|
{
|
||||||
files: ["**/*.ts", "**/*.tsx", "**/*.css"],
|
files: ["**/*.ts", "**/*.tsx"],
|
||||||
ignores: ["**/generated/**"],
|
ignores: ["**/generated/**"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: typescriptParser,
|
parser: typescriptParser,
|
||||||
@@ -22,10 +20,36 @@ export default [
|
|||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
},
|
},
|
||||||
|
projectService: true,
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
localStorage: "readonly",
|
||||||
|
sessionStorage: "readonly",
|
||||||
|
document: "readonly",
|
||||||
|
window: "readonly",
|
||||||
|
setTimeout: "readonly",
|
||||||
|
clearTimeout: "readonly",
|
||||||
|
setInterval: "readonly",
|
||||||
|
clearInterval: "readonly",
|
||||||
|
HTMLElement: "readonly",
|
||||||
|
HTMLDivElement: "readonly",
|
||||||
|
HTMLButtonElement: "readonly",
|
||||||
|
HTMLAnchorElement: "readonly",
|
||||||
|
MouseEvent: "readonly",
|
||||||
|
Node: "readonly",
|
||||||
|
File: "readonly",
|
||||||
|
Blob: "readonly",
|
||||||
|
FormData: "readonly",
|
||||||
|
alert: "readonly",
|
||||||
|
confirm: "readonly",
|
||||||
|
prompt: "readonly",
|
||||||
|
React: "readonly",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
"@typescript-eslint": typescriptPlugin,
|
"@typescript-eslint": typescriptPlugin,
|
||||||
|
react: reactPlugin,
|
||||||
|
"react-hooks": reactHooksPlugin,
|
||||||
tailwindcss,
|
tailwindcss,
|
||||||
prettier,
|
prettier,
|
||||||
},
|
},
|
||||||
@@ -35,11 +59,19 @@ export default [
|
|||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||||
|
"no-undef": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"error",
|
"error",
|
||||||
{ argsIgnorePattern: "^_" },
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
ignoreRestSiblings: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
"no-useless-catch": "off",
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
|||||||
2898
frontend/package-lock.json
generated
2898
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,21 +24,22 @@
|
|||||||
"tailwind-merge": "^3.5.0"
|
"tailwind-merge": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.17.0",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.8",
|
"@types/react-dom": "^19.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||||
"@typescript-eslint/parser": "^8.57.0",
|
"@typescript-eslint/parser": "^8.13.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.17.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||||
"orval": "^7.5.0",
|
"orval": "^8.5.3",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.3.3",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.5"
|
"vite": "^6.0.5"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useLogin, useLogout, useGetMe } from '../generated/anthoLumeAPIV1';
|
import { useLogin, useLogout, useGetMe } from '../generated/anthoLumeAPIV1';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ interface AuthState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType extends AuthState {
|
interface AuthContextType extends AuthState {
|
||||||
login: (username: string, password: string) => Promise<void>;
|
login: (_username: string, _password: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,50 +32,56 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// Update auth state based on /me endpoint response
|
// Update auth state based on /me endpoint response
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (meLoading) {
|
setAuthState(prev => {
|
||||||
// Still checking authentication
|
if (meLoading) {
|
||||||
setAuthState((prev) => ({ ...prev, isCheckingAuth: true }));
|
// Still checking authentication
|
||||||
} else if (meData?.data) {
|
return { ...prev, isCheckingAuth: true };
|
||||||
// User is authenticated
|
} else if (meData?.data) {
|
||||||
setAuthState({
|
// User is authenticated
|
||||||
isAuthenticated: true,
|
return {
|
||||||
user: meData.data,
|
isAuthenticated: true,
|
||||||
isCheckingAuth: false,
|
user: meData.data,
|
||||||
});
|
isCheckingAuth: false,
|
||||||
} else if (meError) {
|
};
|
||||||
// User is not authenticated or error occurred
|
} else if (meError) {
|
||||||
setAuthState({
|
// User is not authenticated or error occurred
|
||||||
isAuthenticated: false,
|
return {
|
||||||
user: null,
|
isAuthenticated: false,
|
||||||
isCheckingAuth: false,
|
user: null,
|
||||||
});
|
isCheckingAuth: false,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
}, [meData, meError, meLoading]);
|
}, [meData, meError, meLoading]);
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = useCallback(
|
||||||
try {
|
async (username: string, password: string) => {
|
||||||
const response = await loginMutation.mutateAsync({
|
try {
|
||||||
data: {
|
const response = await loginMutation.mutateAsync({
|
||||||
username,
|
data: {
|
||||||
password,
|
username,
|
||||||
},
|
password,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// The backend uses session-based authentication, so no token to store
|
// The backend uses session-based authentication, so no token to store
|
||||||
// The session cookie is automatically set by the browser
|
// The session cookie is automatically set by the browser
|
||||||
setAuthState({
|
setAuthState({
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
user: response.data,
|
user: response.data,
|
||||||
isCheckingAuth: false,
|
isCheckingAuth: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (err) {
|
} catch (_error) {
|
||||||
throw new Error('Login failed');
|
throw new Error('Login failed');
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[loginMutation, navigate]
|
||||||
|
);
|
||||||
|
|
||||||
const logout = () => {
|
const logout = useCallback(() => {
|
||||||
logoutMutation.mutate(undefined, {
|
logoutMutation.mutate(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setAuthState({
|
setAuthState({
|
||||||
@@ -86,12 +92,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
navigate('/login');
|
navigate('/login');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
}, [logoutMutation, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ ...authState, login, logout }}>
|
<AuthContext.Provider value={{ ...authState, login, logout }}>{children}</AuthContext.Provider>
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ const TOKEN_KEY = 'antholume_token';
|
|||||||
|
|
||||||
// Request interceptor to add auth token to requests
|
// Request interceptor to add auth token to requests
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
(config) => {
|
config => {
|
||||||
const token = localStorage.getItem(TOKEN_KEY);
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
if (token && config.headers) {
|
if (token && config.headers) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
error => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Response interceptor to handle auth errors
|
// Response interceptor to handle auth errors
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
(response) => {
|
response => {
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
error => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// Clear token on auth failure
|
// Clear token on auth failure
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
|||||||
type LinkProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };
|
type LinkProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };
|
||||||
|
|
||||||
const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string => {
|
const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string => {
|
||||||
const baseClass = 'transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white';
|
const baseClass =
|
||||||
|
'transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white';
|
||||||
|
|
||||||
if (variant === 'secondary') {
|
if (variant === 'secondary') {
|
||||||
return `${baseClass} bg-black shadow-md hover:text-black hover:bg-white`;
|
return `${baseClass} bg-black shadow-md hover:text-black hover:bg-white`;
|
||||||
@@ -22,11 +23,7 @@ const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string
|
|||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ variant = 'default', children, className = '', ...props }, ref) => {
|
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button ref={ref} className={`${getVariantClasses(variant)} ${className}`.trim()} {...props}>
|
||||||
ref={ref}
|
|
||||||
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -38,11 +35,7 @@ Button.displayName = 'Button';
|
|||||||
export const ButtonLink = forwardRef<HTMLAnchorElement, LinkProps>(
|
export const ButtonLink = forwardRef<HTMLAnchorElement, LinkProps>(
|
||||||
({ variant = 'default', children, className = '', ...props }, ref) => {
|
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a ref={ref} className={`${getVariantClasses(variant)} ${className}`.trim()} {...props}>
|
||||||
ref={ref}
|
|
||||||
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,32 +44,35 @@ export default function HamburgerMenu() {
|
|||||||
className="absolute -top-2 z-50 flex size-7 cursor-pointer opacity-0 lg:hidden"
|
className="absolute -top-2 z-50 flex size-7 cursor-pointer opacity-0 lg:hidden"
|
||||||
id="mobile-nav-checkbox"
|
id="mobile-nav-checkbox"
|
||||||
checked={isOpen}
|
checked={isOpen}
|
||||||
onChange={(e) => setIsOpen(e.target.checked)}
|
onChange={e => setIsOpen(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hamburger icon lines with CSS animations - hidden on desktop */}
|
{/* Hamburger icon lines with CSS animations - hidden on desktop */}
|
||||||
<span
|
<span
|
||||||
className="transition-background z-40 mt-0.5 h-0.5 w-7 bg-black transition-opacity transition-transform duration-500 lg:hidden dark:bg-white"
|
className="z-40 mt-0.5 h-0.5 w-7 bg-black transition-opacity duration-500 lg:hidden dark:bg-white"
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: '5px 0px',
|
transformOrigin: '5px 0px',
|
||||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
transition:
|
||||||
|
'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||||
transform: isOpen ? 'rotate(45deg) translate(2px, -2px)' : 'none',
|
transform: isOpen ? 'rotate(45deg) translate(2px, -2px)' : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="transition-background z-40 mt-1 h-0.5 w-7 bg-black transition-opacity transition-transform duration-500 lg:hidden dark:bg-white"
|
className="z-40 mt-1 h-0.5 w-7 bg-black transition-opacity duration-500 lg:hidden dark:bg-white"
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: '0% 100%',
|
transformOrigin: '0% 100%',
|
||||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
transition:
|
||||||
|
'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||||
opacity: isOpen ? 0 : 1,
|
opacity: isOpen ? 0 : 1,
|
||||||
transform: isOpen ? 'rotate(0deg) scale(0.2, 0.2)' : 'none',
|
transform: isOpen ? 'rotate(0deg) scale(0.2, 0.2)' : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="transition-background z-40 mt-1 h-0.5 w-7 bg-black transition-opacity transition-transform duration-500 lg:hidden dark:bg-white"
|
className="z-40 mt-1 h-0.5 w-7 bg-black transition-opacity duration-500 lg:hidden dark:bg-white"
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: '0% 0%',
|
transformOrigin: '0% 0%',
|
||||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
transition:
|
||||||
|
'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||||
transform: isOpen ? 'rotate(-45deg) translate(0, 6px)' : 'none',
|
transform: isOpen ? 'rotate(-45deg) translate(0, 6px)' : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -102,7 +105,7 @@ export default function HamburgerMenu() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
{navItems.map((item) => (
|
{navItems.map(item => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
@@ -120,11 +123,13 @@ export default function HamburgerMenu() {
|
|||||||
|
|
||||||
{/* Admin section - only visible for admins */}
|
{/* Admin section - only visible for admins */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className={`my-2 flex flex-col gap-4 border-l-4 p-2 pl-6 transition-colors duration-200 ${
|
<div
|
||||||
hasPrefix(location.pathname, '/admin')
|
className={`my-2 flex flex-col gap-4 border-l-4 p-2 pl-6 transition-colors duration-200 ${
|
||||||
? 'border-purple-500 dark:text-white'
|
hasPrefix(location.pathname, '/admin')
|
||||||
: 'border-transparent text-gray-400'
|
? 'border-purple-500 dark:text-white'
|
||||||
}`}>
|
: 'border-transparent text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{/* Admin header - always shown */}
|
{/* Admin header - always shown */}
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
@@ -141,7 +146,7 @@ export default function HamburgerMenu() {
|
|||||||
|
|
||||||
{hasPrefix(location.pathname, '/admin') && (
|
{hasPrefix(location.pathname, '/admin') && (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{adminSubItems.map((item) => (
|
{adminSubItems.map(item => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
@@ -164,7 +169,8 @@ export default function HamburgerMenu() {
|
|||||||
<a
|
<a
|
||||||
className="absolute bottom-0 flex w-full flex-col items-center justify-center gap-2 p-6 text-black dark:text-white"
|
className="absolute bottom-0 flex w-full flex-col items-center justify-center gap-2 p-6 text-black dark:text-white"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://gitea.va.reichard.io/evan/AnthoLume" rel="noreferrer"
|
href="https://gitea.va.reichard.io/evan/AnthoLume"
|
||||||
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<span className="text-xs">v1.0.0</span>
|
<span className="text-xs">v1.0.0</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export default function Layout() {
|
|||||||
{ path: '/search', title: 'Search' },
|
{ path: '/search', title: 'Search' },
|
||||||
{ path: '/settings', title: 'Settings' },
|
{ path: '/settings', title: 'Settings' },
|
||||||
];
|
];
|
||||||
const currentPageTitle = navItems.find(item => location.pathname === item.path)?.title || 'Documents';
|
const currentPageTitle =
|
||||||
|
navItems.find(item => location.pathname === item.path)?.title || 'Documents';
|
||||||
|
|
||||||
// Show loading while checking authentication status
|
// Show loading while checking authentication status
|
||||||
if (isCheckingAuth) {
|
if (isCheckingAuth) {
|
||||||
@@ -61,12 +62,13 @@ export default function Layout() {
|
|||||||
<HamburgerMenu />
|
<HamburgerMenu />
|
||||||
|
|
||||||
{/* Header Title */}
|
{/* Header Title */}
|
||||||
<h1 className="px-6 text-xl font-bold lg:ml-44 dark:text-white">
|
<h1 className="px-6 text-xl font-bold lg:ml-44 dark:text-white">{currentPageTitle}</h1>
|
||||||
{currentPageTitle}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* User Dropdown */}
|
{/* User Dropdown */}
|
||||||
<div className="relative flex w-full items-center justify-end space-x-4 p-4" ref={dropdownRef}>
|
<div
|
||||||
|
className="relative flex w-full items-center justify-end space-x-4 p-4"
|
||||||
|
ref={dropdownRef}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||||
className="relative block text-gray-800 dark:text-gray-200"
|
className="relative block text-gray-800 dark:text-gray-200"
|
||||||
@@ -86,7 +88,7 @@ export default function Layout() {
|
|||||||
<Link
|
<Link
|
||||||
to="/settings"
|
to="/settings"
|
||||||
onClick={() => setIsUserDropdownOpen(false)}
|
onClick={() => setIsUserDropdownOpen(false)}
|
||||||
className="text-md block px-4 py-2 text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
className="block px-4 py-2 text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
@@ -95,7 +97,7 @@ export default function Layout() {
|
|||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="text-md block w-full px-4 py-2 text-left text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
className="block w-full px-4 py-2 text-left text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
@@ -109,10 +111,13 @@ export default function Layout() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||||
className="text-md flex cursor-pointer items-center gap-2 py-4 text-gray-500 dark:text-white"
|
className="flex cursor-pointer items-center gap-2 py-4 text-gray-500 dark:text-white"
|
||||||
>
|
>
|
||||||
<span>{userData?.username || 'User'}</span>
|
<span>{userData?.username || 'User'}</span>
|
||||||
<span className="text-gray-800 transition-transform duration-200 dark:text-gray-200" style={{ transform: isUserDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
<span
|
||||||
|
className="text-gray-800 transition-transform duration-200 dark:text-gray-200"
|
||||||
|
style={{ transform: isUserDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||||
|
>
|
||||||
<ChevronDown size={20} />
|
<ChevronDown size={20} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -120,8 +125,15 @@ export default function Layout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="relative overflow-hidden" style={{ height: 'calc(100dvh - 4rem - env(safe-area-inset-top))' }}>
|
<main
|
||||||
<div id="container" className="h-dvh overflow-auto px-4 md:px-6 lg:ml-48" style={{ paddingBottom: 'calc(5em + env(safe-area-inset-bottom) * 2)' }}>
|
className="relative overflow-hidden"
|
||||||
|
style={{ height: 'calc(100dvh - 4rem - env(safe-area-inset-top))' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="container"
|
||||||
|
className="h-dvh overflow-auto px-4 md:px-6 lg:ml-48"
|
||||||
|
style={{ paddingBottom: 'calc(5em + env(safe-area-inset-bottom) * 2)' }}
|
||||||
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -183,12 +183,7 @@ function DocumentList() {
|
|||||||
return <SkeletonTable rows={10} columns={5} />;
|
return <SkeletonTable rows={10} columns={5} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Table columns={columns} data={data?.documents || []} />;
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
data={data?.documents || []}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -32,17 +32,13 @@ export function Skeleton({
|
|||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
width: width !== undefined ? (typeof width === 'number' ? `${width}px` : width) : undefined,
|
width: width !== undefined ? (typeof width === 'number' ? `${width}px` : width) : undefined,
|
||||||
height: height !== undefined ? (typeof height === 'number' ? `${height}px` : height) : undefined,
|
height:
|
||||||
|
height !== undefined ? (typeof height === 'number' ? `${height}px` : height) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(baseClasses, variantClasses[variant], animationClasses[animation], className)}
|
||||||
baseClasses,
|
|
||||||
variantClasses[variant],
|
|
||||||
animationClasses[animation],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -61,10 +57,7 @@ export function SkeletonText({ lines = 3, className = '', lineClassName = '' }:
|
|||||||
<Skeleton
|
<Skeleton
|
||||||
key={i}
|
key={i}
|
||||||
variant="text"
|
variant="text"
|
||||||
className={cn(
|
className={cn(lineClassName, i === lines - 1 && lines > 1 ? 'w-3/4' : 'w-full')}
|
||||||
lineClassName,
|
|
||||||
i === lines - 1 && lines > 1 ? 'w-3/4' : 'w-full'
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -85,14 +78,7 @@ export function SkeletonAvatar({ size = 'md', className = '' }: SkeletonAvatarPr
|
|||||||
|
|
||||||
const pixelSize = typeof size === 'number' ? size : sizeMap[size];
|
const pixelSize = typeof size === 'number' ? size : sizeMap[size];
|
||||||
|
|
||||||
return (
|
return <Skeleton variant="circular" width={pixelSize} height={pixelSize} className={className} />;
|
||||||
<Skeleton
|
|
||||||
variant="circular"
|
|
||||||
width={pixelSize}
|
|
||||||
height={pixelSize}
|
|
||||||
className={className}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SkeletonCardProps {
|
interface SkeletonCardProps {
|
||||||
@@ -111,7 +97,12 @@ export function SkeletonCard({
|
|||||||
textLines = 3,
|
textLines = 3,
|
||||||
}: SkeletonCardProps) {
|
}: SkeletonCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('bg-white dark:bg-gray-700 rounded-lg p-4 border dark:border-gray-600', className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'bg-white dark:bg-gray-700 rounded-lg p-4 border dark:border-gray-600',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{showAvatar && (
|
{showAvatar && (
|
||||||
<div className="mb-4 flex items-start gap-4">
|
<div className="mb-4 flex items-start gap-4">
|
||||||
<SkeletonAvatar />
|
<SkeletonAvatar />
|
||||||
@@ -121,12 +112,8 @@ export function SkeletonCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showTitle && (
|
{showTitle && <Skeleton variant="text" className="mb-4 h-6 w-1/2" />}
|
||||||
<Skeleton variant="text" className="mb-4 h-6 w-1/2" />
|
{showText && <SkeletonText lines={textLines} />}
|
||||||
)}
|
|
||||||
{showText && (
|
|
||||||
<SkeletonText lines={textLines} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -163,7 +150,10 @@ export function SkeletonTable({
|
|||||||
<tr key={rowIndex} className="border-b last:border-0 dark:border-gray-600">
|
<tr key={rowIndex} className="border-b last:border-0 dark:border-gray-600">
|
||||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||||
<td key={colIndex} className="p-3">
|
<td key={colIndex} className="p-3">
|
||||||
<Skeleton variant="text" className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'} />
|
<Skeleton
|
||||||
|
variant="text"
|
||||||
|
className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -220,11 +210,12 @@ export function InlineLoader({ size = 'md', className = '' }: InlineLoaderProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center justify-center', className)}>
|
<div className={cn('flex items-center justify-center', className)}>
|
||||||
<div className={`${sizeMap[size]} animate-spin rounded-full border-gray-200 border-t-blue-500 dark:border-gray-600`} />
|
<div
|
||||||
|
className={`${sizeMap[size]} animate-spin rounded-full border-gray-200 border-t-blue-500 dark:border-gray-600`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export SkeletonTable for backward compatibility
|
// Re-export SkeletonTable for backward compatibility
|
||||||
export { SkeletonTable as SkeletonTableExport };
|
export { SkeletonTable as SkeletonTableExport };
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cn } from '../utils/cn';
|
|||||||
export interface Column<T> {
|
export interface Column<T> {
|
||||||
key: keyof T;
|
key: keyof T;
|
||||||
header: string;
|
header: string;
|
||||||
render?: (value: any, row: T, index: number) => React.ReactNode;
|
render?: (value: any, _row: T, _index: number) => React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +17,47 @@ export interface TableProps<T> {
|
|||||||
rowKey?: keyof T | ((row: T) => string);
|
rowKey?: keyof T | ((row: T) => string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skeleton table component for loading state
|
||||||
|
function SkeletonTable({
|
||||||
|
rows = 5,
|
||||||
|
columns = 4,
|
||||||
|
className = '',
|
||||||
|
}: {
|
||||||
|
rows?: number;
|
||||||
|
columns?: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn('bg-white dark:bg-gray-700 rounded-lg overflow-hidden', className)}>
|
||||||
|
<table className="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b dark:border-gray-600">
|
||||||
|
{Array.from({ length: columns }).map((_, i) => (
|
||||||
|
<th key={i} className="p-3">
|
||||||
|
<Skeleton variant="text" className="h-5 w-3/4" />
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||||
|
<tr key={rowIndex} className="border-b last:border-0 dark:border-gray-600">
|
||||||
|
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||||
|
<td key={colIndex} className="p-3">
|
||||||
|
<Skeleton
|
||||||
|
variant="text"
|
||||||
|
className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Table<T extends Record<string, any>>({
|
export function Table<T extends Record<string, any>>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
@@ -24,50 +65,18 @@ export function Table<T extends Record<string, any>>({
|
|||||||
emptyMessage = 'No Results',
|
emptyMessage = 'No Results',
|
||||||
rowKey,
|
rowKey,
|
||||||
}: TableProps<T>) {
|
}: TableProps<T>) {
|
||||||
const getRowKey = (row: T, index: number): string => {
|
const getRowKey = (_row: T, index: number): string => {
|
||||||
if (typeof rowKey === 'function') {
|
if (typeof rowKey === 'function') {
|
||||||
return rowKey(row);
|
return rowKey(_row);
|
||||||
}
|
}
|
||||||
if (rowKey) {
|
if (rowKey) {
|
||||||
return String(row[rowKey] ?? index);
|
return String(_row[rowKey] ?? index);
|
||||||
}
|
}
|
||||||
return `row-${index}`;
|
return `row-${index}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skeleton table component for loading state
|
|
||||||
function SkeletonTable({ rows = 5, columns = 4, className = '' }: { rows?: number; columns?: number; className?: string }) {
|
|
||||||
return (
|
|
||||||
<div className={cn('bg-white dark:bg-gray-700 rounded-lg overflow-hidden', className)}>
|
|
||||||
<table className="min-w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b dark:border-gray-600">
|
|
||||||
{Array.from({ length: columns }).map((_, i) => (
|
|
||||||
<th key={i} className="p-3">
|
|
||||||
<Skeleton variant="text" className="h-5 w-3/4" />
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
|
||||||
<tr key={rowIndex} className="border-b last:border-0 dark:border-gray-600">
|
|
||||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
|
||||||
<td key={colIndex} className="p-3">
|
|
||||||
<Skeleton variant="text" className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'} />
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <SkeletonTable rows={5} columns={columns.length} />;
|
||||||
<SkeletonTable rows={5} columns={columns.length} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -76,7 +85,7 @@ export function Table<T extends Record<string, any>>({
|
|||||||
<table className="min-w-full bg-white dark:bg-gray-700">
|
<table className="min-w-full bg-white dark:bg-gray-700">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b dark:border-gray-600">
|
<tr className="border-b dark:border-gray-600">
|
||||||
{columns.map((column) => (
|
{columns.map(column => (
|
||||||
<th
|
<th
|
||||||
key={String(column.key)}
|
key={String(column.key)}
|
||||||
className={`p-3 text-left text-gray-500 dark:text-white ${column.className || ''}`}
|
className={`p-3 text-left text-gray-500 dark:text-white ${column.className || ''}`}
|
||||||
@@ -98,18 +107,13 @@ export function Table<T extends Record<string, any>>({
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
data.map((row, index) => (
|
data.map((row, index) => (
|
||||||
<tr
|
<tr key={getRowKey(row, index)} className="border-b dark:border-gray-600">
|
||||||
key={getRowKey(row, index)}
|
{columns.map(column => (
|
||||||
className="border-b dark:border-gray-600"
|
|
||||||
>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<td
|
<td
|
||||||
key={`${getRowKey(row, index)}-${String(column.key)}`}
|
key={`${getRowKey(row, index)}-${String(column.key)}`}
|
||||||
className={`p-3 text-gray-700 dark:text-gray-300 ${column.className || ''}`}
|
className={`p-3 text-gray-700 dark:text-gray-300 ${column.className || ''}`}
|
||||||
>
|
>
|
||||||
{column.render
|
{column.render ? column.render(row[column.key], row, index) : row[column.key]}
|
||||||
? column.render(row[column.key], row, index)
|
|
||||||
: row[column.key]}
|
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export interface ToastProps {
|
|||||||
onClose?: (id: string) => void;
|
onClose?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getToastStyles = (type: ToastType) => {
|
const getToastStyles = (_type: ToastType) => {
|
||||||
const baseStyles = 'flex items-center gap-3 p-4 rounded-lg shadow-lg border-l-4 transition-all duration-300';
|
const baseStyles =
|
||||||
|
'flex items-center gap-3 p-4 rounded-lg shadow-lg border-l-4 transition-all duration-300';
|
||||||
|
|
||||||
const typeStyles = {
|
const typeStyles = {
|
||||||
info: 'bg-blue-50 dark:bg-blue-900/30 border-blue-500 dark:border-blue-400',
|
info: 'bg-blue-50 dark:bg-blue-900/30 border-blue-500 dark:border-blue-400',
|
||||||
@@ -69,15 +70,11 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${baseStyles} ${typeStyles[type]} ${
|
className={`${baseStyles} ${typeStyles[type]} ${
|
||||||
isAnimatingOut
|
isAnimatingOut ? 'translate-x-full opacity-0' : 'animate-slideInRight opacity-100'
|
||||||
? 'translate-x-full opacity-0'
|
|
||||||
: 'animate-slideInRight opacity-100'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{icons[type]}
|
{icons[type]}
|
||||||
<p className={`flex-1 text-sm font-medium ${textStyles[type]}`}>
|
<p className={`flex-1 text-sm font-medium ${textStyles[type]}`}>{message}</p>
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className={`ml-2 opacity-70 transition-opacity hover:opacity-100 ${textStyles[type]}`}
|
className={`ml-2 opacity-70 transition-opacity hover:opacity-100 ${textStyles[type]}`}
|
||||||
|
|||||||
@@ -16,33 +16,50 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|||||||
const [toasts, setToasts] = useState<(ToastProps & { id: string })[]>([]);
|
const [toasts, setToasts] = useState<(ToastProps & { id: string })[]>([]);
|
||||||
|
|
||||||
const removeToast = useCallback((id: string) => {
|
const removeToast = useCallback((id: string) => {
|
||||||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showToast = useCallback((message: string, type: ToastType = 'info', duration?: number): string => {
|
const showToast = useCallback(
|
||||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
(message: string, _type: ToastType = 'info', _duration?: number): string => {
|
||||||
setToasts((prev) => [...prev, { id, type, message, duration, onClose: removeToast }]);
|
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
return id;
|
setToasts(prev => [
|
||||||
}, [removeToast]);
|
...prev,
|
||||||
|
{ id, type: _type, message, duration: _duration, onClose: removeToast },
|
||||||
|
]);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
[removeToast]
|
||||||
|
);
|
||||||
|
|
||||||
const showInfo = useCallback((message: string, duration?: number) => {
|
const showInfo = useCallback(
|
||||||
return showToast(message, 'info', duration);
|
(message: string, _duration?: number) => {
|
||||||
}, [showToast]);
|
return showToast(message, 'info', _duration);
|
||||||
|
},
|
||||||
|
[showToast]
|
||||||
|
);
|
||||||
|
|
||||||
const showWarning = useCallback((message: string, duration?: number) => {
|
const showWarning = useCallback(
|
||||||
return showToast(message, 'warning', duration);
|
(message: string, _duration?: number) => {
|
||||||
}, [showToast]);
|
return showToast(message, 'warning', _duration);
|
||||||
|
},
|
||||||
|
[showToast]
|
||||||
|
);
|
||||||
|
|
||||||
const showError = useCallback((message: string, duration?: number) => {
|
const showError = useCallback(
|
||||||
return showToast(message, 'error', duration);
|
(message: string, _duration?: number) => {
|
||||||
}, [showToast]);
|
return showToast(message, 'error', _duration);
|
||||||
|
},
|
||||||
|
[showToast]
|
||||||
|
);
|
||||||
|
|
||||||
const clearToasts = useCallback(() => {
|
const clearToasts = useCallback(() => {
|
||||||
setToasts([]);
|
setToasts([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContext.Provider value={{ showToast, showInfo, showWarning, showError, removeToast, clearToasts }}>
|
<ToastContext.Provider
|
||||||
|
value={{ showToast, showInfo, showWarning, showError, removeToast, clearToasts }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<ToastContainer toasts={toasts} />
|
<ToastContainer toasts={toasts} />
|
||||||
</ToastContext.Provider>
|
</ToastContext.Provider>
|
||||||
@@ -61,7 +78,7 @@ function ToastContainer({ toasts }: ToastContainerProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2">
|
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2">
|
||||||
<div className="pointer-events-auto">
|
<div className="pointer-events-auto">
|
||||||
{toasts.map((toast) => (
|
{toasts.map(toast => (
|
||||||
<Toast key={toast.id} {...toast} />
|
<Toast key={toast.id} {...toast} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ export {
|
|||||||
SkeletonTable,
|
SkeletonTable,
|
||||||
SkeletonButton,
|
SkeletonButton,
|
||||||
PageLoader,
|
PageLoader,
|
||||||
InlineLoader
|
InlineLoader,
|
||||||
} from './Skeleton';
|
} from './Skeleton';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,7 @@
|
|||||||
* OpenAPI spec version: 1.0.0
|
* OpenAPI spec version: 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type BackupType = typeof BackupType[keyof typeof BackupType];
|
export type BackupType = (typeof BackupType)[keyof typeof BackupType];
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const BackupType = {
|
export const BackupType = {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type GetActivityParams = {
|
export type GetActivityParams = {
|
||||||
doc_filter?: boolean;
|
doc_filter?: boolean;
|
||||||
document_id?: string;
|
document_id?: string;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type GetDocumentsParams = {
|
export type GetDocumentsParams = {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type GetImportDirectoryParams = {
|
export type GetImportDirectoryParams = {
|
||||||
directory?: string;
|
directory?: string;
|
||||||
select?: string;
|
select?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type GetLogsParams = {
|
export type GetLogsParams = {
|
||||||
filter?: string;
|
filter?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type GetProgressListParams = {
|
export type GetProgressListParams = {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
document?: string;
|
document?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
import type { GetSearchSource } from './getSearchSource';
|
import type { GetSearchSource } from './getSearchSource';
|
||||||
|
|
||||||
export type GetSearchParams = {
|
export type GetSearchParams = {
|
||||||
query: string;
|
query: string;
|
||||||
source: GetSearchSource;
|
source: GetSearchSource;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
* OpenAPI spec version: 1.0.0
|
* OpenAPI spec version: 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type GetSearchSource = typeof GetSearchSource[keyof typeof GetSearchSource];
|
export type GetSearchSource = (typeof GetSearchSource)[keyof typeof GetSearchSource];
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const GetSearchSource = {
|
export const GetSearchSource = {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
* OpenAPI spec version: 1.0.0
|
* OpenAPI spec version: 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ImportResultStatus = typeof ImportResultStatus[keyof typeof ImportResultStatus];
|
export type ImportResultStatus = (typeof ImportResultStatus)[keyof typeof ImportResultStatus];
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const ImportResultStatus = {
|
export const ImportResultStatus = {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
* OpenAPI spec version: 1.0.0
|
* OpenAPI spec version: 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ImportType = typeof ImportType[keyof typeof ImportType];
|
export type ImportType = (typeof ImportType)[keyof typeof ImportType];
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const ImportType = {
|
export const ImportType = {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
* OpenAPI spec version: 1.0.0
|
* OpenAPI spec version: 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type OperationType = typeof OperationType[keyof typeof OperationType];
|
export type OperationType = (typeof OperationType)[keyof typeof OperationType];
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const OperationType = {
|
export const OperationType = {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* OpenAPI spec version: 1.0.0
|
* OpenAPI spec version: 1.0.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type PostAdminActionBodyAction = typeof PostAdminActionBodyAction[keyof typeof PostAdminActionBodyAction];
|
export type PostAdminActionBodyAction =
|
||||||
|
(typeof PostAdminActionBodyAction)[keyof typeof PostAdminActionBodyAction];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||||
export const PostAdminActionBodyAction = {
|
export const PostAdminActionBodyAction = {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ body {
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
height: calc(100% + env(safe-area-inset-bottom));
|
height: calc(100% + env(safe-area-inset-bottom));
|
||||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
||||||
env(safe-area-inset-left);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ export default function AdminImportPage() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (response) => {
|
onSuccess: _response => {
|
||||||
showInfo('Import completed successfully');
|
showInfo('Import completed successfully');
|
||||||
// Redirect to import results page after a short delay
|
// Redirect to import results page after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '/admin/import-results';
|
window.location.href = '/admin/import-results';
|
||||||
}, 1500);
|
}, 1500);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: error => {
|
||||||
showError('Import failed: ' + (error as any).message);
|
showError('Import failed: ' + (error as any).message);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -68,19 +68,13 @@ export default function AdminImportPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<div
|
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
<p className="text-lg font-semibold text-gray-500">Selected Import Directory</p>
|
||||||
>
|
|
||||||
<p className="text-lg font-semibold text-gray-500">
|
|
||||||
Selected Import Directory
|
|
||||||
</p>
|
|
||||||
<form className="flex flex-col gap-4" onSubmit={handleImport}>
|
<form className="flex flex-col gap-4" onSubmit={handleImport}>
|
||||||
<div className="flex w-full justify-between gap-4">
|
<div className="flex w-full justify-between gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<FolderOpen size={20} />
|
<FolderOpen size={20} />
|
||||||
<p className="break-all text-lg font-medium">
|
<p className="break-all text-lg font-medium">{selectedDirectory}</p>
|
||||||
{selectedDirectory}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-4 flex flex-col justify-around gap-2">
|
<div className="mr-4 flex flex-col justify-around gap-2">
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
@@ -126,17 +120,11 @@ export default function AdminImportPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<table
|
<table className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700">
|
||||||
className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<thead className="text-gray-800 dark:text-gray-400">
|
<thead className="text-gray-800 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th className="w-12 border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800"></th>
|
||||||
className="w-12 border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800"
|
<th className="break-all border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800">
|
||||||
></th>
|
|
||||||
<th
|
|
||||||
className="break-all border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800"
|
|
||||||
>
|
|
||||||
{currentPath}
|
{currentPath}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -144,9 +132,7 @@ export default function AdminImportPage() {
|
|||||||
<tbody className="text-black dark:text-white">
|
<tbody className="text-black dark:text-white">
|
||||||
{currentPath !== '/' && (
|
{currentPath !== '/' && (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400"></td>
|
||||||
className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400"
|
|
||||||
></td>
|
|
||||||
<td className="border-b border-gray-200 p-3">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<button onClick={handleNavigateUp}>
|
<button onClick={handleNavigateUp}>
|
||||||
<p>../</p>
|
<p>../</p>
|
||||||
@@ -156,14 +142,14 @@ export default function AdminImportPage() {
|
|||||||
)}
|
)}
|
||||||
{directories.length === 0 ? (
|
{directories.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="p-3 text-center" colSpan={2}>No Folders</td>
|
<td className="p-3 text-center" colSpan={2}>
|
||||||
|
No Folders
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
directories.map((item) => (
|
directories.map(item => (
|
||||||
<tr key={item.name}>
|
<tr key={item.name}>
|
||||||
<td
|
<td className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400">
|
||||||
className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
|
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
|
||||||
<FolderOpen size={20} />
|
<FolderOpen size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,24 +13,16 @@ export default function AdminImportResultsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<table
|
<table className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700">
|
||||||
className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<thead className="text-gray-800 dark:text-gray-400">
|
<thead className="text-gray-800 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Document
|
Document
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Error
|
Error
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -38,7 +30,9 @@ export default function AdminImportResultsPage() {
|
|||||||
<tbody className="text-black dark:text-white">
|
<tbody className="text-black dark:text-white">
|
||||||
{results.length === 0 ? (
|
{results.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="p-3 text-center" colSpan={3}>No Results</td>
|
<td className="p-3 text-center" colSpan={3}>
|
||||||
|
No Results
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
results.map((result: ImportResult, index: number) => (
|
results.map((result: ImportResult, index: number) => (
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { Search } from 'lucide-react';
|
|||||||
export default function AdminLogsPage() {
|
export default function AdminLogsPage() {
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
const { data: logsData, isLoading, refetch } = useGetLogs(
|
const { data: logsData, isLoading, refetch } = useGetLogs(filter ? { filter } : {});
|
||||||
filter ? { filter } : {}
|
|
||||||
);
|
|
||||||
|
|
||||||
const logs = logsData?.data?.logs || [];
|
const logs = logsData?.data?.logs || [];
|
||||||
|
|
||||||
@@ -24,28 +22,26 @@ export default function AdminLogsPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Filter Form */}
|
{/* Filter Form */}
|
||||||
<div
|
<div className="mb-4 flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="mb-4 flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
|
||||||
<div className="flex w-full grow flex-col">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span
|
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm">
|
||||||
className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"
|
|
||||||
>
|
|
||||||
<Search size={15} />
|
<Search size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={e => setFilter(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white p-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white p-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
placeholder="JQ Filter"
|
placeholder="JQ Filter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<Button variant="secondary" type="submit">Filter</Button>
|
<Button variant="secondary" type="submit">
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,18 +33,21 @@ export default function AdminPage() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (response) => {
|
onSuccess: response => {
|
||||||
// Handle file download
|
// Handle file download
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.setAttribute('download', `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`);
|
link.setAttribute(
|
||||||
|
'download',
|
||||||
|
`AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`
|
||||||
|
);
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
showInfo('Backup completed successfully');
|
showInfo('Backup completed successfully');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: error => {
|
||||||
showError('Backup failed: ' + (error as any).message);
|
showError('Backup failed: ' + (error as any).message);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -67,7 +70,7 @@ export default function AdminPage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showInfo('Restore completed successfully');
|
showInfo('Restore completed successfully');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: error => {
|
||||||
showError('Restore failed: ' + (error as any).message);
|
showError('Restore failed: ' + (error as any).message);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -85,7 +88,7 @@ export default function AdminPage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showInfo('Metadata matching started');
|
showInfo('Metadata matching started');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: error => {
|
||||||
showError('Metadata matching failed: ' + (error as any).message);
|
showError('Metadata matching failed: ' + (error as any).message);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -103,7 +106,7 @@ export default function AdminPage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showInfo('Cache tables started');
|
showInfo('Cache tables started');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: error => {
|
||||||
showError('Cache tables failed: ' + (error as any).message);
|
showError('Cache tables failed: ' + (error as any).message);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -117,9 +120,7 @@ export default function AdminPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full grow flex-col gap-4">
|
<div className="flex w-full grow flex-col gap-4">
|
||||||
{/* Backup & Restore Card */}
|
{/* Backup & Restore Card */}
|
||||||
<div
|
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<p className="mb-2 text-lg font-semibold">Backup & Restore</p>
|
<p className="mb-2 text-lg font-semibold">Backup & Restore</p>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Backup Form */}
|
{/* Backup Form */}
|
||||||
@@ -130,7 +131,7 @@ export default function AdminPage() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="backup_covers"
|
id="backup_covers"
|
||||||
checked={backupTypes.covers}
|
checked={backupTypes.covers}
|
||||||
onChange={(e) => setBackupTypes({ ...backupTypes, covers: e.target.checked })}
|
onChange={e => setBackupTypes({ ...backupTypes, covers: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="backup_covers">Covers</label>
|
<label htmlFor="backup_covers">Covers</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,40 +140,39 @@ export default function AdminPage() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="backup_documents"
|
id="backup_documents"
|
||||||
checked={backupTypes.documents}
|
checked={backupTypes.documents}
|
||||||
onChange={(e) => setBackupTypes({ ...backupTypes, documents: e.target.checked })}
|
onChange={e => setBackupTypes({ ...backupTypes, documents: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="backup_documents">Documents</label>
|
<label htmlFor="backup_documents">Documents</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-10 w-40">
|
<div className="h-10 w-40">
|
||||||
<Button variant="secondary" type="submit">Backup</Button>
|
<Button variant="secondary" type="submit">
|
||||||
|
Backup
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Restore Form */}
|
{/* Restore Form */}
|
||||||
<form
|
<form onSubmit={handleRestoreSubmit} className="flex grow justify-between">
|
||||||
onSubmit={handleRestoreSubmit}
|
|
||||||
className="flex grow justify-between"
|
|
||||||
>
|
|
||||||
<div className="flex w-1/2 items-center">
|
<div className="flex w-1/2 items-center">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".zip"
|
accept=".zip"
|
||||||
onChange={(e) => setRestoreFile(e.target.files?.[0] || null)}
|
onChange={e => setRestoreFile(e.target.files?.[0] || null)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-10 w-40">
|
<div className="h-10 w-40">
|
||||||
<Button variant="secondary" type="submit">Restore</Button>
|
<Button variant="secondary" type="submit">
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tasks Card */}
|
{/* Tasks Card */}
|
||||||
<div
|
<div className="flex grow flex-col rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="flex grow flex-col rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<p className="text-lg font-semibold">Tasks</p>
|
<p className="text-lg font-semibold">Tasks</p>
|
||||||
<table className="min-w-full bg-white text-sm dark:bg-gray-700">
|
<table className="min-w-full bg-white text-sm dark:bg-gray-700">
|
||||||
<tbody className="text-black dark:text-white">
|
<tbody className="text-black dark:text-white">
|
||||||
@@ -182,7 +182,9 @@ export default function AdminPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="float-right py-2">
|
<td className="float-right py-2">
|
||||||
<div className="h-10 w-40 text-base">
|
<div className="h-10 w-40 text-base">
|
||||||
<Button variant="secondary" onClick={handleMetadataMatch}>Run</Button>
|
<Button variant="secondary" onClick={handleMetadataMatch}>
|
||||||
|
Run
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -192,7 +194,9 @@ export default function AdminPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="float-right py-2">
|
<td className="float-right py-2">
|
||||||
<div className="h-10 w-40 text-base">
|
<div className="h-10 w-40 text-base">
|
||||||
<Button variant="secondary" onClick={handleCacheTables}>Run</Button>
|
<Button variant="secondary" onClick={handleCacheTables}>
|
||||||
|
Run
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -118,19 +118,21 @@ export default function AdminUsersPage() {
|
|||||||
{/* Add User Form */}
|
{/* Add User Form */}
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<div className="absolute left-10 top-10 rounded bg-gray-200 p-3 shadow-lg shadow-gray-500 transition-all duration-200 dark:bg-gray-600 dark:shadow-gray-900">
|
<div className="absolute left-10 top-10 rounded bg-gray-200 p-3 shadow-lg shadow-gray-500 transition-all duration-200 dark:bg-gray-600 dark:shadow-gray-900">
|
||||||
<form onSubmit={handleCreateUser}
|
<form
|
||||||
className="flex flex-col gap-2 text-sm text-black dark:text-white">
|
onSubmit={handleCreateUser}
|
||||||
|
className="flex flex-col gap-2 text-sm text-black dark:text-white"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newUsername}
|
value={newUsername}
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
onChange={e => setNewUsername(e.target.value)}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
className="bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white"
|
className="bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
className="bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white"
|
className="bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white"
|
||||||
/>
|
/>
|
||||||
@@ -139,7 +141,7 @@ export default function AdminUsersPage() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="new_is_admin"
|
id="new_is_admin"
|
||||||
checked={newIsAdmin}
|
checked={newIsAdmin}
|
||||||
onChange={(e) => setNewIsAdmin(e.target.checked)}
|
onChange={e => setNewIsAdmin(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="new_is_admin">Admin</label>
|
<label htmlFor="new_is_admin">Admin</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,21 +165,29 @@ export default function AdminUsersPage() {
|
|||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">User</th>
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">Password</th>
|
User
|
||||||
<th className="border-b border-gray-200 p-3 text-left text-center font-normal uppercase dark:border-gray-800">
|
</th>
|
||||||
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
|
Password
|
||||||
|
</th>
|
||||||
|
<th className="border-b border-gray-200 p-3 text-center font-normal uppercase dark:border-gray-800">
|
||||||
Permissions
|
Permissions
|
||||||
</th>
|
</th>
|
||||||
<th className="w-48 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">Created</th>
|
<th className="w-48 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
|
Created
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="text-black dark:text-white">
|
<tbody className="text-black dark:text-white">
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="p-3 text-center" colSpan={5}>No Results</td>
|
<td className="p-3 text-center" colSpan={5}>
|
||||||
|
No Results
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
users.map((user) => (
|
users.map(user => (
|
||||||
<tr key={user.id}>
|
<tr key={user.id}>
|
||||||
{/* Delete Button */}
|
{/* Delete Button */}
|
||||||
<td className="relative cursor-pointer border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400">
|
<td className="relative cursor-pointer border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
SkeletonTable,
|
SkeletonTable,
|
||||||
SkeletonButton,
|
SkeletonButton,
|
||||||
PageLoader,
|
PageLoader,
|
||||||
InlineLoader
|
InlineLoader,
|
||||||
} from '../components/Skeleton';
|
} from '../components/Skeleton';
|
||||||
|
|
||||||
export default function ComponentDemoPage() {
|
export default function ComponentDemoPage() {
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ export default function DocumentPage() {
|
|||||||
|
|
||||||
const document = docData?.data?.document as Document;
|
const document = docData?.data?.document as Document;
|
||||||
const progressDataArray = progressData?.data?.progress;
|
const progressDataArray = progressData?.data?.progress;
|
||||||
const progress = Array.isArray(progressDataArray) ? progressDataArray[0] as Progress : undefined;
|
const progress = Array.isArray(progressDataArray)
|
||||||
|
? (progressDataArray[0] as Progress)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
return <div className="text-gray-500 dark:text-white">Document not found</div>;
|
return <div className="text-gray-500 dark:text-white">Document not found</div>;
|
||||||
@@ -75,13 +77,9 @@ export default function DocumentPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative size-full">
|
<div className="relative size-full">
|
||||||
<div
|
<div className="size-full overflow-scroll rounded bg-white p-4 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="size-full overflow-scroll rounded bg-white p-4 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
{/* Document Info - Left Column */}
|
{/* Document Info - Left Column */}
|
||||||
<div
|
<div className="relative float-left mb-2 mr-4 flex w-44 flex-col gap-2 md:w-60 lg:w-80">
|
||||||
className="relative float-left mb-2 mr-4 flex w-44 flex-col gap-2 md:w-60 lg:w-80"
|
|
||||||
>
|
|
||||||
{/* Cover Image */}
|
{/* Cover Image */}
|
||||||
{document.filepath && (
|
{document.filepath && (
|
||||||
<div className="h-60 w-full rounded bg-gray-200 object-fill dark:bg-gray-600">
|
<div className="h-60 w-full rounded bg-gray-200 object-fill dark:bg-gray-600">
|
||||||
@@ -124,7 +122,12 @@ export default function DocumentPage() {
|
|||||||
title="Download"
|
title="Download"
|
||||||
>
|
>
|
||||||
<svg className="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003-3h4a3 3 0 003 3v1m0-3l-3 3m0 0L4 20" />
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 16v1a3 3 0 003-3h4a3 3 0 003 3v1m0-3l-3 3m0 0L4 20"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -192,15 +195,11 @@ export default function DocumentPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Created</p>
|
<p className="text-gray-500">Created</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{new Date(document.created_at).toLocaleDateString()}</p>
|
||||||
{new Date(document.created_at).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Updated</p>
|
<p className="text-gray-500">Updated</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{new Date(document.updated_at).toLocaleDateString()}</p>
|
||||||
{new Date(document.updated_at).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -213,9 +212,7 @@ export default function DocumentPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-gray-500">Est. Time Left:</p>
|
<p className="text-gray-500">Est. Time Left:</p>
|
||||||
<p className="whitespace-nowrap font-medium">
|
<p className="whitespace-nowrap font-medium">{niceSeconds(totalTimeLeftSeconds)}</p>
|
||||||
{niceSeconds(totalTimeLeftSeconds)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div
|
<div className="flex size-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
|
||||||
className="flex size-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<div className="relative my-auto h-48 min-w-fit">
|
<div className="relative my-auto h-48 min-w-fit">
|
||||||
<Link to={`/documents/${doc.id}`}>
|
<Link to={`/documents/${doc.id}`}>
|
||||||
<img
|
<img
|
||||||
@@ -51,13 +49,13 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
<div className="inline-flex shrink-0 items-center">
|
<div className="inline-flex shrink-0 items-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400">Title</p>
|
<p className="text-gray-400">Title</p>
|
||||||
<p className="font-medium">{doc.title || "Unknown"}</p>
|
<p className="font-medium">{doc.title || 'Unknown'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex shrink-0 items-center">
|
<div className="inline-flex shrink-0 items-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400">Author</p>
|
<p className="text-gray-400">Author</p>
|
||||||
<p className="font-medium">{doc.author || "Unknown"}</p>
|
<p className="font-medium">{doc.author || 'Unknown'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex shrink-0 items-center">
|
<div className="inline-flex shrink-0 items-center">
|
||||||
@@ -73,9 +71,7 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="absolute bottom-4 right-4 flex flex-col gap-2 text-gray-500 dark:text-gray-400">
|
||||||
className="absolute bottom-4 right-4 flex flex-col gap-2 text-gray-500 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<Link to={`/activity?document=${doc.id}`}>
|
<Link to={`/activity?document=${doc.id}`}>
|
||||||
<Activity size={20} />
|
<Activity size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -92,10 +88,6 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function DocumentsPage() {
|
export default function DocumentsPage() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -152,21 +144,17 @@ export default function DocumentsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Search Form */}
|
{/* Search Form */}
|
||||||
<div
|
<div className="mb-4 flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="mb-4 flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleSubmit}>
|
||||||
<div className="flex w-full grow flex-col">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span
|
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm">
|
||||||
className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"
|
|
||||||
>
|
|
||||||
<Search size={15} />
|
<Search size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white p-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white p-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
placeholder="Search Author / Title"
|
placeholder="Search Author / Title"
|
||||||
name="search"
|
name="search"
|
||||||
@@ -174,7 +162,9 @@ export default function DocumentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<Button variant="secondary" type="submit">Search</Button>
|
<Button variant="secondary" type="submit">
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,9 +197,7 @@ export default function DocumentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload Button */}
|
{/* Upload Button */}
|
||||||
<div
|
<div className="fixed bottom-6 right-6 flex items-center justify-center rounded-full">
|
||||||
className="fixed bottom-6 right-6 flex items-center justify-center rounded-full"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="upload-file-button"
|
id="upload-file-button"
|
||||||
@@ -220,11 +208,7 @@ export default function DocumentsPage() {
|
|||||||
<div
|
<div
|
||||||
className={`absolute bottom-0 right-0 z-10 flex w-72 flex-col gap-2 rounded bg-gray-800 p-4 text-sm text-white transition-opacity duration-200 dark:bg-gray-200 dark:text-black ${uploadMode ? 'visible opacity-100' : 'invisible opacity-0'}`}
|
className={`absolute bottom-0 right-0 z-10 flex w-72 flex-col gap-2 rounded bg-gray-800 p-4 text-sm text-white transition-opacity duration-200 dark:bg-gray-200 dark:text-black ${uploadMode ? 'visible opacity-100' : 'invisible opacity-0'}`}
|
||||||
>
|
>
|
||||||
<form
|
<form method="POST" encType="multipart/form-data" className="flex flex-col gap-2">
|
||||||
method="POST"
|
|
||||||
encType="multipart/form-data"
|
|
||||||
className="flex flex-col gap-2"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".epub"
|
accept=".epub"
|
||||||
@@ -236,7 +220,7 @@ export default function DocumentsPage() {
|
|||||||
<button
|
<button
|
||||||
className="bg-gray-500 px-2 py-1 font-medium text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-800"
|
className="bg-gray-500 px-2 py-1 font-medium text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-800"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleFileChange({ target: { files: fileInputRef.current?.files } } as any);
|
handleFileChange({ target: { files: fileInputRef.current?.files } } as any);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -44,7 +44,15 @@ interface StreakCardProps {
|
|||||||
maxStreakEndDate: string;
|
maxStreakEndDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StreakCard({ window, currentStreak, currentStreakStartDate, currentStreakEndDate, maxStreak, maxStreakStartDate, maxStreakEndDate }: StreakCardProps) {
|
function StreakCard({
|
||||||
|
window,
|
||||||
|
currentStreak,
|
||||||
|
currentStreakStartDate,
|
||||||
|
currentStreakEndDate,
|
||||||
|
maxStreak,
|
||||||
|
maxStreakStartDate,
|
||||||
|
maxStreakEndDate,
|
||||||
|
}: StreakCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="relative w-full rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700">
|
<div className="relative w-full rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700">
|
||||||
@@ -107,7 +115,9 @@ function LeaderboardCard({ name, data }: LeaderboardCardProps) {
|
|||||||
{data.all.length === 0 ? (
|
{data.all.length === 0 ? (
|
||||||
<p className="text-5xl font-bold text-black dark:text-white">N/A</p>
|
<p className="text-5xl font-bold text-black dark:text-white">N/A</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-5xl font-bold text-black dark:text-white">{data.all[0]?.user_id || 'N/A'}</p>
|
<p className="text-5xl font-bold text-black dark:text-white">
|
||||||
|
{data.all[0]?.user_id || 'N/A'}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,25 +196,10 @@ export default function HomePage() {
|
|||||||
|
|
||||||
{/* Info Cards */}
|
{/* Info Cards */}
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
<InfoCard
|
<InfoCard title="Documents" size={dbInfo?.documents_size || 0} link="./documents" />
|
||||||
title="Documents"
|
<InfoCard title="Activity Records" size={dbInfo?.activity_size || 0} link="./activity" />
|
||||||
size={dbInfo?.documents_size || 0}
|
<InfoCard title="Progress Records" size={dbInfo?.progress_size || 0} link="./progress" />
|
||||||
link="./documents"
|
<InfoCard title="Devices" size={dbInfo?.devices_size || 0} />
|
||||||
/>
|
|
||||||
<InfoCard
|
|
||||||
title="Activity Records"
|
|
||||||
size={dbInfo?.activity_size || 0}
|
|
||||||
link="./activity"
|
|
||||||
/>
|
|
||||||
<InfoCard
|
|
||||||
title="Progress Records"
|
|
||||||
size={dbInfo?.progress_size || 0}
|
|
||||||
link="./progress"
|
|
||||||
/>
|
|
||||||
<InfoCard
|
|
||||||
title="Devices"
|
|
||||||
size={dbInfo?.devices_size || 0}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Streak Cards */}
|
{/* Streak Cards */}
|
||||||
@@ -227,15 +222,15 @@ export default function HomePage() {
|
|||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<LeaderboardCard
|
<LeaderboardCard
|
||||||
name="WPM"
|
name="WPM"
|
||||||
data={userStats?.wpm || { all: [], year: [], month: [], week: []}}
|
data={userStats?.wpm || { all: [], year: [], month: [], week: [] }}
|
||||||
/>
|
/>
|
||||||
<LeaderboardCard
|
<LeaderboardCard
|
||||||
name="Duration"
|
name="Duration"
|
||||||
data={userStats?.duration || { all: [], year: [], month: [], week: []}}
|
data={userStats?.duration || { all: [], year: [], month: [], week: [] }}
|
||||||
/>
|
/>
|
||||||
<LeaderboardCard
|
<LeaderboardCard
|
||||||
name="Words"
|
name="Words"
|
||||||
data={userStats?.words || { all: [], year: [], month: [], week: []}}
|
data={userStats?.words || { all: [], year: [], month: [], week: [] }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await login(username, password);
|
await login(username, password);
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
showError('Invalid credentials');
|
showError('Invalid credentials');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -37,9 +37,7 @@ export default function LoginPage() {
|
|||||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-800 dark:text-white">
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-800 dark:text-white">
|
||||||
<div className="flex w-full flex-wrap">
|
<div className="flex w-full flex-wrap">
|
||||||
<div className="flex w-full flex-col md:w-1/2">
|
<div className="flex w-full flex-col md:w-1/2">
|
||||||
<div
|
<div className="my-auto flex flex-col justify-center px-8 pt-8 md:justify-start md:px-24 md:pt-0 lg:px-32">
|
||||||
className="my-auto flex flex-col justify-center px-8 pt-8 md:justify-start md:px-24 md:pt-0 lg:px-32"
|
|
||||||
>
|
|
||||||
<p className="text-center text-3xl">Welcome.</p>
|
<p className="text-center text-3xl">Welcome.</p>
|
||||||
<form className="flex flex-col pt-3 md:pt-8" onSubmit={handleSubmit}>
|
<form className="flex flex-col pt-3 md:pt-8" onSubmit={handleSubmit}>
|
||||||
<div className="flex flex-col pt-4">
|
<div className="flex flex-col pt-4">
|
||||||
@@ -47,7 +45,7 @@ export default function LoginPage() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
required
|
required
|
||||||
@@ -60,7 +58,7 @@ export default function LoginPage() {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -20,35 +20,29 @@ export default function SearchPage() {
|
|||||||
<div className="flex w-full flex-col gap-4 md:flex-row">
|
<div className="flex w-full flex-col gap-4 md:flex-row">
|
||||||
<div className="flex grow flex-col gap-4">
|
<div className="flex grow flex-col gap-4">
|
||||||
{/* Search Form */}
|
{/* Search Form */}
|
||||||
<div
|
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleSubmit}>
|
||||||
<div className="flex w-full grow flex-col">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span
|
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm">
|
||||||
className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"
|
|
||||||
>
|
|
||||||
<Search size={15} />
|
<Search size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
placeholder="Query"
|
placeholder="Query"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex min-w-[12em]">
|
<div className="relative flex min-w-[12em]">
|
||||||
<span
|
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm">
|
||||||
className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"
|
|
||||||
>
|
|
||||||
<Book size={15} />
|
<Book size={15} />
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={source}
|
value={source}
|
||||||
onChange={(e) => setSource(e.target.value as GetSearchSource)}
|
onChange={e => setSource(e.target.value as GetSearchSource)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
>
|
>
|
||||||
<option value="LibGen">Library Genesis</option>
|
<option value="LibGen">Library Genesis</option>
|
||||||
@@ -56,44 +50,32 @@ export default function SearchPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<Button variant="secondary" type="submit">Search</Button>
|
<Button variant="secondary" type="submit">
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Results Table */}
|
{/* Search Results Table */}
|
||||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<table
|
<table className="min-w-full bg-white text-sm leading-normal md:text-sm dark:bg-gray-700">
|
||||||
className="min-w-full bg-white text-sm leading-normal md:text-sm dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
<thead className="text-gray-800 dark:text-gray-400">
|
<thead className="text-gray-800 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th className="w-12 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"></th>
|
||||||
className="w-12 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
></th>
|
|
||||||
<th
|
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Document
|
Document
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Series
|
Series
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Type
|
Type
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Size
|
Size
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="hidden border-b border-gray-200 p-3 text-left font-normal uppercase md:block dark:border-gray-800">
|
||||||
className="hidden border-b border-gray-200 p-3 text-left font-normal uppercase md:block dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Date
|
Date
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -101,43 +83,44 @@ export default function SearchPage() {
|
|||||||
<tbody className="text-black dark:text-white">
|
<tbody className="text-black dark:text-white">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="p-3 text-center" colSpan={6}>Loading...</td>
|
<td className="p-3 text-center" colSpan={6}>
|
||||||
|
Loading...
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!isLoading && !results && (
|
{!isLoading && !results && (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="p-3 text-center" colSpan={6}>No Results</td>
|
<td className="p-3 text-center" colSpan={6}>
|
||||||
|
No Results
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!isLoading && results && results.map((item: any) => (
|
{!isLoading &&
|
||||||
<tr key={item.id}>
|
results &&
|
||||||
<td
|
results.map((item: any) => (
|
||||||
className="border-b border-gray-200 p-3 text-gray-500 dark:text-gray-500"
|
<tr key={item.id}>
|
||||||
>
|
<td className="border-b border-gray-200 p-3 text-gray-500 dark:text-gray-500">
|
||||||
<button
|
<button className="hover:text-purple-600" title="Download">
|
||||||
className="hover:text-purple-600"
|
<Download size={15} />
|
||||||
title="Download"
|
</button>
|
||||||
>
|
</td>
|
||||||
<Download size={15} />
|
<td className="border-b border-gray-200 p-3">
|
||||||
</button>
|
{item.author || 'N/A'} - {item.title || 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
<td className="border-b border-gray-200 p-3">
|
<td className="border-b border-gray-200 p-3">
|
||||||
{item.author || 'N/A'} - {item.title || 'N/A'}
|
<p>{item.series || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="border-b border-gray-200 p-3">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{item.series || 'N/A'}</p>
|
<p>{item.file_type || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="border-b border-gray-200 p-3">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{item.file_type || 'N/A'}</p>
|
<p>{item.file_size || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="border-b border-gray-200 p-3">
|
<td className="hidden border-b border-gray-200 p-3 md:table-cell">
|
||||||
<p>{item.file_size || 'N/A'}</p>
|
<p>{item.upload_date || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="hidden border-b border-gray-200 p-3 md:table-cell">
|
</tr>
|
||||||
<p>{item.upload_date || 'N/A'}</p>
|
))}
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ export default function SettingsPage() {
|
|||||||
setPassword('');
|
setPassword('');
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
showError('Failed to update password: ' + (error.response?.data?.message || error.message || 'Unknown error'));
|
showError(
|
||||||
|
'Failed to update password: ' +
|
||||||
|
(error.response?.data?.message || error.message || 'Unknown error')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +57,10 @@ export default function SettingsPage() {
|
|||||||
});
|
});
|
||||||
showInfo('Timezone updated successfully');
|
showInfo('Timezone updated successfully');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
showError('Failed to update timezone: ' + (error.response?.data?.message || error.message || 'Unknown error'));
|
showError(
|
||||||
|
'Failed to update timezone: ' +
|
||||||
|
(error.response?.data?.message || error.message || 'Unknown error')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,35 +107,26 @@ export default function SettingsPage() {
|
|||||||
<div className="flex w-full flex-col gap-4 md:flex-row">
|
<div className="flex w-full flex-col gap-4 md:flex-row">
|
||||||
{/* User Profile Card */}
|
{/* User Profile Card */}
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div className="flex flex-col items-center rounded bg-white p-4 text-gray-500 shadow-lg md:w-60 lg:w-80 dark:bg-gray-700 dark:text-white">
|
||||||
className="flex flex-col items-center rounded bg-white p-4 text-gray-500 shadow-lg md:w-60 lg:w-80 dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<User size={60} />
|
<User size={60} />
|
||||||
<p className="text-lg">{settingsData?.data.user.username || "N/A"}</p>
|
<p className="text-lg">{settingsData?.data.user.username || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex grow flex-col gap-4">
|
<div className="flex grow flex-col gap-4">
|
||||||
{/* Change Password Form */}
|
{/* Change Password Form */}
|
||||||
<div
|
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<p className="mb-2 text-lg font-semibold">Change Password</p>
|
<p className="mb-2 text-lg font-semibold">Change Password</p>
|
||||||
<form
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handlePasswordSubmit}>
|
||||||
className="flex flex-col gap-4 lg:flex-row"
|
|
||||||
onSubmit={handlePasswordSubmit}
|
|
||||||
>
|
|
||||||
<div className="flex grow flex-col">
|
<div className="flex grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span
|
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm">
|
||||||
className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"
|
|
||||||
>
|
|
||||||
<Lock size={15} />
|
<Lock size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
@@ -137,44 +134,37 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex grow flex-col">
|
<div className="flex grow flex-col">
|
||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<span
|
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm">
|
||||||
className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"
|
|
||||||
>
|
|
||||||
<Lock size={15} />
|
<Lock size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
placeholder="New Password"
|
placeholder="New Password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<Button variant="secondary" type="submit">Submit</Button>
|
<Button variant="secondary" type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Change Timezone Form */}
|
{/* Change Timezone Form */}
|
||||||
<div
|
<div className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<p className="mb-2 text-lg font-semibold">Change Timezone</p>
|
<p className="mb-2 text-lg font-semibold">Change Timezone</p>
|
||||||
<form
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleTimezoneSubmit}>
|
||||||
className="flex flex-col gap-4 lg:flex-row"
|
|
||||||
onSubmit={handleTimezoneSubmit}
|
|
||||||
>
|
|
||||||
<div className="relative flex grow">
|
<div className="relative flex grow">
|
||||||
<span
|
<span className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm">
|
||||||
className="inline-flex items-center border-y border-l border-gray-300 bg-white px-3 text-sm text-gray-500 shadow-sm"
|
|
||||||
>
|
|
||||||
<Clock size={15} />
|
<Clock size={15} />
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={timezone || 'UTC'}
|
value={timezone || 'UTC'}
|
||||||
onChange={(e) => setTimezone(e.target.value)}
|
onChange={e => setTimezone(e.target.value)}
|
||||||
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
className="w-full flex-1 appearance-none rounded-none border border-gray-300 bg-white px-4 py-2 text-base text-gray-700 shadow-sm placeholder:text-gray-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
>
|
>
|
||||||
<option value="UTC">UTC</option>
|
<option value="UTC">UTC</option>
|
||||||
@@ -190,32 +180,26 @@ export default function SettingsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<Button variant="secondary" type="submit">Submit</Button>
|
<Button variant="secondary" type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Devices Table */}
|
{/* Devices Table */}
|
||||||
<div
|
<div className="flex grow flex-col rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||||
className="flex grow flex-col rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<p className="text-lg font-semibold">Devices</p>
|
<p className="text-lg font-semibold">Devices</p>
|
||||||
<table className="min-w-full bg-white text-sm dark:bg-gray-700">
|
<table className="min-w-full bg-white text-sm dark:bg-gray-700">
|
||||||
<thead className="text-gray-800 dark:text-gray-400">
|
<thead className="text-gray-800 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 pl-0 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 pl-0 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Last Sync
|
Last Sync
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Created
|
Created
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -223,7 +207,9 @@ export default function SettingsPage() {
|
|||||||
<tbody className="text-black dark:text-white">
|
<tbody className="text-black dark:text-white">
|
||||||
{!settingsData?.data.devices || settingsData.data.devices.length === 0 ? (
|
{!settingsData?.data.devices || settingsData.data.devices.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="p-3 text-center" colSpan={3}>No Results</td>
|
<td className="p-3 text-center" colSpan={3}>
|
||||||
|
No Results
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
settingsData.data.devices.map((device: any) => (
|
settingsData.data.devices.map((device: any) => (
|
||||||
@@ -232,10 +218,14 @@ export default function SettingsPage() {
|
|||||||
<p>{device.device_name || 'Unknown'}</p>
|
<p>{device.device_name || 'Unknown'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
<p>{device.last_synced ? new Date(device.last_synced).toLocaleString() : 'N/A'}</p>
|
<p>
|
||||||
|
{device.last_synced ? new Date(device.last_synced).toLocaleString() : 'N/A'}
|
||||||
|
</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
<p>{device.created_at ? new Date(device.created_at).toLocaleString() : 'N/A'}</p>
|
<p>
|
||||||
|
{device.created_at ? new Date(device.created_at).toLocaleString() : 'N/A'}
|
||||||
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
|
|||||||
Reference in New Issue
Block a user