Merge pull request #4 from acid-info/topic-implement-post-container
Implement post container & style updates for desktop / mobile
This commit is contained in:
commit
aa32cfab87
|
@ -33,3 +33,5 @@ yarn-error.log*
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Tag } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { nope } from '@/utils/general.utils'
|
||||||
|
|
||||||
|
type FilterTagsProps = {
|
||||||
|
tags: string[]
|
||||||
|
selectedTags: string[]
|
||||||
|
onTagClick?: (tag: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilterTags(props: FilterTagsProps) {
|
||||||
|
const { tags = [], onTagClick = nope, selectedTags } = props
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Tags>
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<Tag
|
||||||
|
size="small"
|
||||||
|
disabled={false}
|
||||||
|
key={index}
|
||||||
|
onClick={() => onTagClick(tag)}
|
||||||
|
variant={selectedTags.includes(tag) ? 'filled' : 'outlined'}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Tags>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
padding: 8px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Tags = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-right: 14px;
|
||||||
|
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:first-child {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as FilterTags } from './FilterTags'
|
|
@ -1,30 +0,0 @@
|
||||||
import { Typography } from "@acid-info/lsd-react";
|
|
||||||
import styled from "@emotion/styled";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Typography genericFontFamily="serif" component="span" variant="h2">
|
|
||||||
LOGOS →{" "}
|
|
||||||
<Title genericFontFamily="serif" component="span" variant="h2">
|
|
||||||
PRESS ENGINE
|
|
||||||
</Title>
|
|
||||||
</Typography>
|
|
||||||
<Description component="div" variant="label2">
|
|
||||||
Blog with media written by Logos members
|
|
||||||
</Description>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
padding: 16px 16px 8px 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Title = styled(Typography)`
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Description = styled(Typography)`
|
|
||||||
margin-top: 6px;
|
|
||||||
`;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as Header } from './Header';
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { Tag } from "@acid-info/lsd-react";
|
|
||||||
import styled from "@emotion/styled";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Tags>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Privacy
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Security
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Liberty
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Censorship
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Decentralization
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false} style={{ whiteSpace: "nowrap" }}>
|
|
||||||
Openness / inclusivity
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Innovation
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Interview
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Podcast
|
|
||||||
</Tag>
|
|
||||||
<Tag size="small" disabled={false}>
|
|
||||||
Law
|
|
||||||
</Tag>
|
|
||||||
</Tags>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
padding: 16px 0 16px 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Tags = styled.div`
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-right: 16px;
|
|
||||||
`;
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as HeaderTags } from './HeaderTags';
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
export default function Hero() {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title genericFontFamily="serif" component="span" variant="h2">
|
||||||
|
LOGOS →{' '}
|
||||||
|
<Title
|
||||||
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
|
genericFontFamily="serif"
|
||||||
|
component="span"
|
||||||
|
variant="h2"
|
||||||
|
>
|
||||||
|
PRESS ENGINE
|
||||||
|
</Title>
|
||||||
|
</Title>
|
||||||
|
<Description component="div" variant="label2">
|
||||||
|
Blog with media written by Logos members
|
||||||
|
</Description>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 16px 8px 16px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled(Typography)`
|
||||||
|
// temporary breakpoint
|
||||||
|
@media (min-width: 1440px) {
|
||||||
|
padding-block: 16px;
|
||||||
|
font-size: 90px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Description = styled(Typography)`
|
||||||
|
margin-top: 6px;
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Hero } from './Hero'
|
|
@ -1,18 +1,20 @@
|
||||||
import styled from "@emotion/styled";
|
import styled from '@emotion/styled'
|
||||||
import { LogosIcon } from "../icons/LogosIcon";
|
import { IconButton, Typography } from '@acid-info/lsd-react'
|
||||||
import { IconButton, Typography } from "@acid-info/lsd-react";
|
import { LogosIcon } from '../Icons/LogosIcon'
|
||||||
import { MoonIcon } from "../icons/MoonIcon";
|
import { SunIcon } from '../Icons/SunIcon'
|
||||||
import { SunIcon } from "../icons/SunIcon";
|
import { MoonIcon } from '../Icons/MoonIcon'
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
isDark: boolean;
|
isDark: boolean
|
||||||
toggle: () => void;
|
toggle: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Navbar({ isDark, toggle }: NavbarProps) {
|
export default function Navbar({ isDark, toggle }: NavbarProps) {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<LogosIcon color="primary" />
|
<LogosIconContainer>
|
||||||
|
<LogosIcon color="primary" />
|
||||||
|
</LogosIconContainer>
|
||||||
<Icons>
|
<Icons>
|
||||||
<IconButton size="small" onClick={() => toggle()}>
|
<IconButton size="small" onClick={() => toggle()}>
|
||||||
{isDark ? <SunIcon color="primary" /> : <MoonIcon color="primary" />}
|
{isDark ? <SunIcon color="primary" /> : <MoonIcon color="primary" />}
|
||||||
|
@ -22,22 +24,37 @@ export default function Navbar({ isDark, toggle }: NavbarProps) {
|
||||||
</Selector>
|
</Selector>
|
||||||
</Icons>
|
</Icons>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.nav`
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid rgb(var(--lsd-theme-primary));
|
border-bottom: 1px solid rgb(var(--lsd-theme-primary));
|
||||||
`;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
background: rgb(var(--lsd-surface-primary));
|
||||||
|
z-index: 100;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LogosIconContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-left: unset;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const Icons = styled.div`
|
const Icons = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
margin-left: auto;
|
||||||
|
`
|
||||||
|
|
||||||
const Selector = styled(IconButton)`
|
const Selector = styled(IconButton)`
|
||||||
border-left: none;
|
border-left: none;
|
||||||
`;
|
`
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
export const NavbarFiller = styled.div`
|
||||||
|
height: var(--lpe-nav-rendered-height);
|
||||||
|
`
|
|
@ -68,7 +68,7 @@ export default function Post({
|
||||||
{description}
|
{description}
|
||||||
</CustomTypography>
|
</CustomTypography>
|
||||||
),
|
),
|
||||||
[classType, description, size],
|
[classType, description],
|
||||||
)
|
)
|
||||||
|
|
||||||
const _thumbnail = useMemo(() => {
|
const _thumbnail = useMemo(() => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Post, { PostProps } from './Post'
|
import { PostContainer } from '../PostContainer'
|
||||||
|
import { PostProps } from './Post'
|
||||||
|
|
||||||
const postsData: PostProps[] = [
|
const postsData: PostProps[] = [
|
||||||
{
|
{
|
||||||
|
@ -18,7 +19,7 @@ const postsData: PostProps[] = [
|
||||||
tags: ['Privacy', 'Security', 'Liberty'],
|
tags: ['Privacy', 'Security', 'Liberty'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
aspectRatio: 'portrait', // different aspect ratio
|
aspectRatio: 'portrait', // different aspect ratio - portrait
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://images.pexels.com/photos/4992820/pexels-photo-4992820.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
|
'https://images.pexels.com/photos/4992820/pexels-photo-4992820.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
|
@ -56,31 +57,67 @@ const postsData: PostProps[] = [
|
||||||
description:
|
description:
|
||||||
'We built a pedal-powered generator and controller, which is practical to use as an energy source and exercise machine in a household -- and which you can integrate into a solar PV',
|
'We built a pedal-powered generator and controller, which is practical to use as an energy source and exercise machine in a household -- and which you can integrate into a solar PV',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
showImage: false, // without image
|
||||||
|
classType: 'article',
|
||||||
|
date: new Date(),
|
||||||
|
title: 'Satoshi breaks their silence: Inside the mind of the OG anon',
|
||||||
|
description:
|
||||||
|
"Bitcoin's creator reveals their feelings on privacy, CBDCs and their favorite NFT collection in an unprecedented interview with Acid.info",
|
||||||
|
author: 'Jason Freeman',
|
||||||
|
tags: ['Privacy', 'Security', 'Liberty'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
aspectRatio: 'square', // square
|
||||||
|
imageUrl:
|
||||||
|
'https://images.pexels.com/photos/6477673/pexels-photo-6477673.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
|
||||||
|
date: new Date(),
|
||||||
|
title: 'How to Build a Practical Household Bike Generator',
|
||||||
|
description:
|
||||||
|
'We built a pedal-powered generator and controller, which is practical to use as an energy source and exercise machine in a household -- and which you can integrate into a solar PV',
|
||||||
|
author: 'Jason Freeman',
|
||||||
|
tags: ['Privacy', 'Security', 'Liberty'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// featured
|
||||||
|
imageUrl:
|
||||||
|
'https://images.pexels.com/photos/6227715/pexels-photo-6227715.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
|
||||||
|
date: new Date(),
|
||||||
|
title: 'How to Build a Practical Household Bike Generator',
|
||||||
|
description:
|
||||||
|
'We built a pedal-powered generator and controller, which is practical to use as an energy source and exercise machine in a household -- and which you can integrate into a solar PV',
|
||||||
|
author: 'Jason Freeman',
|
||||||
|
tags: ['Privacy', 'Security', 'Liberty'],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const PostsDemo = () => {
|
const PostsDemo = () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '78px' }}>
|
<div style={{ marginTop: '78px' }}>
|
||||||
{/* For Demo purposes only. Use inline CSS and styled components temporarily */}
|
{/* For Demo purposes only. Use inline CSS and styled components temporarily */}
|
||||||
<div
|
<PostContainer title="Featured" postsData={[postsData[7]]} />
|
||||||
style={{
|
<PostContainer
|
||||||
display: 'flex',
|
style={{ marginTop: '108px' }}
|
||||||
flexDirection: 'column',
|
title="Latest Posts"
|
||||||
padding: '16px',
|
postsData={[postsData[3], postsData[0], postsData[3], postsData[2]]}
|
||||||
gap: '24px',
|
/>
|
||||||
}}
|
<PostContainer
|
||||||
>
|
style={{ marginTop: '16px' }}
|
||||||
{postsData.map((post, index) => (
|
postsData={[postsData[3], postsData[5]]}
|
||||||
<div
|
/>
|
||||||
style={{
|
<PostContainer
|
||||||
padding: '16px 0',
|
style={{ marginTop: '16px' }}
|
||||||
borderTop: '1px solid rgb(var(--lsd-theme-primary))',
|
postsData={[postsData[1], postsData[5], postsData[5], postsData[6]]}
|
||||||
}}
|
/>
|
||||||
>
|
<PostContainer
|
||||||
<Post key={index} {...post} />
|
style={{ marginTop: '108px' }}
|
||||||
</div>
|
title="Podcasts"
|
||||||
))}
|
postsData={[postsData[2], postsData[2]]}
|
||||||
</div>
|
/>
|
||||||
|
<PostContainer
|
||||||
|
style={{ marginTop: '16px', paddingBottom: '108px' }}
|
||||||
|
postsData={[postsData[2], postsData[2], postsData[2], postsData[2]]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as Post } from './Post'
|
export { default as Post } from './Post'
|
||||||
export { default as PostsDemo } from './PostsDemo'
|
export { default as PostsDemo } from './PostsDemo'
|
||||||
|
export type { PostProps } from './Post'
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { CommonProps } from '@acid-info/lsd-react/dist/utils/useCommonProps'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { Post, PostProps } from '../Post'
|
||||||
|
import { Typography } from '@acid-info/lsd-react'
|
||||||
|
|
||||||
|
export type PostContainerProps = CommonProps &
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
title?: string
|
||||||
|
postsData: PostProps[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PostContainer({
|
||||||
|
title,
|
||||||
|
postsData,
|
||||||
|
...props
|
||||||
|
}: PostContainerProps) {
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
{title && (<Title variant="body1" genericFontFamily="sans-serif">{title}</Title>)}
|
||||||
|
<Container>
|
||||||
|
{postsData.map((post, index) => (
|
||||||
|
<PostWrapper key={index}>
|
||||||
|
<Post {...post} />
|
||||||
|
</PostWrapper>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
// temporariy breakpoint
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const PostWrapper = styled.div`
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid rgb(var(--lsd-theme-primary));
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled(Typography)`
|
||||||
|
padding: 0 16px;
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as PostContainer } from './PostContainer'
|
|
@ -1,8 +0,0 @@
|
||||||
.searchBox {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchBox > div {
|
|
||||||
border-left: none !important;
|
|
||||||
border-right: none !important;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Autocomplete } from "@acid-info/lsd-react";
|
|
||||||
import styles from "./Search.module.css";
|
|
||||||
|
|
||||||
export default function Search() {
|
|
||||||
return (
|
|
||||||
<Autocomplete
|
|
||||||
className={styles.searchBox}
|
|
||||||
placeholder="Search through the LPE posts.."
|
|
||||||
withIcon
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as Search } from './Search';
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
.searchBox {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton{
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox{
|
||||||
|
border: none !important;
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
import {
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
IconButton,
|
||||||
|
SearchIcon,
|
||||||
|
CloseIcon,
|
||||||
|
} from '@acid-info/lsd-react'
|
||||||
|
import styles from './Search.module.css'
|
||||||
|
import { SearchbarContainer } from '@/components/Searchbar/SearchbarContainer'
|
||||||
|
import { copyConfigs } from '@/configs/copy.configs'
|
||||||
|
import { ESearchScope } from '@/types/ui.types'
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
import FilterTags from '@/components/FilterTags/FilterTags'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
|
||||||
|
export type SearchbarProps = {
|
||||||
|
searchScope?: ESearchScope
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Searchbar(props: SearchbarProps) {
|
||||||
|
const { searchScope = ESearchScope.GLOBAL } = props
|
||||||
|
|
||||||
|
const [active, setActive] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
|
const [query, setQuery] = useState<string>('')
|
||||||
|
const [filterTags, setFilterTags] = useState<string[]>([])
|
||||||
|
|
||||||
|
const validateSearch = useCallback(() => {
|
||||||
|
return query.length > 0 || filterTags.length > 0
|
||||||
|
}, [query, filterTags])
|
||||||
|
|
||||||
|
const performSearch = useCallback(() => {
|
||||||
|
if (!validateSearch()) return
|
||||||
|
}, [validateSearch])
|
||||||
|
|
||||||
|
const performClear = useCallback(() => {
|
||||||
|
// TODO: clear input.value seems to be not working. When set to undefined, the input value is still there.
|
||||||
|
setQuery('')
|
||||||
|
setFilterTags([])
|
||||||
|
}, [setQuery, setFilterTags])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
performSearch()
|
||||||
|
}, [filterTags, performSearch])
|
||||||
|
|
||||||
|
const handleTagClick = (tag: string) => {
|
||||||
|
let newSelectedTags = [...filterTags]
|
||||||
|
if (newSelectedTags.includes(tag)) {
|
||||||
|
newSelectedTags = newSelectedTags.filter((t) => t !== tag)
|
||||||
|
} else {
|
||||||
|
newSelectedTags.push(tag)
|
||||||
|
}
|
||||||
|
setFilterTags(newSelectedTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const constructCollapseText = () => {
|
||||||
|
let txt = ''
|
||||||
|
if (query !== undefined && query.length > 0) {
|
||||||
|
txt += `<span>${query}</span>`
|
||||||
|
}
|
||||||
|
if (filterTags.length > 0) {
|
||||||
|
if (txt.length > 0) txt += '<b> . </b>'
|
||||||
|
txt += `${filterTags.map((t) => `<small>[${t}]</small>`).join(' ')}`
|
||||||
|
}
|
||||||
|
return txt
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCollapsed = validateSearch() && !active
|
||||||
|
|
||||||
|
const placeholder =
|
||||||
|
searchScope === ESearchScope.GLOBAL
|
||||||
|
? copyConfigs.search.searchbarPlaceholders.global()
|
||||||
|
: copyConfigs.search.searchbarPlaceholders.article()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchbarContainer onUnfocus={() => setActive(false)}>
|
||||||
|
<SearchBox>
|
||||||
|
<TextField
|
||||||
|
className={styles.searchBox}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={query as string}
|
||||||
|
onFocus={() => setActive(true)}
|
||||||
|
onChange={(e) => {
|
||||||
|
setQuery(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
className={styles.searchButton}
|
||||||
|
onClick={() =>
|
||||||
|
validateSearch() ? performClear() : performSearch()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{validateSearch() ? <CloseIcon /> : <SearchIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</SearchBox>
|
||||||
|
<TagsWrapper className={active ? 'active' : ''}>
|
||||||
|
<FilterTags
|
||||||
|
tags={copyConfigs.search.filterTags}
|
||||||
|
onTagClick={handleTagClick}
|
||||||
|
selectedTags={filterTags}
|
||||||
|
/>
|
||||||
|
</TagsWrapper>
|
||||||
|
<Collapsed
|
||||||
|
className={isCollapsed ? 'enabled' : ''}
|
||||||
|
onClick={() => setActive(true)}
|
||||||
|
dangerouslySetInnerHTML={{ __html: constructCollapseText() }}
|
||||||
|
/>
|
||||||
|
</SearchbarContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagsWrapper = styled.div`
|
||||||
|
transition: height 250ms ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Collapsed = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
background: rgb(var(--lsd-surface-primary));
|
||||||
|
padding: 8px 14px;
|
||||||
|
width: calc(90% - 28px);
|
||||||
|
position: absolute;
|
||||||
|
z-index: auto;
|
||||||
|
|
||||||
|
top: -100%;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
transition: top 250ms ease-in-out;
|
||||||
|
|
||||||
|
&.enabled {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
> * {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
> *:not(:first-child) {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const SearchBox = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`
|
|
@ -0,0 +1,58 @@
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { uiConfigs } from '@/configs/ui.configs'
|
||||||
|
import { useIsScrolling, useOutsideClick, useSticky } from '@/utils/ui.utils'
|
||||||
|
import { PropsWithChildren, useEffect } from 'react'
|
||||||
|
import { nope } from '@/utils/general.utils'
|
||||||
|
|
||||||
|
type Props = PropsWithChildren<{
|
||||||
|
onUnfocus?: () => void
|
||||||
|
}>
|
||||||
|
|
||||||
|
export function SearchbarContainer({ children, onUnfocus = nope }: Props) {
|
||||||
|
const { sticky, stickyRef, height } = useSticky<HTMLDivElement>(
|
||||||
|
uiConfigs.navbarRenderedHeight,
|
||||||
|
)
|
||||||
|
const { isOutside } = useOutsideClick<HTMLDivElement>(stickyRef)
|
||||||
|
const isScrolling = useIsScrolling()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOutside && onUnfocus) {
|
||||||
|
onUnfocus()
|
||||||
|
}
|
||||||
|
if (isScrolling && onUnfocus) {
|
||||||
|
console.log('scrolling', isScrolling)
|
||||||
|
onUnfocus()
|
||||||
|
}
|
||||||
|
}, [isOutside, stickyRef, isScrolling, onUnfocus])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchBarWrapper ref={stickyRef} className={sticky ? 'sticky' : ''}>
|
||||||
|
{children}
|
||||||
|
</SearchBarWrapper>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: `${height}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchBarWrapper = styled.div<Props>`
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background: rgb(var(--lsd-surface-primary));
|
||||||
|
border-bottom: 1px solid rgb(var(--lsd-border-primary));
|
||||||
|
border-top: 1px solid rgb(var(--lsd-border-primary));
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.sticky {
|
||||||
|
position: fixed;
|
||||||
|
top: ${uiConfigs.navbarRenderedHeight - 1}px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
`
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Searchbar } from './Searchbar'
|
|
@ -0,0 +1,20 @@
|
||||||
|
export const copyConfigs = {
|
||||||
|
search: {
|
||||||
|
searchbarPlaceholders: {
|
||||||
|
global: () => 'Search through the LPE posts...',
|
||||||
|
article: () => `Search through the article`,
|
||||||
|
},
|
||||||
|
filterTags: [
|
||||||
|
'Privacy',
|
||||||
|
'Security',
|
||||||
|
'Liberty',
|
||||||
|
'Censorship',
|
||||||
|
'Decentralization',
|
||||||
|
'Openness / inclusivity',
|
||||||
|
'Innovation',
|
||||||
|
'Interview',
|
||||||
|
'Podcast',
|
||||||
|
'Law',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const uiConfigs = {
|
||||||
|
navbarRenderedHeight: 45,
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Navbar } from '@/components/Navbar'
|
||||||
|
import useIsDarkState from '@/states/isDarkState/isDarkState'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
||||||
|
import { Searchbar } from '@/components/Searchbar'
|
||||||
|
import { ESearchScope } from '@/types/ui.types'
|
||||||
|
|
||||||
|
export default function ArticleLayout(props: PropsWithChildren<any>) {
|
||||||
|
const isDarkState = useIsDarkState()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header>
|
||||||
|
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
||||||
|
<NavbarFiller />
|
||||||
|
<Searchbar searchScope={ESearchScope.ARTICLE} />
|
||||||
|
</header>
|
||||||
|
<main>{props.children}</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as ArticleLayout } from './Article.layout'
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Navbar } from '@/components/Navbar'
|
||||||
|
import useIsDarkState from '@/states/isDarkState/isDarkState'
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
import { Hero } from '@/components/Hero'
|
||||||
|
import { NavbarFiller } from '@/components/Navbar/NavbarFiller'
|
||||||
|
import { Searchbar } from '@/components/Searchbar'
|
||||||
|
|
||||||
|
export default function DefaultLayout(props: PropsWithChildren<any>) {
|
||||||
|
const isDarkState = useIsDarkState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header>
|
||||||
|
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
||||||
|
<NavbarFiller />
|
||||||
|
<Hero />
|
||||||
|
<Searchbar />
|
||||||
|
</header>
|
||||||
|
<main>{props.children}</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as DefaultLayout } from './Default.layout'
|
|
@ -1,17 +0,0 @@
|
||||||
import { Header } from "@/components/Header";
|
|
||||||
import { HeaderTags } from "@/components/HeaderTags";
|
|
||||||
import { Navbar } from "@/components/Navbar";
|
|
||||||
import { Search } from "@/components/Search";
|
|
||||||
import useIsDarkState from "@/states/isDarkState/isDarkState";
|
|
||||||
|
|
||||||
export default function HeaderLayout() {
|
|
||||||
const isDarkState = useIsDarkState();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Navbar isDark={isDarkState.get()} toggle={isDarkState.toggle} />
|
|
||||||
<Header />
|
|
||||||
<HeaderTags />
|
|
||||||
<Search />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as HeaderLayout } from './Header.layout';
|
|
|
@ -1,17 +1,40 @@
|
||||||
import useIsDarkState from "@/states/isDarkState/isDarkState";
|
import useIsDarkState from '@/states/isDarkState/isDarkState'
|
||||||
import { defaultThemes, ThemeProvider } from "@acid-info/lsd-react";
|
import { defaultThemes, ThemeProvider } from '@acid-info/lsd-react'
|
||||||
import { css, Global } from "@emotion/react";
|
import { css, Global } from '@emotion/react'
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from 'next/app'
|
||||||
import Head from "next/head";
|
import Head from 'next/head'
|
||||||
|
import { uiConfigs } from '@/configs/ui.configs'
|
||||||
|
import { DefaultLayout } from '@/layouts/DefaultLayout'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { NextComponentType, NextPageContext } from 'next'
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
type NextLayoutComponentType<P = {}> = NextComponentType<
|
||||||
const isDark = useIsDarkState().get();
|
NextPageContext,
|
||||||
|
any,
|
||||||
|
P
|
||||||
|
> & {
|
||||||
|
getLayout?: (page: ReactNode) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppLayoutProps<P = {}> = AppProps & {
|
||||||
|
Component: NextLayoutComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppLayoutProps) {
|
||||||
|
const isDark = useIsDarkState().get()
|
||||||
|
|
||||||
|
const getLayout =
|
||||||
|
Component.getLayout ||
|
||||||
|
((page: ReactNode) => <DefaultLayout>{page}</DefaultLayout>)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={isDark ? defaultThemes.dark : defaultThemes.light}>
|
<ThemeProvider theme={isDark ? defaultThemes.dark : defaultThemes.light}>
|
||||||
<Component {...pageProps} />
|
|
||||||
<Head>
|
<Head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
<title>Acid</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<Global
|
<Global
|
||||||
styles={css`
|
styles={css`
|
||||||
|
@ -22,8 +45,12 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
:root {
|
||||||
|
--lpe-nav-rendered-height: ${uiConfigs.navbarRenderedHeight}px;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
{getLayout(<Component {...pageProps} />)}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { NextPage } from 'next'
|
||||||
|
import { ArticleLayout } from '@/layouts/ArticleLayout'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
type Props = NextPage<{}>
|
||||||
|
|
||||||
|
const ArticlePage = (props: Props) => {
|
||||||
|
return <article>article</article>
|
||||||
|
}
|
||||||
|
|
||||||
|
ArticlePage.getLayout = function getLayout(page: ReactNode) {
|
||||||
|
return <ArticleLayout>{page}</ArticleLayout>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArticlePage
|
|
@ -1,23 +1,9 @@
|
||||||
import PostsDemo from '@/components/Post/PostsDemo'
|
import PostsDemo from '@/components/Post/PostsDemo'
|
||||||
import { HeaderLayout } from '@/layouts/HeaderLayout'
|
|
||||||
import Head from 'next/head'
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<PostsDemo />
|
||||||
<title>Logos Press Engine</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Blog with media written by Logos members"
|
|
||||||
/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<HeaderLayout />
|
|
||||||
<PostsDemo />
|
|
||||||
</main>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
:root {
|
|
||||||
--max-width: 1100px;
|
|
||||||
--border-radius: 12px;
|
|
||||||
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
|
|
||||||
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
|
|
||||||
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
|
|
||||||
|
|
||||||
--foreground-rgb: 0, 0, 0;
|
|
||||||
--background-start-rgb: 214, 219, 220;
|
|
||||||
--background-end-rgb: 255, 255, 255;
|
|
||||||
|
|
||||||
--primary-glow: conic-gradient(
|
|
||||||
from 180deg at 50% 50%,
|
|
||||||
#16abff33 0deg,
|
|
||||||
#0885ff33 55deg,
|
|
||||||
#54d6ff33 120deg,
|
|
||||||
#0071ff33 160deg,
|
|
||||||
transparent 360deg
|
|
||||||
);
|
|
||||||
--secondary-glow: radial-gradient(
|
|
||||||
rgba(255, 255, 255, 1),
|
|
||||||
rgba(255, 255, 255, 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
--tile-start-rgb: 239, 245, 249;
|
|
||||||
--tile-end-rgb: 228, 232, 233;
|
|
||||||
--tile-border: conic-gradient(
|
|
||||||
#00000080,
|
|
||||||
#00000040,
|
|
||||||
#00000030,
|
|
||||||
#00000020,
|
|
||||||
#00000010,
|
|
||||||
#00000010,
|
|
||||||
#00000080
|
|
||||||
);
|
|
||||||
|
|
||||||
--callout-rgb: 238, 240, 241;
|
|
||||||
--callout-border-rgb: 172, 175, 176;
|
|
||||||
--card-rgb: 180, 185, 188;
|
|
||||||
--card-border-rgb: 131, 134, 135;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--foreground-rgb: 255, 255, 255;
|
|
||||||
--background-start-rgb: 0, 0, 0;
|
|
||||||
--background-end-rgb: 0, 0, 0;
|
|
||||||
|
|
||||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
|
||||||
--secondary-glow: linear-gradient(
|
|
||||||
to bottom right,
|
|
||||||
rgba(1, 65, 255, 0),
|
|
||||||
rgba(1, 65, 255, 0),
|
|
||||||
rgba(1, 65, 255, 0.3)
|
|
||||||
);
|
|
||||||
|
|
||||||
--tile-start-rgb: 2, 13, 46;
|
|
||||||
--tile-end-rgb: 2, 5, 19;
|
|
||||||
--tile-border: conic-gradient(
|
|
||||||
#ffffff80,
|
|
||||||
#ffffff40,
|
|
||||||
#ffffff30,
|
|
||||||
#ffffff20,
|
|
||||||
#ffffff10,
|
|
||||||
#ffffff10,
|
|
||||||
#ffffff80
|
|
||||||
);
|
|
||||||
|
|
||||||
--callout-rgb: 20, 20, 20;
|
|
||||||
--callout-border-rgb: 108, 108, 108;
|
|
||||||
--card-rgb: 100, 100, 100;
|
|
||||||
--card-border-rgb: 200, 200, 200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: rgb(var(--foreground-rgb));
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
rgb(var(--background-end-rgb))
|
|
||||||
)
|
|
||||||
rgb(var(--background-start-rgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum ESearchScope {
|
||||||
|
GLOBAL = 'global',
|
||||||
|
ARTICLE = 'article',
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export const nope = (...args: any) => {}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { RefObject, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export const useSticky = <T extends HTMLElement>(dy: number = 0) => {
|
||||||
|
const stickyRef = useRef<T>(null)
|
||||||
|
const [sticky, setSticky] = useState(false)
|
||||||
|
const [offset, setOffset] = useState(0)
|
||||||
|
const [height, setHeight] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stickyRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOffset(stickyRef.current.offsetTop)
|
||||||
|
setHeight(stickyRef.current.clientHeight)
|
||||||
|
}, [stickyRef, setOffset])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!stickyRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSticky(window.scrollY > offset - dy)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [dy, setSticky, stickyRef, offset])
|
||||||
|
|
||||||
|
return { stickyRef, sticky, height: sticky ? height : 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOutsideClick = <T extends HTMLDivElement = HTMLDivElement>(
|
||||||
|
ref: RefObject<T>,
|
||||||
|
) => {
|
||||||
|
const [isOutside, setIsOutside] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: any) {
|
||||||
|
const isOutside = !!(ref.current && !ref.current.contains(event.target))
|
||||||
|
setIsOutside(isOutside)
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [ref])
|
||||||
|
|
||||||
|
return { isOutside }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useIsScrolling = () => {
|
||||||
|
const [isScrolling, setIsScrolling] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: NodeJS.Timeout
|
||||||
|
function handleScroll() {
|
||||||
|
setIsScrolling(true)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => setIsScrolling(false), 100)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [setIsScrolling])
|
||||||
|
return isScrolling
|
||||||
|
}
|
Loading…
Reference in New Issue