migrate to blitzjs

This commit is contained in:
m5r
2021-07-31 22:33:18 +08:00
parent 4aa646ab43
commit fc4278ca7b
218 changed files with 19100 additions and 27038 deletions

View File

@ -0,0 +1,84 @@
import { useState, ReactNode, PropsWithoutRef } from "react"
import { FormProvider, useForm, UseFormProps } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
export interface FormProps<S extends z.ZodType<any, any>>
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
/** All your form fields */
children?: ReactNode
/** Text to display in the submit button */
submitText?: string
schema?: S
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
initialValues?: UseFormProps<z.infer<S>>["defaultValues"]
}
interface OnSubmitResult {
FORM_ERROR?: string
[prop: string]: any
}
export const FORM_ERROR = "FORM_ERROR"
export function Form<S extends z.ZodType<any, any>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
const ctx = useForm<z.infer<S>>({
mode: "onBlur",
resolver: schema ? zodResolver(schema) : undefined,
defaultValues: initialValues,
})
const [formError, setFormError] = useState<string | null>(null)
return (
<FormProvider {...ctx}>
<form
onSubmit={ctx.handleSubmit(async (values) => {
const result = (await onSubmit(values)) || {}
for (const [key, value] of Object.entries(result)) {
if (key === FORM_ERROR) {
setFormError(value)
} else {
ctx.setError(key as any, {
type: "submit",
message: value,
})
}
}
})}
className="form"
{...props}
>
{/* Form fields supplied as children are rendered here */}
{children}
{formError && (
<div role="alert" style={{ color: "red" }}>
{formError}
</div>
)}
{submitText && (
<button type="submit" disabled={ctx.formState.isSubmitting}>
{submitText}
</button>
)}
<style global jsx>{`
.form > * + * {
margin-top: 1rem;
}
`}</style>
</form>
</FormProvider>
)
}
export default Form

View File

@ -0,0 +1,58 @@
import { forwardRef, PropsWithoutRef } from "react"
import { useFormContext } from "react-hook-form"
export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
/** Field name. */
name: string
/** Field label. */
label: string
/** Field type. Doesn't include radio buttons and checkboxes */
type?: "text" | "password" | "email" | "number"
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
}
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({ label, outerProps, name, ...props }, ref) => {
const {
register,
formState: { isSubmitting, errors },
} = useFormContext()
const error = Array.isArray(errors[name])
? errors[name].join(", ")
: errors[name]?.message || errors[name]
return (
<div {...outerProps}>
<label>
{label}
<input disabled={isSubmitting} {...register(name)} {...props} />
</label>
{error && (
<div role="alert" style={{ color: "red" }}>
{error}
</div>
)}
<style jsx>{`
label {
display: flex;
flex-direction: column;
align-items: start;
font-size: 1rem;
}
input {
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
border: 1px solid purple;
appearance: none;
margin-top: 0.5rem;
}
`}</style>
</div>
)
}
)
export default LabeledTextField

View File

@ -0,0 +1,11 @@
import { useQuery } from "blitz"
import getCurrentCustomer from "../../customers/queries/get-current-customer"
export default function useCurrentCustomer() {
const [customer] = useQuery(getCurrentCustomer, null)
return {
customer,
hasCompletedOnboarding: Boolean(!!customer && customer.accountSid && customer.authToken),
}
}

View File

@ -0,0 +1,15 @@
import { useQuery } from "blitz"
import getCurrentCustomerPhoneNumber from "../../phone-numbers/queries/get-current-customer-phone-number"
import useCurrentCustomer from "./use-current-customer"
export default function useCustomerPhoneNumber() {
const { hasCompletedOnboarding } = useCurrentCustomer()
const [customerPhoneNumber] = useQuery(
getCurrentCustomerPhoneNumber,
{},
{ enabled: hasCompletedOnboarding }
)
return customerPhoneNumber
}

View File

@ -0,0 +1,24 @@
import { Routes, useRouter } from "blitz"
import useCurrentCustomer from "./use-current-customer"
import useCustomerPhoneNumber from "./use-customer-phone-number"
export default function useRequireOnboarding() {
const router = useRouter()
const { customer, hasCompletedOnboarding } = useCurrentCustomer()
const customerPhoneNumber = useCustomerPhoneNumber()
if (!hasCompletedOnboarding) {
throw router.push(Routes.StepTwo())
}
/*if (!customer.paddleCustomerId || !customer.paddleSubscriptionId) {
throw router.push(Routes.StepTwo());
return;
}*/
console.log("customerPhoneNumber", customerPhoneNumber)
if (!customerPhoneNumber) {
throw router.push(Routes.StepThree())
}
}

