From f2f3386bdbc691be4f9b6587ea6af0d0f0e7fca0 Mon Sep 17 00:00:00 2001 From: Wanjohi <71614375+wanjohiryan@users.noreply.github.com> Date: Mon, 16 Sep 2024 21:10:33 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Onboarding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routes/(onboarding)/new/[id]/index.tsx | 9 + .../www/src/routes/(onboarding)/new/index.tsx | 217 ++++++++++++++++++ packages/ui/src/design/utils.ts | 2 +- packages/ui/src/index.ts | 5 +- packages/ui/src/popup.ts | 67 ++++++ packages/ui/src/react/button.tsx | 147 ++++++++++++ packages/ui/src/react/index.ts | 5 +- packages/ui/src/react/utils.ts | 15 ++ 8 files changed, 462 insertions(+), 5 deletions(-) create mode 100644 apps/www/src/routes/(onboarding)/new/[id]/index.tsx create mode 100644 apps/www/src/routes/(onboarding)/new/index.tsx create mode 100644 packages/ui/src/popup.ts create mode 100644 packages/ui/src/react/button.tsx create mode 100644 packages/ui/src/react/utils.ts diff --git a/apps/www/src/routes/(onboarding)/new/[id]/index.tsx b/apps/www/src/routes/(onboarding)/new/[id]/index.tsx new file mode 100644 index 00000000..e1833959 --- /dev/null +++ b/apps/www/src/routes/(onboarding)/new/[id]/index.tsx @@ -0,0 +1,9 @@ +import { component$ } from "@builder.io/qwik"; +import { useLocation } from "@builder.io/qwik-city"; + +export default component$(() => { + const location = useLocation() + return ( +
{location.params.id}
+ ) +}) \ No newline at end of file diff --git a/apps/www/src/routes/(onboarding)/new/index.tsx b/apps/www/src/routes/(onboarding)/new/index.tsx new file mode 100644 index 00000000..867df30c --- /dev/null +++ b/apps/www/src/routes/(onboarding)/new/index.tsx @@ -0,0 +1,217 @@ +import { auth } from "@nestri/ui"; +import { Button } from "@nestri/ui/react" +import { buttonVariants } from "@nestri/ui/design"; +// import { type Provider } from "@supabase/supabase-js"; +import { Link, useLocation } from "@builder.io/qwik-city"; +import { $, component$, useSignal } from "@builder.io/qwik"; + +type AuthFlowProps = { + flow: string +} + +const flow: any = "join" + +export default component$(() => { + const isLoading = useSignal(false); + const isGHLoading = useSignal(false); + const setIsLoading = $((v: boolean) => isLoading.value = v) + const setIsGHLoading = $((v: boolean) => isGHLoading.value = v) + const location = useLocation(); + + // const authenticateUser = $(async (provider: any) => { + // await auth.openWindow(`${location.url.origin}/api/auth/${provider}`) + // }) + + return ( + <> +
+
+ {/* Nestri Logo */} +

+ {flow == "login" ? "Login" : "Join"} +

+ {flow == "join" ? ( +

+ Already have an account? + Login + +

+ ) : ( +

+ Don't have an account yet? + Join + +

+ )} +
+ {/* await authenticateUser("discord")} + size="md" + class="w-full gap-4 from-[#5865F2] to-[#4445e7] [--btn-border-color:#3836cc] dark:border-[#8093f9]/75"> + + + + + + + + Continue with Discord + + */} + + + + + + + + + + Link your Steam account + +
+ + + + + + + + + + Link your Epic Games account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Link your GOG.com account + +
+ + + {flow == "join" ? ( +

+ By creating an account, you agree to our
+ Terms of Service + and Privacy Policy + +

+ ) : ( +
+ )} +
+
+ + ) +}) \ No newline at end of file diff --git a/packages/ui/src/design/utils.ts b/packages/ui/src/design/utils.ts index 1a860ee3..1b07da65 100644 --- a/packages/ui/src/design/utils.ts +++ b/packages/ui/src/design/utils.ts @@ -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)) diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b22365c5..14b7afc0 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -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" \ No newline at end of file diff --git a/packages/ui/src/popup.ts b/packages/ui/src/popup.ts new file mode 100644 index 00000000..5e674b0b --- /dev/null +++ b/packages/ui/src/popup.ts @@ -0,0 +1,67 @@ +export const openWindow = (url: string): Promise => { + 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 + } + } + }); +}; \ No newline at end of file diff --git a/packages/ui/src/react/button.tsx b/packages/ui/src/react/button.tsx new file mode 100644 index 00000000..9a1e4efb --- /dev/null +++ b/packages/ui/src/react/button.tsx @@ -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, ButtonVariantProps { + class?: string; + children?: React.ReactElement; + onClick?: () => void | Promise; + setIsLoading?: (v: boolean) => void; + isLoading: boolean; + disabled?: boolean; + loadingTime?: number; + href?: string; +} + +export interface ButtonIconProps extends React.HTMLAttributes, ButtonVariantIconProps { + isLoading: boolean; + height?: number; + width?: number; + class?:string; +} + +export interface ButtonLabelProps extends React.HTMLAttributes, ButtonVariantIconProps { + loadingText?: string; + class?: string; + isLoading: boolean; +} + +export const Icon: React.FC = (({ + class:className, + children, + size = "md", + isLoading, + type = "leading", + height = 20, + width = 20, +}) => { + + if (isLoading) { + return ( + + + + + ) + // 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(({ + class:className, + children, + loadingText, + isLoading, + ...props +}, forwardedRef) => { + + return ( + {isLoading && loadingText ? loadingText : children} + ) +}) + +const Root = React.forwardRef(({ + 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 ( + + {children} + + ) +}) + +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: +{/* */ } \ No newline at end of file diff --git a/packages/ui/src/react/index.ts b/packages/ui/src/react/index.ts index ca85041b..25b296d0 100644 --- a/packages/ui/src/react/index.ts +++ b/packages/ui/src/react/index.ts @@ -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" \ No newline at end of file diff --git a/packages/ui/src/react/utils.ts b/packages/ui/src/react/utils.ts new file mode 100644 index 00000000..a8a273be --- /dev/null +++ b/packages/ui/src/react/utils.ts @@ -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) + }); + } \ No newline at end of file