-
Notifications
You must be signed in to change notification settings - Fork 440
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
30cfe33
commit 1693802
Showing
8 changed files
with
343 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
"use client"; | ||
|
||
import { AuthBoundary } from "@/components/AuthBoundary"; | ||
//import { useWeb3ConnectionManager } from "@/components/Web3ConnectionManager/hooks/useWeb3ConnectionManager" | ||
import { buttonVariants } from "@/components/ui/Button"; | ||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/ToggleGroup"; | ||
import useIsStuck from "@/hooks/useIsStuck"; | ||
import useScrollspy from "@/hooks/useScrollSpy"; | ||
import { cn } from "@/lib/cssUtils"; | ||
import { Plus } from "@phosphor-icons/react"; | ||
import { useAtom, useAtomValue, useSetAtom } from "jotai"; | ||
import Link from "next/link"; | ||
import { useEffect } from "react"; | ||
import { activeSectionAtom, isNavStuckAtom, isSearchStuckAtom } from "../atoms"; | ||
import { ACTIVE_SECTION } from "../constants"; | ||
|
||
export const smoothScrollTo = (id: string) => { | ||
const target = document.getElementById(id); | ||
|
||
if (!target) return; | ||
|
||
window.scrollTo({ | ||
behavior: "smooth", | ||
top: target.offsetTop, | ||
}); | ||
}; | ||
|
||
const Nav = () => { | ||
const isNavStuck = useAtomValue(isNavStuckAtom); | ||
const isSearchStuck = useAtomValue(isSearchStuckAtom); | ||
const [activeSection, setActiveSection] = useAtom(activeSectionAtom); | ||
const spyActiveSection = useScrollspy(Object.values(ACTIVE_SECTION), 100); | ||
useEffect(() => { | ||
if (!spyActiveSection) return; | ||
setActiveSection( | ||
spyActiveSection as (typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION], | ||
); | ||
}, [spyActiveSection, setActiveSection]); | ||
|
||
return ( | ||
<ToggleGroup | ||
type="single" | ||
className="gap-2" | ||
size={isSearchStuck ? "sm" : "lg"} | ||
variant={isNavStuck ? "secondary" : "mono"} | ||
onValueChange={(value) => | ||
value && | ||
setActiveSection( | ||
value as (typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION], | ||
) | ||
} | ||
value={activeSection} | ||
> | ||
<ToggleGroupItem | ||
value={ACTIVE_SECTION.yourGuilds} | ||
className={cn("rounded-xl transition-all", { | ||
"rounded-lg": isSearchStuck, | ||
})} | ||
onClick={() => smoothScrollTo(ACTIVE_SECTION.yourGuilds)} | ||
> | ||
Your guilds | ||
</ToggleGroupItem> | ||
<ToggleGroupItem | ||
value={ACTIVE_SECTION.exploreGuilds} | ||
className={cn("rounded-xl transition-all", { | ||
"rounded-lg": isSearchStuck, | ||
})} | ||
onClick={() => smoothScrollTo(ACTIVE_SECTION.exploreGuilds)} | ||
> | ||
Explore guilds | ||
</ToggleGroupItem> | ||
</ToggleGroup> | ||
); | ||
}; | ||
|
||
const CreateGuildLink = () => { | ||
const isNavStuck = useAtomValue(isNavStuckAtom); | ||
return ( | ||
<Link | ||
href="/create-guild" | ||
aria-label="Create guild" | ||
prefetch={false} | ||
className={buttonVariants({ | ||
variant: "ghost", | ||
size: "sm", | ||
className: [ | ||
// Temporarily, until we don't migrate the scrollable Tabs component | ||
"min-h-11 w-11 gap-1.5 px-0 sm:min-h-0 sm:w-auto sm:px-3", | ||
{ | ||
"text-white": !isNavStuck, | ||
}, | ||
], | ||
})} | ||
> | ||
<Plus /> | ||
<span className="hidden sm:inline-block">Create guild</span> | ||
</Link> | ||
); | ||
}; | ||
|
||
export const StickyBar = () => { | ||
//const { isWeb3Connected } = useWeb3ConnectionManager() | ||
const setIsNavStuck = useSetAtom(isNavStuckAtom); | ||
const isSearchStuck = useAtomValue(isSearchStuckAtom); | ||
const { ref: navToggleRef } = useIsStuck(setIsNavStuck); | ||
|
||
return ( | ||
<div | ||
className={cn( | ||
"sticky top-0 z-10 flex h-16 w-full items-center transition-all", | ||
{ | ||
"h-12": isSearchStuck, | ||
}, | ||
)} | ||
ref={navToggleRef} | ||
> | ||
<div className="relative flex w-full items-center justify-between"> | ||
<Nav /> | ||
<AuthBoundary fallback={null}> | ||
<CreateGuildLink /> | ||
</AuthBoundary> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
import { atom } from "jotai"; | ||
import { ACTIVE_SECTION } from "./constants"; | ||
|
||
export const searchAtom = atom<string | undefined>(undefined); | ||
export const isNavStuckAtom = atom(false); | ||
export const isSearchStuckAtom = atom(false); | ||
export const activeSectionAtom = atom< | ||
(typeof ACTIVE_SECTION)[keyof typeof ACTIVE_SECTION] | ||
>(ACTIVE_SECTION.yourGuilds); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
"use client"; | ||
|
||
import { cn } from "@/lib/cssUtils"; | ||
import * as TogglePrimitive from "@radix-ui/react-toggle"; | ||
import { type VariantProps, cva } from "class-variance-authority"; | ||
import { | ||
type ComponentPropsWithoutRef, | ||
type ElementRef, | ||
forwardRef, | ||
} from "react"; | ||
|
||
const toggleVariants = cva( | ||
"inline-flex items-center justify-center rounded-lg text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-4 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground font-medium", | ||
{ | ||
variants: { | ||
variant: { | ||
secondary: | ||
"text-secondary-foreground hover:bg-secondary data-[state=on]:bg-secondary active:bg-secondary-hover", | ||
primary: | ||
"hover:bg-secondary-hover active:bg-secondary-active data-[state=on]:bg-primary data-[state=on]:text-primary-foreground bg-secondary text-secondary-foreground", | ||
mono: "text-white hover:bg-white/10 data-[state=on]:bg-white/15 hover:text-white data-[state=on]:text-white", | ||
}, | ||
size: { | ||
sm: "h-8 px-2.5", | ||
md: "h-10 px-3", | ||
lg: "h-11 px-5 font-semibold text-base", | ||
icon: "size-9", | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: "secondary", | ||
size: "md", | ||
}, | ||
}, | ||
); | ||
|
||
const Toggle = forwardRef< | ||
ElementRef<typeof TogglePrimitive.Root>, | ||
ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & | ||
VariantProps<typeof toggleVariants> | ||
>(({ className, variant, size, ...props }, ref) => ( | ||
<TogglePrimitive.Root | ||
ref={ref} | ||
className={cn(toggleVariants({ variant, size, className }))} | ||
{...props} | ||
/> | ||
)); | ||
|
||
Toggle.displayName = TogglePrimitive.Root.displayName; | ||
|
||
export { Toggle, toggleVariants }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
"use client"; | ||
|
||
import { toggleVariants } from "@/components/ui/Toggle"; | ||
import { cn } from "@/lib/cssUtils"; | ||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; | ||
import type { VariantProps } from "class-variance-authority"; | ||
import { | ||
type ComponentPropsWithoutRef, | ||
type ElementRef, | ||
createContext, | ||
forwardRef, | ||
useContext, | ||
} from "react"; | ||
|
||
const ToggleGroupContext = createContext<VariantProps<typeof toggleVariants>>({ | ||
size: "md", | ||
variant: "secondary", | ||
}); | ||
|
||
const ToggleGroup = forwardRef< | ||
ElementRef<typeof ToggleGroupPrimitive.Root>, | ||
ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & | ||
VariantProps<typeof toggleVariants> | ||
>(({ className, variant, size, children, ...props }, ref) => ( | ||
<ToggleGroupPrimitive.Root | ||
ref={ref} | ||
className={cn("flex w-max items-center justify-center gap-1", className)} | ||
{...props} | ||
> | ||
<ToggleGroupContext.Provider value={{ variant, size }}> | ||
{children} | ||
</ToggleGroupContext.Provider> | ||
</ToggleGroupPrimitive.Root> | ||
)); | ||
|
||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; | ||
|
||
const ToggleGroupItem = forwardRef< | ||
ElementRef<typeof ToggleGroupPrimitive.Item>, | ||
ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & | ||
VariantProps<typeof toggleVariants> | ||
>(({ className, children, variant, size, ...props }, ref) => { | ||
const context = useContext(ToggleGroupContext); | ||
|
||
return ( | ||
<ToggleGroupPrimitive.Item | ||
ref={ref} | ||
className={cn( | ||
toggleVariants({ | ||
variant: context.variant || variant, | ||
size: context.size || size, | ||
}), | ||
className, | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
</ToggleGroupPrimitive.Item> | ||
); | ||
}); | ||
|
||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; | ||
|
||
export { ToggleGroup, ToggleGroupItem }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { | ||
type Dispatch, | ||
type MutableRefObject, | ||
type SetStateAction, | ||
useEffect, | ||
useRef, | ||
useState, | ||
} from "react"; | ||
|
||
/** | ||
* The IntersectionObserver triggers if the element is off the viewport, so we have | ||
* to set top="-1px" or bottom="-1px" on the sticky element instead of 0 | ||
*/ | ||
const useIsStuck = ( | ||
setIsStuck?: Dispatch<SetStateAction<boolean>>, | ||
): { ref: MutableRefObject<null>; isStuck?: boolean } => { | ||
const ref = useRef(null); | ||
const [isStuck, setIsStuckLocal] = useState(false); | ||
const setIsStuckActive = setIsStuck ?? setIsStuckLocal; | ||
|
||
useEffect(() => { | ||
Check warning on line 21 in src/hooks/useIsStuck.ts GitHub Actions / quality-assurancelint/correctness/useExhaustiveDependencies
|
||
if (!ref.current) return; | ||
const cachedRef = ref.current; | ||
const topOffsetPx = Number.parseInt(getComputedStyle(cachedRef).top) + 1; | ||
const bottomOffsetPx = | ||
Number.parseInt(getComputedStyle(cachedRef).bottom) + 1; | ||
|
||
const observer = new IntersectionObserver( | ||
([e]) => { | ||
setIsStuckActive( | ||
!e.isIntersecting && | ||
(e.boundingClientRect.top < topOffsetPx || | ||
e.boundingClientRect.bottom > bottomOffsetPx), | ||
); | ||
}, | ||
{ | ||
threshold: [1], | ||
rootMargin: `-${topOffsetPx || 0}px 0px 0px ${bottomOffsetPx || 0}px`, | ||
}, | ||
); | ||
observer.observe(cachedRef); | ||
return () => observer.unobserve(cachedRef); | ||
}, [ref]); | ||
|
||
return { ref, isStuck: setIsStuck ? undefined : isStuck }; | ||
}; | ||
|
||
export default useIsStuck; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { useLayoutEffect, useState } from "react"; | ||
|
||
// Restrict value to be between the range [0, value] | ||
const clamp = (value: number) => Math.max(0, value); | ||
|
||
// Check if number is between two values | ||
const isBetween = (value: number, floor: number, ceil: number) => | ||
value >= floor && value <= ceil; | ||
|
||
const useScrollspy = (ids: string[], offset = 0) => { | ||
const [activeId, setActiveId] = useState(""); | ||
|
||
useLayoutEffect(() => { | ||
const listener = () => { | ||
const scroll = window.scrollY; | ||
|
||
const position = ids | ||
.map((id) => { | ||
const element = document.getElementById(id); | ||
|
||
if (!element) return { id, top: -1, bottom: -1 }; | ||
|
||
const rect = element.getBoundingClientRect(); | ||
const top = clamp(rect.top + scroll - offset); | ||
const bottom = clamp(rect.bottom + scroll - offset); | ||
|
||
return { id, top, bottom }; | ||
}) | ||
.find(({ top, bottom }) => isBetween(scroll, top, bottom)); | ||
|
||
setActiveId(position?.id || ""); | ||
}; | ||
|
||
listener(); | ||
|
||
window.addEventListener("resize", listener); | ||
window.addEventListener("scroll", listener); | ||
|
||
return () => { | ||
window.removeEventListener("resize", listener); | ||
window.removeEventListener("scroll", listener); | ||
}; | ||
}, [ids, offset]); | ||
|
||
return activeId; | ||
}; | ||
|
||
export default useScrollspy; |