diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b58b603
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/lpe-frontend.iml b/.idea/lpe-frontend.iml
new file mode 100644
index 0000000..0c8867d
--- /dev/null
+++ b/.idea/lpe-frontend.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..c098ab5
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/FilterTags/FilterTags.tsx b/src/components/FilterTags/FilterTags.tsx
new file mode 100644
index 0000000..f64ed11
--- /dev/null
+++ b/src/components/FilterTags/FilterTags.tsx
@@ -0,0 +1,58 @@
+import { Tag } from '@acid-info/lsd-react'
+import styled from '@emotion/styled'
+import { useState } from 'react'
+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 (
+
+
+ {tags.map((tag, index) => (
+ onTagClick(tag)}
+ selected={selectedTags.includes(tag)}
+ variant={selectedTags.includes(tag) ? 'filled' : 'outlined'}
+ >
+ {tag}
+
+ ))}
+
+
+ )
+}
+
+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;
+ }
+`
diff --git a/src/components/FilterTags/index.ts b/src/components/FilterTags/index.ts
new file mode 100644
index 0000000..06b8e87
--- /dev/null
+++ b/src/components/FilterTags/index.ts
@@ -0,0 +1 @@
+export { default as FilterTags } from './HeaderTags'
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
deleted file mode 100644
index 2d31985..0000000
--- a/src/components/Header/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Header } from './Header';
\ No newline at end of file
diff --git a/src/components/HeaderTags/HeaderTags.tsx b/src/components/HeaderTags/HeaderTags.tsx
deleted file mode 100644
index 3e3ad9c..0000000
--- a/src/components/HeaderTags/HeaderTags.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Tag } from "@acid-info/lsd-react";
-import styled from "@emotion/styled";
-
-export default function Header() {
- return (
-
-
-
- Privacy
-
-
- Security
-
-
- Liberty
-
-
- Censorship
-
-
- Decentralization
-
-
- Openness / inclusivity
-
-
- Innovation
-
-
- Interview
-
-
- Podcast
-
-
- Law
-
-
-
- );
-}
-
-const Container = styled.div`
- padding: 16px 0 16px 16px;
-`;
-
-const Tags = styled.div`
- display: flex;
- gap: 8px;
- overflow-x: auto;
- padding-right: 16px;
-`;
diff --git a/src/components/HeaderTags/index.ts b/src/components/HeaderTags/index.ts
deleted file mode 100644
index f80bc45..0000000
--- a/src/components/HeaderTags/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as HeaderTags } from './HeaderTags';
\ No newline at end of file
diff --git a/src/components/Header/Header.tsx b/src/components/Hero/Hero.tsx
similarity index 77%
rename from src/components/Header/Header.tsx
rename to src/components/Hero/Hero.tsx
index d257254..ffa0c3b 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Hero/Hero.tsx
@@ -1,11 +1,11 @@
-import { Typography } from "@acid-info/lsd-react";
-import styled from "@emotion/styled";
+import { Typography } from '@acid-info/lsd-react'
+import styled from '@emotion/styled'
-export default function Header() {
+export default function Hero() {
return (
- LOGOS →{" "}
+ LOGOS →{' '}
PRESS ENGINE
@@ -14,17 +14,17 @@ export default function Header() {
Blog with media written by Logos members
- );
+ )
}
const Container = styled.div`
padding: 16px 16px 8px 16px;
-`;
+`
const Title = styled(Typography)`
white-space: nowrap;
-`;
+`
const Description = styled(Typography)`
margin-top: 6px;
-`;
+`
diff --git a/src/components/Hero/index.ts b/src/components/Hero/index.ts
new file mode 100644
index 0000000..a3465b7
--- /dev/null
+++ b/src/components/Hero/index.ts
@@ -0,0 +1 @@
+export { default as Hero } from './Hero'
diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx
index 596940d..489c292 100644
--- a/src/components/Navbar/Navbar.tsx
+++ b/src/components/Navbar/Navbar.tsx
@@ -1,12 +1,12 @@
-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 { LogosIcon } from '../icons/LogosIcon'
+import { IconButton, Typography } from '@acid-info/lsd-react'
+import { MoonIcon } from '../icons/MoonIcon'
+import { SunIcon } from '../icons/SunIcon'
interface NavbarProps {
- isDark: boolean;
- toggle: () => void;
+ isDark: boolean
+ toggle: () => void
}
export default function Navbar({ isDark, toggle }: NavbarProps) {
@@ -22,22 +22,27 @@ export default function Navbar({ isDark, toggle }: NavbarProps) {
- );
+ )
}
-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 Icons = styled.div`
display: flex;
align-items: center;
-`;
+`
const Selector = styled(IconButton)`
border-left: none;
-`;
+`
diff --git a/src/components/Navbar/NavbarFiller.tsx b/src/components/Navbar/NavbarFiller.tsx
new file mode 100644
index 0000000..724707e
--- /dev/null
+++ b/src/components/Navbar/NavbarFiller.tsx
@@ -0,0 +1,5 @@
+import styled from '@emotion/styled'
+
+export const NavbarFiller = styled.div`
+ height: var(--lpe-nav-rendered-height);
+`
diff --git a/src/components/Search/Search.module.css b/src/components/Search/Search.module.css
deleted file mode 100644
index ba92798..0000000
--- a/src/components/Search/Search.module.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.searchBox {
- width: 100% !important;
-}
-
-.searchBox > div {
- border-left: none !important;
- border-right: none !important;
-}
\ No newline at end of file
diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx
deleted file mode 100644
index 97763b6..0000000
--- a/src/components/Search/Search.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Autocomplete } from "@acid-info/lsd-react";
-import styles from "./Search.module.css";
-
-export default function Search() {
- return (
-
- );
-}
diff --git a/src/components/Search/index.ts b/src/components/Search/index.ts
deleted file mode 100644
index 5f65bc8..0000000
--- a/src/components/Search/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Search } from './Search';
\ No newline at end of file
diff --git a/src/components/Searchbar/Search.module.css b/src/components/Searchbar/Search.module.css
new file mode 100644
index 0000000..71bfef8
--- /dev/null
+++ b/src/components/Searchbar/Search.module.css
@@ -0,0 +1,11 @@
+.searchBox {
+ width: 100% !important;
+}
+
+.active::placeholder{
+ /*opacity: 1 !important;*/
+}
+
+.searchBox > div {
+ border: none !important;
+}
diff --git a/src/components/Searchbar/Searchbar.tsx b/src/components/Searchbar/Searchbar.tsx
new file mode 100644
index 0000000..1f03aaa
--- /dev/null
+++ b/src/components/Searchbar/Searchbar.tsx
@@ -0,0 +1,130 @@
+import { Autocomplete } 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(undefined)
+ const [filterTags, setFilterTags] = useState([])
+
+ const performSearch = () => {
+ console.log('performing search', query, filterTags)
+ if (!validateSearch()) return
+ }
+
+ useEffect(() => {
+ performSearch()
+ }, [filterTags])
+
+ 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) => {
+ if (e.key === 'Enter') {
+ performSearch()
+ }
+ }
+
+ const validateSearch = () => {
+ return (query !== undefined && query.length > 0) || filterTags.length > 0
+ }
+
+ const isCollapsed = validateSearch() && !active
+
+ const constructCollapseText = () => {
+ let txt = ''
+ if (query !== undefined && query.length > 0) {
+ txt += query
+ }
+ if (filterTags.length > 0) {
+ if (txt.length > 0) txt += ' . '
+ txt += `${filterTags.map((t) => `[${t}]`).join(',')}`
+ }
+ return txt
+ }
+
+ return (
+ setActive(false)}>
+ setActive(true)}
+ inputProps={{
+ onChange: (e) => setQuery(e.target.value),
+ onKeyUp: (e) => handleEnter(e),
+ className: isCollapsed ? styles.active : '',
+ }}
+ />
+
+
+
+
+ setActive(true)}
+ >
+ {constructCollapseText()}
+
+
+ )
+}
+
+const TagsWrapper = styled.div`
+ transition: height 250ms ease-in-out;
+ overflow: hidden;
+ height: 0;
+
+ &.active {
+ height: 45px;
+ }
+`
+
+const Collapsed = styled.div`
+ 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;
+ }
+`
diff --git a/src/components/Searchbar/SearchbarContainer.tsx b/src/components/Searchbar/SearchbarContainer.tsx
new file mode 100644
index 0000000..2082909
--- /dev/null
+++ b/src/components/Searchbar/SearchbarContainer.tsx
@@ -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(
+ uiConfigs.navbarRenderedHeight,
+ )
+ const { isOutside } = useOutsideClick(stickyRef)
+ const isScrolling = useIsScrolling()
+
+ useEffect(() => {
+ if (isOutside && onUnfocus) {
+ onUnfocus()
+ }
+ if (isScrolling && onUnfocus) {
+ console.log('scrolling', isScrolling)
+ onUnfocus()
+ }
+ }, [isOutside, stickyRef, isScrolling])
+
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
+
+const SearchBarWrapper = styled.div`
+ 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;
+ }
+`
diff --git a/src/components/Searchbar/index.ts b/src/components/Searchbar/index.ts
new file mode 100644
index 0000000..6ece6a9
--- /dev/null
+++ b/src/components/Searchbar/index.ts
@@ -0,0 +1 @@
+export { default as Searchbar } from './Searchbar'
diff --git a/src/configs/copy.configs.ts b/src/configs/copy.configs.ts
new file mode 100644
index 0000000..9fe379c
--- /dev/null
+++ b/src/configs/copy.configs.ts
@@ -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',
+ ],
+ },
+}
diff --git a/src/configs/ui.configs.ts b/src/configs/ui.configs.ts
new file mode 100644
index 0000000..1036bc5
--- /dev/null
+++ b/src/configs/ui.configs.ts
@@ -0,0 +1,3 @@
+export const uiConfigs = {
+ navbarRenderedHeight: 45,
+}
diff --git a/src/layouts/ArticleLayout/Article.layout.tsx b/src/layouts/ArticleLayout/Article.layout.tsx
new file mode 100644
index 0000000..0479971
--- /dev/null
+++ b/src/layouts/ArticleLayout/Article.layout.tsx
@@ -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) {
+ const isDarkState = useIsDarkState()
+ return (
+ <>
+
+ {props.children}
+ >
+ )
+}
diff --git a/src/layouts/ArticleLayout/index.ts b/src/layouts/ArticleLayout/index.ts
new file mode 100644
index 0000000..a30058d
--- /dev/null
+++ b/src/layouts/ArticleLayout/index.ts
@@ -0,0 +1 @@
+export { default as ArticleLayout } from './Article.layout'
diff --git a/src/layouts/DefaultLayout/Default.layout.tsx b/src/layouts/DefaultLayout/Default.layout.tsx
new file mode 100644
index 0000000..7e1cf14
--- /dev/null
+++ b/src/layouts/DefaultLayout/Default.layout.tsx
@@ -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) {
+ const isDarkState = useIsDarkState()
+
+ return (
+ <>
+
+ {props.children}
+ >
+ )
+}
diff --git a/src/layouts/DefaultLayout/index.ts b/src/layouts/DefaultLayout/index.ts
new file mode 100644
index 0000000..c58a2b6
--- /dev/null
+++ b/src/layouts/DefaultLayout/index.ts
@@ -0,0 +1 @@
+export { default as DefaultLayout } from './Default.layout'
diff --git a/src/layouts/HeaderLayout/Header.layout.tsx b/src/layouts/HeaderLayout/Header.layout.tsx
deleted file mode 100644
index 3f9efb7..0000000
--- a/src/layouts/HeaderLayout/Header.layout.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
-
- >
- );
-}
diff --git a/src/layouts/HeaderLayout/index.ts b/src/layouts/HeaderLayout/index.ts
deleted file mode 100644
index aababef..0000000
--- a/src/layouts/HeaderLayout/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as HeaderLayout } from './Header.layout';
\ No newline at end of file
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 4e15ff0..1b6048e 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,17 +1,27 @@
-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'
export default function App({ Component, pageProps }: AppProps) {
- const isDark = useIsDarkState().get();
+ const isDark = useIsDarkState().get()
+
+ //TODO: fix this
+ //@ts-ignore
+ const getLayout =
+ Component.getLayout || ((page) => {page})
return (
-
-
+ Acid
+
+ {getLayout()}
- );
+ )
}
diff --git a/src/pages/article/[:slug].tsx b/src/pages/article/[:slug].tsx
new file mode 100644
index 0000000..ccb7c14
--- /dev/null
+++ b/src/pages/article/[:slug].tsx
@@ -0,0 +1,14 @@
+import { NextPage } from 'next'
+import { ArticleLayout } from '@/layouts/ArticleLayout'
+
+type Props = NextPage<{}>
+
+const ArticlePage = (props: Props) => {
+ return article
+}
+
+ArticlePage.getLayout = function getLayout(page) {
+ return {page}
+}
+
+export default ArticlePage
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 42cab72..0a3fb38 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -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 (
<>
-
- Logos Press Engine
-
-
-
-
-
-
-
-
+
>
)
}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index d4f491e..e69de29 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -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;
- }
-}
diff --git a/src/types/ui.types.ts b/src/types/ui.types.ts
new file mode 100644
index 0000000..6ef6e21
--- /dev/null
+++ b/src/types/ui.types.ts
@@ -0,0 +1,4 @@
+export enum ESearchScope {
+ GLOBAL = 'global',
+ ARTICLE = 'article',
+}
diff --git a/src/utils/.placeholder b/src/utils/.placeholder
deleted file mode 100644
index e69de29..0000000
diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts
new file mode 100644
index 0000000..3b10ad8
--- /dev/null
+++ b/src/utils/general.utils.ts
@@ -0,0 +1 @@
+export const nope = (...args: any) => {}
diff --git a/src/utils/ui.utils.ts b/src/utils/ui.utils.ts
new file mode 100644
index 0000000..46be6a1
--- /dev/null
+++ b/src/utils/ui.utils.ts
@@ -0,0 +1,59 @@
+import { useEffect, useRef, useState } from 'react'
+
+export const useSticky = (dy: number = 0) => {
+ const stickyRef = useRef(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 = (ref) => {
+ const [isOutside, setIsOutside] = useState(false)
+ useEffect(() => {
+ function handleClickOutside(event) {
+ setIsOutside(ref.current && !ref.current.contains(event.target))
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+ }, [ref])
+
+ return { isOutside }
+}
+
+export const useIsScrolling = () => {
+ const [isScrolling, setIsScrolling] = useState(false)
+ useEffect(() => {
+ let timeout
+ function handleScroll() {
+ setIsScrolling(true)
+ clearTimeout(timeout)
+ timeout = setTimeout(() => setIsScrolling(false), 100)
+ }
+ window.addEventListener('scroll', handleScroll)
+ return () => window.removeEventListener('scroll', handleScroll)
+ }, [setIsScrolling])
+ return isScrolling
+}