scraping and improvements
This commit is contained in:
@@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTheme } from '../../lib/theme';
|
||||
import Button from '../ui/Button';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
|
||||
export default function Header() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -14,6 +15,24 @@ export default function Header() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const addStoryItems = [
|
||||
{
|
||||
href: '/add-story',
|
||||
label: 'Manual Entry',
|
||||
description: 'Add a story by manually entering details'
|
||||
},
|
||||
{
|
||||
href: '/stories/import',
|
||||
label: 'Import from URL',
|
||||
description: 'Import a single story from a website'
|
||||
},
|
||||
{
|
||||
href: '/stories/import/bulk',
|
||||
label: 'Bulk Import',
|
||||
description: 'Import multiple stories from a list of URLs'
|
||||
}
|
||||
];
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
router.push('/login');
|
||||
@@ -57,12 +76,10 @@ export default function Header() {
|
||||
>
|
||||
Authors
|
||||
</Link>
|
||||
<Link
|
||||
href="/add-story"
|
||||
className="theme-text hover:theme-accent transition-colors font-medium"
|
||||
>
|
||||
Add Story
|
||||
</Link>
|
||||
<Dropdown
|
||||
trigger="Add Story"
|
||||
items={addStoryItems}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
{/* Right side actions */}
|
||||
@@ -131,13 +148,32 @@ export default function Header() {
|
||||
>
|
||||
Authors
|
||||
</Link>
|
||||
<Link
|
||||
href="/add-story"
|
||||
className="theme-text hover:theme-accent transition-colors font-medium px-2 py-1"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Add Story
|
||||
</Link>
|
||||
<div className="px-2 py-1">
|
||||
<div className="font-medium theme-text mb-1">Add Story</div>
|
||||
<div className="pl-4 space-y-1">
|
||||
<Link
|
||||
href="/add-story"
|
||||
className="block theme-text hover:theme-accent transition-colors text-sm py-1"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Manual Entry
|
||||
</Link>
|
||||
<Link
|
||||
href="/stories/import"
|
||||
className="block theme-text hover:theme-accent transition-colors text-sm py-1"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Import from URL
|
||||
</Link>
|
||||
<Link
|
||||
href="/stories/import/bulk"
|
||||
className="block theme-text hover:theme-accent transition-colors text-sm py-1"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Bulk Import
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="theme-text hover:theme-accent transition-colors font-medium px-2 py-1"
|
||||
|
||||
98
frontend/src/components/ui/Dropdown.tsx
Normal file
98
frontend/src/components/ui/Dropdown.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface DropdownItem {
|
||||
href: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
trigger: string;
|
||||
items: DropdownItem[];
|
||||
className?: string;
|
||||
onItemClick?: () => void;
|
||||
}
|
||||
|
||||
export default function Dropdown({ trigger, items, className = '', onItemClick }: DropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const handleItemClick = () => {
|
||||
setIsOpen(false);
|
||||
onItemClick?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative ${className}`}
|
||||
ref={dropdownRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="theme-text hover:theme-accent transition-colors font-medium flex items-center gap-1"
|
||||
>
|
||||
{trigger}
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 mt-1 w-64 theme-card theme-shadow border theme-border rounded-lg py-2 z-50">
|
||||
{items.map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.href}
|
||||
onClick={handleItemClick}
|
||||
className="block px-4 py-2 theme-text hover:theme-accent transition-colors"
|
||||
>
|
||||
<div className="font-medium">{item.label}</div>
|
||||
{item.description && (
|
||||
<div className="text-sm theme-text-secondary mt-1">{item.description}</div>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user