View File

@ -0,0 +1,22 @@
import { ReactNode } from "react"
import { Head } from "blitz"
type LayoutProps = {
title?: string
children: ReactNode
}
const BaseLayout = ({ title, children }: LayoutProps) => {
return (
<>
<Head>
<title>{title || "virtual-phone"}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
{children}
</>
)
}
export default BaseLayout

View File

@ -0,0 +1,81 @@
import type { ReactNode } from "react"
import Link from "next/link"
import { useRouter } from "next/router"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import {
faPhoneAlt as fasPhone,
faTh as fasTh,
faComments as fasComments,
faCog as fasCog,
} from "@fortawesome/pro-solid-svg-icons"
import {
faPhoneAlt as farPhone,
faTh as farTh,
faComments as farComments,
faCog as farCog,
} from "@fortawesome/pro-regular-svg-icons"
export default function Footer() {
return (
<footer className="grid grid-cols-4" style={{ flex: "0 0 50px" }}>
<NavLink
label="Calls"
path="/calls"
icons={{
active: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={fasPhone} />,
inactive: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={farPhone} />,
}}
/>
<NavLink
label="Keypad"
path="/keypad"
icons={{
active: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={fasTh} />,
inactive: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={farTh} />,
}}
/>
<NavLink
label="Messages"
path="/messages"
icons={{
active: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={fasComments} />,
inactive: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={farComments} />,
}}
/>
<NavLink
label="Settings"
path="/settings"
icons={{
active: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={fasCog} />,
inactive: <FontAwesomeIcon size="lg" className="w-6 h-6" icon={farCog} />,
}}
/>
</footer>
)
}
type NavLinkProps = {
path: string
label: string
icons: {
active: ReactNode
inactive: ReactNode
}
}
function NavLink({ path, label, icons }: NavLinkProps) {
const router = useRouter()
const isActiveRoute = router.pathname.startsWith(path)
const icon = isActiveRoute ? icons.active : icons.inactive
return (
<div className="flex flex-col items-center justify-around h-full">
<Link href={path}>
<a className="flex flex-col items-center">
{icon}
<span className="text-xs">{label}</span>
</a>
</Link>
</div>
)
}

View File

@ -0,0 +1,101 @@
import type { ErrorInfo, FunctionComponent } from "react"
import { Component } from "react"
import Head from "next/head"
import type { WithRouterProps } from "next/dist/client/with-router"
import { withRouter } from "next/router"
import appLogger from "../../../../integrations/logger"
import Footer from "./footer"
type Props = {
title: string
pageTitle?: string
hideFooter?: true
}
const logger = appLogger.child({ module: "Layout" })
const Layout: FunctionComponent<Props> = ({
children,
title,
pageTitle = title,
hideFooter = false,
}) => {
return (
<>
{pageTitle ? (
<Head>
<title>{pageTitle}</title>
</Head>
) : null}
<div className="h-full w-full overflow-hidden fixed bg-gray-50">
<div className="flex flex-col w-full h-full">
<div className="flex flex-col flex-1 w-full overflow-y-auto">
<main className="flex-1 my-0 h-full">
<ErrorBoundary>{children}</ErrorBoundary>
</main>
</div>
{!hideFooter ? <Footer /> : null}
</div>
</div>
</>
)
}
type ErrorBoundaryState =
| {
isError: false
}
| {
isError: true
errorMessage: string
}
const ErrorBoundary = withRouter(
class ErrorBoundary extends Component<WithRouterProps, ErrorBoundaryState> {
public readonly state = {
isError: false,
} as const
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return {
isError: true,
errorMessage: error.message,
}
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
logger.error(error, errorInfo.componentStack)
}
public render() {
if (this.state.isError) {
return (
<>
<h2 className="mt-6 text-center text-3xl leading-9 font-extrabold text-gray-900">
Oops, something went wrong.
</h2>
<p className="mt-2 text-center text-lg leading-5 text-gray-600">
Would you like to{" "}
<button
className="inline-flex space-x-2 items-center text-left"
onClick={this.props.router.reload}
>
<span className="transition-colors duration-150 border-b border-primary-200 hover:border-primary-500">
reload the page
</span>
</button>{" "}
?
</p>
</>
)
}
return this.props.children
}
}
)
export default Layout

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;