mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
✨ feat: Onboarding
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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
67
packages/ui/src/popup.ts
Normal 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
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
147
packages/ui/src/react/button.tsx
Normal file
147
packages/ui/src/react/button.tsx
Normal 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> */ }
|
||||
@@ -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"
|
||||
15
packages/ui/src/react/utils.ts
Normal file
15
packages/ui/src/react/utils.ts
Normal 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)
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user