remixed v0

This commit is contained in:
m5r
2022-05-14 12:22:06 +02:00
parent 9275d4499b
commit 98b89ae0f7
338 changed files with 22549 additions and 44628 deletions

View File

@ -0,0 +1,51 @@
import type { FunctionComponent, ReactChild } from "react";
type AlertVariant = "error" | "success" | "info" | "warning";
type AlertVariantProps = {
backgroundColor: string;
titleTextColor: string;
messageTextColor: string;
};
type Props = {
title: ReactChild;
message: ReactChild;
variant: AlertVariant;
};
const ALERT_VARIANTS: Record<AlertVariant, AlertVariantProps> = {
error: {
backgroundColor: "bg-red-50",
titleTextColor: "text-red-800",
messageTextColor: "text-red-700",
},
success: {
backgroundColor: "bg-green-50",
titleTextColor: "text-green-800",
messageTextColor: "text-green-700",
},
info: {
backgroundColor: "bg-primary-50",
titleTextColor: "text-primary-800",
messageTextColor: "text-primary-700",
},
warning: {
backgroundColor: "bg-yellow-50",
titleTextColor: "text-yellow-800",
messageTextColor: "text-yellow-700",
},
};
const Alert: FunctionComponent<Props> = ({ title, message, variant }) => {
const variantProperties = ALERT_VARIANTS[variant];
return (
<div className={`rounded-md p-4 ${variantProperties.backgroundColor}`}>
<h3 className={`text-sm leading-5 font-medium ${variantProperties.titleTextColor}`}>{title}</h3>
<div className={`mt-2 text-sm leading-5 ${variantProperties.messageTextColor}`}>{message}</div>
</div>
);
};
export default Alert;

View File

@ -0,0 +1,26 @@
import type { ButtonHTMLAttributes, FunctionComponent } from "react";
import { useTransition } from "@remix-run/react";
import clsx from "clsx";
type Props = ButtonHTMLAttributes<HTMLButtonElement>;
const Button: FunctionComponent<Props> = ({ children, ...props }) => {
const transition = useTransition();
return (
<button
className={clsx(
"w-full flex justify-center py-2 px-4 border border-transparent text-base font-medium rounded-md text-white focus:outline-none focus:border-primary-700 focus:shadow-outline-primary transition duration-150 ease-in-out",
{
"bg-primary-400 cursor-not-allowed": transition.state === "submitting",
"bg-primary-600 hover:bg-primary-700": transition.state !== "submitting",
},
)}
{...props}
>
{children}
</button>
);
}
export default Button;

View File

@ -0,0 +1,44 @@
import type { ReactNode } from "react";
import { NavLink } from "@remix-run/react";
import { IoCall, IoKeypad, IoChatbubbles, IoSettings } from "react-icons/io5";
import clsx from "clsx";
export default function Footer() {
return (
<footer
className="grid grid-cols-4 bg-[#F7F7F7] border-t border-gray-400 border-opacity-25 py-3 z-10"
style={{ flex: "0 0 50px" }}
>
<FooterLink label="Calls" path="/calls" icon={<IoCall className="w-6 h-6" />} />
<FooterLink label="Keypad" path="/keypad" icon={<IoKeypad className="w-6 h-6" />} />
<FooterLink label="Messages" path="/messages" icon={<IoChatbubbles className="w-6 h-6" />} />
<FooterLink label="Settings" path="/settings" icon={<IoSettings className="w-6 h-6" />} />
</footer>
);
}
type FooterLinkProps = {
path: string;
label: string;
icon: ReactNode;
};
function FooterLink({ path, label, icon }: FooterLinkProps) {
return (
<div className="flex flex-col items-center justify-around h-full">
<NavLink
to={path}
prefetch="none"
className={({ isActive }) =>
clsx("flex flex-col items-center", {
"text-primary-500": isActive,
"text-[#959595]": !isActive,
})
}
>
{icon}
<span className="text-xs">{label}</span>
</NavLink>
</div>
);
}

View File

