dc-copy/src/Header.tsx
2025-04-02 02:10:11 +09:00

545 lines
27 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { cn } from './util/cn';
// Define interfaces for the components
interface DropdownItem {
name: string;
href?: string;
}
interface DropdownMenuProps {
items: DropdownItem[];
isVisible: boolean;
widthClass: string;
leftClass: string;
}
interface NavItemWithDropdownProps {
title: string;
items: DropdownItem[];
widthClass: string;
leftClass: string;
}
interface TickerItem {
id: number;
text: string;
href: string;
highlight?: string;
highlightClass: string;
}
// Helper component for dropdown menus
const DropdownMenu: React.FC<DropdownMenuProps> = ({ items, isVisible, widthClass, leftClass }) => (
<div
className={cn(
'absolute top-full box-content',
leftClass,
widthClass,
'pt-3 pr-[3px] pb-[10px] pl-[17px]',
'bg-custom-blue-dark z-4002',
isVisible ? 'block' : 'hidden',
'transition-opacity duration-150 ease-in-out',
isVisible ? 'opacity-100' : 'opacity-0'
)}
>
<span className="absolute left-0 top-0 w-[15px] h-[15px] bg-sp-img bg-[-96px_0px] z-10"></span>
<ul className="list-none">
{items.map((item) => (
<li key={item.name} className="relative">
<a
href={item.href || '#'}
className="block text-white text-xs font-normal leading-6 tracking-0.025em text-shadow-blue-dark hover:text-custom-yellow"
>
{item.name}
</a>
</li>
))}
</ul>
</div>
);
// Helper component for Nav Items with Dropdowns
const NavItemWithDropdown: React.FC<NavItemWithDropdownProps> = ({ title, items, widthClass, leftClass }) => {
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const timeoutRef = useRef<number | null>(null);
const showDropdown = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
setIsDropdownVisible(true);
};
const hideDropdown = () => {
timeoutRef.current = setTimeout(() => {
setIsDropdownVisible(false);
}, 150);
};
return (
<li
className={cn(
'relative float-left ml-5'
)}
onMouseEnter={showDropdown}
onMouseLeave={hideDropdown}
>
<a href="#" className="text-white text-sm font-bold leading-11 tracking-0.025em text-shadow-blue-dark">
{title}
</a>
{title === '갤러리' && <span className="block w-[77px] h-[12px] absolute left-0 bottom-0"></span>}
<DropdownMenu
items={items}
isVisible={isDropdownVisible}
widthClass={widthClass}
leftClass={leftClass}
/>
</li>
);
};
// Helper component for More Button Dropdown
const MoreButtonDropdown: React.FC<Omit<NavItemWithDropdownProps, 'title'>> = ({ items, widthClass, leftClass }) => {
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const timeoutRef = useRef<number | null>(null);
const showDropdown = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
setIsDropdownVisible(true);
};
const hideDropdown = () => {
timeoutRef.current = setTimeout(() => {
setIsDropdownVisible(false);
}, 150);
};
return (
<li
className="relative float-left ml-5"
onMouseEnter={showDropdown}
onMouseLeave={hideDropdown}
>
<button
type="button"
className="text-white text-sm font-bold leading-11 tracking-0.025em cursor-pointer align-middle bg-transparent border-none"
aria-haspopup="true"
aria-expanded={isDropdownVisible}
>
<span className="sr-only"></span>
</button>
<DropdownMenu
items={items}
isVisible={isDropdownVisible}
widthClass={widthClass}
leftClass={leftClass}
/>
</li>
);
};
// Main Ticker component
const InfoTicker: React.FC = () => {
const tickerItems: TickerItem[] = [
{ id: 1, text: '디시 로터리 응모', href: '#', highlightClass: '' },
{ id: 2, text: '어제 %HIGHLIGHT%개 게시글 등록', href: '#', highlight: '911,981', highlightClass: 'text-custom-yellow-light' },
{ id: 3, text: '어제 %HIGHLIGHT%개 댓글 등록', href: '#', highlight: '2,527,905', highlightClass: 'text-custom-cyan' },
{ id: 4, text: '총 갤러리 수 %HIGHLIGHT%개', href: '#', highlight: '80,516', highlightClass: 'text-custom-pink' },
];
const [currentItemIndex, setCurrentItemIndex] = useState(0);
const [animating, setAnimating] = useState(false);
useEffect(() => {
const intervalId = setInterval(() => {
setAnimating(true);
const animTimeout = setTimeout(() => {
setAnimating(false);
setCurrentItemIndex((prevIndex) => (prevIndex + 1) % tickerItems.length);
}, 500); // Animation duration
return () => clearTimeout(animTimeout);
}, 3000);
return () => clearInterval(intervalId);
}, [tickerItems.length]);
const currentItem = tickerItems[currentItemIndex];
const nextItemIndex = (currentItemIndex + 1) % tickerItems.length;
const nextItem = tickerItems[nextItemIndex];
const renderTickerItem = (item: TickerItem) => {
const parts = item.text.split('%HIGHLIGHT%');
return (
<a href={item.href} className="text-white text-sm">
{parts[0]}
{item.highlight && (
<em className={cn(`not-italic font-bold tracking-0.7px`, item.highlightClass)}>
{` ${item.highlight} `}
</em>
)}
{parts[1]}
</a>
);
};
return (
<div className="w-[306px] h-11 text-center text-sm text-white leading-11 overflow-hidden">
<div className="relative w-full h-full box-border px-[6.5px]">
<div className="h-11 flex items-center justify-center bg-custom-blue-hover" key={currentItem.id}>
{renderTickerItem(currentItem)}
</div>
<div
className={cn(
"transition-transform duration-500 ease-in z-10",
animating ? "-translate-y-full" : ""
)}
key={nextItem.id}
>
<div className="h-11 flex items-center justify-center bg-custom-blue-hover">
{renderTickerItem(nextItem)}
</div>
</div>
</div>
</div>
);
};
// Main Navigation Bar Component
export function GlobalNavigationBar(): JSX.Element {
const galleryItems: DropdownItem[] = [
{ name: '게임', href: '#' }, { name: '연예/방송', href: '#' }, { name: '스포츠', href: '#' },
{ name: '교육/금융/IT', href: '#' }, { name: '여행/음식/생물', href: '#' }, { name: '취미/생활', href: '#' },
];
const moreItems: DropdownItem[] = [
{ name: '디시뉴스', href: '#' }, { name: '디시게임', href: '#' }, { name: '디시위키', href: '#' },
{ name: '이벤트', href: '#' }, { name: '디시콘', href: '#' },
];
return (
<div className={cn("bg-custom-blue-hover relative min-w-[1160px] border border-custom-blue-hover")}>
<nav className="w-[1160px] h-11 mx-auto flex justify-between items-center">
<h2 className="sr-only">GNB</h2>
<ul className="list-none m-0 p-0 flex items-center">
<NavItemWithDropdown
title="갤러리"
items={galleryItems}
widthClass="w-[104px]"
leftClass="left-0"
/>
<li className="relative float-left ml-5">
<a href="#" className="text-custom-yellow text-sm font-bold leading-11 tracking-0.025em text-shadow-blue-dark">
</a>
</li>
<li className="relative float-left ml-5">
<a href="#" className="text-white text-sm font-bold leading-11 tracking-0.025em text-shadow-blue-dark">
</a>
</li>
<li className="relative float-left ml-5">
<a href="#" className="text-white text-sm font-bold leading-11 tracking-0.025em text-shadow-blue-dark">
</a>
</li>
<li className="relative float-left ml-5">
<a href="#" className="text-white text-sm font-bold leading-11 tracking-0.025em text-shadow-blue-dark">
</a>
</li>
<li className="relative float-left ml-5">
<a href="#" className="text-white text-sm font-bold leading-11 tracking-0.025em text-shadow-blue-dark">
</a>
</li>
<MoreButtonDropdown
items={moreItems}
widthClass="w-[67px]"
leftClass="left-[-7px]"
/>
</ul>
<InfoTicker />
</nav>
</div>
);
}
export function Header({
isMinorGallery = true, // Default value for isMinorGallery
}: {
isMinorGallery?: boolean;
}) {
const [isNightAlertVisible, setIsNightAlertVisible] = useState(true);
const logo_img = "/dcin_logo.png"; // Hardcoded based on original script
const minor_gallery_img = "/tit_mgallery.png"; // Hardcoded based on original script
return (
<header className="bg-transparent">
<div className="relative w-[1160px] h-[105px] mx-auto bg-transparent">
{/* Logo */}
<h1 className="overflow-hidden absolute left-0 top-[40px] text-center bg-transparent">
{/* Assuming two logo images or variations were intended */}
<a href="/" className="text-custom-gray-dark float-left">
<img src={logo_img} alt="DCInside Logo" className="bg-transparent" />
</a>
{isMinorGallery && (
<a href="/" className="text-custom-gray-dark float-left">
<img src={minor_gallery_img} alt="DCInside Logo Alt" className="bg-transparent" />
</a>
)}
</h1>
{/* Search Bar */}
<div className="absolute left-1/2 top-[37px] -ml-[182px] bg-transparent">
<h2 className="sr-only"> </h2> {/* Replaced complex styling with sr-only */}
<form action="/search" method="get" className="bg-transparent"> {/* Added form action/method placeholders */}
<fieldset className="bg-transparent">
<legend className="sr-only"></legend> {/* Replaced complex styling with sr-only */}
<div className="bg-custom-blue-hover w-[364px] h-[46px]">
<div className="bg-white float-left w-[315px] mt-1 ml-1">
<input
type="text"
name="keyword" // Added name attribute
placeholder="갤러리 & 통합 검색" // Added placeholder
className="align-middle text-sm font-apple-dotum w-[297px] h-[38px] px-[9px] bg-transparent text-custom-gray-dark font-bold border-none outline-none"
/>
</div>
<button
type="submit"
className="cursor-pointer align-middle text-xs font-apple-dotum bg-transparent bg-[url('https://nstatic.dcinside.com/dc/w/images/sp/sp_img.png?1112')] bg-no-repeat float-left w-[45px] h-[44px] bg-[0px_-89px]"
aria-label="검색" // Added aria-label for accessibility
>
<span className="sr-only"></span> {/* Replaced complex styling with sr-only */}
</button>
</div>
{/* Search Suggestions Dropdown (Initially Hidden) */}
<div className="bg-white absolute w-full box-border border-2 border-custom-blue-dark z-[4001] left-0 top-[46px] hidden">
{/* Dropdown content would go here */}
</div>
</fieldset>
</form>
{/* Another absolute positioned div from source (Initially Hidden) */}
<div className="bg-white absolute w-full box-border border-2 border-custom-blue-dark z-[4001] left-0 top-[45px] hidden">
{/* Content, if any, would go here */}
</div>
</div>
{/* Top Right Navigation */}
<div className="absolute top-[9px] right-0 flex items-center">
<ul className="list-none flex items-center overflow-inherit leading-[17px] divide-solid divide-x divide-custom-border-gray">
{['갤러리', '미니갤', '인물갤', '갤로그', '디시트렌드', '디시뉴스', '디시게임', '이벤트', '디시콘'].map((item) => (
<li key={item} className="h-[10px] flex items-center">
<a href="#" className="text-custom-dropdown-text text-[11px] px-1.5 py-0.5 block"> {/* Added padding/block */}
{item}
</a>
</li>
))}
</ul>
<div className="flex items-center">
<a
href="#"
className="bg-custom-blue-hover text-white text-[11px] inline-block px-[3px] h-[16px] box-content leading-[16px]
border border-custom-blue-dark shadow-[0_-1px_0_rgb(29,39,97)] ml-1.5
" // Added margin
>
</a>
</div>
{/* Night Mode Toggle */}
<div className="relative ml-[10px]"> {/* Added margin from style */}
<a href="#" className="text-custom-dropdown-text text-[11px] leading-[18px] flex items-center">
<em className="inline-block w-[11px] h-[11px] bg-sp-img bg-[-255px_-844px] mr-1 align-[-1px]"></em>
</a>
{/* Night Mode Tooltip */}
{isNightAlertVisible && (
<div className="bg-transparent absolute z-[999] left-[-35px] top-[25px] cursor-pointer"
onClick={() => setIsNightAlertVisible(false)}
>
<div className="bg-custom-green-light relative border border-custom-border-gray px-[10px] text-white text-xs h-[23px] rounded-full flex items-center"> {/* Used flex to center text */}
<p className="bg-transparent whitespace-nowrap overflow-hidden max-w-[780px]">
</p>
</div>
</div>
)}
</div>
</div>
</div>
</header>
);
}
export function VisitHistory({
recentVisits,
}: {
recentVisits?: {
id: number;
name: string;
isMinor?: boolean;
}[];
}) {
recentVisits = recentVisits ?? [];
// --- State for Interactivity (Example - not fully implemented in static version) ---
const [isDropdownOpen, setIsDropdownOpen] = useState(false); // To control the main dropdown visibility
const [activeTab, setActiveTab] = useState('recent'); // 'recent' or 'favorites' for the dropdown tabs
return (
<div className="w-[1160px] mx-auto relative"> {/* Added relative positioning for children */}
{/* --- Horizontal Recent Visit Bar --- */}
<div className="bg-custom-dropdown-bg h-[38px] leading-[38px] pr-[66px] pl-[12px] align-top box-border text-[rgb(68,68,68)] w-[1160px] relative"> {/* Adjusted position to relative, or keep absolute if intended */}
<h3 className="float-left text-xs text-custom-blue-dark align-top tracking-[-0.075em] leading-[38px]">
{activeTab === 'recent' ? '최근 방문' : '즐겨찾기 갤러리'} {/* Dynamic text based on active tab */}
</h3>
{/* "Open Layer" Button - Toggles Dropdown */}
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)} // Example state toggle
aria-label="최근 방문/즐겨찾기 레이어 열기"
className="bg-transparent cursor-pointer align-middle font-apple-dotum text-xs relative top-[12px] float-left w-[15px] h-[15px] ml-[10px]"
>
<span className="sr-only"> </span>
<em className={`bg-transparent inline-block w-[15px] h-[15px] bg-sp-img bg-[-56px_-168px] mt-[1px] align-top`}></em>
</button>
{/* "Previous" Button */}
<button aria-label="이전 목록" className="cursor-pointer align-middle font-apple-dotum
text-[12px] float-left relative top-[13px] left-0
text-xs ml-[15px] mr-[9px] leading-[12px]">
<span className="sr-only"></span>
<em className={`inline-block w-[5px] h-[11px] bg-sp-img bg-[-119px_-56px] leading-[38px]`}></em>
</button>
{/* Visit List Container */}
<div className="overflow-hidden float-left"> {/* Added margins to avoid overlap with buttons */}
<ul className="list-none relative whitespace-nowrap space-x-[15px]">
{recentVisits.map((item) => (
<li key={item.id} className="inline-block text-xs align-middle leading-[38px]"> {/* Adjusted alignment */}
<a href="#" className="bg-transparent text-[rgb(68,68,68)] inline-block tracking-[-0.075em] leading-[38px]">
{item.name}
</a>
{item.isMinor && (
<em className="inline-block h-[12px] leading-[12px] mt-[1px] ml-[2px] align-text-top text-[13px] not-italic text-custom-dropdown-text">
</em>
)}
<button aria-label={`${item.name} 삭제`} className="bg-transparent cursor-pointer align-top font-apple-dotum text-xs h-[36px] ml-[12px] leading-[38px]">
<span className="sr-only hidden"></span> {/* Hidden in original */}
<em className={`bg-transparent inline-block w-[7px] h-[7px] align-[1px] bg-sp-img bg-[-264px_-961px] cursor-pointer`}></em>
</button>
</li>
))}
</ul>
{/* Hidden list from original HTML */}
<ul className="list-none relative whitespace-nowrap bg-transparent hidden"></ul>
</div>
{/* "Next" Button */}
<button aria-label="다음 목록" className="bg-transparent cursor-pointer align-middle font-apple-dotum text-xs right-[51px] absolute top-[13px] leading-[12px]">
<span className="sr-only"></span>
<em className={`bg-transparent inline-block w-[5px] h-[11px] bg-sp-img bg-[-110px_-56px] leading-[38px]`}></em>
</button>
{/* "All" Button */}
<button
onClick={() => setIsDropdownOpen(true)} // Example: Open dropdown on click
className="bg-transparent cursor-pointer align-middle font-apple-dotum text-xs absolute right-[12px] top-0 leading-[38px] text-custom-blue-dark underline">
</button>
</div>
{/* --- Dropdown/Modal (Initially Hidden) --- */}
{/* Controlled by isDropdownOpen state */}
<div className={cn(
`bg-white absolute z-[4000] text-left border border-custom-border-gray w-[1160px] left-0 top-[38px]`,
isDropdownOpen ? 'block' : 'hidden'
)}>
{/* Adjusted top position relative to bar */}
<div className="bg-transparent overflow-hidden relative w-full h-auto px-[8px] min-h-[197px] text-xs leading-normal box-border">
{/* Tabs */}
<div className="">
<ul className="list-none box-border">
<li className="float-left w-1/2 text-center border-b border-custom-blue-hover relative"> {/* Adjusted border color */}
<button
onClick={() => setActiveTab('recent')} // Example state change
className={`cursor-pointer align-middle font-apple-dotum text-sm relative w-full h-[40px] font-bold ${
activeTab === 'recent' ? 'text-custom-blue-hover' : 'text-custom-gray-dark'}`}
>
</button>
{activeTab === 'recent' && (
<div className='absolute left-0 bottom-0 w-full right-0 h-1 bg-custom-blue-dark'></div>
)}
</li>
<li className={cn`float-left w-1/2 text-center border-b border-custom-blue-hover relative` /* Adjusted border color */}>
<button
onClick={() => setActiveTab('favorites')} // Example state change
className={`cursor-pointer align-middle font-apple-dotum text-sm relative w-full h-[40px] font-bold ${
activeTab === 'favorites' ? 'text-custom-blue-hover' : 'text-custom-gray-dark'}`}>
</button>
{activeTab === 'favorites' && (
<div className='absolute left-0 bottom-0 w-full right-0 h-1 bg-custom-blue-dark'></div>
)}
</li>
</ul>
</div>
<div className="clear-both"> {/* Added clear-both and padding */}
{/* Recent Visit Content (Visible when activeTab is 'recent') */}
<div className={cn`${activeTab === 'recent' ? 'block' : 'hidden'}`}>
<div className="overflow-hidden">
<ul className="list-none overflow-hidden min-h-[123px] mt-[10px] -ml-[17px] px-[10px]">
{recentVisits.map((item) => (
<li key={`dd-${item.id}`} className="overflow-hidden float-left w-[146px] relative ml-[16px] mr-[3px] text-custom-gray-dark">
<a href="#" className="bg-transparent text-custom-gray-dark leading-[22px] inline-block max-w-[82%] align-top text-ellipsis overflow-hidden whitespace-nowrap">
{item.name}
</a>
{item.isMinor && (
<em className="bg-transparent
not-italic
inline-block h-[12px] leading-[12px] mt-[2px] ml-[2px] align-text-top text-[13px] text-custom-dropdown-text">
</em>
)}
<button aria-label={`${item.name} 삭제`} className="bg-transparent cursor-pointer align-top font-apple-dotum text-xs h-[22px] absolute top-0 right-0">
<span className="sr-only"></span>
<em className={`bg-transparent inline-block w-[7px] h-[7px] align-[1px] bg-sp-img bg-[-127px_-153px] cursor-pointer`}></em>
</button>
</li>
))}
</ul>
<div className="bg-transparent relative h-[20px] mt-[5px] mb-[13px]">
<button className="bg-transparent cursor-pointer align-middle font-apple-dotum text-xs absolute right-[6px] bottom-[-1px] text-custom-blue-dark underline p-[3px]">
</button>
</div>
</div>
</div>
{/* Favorites Content (Visible when activeTab is 'favorites') */}
<div className={cn`${activeTab === 'favorites' ? 'block' : 'hidden'} min-h-[158px] relative`}> {/* Added min-height and relative */}
{/* In a real app, you'd fetch and display favorites or show login prompt */}
<p className="bg-transparent float-left px-[19px] text-xs text-custom-gray-medium absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 mt-[24px]">
<a href="#" className="bg-transparent text-custom-gray-medium">
<span className="bg-transparent underline"></span> .
</a>
</p>
</div>
</div>
</div>
</div>
</div>
);
}