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
|
||||
*.tsbuildinfo
|
||||
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 { LogosIcon } from "../icons/LogosIcon";
|
||||
import { IconButton, Typography } from "@acid-info/lsd-react";
|
||||
import { MoonIcon } from "../icons/MoonIcon";
|
||||
import { SunIcon } from "../icons/SunIcon";
|
||||
import styled from '@emotion/styled'
|
||||
import { IconButton, Typography } from '@acid-info/lsd-react'
|
||||
import { LogosIcon } from '../Icons/LogosIcon'
|
||||
import { SunIcon } from '../Icons/SunIcon'
|
||||
import { MoonIcon } from '../Icons/MoonIcon'
|
||||
|
||||
interface NavbarProps {
|
||||
isDark: boolean;
|
||||
toggle: () => void;
|
||||
isDark: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Navbar({ isDark, toggle }: NavbarProps) {
|
||||
return (
|
||||
<Container>
|
||||
<LogosIcon color="primary" />
|
||||
<LogosIconContainer>
|
||||
<LogosIcon color="primary" />
|
||||
</LogosIconContainer>
|
||||
<Icons>
|
||||
<IconButton size="small" onClick={() => toggle()}>
|
||||
{isDark ? <SunIcon color="primary" /> : <MoonIcon color="primary" />}
|
||||
|
@ -22,22 +24,37 @@ export default function Navbar({ isDark, toggle }: NavbarProps) {
|
|||
</Selector>
|
||||
</Icons>
|
||||
</Container>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled.nav`
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
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`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
margin-left: auto;
|
||||
`
|
||||
|
||||
const Selector = styled(IconButton)`
|
||||
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}
|
||||
</CustomTypography>
|
||||
),
|
||||
[classType, description, size],
|
||||
[classType, description],
|
||||
)
|
||||
|
||||
const _thumbnail = useMemo(() => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Post, { PostProps } from './Post'
|
||||
import { PostContainer } from '../PostContainer'
|
||||
import { PostProps } from './Post'
|
||||
|
||||
const postsData: PostProps[] = [
|
||||
{
|
||||
|
@ -18,7 +19,7 @@ const postsData: PostProps[] = [
|
|||
tags: ['Privacy', 'Security', 'Liberty'],
|
||||
},
|
||||
{
|
||||
aspectRatio: 'portrait', // different aspect ratio
|
||||
aspectRatio: 'portrait', // different aspect ratio - portrait
|
||||
imageUrl:
|
||||
'https://images.pexels.com/photos/4992820/pexels-photo-4992820.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2',
|
||||
date: new Date(),
|
||||
|
@ -56,31 +57,67 @@ const postsData: PostProps[] = [
|
|||
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',
|
||||
},
|
||||
{
|
||||
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 = () => {
|
||||
return (
|
||||
<div style={{ marginTop: '78px' }}>
|
||||
{/* For Demo purposes only. Use inline CSS and styled components temporarily */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '16px',
|
||||
gap: '24px',
|
||||
}}
|
||||
>
|
||||
{postsData.map((post, index) => (
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 0',
|
||||
borderTop: '1px solid rgb(var(--lsd-theme-primary))',
|
||||
}}
|
||||
>
|
||||
<Post key={index} {...post} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PostContainer title="Featured" postsData={[postsData[7]]} />
|
||||
<PostContainer
|
||||
style={{ marginTop: '108px' }}
|
||||
title="Latest Posts"
|
||||
postsData={[postsData[3], postsData[0], postsData[3], postsData[2]]}
|
||||
/>
|
||||
<PostContainer
|
||||
style={{ marginTop: '16px' }}
|
||||
postsData={[postsData[3], postsData[5]]}
|
||||
/>
|
||||
<PostContainer
|
||||
style={{ marginTop: '16px' }}
|
||||
postsData={[postsData[1], postsData[5], postsData[5], postsData[6]]}
|
||||
/>
|
||||
<PostContainer
|
||||
style={{ marginTop: '108px' }}
|
||||
title="Podcasts"
|
||||
postsData={[postsData[2], postsData[2]]}
|
||||
/>
|
||||
<PostContainer
|
||||
style={{ marginTop: '16px', paddingBottom: '108px' }}
|
||||
postsData={[postsData[2], postsData[2], postsData[2], postsData[2]]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { default as Post } from './Post'
|
||||
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 { defaultThemes, ThemeProvider } from "@acid-info/lsd-react";
|
||||
import { css, Global } from "@emotion/react";
|
||||
import type { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import useIsDarkState from '@/states/isDarkState/isDarkState'
|
||||
import { defaultThemes, ThemeProvider } from '@acid-info/lsd-react'
|
||||
import { css, Global } from '@emotion/react'
|
||||
import type { AppProps } from 'next/app'
|
||||
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) {
|
||||
const isDark = useIsDarkState().get();
|
||||
type NextLayoutComponentType<P = {}> = NextComponentType<
|
||||
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 (
|
||||
<ThemeProvider theme={isDark ? defaultThemes.dark : defaultThemes.light}>
|
||||
<Component {...pageProps} />
|
||||
<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>
|
||||
<Global
|
||||
styles={css`
|
||||
|
@ -22,8 +45,12 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:root {
|
||||
--lpe-nav-rendered-height: ${uiConfigs.navbarRenderedHeight}px;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</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 { HeaderLayout } from '@/layouts/HeaderLayout'
|
||||
import Head from 'next/head'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<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>
|
||||
<PostsDemo />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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