Skip to content

Commit 99e240c

Browse files
committed
feat: add top banner
1 parent 008dec3 commit 99e240c

File tree

9 files changed

+164
-16
lines changed

9 files changed

+164
-16
lines changed

src/components/layout/Header.tsx

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,52 @@ import ThemeSwitcher from '@/components/buttons/ThemeSwitcher';
1313
import { RepoModal } from '@/components/dialog/RepoModal';
1414
import AvatarWithDropdown from '@/components/dropdown/AvatarWithDropdown';
1515

16+
import { getHeaderAd } from '@/services/home';
17+
18+
import TopBanner from './TopBanner';
1619
import { LoginButton } from '../buttons/LoginButton';
1720
import SearchInput from '../search/SearchInput';
1821

19-
const Header = () => {
22+
import { AdvertItem } from '@/types/home';
23+
24+
interface Props {
25+
hiddenAd: () => void;
26+
showAd: () => void;
27+
}
28+
29+
const Header = ({ hiddenAd, showAd }: Props) => {
2030
const router = useRouter();
2131
const { isLogin } = useLoginContext();
2232
const [curPath, setCurPath] = useState('');
23-
const { t } = useTranslation('common');
33+
const { t, i18n } = useTranslation('common');
34+
const [adData, setAdData] = useState<AdvertItem | null>(null);
35+
const [_, setHasHeaderAd] = useState(false);
36+
37+
const handleCloseAd = () => {
38+
hiddenAd();
39+
setHasHeaderAd(false);
40+
};
41+
42+
const initHeaderAd = async () => {
43+
const res = await getHeaderAd();
44+
if (res.success) {
45+
if (res.data.length > 0) {
46+
if (localStorage.adClosed === res.data[0].aid) {
47+
setHasHeaderAd(false);
48+
} else {
49+
showAd();
50+
setHasHeaderAd(true);
51+
setAdData(res.data[0]);
52+
}
53+
} else {
54+
setHasHeaderAd(false);
55+
}
56+
}
57+
};
58+
59+
useEffect(() => {
60+
initHeaderAd();
61+
}, []);
2462

2563
useEffect(() => {
2664
setCurPath(router.pathname);
@@ -40,7 +78,14 @@ const Header = () => {
4078
);
4179

4280
return (
43-
<div className='fixed z-10 h-14 w-full bg-white shadow-sm backdrop-blur dark:border dark:border-gray-50/[0.06] dark:bg-transparent'>
81+
<div className='fixed z-10 w-full bg-white shadow-sm backdrop-blur dark:border dark:border-gray-50/[0.06] dark:bg-transparent'>
82+
{adData && (
83+
<TopBanner
84+
i18n_lang={i18n.language}
85+
data={adData}
86+
onClose={handleCloseAd}
87+
/>
88+
)}
4489
<nav className='mx-auto flex max-w-5xl items-center justify-between px-2 py-2 md:py-0 lg:px-0 xl:max-w-5xl 2xl:max-w-7xl'>
4590
{/* pc 端显示的 logo */}
4691
<span className='hidden py-2 md:block'>

src/components/layout/Layout.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { useRouter } from 'next/router';
2-
import { ReactNode, useMemo } from 'react';
2+
import { ReactNode, useMemo, useState } from 'react';
33

44
import Header from '@/components/layout/Header';
55
import { Side } from '@/components/side/Side';
6-
7-
import TagList from '../side/TagList';
6+
import TagList from '@/components/side/TagList';
87

98
interface LayoutProps {
109
children: ReactNode;
@@ -13,6 +12,15 @@ interface LayoutProps {
1312
const Layout: React.FC<LayoutProps> = ({ children }) => {
1413
const router = useRouter();
1514
const { pathname } = router;
15+
const [showHeaderAd, setShowHeaderAd] = useState(false);
16+
17+
const handleShowAd = () => {
18+
setShowHeaderAd(true);
19+
};
20+
21+
const handleColseAd = () => {
22+
setShowHeaderAd(false);
23+
};
1624

1725
const isSinglePage = useMemo(() => {
1826
const singlePageRoutes = [
@@ -23,15 +31,21 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
2331
return singlePageRoutes.includes(pathname);
2432
}, [pathname]);
2533

34+
const ptValue = showHeaderAd ? '5.5rem' : '3.5rem';
35+
const topValue = showHeaderAd ? '6rem' : '4rem';
36+
2637
return (
2738
<>
28-
<Header />
29-
<main className='container mx-auto px-0 pt-14 xl:px-40 2xl:px-56'>
39+
<Header hiddenAd={handleColseAd} showAd={handleShowAd} />
40+
<main
41+
className='container mx-auto px-0 xl:px-40 2xl:px-56'
42+
style={{ paddingTop: ptValue }}
43+
>
3044
{isSinglePage ? (
3145
<div>{children}</div>
3246
) : (
3347
<div className='flex flex-row md:border-none'>
34-
{pathname === '/' && <TagList />}
48+
{pathname === '/' && <TagList topValue={topValue} />}
3549
<div
3650
className={`relative ${
3751
pathname === '/'
@@ -42,7 +56,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
4256
{children}
4357
</div>
4458
<div className='relative hidden w-3/12 shrink-0 md:block'>
45-
<Side isHome={pathname === '/'} />
59+
<Side isHome={pathname === '/'} topValue={topValue} />
4660
</div>
4761
</div>
4862
)}

src/components/layout/TopBanner.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { useEffect, useState } from 'react';
2+
import { IoMdClose } from 'react-icons/io';
3+
4+
import { redirectRecord } from '@/services/home';
5+
6+
import { NoPrefetchLink } from '../links/CustomLink';
7+
8+
import { AdvertItem } from '@/types/home';
9+
10+
interface Props {
11+
data: AdvertItem;
12+
i18n_lang: string;
13+
onClose: () => void;
14+
}
15+
16+
const TopBanner = ({ data, i18n_lang, onClose }: Props) => {
17+
const [isClosed, setIsClosed] = useState(false);
18+
const adText = i18n_lang === 'en' ? data.text_en : data.text;
19+
const clickText = i18n_lang === 'en' ? 'Click' : '查看';
20+
21+
const handleCloseAd = () => {
22+
setIsClosed(true);
23+
localStorage.adClosed = data.aid;
24+
onClose();
25+
};
26+
27+
useEffect(() => {
28+
if (localStorage.adClosed === data.aid) {
29+
setIsClosed(true);
30+
}
31+
}, []);
32+
33+
const onClickLink = (aid: string) => {
34+
redirectRecord('', aid, 'ad');
35+
};
36+
37+
if (isClosed) return null;
38+
return (
39+
<div className='relative'>
40+
<NoPrefetchLink href={data.url}>
41+
<a
42+
target='_blank'
43+
onClick={() => onClickLink(data.aid)}
44+
className='block'
45+
>
46+
<div className='flex h-8 w-full items-center justify-center bg-gradient-to-r from-blue-600 to-blue-500 px-4 dark:from-blue-700/90 dark:to-blue-600/90'>
47+
<span className='max-w-[60%] truncate text-sm text-white'>
48+
{adText}
49+
</span>
50+
<span className='ml-2 shrink-0 text-xs text-white/90'>
51+
{clickText}
52+
</span>
53+
</div>
54+
</a>
55+
</NoPrefetchLink>
56+
57+
<button
58+
className='absolute right-2 top-1/2 -translate-y-1/2 p-1 text-white opacity-80 transition-colors hover:opacity-100'
59+
onClick={handleCloseAd}
60+
>
61+
<IoMdClose className='h-4 w-4' />
62+
</button>
63+
</div>
64+
);
65+
};
66+
export default TopBanner;

src/components/side/Side.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import UserStatus from './UserStatus';
1414

1515
import { AdvertItems } from '@/types/home';
1616

17-
export const Side = ({ isHome }: { isHome: boolean }) => {
17+
interface Props {
18+
isHome: boolean;
19+
topValue: string;
20+
}
21+
22+
export const Side = ({ isHome, topValue }: Props) => {
1823
const { t, i18n } = useTranslation('common');
1924
const [displayAdOnly, setDisplayAdOnly] = useState(false);
2025
const containerRef = useRef<HTMLDivElement>(null);
@@ -64,6 +69,7 @@ export const Side = ({ isHome }: { isHome: boolean }) => {
6469
displayAdOnly={displayAdOnly}
6570
t={t}
6671
i18n_lang={i18n.language}
72+
topValue={topValue}
6773
/>
6874
)}
6975
</>

src/components/side/SideAd.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface Props {
99
displayAdOnly?: boolean;
1010
t: (key: string) => string;
1111
i18n_lang?: string;
12+
topValue?: string;
1213
}
1314

1415
export const SideAd: NextPage<Props> = ({ data, t, i18n_lang }) => {
@@ -26,10 +27,12 @@ export const SideFixAd: NextPage<Props> = ({
2627
displayAdOnly,
2728
t,
2829
i18n_lang,
30+
topValue,
2931
}) => {
3032
return (
3133
<div
32-
className='fixed top-16 ml-3 max-w-[244px] space-y-2'
34+
className='fixed ml-3 max-w-[244px] space-y-2'
35+
style={{ top: topValue }}
3336
hidden={!displayAdOnly}
3437
>
3538
{data.map((item: AdvertItem) => (

src/components/side/TagList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { TagListSkeleton } from '../loading/skeleton';
1414

1515
import { Tag } from '@/types/tag';
1616

17-
export default function TagList() {
17+
interface TagListProps {
18+
topValue: string;
19+
}
20+
21+
export default function TagList({ topValue }: TagListProps) {
1822
const { t, i18n } = useTranslation('home');
1923
const defaultTag: Tag = {
2024
name: '综合',
@@ -76,7 +80,7 @@ export default function TagList() {
7680

7781
return (
7882
<div className='hidden max-w-[162px] shrink-0 lg:block lg:w-2/12 lg:grow-0'>
79-
<div className='fixed top-16 pl-2'>
83+
<div className='fixed pl-2' style={{ top: topValue }}>
8084
<div className='w-[140px] rounded-lg bg-white px-3 py-2 dark:bg-gray-800'>
8185
<div className='px-1 pb-1'>
8286
<div className='border-b border-b-gray-200 pb-2 dark:border-b-gray-600 dark:text-gray-300'>

src/services/home.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { makeUrl } from '@/utils/api';
55
import { fetcher } from './base';
66

77
import {
8+
AdvertItems,
89
HomeItem,
910
HomeItems,
1011
RecommendItems,
@@ -98,3 +99,10 @@ export const getSitemap = async (): Promise<Sitemap> => {
9899
const data = await fetcher<Sitemap>(makeUrl(`/sitemap/`));
99100
return data;
100101
};
102+
103+
export const getHeaderAd = async (): Promise<AdvertItems> => {
104+
const data = await fetcher<AdvertItems>(
105+
makeUrl(`/sponsor/`, { position: 'top' })
106+
);
107+
return data;
108+
};

src/types/home.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ export interface AdvertItem {
6868
is_reward: boolean;
6969
year?: number;
7070
day?: number;
71+
text?: string;
72+
text_en?: string;
7173
percent: number;
7274
}
7375

src/utils/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import stringify from './qs-stringify';
22

33
export const ITEMS_PER_PAGE = 10;
44

5-
const LOCAL_API_HOST = 'https://frp.hellogithub.com';
6-
// const LOCAL_API_HOST = 'http://127.0.0.1:8001';
5+
// const LOCAL_API_HOST = 'https://frp.hellogithub.com';
6+
const LOCAL_API_HOST = 'http://127.0.0.1:8001';
77
const PRODUCTION_API_HOST = 'https://api.hellogithub.com';
88
const ABROAD_API_HOST = 'https://abroad.hellogithub.com';
99

0 commit comments

Comments
 (0)