From 73cdd0b1ac54444c58a37551a61b30a4c8f69438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Fri, 1 Nov 2024 18:29:55 -0300 Subject: [PATCH 1/8] chore: create identity start page --- src/assets/encrypted-data-illustration.svg | 13 +++++++ src/constants/routes.ts | 3 +- src/main.tsx | 9 +++-- src/pages/onboarding/CreateNewIdentity.tsx | 43 ++++++++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 src/assets/encrypted-data-illustration.svg create mode 100644 src/pages/onboarding/CreateNewIdentity.tsx diff --git a/src/assets/encrypted-data-illustration.svg b/src/assets/encrypted-data-illustration.svg new file mode 100644 index 0000000..c8032e7 --- /dev/null +++ b/src/assets/encrypted-data-illustration.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 4e940c9..2d948e8 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -2,7 +2,7 @@ const LOGIN = "/login"; const UNLOCK = "/unlock"; const RESTORE_WITH_PRIVATE_KEY = "/private-key"; const RESTORE_WITH_SEED_PHRASE = "/seed-phrase"; - +const CREATE_IDENTITY = "/create-identity"; const HOME = "/home"; export default { @@ -11,4 +11,5 @@ export default { HOME, RESTORE_WITH_PRIVATE_KEY, RESTORE_WITH_SEED_PHRASE, + CREATE_IDENTITY, }; diff --git a/src/main.tsx b/src/main.tsx index 1aff50c..6753deb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,15 +5,16 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import LanguageProvider from "./context/LanguageContext"; import DefaultLayout from "./layouts/Default"; -import CreateIdentity from "./pages/onboarding/CreateIdentity"; import ConnectCompany from "./pages/onboarding/ConnectCompany"; import ProfileInfo from "./pages/onboarding/ProfileInfo"; import Unlock from "./pages/Unlock"; import Login from "./pages/Login"; -import RecoverWithPrivateKey from "./pages/RecoverWithPrivateKey"; import RecoverWithSeedPhrase from "./pages/RecoverWithSeedPhrase"; import Home from "./pages/Home"; + +import CreateNewIdentity from "./pages/onboarding/CreateNewIdentity"; + import routes from "./constants/routes"; import "./index.css"; @@ -41,8 +42,8 @@ const router = createBrowserRouter([ element: , }, { - path: "/create-identity", - element: , + path: routes.CREATE_IDENTITY, + element: , }, { path: "/connect-company", diff --git a/src/pages/onboarding/CreateNewIdentity.tsx b/src/pages/onboarding/CreateNewIdentity.tsx new file mode 100644 index 0000000..d0503fc --- /dev/null +++ b/src/pages/onboarding/CreateNewIdentity.tsx @@ -0,0 +1,43 @@ +import { FormattedMessage } from "react-intl"; +import { Button } from "@/components/ui/button"; +import encryptedDataIllustration from "@/assets/encrypted-data-illustration.svg"; + +export default function CreateNewIdentity() { + return ( +
+
+
+

+ +

+ + + + +
+ + Encrypted data +
+ + +
+ ); +} From 19366f517669ca0d1dc7d15030ab7b709917775f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Wed, 6 Nov 2024 02:24:16 -0300 Subject: [PATCH 2/8] chore: add more onboarding components --- src/components/DocumentUpload.tsx | 51 +++++++++ src/components/ui/input.tsx | 83 ++++++++++---- src/constants/routes.ts | 6 + src/main.tsx | 18 +-- src/pages/onboarding/EmailVerification.tsx | 110 +++++++++++++++++++ src/pages/onboarding/OptionalInformation.tsx | 57 ++++++++++ src/pages/onboarding/RequiredInformation.tsx | 43 ++++++++ 7 files changed, 342 insertions(+), 26 deletions(-) create mode 100644 src/components/DocumentUpload.tsx create mode 100644 src/pages/onboarding/EmailVerification.tsx create mode 100644 src/pages/onboarding/OptionalInformation.tsx create mode 100644 src/pages/onboarding/RequiredInformation.tsx diff --git a/src/components/DocumentUpload.tsx b/src/components/DocumentUpload.tsx new file mode 100644 index 0000000..783461e --- /dev/null +++ b/src/components/DocumentUpload.tsx @@ -0,0 +1,51 @@ +import { forwardRef, useState } from "react"; +import { cn } from "@/lib/utils"; +import { UploadIcon } from "lucide-react"; + +export interface DocumentUploadProps + extends React.InputHTMLAttributes {} + +const DocumentUpload = forwardRef( + ({ className, id, ...props }, ref) => { + const [isFocused, setIsFocused] = useState(false); + + return ( +
+ setIsFocused(true)} + onBlur={() => setIsFocused(false)} + {...props} + /> + +
+
+ +
+
+ + Upload your identity document + + + PNG or JPG (Max. 5mb) + +
+
+
+ ); + } +); +DocumentUpload.displayName = "DocumentUpload"; + +export { DocumentUpload }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index a921025..ea070ba 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,25 +1,70 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" +import { forwardRef, useState } from "react"; +import { cn } from "@/lib/utils"; export interface InputProps - extends React.InputHTMLAttributes {} + extends React.InputHTMLAttributes { + label: string; + required?: boolean; +} + +const Input = forwardRef( + ({ className, type, label, required = false, id, ...props }, ref) => { + const [isFocused, setIsFocused] = useState(false); + const [hasValue, setHasValue] = useState(false); + + const handleBlur = (e: React.FocusEvent) => { + setIsFocused(false); + setHasValue(!!e.target.value); + }; + + const handleChange = (e: React.ChangeEvent) => { + setHasValue(!!e.target.value); + }; -const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { return ( - - ) +
+ setIsFocused(true)} + onBlur={handleBlur} + onChange={handleChange} + {...props} + /> + +
+ ); } -) -Input.displayName = "Input" +); +Input.displayName = "Input"; -export { Input } +export { Input }; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 2d948e8..d4afb9e 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -4,6 +4,9 @@ const RESTORE_WITH_PRIVATE_KEY = "/private-key"; const RESTORE_WITH_SEED_PHRASE = "/seed-phrase"; const CREATE_IDENTITY = "/create-identity"; const HOME = "/home"; +const REQUIRED_INFORMATION = "/required-information"; +const EMAIL_VERIFICATION = "/email-verification"; +const OPTIONAL_INFORMATION = "/optional-information"; export default { LOGIN, @@ -12,4 +15,7 @@ export default { RESTORE_WITH_PRIVATE_KEY, RESTORE_WITH_SEED_PHRASE, CREATE_IDENTITY, + REQUIRED_INFORMATION, + EMAIL_VERIFICATION, + OPTIONAL_INFORMATION, }; diff --git a/src/main.tsx b/src/main.tsx index 6753deb..0b3056d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,9 +5,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import LanguageProvider from "./context/LanguageContext"; import DefaultLayout from "./layouts/Default"; -import ConnectCompany from "./pages/onboarding/ConnectCompany"; -import ProfileInfo from "./pages/onboarding/ProfileInfo"; - +import RequiredInformation from "./pages/onboarding/RequiredInformation"; import Unlock from "./pages/Unlock"; import Login from "./pages/Login"; import RecoverWithSeedPhrase from "./pages/RecoverWithSeedPhrase"; @@ -19,6 +17,8 @@ import routes from "./constants/routes"; import "./index.css"; import "./styles/fonts.css"; +import EmailVerification from "./pages/onboarding/EmailVerification"; +import OptionalInformation from "./pages/onboarding/OptionalInformation"; const router = createBrowserRouter([ { @@ -46,12 +46,16 @@ const router = createBrowserRouter([ element: , }, { - path: "/connect-company", - element: , + path: routes.REQUIRED_INFORMATION, + element: , + }, + { + path: routes.EMAIL_VERIFICATION, + element: , }, { - path: "/profile-info", - element: , + path: routes.OPTIONAL_INFORMATION, + element: , }, ]); diff --git a/src/pages/onboarding/EmailVerification.tsx b/src/pages/onboarding/EmailVerification.tsx new file mode 100644 index 0000000..8877be5 --- /dev/null +++ b/src/pages/onboarding/EmailVerification.tsx @@ -0,0 +1,110 @@ +import { useRef } from "react"; +import { FormattedMessage } from "react-intl"; +import { Button } from "@/components/ui/button"; + +function CodeInputs() { + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + const handleInput = ( + e: React.ChangeEvent, + index: number + ) => { + const value = e.target.value; + + // Ensure only digits are entered + if (!/^\d*$/.test(value)) { + e.target.value = ""; // Clear any non-numeric input + return; + } + + if (value.length === 1 && index < inputRefs.current.length - 1) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = ( + e: React.KeyboardEvent, + index: number + ) => { + if (e.key === "Backspace" && e.currentTarget.value === "" && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + return ( +
+ {[...Array(5)].map((_, index) => ( + (inputRefs.current[index] = el)} + className="w-12 h-12 border border-[#1B0F0014] rounded-[8px] bg-elevation-200 text-text-300 text-3xl font-light text-center focus:outline-none" + style={{ fontSize: "28px" }} + onChange={(e) => handleInput(e, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + /> + ))} +
+ ); +} + +export default function EmailVerification() { + return ( +
+
+
+

+ +

+ + + + +
+ +
+ + + +
+
+ + +
+ ); +} diff --git a/src/pages/onboarding/OptionalInformation.tsx b/src/pages/onboarding/OptionalInformation.tsx new file mode 100644 index 0000000..2dac22e --- /dev/null +++ b/src/pages/onboarding/OptionalInformation.tsx @@ -0,0 +1,57 @@ +import { FormattedMessage } from "react-intl"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { DocumentUpload } from "@/components/DocumentUpload"; + +export default function OptionalInformation() { + return ( +
+
+
+

+ +

+ + + + +
+ +
+ + + +
+
+ +
+ + + +
+
+ ); +} diff --git a/src/pages/onboarding/RequiredInformation.tsx b/src/pages/onboarding/RequiredInformation.tsx new file mode 100644 index 0000000..2305e66 --- /dev/null +++ b/src/pages/onboarding/RequiredInformation.tsx @@ -0,0 +1,43 @@ +import { FormattedMessage } from "react-intl"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +export default function RequiredInformation() { + return ( +
+
+
+

+ +

+ + + + +
+ +
+ + + +
+
+ + +
+ ); +} From d1d760bad1acd8e32ce525569690d48e1de31eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Wed, 6 Nov 2024 10:14:30 -0300 Subject: [PATCH 3/8] chore: fix vercel deployment --- src/pages/onboarding/CreateIdentity.tsx | 367 --------------------- src/pages/onboarding/EmailVerification.tsx | 3 +- 2 files changed, 1 insertion(+), 369 deletions(-) delete mode 100644 src/pages/onboarding/CreateIdentity.tsx diff --git a/src/pages/onboarding/CreateIdentity.tsx b/src/pages/onboarding/CreateIdentity.tsx deleted file mode 100644 index 581cfac..0000000 --- a/src/pages/onboarding/CreateIdentity.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Input } from "@/components/ui/input"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, -} from "@/components/ui/form"; -import { Button } from "@/components/ui/button"; - -import defaultAvatar from "@/assets/default-avatar.svg"; -import { useState } from "react"; -import { Checkbox } from "@/components/ui/checkbox"; - -const createIdentityFormSchema = z.object({ - fullName: z.string().min(1, "Full name is required"), - dateOfBirth: z.object({ - day: z.string().regex(/^(0?[1-9]|[12][0-9]|3[01])$/, "Invalid day"), - month: z.string().regex(/^(0?[1-9]|1[0-2])$/, "Invalid month"), - year: z - .string() - .regex(/^\d{4}$/, "Invalid year") - .refine((val) => parseInt(val) <= new Date().getFullYear()), - }), - city: z.string().min(1), - country: z.string().min(1), - address: z.string().min(1), - email: z.string().email(), - taxId: z.string(), - - identityDocument: z - .instanceof(File) - .refine((file) => file.size < 5 * 1024 * 1024), -}); - -function CreateIdentityForm() { - const [isPreviewing, setIsPreviewing] = useState(false); - const [hasAgreedToTerms, setHasAgreedToTerms] = useState(false); - - const form = useForm>({ - resolver: zodResolver(createIdentityFormSchema), - defaultValues: { - fullName: "", - dateOfBirth: { - day: "", - month: "", - year: "", - }, - city: "", - country: "", - address: "", - email: "", - taxId: "", - }, - }); - - function onSubmit(values: z.infer) { - console.log(values); - - setIsPreviewing(true); - } - - return ( -
- -
- default-avatar -
- {isPreviewing ? "" : "Add photo"} -
-
- ( - - Full name - - {isPreviewing ? ( -
- {field.value} -
- ) : ( - - )} -
-
- )} - /> - -
- {isPreviewing ? ( -
-
Date of birth
-
- {form.watch("dateOfBirth.day")}- - {form.watch("dateOfBirth.month")}- - {form.watch("dateOfBirth.year")} -
-
- ) : ( - <> - ( - - Date - - - - - )} - /> - - ( - - - - - - )} - /> - - ( - - - - - - )} - /> - - )} -
- - ( - - - City of birth - - - {isPreviewing ? ( -
- {field.value} -
- ) : ( - - )} -
-
- )} - /> - - ( - - - Country of birth - - - {isPreviewing ? ( -
- {field.value} -
- ) : ( - - )} -
-
- )} - /> - - ( - - - Postal address - - - {isPreviewing ? ( -
- {field.value} -
- ) : ( - - )} -
-
- )} - /> - - ( - - Email - - {isPreviewing ? ( -
- {field.value} -
- ) : ( - - )} -
-
- )} - /> - - ( - - - Social security number - - - {isPreviewing ? ( -
- {field.value} -
- ) : ( - - )} -
-
- )} - /> - - ( - - Identity document - - {isPreviewing ? ( -
- {value.name} -
- ) : ( - - onChange(event.target.files && event.target.files[0]) - } - /> - )} -
-
- )} - /> - - {isPreviewing ? ( -
-
- setHasAgreedToTerms(!hasAgreedToTerms)} - /> - -
-
- - -
-
- ) : ( - - )} - - - ); -} - -function CreateIdentity() { - return ( -
-

Create Identity

-
- -
-
- ); -} - -export default CreateIdentity; diff --git a/src/pages/onboarding/EmailVerification.tsx b/src/pages/onboarding/EmailVerification.tsx index 8877be5..da1b754 100644 --- a/src/pages/onboarding/EmailVerification.tsx +++ b/src/pages/onboarding/EmailVerification.tsx @@ -11,9 +11,8 @@ function CodeInputs() { ) => { const value = e.target.value; - // Ensure only digits are entered if (!/^\d*$/.test(value)) { - e.target.value = ""; // Clear any non-numeric input + e.target.value = ""; return; } From 48852e21c69196107a54a758c8dad0ecc028ed56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Thu, 7 Nov 2024 23:28:33 -0300 Subject: [PATCH 4/8] chore: update select component layout --- .gitignore | 1 + src/components/DocumentUpload.tsx | 8 +- src/components/ui/select.tsx | 261 +++++++++++-------- src/pages/onboarding/OptionalInformation.tsx | 37 +++ 4 files changed, 201 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index a547bf3..de9ee66 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +coverage # Editor directories and files .vscode/* diff --git a/src/components/DocumentUpload.tsx b/src/components/DocumentUpload.tsx index 783461e..dfebf0d 100644 --- a/src/components/DocumentUpload.tsx +++ b/src/components/DocumentUpload.tsx @@ -1,6 +1,6 @@ import { forwardRef, useState } from "react"; -import { cn } from "@/lib/utils"; import { UploadIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; export interface DocumentUploadProps extends React.InputHTMLAttributes {} @@ -26,15 +26,15 @@ const DocumentUpload = forwardRef(
- +
- + Upload your identity document diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index fe56d4d..25c6a0b 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,148 +1,207 @@ -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { Check, ChevronDown, ChevronUp } from "lucide-react" +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; +import { cn } from "@/lib/utils"; -import { cn } from "@/lib/utils" +interface SelectProps { + children: React.ReactNode; + onValueChange?: (value: string) => void; +} + +const Select: React.FC = ({ children, onValueChange }) => { + const [hasValue, setHasValue] = React.useState(false); + const [triggerWidth, setTriggerWidth] = React.useState(null); + const [isOpen, setIsOpen] = React.useState(false); -const Select = SelectPrimitive.Root + const handleValueChange = (value: string) => { + setHasValue(!!value); + if (onValueChange) onValueChange(value); + }; -const SelectGroup = SelectPrimitive.Group + return ( + + {React.Children.map(children, (child) => { + if (React.isValidElement(child) && child.type === SelectTrigger) { + return React.cloneElement(child as React.ReactElement, { + hasValue, + setTriggerWidth, + isOpen, + }); + } + if (React.isValidElement(child) && child.type === SelectContent) { + return React.cloneElement(child as React.ReactElement, { + triggerWidth, + }); + } + return child; + })} + + ); +}; -const SelectValue = SelectPrimitive.Value +interface SelectTriggerProps + extends React.ComponentPropsWithoutRef { + label: string; + hasValue?: boolean; + setTriggerWidth?: (width: number) => void; + isOpen?: boolean; +} const SelectTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - span]:line-clamp-1", - className - )} - {...props} - > - {children} - - - - -)) -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName - -const SelectScrollUpButton = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName - -const SelectScrollDownButton = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName + SelectTriggerProps +>(({ className, label, hasValue, setTriggerWidth, isOpen, ...props }, ref) => { + const triggerRef = React.useRef(null); + + React.useEffect(() => { + if (triggerRef.current && setTriggerWidth) { + setTriggerWidth(triggerRef.current.offsetWidth); + } + }, [setTriggerWidth]); + + return ( +
+ { + if (typeof ref === "function") ref(node); + else if (ref) + (ref as React.MutableRefObject).current = + node; + triggerRef.current = node; + }} + className={cn( + "flex h-[58px] w-full items-center justify-between rounded-[8px] border border-[#1B0F0014] bg-elevation-200 px-4 text-sm transition-all duration-200 ease-in-out outline-none", + hasValue ? "pt-6 pb-2" : "pt-5 pb-3", + className + )} + {...props} + > +
+ +
+
+ + {isOpen ? ( + + ) : ( + + )} + +
+
+ +
+ ); +}); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +interface SelectContentProps + extends React.ComponentPropsWithoutRef { + triggerWidth?: number | null; +} const SelectContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, position = "popper", ...props }, ref) => ( - - - - ( + ( + { className, children, triggerWidth, position = "popper", ...props }, + ref + ) => ( + + - {children} - - - - -)) -SelectContent.displayName = SelectPrimitive.Content.displayName + + {children} + + + + ) +); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectGroup = SelectPrimitive.Group; +const SelectValue = SelectPrimitive.Value; + +interface SelectLabelProps + extends React.ComponentPropsWithoutRef {} const SelectLabel = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef + SelectLabelProps >(({ className, ...props }, ref) => ( -)) -SelectLabel.displayName = SelectPrimitive.Label.displayName +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +interface SelectItemProps + extends React.ComponentPropsWithoutRef {} const SelectItem = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef + SelectItemProps >(({ className, children, ...props }, ref) => ( - + {children} + - + - - {children} -)) -SelectItem.displayName = SelectPrimitive.Item.displayName +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +interface SelectSeparatorProps + extends React.ComponentPropsWithoutRef {} const SelectSeparator = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef + SelectSeparatorProps >(({ className, ...props }, ref) => ( -)) -SelectSeparator.displayName = SelectPrimitive.Separator.displayName +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, @@ -153,6 +212,4 @@ export { SelectLabel, SelectItem, SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, -} +}; diff --git a/src/pages/onboarding/OptionalInformation.tsx b/src/pages/onboarding/OptionalInformation.tsx index 2dac22e..15d6fe2 100644 --- a/src/pages/onboarding/OptionalInformation.tsx +++ b/src/pages/onboarding/OptionalInformation.tsx @@ -2,6 +2,14 @@ import { FormattedMessage } from "react-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { DocumentUpload } from "@/components/DocumentUpload"; +import { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectItem, +} from "@/components/ui/select"; export default function OptionalInformation() { return ( @@ -28,6 +36,35 @@ export default function OptionalInformation() {
+
From e48e316b9992cefbb41711315daa4a93bf5962c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Thu, 7 Nov 2024 23:35:09 -0300 Subject: [PATCH 5/8] chore: fix component typedefs --- src/components/ui/select.tsx | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 25c6a0b..7db0aeb 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -66,11 +66,11 @@ const SelectTrigger = React.forwardRef<
{ - if (typeof ref === "function") ref(node); - else if (ref) - (ref as React.MutableRefObject).current = - node; + // @ts-expect-error This is a valid mutable property triggerRef.current = node; + + if (typeof ref === "function") ref(node); + else if (ref) ref.current = node; }} className={cn( "flex h-[58px] w-full items-center justify-between rounded-[8px] border border-[#1B0F0014] bg-elevation-200 px-4 text-sm transition-all duration-200 ease-in-out outline-none", @@ -146,8 +146,9 @@ SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; -interface SelectLabelProps - extends React.ComponentPropsWithoutRef {} +type SelectLabelProps = React.ComponentPropsWithoutRef< + typeof SelectPrimitive.Label +>; const SelectLabel = React.forwardRef< React.ElementRef, @@ -161,8 +162,9 @@ const SelectLabel = React.forwardRef< )); SelectLabel.displayName = SelectPrimitive.Label.displayName; -interface SelectItemProps - extends React.ComponentPropsWithoutRef {} +type SelectItemProps = React.ComponentPropsWithoutRef< + typeof SelectPrimitive.Item +>; const SelectItem = React.forwardRef< React.ElementRef, @@ -171,7 +173,7 @@ const SelectItem = React.forwardRef< {} +type SelectSeparatorProps = React.ComponentPropsWithoutRef< + typeof SelectPrimitive.Separator +>; const SelectSeparator = React.forwardRef< React.ElementRef, From 7c54faa8f98fbf6196fcef0d764a6ef9a310d034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Fri, 8 Nov 2024 00:17:18 -0300 Subject: [PATCH 6/8] chore: fix upload component behavior --- src/components/DocumentUpload.tsx | 153 +++++++++++++++++++++++------- src/utils/index.ts | 25 +++++ 2 files changed, 145 insertions(+), 33 deletions(-) diff --git a/src/components/DocumentUpload.tsx b/src/components/DocumentUpload.tsx index dfebf0d..68291bd 100644 --- a/src/components/DocumentUpload.tsx +++ b/src/components/DocumentUpload.tsx @@ -1,51 +1,138 @@ import { forwardRef, useState } from "react"; -import { UploadIcon } from "lucide-react"; +import { UploadIcon, CheckCircle, Trash2, Trash } from "lucide-react"; import { cn } from "@/lib/utils"; +import { formatFileSize, truncateFileName } from "@/utils"; -export interface DocumentUploadProps - extends React.InputHTMLAttributes {} +export type DocumentUploadProps = React.InputHTMLAttributes; const DocumentUpload = forwardRef( ({ className, id, ...props }, ref) => { const [isFocused, setIsFocused] = useState(false); + const [fileName, setFileName] = useState(null); + const [fileSize, setFileSize] = useState(null); + const [progress, setProgress] = useState(0); + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + setFileName(truncateFileName(file.name)); + setFileSize(file.size); + setProgress(0); + uploadFile(file); + } + }; + + const clearFileSelection = () => { + setFileName(null); + setFileSize(null); + setProgress(0); + if (ref && typeof ref === "object" && ref.current) { + ref.current.value = ""; + } + }; + + const uploadFile = async (file: File) => { + const formData = new FormData(); + formData.append("file", file); + + // todo: replace the endpoint with the actual upload endpoint + const response = await fetch("/upload-endpoint", { + method: "POST", + body: formData, + }); + + if (response.ok) { + console.log("Upload complete"); + setProgress(100); + } else { + console.error("Upload failed"); + } + }; return ( -
- setIsFocused(true)} - onBlur={() => setIsFocused(false)} - {...props} - /> - -
-
- -
-
- - Upload your identity document - - - PNG or JPG (Max. 5mb) - +
+
+ setIsFocused(true)} + onBlur={() => setIsFocused(false)} + onChange={handleFileChange} + {...props} + /> + +
+
+ +
+
+ + Upload your identity document + + + PNG or JPG (Max. 5mb) + +
+ + {fileName && ( +
+
+
+
+ + {fileName} + + + + {formatFileSize(fileSize || 0)} + +
+ + +
+ + +
+ +
+
+
+
+ {progress}% +
+
+ )}
); } ); + DocumentUpload.displayName = "DocumentUpload"; export { DocumentUpload }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 99ff25b..0497682 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,3 +11,28 @@ export const detectBrowserLanguage = () => { return language; }; + +export const formatFileSize = (size: number) => { + if (size < 1024) return `${size} B`; + + if (size < 1048576) return `${(size / 1024).toFixed(2)} KB`; + + return `${(size / 1048576).toFixed(2)} MB`; +}; + +export const truncateFileName = (name: string) => { + const maxNameLength = 20; + const extensionIndex = name.lastIndexOf("."); + + if (extensionIndex === -1) + return name.length > maxNameLength + ? `${name.slice(0, 5)}...${name.slice(-3)}` + : name; + + const extension = name.slice(extensionIndex); + const baseName = name.slice(0, extensionIndex); + + return baseName.length > maxNameLength + ? `${baseName.slice(0, 5)}...${baseName.slice(-3)}${extension}` + : name; +}; From f31eff631a84903df7ca4f704f1daf7d19fabd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Fri, 8 Nov 2024 08:31:08 -0300 Subject: [PATCH 7/8] wip --- src/components/DocumentUpload.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DocumentUpload.tsx b/src/components/DocumentUpload.tsx index 68291bd..1dadd99 100644 --- a/src/components/DocumentUpload.tsx +++ b/src/components/DocumentUpload.tsx @@ -1,5 +1,5 @@ import { forwardRef, useState } from "react"; -import { UploadIcon, CheckCircle, Trash2, Trash } from "lucide-react"; +import { UploadIcon, CheckCircle, Trash } from "lucide-react"; import { cn } from "@/lib/utils"; import { formatFileSize, truncateFileName } from "@/utils"; From 0e6ca126c0a2bcd277004a7cecd1e9b9a1ac77b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Salom=C3=A3o?= Date: Fri, 8 Nov 2024 11:59:11 -0300 Subject: [PATCH 8/8] chore: add navigation between screens --- src/assets/success-illustration.svg | 5 + src/main.tsx | 10 + src/pages/onboarding/ConfirmIdentity.tsx | 186 +++++++++++++++++++ src/pages/onboarding/CreateNewIdentity.tsx | 10 +- src/pages/onboarding/EmailVerification.tsx | 18 +- src/pages/onboarding/OptionalInformation.tsx | 11 +- src/pages/onboarding/RequiredInformation.tsx | 10 +- src/pages/onboarding/Success.tsx | 43 +++++ 8 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 src/assets/success-illustration.svg create mode 100644 src/pages/onboarding/ConfirmIdentity.tsx create mode 100644 src/pages/onboarding/Success.tsx diff --git a/src/assets/success-illustration.svg b/src/assets/success-illustration.svg new file mode 100644 index 0000000..7f2d309 --- /dev/null +++ b/src/assets/success-illustration.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main.tsx b/src/main.tsx index 0b3056d..938e3cf 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,6 +12,7 @@ import RecoverWithSeedPhrase from "./pages/RecoverWithSeedPhrase"; import Home from "./pages/Home"; import CreateNewIdentity from "./pages/onboarding/CreateNewIdentity"; +import Success from "./pages/onboarding/Success"; import routes from "./constants/routes"; @@ -19,6 +20,7 @@ import "./index.css"; import "./styles/fonts.css"; import EmailVerification from "./pages/onboarding/EmailVerification"; import OptionalInformation from "./pages/onboarding/OptionalInformation"; +import ConfirmIdentity from "./pages/onboarding/ConfirmIdentity"; const router = createBrowserRouter([ { @@ -57,6 +59,14 @@ const router = createBrowserRouter([ path: routes.OPTIONAL_INFORMATION, element: , }, + { + path: "/success", + element: , + }, + { + path: "/confirm-identity", + element: , + }, ]); createRoot(document.getElementById("root")!).render( diff --git a/src/pages/onboarding/ConfirmIdentity.tsx b/src/pages/onboarding/ConfirmIdentity.tsx new file mode 100644 index 0000000..192b3e5 --- /dev/null +++ b/src/pages/onboarding/ConfirmIdentity.tsx @@ -0,0 +1,186 @@ +import { useNavigate } from "react-router-dom"; +import { FormattedMessage } from "react-intl"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; + +function RequiredInformation() { + return ( +
+
+ + + +
+ +
+
+ + + + John Doe +
+ + + +
+ + + + + example@bit.cr + +
+ + + +
+ + + + + The One Street 86, 11490, Nowhere + +
+
+
+ ); +} + +function OptionalInformation() { + return ( +
+
+ + + +
+ +
+
+ + + + - +
+ + + +
+ + + + - +
+ + + +
+ + + + - +
+ + + +
+ + + + - +
+ +
+ + + + - +
+
+
+ ); +} + +export default function ConfirmIdentity() { + const navigate = useNavigate(); + + const signToConfirm = () => navigate("/success"); + + return ( +
+
+

+ +

+ + + + +
+ +
+ + +
+ + +
+ ); +} diff --git a/src/pages/onboarding/CreateNewIdentity.tsx b/src/pages/onboarding/CreateNewIdentity.tsx index d0503fc..cc17247 100644 --- a/src/pages/onboarding/CreateNewIdentity.tsx +++ b/src/pages/onboarding/CreateNewIdentity.tsx @@ -1,8 +1,13 @@ +import { useNavigate } from "react-router-dom"; import { FormattedMessage } from "react-intl"; import { Button } from "@/components/ui/button"; import encryptedDataIllustration from "@/assets/encrypted-data-illustration.svg"; export default function CreateNewIdentity() { + const navigate = useNavigate(); + + const startCreatingIdentity = () => navigate("/required-information"); + return (
@@ -31,7 +36,10 @@ export default function CreateNewIdentity() { />
- + +
- +
+ ); +}