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>
<link rel="manifest" href="/manifest.json" />
<script type="module" crossorigin src="/assets/index-DiNL9yHX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C8sHRJp6.css">
<script type="module" crossorigin src="/assets/index-CAfunjs7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BfBW0EJh.css">
</head>
<body>
<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": {
"@tanstack/react-query": "^5.62.16",
"axios": "^1.13.6",
"lucide-react": "^0.577.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1"
@@ -4436,6 +4437,15 @@
"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": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@tanstack/react-query": "^5.62.16",
"axios": "^1.13.6",
"lucide-react": "^0.577.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1"
+31 -34
View File
@@ -5,7 +5,7 @@ import { useLogin, useLogout, useGetMe } from '../generated/anthoLumeAPIV1';
interface AuthState {
isAuthenticated: boolean;
user: { username: string; is_admin: boolean } | null;
token: string | null;
isCheckingAuth: boolean;
}
interface AuthContextType extends AuthState {
@@ -15,76 +15,73 @@ interface AuthContextType extends AuthState {
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const TOKEN_KEY = 'antholume_token';
export function AuthProvider({ children }: { children: ReactNode }) {
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
user: null,
token: null,
isCheckingAuth: true, // Start with checking state to prevent redirects during initial load
});
const loginMutation = useLogin();
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();
// Check for existing token on mount
// Update auth state based on /me endpoint response
useEffect(() => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
setAuthState((prev) => ({ ...prev, token, isAuthenticated: true }));
}
}, []);
// Fetch user data when authenticated
useEffect(() => {
if (meData?.data && authState.isAuthenticated) {
setAuthState((prev) => ({
...prev,
if (meLoading) {
// Still checking authentication
setAuthState((prev) => ({ ...prev, isCheckingAuth: true }));
} else if (meData?.data) {
// User is authenticated
setAuthState({
isAuthenticated: true,
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) => {
try {
loginMutation.mutate({
const response = await loginMutation.mutateAsync({
data: {
username,
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({
isAuthenticated: true,
user: null,
token,
user: response.data,
isCheckingAuth: false,
});
navigate('/');
},
onError: () => {
throw new Error('Login failed');
},
});
} catch (err) {
throw err;
throw new Error('Login failed');
}
};
const logout = () => {
logoutMutation.mutate(undefined, {
onSuccess: () => {
localStorage.removeItem(TOKEN_KEY);
setAuthState({
isAuthenticated: false,
user: null,
token: null,
isCheckingAuth: false,
});
navigate('/login');
},
+6 -1
View File
@@ -6,9 +6,14 @@ interface ProtectedRouteProps {
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated } = useAuth();
const { isAuthenticated, isCheckingAuth } = useAuth();
const location = useLocation();
// Show loading while checking authentication status
if (isCheckingAuth) {
return <div className="text-gray-500 dark:text-white">Loading...</div>;
}
if (!isAuthenticated) {
// Redirect to login with the current location saved
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';
+80 -27
View File
@@ -1,36 +1,65 @@
import { useState, useEffect, useRef } from 'react';
import { Link, useLocation, Outlet, Navigate } from 'react-router-dom';
import { useGetMe } from '../generated/anthoLumeAPIV1';
import { useAuth } from '../auth/AuthContext';
import { Home, FileText, Activity, Search, Settings, User, ChevronDown } from 'lucide-react';
interface NavItem {
path: string;
label: string;
icon: string;
icon: React.ElementType;
title: string;
}
const navItems: NavItem[] = [
{ path: '/', label: 'Home', icon: 'home' },
{ path: '/documents', label: 'Documents', icon: 'documents' },
{ path: '/progress', label: 'Progress', icon: 'activity' },
{ path: '/activity', label: 'Activity', icon: 'activity' },
{ path: '/search', label: 'Search', icon: 'search' },
{ path: '/', label: 'Home', icon: Home, title: 'Home' },
{ path: '/documents', label: 'Documents', icon: FileText, title: 'Documents' },
{ path: '/progress', label: 'Progress', icon: Activity, title: 'Progress' },
{ path: '/activity', label: 'Activity', icon: Activity, title: 'Activity' },
{ path: '/search', label: 'Search', icon: Search, title: 'Search' },
{ path: '/settings', label: 'Settings', icon: Settings, title: 'Settings' },
];
export default function Layout() {
const location = useLocation();
const { isAuthenticated, user, logout } = useAuth();
const { isAuthenticated, user, logout, isCheckingAuth } = useAuth();
const { data } = useGetMe(isAuthenticated ? {} : undefined);
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
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
const handleLogout = () => {
logout();
};
return (
<div className="bg-gray-100 dark:bg-gray-800 min-h-screen">
{/* Header */}
@@ -40,7 +69,7 @@ export default function Layout() {
<input
type="checkbox"
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-1 dark:bg-white"></span>
@@ -48,6 +77,7 @@ export default function Layout() {
<div
id="menu"
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">
<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'
}`}
>
<item.icon size={20} />
<span className="mx-4 text-sm font-normal">{item.label}</span>
</Link>
))}
@@ -81,44 +112,66 @@ export default function Layout() {
{/* Header Title */}
<h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44">
<Link to="/documents">Documents</Link>
{currentPageTitle}
</h1>
{/* User Dropdown */}
<div className="relative flex items-center justify-end w-full p-4 space-x-4">
<input type="checkbox" id="user-dropdown-button" className="hidden" />
<div
id="user-dropdown"
className="transition duration-200 z-20 absolute right-4 top-16 pt-4"
<div className="relative flex items-center justify-end w-full p-4 space-x-4" ref={dropdownRef}>
<button
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
className="relative block text-gray-800 dark:text-gray-200"
>
<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="py-1">
<div
className="py-1"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<Link
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"
role="menuitem"
>
Settings
<span className="flex flex-col">
<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"
>
Logout
<span className="flex flex-col">
<span>Logout</span>
</span>
</button>
</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>
</div>
</label>
<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>
{/* Main Content */}
<main className="relative overflow-hidden">
<div id="container" className="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48">
<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)' }}>
<Outlet />
</div>
</main>
+55
View File
@@ -44,3 +44,58 @@ main {
visibility: hidden;
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 { Link } from 'react-router-dom';
import { useGetDocuments, useCreateDocument } from '../generated/anthoLumeAPIV1';
import { Activity, Download, Search, Upload } from 'lucide-react';
import { Button } from '../components/Button';
interface DocumentCardProps {
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) {
const percentage = doc.percentage || 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"
>
<Link to={`/activity?document=${doc.id}`}>
<ActivityIcon />
<Activity size={20} />
</Link>
{doc.filepath ? (
<Link to={`/documents/${doc.id}/file`}>
<DownloadIcon />
<Download size={20} />
</Link>
) : (
<DownloadIcon disabled />
<Download size={20} className="text-gray-400" />
)}
</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() {
const [search, setSearch] = useState('');
@@ -203,7 +160,7 @@ export default function DocumentsPage() {
<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"
>
<SearchIcon />
<Search size={15} />
</span>
<input
type="text"
@@ -216,12 +173,7 @@ export default function DocumentsPage() {
</div>
</div>
<div className="lg:w-60">
<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>
<Button variant="secondary" type="submit">Search</Button>
</div>
</form>
</div>
@@ -265,7 +217,7 @@ export default function DocumentsPage() {
onChange={() => setUploadMode(!uploadMode)}
/>
<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
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"
htmlFor="upload-file-button"
>
<UploadIcon />
<Upload size={34} />
</label>
</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 { Button } from '../components/Button';
export default function LoginPage() {
const [username, setUsername] = useState('');
@@ -7,7 +9,15 @@ export default function LoginPage() {
const [error, setError] = useState('');
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) => {
e.preventDefault();
@@ -59,13 +69,14 @@ export default function LoginPage() {
<span className="absolute -bottom-5 text-red-400 text-xs">{error}</span>
</div>
</div>
<button
<Button
variant="secondary"
type="submit"
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'}
</button>
</Button>
</form>
<div className="pt-12 pb-12 text-center">
<p className="mt-4">
+6 -42
View File
@@ -1,39 +1,8 @@
import { useState, FormEvent } from 'react';
import { useGetSearch } from '../generated/anthoLumeAPIV1';
import { GetSearchSource } from '../generated/model/getSearchSource';
// 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>
);
}
// 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>
);
}
import { Search, Book, Download } from 'lucide-react';
import { Button } from '../components/Button';
export default function SearchPage() {
const [query, setQuery] = useState('');
@@ -60,7 +29,7 @@ export default function SearchPage() {
<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"
>
<SearchIcon />
<Search size={15} />
</span>
<input
type="text"
@@ -75,7 +44,7 @@ export default function SearchPage() {
<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"
>
<DocumentsIcon />
<Book size={15} />
</span>
<select
value={source}
@@ -87,12 +56,7 @@ export default function SearchPage() {
</select>
</div>
<div className="lg:w-60">
<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>
<Button variant="secondary" type="submit">Search</Button>
</div>
</form>
</div>
@@ -154,7 +118,7 @@ export default function SearchPage() {
className="hover:text-purple-600"
title="Download"
>
<DownloadIcon />
<Download size={15} />
</button>
</td>
<td className="p-3 border-b border-gray-200">
+8 -46
View File
@@ -1,35 +1,7 @@
import { useState, FormEvent } from 'react';
import { useGetSettings } from '../generated/anthoLumeAPIV1';
// User icon SVG
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>
);
}
import { User, Lock, Clock } from 'lucide-react';
import { Button } from '../components/Button';
export default function SettingsPage() {
const { data, isLoading } = useGetSettings();
@@ -60,7 +32,7 @@ export default function SettingsPage() {
<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"
>
<UserIcon />
<User size={60} />
<p className="text-lg">{settingsData?.user?.username}</p>
</div>
</div>
@@ -80,7 +52,7 @@ export default function SettingsPage() {
<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"
>
<PasswordIcon />
<Lock size={15} />
</span>
<input
type="password"
@@ -96,7 +68,7 @@ export default function SettingsPage() {
<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"
>
<PasswordIcon />
<Lock size={15} />
</span>
<input
type="password"
@@ -108,12 +80,7 @@ export default function SettingsPage() {
</div>
</div>
<div className="lg:w-60">
<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>
<Button variant="secondary" type="submit">Submit</Button>
</div>
</form>
</div>
@@ -131,7 +98,7 @@ export default function SettingsPage() {
<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"
>
<ClockIcon />
<Clock size={15} />
</span>
<select
value={timezone}
@@ -151,12 +118,7 @@ export default function SettingsPage() {
</select>
</div>
<div className="lg:w-60">
<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>
<Button variant="secondary" type="submit">Submit</Button>
</div>
</form>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
darkMode: 'media',
theme: {
extend: {},
},
+4
View File
@@ -13,6 +13,10 @@ export default defineConfig({
target: 'http://localhost:8585',
changeOrigin: true,
},
'/manifest.json': {
target: 'http://localhost:8585',
changeOrigin: true,
},
},
},
build: {