Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navbar mobile UI rework #181

Merged
merged 8 commits into from
Aug 27, 2024
3 changes: 1 addition & 2 deletions apps/web/src/app/(index)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ interface CardProps {
}

const LandingCard: FC<CardProps> = ({ heading, children, className, buttonText, buttonLink, disabled = false }) => {
console.log(heading, disabled);
return (
<Card className={cn("bg-card/60", className)}>
<CardHeader>
Expand Down Expand Up @@ -75,7 +74,7 @@ export default function Home() {
// console.log(queryClient)

return (
<div className="flex flex-wrap gap-3 items-center justify-between py-5">
<div className="flex flex-wrap gap-3 items-center justify-between">
<LandingCard
heading="Transactions"
buttonLink="/transactions"
Expand Down
64 changes: 37 additions & 27 deletions apps/web/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ import {
// } from "@/components/ui/dropdown-menu";
import { usePathname } from "next/navigation";

const RadiantLogoDark = ({ width, height, className } : { width: number, height: number, className?: string }) => {
const RadiantLogoDark = ({ className } : { className?: string }) => {
return (
<div className={className}>
<Image src={radiantLogoDark} alt="RadiantCommons.com Logo" width={width} height={height} priority/>
<Image src={radiantLogoDark} alt="RadiantCommons.com Logo" priority/>
</div>
);
};

const RadiantLogoLight = ({ width, height, className } : { width: number, height: number, className?: string }) => {
const RadiantLogoLight = ({ className } : { className?: string }) => {
return (
<div className={className}>
<Image src={radiantLogoLight} alt="RadiantCommons.com Logo" width={width} height={height} priority/>
<Image src={radiantLogoLight} alt="RadiantCommons.com Logo" priority/>
</div>
);
};
Expand All @@ -51,9 +51,7 @@ const RadiantLogoLight = ({ width, height, className } : { width: number, height
// TBQF I don't think this sort of error checking is necessary. If anything, it'll cause errors whenever new paths are updated.
// if (!segments.every(isBreadcrumbPath)) return null;

const Breadcrumbs = () => {
const pathName = usePathname();

const Breadcrumbs : FC<{ pathName: string }>= ({ pathName }) => {
// Don't show breadcrumbs if on index.
if (pathName === "/") return null;

Expand Down Expand Up @@ -129,31 +127,43 @@ const Breadcrumbs = () => {
}
};

export const Navbar : FC = () => {
export const Navbar: FC = () => {
const pathName = usePathname();
return (
<div className="flex flex-wrap justify-between items-center p-8 gap-2 max-w-[1400px] mx-auto">
<div className="flex-grow flex flex-wrap">
<Link href="https://radiantcommons.com" className="" >
<RadiantLogoDark height={48} width={48} className="dark:block hidden"/>
<RadiantLogoLight height={48} width={48} className="dark:hidden"/>
</Link>
<div className="flex items-center">
<h1 className={`font-semibold text-2xl ml-1 mr-3 ${workSans.className}`}><Link href="/">Cuiloa</Link></h1>
<div className="flex flex-wrap justify-between items-center px-4 py-8 sm:px-8 sm:py-16 sm:gap-2 gap-0 max-w-[1400px] mx-auto">
<div className="flex flex-wrap grow items-center sm:w-auto w-2/3">
<Link href="https://radiantcommons.com">
<RadiantLogoDark className="sm:w-12 sm:h-12 w-9 h-9 dark:block hidden" />
<RadiantLogoLight className="sm:w-12 sm:h-12 w-9 h-9 dark:hidden" />
</Link>
<h1
className={`font-semibold text-2xl ml-1 mr-3 ${workSans.className}`}
>
<Link href="/" className="hover:underline">
Cuiloa
</Link>
</h1>
{/* NOTE: the 5px of padding-top is to better align the smaller text with the text above, please keep it. */}
<p className={`text-link font-medium pt-[5px] ${workSans.className}`}>
<Link href="https://penumbra.zone/" className="">
A Block Explorer For Penumbra
<p
className={`sm:w-fit w-2/3 sm:basis-auto basis-full font-medium pt-[5px] ${workSans.className}`}
>
<Link
href="https://penumbra.zone/"
className="hover:underline text-link"
>
Block Explorer For Penumbra
</Link>
</p>
</div>
<div className="flex items-center gap-2 sm:w-auto mb-auto">
<SearchBar className="w-9 h-9 sm:w-40 md:w-56 lg:w-80 sm:h-11" />
<ThemeToggleButton className="w-9 sm:w-11 h-9 sm:h-11" />
</div>
{pathName !== "/" ? (
<div className="w-full h-5 pt-2 sm:pt-0">
<Breadcrumbs pathName={pathName} />
</div>
) : null}
</div>
<div className="flex items-center gap-2">
<SearchBar className="w-5/6 sm:max-w-40"/>
<ThemeToggleButton />
</div>
<div className="w-full h-5">
<Breadcrumbs />
</div>
</div>
);
};
83 changes: 70 additions & 13 deletions apps/web/src/components/Searchbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"use client";

import { type FC, useRef, useState, useEffect } from "react";
import { Command, CommandInput } from "../ui/command";
import { type FC, useRef, useState, useEffect, useCallback } from "react";
import { CommandInput, CommandDialog } from "../ui/command";
import { useToast } from "@/components/ui/use-toast";
import { usePathname, useRouter } from "next/navigation";
import { useOnClickOutside } from "usehooks-ts";
import { SearchValidator } from "@/lib/validators/search";
import { cn } from "@/lib/utils";
import { Button } from "../ui/button";
import { Search } from "lucide-react";
import { Input } from "../ui/input";

interface SearchProps {
className?: string;
Expand All @@ -18,6 +21,8 @@ const SearchBar : FC<SearchProps> = ({ className }) => {
const [input, setInput] = useState<string>("");
const cmdRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);

const [open, setOpen] = useState(false);
const { toast } = useToast();

useOnClickOutside(cmdRef, () => {
Expand All @@ -30,19 +35,70 @@ const SearchBar : FC<SearchProps> = ({ className }) => {
cmdRef.current?.blur();
}, [pathname]);

const searchCmd = () => {
router.push(`/search/${input}`);
};
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);

const search = useCallback((command: () => unknown) => {
setOpen(false);
command();
}, []);

return (
<Command
ref={cmdRef}
className={cn("relative rounded-full bg-popover border max-w-lg z-50 overflow-visible", className)}
shouldFilter={false}>
<div className={cn("", className)}>
<div className="hidden sm:inline-flex relative items-center rounded-full bg-popover border w-full z-50 overflow-visible pl-3 gap-2">
<Search className="w-5 h-5"/>
<Input
ref={inputRef}
className={"border-none px-0 mt-[2px] rounded-full focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:ring-transparent"}
placeholder="Search"
value={input}
onChange={(text) => {
console.log("text: ", text.currentTarget.value);
setInput(text.currentTarget.value);
}}
onKeyDown={(e) => {
// Aside: Now that this is just a single command input, maybe just convert this to a generic input box?
if (e.key === "Enter" && input.length !== 0) {
const searchQuery = SearchValidator.safeParse(input);
if (searchQuery.success) {
search(() => router.push(`/search/${searchQuery.data.value as string}`));
}
else {
toast({
variant: "destructive",
title: "Invalid search query.",
description: "Try again with a block height, hash hash, or IBC identifier.",
});
}
}
}}
/>
<kbd className="pointer-events-none hidden h-5 w-5 mr-3 select-none items-center bg-popover font-mono font-medium opacity-100 sm:flex text-xs text-muted-foreground/75">
⌘K
</kbd>
</div>
<Button
variant="outline"
size="icon"
className={cn(
"sm:hidden rounded-full border h-9 w-9 justify-center items-center bg-popover text-sm font-normal shadow-none",
)}
onClick={() => setOpen(true)}
>
<Search className="w-4 h-4"/>
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
className="text-sm"
ref={inputRef}
placeholder="Search..."
placeholder="Search for transactions, blocks, IBC data..."
value={input}
onValueChange={(text) => {
setInput(text);
Expand All @@ -52,7 +108,7 @@ const SearchBar : FC<SearchProps> = ({ className }) => {
if (e.key === "Enter" && input.length !== 0) {
const searchQuery = SearchValidator.safeParse(input);
if (searchQuery.success) {
searchCmd();
search(() => router.push(`/search/${searchQuery.data.value as string}`));
}
else {
toast({
Expand All @@ -64,7 +120,8 @@ const SearchBar : FC<SearchProps> = ({ className }) => {
}
}}
/>
</Command>
</CommandDialog>
</div>
);
};

Expand Down
14 changes: 10 additions & 4 deletions apps/web/src/components/ThemeToggleButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"use client";

import { useState, useEffect } from "react";
import { useState, useEffect, FC } from "react";
import { LeftPartialEclipse, RightPartialEclipse } from "./EclipseIcon";
import { useTheme } from "next-themes";
import { Button } from "../ui/button";
import { cn } from "@/lib/utils";

export const ThemeToggleButton = () => {

interface ThemeToggleProps {
className?: string;
}

export const ThemeToggleButton : FC<ThemeToggleProps> = ({ className }) => {
const { setTheme, theme } = useTheme();
const [mounted, setMounted] = useState(false);

Expand All @@ -18,8 +24,8 @@ export const ThemeToggleButton = () => {
const isLight = theme === "light";

return (
<Button className="rounded-full" variant="outline" size="icon" onClick={() => setTheme(isLight ? "dark" : "light")}>
{isLight ? <LeftPartialEclipse height={16} width={16}/>: <RightPartialEclipse height={16} width={16} />}
<Button className={cn("rounded-full", className)} variant="outline" size="icon" onClick={() => setTheme(isLight ? "dark" : "light")}>
{isLight ? <LeftPartialEclipse height={16} width={16}/> : <RightPartialEclipse height={16} width={16} />}
</Button>
);
};
25 changes: 25 additions & 0 deletions apps/web/src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react";

import { cn } from "@/lib/utils";

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";

export { Input };