@ -0,0 +1,35 @@
import { useNavigate } from "@remix-run/react";
import { IoSettings, IoAlertCircleOutline } from "react-icons/io5";
export default function InactiveSubscription() {
const navigate = useNavigate();
return (
<div className="flex items-end justify-center min-h-full overflow-y-hidden pt-4 px-4 pb-4 text-center md:block md:p-0 z-10">
<span className="hidden md:inline-block md:align-middle md:h-screen">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all md:my-8 md:align-middle md:max-w-lg md:w-full md:p-6">
<div className="text-center my-auto p-4">
<IoAlertCircleOutline className="mx-auto h-12 w-12 text-gray-400" aria-hidden="true" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
You don&#39;t have any active subscription
</h3>
<p className="mt-1 text-sm text-gray-500 max-w-sm mx-auto break-normal whitespace-normal">
You need an active subscription to use this feature.
<br />
Head over to your settings to pick up a plan.
</p>
<div className="mt-6">
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={() => navigate("/settings/billing")}
>
<IoSettings className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Choose a plan
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,46 @@
import type { FunctionComponent, InputHTMLAttributes, ReactNode } from "react";
import clsx from "clsx";
type Props = {
name: string;
label: ReactNode;
sideLabel?: ReactNode;
type?: "text" | "password" | "email";
error?: string;
} & InputHTMLAttributes<HTMLInputElement>;
const LabeledTextField: FunctionComponent<Props> = ({ name, label, sideLabel, type = "text", error, ...props }) => {
const hasSideLabel = !!sideLabel;
return (
<div className="mb-6">
<label
htmlFor={name}
className={clsx("text-sm font-medium leading-5 text-gray-700", {
block: !hasSideLabel,
"flex justify-between": hasSideLabel,
})}
>
{label}
{sideLabel ?? null}
</label>
<div className="mt-1 rounded-md shadow-sm">
<input
id={name}
name={name}
type={type}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:shadow-outline-primary focus:border-primary-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
required
{...props}
/>
</div>
{error ? (
<div role="alert" className="text-red-600">
{error}
</div>
) : null}
</div>
);
};
export default LabeledTextField;

View File

@ -0,0 +1,13 @@
import type { FunctionComponent } from "react";
type Props = {
className?: string;
};
const Logo: FunctionComponent<Props> = ({ className }) => (
<div className={className}>
<img src="/shellphone.png" alt="app logo" />
</div>
);
export default Logo;

View File

@ -0,0 +1,25 @@
import { Link } from "@remix-run/react";
import { IoSettings, IoAlertCircleOutline } from "react-icons/io5";
export default function MissingTwilioCredentials() {
return (
<div className="text-center my-auto">
<IoAlertCircleOutline className="mx-auto h-12 w-12 text-gray-400" aria-hidden="true" />
<h3 className="mt-2 text-sm font-medium text-gray-900">You haven&#39;t set up any phone number yet</h3>
<p className="mt-1 text-sm text-gray-500">
Head over to your settings
<br />
to set up your phone number.
</p>
<div className="mt-6">
<Link
to="/settings/account"
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2"
>
<IoSettings className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Set up my phone number
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,61 @@
import type { FunctionComponent, MutableRefObject, PropsWithChildren } from "react";
import { Fragment } from "react";
import { Transition, Dialog } from "@headlessui/react";
type Props = {
initialFocus?: MutableRefObject<HTMLElement | null> | undefined;
isOpen: boolean;
onClose: () => void;
};
const Modal: FunctionComponent<PropsWithChildren<Props>> = ({ children, initialFocus, isOpen, onClose }) => {
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
className="fixed z-30 inset-0 overflow-y-auto"
initialFocus={initialFocus}
onClose={onClose}
open={isOpen}
static
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center md:block md:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden md:inline-block md:align-middle md:h-screen">&#8203;</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 md:translate-y-0 md:scale-95"
enterTo="opacity-100 translate-y-0 md:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 md:scale-100"
leaveTo="opacity-0 translate-y-4 md:translate-y-0 md:scale-95"
>
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all md:my-8 md:align-middle md:max-w-lg md:w-full md:p-6">
{children}
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};
export const ModalTitle: FunctionComponent<PropsWithChildren<{}>> = ({ children }) => (
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
{children}
</Dialog.Title>
);
export default Modal;

View File

@ -0,0 +1,17 @@
import type { FunctionComponent } from "react";
import clsx from "clsx";
type Props = {
className?: string;
title: string;
};
const PageTitle: FunctionComponent<Props> = ({ className, title }) => {
return (
<div className={clsx(className, "flex flex-col space-y-6 p-3")}>
<h2 className="text-3xl font-bold">{title}</h2>
</div>
);
};
export default PageTitle;

View File

@ -0,0 +1,23 @@
export default function PhoneInitLoader() {
return (
<div className="px-4 my-auto text-center space-y-2">
<svg
className="animate-spin mx-auto h-5 w-5 text-primary-700"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p>We&#39;re finalizing your &#128026;phone initialization.</p>
<p>
You don&#39;t have to refresh this page, we will do it automatically for you when your phone is ready.
</p>
</div>
);
}

View File

@ -0,0 +1,68 @@
import { Fragment } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { HiCheck as CheckIcon, HiSelector as SelectorIcon } from "react-icons/hi";
import clsx from "clsx";
type Option = { name: string; value: string };
type Props = {
options: Option[];
onChange: (selectedValue: Option) => void;
value: Option;
};
export default function Select({ options, onChange, value }: Props) {
return (
<Listbox value={value} onChange={onChange}>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white rounded-lg shadow-md cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-orange-300 focus-visible:ring-offset-2 focus-visible:border-indigo-500 sm:text-sm">
<span className="block truncate">{value.name}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute w-full py-1 mt-1 overflow-auto text-base bg-white rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{options.map((option, index) => (
<Listbox.Option
key={`option-${option}-${index}`}
className={({ active }) =>
clsx(
"cursor-default select-none relative py-2 pl-10 pr-4",
active ? "text-amber-900 bg-amber-100" : "text-gray-900",
)
}
value={option}
>
{({ selected, active }) => (
<>
<span
className={clsx("block truncate", selected ? "font-medium" : "font-normal")}
>
{option.name}
</span>
{selected ? (
<span
className={clsx(
"absolute inset-y-0 left-0 flex items-center pl-3",
active ? "text-amber-600" : "text-amber-600",
)}
>
<CheckIcon className="w-5 h-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
);
}

View File

@ -0,0 +1,15 @@
.ring {
display: inline-block;
width: 50px;
height: 50px;
border: 3px solid rgba(0, 0, 0, 0.15);
border-radius: 50%;
border-top-color: currentColor;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,15 @@
import type { LinksFunction } from "@remix-run/node";
import styles from "./spinner.css";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: styles },
];
export default function Spinner() {
return (
<div className="h-full flex">
<div className="ring m-auto text-primary-400" />
</div>
);
}