diff --git a/app/api/newsletter/_mailchimp.ts b/app/api/newsletter/_mailchimp.ts index 6dfd326..bc6c296 100644 --- a/app/api/newsletter/_mailchimp.ts +++ b/app/api/newsletter/_mailchimp.ts @@ -1,4 +1,4 @@ -import getConfig from "next/config"; +import { getConfig } from "blitz"; import got from "got"; const { serverRuntimeConfig } = getConfig(); diff --git a/app/api/newsletter/subscribe.ts b/app/api/newsletter/subscribe.ts index 8eb323c..c6f4a38 100644 --- a/app/api/newsletter/subscribe.ts +++ b/app/api/newsletter/subscribe.ts @@ -1,4 +1,4 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import type { BlitzApiRequest, BlitzApiResponse } from "blitz"; import zod from "zod"; import type { ApiError } from "../_types"; @@ -14,8 +14,8 @@ const bodySchema = zod.object({ }); export default async function subscribeToNewsletter( - req: NextApiRequest, - res: NextApiResponse + req: BlitzApiRequest, + res: BlitzApiResponse ) { if (req.method !== "POST") { const statusCode = 405; diff --git a/app/core/layouts/layout/footer.tsx b/app/core/layouts/layout/footer.tsx index 77660b1..8069eab 100644 --- a/app/core/layouts/layout/footer.tsx +++ b/app/core/layouts/layout/footer.tsx @@ -1,6 +1,5 @@ import type { ReactNode } from "react"; -import Link from "next/link"; -import { useRouter } from "next/router"; +import { Link, useRouter } from "blitz"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPhoneAlt as fasPhone, diff --git a/app/core/layouts/layout/index.tsx b/app/core/layouts/layout/index.tsx index ff72e1c..af69c0d 100644 --- a/app/core/layouts/layout/index.tsx +++ b/app/core/layouts/layout/index.tsx @@ -1,8 +1,7 @@ import type { ErrorInfo, FunctionComponent } from "react"; import { Component } from "react"; -import Head from "next/head"; +import { Head, withRouter } from "blitz"; import type { WithRouterProps } from "next/dist/client/with-router"; -import { withRouter } from "next/router"; import appLogger from "../../../../integrations/logger"; diff --git a/app/keypad/pages/keypad.tsx b/app/keypad/pages/keypad.tsx new file mode 100644 index 0000000..0f532ca --- /dev/null +++ b/app/keypad/pages/keypad.tsx @@ -0,0 +1,126 @@ +import type { BlitzPage } from "blitz"; +import type { FunctionComponent } from "react"; +import { atom, useAtom } from "jotai"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBackspace, faPhoneAlt as faPhone } from "@fortawesome/pro-solid-svg-icons"; + +import Layout from "../../core/layouts/layout"; +import useRequireOnboarding from "../../core/hooks/use-require-onboarding"; + +const pageTitle = "Keypad"; + +const Keypad: BlitzPage = () => { + useRequireOnboarding(); + const phoneNumber = useAtom(phoneNumberAtom)[0]; + const pressBackspace = useAtom(pressBackspaceAtom)[1]; + + return ( + +
+
+ {phoneNumber} +
+ +
+ + + + ABC + + + DEF + + + + + GHI + + + JKL + + + MNO + + + + + PQRS + + + TUV + + + WXYZ + + + + + + + + +
+ +
+
+ +
+
+
+
+
+ ); +}; + +const ZeroDigit: FunctionComponent = () => { + // TODO: long press, convert + to 0 + const pressDigit = useAtom(pressDigitAtom)[1]; + const onClick = () => pressDigit("0"); + + return ( +
+ 0 + +
+ ); +}; + +const Row: FunctionComponent = ({ children }) => ( +
{children}
+); + +const Digit: FunctionComponent<{ digit: string }> = ({ children, digit }) => { + const pressDigit = useAtom(pressDigitAtom)[1]; + const onClick = () => pressDigit(digit); + + return ( +
+ {digit} + {children} +
+ ); +}; + +const DigitLetters: FunctionComponent = ({ children }) => ( +
{children}
+); + +const phoneNumberAtom = atom(""); +const pressDigitAtom = atom(null, (get, set, digit) => { + if (get(phoneNumberAtom).length > 17) { + return; + } + + set(phoneNumberAtom, (prevState) => prevState + digit); +}); +const pressBackspaceAtom = atom(null, (get, set) => { + if (get(phoneNumberAtom).length === 0) { + return; + } + + set(phoneNumberAtom, (prevState) => prevState.slice(0, -1)); +}); + +export default Keypad; diff --git a/app/messages/api/webhook/incoming-message.ts b/app/messages/api/webhook/incoming-message.ts index c9d9f13..a168569 100644 --- a/app/messages/api/webhook/incoming-message.ts +++ b/app/messages/api/webhook/incoming-message.ts @@ -1,4 +1,4 @@ -import type { NextApiRequest, NextApiResponse } from "next"; +import type { BlitzApiRequest, BlitzApiResponse } from "blitz"; import twilio from "twilio"; import type { ApiError } from "../../../api/_types"; @@ -9,7 +9,7 @@ import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; const logger = appLogger.child({ route: "/api/webhook/incoming-message" }); -export default async function incomingMessageHandler(req: NextApiRequest, res: NextApiResponse) { +export default async function incomingMessageHandler(req: BlitzApiRequest, res: BlitzApiResponse) { if (req.method !== "POST") { const statusCode = 405; const apiError: ApiError = { diff --git a/app/onboarding/pages/welcome/step-one.tsx b/app/onboarding/pages/welcome/step-one.tsx index a1541a5..cd02a11 100644 --- a/app/onboarding/pages/welcome/step-one.tsx +++ b/app/onboarding/pages/welcome/step-one.tsx @@ -1,7 +1,9 @@ -import type { BlitzPage } from "blitz"; +import type { BlitzPage, GetServerSideProps } from "blitz"; +import { getSession, Routes } from "blitz"; import OnboardingLayout from "../../components/onboarding-layout"; import useCurrentCustomer from "../../../core/hooks/use-current-customer"; +import db from "../../../../db"; const StepOne: BlitzPage = () => { useCurrentCustomer(); // preload for step two @@ -20,4 +22,29 @@ const StepOne: BlitzPage = () => { StepOne.authenticate = true; +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getSession(req, res); + if (!session.userId) { + await session.$revoke(); + return { + redirect: { + destination: Routes.Home().pathname, + permanent: false, + }, + }; + } + + const phoneNumber = await db.phoneNumber.findFirst({ where: { customerId: session.userId } }); + if (!phoneNumber) { + return { props: {} }; + } + + return { + redirect: { + destination: Routes.Messages().pathname, + permanent: false, + }, + }; +}; + export default StepOne; diff --git a/app/onboarding/pages/welcome/step-three.tsx b/app/onboarding/pages/welcome/step-three.tsx index e375c75..a8b0991 100644 --- a/app/onboarding/pages/welcome/step-three.tsx +++ b/app/onboarding/pages/welcome/step-three.tsx @@ -89,7 +89,27 @@ StepThree.authenticate = true; export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { const session = await getSession(req, res); - const customer = await db.customer.findFirst({ where: { id: session.userId! } }); + if (!session.userId) { + await session.$revoke(); + return { + redirect: { + destination: Routes.Home().pathname, + permanent: false, + }, + }; + } + + const phoneNumber = await db.phoneNumber.findFirst({ where: { customerId: session.userId } }); + if (phoneNumber) { + return { + redirect: { + destination: Routes.Messages().pathname, + permanent: false, + }, + }; + } + + const customer = await db.customer.findFirst({ where: { id: session.userId } }); if (!customer) { return { redirect: { diff --git a/app/onboarding/pages/welcome/step-two.tsx b/app/onboarding/pages/welcome/step-two.tsx index 1365f5b..f04c502 100644 --- a/app/onboarding/pages/welcome/step-two.tsx +++ b/app/onboarding/pages/welcome/step-two.tsx @@ -1,12 +1,13 @@ -import type { BlitzPage } from "blitz"; -import { Routes, useMutation, useRouter } from "blitz"; +import type { BlitzPage, GetServerSideProps } from "blitz"; +import { getSession, Routes, useMutation, useRouter } from "blitz"; import clsx from "clsx"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; +import db from "db"; +import setTwilioApiFields from "../../mutations/set-twilio-api-fields"; import OnboardingLayout from "../../components/onboarding-layout"; import useCurrentCustomer from "../../../core/hooks/use-current-customer"; -import setTwilioApiFields from "../../mutations/set-twilio-api-fields"; type Form = { twilioAccountSid: string; @@ -100,4 +101,29 @@ const StepTwo: BlitzPage = () => { StepTwo.authenticate = true; +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getSession(req, res); + if (!session.userId) { + await session.$revoke(); + return { + redirect: { + destination: Routes.Home().pathname, + permanent: false, + }, + }; + } + + const phoneNumber = await db.phoneNumber.findFirst({ where: { customerId: session.userId } }); + if (!phoneNumber) { + return { props: {} }; + } + + return { + redirect: { + destination: Routes.Messages().pathname, + permanent: false, + }, + }; +}; + export default StepTwo; diff --git a/app/settings/components/alert.tsx b/app/settings/components/alert.tsx new file mode 100644 index 0000000..3e9c80a --- /dev/null +++ b/app/settings/components/alert.tsx @@ -0,0 +1,97 @@ +import type { ReactElement } from "react"; + +type AlertVariant = "error" | "success" | "info" | "warning"; + +type AlertVariantProps = { + backgroundColor: string; + icon: ReactElement; + titleTextColor: string; + messageTextColor: string; +}; + +type Props = { + title: string; + message: string; + variant: AlertVariant; +}; + +const ALERT_VARIANTS: Record = { + error: { + backgroundColor: "bg-red-50", + icon: ( + + + + ), + titleTextColor: "text-red-800", + messageTextColor: "text-red-700", + }, + success: { + backgroundColor: "bg-green-50", + icon: ( + + + + ), + titleTextColor: "text-green-800", + messageTextColor: "text-green-700", + }, + info: { + backgroundColor: "bg-primary-50", + icon: ( + + + + ), + titleTextColor: "text-primary-800", + messageTextColor: "text-primary-700", + }, + warning: { + backgroundColor: "bg-yellow-50", + icon: ( + + + + ), + titleTextColor: "text-yellow-800", + messageTextColor: "text-yellow-700", + }, +}; + +export default function Alert({ title, message, variant }: Props) { + const variantProperties = ALERT_VARIANTS[variant]; + + return ( +
+
+
{variantProperties.icon}
+
+

+ {title} +

+
+ {message} +
+
+
+
+ ); +} diff --git a/app/settings/components/button.tsx b/app/settings/components/button.tsx new file mode 100644 index 0000000..4d731f8 --- /dev/null +++ b/app/settings/components/button.tsx @@ -0,0 +1,48 @@ +import type { ButtonHTMLAttributes, FunctionComponent, MouseEventHandler } from "react"; +import clsx from "clsx"; + +type Props = { + variant: Variant; + onClick?: MouseEventHandler; + isDisabled?: boolean; + type: ButtonHTMLAttributes["type"]; +}; + +const Button: FunctionComponent = ({ children, type, variant, onClick, isDisabled }) => { + return ( + + ); +}; + +export default Button; + +type Variant = "error" | "default"; + +type VariantStyle = { + base: string; + disabled: string; +}; + +const VARIANTS_STYLES: Record = { + error: { + base: "bg-red-600 hover:bg-red-700 focus:ring-red-500", + disabled: "bg-red-400 cursor-not-allowed focus:ring-red-500", + }, + default: { + base: "bg-primary-600 hover:bg-primary-700 focus:ring-primary-500", + disabled: "bg-primary-400 cursor-not-allowed focus:ring-primary-500", + }, +}; diff --git a/app/settings/components/danger-zone.tsx b/app/settings/components/danger-zone.tsx new file mode 100644 index 0000000..6df63e3 --- /dev/null +++ b/app/settings/components/danger-zone.tsx @@ -0,0 +1,101 @@ +import { useRef, useState } from "react"; +import clsx from "clsx"; + +import Button from "./button"; +import SettingsSection from "./settings-section"; +import Modal, { ModalTitle } from "./modal"; +import useCurrentCustomer from "../../core/hooks/use-current-customer"; + +export default function DangerZone() { + const customer = useCurrentCustomer(); + const [isDeletingUser, setIsDeletingUser] = useState(false); + const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); + const modalCancelButtonRef = useRef(null); + + const closeModal = () => { + if (isDeletingUser) { + return; + } + + setIsConfirmationModalOpen(false); + }; + const onConfirm = () => { + setIsDeletingUser(true); + // user.deleteUser(); + }; + + return ( + +
+
+

+ Once you delete your account, all of its data will be permanently deleted. +

+ + + + +
+
+ + +
+
+ Delete my account +
+

+ Are you sure you want to delete your account? Your subscription will + be cancelled and your data permanently deleted. +

+

+ You are free to create a new account with the same email address if + you ever wish to come back. +

+
+
+
+
+ + +
+
+
+ ); +} diff --git a/app/settings/components/divider.tsx b/app/settings/components/divider.tsx new file mode 100644 index 0000000..8c78520 --- /dev/null +++ b/app/settings/components/divider.tsx @@ -0,0 +1,9 @@ +export default function Divider() { + return ( +
+
+
+
+
+ ); +} diff --git a/app/settings/components/modal.tsx b/app/settings/components/modal.tsx new file mode 100644 index 0000000..4183317 --- /dev/null +++ b/app/settings/components/modal.tsx @@ -0,0 +1,63 @@ +import type { FunctionComponent, MutableRefObject } from "react"; +import { Fragment } from "react"; +import { Transition, Dialog } from "@headlessui/react"; + +type Props = { + initialFocus?: MutableRefObject | undefined; + isOpen: boolean; + onClose: () => void; +}; + +const Modal: FunctionComponent = ({ children, initialFocus, isOpen, onClose }) => { + return ( + + +
+ + + + + {/* This element is to trick the browser into centering the modal contents. */} + + ​ + + +
+ {children} +
+
+
+
+
+ ); +}; + +export const ModalTitle: FunctionComponent = ({ children }) => ( + + {children} + +); + +export default Modal; diff --git a/app/settings/components/profile-informations.tsx b/app/settings/components/profile-informations.tsx new file mode 100644 index 0000000..c822d20 --- /dev/null +++ b/app/settings/components/profile-informations.tsx @@ -0,0 +1,134 @@ +import type { FunctionComponent } from "react"; +import { useEffect, useState } from "react"; +import { useRouter } from "blitz"; +import { useForm } from "react-hook-form"; + +import Alert from "./alert"; +import Button from "./button"; +import SettingsSection from "./settings-section"; +import useCurrentCustomer from "../../core/hooks/use-current-customer"; + +import appLogger from "../../../integrations/logger"; + +type Form = { + name: string; + email: string; +}; + +const logger = appLogger.child({ module: "profile-settings" }); + +const ProfileInformations: FunctionComponent = () => { + const { customer } = useCurrentCustomer(); + const router = useRouter(); + const { + register, + handleSubmit, + setValue, + formState: { isSubmitting, isSubmitSuccessful }, + } = useForm
(); + const [errorMessage, setErrorMessage] = useState(""); + + useEffect(() => { + setValue("name", customer?.user.name ?? ""); + setValue("email", customer?.user.email ?? ""); + }, [setValue, customer]); + + const onSubmit = handleSubmit(async ({ name, email }) => { + if (isSubmitting) { + return; + } + + try { + // TODO + // await customer.updateUser({ email, data: { name } }); + } catch (error) { + logger.error(error.response, "error updating user infos"); + + if (error.response.status === 401) { + logger.error("session expired, redirecting to sign in page"); + return router.push("/auth/sign-in"); + } + + setErrorMessage(error.response.data.errorMessage); + } + }); + + return ( + + + {errorMessage ? ( +
+ +
+ ) : null} + + {isSubmitSuccessful ? ( +
+ +
+ ) : null} + +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ ); +}; + +export default ProfileInformations; diff --git a/app/settings/components/settings-layout.tsx b/app/settings/components/settings-layout.tsx new file mode 100644 index 0000000..e03c92f --- /dev/null +++ b/app/settings/components/settings-layout.tsx @@ -0,0 +1,28 @@ +import type { FunctionComponent } from "react"; +import { useRouter } from "blitz"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronLeft } from "@fortawesome/pro-regular-svg-icons"; + +import Layout from "../../core/layouts/layout"; + +const pageTitle = "User Settings"; + +const SettingsLayout: FunctionComponent = ({ children }) => { + const router = useRouter(); + + return ( + +
+
+ + Back + +
+
+ +
{children}
+
+ ); +}; + +export default SettingsLayout; diff --git a/app/settings/components/settings-section.tsx b/app/settings/components/settings-section.tsx new file mode 100644 index 0000000..f2e14d1 --- /dev/null +++ b/app/settings/components/settings-section.tsx @@ -0,0 +1,18 @@ +import type { FunctionComponent, ReactNode } from "react"; + +type Props = { + title: string; + description?: ReactNode; +}; + +const SettingsSection: FunctionComponent = ({ children, title, description }) => ( +
+
+

{title}

+ {description ?

{description}

: null} +
+
{children}
+
+); + +export default SettingsSection; diff --git a/app/settings/components/update-password.tsx b/app/settings/components/update-password.tsx new file mode 100644 index 0000000..54df591 --- /dev/null +++ b/app/settings/components/update-password.tsx @@ -0,0 +1,133 @@ +import type { FunctionComponent } from "react"; +import { useState } from "react"; +import { useRouter } from "blitz"; +import { useForm } from "react-hook-form"; + +import Alert from "./alert"; +import Button from "./button"; +import SettingsSection from "./settings-section"; +import useCurrentCustomer from "../../core/hooks/use-current-customer"; + +import appLogger from "../../../integrations/logger"; + +const logger = appLogger.child({ module: "update-password" }); + +type Form = { + newPassword: string; + newPasswordConfirmation: string; +}; + +const UpdatePassword: FunctionComponent = () => { + const customer = useCurrentCustomer(); + const router = useRouter(); + const { + register, + handleSubmit, + formState: { isSubmitting, isSubmitSuccessful }, + } = useForm
(); + const [errorMessage, setErrorMessage] = useState(""); + + const onSubmit = handleSubmit(async ({ newPassword, newPasswordConfirmation }) => { + if (isSubmitting) { + return; + } + + if (newPassword !== newPasswordConfirmation) { + setErrorMessage("New passwords don't match"); + return; + } + + try { + // TODO + // await customer.updateUser({ password: newPassword }); + } catch (error) { + logger.error(error.response, "error updating user infos"); + + if (error.response.status === 401) { + logger.error("session expired, redirecting to sign in page"); + return router.push("/auth/sign-in"); + } + + setErrorMessage(error.response.data.errorMessage); + } + }); + + return ( + + + {errorMessage ? ( +
+ +
+ ) : null} + + {isSubmitSuccessful ? ( +
+ +
+ ) : null} + +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ ); +}; + +export default UpdatePassword; diff --git a/app/settings/pages/settings.tsx b/app/settings/pages/settings.tsx new file mode 100644 index 0000000..21f734c --- /dev/null +++ b/app/settings/pages/settings.tsx @@ -0,0 +1,58 @@ +import type { BlitzPage } from "blitz"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCreditCard, faUserCircle } from "@fortawesome/pro-regular-svg-icons"; + +import Layout from "../../core/layouts/layout"; + +import appLogger from "../../../integrations/logger"; +import useRequireOnboarding from "../../core/hooks/use-require-onboarding"; + +const logger = appLogger.child({ page: "/settings" }); + +/* eslint-disable react/display-name */ +const navigation = [ + { + name: "Account", + href: "/settings/account", + icon: ({ className = "w-8 h-8" }) => ( + + ), + }, + { + name: "Billing", + href: "/settings/billing", + icon: ({ className = "w-8 h-8" }) => ( + + ), + }, +]; +/* eslint-enable react/display-name */ + +const Settings: BlitzPage = () => { + useRequireOnboarding(); + + return ( + +
+ +
+
+ ); +}; + +Settings.authenticate = true; + +export default Settings; diff --git a/app/settings/pages/settings/account.tsx b/app/settings/pages/settings/account.tsx new file mode 100644 index 0000000..6d2424d --- /dev/null +++ b/app/settings/pages/settings/account.tsx @@ -0,0 +1,36 @@ +import type { BlitzPage } from "blitz"; + +import SettingsLayout from "../../components/settings-layout"; +import ProfileInformations from "../../components/profile-informations"; +import Divider from "../../components/divider"; +import UpdatePassword from "../../components/update-password"; +import DangerZone from "../../components/danger-zone"; +import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; + +const Account: BlitzPage = () => { + useRequireOnboarding(); + + return ( + +
+ + +
+ +
+ + + +
+ +
+ + +
+
+ ); +}; + +Account.authenticate = true; + +export default Account; diff --git a/app/settings/pages/settings/billing.tsx b/app/settings/pages/settings/billing.tsx new file mode 100644 index 0000000..cfc1791 --- /dev/null +++ b/app/settings/pages/settings/billing.tsx @@ -0,0 +1,132 @@ +/* TODO +import type { FunctionComponent, MouseEventHandler } from "react"; +import type { BlitzPage } from "blitz"; +import { ExternalLinkIcon } from "@heroicons/react/outline"; + +import SettingsLayout from "../../components/settings/settings-layout"; +import SettingsSection from "../../components/settings/settings-section"; +import BillingPlans from "../../components/billing/billing-plans"; +import Divider from "../../components/divider"; + +import useSubscription from "../../hooks/use-subscription"; + +import { withPageOnboardingRequired } from "../../../lib/session-helpers"; +import type { Subscription } from "../../database/subscriptions"; +import { findUserSubscription } from "../../database/subscriptions"; + +import appLogger from "../../../lib/logger"; +import ConnectedLayout from "../../components/connected-layout"; + +const logger = appLogger.child({ page: "/account/settings/billing" }); + +type Props = { + subscription: Subscription | null; +}; + +const Billing: BlitzPage = ({ subscription }) => { + /!* + TODO: I want to be able to + - renew subscription (after pause/cancel for example) (message like "your subscription expired, would you like to renew ?") + - know when is the last time I paid and for how much + - know when is the next time I will pay and for how much + *!/ + const { cancelSubscription, updatePaymentMethod } = useSubscription(); + + return ( + + +
+ {subscription ? ( + <> + + + updatePaymentMethod({ + updateUrl: subscription.updateUrl, + }) + } + text="Update payment method on Paddle" + /> + + +
+ +
+ + + + + +
+ +
+ + + + cancelSubscription({ + cancelUrl: subscription.cancelUrl, + }) + } + text="Cancel subscription on Paddle" + /> + + + ) : ( + + + + )} +
+
+
+ ); +}; + +export default Billing; + +type PaddleLinkProps = { + onClick: MouseEventHandler; + text: string; +}; + +const PaddleLink: FunctionComponent = ({ onClick, text }) => ( + +); + +export const getServerSideProps = withPageOnboardingRequired( + async (context, user) => { + // const subscription = await findUserSubscription({ userId: user.id }); + + return { + props: { subscription: null }, + }; + }, +); +*/ + +import type { BlitzPage } from "blitz"; +import { useRouter } from "blitz"; +import { useEffect } from "react"; + +import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; + +const Billing: BlitzPage = () => { + useRequireOnboarding(); + const router = useRouter(); + + useEffect(() => { + router.push("/messages"); + }); + + return null; +}; + +Billing.authenticate = true; + +export default Billing; diff --git a/package-lock.json b/package-lock.json index 98260bb..2890ca9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1034,6 +1034,11 @@ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", "integrity": "sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==" }, + "@headlessui/react": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz", + "integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==" + }, "@heroicons/react": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.3.tgz", @@ -9023,6 +9028,11 @@ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, + "jotai": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-1.2.2.tgz", + "integrity": "sha512-iqkkUdWsH2Mk4HY1biba/8kA77+8liVBy8E0d8Nce29qow4h9mzdDhpTasAruuFYPycw6JvfZgL5RB0JJuIZjw==" + }, "joycon": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.0.1.tgz", diff --git a/package.json b/package.json index 5526da1..d570b78 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@fortawesome/pro-regular-svg-icons": "file:./fontawesome/fortawesome-pro-regular-svg-icons-5.15.3.tgz", "@fortawesome/pro-solid-svg-icons": "file:./fontawesome/fortawesome-pro-solid-svg-icons-5.15.3.tgz", "@fortawesome/react-fontawesome": "0.1.14", + "@headlessui/react": "1.4.0", "@heroicons/react": "1.0.3", "@hookform/resolvers": "2.6.1", "@prisma/client": "2.27.0", @@ -49,6 +50,7 @@ "clsx": "1.1.1", "concurrently": "6.2.0", "got": "11.8.2", + "jotai": "1.2.2", "pino": "6.13.0", "pino-pretty": "5.1.2", "postcss": "8.3.6",