This commit is contained in:
2026-03-16 08:03:03 -04:00
parent 4306d86080
commit 3e9a193d08
19 changed files with 467 additions and 304 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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
View 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
View File
@@ -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",
+1
View File
@@ -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"
+36 -39
View File
@@ -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);
setAuthState({
isAuthenticated: true,
user: null,
token,
});
navigate('/');
},
onError: () => {
throw new Error('Login failed');
},
}); });
// The backend uses session-based authentication, so no token to store
// The session cookie is automatically set by the browser
setAuthState({
isAuthenticated: true,
user: response.data,
isCheckingAuth: false,
});
navigate('/');
} 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 -1
View File
@@ -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
View 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';
+92 -39
View File
@@ -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"
> >
<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"> <User size={20} />
<div className="py-1"> </button>
<Link
to="/settings" {isUserDropdownOpen && (
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" <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="py-1"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
> >
Settings <Link
</Link> to="/settings"
<button onClick={() => setIsUserDropdownOpen(false)}
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"
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">
</button> <span>Settings</span>
</span>
</Link>
<button
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"
role="menuitem"
>
<span className="flex flex-col">
<span>Logout</span>
</span>
</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
<span>{userData?.username || 'User'}</span> onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
</div> className="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
</label> >
<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)' }}>
<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>
+55
View File
@@ -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);
}
}
+11 -59
View File
@@ -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>
+16 -5
View File
@@ -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">
+6 -42
View File
@@ -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">
+8 -46
View File
@@ -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 -1
View File
@@ -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: {},
}, },
+4
View File
@@ -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: {