wip 9
This commit is contained in:
18
AGENTS.md
18
AGENTS.md
@@ -8,6 +8,18 @@ Currently mid migration from go templates (`./templates`) to React App (`./front
|
|||||||
- **Frontend**: React with Vite, currently migrating from Go templates (using the V1 API)
|
- **Frontend**: React with Vite, currently migrating from Go templates (using the V1 API)
|
||||||
- **API**: OpenAPI 3.0 spec, generates Go server (oapi-codegen) and TS client (orval)
|
- **API**: OpenAPI 3.0 spec, generates Go server (oapi-codegen) and TS client (orval)
|
||||||
|
|
||||||
|
## Frontend Linting
|
||||||
|
The frontend uses ESLint and Prettier for code quality and formatting.
|
||||||
|
|
||||||
|
### Running Linting
|
||||||
|
- **Check linting**: `cd frontend && npm run lint`
|
||||||
|
- **Fix linting issues**: `cd frontend && npm run lint:fix`
|
||||||
|
- **Check formatting**: `cd frontend && npm run format`
|
||||||
|
- **Format files**: `cd frontend && npm run format:fix`
|
||||||
|
|
||||||
|
### When to Run Linting
|
||||||
|
Run linting after making any changes to the frontend to ensure code quality and consistency. All new code should pass linting before committing.
|
||||||
|
|
||||||
## Data Flow (CRITICAL for migrations)
|
## Data Flow (CRITICAL for migrations)
|
||||||
1. Database schema → SQL queries (`database/query.sql`, `database/query.sql.go`)
|
1. Database schema → SQL queries (`database/query.sql`, `database/query.sql.go`)
|
||||||
2. SQLC models → API handlers (`api/v1/*.go`)
|
2. SQLC models → API handlers (`api/v1/*.go`)
|
||||||
@@ -24,6 +36,12 @@ Currently mid migration from go templates (`./templates`) to React App (`./front
|
|||||||
- Go backend: `go generate ./api/v1/generate.go`
|
- Go backend: `go generate ./api/v1/generate.go`
|
||||||
- TS client: `cd frontend && npm run generate:api`
|
- TS client: `cd frontend && npm run generate:api`
|
||||||
|
|
||||||
|
## Frontend Key Files and Directories
|
||||||
|
- **Source code**: `frontend/src/`
|
||||||
|
- **Configuration**: `frontend/eslint.config.js`, `frontend/.prettierrc`, `frontend/tsconfig.json`
|
||||||
|
- **Build output**: `frontend/dist/`
|
||||||
|
- **Generated API client**: `frontend/src/generated/`
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
- Database queries: `database/query.sql` → SQLc Query shows actual fields returned
|
- Database queries: `database/query.sql` → SQLc Query shows actual fields returned
|
||||||
- SQLC models: `database/query.sql.go` → SQLc Generated Go struct definitions
|
- SQLC models: `database/query.sql.go` → SQLc Generated Go struct definitions
|
||||||
|
|||||||
11
frontend/.prettierrc
Normal file
11
frontend/.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
50
frontend/eslint.config.js
Normal file
50
frontend/eslint.config.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import typescriptParser from "@typescript-eslint/parser";
|
||||||
|
import typescriptPlugin from "@typescript-eslint/eslint-plugin";
|
||||||
|
import reactPlugin from "eslint-plugin-react";
|
||||||
|
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
||||||
|
import tailwindcss from "eslint-plugin-tailwindcss";
|
||||||
|
import prettier from "eslint-plugin-prettier";
|
||||||
|
import eslintConfigPrettier from "eslint-config-prettier";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactPlugin.configs.flat.recommended,
|
||||||
|
reactHooksPlugin.configs.flat.recommended,
|
||||||
|
{
|
||||||
|
files: ["**/*.ts", "**/*.tsx", "**/*.css"],
|
||||||
|
ignores: ["**/generated/**"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"@typescript-eslint": typescriptPlugin,
|
||||||
|
tailwindcss,
|
||||||
|
prettier,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...eslintConfigPrettier.rules,
|
||||||
|
...tailwindcss.configs.recommended.rules,
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ argsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
1681
frontend/package-lock.json
generated
1681
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,11 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate:api": "orval"
|
"generate:api": "orval",
|
||||||
|
"lint": "eslint src --max-warnings=0",
|
||||||
|
"lint:fix": "eslint src --fix",
|
||||||
|
"format": "prettier --check src",
|
||||||
|
"format:fix": "prettier --write src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.62.16",
|
"@tanstack/react-query": "^5.62.16",
|
||||||
@@ -22,10 +26,19 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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/parser": "^8.57.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-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||||
"orval": "^7.5.0",
|
"orval": "^7.5.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.5"
|
"vite": "^6.0.5"
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export default function HamburgerMenu() {
|
|||||||
const isAdmin = user?.is_admin ?? false;
|
const isAdmin = user?.is_admin ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col z-40 relative ml-6">
|
<div className="relative z-40 ml-6 flex flex-col">
|
||||||
{/* Checkbox input for state management */}
|
{/* Checkbox input for state management */}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0"
|
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)}
|
||||||
@@ -49,7 +49,7 @@ export default function HamburgerMenu() {
|
|||||||
|
|
||||||
{/* Hamburger icon lines with CSS animations - hidden on desktop */}
|
{/* Hamburger icon lines with CSS animations - hidden on desktop */}
|
||||||
<span
|
<span
|
||||||
className="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white transition-transform transition-background transition-opacity duration-500"
|
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"
|
||||||
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',
|
||||||
@@ -57,7 +57,7 @@ export default function HamburgerMenu() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white transition-transform transition-background transition-opacity duration-500"
|
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"
|
||||||
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',
|
||||||
@@ -66,7 +66,7 @@ export default function HamburgerMenu() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white transition-transform transition-background transition-opacity duration-500"
|
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"
|
||||||
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',
|
||||||
@@ -77,7 +77,7 @@ export default function HamburgerMenu() {
|
|||||||
{/* Navigation menu with slide animation */}
|
{/* Navigation menu with slide animation */}
|
||||||
<div
|
<div
|
||||||
id="menu"
|
id="menu"
|
||||||
className="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg"
|
className="fixed -ml-6 h-full w-56 bg-white shadow-lg lg:w-48 dark:bg-gray-700"
|
||||||
style={{
|
style={{
|
||||||
top: 0,
|
top: 0,
|
||||||
paddingTop: 'env(safe-area-inset-top)',
|
paddingTop: 'env(safe-area-inset-top)',
|
||||||
@@ -96,8 +96,8 @@ export default function HamburgerMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<div className="h-16 flex justify-end lg:justify-around">
|
<div className="flex h-16 justify-end lg:justify-around">
|
||||||
<p className="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0">
|
<p className="my-auto pr-8 text-right text-xl font-bold lg:pr-0 dark:text-white">
|
||||||
AnthoLume
|
AnthoLume
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +107,7 @@ export default function HamburgerMenu() {
|
|||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className={`flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 ${
|
className={`my-2 flex w-full items-center justify-start border-l-4 p-2 pl-6 transition-colors duration-200 ${
|
||||||
location.pathname === item.path
|
location.pathname === item.path
|
||||||
? 'border-purple-500 dark:text-white'
|
? 'border-purple-500 dark:text-white'
|
||||||
: 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
: 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
||||||
@@ -120,7 +120,7 @@ export default function HamburgerMenu() {
|
|||||||
|
|
||||||
{/* Admin section - only visible for admins */}
|
{/* Admin section - only visible for admins */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className={`flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4 ${
|
<div className={`my-2 flex flex-col gap-4 border-l-4 p-2 pl-6 transition-colors duration-200 ${
|
||||||
hasPrefix(location.pathname, '/admin')
|
hasPrefix(location.pathname, '/admin')
|
||||||
? 'border-purple-500 dark:text-white'
|
? 'border-purple-500 dark:text-white'
|
||||||
: 'border-transparent text-gray-400'
|
: 'border-transparent text-gray-400'
|
||||||
@@ -129,7 +129,7 @@ export default function HamburgerMenu() {
|
|||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className={`flex justify-start w-full ${
|
className={`flex w-full justify-start ${
|
||||||
location.pathname === '/admin' && !hasPrefix(location.pathname, '/admin/')
|
location.pathname === '/admin' && !hasPrefix(location.pathname, '/admin/')
|
||||||
? 'dark:text-white'
|
? 'dark:text-white'
|
||||||
: 'text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
: 'text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
||||||
@@ -146,7 +146,7 @@ export default function HamburgerMenu() {
|
|||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className={`flex justify-start w-full ${
|
className={`flex w-full justify-start ${
|
||||||
location.pathname === item.path
|
location.pathname === item.path
|
||||||
? 'dark:text-white'
|
? 'dark:text-white'
|
||||||
: 'text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
: 'text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
||||||
@@ -162,9 +162,9 @@ export default function HamburgerMenu() {
|
|||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
<a
|
<a
|
||||||
className="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 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"
|
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>
|
||||||
|
|||||||
@@ -54,19 +54,19 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 min-h-screen">
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-800">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between w-full h-16">
|
<div className="flex h-16 w-full items-center justify-between">
|
||||||
{/* Mobile Navigation Button with CSS animations */}
|
{/* Mobile Navigation Button with CSS animations */}
|
||||||
<HamburgerMenu />
|
<HamburgerMenu />
|
||||||
|
|
||||||
{/* Header Title */}
|
{/* Header Title */}
|
||||||
<h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
<h1 className="px-6 text-xl font-bold lg:ml-44 dark:text-white">
|
||||||
{currentPageTitle}
|
{currentPageTitle}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* User Dropdown */}
|
{/* User Dropdown */}
|
||||||
<div className="relative flex items-center justify-end w-full p-4 space-x-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"
|
||||||
@@ -75,8 +75,8 @@ export default function Layout() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isUserDropdownOpen && (
|
{isUserDropdownOpen && (
|
||||||
<div className="transition duration-200 z-20 absolute right-4 top-16 pt-4">
|
<div className="absolute right-4 top-16 z-20 pt-4 transition duration-200">
|
||||||
<div className="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5">
|
<div className="w-40 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 dark:bg-gray-700 dark:shadow-gray-800">
|
||||||
<div
|
<div
|
||||||
className="py-1"
|
className="py-1"
|
||||||
role="menu"
|
role="menu"
|
||||||
@@ -86,7 +86,7 @@ export default function Layout() {
|
|||||||
<Link
|
<Link
|
||||||
to="/settings"
|
to="/settings"
|
||||||
onClick={() => setIsUserDropdownOpen(false)}
|
onClick={() => setIsUserDropdownOpen(false)}
|
||||||
className="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
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"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
@@ -95,7 +95,7 @@ export default function Layout() {
|
|||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 w-full text-left"
|
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"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
@@ -109,10 +109,10 @@ export default function Layout() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||||
className="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
|
className="text-md 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 dark:text-gray-200 transition-transform duration-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>
|
||||||
@@ -121,7 +121,7 @@ export default function Layout() {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="relative overflow-hidden" style={{ height: 'calc(100dvh - 4rem - env(safe-area-inset-top))' }}>
|
<main className="relative overflow-hidden" style={{ height: 'calc(100dvh - 4rem - env(safe-area-inset-top))' }}>
|
||||||
<div id="container" className="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48" style={{ paddingBottom: 'calc(5em + env(safe-area-inset-bottom) * 2)' }}>
|
<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>
|
||||||
|
|||||||
@@ -113,16 +113,16 @@ export function SkeletonCard({
|
|||||||
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="flex items-start gap-4 mb-4">
|
<div className="mb-4 flex items-start gap-4">
|
||||||
<SkeletonAvatar />
|
<SkeletonAvatar />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Skeleton variant="text" className="w-3/4 mb-2" />
|
<Skeleton variant="text" className="mb-2 w-3/4" />
|
||||||
<Skeleton variant="text" className="w-1/2" />
|
<Skeleton variant="text" className="w-1/2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showTitle && (
|
{showTitle && (
|
||||||
<Skeleton variant="text" className="w-1/2 mb-4 h-6" />
|
<Skeleton variant="text" className="mb-4 h-6 w-1/2" />
|
||||||
)}
|
)}
|
||||||
{showText && (
|
{showText && (
|
||||||
<SkeletonText lines={textLines} />
|
<SkeletonText lines={textLines} />
|
||||||
@@ -152,7 +152,7 @@ export function SkeletonTable({
|
|||||||
<tr className="border-b dark:border-gray-600">
|
<tr className="border-b dark:border-gray-600">
|
||||||
{Array.from({ length: columns }).map((_, i) => (
|
{Array.from({ length: columns }).map((_, i) => (
|
||||||
<th key={i} className="p-3">
|
<th key={i} className="p-3">
|
||||||
<Skeleton variant="text" className="w-3/4 h-5" />
|
<Skeleton variant="text" className="h-5 w-3/4" />
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -160,7 +160,7 @@ export function SkeletonTable({
|
|||||||
)}
|
)}
|
||||||
<tbody>
|
<tbody>
|
||||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||||
<tr key={rowIndex} className="border-b dark:border-gray-600 last:border-0">
|
<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'} />
|
||||||
@@ -199,9 +199,9 @@ export function PageLoader({ message = 'Loading...', className = '' }: PageLoade
|
|||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col items-center justify-center min-h-[400px] gap-4', className)}>
|
<div className={cn('flex flex-col items-center justify-center min-h-[400px] gap-4', className)}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-12 h-12 border-4 border-gray-200 dark:border-gray-600 border-t-blue-500 rounded-full animate-spin" />
|
<div className="size-12 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500 dark:border-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 dark:text-gray-400 text-sm font-medium">{message}</p>
|
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{message}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ 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]} border-gray-200 dark:border-gray-600 border-t-blue-500 rounded-full animate-spin`} />
|
<div className={`${sizeMap[size]} animate-spin rounded-full border-gray-200 border-t-blue-500 dark:border-gray-600`} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ export function Table<T extends Record<string, any>>({
|
|||||||
<tr className="border-b dark:border-gray-600">
|
<tr className="border-b dark:border-gray-600">
|
||||||
{Array.from({ length: columns }).map((_, i) => (
|
{Array.from({ length: columns }).map((_, i) => (
|
||||||
<th key={i} className="p-3">
|
<th key={i} className="p-3">
|
||||||
<Skeleton variant="text" className="w-3/4 h-5" />
|
<Skeleton variant="text" className="h-5 w-3/4" />
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||||
<tr key={rowIndex} className="border-b dark:border-gray-600 last:border-0">
|
<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'} />
|
||||||
@@ -79,7 +79,7 @@ export function Table<T extends Record<string, any>>({
|
|||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<th
|
<th
|
||||||
key={String(column.key)}
|
key={String(column.key)}
|
||||||
className={`text-left p-3 text-gray-500 dark:text-white ${column.className || ''}`}
|
className={`p-3 text-left text-gray-500 dark:text-white ${column.className || ''}`}
|
||||||
>
|
>
|
||||||
{column.header}
|
{column.header}
|
||||||
</th>
|
</th>
|
||||||
@@ -91,7 +91,7 @@ export function Table<T extends Record<string, any>>({
|
|||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={columns.length}
|
colSpan={columns.length}
|
||||||
className="text-center p-3 text-gray-700 dark:text-gray-300"
|
className="p-3 text-center text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp
|
|||||||
<div
|
<div
|
||||||
className={`${baseStyles} ${typeStyles[type]} ${
|
className={`${baseStyles} ${typeStyles[type]} ${
|
||||||
isAnimatingOut
|
isAnimatingOut
|
||||||
? 'opacity-0 translate-x-full'
|
? 'translate-x-full opacity-0'
|
||||||
: 'animate-slideInRight opacity-100'
|
: 'animate-slideInRight opacity-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -80,7 +80,7 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className={`ml-2 opacity-70 hover:opacity-100 transition-opacity ${textStyles[type]}`}
|
className={`ml-2 opacity-70 transition-opacity hover:opacity-100 ${textStyles[type]}`}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function ToastContainer({ toasts }: ToastContainerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full pointer-events-none">
|
<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} />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function ActivityPage() {
|
|||||||
render: (_: any, row: any) => (
|
render: (_: any, row: any) => (
|
||||||
<Link
|
<Link
|
||||||
to={`/documents/${row.document_id}`}
|
to={`/documents/${row.document_id}`}
|
||||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||||
>
|
>
|
||||||
{row.author || 'Unknown'} - {row.title || 'Unknown'}
|
{row.author || 'Unknown'} - {row.title || 'Unknown'}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -69,21 +69,21 @@ export default function AdminImportPage() {
|
|||||||
<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 flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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">
|
<p className="text-lg font-semibold text-gray-500">
|
||||||
Selected Import Directory
|
Selected Import Directory
|
||||||
</p>
|
</p>
|
||||||
<form className="flex gap-4 flex-col" onSubmit={handleImport}>
|
<form className="flex flex-col gap-4" onSubmit={handleImport}>
|
||||||
<div className="flex justify-between gap-4 w-full">
|
<div className="flex w-full justify-between gap-4">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex items-center gap-4">
|
||||||
<FolderOpen size={20} />
|
<FolderOpen size={20} />
|
||||||
<p className="font-medium text-lg break-all">
|
<p className="break-all text-lg font-medium">
|
||||||
{selectedDirectory}
|
{selectedDirectory}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-around gap-2 mr-4">
|
<div className="mr-4 flex flex-col justify-around gap-2">
|
||||||
<div className="inline-flex gap-2 items-center">
|
<div className="inline-flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
id="direct"
|
id="direct"
|
||||||
@@ -92,7 +92,7 @@ export default function AdminImportPage() {
|
|||||||
/>
|
/>
|
||||||
<label htmlFor="direct">Direct</label>
|
<label htmlFor="direct">Direct</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex gap-2 items-center">
|
<div className="inline-flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
id="copy"
|
id="copy"
|
||||||
@@ -127,15 +127,15 @@ export default function AdminImportPage() {
|
|||||||
<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 leading-normal bg-white dark:bg-gray-700 text-sm"
|
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="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 w-12"
|
className="w-12 border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800"
|
||||||
></th>
|
></th>
|
||||||
<th
|
<th
|
||||||
className="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 break-all"
|
className="break-all border-b border-gray-200 p-3 text-left font-normal dark:border-gray-800"
|
||||||
>
|
>
|
||||||
{currentPath}
|
{currentPath}
|
||||||
</th>
|
</th>
|
||||||
@@ -145,9 +145,9 @@ export default function AdminImportPage() {
|
|||||||
{currentPath !== '/' && (
|
{currentPath !== '/' && (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
className="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
|
className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400"
|
||||||
></td>
|
></td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<button onClick={handleNavigateUp}>
|
<button onClick={handleNavigateUp}>
|
||||||
<p>../</p>
|
<p>../</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -156,19 +156,19 @@ export default function AdminImportPage() {
|
|||||||
)}
|
)}
|
||||||
{directories.length === 0 ? (
|
{directories.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-center p-3" 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="p-3 border-b border-gray-200 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>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
|
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
|
||||||
<p>{item.name ?? ''}</p>
|
<p>{item.name ?? ''}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,22 +14,22 @@ export default function AdminImportResultsPage() {
|
|||||||
<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 leading-normal bg-white dark:bg-gray-700 text-sm"
|
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="p-3 font-normal text-left uppercase border-b border-gray-200 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="p-3 font-normal text-left uppercase border-b border-gray-200 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="p-3 font-normal text-left uppercase border-b border-gray-200 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>
|
||||||
@@ -38,13 +38,13 @@ 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="text-center p-3" 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) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td
|
<td
|
||||||
className="p-3 border-b border-gray-200 grid"
|
className="grid border-b border-gray-200 p-3"
|
||||||
style={{ gridTemplateColumns: '4rem auto' }}
|
style={{ gridTemplateColumns: '4rem auto' }}
|
||||||
>
|
>
|
||||||
<span className="text-gray-800 dark:text-gray-400">Name:</span>
|
<span className="text-gray-800 dark:text-gray-400">Name:</span>
|
||||||
@@ -56,10 +56,10 @@ export default function AdminImportResultsPage() {
|
|||||||
<span className="text-gray-800 dark:text-gray-400">File:</span>
|
<span className="text-gray-800 dark:text-gray-400">File:</span>
|
||||||
<span>{result.path}</span>
|
<span>{result.path}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{result.status}</p>
|
<p>{result.status}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{result.error || ''}</p>
|
<p>{result.error || ''}</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ export default function AdminLogsPage() {
|
|||||||
<div>
|
<div>
|
||||||
{/* Filter Form */}
|
{/* Filter Form */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 gap-4 flex-col lg:flex-row" onSubmit={handleFilterSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
|
||||||
<div className="flex flex-col w-full grow">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="flex relative">
|
<div className="relative flex">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-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>
|
||||||
@@ -39,7 +39,7 @@ export default function AdminLogsPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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>
|
||||||
@@ -52,7 +52,7 @@ export default function AdminLogsPage() {
|
|||||||
|
|
||||||
{/* Log Display */}
|
{/* Log Display */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col-reverse text-black dark:text-white w-full overflow-scroll"
|
className="flex w-full flex-col-reverse overflow-scroll text-black dark:text-white"
|
||||||
style={{ fontFamily: 'monospace' }}
|
style={{ fontFamily: 'monospace' }}
|
||||||
>
|
>
|
||||||
{logs.map((log: string, index: number) => (
|
{logs.map((log: string, index: number) => (
|
||||||
|
|||||||
@@ -115,16 +115,16 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-4 grow">
|
<div className="flex w-full grow flex-col gap-4">
|
||||||
{/* Backup & Restore Card */}
|
{/* Backup & Restore Card */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 mb-2">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 */}
|
||||||
<form className="flex justify-between" onSubmit={handleBackupSubmit}>
|
<form className="flex justify-between" onSubmit={handleBackupSubmit}>
|
||||||
<div className="flex gap-8 items-center">
|
<div className="flex items-center gap-8">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -144,7 +144,7 @@ export default function AdminPage() {
|
|||||||
<label htmlFor="backup_documents">Documents</label>
|
<label htmlFor="backup_documents">Documents</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-40 h-10">
|
<div className="h-10 w-40">
|
||||||
<Button variant="secondary" type="submit">Backup</Button>
|
<Button variant="secondary" type="submit">Backup</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -152,9 +152,9 @@ export default function AdminPage() {
|
|||||||
{/* Restore Form */}
|
{/* Restore Form */}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleRestoreSubmit}
|
onSubmit={handleRestoreSubmit}
|
||||||
className="flex justify-between grow"
|
className="flex grow justify-between"
|
||||||
>
|
>
|
||||||
<div className="flex items-center w-1/2">
|
<div className="flex w-1/2 items-center">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".zip"
|
accept=".zip"
|
||||||
@@ -162,7 +162,7 @@ export default function AdminPage() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-40 h-10">
|
<div className="h-10 w-40">
|
||||||
<Button variant="secondary" type="submit">Restore</Button>
|
<Button variant="secondary" type="submit">Restore</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -171,17 +171,17 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
{/* Tasks Card */}
|
{/* Tasks Card */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 dark:bg-gray-700 text-sm">
|
<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">
|
||||||
<tr>
|
<tr>
|
||||||
<td className="pl-0">
|
<td className="pl-0">
|
||||||
<p>Metadata Matching</p>
|
<p>Metadata Matching</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 float-right">
|
<td className="float-right py-2">
|
||||||
<div className="w-40 h-10 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>
|
||||||
@@ -190,8 +190,8 @@ export default function AdminPage() {
|
|||||||
<td>
|
<td>
|
||||||
<p>Cache Tables</p>
|
<p>Cache Tables</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 float-right">
|
<td className="float-right py-2">
|
||||||
<div className="w-40 h-10 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>
|
||||||
|
|||||||
@@ -117,22 +117,22 @@ export default function AdminUsersPage() {
|
|||||||
<div className="relative h-full overflow-x-auto">
|
<div className="relative h-full overflow-x-auto">
|
||||||
{/* Add User Form */}
|
{/* Add User Form */}
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<div className="absolute top-10 left-10 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
<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 onSubmit={handleCreateUser}
|
||||||
className="flex flex-col gap-2 text-black dark:text-white text-sm">
|
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="p-2 bg-gray-300 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="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
className="bg-gray-300 p-2 text-black dark:bg-gray-700 dark:text-white"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -144,7 +144,7 @@ export default function AdminUsersPage() {
|
|||||||
<label htmlFor="new_is_admin">Admin</label>
|
<label htmlFor="new_is_admin">Admin</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
className="bg-gray-500 px-2 py-1 font-medium text-white hover:bg-gray-800 dark:text-gray-800 dark:hover:bg-gray-100"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
@@ -155,38 +155,38 @@ export default function AdminUsersPage() {
|
|||||||
|
|
||||||
{/* Users Table */}
|
{/* Users Table */}
|
||||||
<div className="min-w-full overflow-scroll rounded shadow">
|
<div className="min-w-full overflow-scroll rounded shadow">
|
||||||
<table className="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
<table 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 className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-12">
|
<th className="w-12 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">
|
||||||
<button onClick={() => setShowAddForm(!showAddForm)}>
|
<button onClick={() => setShowAddForm(!showAddForm)}>
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">User</th>
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">User</th>
|
||||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Password</th>
|
<th className="border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800">Password</th>
|
||||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 text-center">
|
<th className="border-b border-gray-200 p-3 text-left text-center font-normal uppercase dark:border-gray-800">
|
||||||
Permissions
|
Permissions
|
||||||
</th>
|
</th>
|
||||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-48">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="text-center p-3" 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="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer relative">
|
<td className="relative cursor-pointer border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400">
|
||||||
<button onClick={() => handleDeleteUser(user.id)}>
|
<button onClick={() => handleDeleteUser(user.id)}>
|
||||||
<Trash2 size={20} />
|
<Trash2 size={20} />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
{/* User ID */}
|
{/* User ID */}
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{user.id}</p>
|
<p>{user.id}</p>
|
||||||
</td>
|
</td>
|
||||||
{/* Password Reset */}
|
{/* Password Reset */}
|
||||||
@@ -196,20 +196,20 @@ export default function AdminUsersPage() {
|
|||||||
const password = prompt(`Enter new password for ${user.id}`);
|
const password = prompt(`Enter new password for ${user.id}`);
|
||||||
if (password) handleUpdatePassword(user.id, password);
|
if (password) handleUpdatePassword(user.id, password);
|
||||||
}}
|
}}
|
||||||
className="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
className="bg-gray-500 px-2 py-1 font-medium text-white hover:bg-gray-800 dark:text-gray-800 dark:hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
{/* Admin Toggle */}
|
{/* Admin Toggle */}
|
||||||
<td className="flex gap-2 justify-center p-3 border-b border-gray-200 text-center min-w-40">
|
<td className="flex min-w-40 justify-center gap-2 border-b border-gray-200 p-3 text-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleAdmin(user.id, true)}
|
onClick={() => handleToggleAdmin(user.id, true)}
|
||||||
disabled={user.admin}
|
disabled={user.admin}
|
||||||
className={`px-2 py-1 rounded-md text-white dark:text-black ${
|
className={`rounded-md px-2 py-1 text-white dark:text-black ${
|
||||||
user.admin
|
user.admin
|
||||||
? 'bg-gray-800 dark:bg-gray-100 cursor-default'
|
? 'cursor-default bg-gray-800 dark:bg-gray-100'
|
||||||
: 'bg-gray-400 dark:bg-gray-600 cursor-pointer'
|
: 'cursor-pointer bg-gray-400 dark:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
admin
|
admin
|
||||||
@@ -217,17 +217,17 @@ export default function AdminUsersPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleToggleAdmin(user.id, false)}
|
onClick={() => handleToggleAdmin(user.id, false)}
|
||||||
disabled={!user.admin}
|
disabled={!user.admin}
|
||||||
className={`px-2 py-1 rounded-md text-white dark:text-black ${
|
className={`rounded-md px-2 py-1 text-white dark:text-black ${
|
||||||
!user.admin
|
!user.admin
|
||||||
? 'bg-gray-800 dark:bg-gray-100 cursor-default'
|
? 'cursor-default bg-gray-800 dark:bg-gray-100'
|
||||||
: 'bg-gray-400 dark:bg-gray-600 cursor-pointer'
|
: 'cursor-pointer bg-gray-400 dark:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
user
|
user
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
{/* Created Date */}
|
{/* Created Date */}
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{user.created_at}</p>
|
<p>{user.created_at}</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -42,31 +42,31 @@ export default function ComponentDemoPage() {
|
|||||||
<h1 className="text-2xl font-bold dark:text-white">UI Components Demo</h1>
|
<h1 className="text-2xl font-bold dark:text-white">UI Components Demo</h1>
|
||||||
|
|
||||||
{/* Toast Demos */}
|
{/* Toast Demos */}
|
||||||
<section className="bg-white dark:bg-gray-700 rounded-lg p-6 shadow">
|
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700">
|
||||||
<h2 className="text-xl font-semibold mb-4 dark:text-white">Toast Notifications</h2>
|
<h2 className="mb-4 text-xl font-semibold dark:text-white">Toast Notifications</h2>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleDemoClick}
|
onClick={handleDemoClick}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isLoading ? <InlineLoader size="sm" /> : 'Show Info Toast'}
|
{isLoading ? <InlineLoader size="sm" /> : 'Show Info Toast'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleWarningClick}
|
onClick={handleWarningClick}
|
||||||
className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
|
className="rounded bg-yellow-500 px-4 py-2 text-white hover:bg-yellow-600"
|
||||||
>
|
>
|
||||||
Show Warning Toast (10s)
|
Show Warning Toast (10s)
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleErrorClick}
|
onClick={handleErrorClick}
|
||||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
className="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600"
|
||||||
>
|
>
|
||||||
Show Error Toast
|
Show Error Toast
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleCustomToast}
|
onClick={handleCustomToast}
|
||||||
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
|
className="rounded bg-purple-500 px-4 py-2 text-white hover:bg-purple-600"
|
||||||
>
|
>
|
||||||
Show Custom Toast
|
Show Custom Toast
|
||||||
</button>
|
</button>
|
||||||
@@ -74,15 +74,15 @@ export default function ComponentDemoPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Skeleton Demos */}
|
{/* Skeleton Demos */}
|
||||||
<section className="bg-white dark:bg-gray-700 rounded-lg p-6 shadow">
|
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700">
|
||||||
<h2 className="text-xl font-semibold mb-4 dark:text-white">Skeleton Loading Components</h2>
|
<h2 className="mb-4 text-xl font-semibold dark:text-white">Skeleton Loading Components</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
{/* Basic Skeletons */}
|
{/* Basic Skeletons */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium dark:text-gray-300">Basic Skeletons</h3>
|
<h3 className="text-lg font-medium dark:text-gray-300">Basic Skeletons</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="w-full h-8" />
|
<Skeleton className="h-8 w-full" />
|
||||||
<Skeleton variant="text" className="w-3/4" />
|
<Skeleton variant="text" className="w-3/4" />
|
||||||
<Skeleton variant="text" className="w-1/2" />
|
<Skeleton variant="text" className="w-1/2" />
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -113,7 +113,7 @@ export default function ComponentDemoPage() {
|
|||||||
{/* Skeleton Button */}
|
{/* Skeleton Button */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium dark:text-gray-300">Skeleton Button</h3>
|
<h3 className="text-lg font-medium dark:text-gray-300">Skeleton Button</h3>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex flex-wrap gap-2">
|
||||||
<SkeletonButton width={120} />
|
<SkeletonButton width={120} />
|
||||||
<SkeletonButton className="w-full max-w-xs" />
|
<SkeletonButton className="w-full max-w-xs" />
|
||||||
</div>
|
</div>
|
||||||
@@ -122,9 +122,9 @@ export default function ComponentDemoPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Skeleton Card Demo */}
|
{/* Skeleton Card Demo */}
|
||||||
<section className="bg-white dark:bg-gray-700 rounded-lg p-6 shadow">
|
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700">
|
||||||
<h2 className="text-xl font-semibold mb-4 dark:text-white">Skeleton Cards</h2>
|
<h2 className="mb-4 text-xl font-semibold dark:text-white">Skeleton Cards</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<SkeletonCard />
|
<SkeletonCard />
|
||||||
<SkeletonCard showAvatar />
|
<SkeletonCard showAvatar />
|
||||||
<SkeletonCard showAvatar showTitle showText textLines={4} />
|
<SkeletonCard showAvatar showTitle showText textLines={4} />
|
||||||
@@ -132,20 +132,20 @@ export default function ComponentDemoPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Skeleton Table Demo */}
|
{/* Skeleton Table Demo */}
|
||||||
<section className="bg-white dark:bg-gray-700 rounded-lg p-6 shadow">
|
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700">
|
||||||
<h2 className="text-xl font-semibold mb-4 dark:text-white">Skeleton Table</h2>
|
<h2 className="mb-4 text-xl font-semibold dark:text-white">Skeleton Table</h2>
|
||||||
<SkeletonTable rows={5} columns={4} />
|
<SkeletonTable rows={5} columns={4} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Page Loader Demo */}
|
{/* Page Loader Demo */}
|
||||||
<section className="bg-white dark:bg-gray-700 rounded-lg p-6 shadow">
|
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700">
|
||||||
<h2 className="text-xl font-semibold mb-4 dark:text-white">Page Loader</h2>
|
<h2 className="mb-4 text-xl font-semibold dark:text-white">Page Loader</h2>
|
||||||
<PageLoader message="Loading demo content..." />
|
<PageLoader message="Loading demo content..." />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Inline Loader Demo */}
|
{/* Inline Loader Demo */}
|
||||||
<section className="bg-white dark:bg-gray-700 rounded-lg p-6 shadow">
|
<section className="rounded-lg bg-white p-6 shadow dark:bg-gray-700">
|
||||||
<h2 className="text-xl font-semibold mb-4 dark:text-white">Inline Loader</h2>
|
<h2 className="mb-4 text-xl font-semibold dark:text-white">Inline Loader</h2>
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<InlineLoader size="sm" />
|
<InlineLoader size="sm" />
|
||||||
|
|||||||
@@ -74,19 +74,19 @@ export default function DocumentPage() {
|
|||||||
const totalTimeLeftSeconds = Math.round((100 - percentage) * secondsPerPercent);
|
const totalTimeLeftSeconds = Math.round((100 - percentage) * secondsPerPercent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full relative">
|
<div className="relative size-full">
|
||||||
<div
|
<div
|
||||||
className="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-4"
|
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="flex flex-col gap-2 float-left w-44 md:w-60 lg:w-80 mr-4 mb-2 relative"
|
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="rounded object-fill w-full bg-gray-200 dark:bg-gray-600 h-60">
|
<div className="h-60 w-full rounded bg-gray-200 object-fill dark:bg-gray-600">
|
||||||
<img
|
<img
|
||||||
className="rounded object-cover h-full"
|
className="h-full rounded object-cover"
|
||||||
src={`/api/v1/documents/${document.id}/cover`}
|
src={`/api/v1/documents/${document.id}/cover`}
|
||||||
alt={`${document.title} cover`}
|
alt={`${document.title} cover`}
|
||||||
/>
|
/>
|
||||||
@@ -97,14 +97,14 @@ export default function DocumentPage() {
|
|||||||
{document.filepath && (
|
{document.filepath && (
|
||||||
<a
|
<a
|
||||||
href={`/reader#id=${document.id}&type=REMOTE`}
|
href={`/reader#id=${document.id}&type=REMOTE`}
|
||||||
className="z-10 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm text-center py-1 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none w-full mt-2"
|
className="z-10 mt-2 w-full rounded bg-blue-700 py-1 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
Read
|
Read
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex flex-wrap-reverse justify-between gap-2 z-20 relative my-2">
|
<div className="relative z-20 my-2 flex flex-wrap-reverse justify-between gap-2">
|
||||||
<div className="min-w-[50%] md:mr-2">
|
<div className="min-w-[50%] md:mr-2">
|
||||||
<div className="flex gap-1 text-sm">
|
<div className="flex gap-1 text-sm">
|
||||||
<p className="text-gray-500">ISBN-10:</p>
|
<p className="text-gray-500">ISBN-10:</p>
|
||||||
@@ -123,7 +123,7 @@ export default function DocumentPage() {
|
|||||||
className="z-10 text-gray-500 dark:text-gray-400"
|
className="z-10 text-gray-500 dark:text-gray-400"
|
||||||
title="Download"
|
title="Download"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-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>
|
||||||
@@ -132,34 +132,34 @@ export default function DocumentPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Document Details Grid */}
|
{/* Document Details Grid */}
|
||||||
<div className="grid sm:grid-cols-2 justify-between gap-4 pb-4">
|
<div className="grid justify-between gap-4 pb-4 sm:grid-cols-2">
|
||||||
{/* Title - Editable */}
|
{/* Title - Editable */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="text-gray-500 inline-flex gap-2 relative">
|
<div className="relative inline-flex gap-2 text-gray-500">
|
||||||
<p>Title</p>
|
<p>Title</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative font-medium text-justify hyphens-auto">
|
<div className="relative hyphens-auto text-justify font-medium">
|
||||||
<p>{document.title}</p>
|
<p>{document.title}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Author - Editable */}
|
{/* Author - Editable */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="text-gray-500 inline-flex gap-2 relative">
|
<div className="relative inline-flex gap-2 text-gray-500">
|
||||||
<p>Author</p>
|
<p>Author</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative font-medium text-justify hyphens-auto">
|
<div className="relative hyphens-auto text-justify font-medium">
|
||||||
<p>{document.author}</p>
|
<p>{document.author}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time Read */}
|
{/* Time Read */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="text-gray-500 inline-flex gap-2 relative">
|
<div className="relative inline-flex gap-2 text-gray-500">
|
||||||
<p>Time Read</p>
|
<p>Time Read</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<p className="font-medium text-lg">
|
<p className="text-lg font-medium">
|
||||||
{document.total_time_seconds ? niceSeconds(document.total_time_seconds) : 'N/A'}
|
{document.total_time_seconds ? niceSeconds(document.total_time_seconds) : 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +168,7 @@ export default function DocumentPage() {
|
|||||||
{/* Progress */}
|
{/* Progress */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Progress</p>
|
<p className="text-gray-500">Progress</p>
|
||||||
<p className="font-medium text-lg">
|
<p className="text-lg font-medium">
|
||||||
{percentage ? `${Math.round(percentage)}%` : '0%'}
|
{percentage ? `${Math.round(percentage)}%` : '0%'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,16 +176,16 @@ export default function DocumentPage() {
|
|||||||
|
|
||||||
{/* Description - Editable */}
|
{/* Description - Editable */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="text-gray-500 inline-flex gap-2 relative">
|
<div className="relative inline-flex gap-2 text-gray-500">
|
||||||
<p>Description</p>
|
<p>Description</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative font-medium text-justify hyphens-auto">
|
<div className="relative hyphens-auto text-justify font-medium">
|
||||||
<p>{document.description || 'N/A'}</p>
|
<p>{document.description || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reading Statistics */}
|
{/* Reading Statistics */}
|
||||||
<div className="mt-4 grid sm:grid-cols-3 gap-4">
|
<div className="mt-4 grid gap-4 sm:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Words</p>
|
<p className="text-gray-500">Words</p>
|
||||||
<p className="font-medium">{document.words || 'N/A'}</p>
|
<p className="font-medium">{document.words || 'N/A'}</p>
|
||||||
@@ -206,14 +206,14 @@ export default function DocumentPage() {
|
|||||||
|
|
||||||
{/* Additional Reading Stats - Matching Legacy Template */}
|
{/* Additional Reading Stats - Matching Legacy Template */}
|
||||||
{progress && (
|
{progress && (
|
||||||
<div className="mt-4 grid sm:grid-cols-2 gap-4">
|
<div className="mt-4 grid gap-4 sm:grid-cols-2">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-gray-500">Words / Minute:</p>
|
<p className="text-gray-500">Words / Minute:</p>
|
||||||
<p className="font-medium">{document.wpm || 'N/A'}</p>
|
<p className="font-medium">{document.wpm || 'N/A'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<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="font-medium whitespace-nowrap">
|
<p className="whitespace-nowrap font-medium">
|
||||||
{niceSeconds(totalTimeLeftSeconds)}
|
{niceSeconds(totalTimeLeftSeconds)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full relative">
|
<div className="relative w-full">
|
||||||
<div
|
<div
|
||||||
className="flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded"
|
className="flex size-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700"
|
||||||
>
|
>
|
||||||
<div className="min-w-fit my-auto h-48 relative">
|
<div className="relative my-auto h-48 min-w-fit">
|
||||||
<Link to={`/documents/${doc.id}`}>
|
<Link to={`/documents/${doc.id}`}>
|
||||||
<img
|
<img
|
||||||
className="rounded object-cover h-full"
|
className="h-full rounded object-cover"
|
||||||
src={`/api/v1/documents/${doc.id}/cover`}
|
src={`/api/v1/documents/${doc.id}/cover`}
|
||||||
alt={doc.title}
|
alt={doc.title}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-around dark:text-white w-full text-sm">
|
<div className="flex w-full flex-col justify-around text-sm dark:text-white">
|
||||||
<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>
|
||||||
@@ -74,7 +74,7 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="absolute flex flex-col gap-2 right-4 bottom-4 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} />
|
||||||
@@ -153,13 +153,13 @@ export default function DocumentsPage() {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Search Form */}
|
{/* Search Form */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 gap-4 flex-col lg:flex-row" onSubmit={handleSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleSubmit}>
|
||||||
<div className="flex flex-col w-full grow">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="flex relative">
|
<div className="relative flex">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-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>
|
||||||
@@ -167,7 +167,7 @@ export default function DocumentsPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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"
|
||||||
/>
|
/>
|
||||||
@@ -187,11 +187,11 @@ export default function DocumentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">
|
<div className="mt-4 flex w-full justify-center gap-4 text-black dark:text-white">
|
||||||
{previousPage && previousPage > 0 && (
|
{previousPage && previousPage > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page - 1)}
|
onClick={() => setPage(page - 1)}
|
||||||
className="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
className="w-24 rounded bg-white p-2 text-center text-sm font-medium shadow-lg hover:bg-gray-400 focus:outline-none dark:bg-gray-600 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
◄
|
◄
|
||||||
</button>
|
</button>
|
||||||
@@ -199,7 +199,7 @@ export default function DocumentsPage() {
|
|||||||
{nextPage && nextPage > 0 && (
|
{nextPage && nextPage > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page + 1)}
|
onClick={() => setPage(page + 1)}
|
||||||
className="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
className="w-24 rounded bg-white p-2 text-center text-sm font-medium shadow-lg hover:bg-gray-400 focus:outline-none dark:bg-gray-600 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
►
|
►
|
||||||
</button>
|
</button>
|
||||||
@@ -208,7 +208,7 @@ export default function DocumentsPage() {
|
|||||||
|
|
||||||
{/* Upload Button */}
|
{/* Upload Button */}
|
||||||
<div
|
<div
|
||||||
className="fixed bottom-6 right-6 rounded-full flex items-center justify-center"
|
className="fixed bottom-6 right-6 flex items-center justify-center rounded-full"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -218,7 +218,7 @@ export default function DocumentsPage() {
|
|||||||
onChange={() => setUploadMode(!uploadMode)}
|
onChange={() => setUploadMode(!uploadMode)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute right-0 z-10 bottom-0 rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2 transition-opacity duration-200 ${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"
|
method="POST"
|
||||||
@@ -234,7 +234,7 @@ export default function DocumentsPage() {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="font-medium px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 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();
|
||||||
@@ -246,7 +246,7 @@ export default function DocumentsPage() {
|
|||||||
</form>
|
</form>
|
||||||
<label htmlFor="upload-file-button">
|
<label htmlFor="upload-file-button">
|
||||||
<div
|
<div
|
||||||
className="w-full text-center cursor-pointer font-medium mt-2 px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="mt-2 w-full cursor-pointer bg-gray-500 px-2 py-1 text-center font-medium text-gray-800 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-800"
|
||||||
onClick={handleCancelUpload}
|
onClick={handleCancelUpload}
|
||||||
>
|
>
|
||||||
Cancel Upload
|
Cancel Upload
|
||||||
@@ -254,7 +254,7 @@ export default function DocumentsPage() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label
|
<label
|
||||||
className="w-16 h-16 bg-gray-800 dark:bg-gray-200 rounded-full flex items-center justify-center opacity-30 hover:opacity-100 transition-all duration-200 cursor-pointer"
|
className="flex size-16 cursor-pointer items-center justify-center rounded-full bg-gray-800 opacity-30 transition-all duration-200 hover:opacity-100 dark:bg-gray-200"
|
||||||
htmlFor="upload-file-button"
|
htmlFor="upload-file-button"
|
||||||
>
|
>
|
||||||
<Upload size={34} />
|
<Upload size={34} />
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ function InfoCard({ title, size, link }: InfoCardProps) {
|
|||||||
if (link) {
|
if (link) {
|
||||||
return (
|
return (
|
||||||
<Link to={link} className="w-full">
|
<Link to={link} className="w-full">
|
||||||
<div className="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
<div className="flex w-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
|
||||||
<div className="flex flex-col justify-around dark:text-white w-full text-sm">
|
<div className="flex w-full flex-col justify-around text-sm dark:text-white">
|
||||||
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
|
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
|
||||||
<p className="text-sm text-gray-400">{title}</p>
|
<p className="text-sm text-gray-400">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,8 +24,8 @@ function InfoCard({ title, size, link }: InfoCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
<div className="flex w-full gap-4 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
|
||||||
<div className="flex flex-col justify-around dark:text-white w-full text-sm">
|
<div className="flex w-full flex-col justify-around text-sm dark:text-white">
|
||||||
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
|
<p className="text-2xl font-bold text-black dark:text-white">{size}</p>
|
||||||
<p className="text-sm text-gray-400">{title}</p>
|
<p className="text-sm text-gray-400">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,15 +47,15 @@ interface StreakCardProps {
|
|||||||
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 px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
<div className="relative w-full rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700">
|
||||||
<p className="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
<p className="w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white">
|
||||||
{window === 'WEEK' ? 'Weekly Read Streak' : 'Daily Read Streak'}
|
{window === 'WEEK' ? 'Weekly Read Streak' : 'Daily Read Streak'}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-end my-6 space-x-2">
|
<div className="my-6 flex items-end space-x-2">
|
||||||
<p className="text-5xl font-bold text-black dark:text-white">{currentStreak}</p>
|
<p className="text-5xl font-bold text-black dark:text-white">{currentStreak}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:text-white">
|
<div className="dark:text-white">
|
||||||
<div className="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200">
|
<div className="mb-2 flex items-center justify-between border-b border-gray-200 pb-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p>{window === 'WEEK' ? 'Current Weekly Streak' : 'Current Daily Streak'}</p>
|
<p>{window === 'WEEK' ? 'Current Weekly Streak' : 'Current Daily Streak'}</p>
|
||||||
<div className="flex items-end text-sm text-gray-400">
|
<div className="flex items-end text-sm text-gray-400">
|
||||||
@@ -64,7 +64,7 @@ function StreakCard({ window, currentStreak, currentStreakStartDate, currentStre
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-end font-bold">{currentStreak}</div>
|
<div className="flex items-end font-bold">{currentStreak}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between pb-2 mb-2 text-sm">
|
<div className="mb-2 flex items-center justify-between pb-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p>{window === 'WEEK' ? 'Best Weekly Streak' : 'Best Daily Streak'}</p>
|
<p>{window === 'WEEK' ? 'Best Weekly Streak' : 'Best Daily Streak'}</p>
|
||||||
<div className="flex items-end text-sm text-gray-400">
|
<div className="flex items-end text-sm text-gray-400">
|
||||||
@@ -87,13 +87,13 @@ interface LeaderboardCardProps {
|
|||||||
function LeaderboardCard({ name, data }: LeaderboardCardProps) {
|
function LeaderboardCard({ name, data }: LeaderboardCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex flex-col justify-between h-full w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
<div className="flex size-full flex-col justify-between rounded bg-white px-4 py-6 shadow-lg dark:bg-gray-700">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<p className="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
<p className="w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white">
|
||||||
{name} Leaderboard
|
{name} Leaderboard
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 text-xs text-gray-400 items-center">
|
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||||||
<span className="cursor-pointer hover:text-black dark:hover:text-white">all</span>
|
<span className="cursor-pointer hover:text-black dark:hover:text-white">all</span>
|
||||||
<span className="cursor-pointer hover:text-black dark:hover:text-white">year</span>
|
<span className="cursor-pointer hover:text-black dark:hover:text-white">year</span>
|
||||||
<span className="cursor-pointer hover:text-black dark:hover:text-white">month</span>
|
<span className="cursor-pointer hover:text-black dark:hover:text-white">month</span>
|
||||||
@@ -103,7 +103,7 @@ function LeaderboardCard({ name, data }: LeaderboardCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* All time data */}
|
{/* All time data */}
|
||||||
<div className="flex items-end my-6 space-x-2">
|
<div className="my-6 flex items-end space-x-2">
|
||||||
{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>
|
||||||
) : (
|
) : (
|
||||||
@@ -115,7 +115,7 @@ function LeaderboardCard({ name, data }: LeaderboardCardProps) {
|
|||||||
{data.all.slice(0, 3).map((item: any, index: number) => (
|
{data.all.slice(0, 3).map((item: any, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`flex items-center justify-between pt-2 pb-2 text-sm ${index > 0 ? 'border-t border-gray-200' : ''}`}
|
className={`flex items-center justify-between py-2 text-sm ${index > 0 ? 'border-t border-gray-200' : ''}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>{item.user_id}</p>
|
<p>{item.user_id}</p>
|
||||||
@@ -132,7 +132,7 @@ function LeaderboardCard({ name, data }: LeaderboardCardProps) {
|
|||||||
function GraphVisualization({ data }: { data: GraphDataPoint[] }) {
|
function GraphVisualization({ data }: { data: GraphDataPoint[] }) {
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-24 flex items-center justify-center bg-gray-100 dark:bg-gray-600">
|
<div className="relative flex h-24 items-center justify-center bg-gray-100 dark:bg-gray-600">
|
||||||
<p className="text-gray-400 dark:text-gray-300">No data available</p>
|
<p className="text-gray-400 dark:text-gray-300">No data available</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -142,14 +142,14 @@ function GraphVisualization({ data }: { data: GraphDataPoint[] }) {
|
|||||||
const maxMinutes = Math.max(...data.map(d => d.minutes_read), 1);
|
const maxMinutes = Math.max(...data.map(d => d.minutes_read), 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-24 flex items-end justify-between p-2 bg-gray-100 dark:bg-gray-600">
|
<div className="relative flex h-24 items-end justify-between bg-gray-100 p-2 dark:bg-gray-600">
|
||||||
{data.map((point, i) => (
|
{data.map((point, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex-1 mx-0.5 bg-blue-500 hover:bg-blue-600 transition-colors relative group"
|
className="group relative mx-0.5 flex-1 bg-blue-500 transition-colors hover:bg-blue-600"
|
||||||
style={{ height: `${(point.minutes_read / maxMinutes) * 100}%` }}
|
style={{ height: `${(point.minutes_read / maxMinutes) * 100}%` }}
|
||||||
>
|
>
|
||||||
<div className="absolute bottom-full mb-1 left-0 w-full text-xs text-center text-gray-600 dark:text-gray-300 opacity-0 group-hover:opacity-100 pointer-events-none">
|
<div className="pointer-events-none absolute bottom-full left-0 mb-1 w-full text-center text-xs text-gray-600 opacity-0 group-hover:opacity-100 dark:text-gray-300">
|
||||||
{point.minutes_read} min
|
{point.minutes_read} min
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,8 +176,8 @@ export default function HomePage() {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Daily Read Totals Graph */}
|
{/* Daily Read Totals Graph */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="relative w-full bg-white shadow-lg dark:bg-gray-700 rounded">
|
<div className="relative w-full rounded bg-white shadow-lg dark:bg-gray-700">
|
||||||
<p className="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
<p className="absolute left-5 top-3 w-max border-b border-gray-200 text-sm font-semibold text-gray-700 dark:border-gray-500 dark:text-white">
|
||||||
Daily Read Totals
|
Daily Read Totals
|
||||||
</p>
|
</p>
|
||||||
<GraphVisualization data={graphData || []} />
|
<GraphVisualization data={graphData || []} />
|
||||||
@@ -244,13 +244,13 @@ export default function HomePage() {
|
|||||||
{docs?.slice(0, 6).map((doc: any) => (
|
{docs?.slice(0, 6).map((doc: any) => (
|
||||||
<div
|
<div
|
||||||
key={doc.id}
|
key={doc.id}
|
||||||
className="flex flex-col gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
className="flex flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white"
|
||||||
>
|
>
|
||||||
<h3 className="font-medium text-lg">{doc.title}</h3>
|
<h3 className="text-lg font-medium">{doc.title}</h3>
|
||||||
<p className="text-sm">{doc.author}</p>
|
<p className="text-sm">{doc.author}</p>
|
||||||
<Link
|
<Link
|
||||||
to={`/documents/${doc.id}`}
|
to={`/documents/${doc.id}`}
|
||||||
className="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded text-sm text-center py-1 dark:bg-blue-600 dark:hover:bg-blue-700"
|
className="rounded bg-blue-700 py-1 text-center text-sm font-medium text-white hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
View Document
|
View Document
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -34,34 +34,34 @@ export default function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 dark:text-white min-h-screen">
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-800 dark:text-white">
|
||||||
<div className="flex flex-wrap w-full">
|
<div className="flex w-full flex-wrap">
|
||||||
<div className="flex flex-col w-full md:w-1/2">
|
<div className="flex w-full flex-col md:w-1/2">
|
||||||
<div
|
<div
|
||||||
className="flex flex-col justify-center px-8 pt-8 my-auto md:justify-start md:pt-0 md:px-24 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-3xl text-center">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">
|
||||||
<div className="flex relative">
|
<div className="relative flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col pt-4 mb-12">
|
<div className="mb-12 flex flex-col pt-4">
|
||||||
<div className="flex relative">
|
<div className="relative flex">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -72,12 +72,12 @@ export default function LoginPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full px-4 py-2 text-base font-semibold text-center transition duration-200 ease-in focus:outline-none focus:ring-2 disabled:opacity-50"
|
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-none focus:ring-2 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isLoading ? 'Logging in...' : 'Login'}
|
{isLoading ? 'Logging in...' : 'Login'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
<div className="pt-12 pb-12 text-center">
|
<div className="py-12 text-center">
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
<a href="/local" className="font-semibold underline">
|
<a href="/local" className="font-semibold underline">
|
||||||
Offline / Local Mode
|
Offline / Local Mode
|
||||||
@@ -86,8 +86,8 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden image-fader w-1/2 shadow-2xl h-screen relative md:block">
|
<div className="image-fader relative hidden h-screen w-1/2 shadow-2xl md:block">
|
||||||
<div className="w-full h-screen object-cover ease-in-out top-0 left-0 bg-gray-300 flex items-center justify-center">
|
<div className="left-0 top-0 flex h-screen w-full items-center justify-center bg-gray-300 object-cover ease-in-out">
|
||||||
<span className="text-gray-500">AnthoLume</span>
|
<span className="text-gray-500">AnthoLume</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function ProgressPage() {
|
|||||||
render: (_: any, row: any) => (
|
render: (_: any, row: any) => (
|
||||||
<Link
|
<Link
|
||||||
to={`/documents/${row.document_id}`}
|
to={`/documents/${row.document_id}`}
|
||||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||||
>
|
>
|
||||||
{row.author || 'Unknown'} - {row.title || 'Unknown'}
|
{row.author || 'Unknown'} - {row.title || 'Unknown'}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ export default function SearchPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col md:flex-row gap-4">
|
<div className="flex w-full flex-col gap-4 md:flex-row">
|
||||||
<div className="flex flex-col gap-4 grow">
|
<div className="flex grow flex-col gap-4">
|
||||||
{/* Search Form */}
|
{/* Search Form */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 gap-4 flex-col lg:flex-row" onSubmit={handleSubmit}>
|
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleSubmit}>
|
||||||
<div className="flex flex-col w-full grow">
|
<div className="flex w-full grow flex-col">
|
||||||
<div className="flex relative">
|
<div className="relative flex">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-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>
|
||||||
@@ -35,21 +35,21 @@ export default function SearchPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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="flex relative min-w-[12em]">
|
<div className="relative flex min-w-[12em]">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-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="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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>
|
||||||
<option value="Annas Archive">Annas Archive</option>
|
<option value="Annas Archive">Annas Archive</option>
|
||||||
@@ -64,35 +64,35 @@ export default function SearchPage() {
|
|||||||
{/* 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 leading-normal bg-white dark:bg-gray-700 text-sm md:text-sm"
|
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 p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
className="w-12 border-b border-gray-200 p-3 text-left font-normal uppercase dark:border-gray-800"
|
||||||
></th>
|
></th>
|
||||||
<th
|
<th
|
||||||
className="p-3 font-normal text-left uppercase border-b border-gray-200 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="p-3 font-normal text-left uppercase border-b border-gray-200 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="p-3 font-normal text-left uppercase border-b border-gray-200 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="p-3 font-normal text-left uppercase border-b border-gray-200 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="p-3 hidden md:block font-normal text-left uppercase border-b border-gray-200 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>
|
||||||
@@ -101,18 +101,18 @@ export default function SearchPage() {
|
|||||||
<tbody className="text-black dark:text-white">
|
<tbody className="text-black dark:text-white">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-center p-3" colSpan={6}>Loading...</td>
|
<td className="p-3 text-center" colSpan={6}>Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!isLoading && !results && (
|
{!isLoading && !results && (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="text-center p-3" 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 && results && results.map((item: any) => (
|
||||||
<tr key={item.id}>
|
<tr key={item.id}>
|
||||||
<td
|
<td
|
||||||
className="p-3 border-b border-gray-200 text-gray-500 dark:text-gray-500"
|
className="border-b border-gray-200 p-3 text-gray-500 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="hover:text-purple-600"
|
className="hover:text-purple-600"
|
||||||
@@ -121,19 +121,19 @@ export default function SearchPage() {
|
|||||||
<Download size={15} />
|
<Download size={15} />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
{item.author || 'N/A'} - {item.title || 'N/A'}
|
{item.author || 'N/A'} - {item.title || 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{item.series || 'N/A'}</p>
|
<p>{item.series || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{item.file_type || 'N/A'}</p>
|
<p>{item.file_type || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="border-b border-gray-200 p-3">
|
||||||
<p>{item.file_size || 'N/A'}</p>
|
<p>{item.file_size || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="hidden md:table-cell p-3 border-b border-gray-200">
|
<td className="hidden border-b border-gray-200 p-3 md:table-cell">
|
||||||
<p>{item.upload_date || 'N/A'}</p>
|
<p>{item.upload_date || 'N/A'}</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -60,37 +60,37 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col md:flex-row gap-4">
|
<div className="flex w-full flex-col gap-4 md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700">
|
<div className="flex flex-col items-center rounded bg-white p-4 shadow-lg md:w-60 lg:w-80 dark:bg-gray-700">
|
||||||
<div className="w-16 h-16 bg-gray-200 dark:bg-gray-600 rounded-full mb-4" />
|
<div className="mb-4 size-16 rounded-full bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="w-32 h-6 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-6 w-32 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 grow">
|
<div className="flex grow flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700">
|
<div className="flex flex-col gap-2 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
|
||||||
<div className="w-48 h-6 bg-gray-200 dark:bg-gray-600 rounded mb-4" />
|
<div className="mb-4 h-6 w-48 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-1 h-12 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-12 flex-1 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="flex-1 h-12 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-12 flex-1 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="w-40 h-10 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-10 w-40 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700">
|
<div className="flex flex-col gap-2 rounded bg-white p-4 shadow-lg dark:bg-gray-700">
|
||||||
<div className="w-48 h-6 bg-gray-200 dark:bg-gray-600 rounded mb-4" />
|
<div className="mb-4 h-6 w-48 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-1 h-12 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-12 flex-1 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="w-40 h-10 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-10 w-40 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col p-4 rounded shadow-lg bg-white dark:bg-gray-700">
|
<div className="flex flex-col rounded bg-white p-4 shadow-lg dark:bg-gray-700">
|
||||||
<div className="w-24 h-6 bg-gray-200 dark:bg-gray-600 rounded mb-4" />
|
<div className="mb-4 h-6 w-24 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="flex gap-4 mb-4">
|
<div className="mb-4 flex gap-4">
|
||||||
<div className="flex-1 h-6 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-6 flex-1 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="flex-1 h-6 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-6 flex-1 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
<div className="flex-1 h-6 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-6 flex-1 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 h-32 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div className="h-32 flex-1 rounded bg-gray-200 dark:bg-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,31 +98,31 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col md:flex-row gap-4">
|
<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 p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 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 flex-col gap-4 grow">
|
<div className="flex grow flex-col gap-4">
|
||||||
{/* Change Password Form */}
|
{/* Change Password Form */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 mb-2">Change Password</p>
|
<p className="mb-2 text-lg font-semibold">Change Password</p>
|
||||||
<form
|
<form
|
||||||
className="flex gap-4 flex-col lg:flex-row"
|
className="flex flex-col gap-4 lg:flex-row"
|
||||||
onSubmit={handlePasswordSubmit}
|
onSubmit={handlePasswordSubmit}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col grow">
|
<div className="flex grow flex-col">
|
||||||
<div className="flex relative">
|
<div className="relative flex">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-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>
|
||||||
@@ -130,15 +130,15 @@ export default function SettingsPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col grow">
|
<div className="flex grow flex-col">
|
||||||
<div className="flex relative">
|
<div className="relative flex">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-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>
|
||||||
@@ -146,7 +146,7 @@ export default function SettingsPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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>
|
||||||
@@ -159,23 +159,23 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Change Timezone Form */}
|
{/* Change Timezone Form */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 mb-2">Change Timezone</p>
|
<p className="mb-2 text-lg font-semibold">Change Timezone</p>
|
||||||
<form
|
<form
|
||||||
className="flex gap-4 flex-col lg:flex-row"
|
className="flex flex-col gap-4 lg:flex-row"
|
||||||
onSubmit={handleTimezoneSubmit}
|
onSubmit={handleTimezoneSubmit}
|
||||||
>
|
>
|
||||||
<div className="flex relative grow">
|
<div className="relative flex grow">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-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="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
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>
|
||||||
<option value="America/New_York">America/New_York</option>
|
<option value="America/New_York">America/New_York</option>
|
||||||
@@ -197,24 +197,24 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Devices Table */}
|
{/* Devices Table */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 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 dark:bg-gray-700 text-sm">
|
<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="p-3 pl-0 font-normal text-left uppercase border-b border-gray-200 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="p-3 font-normal text-left uppercase border-b border-gray-200 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="p-3 font-normal text-left uppercase border-b border-gray-200 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>
|
||||||
@@ -223,7 +223,7 @@ 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="text-center p-3" 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) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user