add peer details

This commit is contained in:
Eduard Gert
2026-05-27 14:52:19 +02:00
parent 004a305e46
commit 0e83d2ad94
35 changed files with 1549 additions and 345 deletions
@@ -1,9 +1,13 @@
import { ComponentType, forwardRef, ReactNode } from "react";
import { motion, HTMLMotionProps } from "framer-motion";
import {
ButtonHTMLAttributes,
ComponentType,
forwardRef,
ReactNode,
} from "react";
import { LucideProps } from "lucide-react";
import { cn } from "@/lib/cn";
type Props = HTMLMotionProps<"button"> & {
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
icon?: ComponentType<LucideProps>;
iconNode?: ReactNode;
title: string;
@@ -28,10 +32,9 @@ export const CardNavItem = forwardRef<HTMLButtonElement, Props>(
ref,
) {
return (
<motion.button
<button
ref={ref}
type={type}
whileTap={{ scale: 0.98 }}
className={cn(
"w-full flex items-center gap-3 p-1.5 rounded-lg cursor-default outline-none text-left",
"transition-colors duration-150",
@@ -77,7 +80,7 @@ export const CardNavItem = forwardRef<HTMLButtonElement, Props>(
</p>
)}
</div>
</motion.button>
</button>
);
},
);
@@ -8,6 +8,7 @@ type CopyToClipboardProps = {
size?: number;
iconAlignment?: "left" | "right";
className?: string;
iconClassName?: string;
alwaysShowIcon?: boolean;
};
@@ -17,6 +18,7 @@ export const CopyToClipboard = ({
size = 10,
iconAlignment = "right",
className,
iconClassName,
alwaysShowIcon = false,
}: CopyToClipboardProps) => {
const wrapperRef = useRef<HTMLDivElement>(null);
@@ -57,6 +59,7 @@ export const CopyToClipboard = ({
className={cn(
"shrink-0 inline-flex relative top-[2px] right-[1px]",
iconAlignment === "left" ? "order-first" : "order-last",
iconClassName,
)}
>
<Check
@@ -0,0 +1,70 @@
import { ComponentType } from "react";
import { useTranslation } from "react-i18next";
import { Browser } from "@wailsio/runtime";
import { ExternalLinkIcon, LucideProps } from "lucide-react";
import { cn } from "@/lib/cn";
import { SquareIcon } from "./SquareIcon";
type Props = {
icon: ComponentType<LucideProps>;
title: string;
description?: string;
learnMoreUrl?: string;
learnMoreTopic?: string;
className?: string;
};
const openUrl = (url: string) => {
void Browser.OpenURL(url).catch(() => window.open(url, "_blank"));
};
export const EmptyState = ({
icon,
title,
description,
learnMoreUrl,
learnMoreTopic,
className,
}: Props) => {
const { t } = useTranslation();
return (
<div className={cn("py-12 text-center", className)}>
<div
className={
"flex flex-col items-center justify-center max-w-sm mx-auto"
}
>
<SquareIcon icon={icon} className={"mb-3"} />
<p className={"text-base font-semibold text-nb-gray-200 mb-1"}>
{title}
</p>
{description && (
<p className={"text-sm text-nb-gray-350"}>{description}</p>
)}
{learnMoreUrl && learnMoreTopic && (
<p className={"text-sm text-nb-gray-350"}>
{t("common.learnMoreAbout")}{" "}
<a
href={learnMoreUrl}
onClick={(e) => {
e.preventDefault();
openUrl(learnMoreUrl);
}}
className={cn(
"text-netbird hover:underline underline-offset-4",
"cursor-pointer wails-no-draggable",
"inline-flex items-center gap-1",
)}
>
{learnMoreTopic}
<ExternalLinkIcon
size={12}
className={"shrink-0"}
/>
</a>
</p>
)}
</div>
</div>
);
};
@@ -0,0 +1,34 @@
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import * as React from "react";
import { cn } from "@/lib/cn";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardPortal = HoverCardPrimitive.Portal;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "start", sideOffset = 6, ...props }, ref) => (
<HoverCardPrimitive.Portal>
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-80 rounded-lg border border-nb-gray-900 bg-nb-gray-935",
"p-3 text-nb-gray-200 shadow-lg outline-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger };
@@ -0,0 +1,25 @@
import { ComponentType } from "react";
import { FunnelXIcon, LucideProps } from "lucide-react";
import { useTranslation } from "react-i18next";
import { EmptyState } from "./EmptyState";
type Props = {
icon?: ComponentType<LucideProps>;
title?: string;
description?: string;
};
export const NoResults = ({
icon = FunnelXIcon,
title,
description,
}: Props) => {
const { t } = useTranslation();
return (
<EmptyState
icon={icon}
title={title ?? t("common.noResults.title")}
description={description ?? t("common.noResults.description")}
/>
);
};
@@ -0,0 +1,20 @@
import { UnplugIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { EmptyState } from "./EmptyState";
export const NotConnectedState = () => {
const { t } = useTranslation();
return (
<div
className={
"h-full min-h-[260px] flex items-center justify-center px-6"
}
>
<EmptyState
icon={UnplugIcon}
title={t("notConnected.title")}
description={t("notConnected.description")}
/>
</div>
);
};
@@ -1,15 +1,24 @@
import { forwardRef, InputHTMLAttributes } from "react";
import { forwardRef, InputHTMLAttributes, ReactNode } from "react";
import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/cn";
type Props = InputHTMLAttributes<HTMLInputElement> & {
iconSize?: number;
shortcut?: ReactNode;
};
export const SearchInput = forwardRef<HTMLInputElement, Props>(
function SearchInput({ iconSize = 16, className, ...props }, ref) {
function SearchInput(
{ iconSize = 16, className, disabled, shortcut, ...props },
ref,
) {
return (
<div className={"flex items-center gap-2 px-1 h-10"}>
<div
className={cn(
"flex items-center gap-2 px-1 h-10",
disabled && "opacity-50",
)}
>
<SearchIcon
size={iconSize}
className={"text-nb-gray-300 shrink-0"}
@@ -17,13 +26,29 @@ export const SearchInput = forwardRef<HTMLInputElement, Props>(
<input
ref={ref}
type={"text"}
disabled={disabled}
{...props}
className={cn(
"w-full bg-transparent text-sm text-nb-gray-200 placeholder:text-nb-gray-400",
"outline-none border-none",
disabled && "cursor-not-allowed",
className,
)}
/>
{shortcut && (
<span
className={cn(
"shrink-0 select-none",
"inline-flex items-center justify-center",
"h-5 min-w-[20px] px-1.5 rounded",
"border border-nb-gray-850 bg-nb-gray-920",
"text-[10px] font-medium text-nb-gray-400",
"wails-no-draggable",
)}
>
{shortcut}
</span>
)}
</div>
);
},
@@ -14,7 +14,7 @@ type SquareIconProps = {
export const SquareIcon = ({ icon: Icon, iconSize = 20, className }: SquareIconProps) => (
<div
className={cn(
"h-11 w-11 rounded-xl flex items-center justify-center bg-nb-gray-920 border border-nb-gray-900 text-white",
"h-11 w-11 rounded-lg flex items-center justify-center bg-nb-gray-920 border border-nb-gray-900 text-white",
className,
)}
>
@@ -5,6 +5,7 @@ import { cn } from "@/lib/cn";
type SwitchItemGroupContextValue = {
value: string;
layoutId: string;
disabled: boolean;
};
const SwitchItemGroupContext = createContext<SwitchItemGroupContextValue | null>(null);
@@ -22,18 +23,27 @@ type Props = {
onChange: (value: string) => void;
children: ReactNode;
className?: string;
disabled?: boolean;
};
export const SwitchItemGroup = ({ value, onChange, children, className }: Props) => {
export const SwitchItemGroup = ({
value,
onChange,
children,
className,
disabled = false,
}: Props) => {
const layoutId = useId();
return (
<SwitchItemGroupContext.Provider value={{ value, layoutId }}>
<SwitchItemGroupContext.Provider value={{ value, layoutId, disabled }}>
<RadioGroup.Root
value={value}
onValueChange={onChange}
disabled={disabled}
className={cn(
"flex shrink-0 rounded-lg border border-nb-gray-850 bg-nb-gray-910 p-1 overflow-hidden",
disabled && "opacity-50 pointer-events-none",
className,
)}
>