wip 4
This commit is contained in:
File diff suppressed because one or more lines are too long
145
frontend/dist/assets/index-CAfunjs7.js
vendored
Normal file
145
frontend/dist/assets/index-CAfunjs7.js
vendored
Normal file
File diff suppressed because one or more lines are too long
65
frontend/dist/assets/index-DiNL9yHX.js
vendored
65
frontend/dist/assets/index-DiNL9yHX.js
vendored
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@@ -23,8 +23,8 @@
|
|||||||
/>
|
/>
|
||||||
<title>AnthoLume</title>
|
<title>AnthoLume</title>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<script type="module" crossorigin src="/assets/index-DiNL9yHX.js"></script>
|
<script type="module" crossorigin src="/assets/index-CAfunjs7.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-C8sHRJp6.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BfBW0EJh.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
17
frontend/dist/manifest.json
vendored
Normal file
17
frontend/dist/manifest.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "AnthoLume",
|
||||||
|
"short_name": "AnthoLume",
|
||||||
|
"lang": "en-US",
|
||||||
|
"theme_color": "#1F2937",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/",
|
||||||
|
"start_url": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"purpose": "any",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"src": "/assets/icons/icon512.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.62.16",
|
"@tanstack/react-query": "^5.62.16",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
@@ -4436,6 +4437,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.577.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
|
||||||
|
"integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lunr": {
|
"node_modules/lunr": {
|
||||||
"version": "2.3.9",
|
"version": "2.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.62.16",
|
"@tanstack/react-query": "^5.62.16",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useLogin, useLogout, useGetMe } from '../generated/anthoLumeAPIV1';
|
|||||||
interface AuthState {
|
interface AuthState {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
user: { username: string; is_admin: boolean } | null;
|
user: { username: string; is_admin: boolean } | null;
|
||||||
token: string | null;
|
isCheckingAuth: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType extends AuthState {
|
interface AuthContextType extends AuthState {
|
||||||
@@ -15,76 +15,73 @@ interface AuthContextType extends AuthState {
|
|||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
const TOKEN_KEY = 'antholume_token';
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [authState, setAuthState] = useState<AuthState>({
|
const [authState, setAuthState] = useState<AuthState>({
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
user: null,
|
user: null,
|
||||||
token: null,
|
isCheckingAuth: true, // Start with checking state to prevent redirects during initial load
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginMutation = useLogin();
|
const loginMutation = useLogin();
|
||||||
const logoutMutation = useLogout();
|
const logoutMutation = useLogout();
|
||||||
const { data: meData } = useGetMe(authState.isAuthenticated ? {} : undefined);
|
|
||||||
|
// Always call /me to check authentication status
|
||||||
|
const { data: meData, error: meError, isLoading: meLoading } = useGetMe();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Check for existing token on mount
|
// Update auth state based on /me endpoint response
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem(TOKEN_KEY);
|
if (meLoading) {
|
||||||
if (token) {
|
// Still checking authentication
|
||||||
setAuthState((prev) => ({ ...prev, token, isAuthenticated: true }));
|
setAuthState((prev) => ({ ...prev, isCheckingAuth: true }));
|
||||||
}
|
} else if (meData?.data) {
|
||||||
}, []);
|
// User is authenticated
|
||||||
|
setAuthState({
|
||||||
// Fetch user data when authenticated
|
isAuthenticated: true,
|
||||||
useEffect(() => {
|
|
||||||
if (meData?.data && authState.isAuthenticated) {
|
|
||||||
setAuthState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
user: meData.data,
|
user: meData.data,
|
||||||
}));
|
isCheckingAuth: false,
|
||||||
|
});
|
||||||
|
} else if (meError) {
|
||||||
|
// User is not authenticated or error occurred
|
||||||
|
setAuthState({
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
isCheckingAuth: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [meData, authState.isAuthenticated]);
|
}, [meData, meError, meLoading]);
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
loginMutation.mutate({
|
const response = await loginMutation.mutateAsync({
|
||||||
data: {
|
data: {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
},
|
},
|
||||||
}, {
|
});
|
||||||
onSuccess: () => {
|
|
||||||
const token = localStorage.getItem(TOKEN_KEY) || 'authenticated';
|
|
||||||
localStorage.setItem(TOKEN_KEY, token);
|
|
||||||
|
|
||||||
|
// The backend uses session-based authentication, so no token to store
|
||||||
|
// The session cookie is automatically set by the browser
|
||||||
setAuthState({
|
setAuthState({
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
user: null,
|
user: response.data,
|
||||||
token,
|
isCheckingAuth: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
throw new Error('Login failed');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw new Error('Login failed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
logoutMutation.mutate(undefined, {
|
logoutMutation.mutate(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
|
||||||
setAuthState({
|
setAuthState({
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
user: null,
|
user: null,
|
||||||
token: null,
|
isCheckingAuth: false,
|
||||||
});
|
});
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ interface ProtectedRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||||
const { isAuthenticated } = useAuth();
|
const { isAuthenticated, isCheckingAuth } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Show loading while checking authentication status
|
||||||
|
if (isCheckingAuth) {
|
||||||
|
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
// Redirect to login with the current location saved
|
// Redirect to login with the current location saved
|
||||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
|||||||
52
frontend/src/components/Button.tsx
Normal file
52
frontend/src/components/Button.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { ButtonHTMLAttributes, AnchorHTMLAttributes, forwardRef } from 'react';
|
||||||
|
|
||||||
|
interface BaseButtonProps {
|
||||||
|
variant?: 'default' | 'secondary';
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
type LinkProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };
|
||||||
|
|
||||||
|
const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string => {
|
||||||
|
const baseClass = 'transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white';
|
||||||
|
|
||||||
|
if (variant === 'secondary') {
|
||||||
|
return `${baseClass} bg-black shadow-md hover:text-black hover:bg-white`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClass} bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export const ButtonLink = forwardRef<HTMLAnchorElement, LinkProps>(
|
||||||
|
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
ref={ref}
|
||||||
|
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ButtonLink.displayName = 'ButtonLink';
|
||||||
@@ -1,36 +1,65 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Link, useLocation, Outlet, Navigate } from 'react-router-dom';
|
import { Link, useLocation, Outlet, Navigate } from 'react-router-dom';
|
||||||
import { useGetMe } from '../generated/anthoLumeAPIV1';
|
import { useGetMe } from '../generated/anthoLumeAPIV1';
|
||||||
import { useAuth } from '../auth/AuthContext';
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
import { Home, FileText, Activity, Search, Settings, User, ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
path: string;
|
path: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: React.ElementType;
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ path: '/', label: 'Home', icon: 'home' },
|
{ path: '/', label: 'Home', icon: Home, title: 'Home' },
|
||||||
{ path: '/documents', label: 'Documents', icon: 'documents' },
|
{ path: '/documents', label: 'Documents', icon: FileText, title: 'Documents' },
|
||||||
{ path: '/progress', label: 'Progress', icon: 'activity' },
|
{ path: '/progress', label: 'Progress', icon: Activity, title: 'Progress' },
|
||||||
{ path: '/activity', label: 'Activity', icon: 'activity' },
|
{ path: '/activity', label: 'Activity', icon: Activity, title: 'Activity' },
|
||||||
{ path: '/search', label: 'Search', icon: 'search' },
|
{ path: '/search', label: 'Search', icon: Search, title: 'Search' },
|
||||||
|
{ path: '/settings', label: 'Settings', icon: Settings, title: 'Settings' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isAuthenticated, user, logout } = useAuth();
|
const { isAuthenticated, user, logout, isCheckingAuth } = useAuth();
|
||||||
const { data } = useGetMe(isAuthenticated ? {} : undefined);
|
const { data } = useGetMe(isAuthenticated ? {} : undefined);
|
||||||
const userData = data?.data || user;
|
const userData = data?.data || user;
|
||||||
|
const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
setIsUserDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsUserDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get current page title
|
||||||
|
const currentPageTitle = navItems.find(item => location.pathname === item.path)?.title || 'Documents';
|
||||||
|
|
||||||
|
// Show loading while checking authentication status
|
||||||
|
if (isCheckingAuth) {
|
||||||
|
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to login if not authenticated
|
// Redirect to login if not authenticated
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
logout();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 min-h-screen">
|
<div className="bg-gray-100 dark:bg-gray-800 min-h-screen">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -40,7 +69,7 @@ export default function Layout() {
|
|||||||
<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 lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0"
|
||||||
id="mobile-nav-toggle"
|
id="mobile-nav-button"
|
||||||
/>
|
/>
|
||||||
<span className="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
<span className="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
||||||
<span className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
<span className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||||
@@ -48,6 +77,7 @@ export default function Layout() {
|
|||||||
<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 lg:w-48 bg-white dark:bg-gray-700 shadow-lg"
|
||||||
|
style={{ top: 0, paddingTop: 'env(safe-area-inset-top)' }}
|
||||||
>
|
>
|
||||||
<div className="h-16 flex justify-end lg:justify-around">
|
<div className="h-16 flex 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="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0">
|
||||||
@@ -65,6 +95,7 @@ export default function Layout() {
|
|||||||
: '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'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<item.icon size={20} />
|
||||||
<span className="mx-4 text-sm font-normal">{item.label}</span>
|
<span className="mx-4 text-sm font-normal">{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -81,44 +112,66 @@ export default function Layout() {
|
|||||||
|
|
||||||
{/* Header Title */}
|
{/* Header Title */}
|
||||||
<h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
<h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
||||||
<Link to="/documents">Documents</Link>
|
{currentPageTitle}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* User Dropdown */}
|
{/* User Dropdown */}
|
||||||
<div className="relative flex items-center justify-end w-full p-4 space-x-4">
|
<div className="relative flex items-center justify-end w-full p-4 space-x-4" ref={dropdownRef}>
|
||||||
<input type="checkbox" id="user-dropdown-button" className="hidden" />
|
<button
|
||||||
<div
|
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||||
id="user-dropdown"
|
className="relative block text-gray-800 dark:text-gray-200"
|
||||||
className="transition duration-200 z-20 absolute right-4 top-16 pt-4"
|
|
||||||
>
|
>
|
||||||
|
<User size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isUserDropdownOpen && (
|
||||||
|
<div className="transition duration-200 z-20 absolute right-4 top-16 pt-4">
|
||||||
<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 bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5">
|
||||||
<div className="py-1">
|
<div
|
||||||
|
className="py-1"
|
||||||
|
role="menu"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-labelledby="options-menu"
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
to="/settings"
|
to="/settings"
|
||||||
|
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="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"
|
||||||
|
role="menuitem"
|
||||||
>
|
>
|
||||||
Settings
|
<span className="flex flex-col">
|
||||||
|
<span>Settings</span>
|
||||||
|
</span>
|
||||||
</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="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"
|
||||||
|
role="menuitem"
|
||||||
>
|
>
|
||||||
Logout
|
<span className="flex flex-col">
|
||||||
|
<span>Logout</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor="user-dropdown-button">
|
)}
|
||||||
<div className="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer">
|
|
||||||
|
<button
|
||||||
|
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||||
|
className="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
|
||||||
|
>
|
||||||
<span>{userData?.username || 'User'}</span>
|
<span>{userData?.username || 'User'}</span>
|
||||||
</div>
|
<span className="text-gray-800 dark:text-gray-200 transition-transform duration-200" style={{ transform: isUserDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||||
</label>
|
<ChevronDown size={20} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="relative overflow-hidden">
|
<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">
|
<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)' }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -44,3 +44,58 @@ main {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Navigation */
|
||||||
|
#mobile-nav-button span {
|
||||||
|
transform-origin: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-nav-button span:first-child {
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-nav-button span:nth-last-child(2) {
|
||||||
|
transform-origin: 0% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-nav-button:checked ~ span {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(45deg) translate(2px, -2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-nav-button:checked ~ span:nth-last-child(3) {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(0deg) scale(0.2, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-nav-button:checked ~ span:nth-last-child(2) {
|
||||||
|
transform: rotate(-45deg) translate(0, 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobile-nav-button:checked ~ #menu {
|
||||||
|
transform: translate(0, 0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
#mobile-nav-button ~ #menu {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
top: 0;
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
transform: translate(-100%, 0);
|
||||||
|
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
#menu {
|
||||||
|
transform: translate(calc(-1 * (env(safe-area-inset-left) + 100%)), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState, FormEvent, useRef } from 'react';
|
import { useState, FormEvent, useRef } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useGetDocuments, useCreateDocument } from '../generated/anthoLumeAPIV1';
|
import { useGetDocuments, useCreateDocument } from '../generated/anthoLumeAPIV1';
|
||||||
|
import { Activity, Download, Search, Upload } from 'lucide-react';
|
||||||
|
import { Button } from '../components/Button';
|
||||||
|
|
||||||
interface DocumentCardProps {
|
interface DocumentCardProps {
|
||||||
doc: {
|
doc: {
|
||||||
@@ -16,34 +18,6 @@ interface DocumentCardProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activity icon SVG
|
|
||||||
function ActivityIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-20 h-20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download icon SVG
|
|
||||||
function DownloadIcon({ disabled }: { disabled?: boolean }) {
|
|
||||||
if (disabled) {
|
|
||||||
return (
|
|
||||||
<svg className="w-20 h-20 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<polyline points="21 15 16 10 8 10" />
|
|
||||||
<line x1="12" y1="3" x2="12" y2="21" />
|
|
||||||
<line x1="21" y1="15" x2="21" y2="15" opacity="0" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<svg className="w-20 h-20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<polyline points="21 15 16 10 8 10" />
|
|
||||||
<line x1="12" y1="3" x2="12" y2="21" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DocumentCard({ doc }: DocumentCardProps) {
|
function DocumentCard({ doc }: DocumentCardProps) {
|
||||||
const percentage = doc.percentage || 0;
|
const percentage = doc.percentage || 0;
|
||||||
const totalTimeSeconds = doc.total_time_seconds || 0;
|
const totalTimeSeconds = doc.total_time_seconds || 0;
|
||||||
@@ -102,14 +76,14 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
className="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400"
|
className="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<Link to={`/activity?document=${doc.id}`}>
|
<Link to={`/activity?document=${doc.id}`}>
|
||||||
<ActivityIcon />
|
<Activity size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
{doc.filepath ? (
|
{doc.filepath ? (
|
||||||
<Link to={`/documents/${doc.id}/file`}>
|
<Link to={`/documents/${doc.id}/file`}>
|
||||||
<DownloadIcon />
|
<Download size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<DownloadIcon disabled />
|
<Download size={20} className="text-gray-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,26 +91,9 @@ function DocumentCard({ doc }: DocumentCardProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search icon SVG
|
|
||||||
function SearchIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-15 h-15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<circle cx="11" cy="11" r="8" />
|
|
||||||
<path d="M21 21l-6-6" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload icon SVG
|
|
||||||
function UploadIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-34 h-34" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
||||||
<polyline points="17 8 12 3 7 8" />
|
|
||||||
<line x1="12" y1="3" x2="12" y2="15" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DocumentsPage() {
|
export default function DocumentsPage() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -203,7 +160,7 @@ export default function DocumentsPage() {
|
|||||||
<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 px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<Search size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -216,12 +173,7 @@ export default function DocumentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<button
|
<Button variant="secondary" type="submit">Search</Button>
|
||||||
type="submit"
|
|
||||||
className="font-medium px-4 py-2 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,7 +217,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 ${uploadMode ? 'display-block' : 'display-none'}`}
|
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'}`}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
@@ -304,7 +256,7 @@ export default function DocumentsPage() {
|
|||||||
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="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"
|
||||||
htmlFor="upload-file-button"
|
htmlFor="upload-file-button"
|
||||||
>
|
>
|
||||||
<UploadIcon />
|
<Upload size={34} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useState, FormEvent } from 'react';
|
import { useState, FormEvent, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../auth/AuthContext';
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
import { Button } from '../components/Button';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
@@ -7,7 +9,15 @@ export default function LoginPage() {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { login } = useAuth();
|
const { login, isAuthenticated, isCheckingAuth } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Redirect to home if already logged in
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCheckingAuth && isAuthenticated) {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, isCheckingAuth, navigate]);
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -59,13 +69,14 @@ export default function LoginPage() {
|
|||||||
<span className="absolute -bottom-5 text-red-400 text-xs">{error}</span>
|
<span className="absolute -bottom-5 text-red-400 text-xs">{error}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full px-4 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2 disabled:opacity-50"
|
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"
|
||||||
>
|
>
|
||||||
{isLoading ? 'Logging in...' : 'Login'}
|
{isLoading ? 'Logging in...' : 'Login'}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
<div className="pt-12 pb-12 text-center">
|
<div className="pt-12 pb-12 text-center">
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
|
|||||||
@@ -1,39 +1,8 @@
|
|||||||
import { useState, FormEvent } from 'react';
|
import { useState, FormEvent } from 'react';
|
||||||
import { useGetSearch } from '../generated/anthoLumeAPIV1';
|
import { useGetSearch } from '../generated/anthoLumeAPIV1';
|
||||||
import { GetSearchSource } from '../generated/model/getSearchSource';
|
import { GetSearchSource } from '../generated/model/getSearchSource';
|
||||||
|
import { Search, Book, Download } from 'lucide-react';
|
||||||
// Search icon SVG
|
import { Button } from '../components/Button';
|
||||||
function SearchIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-15 h-15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<circle cx="11" cy="11" r="8" />
|
|
||||||
<path d="M21 21l-6-6" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Documents icon SVG
|
|
||||||
function DocumentsIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-15 h-15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
<line x1="16" y1="13" x2="8" y2="13" />
|
|
||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
|
||||||
<polyline points="10 9 9 9 8 9" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download icon SVG
|
|
||||||
function DownloadIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-15 h-15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<polyline points="21 15 16 10 8 10" />
|
|
||||||
<line x1="12" y1="3" x2="12" y2="21" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
@@ -60,7 +29,7 @@ export default function SearchPage() {
|
|||||||
<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 px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<Search size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -75,7 +44,7 @@ export default function SearchPage() {
|
|||||||
<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 px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
>
|
>
|
||||||
<DocumentsIcon />
|
<Book size={15} />
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={source}
|
value={source}
|
||||||
@@ -87,12 +56,7 @@ export default function SearchPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<button
|
<Button variant="secondary" type="submit">Search</Button>
|
||||||
type="submit"
|
|
||||||
className="font-medium px-4 py-2 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +118,7 @@ export default function SearchPage() {
|
|||||||
className="hover:text-purple-600"
|
className="hover:text-purple-600"
|
||||||
title="Download"
|
title="Download"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<Download size={15} />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 border-b border-gray-200">
|
<td className="p-3 border-b border-gray-200">
|
||||||
|
|||||||
@@ -1,35 +1,7 @@
|
|||||||
import { useState, FormEvent } from 'react';
|
import { useState, FormEvent } from 'react';
|
||||||
import { useGetSettings } from '../generated/anthoLumeAPIV1';
|
import { useGetSettings } from '../generated/anthoLumeAPIV1';
|
||||||
|
import { User, Lock, Clock } from 'lucide-react';
|
||||||
// User icon SVG
|
import { Button } from '../components/Button';
|
||||||
function UserIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-60 h-60" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<circle cx="12" cy="8" r="4" />
|
|
||||||
<path d="M12 12c-4 0-8 3-8 8h16c0-5-4-8-8-8" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password icon SVG
|
|
||||||
function PasswordIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-15 h-15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
||||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clock icon SVG
|
|
||||||
function ClockIcon() {
|
|
||||||
return (
|
|
||||||
<svg className="w-15 h-15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<polyline points="12 6 12 12 16 14" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { data, isLoading } = useGetSettings();
|
const { data, isLoading } = useGetSettings();
|
||||||
@@ -60,7 +32,7 @@ export default function SettingsPage() {
|
|||||||
<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 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"
|
||||||
>
|
>
|
||||||
<UserIcon />
|
<User size={60} />
|
||||||
<p className="text-lg">{settingsData?.user?.username}</p>
|
<p className="text-lg">{settingsData?.user?.username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +52,7 @@ export default function SettingsPage() {
|
|||||||
<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 px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
>
|
>
|
||||||
<PasswordIcon />
|
<Lock size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -96,7 +68,7 @@ export default function SettingsPage() {
|
|||||||
<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 px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
>
|
>
|
||||||
<PasswordIcon />
|
<Lock size={15} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -108,12 +80,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<button
|
<Button variant="secondary" type="submit">Submit</Button>
|
||||||
type="submit"
|
|
||||||
className="font-medium px-4 py-2 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +98,7 @@ export default function SettingsPage() {
|
|||||||
<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 px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||||
>
|
>
|
||||||
<ClockIcon />
|
<Clock size={15} />
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={timezone}
|
value={timezone}
|
||||||
@@ -151,12 +118,7 @@ export default function SettingsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:w-60">
|
<div className="lg:w-60">
|
||||||
<button
|
<Button variant="secondary" type="submit">Submit</Button>
|
||||||
type="submit"
|
|
||||||
className="font-medium px-4 py-2 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
||||||
darkMode: 'class',
|
darkMode: 'media',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export default defineConfig({
|
|||||||
target: 'http://localhost:8585',
|
target: 'http://localhost:8585',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/manifest.json': {
|
||||||
|
target: 'http://localhost:8585',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user