onboarding completed
This commit is contained in:
committed by
Carl-Gerhard Lindesvärd
parent
97627583ec
commit
7d22d2ddad
@@ -1,26 +1,40 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { BanIcon, InfoIcon } from 'lucide-react';
|
||||
|
||||
import { Input } from '../ui/input';
|
||||
import type { InputProps } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Tooltiper } from '../ui/tooltip';
|
||||
|
||||
type InputWithLabelProps = InputProps & {
|
||||
label: string;
|
||||
error?: string | undefined;
|
||||
info?: string;
|
||||
};
|
||||
|
||||
export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(
|
||||
({ label, className, ...props }, ref) => {
|
||||
({ label, className, info, ...props }, ref) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="mb-2 block flex justify-between">
|
||||
<Label className="mb-0" htmlFor={label}>
|
||||
<div className="mb-2 flex items-end justify-between">
|
||||
<Label
|
||||
className="mb-0 flex flex-1 shrink-0 items-center gap-1 whitespace-nowrap"
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
{info && (
|
||||
<Tooltiper content={info}>
|
||||
<InfoIcon size={14} />
|
||||
</Tooltiper>
|
||||
)}
|
||||
</Label>
|
||||
{props.error && (
|
||||
<span className="text-sm leading-none text-destructive">
|
||||
{props.error}
|
||||
</span>
|
||||
<Tooltiper asChild content={props.error}>
|
||||
<div className="flex items-center gap-1 text-sm leading-none text-destructive">
|
||||
Issues
|
||||
<BanIcon size={14} />
|
||||
</div>
|
||||
</Tooltiper>
|
||||
)}
|
||||
</div>
|
||||
<Input ref={ref} id={label} {...props} />
|
||||
|
||||
21
apps/dashboard/src/components/full-width-navbar.tsx
Normal file
21
apps/dashboard/src/components/full-width-navbar.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Logo } from './logo';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const FullWidthNavbar = ({ children, className }: Props) => {
|
||||
return (
|
||||
<div className={cn('border-b border-border bg-background', className)}>
|
||||
<div className="mx-auto flex h-14 w-full items-center justify-between px-4 md:max-w-[95vw] lg:max-w-[80vw]">
|
||||
<Logo />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FullWidthNavbar;
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import useWS from '@/hooks/useWS';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import useWebSocket from 'react-use-websocket';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useOverviewOptions } from '../useOverviewOptions';
|
||||
@@ -28,29 +28,21 @@ const FIFTEEN_SECONDS = 1000 * 15;
|
||||
|
||||
export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
|
||||
const { setLiveHistogram } = useOverviewOptions();
|
||||
const ws = String(process.env.NEXT_PUBLIC_API_URL)
|
||||
.replace(/^https/, 'wss')
|
||||
.replace(/^http/, 'ws');
|
||||
const client = useQueryClient();
|
||||
const [counter, setCounter] = useState(data);
|
||||
const [socketUrl] = useState(`${ws}/live/visitors/${projectId}`);
|
||||
const lastRefresh = useRef(Date.now());
|
||||
|
||||
useWebSocket(socketUrl, {
|
||||
shouldReconnect: () => true,
|
||||
onMessage(event) {
|
||||
const value = parseInt(event.data, 10);
|
||||
if (!isNaN(value)) {
|
||||
setCounter(value);
|
||||
if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) {
|
||||
lastRefresh.current = Date.now();
|
||||
toast('Refreshed data');
|
||||
client.refetchQueries({
|
||||
type: 'active',
|
||||
});
|
||||
}
|
||||
useWS<number>(`/live/visitors/${projectId}`, (value) => {
|
||||
if (!isNaN(value)) {
|
||||
setCounter(value);
|
||||
if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) {
|
||||
lastRefresh.current = Date.now();
|
||||
toast('Refreshed data');
|
||||
client.refetchQueries({
|
||||
type: 'active',
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ const createFlagIcon = (url: string) => {
|
||||
return function (_props: LucideProps) {
|
||||
return (
|
||||
<span
|
||||
className={`fi !block overflow-hidden rounded-[2px] !leading-[1rem] fi-${url}`}
|
||||
className={`fi fis !block overflow-hidden rounded-full !leading-[1rem] fi-${url}`}
|
||||
></span>
|
||||
);
|
||||
} as LucideIcon;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Tooltiper } from '@/components/ui/tooltip';
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
@@ -81,8 +82,10 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
}, [name]);
|
||||
|
||||
return Icon ? (
|
||||
<div className="[&_a]:![&_a]:!h-4 relative h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} />
|
||||
</div>
|
||||
<Tooltiper asChild content={name!}>
|
||||
<div className="[&_a]:![&_a]:!h-4 relative h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} />
|
||||
</div>
|
||||
</Tooltiper>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// prettier-ignore
|
||||
const data = {
|
||||
'chromium os': 'https://upload.wikimedia.org/wikipedia/commons/2/28/Chromium_Logo.svg',
|
||||
'mac os': 'https://upload.wikimedia.org/wikipedia/commons/c/c9/Finder_Icon_macOS_Big_Sur.png',
|
||||
'mac os': 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/MacOS_logo.svg/1200px-MacOS_logo.svg.png',
|
||||
'mobile safari': 'https://upload.wikimedia.org/wikipedia/commons/5/52/Safari_browser_logo.svg',
|
||||
'openpanel.dev': 'https://openpanel.dev',
|
||||
'samsung internet': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Samsung_Internet_logo.svg/1024px-Samsung_Internet_logo.svg.png',
|
||||
@@ -20,7 +20,7 @@ const data = {
|
||||
gmail: 'https://mail.google.com',
|
||||
google: 'https://google.com',
|
||||
instagram: 'https://instagram.com',
|
||||
ios: 'https://upload.wikimedia.org/wikipedia/commons/6/66/Apple_iOS_logo.svg',
|
||||
ios: 'https://cdn0.iconfinder.com/data/icons/flat-round-system/512/apple-1024.png',
|
||||
linkedin: 'https://linkedin.com',
|
||||
linux: 'https://upload.wikimedia.org/wikipedia/commons/3/35/Tux.svg',
|
||||
microlaunch: 'https://microlaunch.net',
|
||||
|
||||
18
apps/dashboard/src/components/sign-out-button.tsx
Normal file
18
apps/dashboard/src/components/sign-out-button.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { SignOutButton as ClerkSignOutButton } from '@clerk/nextjs';
|
||||
import { LogOutIcon } from 'lucide-react';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
|
||||
const SignOutButton = () => {
|
||||
return (
|
||||
<ClerkSignOutButton>
|
||||
<Button variant={'secondary'} icon={LogOutIcon}>
|
||||
Sign out
|
||||
</Button>
|
||||
</ClerkSignOutButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignOutButton;
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { clipboard } from '@/utils/clipboard';
|
||||
import { CopyIcon } from 'lucide-react';
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript';
|
||||
import docco from 'react-syntax-highlighter/dist/cjs/styles/hljs/docco';
|
||||
import docco from 'react-syntax-highlighter/dist/cjs/styles/hljs/vs2015';
|
||||
|
||||
SyntaxHighlighter.registerLanguage('typescript', ts);
|
||||
|
||||
@@ -12,8 +14,29 @@ interface SyntaxProps {
|
||||
|
||||
export default function Syntax({ code }: SyntaxProps) {
|
||||
return (
|
||||
<SyntaxHighlighter wrapLongLines style={docco}>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
<div className="group relative">
|
||||
<button
|
||||
className="absolute right-1 top-1 rounded bg-background p-2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
clipboard(code);
|
||||
}}
|
||||
>
|
||||
<CopyIcon size={12} />
|
||||
</button>
|
||||
<SyntaxHighlighter
|
||||
// wrapLongLines
|
||||
style={docco}
|
||||
language="html"
|
||||
customStyle={{
|
||||
borderRadius: '0.5rem',
|
||||
padding: '1rem',
|
||||
paddingTop: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex h-[20px] items-center rounded-full border px-1.5 text-[10px] font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
'inline-flex h-[20px] items-center rounded-full border px-1.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -15,7 +15,7 @@ const badgeVariants = cva(
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
'border-transparent bg-destructive-foreground text-destructive hover:bg-destructive/80',
|
||||
success:
|
||||
'border-transparent bg-emerald-500 text-emerald-100 hover:bg-emerald-500/80',
|
||||
outline: 'text-foreground',
|
||||
|
||||
68
apps/dashboard/src/components/ui/input-otp.tsx
Normal file
68
apps/dashboard/src/components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
import { Dot } from 'lucide-react';
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
||||
containerClassName
|
||||
)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
InputOTP.displayName = 'InputOTP';
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center', className)} {...props} />
|
||||
));
|
||||
InputOTPGroup.displayName = 'InputOTPGroup';
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]!;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
||||
isActive && 'z-10 ring-2 ring-ring ring-offset-background',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
InputOTPSlot.displayName = 'InputOTPSlot';
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
));
|
||||
InputOTPSeparator.displayName = 'InputOTPSeparator';
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
@@ -100,8 +100,7 @@ const SheetFooter = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
'sticky bottom-0 left-0 right-0 mt-auto bg-background',
|
||||
'mt-auto flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as SwitchPrimitives from '@radix-ui/react-switch';
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
const TooltipPortal = TooltipPrimitive.Portal;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
@@ -49,7 +50,9 @@ export function Tooltiper({
|
||||
<TooltipTrigger asChild={asChild} className={className}>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user