diff --git a/app/features/keypad/components/blurred-keypad.tsx b/app/features/keypad/components/blurred-keypad.tsx new file mode 100644 index 0000000..b3ef2f5 --- /dev/null +++ b/app/features/keypad/components/blurred-keypad.tsx @@ -0,0 +1,18 @@ +import { IoCall } from "react-icons/io5"; + +import Keypad from "~/features/keypad/components/keypad"; + +export default function BlurredKeypad() { + return ( +
+
+
+ + + +
+
+ ); +} diff --git a/app/features/keypad/components/keypad.tsx b/app/features/keypad/components/keypad.tsx index 6d97dc9..1c7473b 100644 --- a/app/features/keypad/components/keypad.tsx +++ b/app/features/keypad/components/keypad.tsx @@ -1,50 +1,45 @@ -import type { FunctionComponent, PropsWithChildren } from "react"; -import type { PressHookProps } from "@react-aria/interactions"; +import { type FunctionComponent, type PropsWithChildren, useRef } from "react"; import { usePress } from "@react-aria/interactions"; +import { useLongPressDigit, usePressDigit } from "~/features/keypad/hooks/atoms"; -type Props = { - onDigitPressProps: (digit: string) => PressHookProps; - onZeroPressProps: PressHookProps; -}; - -const Keypad: FunctionComponent> = ({ children, onDigitPressProps, onZeroPressProps }) => { +const Keypad: FunctionComponent> = ({ children }) => { return (
- - + + ABC - + DEF - + GHI - + JKL - + MNO - + PQRS - + TUV - + WXYZ - - - + + + {typeof children !== "undefined" ? {children} : null}
@@ -57,15 +52,23 @@ const Row: FunctionComponent> = ({ children }) => (
{children}
); -const DigitLetters: FunctionComponent> = ({ children }) =>
{children}
; +const DigitLetters: FunctionComponent> = ({ children }) => ( +
{children}
+); type DigitProps = { digit: string; - onPressProps: Props["onDigitPressProps"]; }; -const Digit: FunctionComponent> = ({ children, digit, onPressProps }) => { - const { pressProps } = usePress(onPressProps(digit)); +const Digit: FunctionComponent> = ({ children, digit }) => { + const pressDigit = usePressDigit(); + const onPressProps = { + onPress() { + // navigator.vibrate(1); // removed in webkit + pressDigit(digit); + }, + }; + const { pressProps } = usePress(onPressProps); return (
@@ -75,11 +78,24 @@ const Digit: FunctionComponent> = ({ children, dig ); }; -type ZeroDigitProps = { - onPressProps: Props["onZeroPressProps"]; -}; - -const ZeroDigit: FunctionComponent> = ({ onPressProps }) => { +const ZeroDigit: FunctionComponent = () => { + const timeoutRef = useRef | null>(null); + const pressDigit = usePressDigit(); + const longPressDigit = useLongPressDigit(); + const onPressProps = { + onPressStart() { + pressDigit("0"); + timeoutRef.current = setTimeout(() => { + longPressDigit("+"); + }, 750); + }, + onPressEnd() { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }, + }; const { pressProps } = usePress(onPressProps); return ( diff --git a/app/features/keypad/hooks/atoms.ts b/app/features/keypad/hooks/atoms.ts new file mode 100644 index 0000000..f836122 --- /dev/null +++ b/app/features/keypad/hooks/atoms.ts @@ -0,0 +1,33 @@ +import { atom, useAtom } from "jotai"; + +const phoneNumberAtom = atom(""); +const pressDigitAtom = atom(null, (get, set, digit: string) => { + if (get(phoneNumberAtom).length > 17) { + return; + } + + if ("0123456789+#*".indexOf(digit) === -1) { + return; + } + + set(phoneNumberAtom, (prevState) => prevState + digit); +}); +const longPressDigitAtom = atom(null, (get, set, replaceWith: string) => { + if (get(phoneNumberAtom).length > 17) { + return; + } + + set(phoneNumberAtom, (prevState) => prevState.slice(0, -1) + replaceWith); +}); +const pressBackspaceAtom = atom(null, (get, set) => { + if (get(phoneNumberAtom).length === 0) { + return; + } + + set(phoneNumberAtom, (prevState) => prevState.slice(0, -1)); +}); + +export const usePhoneNumber = () => useAtom(phoneNumberAtom); +export const useRemoveDigit = () => useAtom(pressBackspaceAtom)[1]; +export const usePressDigit = () => useAtom(pressDigitAtom)[1]; +export const useLongPressDigit = () => useAtom(longPressDigitAtom)[1]; diff --git a/app/features/keypad/hooks/use-on-backspace-press.ts b/app/features/keypad/hooks/use-on-backspace-press.ts new file mode 100644 index 0000000..ee24e8c --- /dev/null +++ b/app/features/keypad/hooks/use-on-backspace-press.ts @@ -0,0 +1,34 @@ +import { useRef } from "react"; +import { usePress } from "@react-aria/interactions"; + +import { useRemoveDigit } from "./atoms"; + +export default function useOnBackspacePress() { + const removeDigit = useRemoveDigit(); + const timeoutRef = useRef | null>(null); + const intervalRef = useRef | null>(null); + const { pressProps: onBackspacePressProps } = usePress({ + onPressStart() { + timeoutRef.current = setTimeout(() => { + removeDigit(); + intervalRef.current = setInterval(removeDigit, 75); + }, 325); + }, + onPressEnd() { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + return; + } + + removeDigit(); + }, + }); + + return onBackspacePressProps; +} diff --git a/app/routes/__app/keypad.tsx b/app/routes/__app/keypad.tsx index 4da1f44..1d53b5a 100644 --- a/app/routes/__app/keypad.tsx +++ b/app/routes/__app/keypad.tsx @@ -1,32 +1,56 @@ -import { Fragment, useRef, useState } from "react"; +import { Fragment } from "react"; +import type { LoaderFunction, MetaFunction } from "@remix-run/node"; import { useNavigate } from "@remix-run/react"; -import { atom, useAtom } from "jotai"; -import { usePress } from "@react-aria/interactions"; +import { json, useLoaderData } from "superjson-remix"; import { Transition } from "@headlessui/react"; import { IoBackspace, IoCall } from "react-icons/io5"; -import { Direction } from "@prisma/client"; +import { Prisma } from "@prisma/client"; -import Keypad from "~/features/keypad/components/keypad"; -// import usePhoneCalls from "~/features/keypad/hooks/use-phone-calls"; import useKeyPress from "~/features/keypad/hooks/use-key-press"; -// import useCurrentUser from "~/features/core/hooks/use-current-user"; -import KeypadErrorModal from "~/features/keypad/components/keypad-error-modal"; +import useOnBackspacePress from "~/features/keypad/hooks/use-on-backspace-press"; +import Keypad from "~/features/keypad/components/keypad"; +import BlurredKeypad from "~/features/keypad/components/blurred-keypad"; +import MissingTwilioCredentials from "~/features/core/components/missing-twilio-credentials"; import InactiveSubscription from "~/features/core/components/inactive-subscription"; +import { getSeoMeta } from "~/utils/seo"; +import db from "~/utils/db.server"; +import { requireLoggedIn } from "~/utils/auth.server"; +import { usePhoneNumber, usePressDigit, useRemoveDigit } from "~/features/keypad/hooks/atoms"; + +export const meta: MetaFunction = () => ({ + ...getSeoMeta({ title: "Keypad" }), +}); + +type KeypadLoaderData = { + hasOngoingSubscription: boolean; + hasPhoneNumber: boolean; + lastRecipientCalled?: string; +}; + +export const loader: LoaderFunction = async ({ request }) => { + const { phoneNumber } = await requireLoggedIn(request); + const hasOngoingSubscription = true; // TODO + const hasPhoneNumber = Boolean(phoneNumber); + const lastCall = + phoneNumber && + (await db.phoneCall.findFirst({ + where: { phoneNumberId: phoneNumber.id }, + orderBy: { createdAt: Prisma.SortOrder.desc }, + })); + return json({ + hasOngoingSubscription, + hasPhoneNumber, + lastRecipientCalled: lastCall?.recipient, + }); +}; export default function KeypadPage() { - const { hasFilledTwilioCredentials, hasPhoneNumber, hasOngoingSubscription } = { - hasFilledTwilioCredentials: false, - hasPhoneNumber: false, - hasOngoingSubscription: false, - }; + const { hasOngoingSubscription, hasPhoneNumber, lastRecipientCalled } = useLoaderData(); const navigate = useNavigate(); - const [isKeypadErrorModalOpen, setIsKeypadErrorModalOpen] = useState(false); - const phoneCalls: any[] = []; //usePhoneCalls(); - const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom); - const removeDigit = useAtom(pressBackspaceAtom)[1]; - const timeoutRef = useRef | null>(null); - const intervalRef = useRef | null>(null); - const pressDigit = useAtom(pressDigitAtom)[1]; + const [phoneNumber, setPhoneNumber] = usePhoneNumber(); + const removeDigit = useRemoveDigit(); + const pressDigit = usePressDigit(); + const onBackspacePress = useOnBackspacePress(); useKeyPress((key) => { if (!hasOngoingSubscription) { return; @@ -38,74 +62,21 @@ export default function KeypadPage() { pressDigit(key); }); - const longPressDigit = useAtom(longPressDigitAtom)[1]; - const onZeroPressProps = { - onPressStart() { - if (!hasOngoingSubscription) { - return; - } - pressDigit("0"); - timeoutRef.current = setTimeout(() => { - longPressDigit("+"); - }, 750); - }, - onPressEnd() { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - }, - }; - const onDigitPressProps = (digit: string) => ({ - onPress() { - // navigator.vibrate(1); // removed in webkit - if (!hasOngoingSubscription) { - return; - } - - pressDigit(digit); - }, - }); - const { pressProps: onBackspacePress } = usePress({ - onPressStart() { - timeoutRef.current = setTimeout(() => { - removeDigit(); - intervalRef.current = setInterval(removeDigit, 75); - }, 325); - }, - onPressEnd() { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - return; - } - - removeDigit(); - }, - }); + if (!hasPhoneNumber) { + return ( + <> + + + + ); + } if (!hasOngoingSubscription) { return ( <> -
-
-
- {phoneNumber} -
- - - -
-
+ ); } @@ -117,24 +88,16 @@ export default function KeypadPage() { {phoneNumber}
- +