feat: Onboarding

This commit is contained in:
Wanjohi
2024-09-16 21:10:33 +03:00
parent 58e93b28e9
commit f2f3386bdb
8 changed files with 462 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))

View File

@@ -4,10 +4,11 @@ export * from "./fonts"
export * from "./input"
export * from "./home-nav-bar"
export * from "./game-card"
export * from "./team-counter"
export * from "./tooltip"
export * from "./footer"
export * from "./router-head"
export * from "./card"
export * from "./router-head"
export * from "./team-counter"
export * as auth from "./popup"
export * as Modal from "./modal"
export { default as Portal } from "./portal"

67
packages/ui/src/popup.ts Normal file
View File

@@ -0,0 +1,67 @@
export const openWindow = (url: string): Promise<void> => {
return new Promise((resolve) => {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
// Open in a new tab for mobile devices
const newTab = window.open(url, '_blank');
localStorage.removeItem("auth_success")
if (newTab) {
const pollTimer = setInterval(() => {
if (newTab.closed) {
clearInterval(pollTimer);
resolve();
return;
} else if (localStorage.getItem("auth_success")) {
clearInterval(pollTimer);
newTab.location.href = 'about:blank';
setTimeout(() => {
newTab.close();
window.focus();
//TODO: Navigate and start onboarding
}, 50);
localStorage.removeItem("auth_success")
resolve();
return;
}
}, 300);
} else {
resolve(); // Resolve if popup couldn't be opened
}
} else {
// Open in a popup for desktop devices
const width = 600;
const height = 600;
const top = window.top!.outerHeight / 2 + window.top!.screenY - (height / 2);
const left = window.top!.outerWidth / 2 + window.top!.screenX - (width / 2);
const popup = window.open(
url,
'Nestri Auth Popup',
`width=${width},height=${height},left=${left},top=${top},toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no,`
);
if (popup) {
const pollTimer = setInterval(() => {
if (popup.closed) {
clearInterval(pollTimer);
localStorage.removeItem("auth_success")
resolve();
return;
}
}, 300);
} else {
resolve(); // Resolve if popup couldn't be opened
}
}
});
};

View File

@@ -0,0 +1,147 @@
/** @jsxImportSource react */
import * as React from "react"
import { motion } from "framer-motion"
import { qwikify$ } from "@builder.io/qwik-react";
import { cn, buttonIcon, buttonVariants, type ButtonVariantProps, type ButtonVariantIconProps } from "@nestri/ui/design";
import {cloneElement,} from "./utils"
export interface ButtonRootProps extends React.HTMLAttributes<HTMLButtonElement | HTMLAnchorElement>, ButtonVariantProps {
class?: string;
children?: React.ReactElement;
onClick?: () => void | Promise<void>;
setIsLoading?: (v: boolean) => void;
isLoading: boolean;
disabled?: boolean;
loadingTime?: number;
href?: string;
}
export interface ButtonIconProps extends React.HTMLAttributes<HTMLElement>, ButtonVariantIconProps {
isLoading: boolean;
height?: number;
width?: number;
class?:string;
}
export interface ButtonLabelProps extends React.HTMLAttributes<HTMLElement>, ButtonVariantIconProps {
loadingText?: string;
class?: string;
isLoading: boolean;
}
export const Icon: React.FC<ButtonIconProps> = (({
class:className,
children,
size = "md",
isLoading,
type = "leading",
height = 20,
width = 20,
}) => {
if (isLoading) {
return (
<motion.svg
height={height}
width={width}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
// className="h-4 w-4 fill-white"
fill="currentColor"
animate={{ rotate: 1080 }}
transition={{
repeat: 1 / 0,
duration: 1,
ease: [.1, .21, .355, .68],
repeatType: "loop"
}}
className={buttonIcon({ size, type, className })}
>
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"></path>
<path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"></path>
</motion.svg>
)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (!isLoading && children) {
return cloneElement(children as React.ReactElement, buttonIcon({ size, type, className }))
}
})
export const Label = React.forwardRef<HTMLElement, ButtonLabelProps>(({
class:className,
children,
loadingText,
isLoading,
...props
}, forwardedRef) => {
return (
<span className={className} {...props} ref={forwardedRef}>{isLoading && loadingText ? loadingText : children}</span>
)
})
const Root = React.forwardRef<HTMLButtonElement & HTMLAnchorElement, ButtonRootProps>(({
class: className,
href,
disabled = false,
loadingTime,
intent = "primary",
variant = "solid",
size = "md",
isLoading,
setIsLoading,
onClick,
children,
...props }, forwardedRef) => {
const handleClick = async () => {
if (setIsLoading) {
if (!isLoading && !disabled) {
setIsLoading(true);
if (onClick) {
await onClick();
} else {
// Simulate an async operation
await new Promise(resolve => setTimeout(resolve, loadingTime));
}
setIsLoading(false);
}
}
};
const Component = href ? 'a' : 'button';
const iconOnly = React.Children.toArray(children).some(child =>
React.isValidElement(child) && child.type === Icon && child.props.type === 'only'
);
const buttonSize = iconOnly ? 'iconOnlyButtonSize' : 'size';
return (
<Component
ref={forwardedRef}
onClick={handleClick}
disabled={isLoading || disabled}
className={cn(buttonVariants[variant as keyof typeof buttonVariants]({ intent, [buttonSize]: size, className }))}
{...props}
>
{children}
</Component>
)
})
export type ButtonRoot = typeof Root;
export type ButtonIcon = typeof Icon;
export type ButtonLabel = typeof Label;
Root.displayName = 'Root';
Icon.displayName = "Icon";
Label.displayName = "Label";
export const Button = {
Root: qwikify$(Root),
Icon: qwikify$(Icon),
Label: qwikify$(Label)
}
//The ✔️ SVG is here:
{/* <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 shrink-0 fill-white"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1ZM17.2071 9.70711C17.5976 9.31658 17.5976 8.68342 17.2071 8.29289C16.8166 7.90237 16.1834 7.90237 15.7929 8.29289L10.5 13.5858L8.20711 11.2929C7.81658 10.9024 7.18342 10.9024 6.79289 11.2929C6.40237 11.6834 6.40237 12.3166 6.79289 12.7071L9.79289 15.7071C10.1834 16.0976 10.8166 16.0976 11.2071 15.7071L17.2071 9.70711Z"></path></svg> */ }

View File

@@ -1,7 +1,8 @@
export * from "./hero-section"
export * from "./react-example"
export * from "./cursor"
export * from "./title-section"
export * from "./button"
export * from "./cursor"
export * from "./motion"
export * from "./title"
export * from "./text"
export * from "./text"

View File

@@ -0,0 +1,15 @@
import React from "react"
import { twMerge } from "tailwind-merge"
/**
* Clone React element.
* The function clones React element and adds Tailwind CSS classnames to the cloned element
* @param element the React element to clone
* @param classNames Tailwind CSS classnames
* @returns { React.ReactElement } - Cloned React element
*/
export function cloneElement(element: React.ReactElement, classNames: string) {
return React.cloneElement(element, {
className: twMerge(element.props.className, classNames)
});
}