├── public
├── favicon.ico
├── assets
│ ├── arcade.png
│ ├── success.png
│ ├── game-console.png
│ ├── online-gaming.png
│ └── screenshot-form.webp
├── vercel.svg
├── thirteen.svg
└── next.svg
├── postcss.config.js
├── .vscode
└── settings.json
├── styles
└── globals.css
├── next.config.js
├── lib
└── utils.ts
├── .eslintrc.json
├── app
├── head.tsx
├── layout.tsx
└── page.tsx
├── pages
└── api
│ └── hello.ts
├── .gitignore
├── components
├── ui
│ ├── label.tsx
│ ├── separator.tsx
│ ├── checkbox.tsx
│ ├── input.tsx
│ ├── switch.tsx
│ ├── radio-group.tsx
│ └── button.tsx
├── FormWrapper.tsx
├── SuccessMessage.tsx
├── AddonsForm.tsx
├── UserInfoForm.tsx
├── FinalStep.tsx
├── SideBar.tsx
└── PlanForm.tsx
├── tsconfig.json
├── hooks
└── useMultiplestepForm.ts
├── package.json
├── README.md
└── tailwind.config.js
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marcosfitzsimons/multi-step-form/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/assets/arcade.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marcosfitzsimons/multi-step-form/HEAD/public/assets/arcade.png
--------------------------------------------------------------------------------
/public/assets/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marcosfitzsimons/multi-step-form/HEAD/public/assets/success.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/game-console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marcosfitzsimons/multi-step-form/HEAD/public/assets/game-console.png
--------------------------------------------------------------------------------
/public/assets/online-gaming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marcosfitzsimons/multi-step-form/HEAD/public/assets/online-gaming.png
--------------------------------------------------------------------------------
/public/assets/screenshot-form.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marcosfitzsimons/multi-step-form/HEAD/public/assets/screenshot-form.webp
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules\\typescript\\lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 |
4 | .border {
5 | background-clip: padding-box;
6 | }
7 |
8 | @tailwind utilities;
9 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | appDir: true,
5 | },
6 | }
7 |
8 | module.exports = nextConfig
9 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "extends": ["next/core-web-vitals", ""],
4 | "plugins": ["tailwindcss"],
5 | "settings": {
6 | "tailwindcss": {
7 | "callees": ["cn"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/head.tsx:
--------------------------------------------------------------------------------
1 | export default function Head() {
2 | return (
3 | <>
4 |
Multi Step Form
5 |
6 |
7 |
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' })
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Label = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Label.displayName = LabelPrimitive.Root.displayName
22 |
23 | export { Label }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": ["./*"]
25 | }
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Ubuntu } from "@next/font/google";
2 |
3 | import "../styles/globals.css";
4 |
5 | const ubuntu = Ubuntu({
6 | variable: "--font-ubuntu",
7 | weight: ["400", "500", "700"],
8 | subsets: ["latin"],
9 | display: "swap",
10 | });
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
22 | {/*
23 | will contain the components returned by the nearest parent
24 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
25 | */}
26 |
27 |
28 | {children}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/hooks/useMultiplestepForm.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 |
5 | export function useMultiplestepForm(steps: number) {
6 | const [currentStepIndex, setCurrentStepIndex] = useState(0)
7 | const [showSuccessMsg, setShowSuccessMsg] = useState(false);
8 |
9 | const nextStep = () => {
10 | if (currentStepIndex < steps - 1) {
11 | setCurrentStepIndex((i) => i + 1)
12 | }
13 | if (currentStepIndex === 3) {
14 | setShowSuccessMsg(true)
15 | }
16 | }
17 |
18 | const previousStep = () => {
19 | if (currentStepIndex > 0) {
20 | setCurrentStepIndex((i) => i - 1)
21 | }
22 | }
23 |
24 | const goTo = (index: number) => {
25 | setCurrentStepIndex(index)
26 | }
27 |
28 | return {
29 | currentStepIndex,
30 | steps,
31 | isFirstStep: currentStepIndex === 0,
32 | isLastStep: currentStepIndex === steps - 1,
33 | showSuccessMsg,
34 | goTo,
35 | nextStep,
36 | previousStep
37 | }
38 | }
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/FormWrapper.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import { motion } from "framer-motion";
3 |
4 | type FormWrapperProps = {
5 | title: string;
6 | description: string;
7 | children: ReactNode;
8 | };
9 |
10 | const formVariants = {
11 | hidden: {
12 | opacity: 0,
13 | x: -50,
14 | },
15 | visible: {
16 | opacity: 1,
17 | x: 0,
18 | },
19 | exit: {
20 | opacity: 0,
21 | x: 50,
22 | transition: {
23 | ease: "easeOut",
24 | },
25 | },
26 | };
27 |
28 | const FormWrapper = ({ title, description, children }: FormWrapperProps) => {
29 | return (
30 |
37 |
38 |
39 | {title}
40 |
41 |
{description}
42 |
43 | {children}
44 |
45 | );
46 | };
47 |
48 | export default FormWrapper;
49 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
26 | );
27 | }
28 | );
29 | Input.displayName = "Input";
30 |
31 | export { Input };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "multi-step-form",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next/font": "^13.1.6",
13 | "@radix-ui/react-checkbox": "^1.0.1",
14 | "@radix-ui/react-label": "^2.0.0",
15 | "@radix-ui/react-radio-group": "^1.1.1",
16 | "@radix-ui/react-separator": "^1.0.1",
17 | "@radix-ui/react-switch": "^1.0.1",
18 | "@radix-ui/react-toggle-group": "^1.0.2",
19 | "@types/node": "18.13.0",
20 | "@types/react": "18.0.28",
21 | "@types/react-dom": "18.0.10",
22 | "class-variance-authority": "^0.4.0",
23 | "clsx": "^1.2.1",
24 | "eslint-config-next": "13.1.6",
25 | "eslint-plugin-tailwindcss": "^3.8.3",
26 | "framer-motion": "^9.0.4",
27 | "lucide-react": "^0.112.0",
28 | "next": "13.1.6",
29 | "react": "18.2.0",
30 | "react-dom": "18.2.0",
31 | "react-rough-notation": "^1.0.3",
32 | "tailwind-merge": "^1.9.1",
33 | "tailwindcss-animate": "^1.0.5",
34 | "typescript": "4.9.5"
35 | },
36 | "devDependencies": {
37 | "autoprefixer": "^10.4.13",
38 | "eslint": "^8.34.0",
39 | "postcss": "^8.4.21",
40 | "tailwindcss": "^3.2.6"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Multi-step form
2 |
3 | ## Table of contents
4 |
5 | - [Overview](#overview)
6 | - [Screenshot](#screenshot)
7 | - [Built with](#built-with)
8 | - [The challenge](#the-challenge)
9 | - [Links](#links)
10 |
11 | - [Author](#author)
12 | - [Acknowledgments](#acknowledgments)
13 |
14 | ## Overview
15 |
16 | ### Screenshot
17 |
18 | 
19 |
20 | ### Built with
21 |
22 | - Semantic HTML5 markup
23 | - Flexbox
24 | - Mobile-first workflow
25 | - [React](https://reactjs.org/) - JS library
26 | - [Next.js 13](https://beta.nextjs.org/docs) - React framework
27 | - [TailwindCSS](https://tailwindcss.com/) - For styles
28 | - [shadcn/ui](https://ui.shadcn.com/) - shadcn/ui components :rocket:
29 | - [Framer Motion](https://www.framer.com/motion/) - For animations
30 |
31 | ### The challenge
32 |
33 | Users should be able to:
34 |
35 | - Complete each step of the sequence
36 | - See a summary of their selections on the final step and confirm their order
37 | - View the optimal layout for the interface depending on their device's screen size
38 | - See hover and focus states for all interactive elements on the page
39 |
40 | ### Links
41 |
42 | - Live Site URL: [Here](https://multi-step-form-tawny.vercel.app/)
43 |
44 | ## Author
45 |
46 | - Website - [Marcos V Fitzsimons](https://marcosfitzsimons.com.ar/)
47 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SwitchPrimitives from "@radix-ui/react-switch";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class", '[data-theme="dark"]'],
4 | content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | sans: ['var(--font-ubuntu)'],
9 | },
10 | keyframes: {
11 | "accordion-down": {
12 | from: { height: 0 },
13 | to: { height: "var(--radix-accordion-content-height)" },
14 | },
15 | "accordion-up": {
16 | from: { height: "var(--radix-accordion-content-height)" },
17 | to: { height: 0 },
18 | },
19 | },
20 | animation: {
21 | "accordion-down": "accordion-down 0.2s ease-out",
22 | "accordion-up": "accordion-up 0.2s ease-out",
23 | },
24 | colors: {
25 | neutral: {
26 | 750: '#313131'
27 | }
28 | },
29 | boxShadow: {
30 | input: `
31 | 0px 1px 0px -1px var(--tw-shadow-color),
32 | 0px 1px 1px -1px var(--tw-shadow-color),
33 | 0px 1px 2px -1px var(--tw-shadow-color),
34 | 0px 2px 4px -2px var(--tw-shadow-color),
35 | 0px 3px 6px -3px var(--tw-shadow-color)
36 | `,
37 | highlight: `
38 | inset 0px 0px 0px 1px var(--tw-shadow-color),
39 | inset 0px 1px 0px var(--tw-shadow-color)
40 | `,
41 | },
42 | },
43 | },
44 | plugins: [require("tailwindcss-animate")],
45 | }
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5 | import { Circle } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | );
20 | });
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | );
41 | });
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
43 |
44 | export { RadioGroup, RadioGroupItem };
45 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { VariantProps, cva } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const buttonVariants = cva(
7 | "outline-none inline-flex items-center justify-center text-sm font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-slate-900 text-white dark:bg-slate-50 dark:text-slate-900",
12 | destructive:
13 | "bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
14 | outline:
15 | "bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
16 | subtle:
17 | "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100",
18 | ghost:
19 | "bg-transparent dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
20 | link: "bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
21 | },
22 | size: {
23 | default: "h-10 py-2 px-4",
24 | sm: "h-9 px-2 rounded-md",
25 | lg: "h-11 px-8 rounded-md",
26 | },
27 | },
28 | defaultVariants: {
29 | variant: "default",
30 | size: "default",
31 | },
32 | }
33 | );
34 |
35 | export interface ButtonProps
36 | extends React.ButtonHTMLAttributes,
37 | VariantProps {}
38 |
39 | const Button = React.forwardRef(
40 | ({ className, variant, size, ...props }, ref) => {
41 | return (
42 |
47 | );
48 | }
49 | );
50 | Button.displayName = "Button";
51 |
52 | export { Button, buttonVariants };
53 |
--------------------------------------------------------------------------------
/components/SuccessMessage.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { motion } from "framer-motion";
3 | import { RefreshCcw } from "lucide-react";
4 | import { Button } from "../components/ui/button";
5 | import successIcon from "../public/assets/success.png";
6 |
7 | const successVariants = {
8 | hidden: {
9 | opacity: 0,
10 | y: 50,
11 | },
12 | visible: {
13 | opacity: 1,
14 | y: 0,
15 | transition: {
16 | ease: "backIn",
17 | duration: 0.6,
18 | },
19 | },
20 | };
21 |
22 | const SuccessMessage = () => {
23 | const refresh = () => window.location.reload();
24 | return (
25 |
31 |
38 |
39 | Thank you!
40 |
41 |
42 | Thanks for confirming your subscription! We hope you have fun using our
43 | plataform. If you ever need support, please feel free to email us at
44 | support@loremgaming.com
45 |
46 |
47 |
48 |
52 | Restart
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default SuccessMessage;
61 |
--------------------------------------------------------------------------------
/components/AddonsForm.tsx:
--------------------------------------------------------------------------------
1 | import { FormItems } from "@/app/page";
2 | import { Checkbox } from "@/components/ui/checkbox";
3 | import FormWrapper from "./FormWrapper";
4 |
5 | type stepProps = FormItems & {
6 | updateForm: (fieldToUpdate: Partial) => void;
7 | };
8 |
9 | const AddonsForm = ({ addOns, yearly, updateForm }: stepProps) => {
10 | function handleCheckboxChange(addOnId: number, checked: boolean) {
11 | const updatedAddOns = addOns.map((addOn) =>
12 | addOn.id === addOnId ? { ...addOn, checked } : addOn
13 | );
14 | updateForm({ addOns: updatedAddOns });
15 | }
16 |
17 | return (
18 |
22 |
23 | {addOns.map((addOn) => (
24 |
30 |
34 | handleCheckboxChange(addOn.id, checked as boolean)
35 | }
36 | />
37 |
38 |
39 |
43 | {addOn.title}
44 |
45 |
{addOn.subtitle}
46 |
47 |
48 | {`+$${yearly ? addOn.price * 10 : addOn.price}${
49 | yearly ? "/yr" : "/mo"
50 | }`}
51 |
52 |
53 |
54 | ))}
55 |
56 |
57 | );
58 | };
59 |
60 | export default AddonsForm;
61 |
--------------------------------------------------------------------------------
/components/UserInfoForm.tsx:
--------------------------------------------------------------------------------
1 | import FormWrapper from "./FormWrapper";
2 | import { Input } from "@/components/ui/input";
3 | import { Label } from "@/components/ui/label";
4 | import { FormItems } from "../app/page";
5 |
6 | type StepProps = FormItems & {
7 | updateForm: (fieldToUpdate: Partial) => void;
8 | errors: Partial;
9 | };
10 |
11 | const UserInfoForm = ({
12 | name,
13 | email,
14 | phone,
15 | errors,
16 | updateForm,
17 | }: StepProps) => {
18 | return (
19 |
23 |
24 |
25 |
Name
26 |
updateForm({ name: e.target.value })}
34 | className="w-full"
35 | required
36 | />
37 | {errors.name &&
{errors.name}
}
38 |
39 |
40 |
Email Address
41 |
updateForm({ email: e.target.value })}
49 | required
50 | />
51 | {errors.email && (
52 |
{errors.email}
53 | )}
54 |
55 |
56 |
Phone Number
57 |
updateForm({ phone: e.target.value })}
65 | required
66 | />
67 | {errors.phone && (
68 |
{errors.phone}
69 | )}
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default UserInfoForm;
77 |
--------------------------------------------------------------------------------
/components/FinalStep.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import FormWrapper from "./FormWrapper";
5 | import { Separator } from "@/components/ui/separator";
6 | import { FormItems } from "@/app/page";
7 |
8 | type StepProps = FormItems & {
9 | goTo: (index: number) => void;
10 | };
11 |
12 | const FinalStep = ({ yearly, plan, addOns, goTo }: StepProps) => {
13 | let planPrice = 0;
14 | switch (plan) {
15 | case "arcade":
16 | planPrice = 9;
17 | break;
18 | case "advanced":
19 | planPrice = 12;
20 | break;
21 | case "pro":
22 | planPrice = 15;
23 | break;
24 | default:
25 | planPrice = 0;
26 | break;
27 | }
28 |
29 | const filteredAddOns = addOns.filter((addOn) => addOn.checked === true);
30 |
31 | const totalAddOnsPrice = filteredAddOns?.reduce(
32 | (acc, obj) => acc + obj.price,
33 | 0
34 | );
35 | console.log(totalAddOnsPrice);
36 |
37 | return (
38 |
42 |
43 |
44 |
45 |
46 |
47 | {`${plan.charAt(0).toUpperCase() + plan.slice(1)} (${
48 | yearly ? "Yearly" : "Monthly"
49 | })`}
50 |
51 | goTo(1)}
53 | className="text-[#6fe79f] text-sm"
54 | >
55 | Change
56 |
57 |
58 |
{`$${
59 | yearly ? planPrice * 10 : planPrice
60 | }${yearly ? "/yr" : "/mo"}`}
61 |
62 | {filteredAddOns.length > 0 &&
}
63 | {filteredAddOns?.map((addOn) => (
64 |
68 |
{addOn.title}
69 |
{`$${yearly ? addOn.price * 10 : addOn.price}${
70 | yearly ? "/yr" : "/mo"
71 | }`}
72 |
73 | ))}
74 |
75 |
76 |
77 | Total (per {yearly ? "year" : "month"})
78 |
79 |
80 | +$
81 | {yearly
82 | ? planPrice * 10 + totalAddOnsPrice * 10
83 | : planPrice + totalAddOnsPrice}
84 | /{yearly ? "yr" : "mo"}
85 |
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | export default FinalStep;
93 |
--------------------------------------------------------------------------------
/components/SideBar.tsx:
--------------------------------------------------------------------------------
1 | import { RoughNotation } from "react-rough-notation";
2 |
3 | type NavProps = {
4 | currentStepIndex: number;
5 | goTo: (index: number) => void;
6 | };
7 |
8 | const SideBar = ({ currentStepIndex, goTo }: NavProps) => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | step 1
16 |
17 | goTo(0)}
20 | className={`text-sm ${
21 | currentStepIndex === 0 ? "text-[#ffe666]" : "text-white"
22 | } md:text-base`}
23 | >
24 |
29 | Your info
30 |
31 |
32 |
33 |
34 |
35 | step 2
36 |
37 | goTo(1)}
40 | className={`text-sm ${
41 | currentStepIndex === 1 ? "text-[#bd284d]" : "text-white"
42 | } md:text-base`}
43 | >
44 |
49 | Select plan
50 |
51 |
52 |
53 |
54 |
55 | step 3
56 |
57 | goTo(2)}
60 | className={`text-sm ${
61 | currentStepIndex === 2 ? "text-[#E7B8FF]" : "text-white"
62 | } md:text-base`}
63 | >
64 |
69 | Add-ons
70 |
71 |
72 |
73 |
74 |
75 | step 4
76 |
77 | goTo(3)}
80 | className={`text-sm ${
81 | currentStepIndex === 3 ? "text-[#6fe79f]" : "text-white"
82 | } md:text-base`}
83 | >
84 |
89 | Summary
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | };
98 |
99 | export default SideBar;
100 |
--------------------------------------------------------------------------------
/components/PlanForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Image from "next/image";
5 | import * as ToggleGroup from "@radix-ui/react-toggle-group";
6 | import { Label } from "@/components/ui/label";
7 | import { Switch } from "@/components/ui/switch";
8 | import FormWrapper from "./FormWrapper";
9 | import { FormItems } from "@/app/page";
10 | import arcadeImg from "../public/assets/arcade.png";
11 | import advancedImg from "../public/assets/game-console.png";
12 | import proImg from "../public/assets/online-gaming.png";
13 |
14 | type stepProps = FormItems & {
15 | updateForm: (fieldToUpdate: Partial) => void;
16 | };
17 |
18 | type Plan = "arcade" | "advanced" | "pro";
19 |
20 | const PlanForm = ({ updateForm, plan, yearly }: stepProps) => {
21 | const [yearlyUpdated, setYearlyUpdated] = useState(yearly);
22 | const [planSelected, setPlanSelected] = useState(plan);
23 |
24 | const handleCheckedChange = (yearlyUpdated: boolean) => {
25 | setYearlyUpdated((prev) => !prev);
26 | updateForm({ yearly: yearlyUpdated });
27 | };
28 |
29 | const handleValueChange = (planSelected: Plan) => {
30 | if (planSelected) {
31 | setPlanSelected(planSelected);
32 | updateForm({ plan: planSelected });
33 | }
34 | };
35 |
36 | return (
37 |
41 |
48 |
52 |
53 |
54 |
Arcade
55 |
{yearly ? "$90/yr" : "$9/mo"}
56 | {yearly && (
57 |
2 months free
58 | )}
59 |
60 |
61 |
65 |
66 |
67 |
Advanced
68 |
{yearly ? "$120/yr" : "$12/mo"}
69 | {yearly && (
70 |
2 months free
71 | )}
72 |
73 |
74 |
75 |
79 |
80 |
81 |
Pro
82 |
{yearly ? "$150/yr" : "$15/mo"}
83 | {yearly && (
84 |
2 months free
85 | )}
86 |
87 |
88 |
89 |
90 |
91 |
95 | Monthly
96 |
97 |
102 |
106 | Yearly
107 |
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default PlanForm;
115 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { useMultiplestepForm } from "hooks/useMultiplestepForm";
6 | import { AnimatePresence } from "framer-motion";
7 | import UserInfoForm from "@/components/UserInfoForm";
8 | import PlanForm from "@/components/PlanForm";
9 | import AddonsForm from "@/components/AddonsForm";
10 | import FinalStep from "@/components/FinalStep";
11 | import SuccessMessage from "@/components/SuccessMessage";
12 | import SideBar from "@/components/SideBar";
13 |
14 | interface AddOn {
15 | id: number;
16 | checked: boolean;
17 | title: string;
18 | subtitle: string;
19 | price: number;
20 | }
21 |
22 | export type FormItems = {
23 | name: string;
24 | email: string;
25 | phone: string;
26 | plan: "arcade" | "advanced" | "pro";
27 | yearly: boolean;
28 | addOns: AddOn[];
29 | };
30 |
31 | const initialValues: FormItems = {
32 | name: "",
33 | email: "",
34 | phone: "",
35 | plan: "arcade",
36 | yearly: false,
37 | addOns: [
38 | {
39 | id: 1,
40 | checked: true,
41 | title: "Online Service",
42 | subtitle: "Access to multiple games",
43 | price: 1,
44 | },
45 | {
46 | id: 2,
47 | checked: false,
48 | title: "Large storage",
49 | subtitle: "Extra 1TB of cloud save",
50 | price: 2,
51 | },
52 | {
53 | id: 3,
54 | checked: false,
55 | title: "Customizable Profile",
56 | subtitle: "Custom theme on your profile",
57 | price: 2,
58 | },
59 | ],
60 | };
61 |
62 | export default function Home() {
63 | const [formData, setFormData] = useState(initialValues);
64 | const [errors, setErrors] = useState>({});
65 | const {
66 | previousStep,
67 | nextStep,
68 | currentStepIndex,
69 | isFirstStep,
70 | isLastStep,
71 | steps,
72 | goTo,
73 | showSuccessMsg,
74 | } = useMultiplestepForm(4);
75 |
76 | function updateForm(fieldToUpdate: Partial) {
77 | const { name, email, phone } = fieldToUpdate;
78 |
79 | if (name && name.trim().length < 3) {
80 | setErrors((prevState) => ({
81 | ...prevState,
82 | name: "Name should be at least 3 characters long",
83 | }));
84 | } else if (name && name.trim().length > 15) {
85 | setErrors((prevState) => ({
86 | ...prevState,
87 | name: "Name should be no longer than 15 characters",
88 | }));
89 | } else {
90 | setErrors((prevState) => ({
91 | ...prevState,
92 | name: "",
93 | }));
94 | }
95 |
96 | if (email && !/\S+@\S+\.\S+/.test(email)) {
97 | setErrors((prevState) => ({
98 | ...prevState,
99 | email: "Please enter a valid email address",
100 | }));
101 | } else {
102 | setErrors((prevState) => ({
103 | ...prevState,
104 | email: "",
105 | }));
106 | }
107 |
108 | if (phone && !/^[0-9]{10}$/.test(phone)) {
109 | setErrors((prevState) => ({
110 | ...prevState,
111 | phone: "Please enter a valid 10-digit phone number",
112 | }));
113 | } else {
114 | setErrors((prevState) => ({
115 | ...prevState,
116 | phone: "",
117 | }));
118 | }
119 |
120 | setFormData({ ...formData, ...fieldToUpdate });
121 | }
122 |
123 | const handleOnSubmit = (e: React.FormEvent) => {
124 | e.preventDefault();
125 | if (Object.values(errors).some((error) => error)) {
126 | return;
127 | }
128 | nextStep();
129 | };
130 |
131 | return (
132 |
137 | {!showSuccessMsg ? (
138 |
139 | ) : (
140 | ""
141 | )}
142 |
145 | {showSuccessMsg ? (
146 |
147 |
148 |
149 | ) : (
150 |
200 | )}
201 |
202 |
203 | );
204 | }
205 |
--------------------------------------------------------------------------------