Skip to content

Commit

Permalink
extract family invite fancy popover into its own component
Browse files Browse the repository at this point in the history
  • Loading branch information
LexSwed committed Sep 9, 2024
1 parent 51ba1c8 commit a6840e2
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,112 +1,100 @@
import { Button, Icon, Popover, Text, mergeDefaultProps, startViewTransition } from "@nou/ui";
import { Button, Icon, Text, mergeDefaultProps } from "@nou/ui";
import { Match, Show, Suspense, Switch, createSignal } from "solid-js";

import { createTranslator } from "~/server/i18n";

import { FamilyInviteBenefits } from "../family-invite-benefits";

import {
MultiScreenPopover,
MultiScreenPopoverContent,
type MultiScreenPopoverControls,
MultiScreenPopoverHeader,
} from "../multi-screen-popover";
import { FamilyInviteQRCode } from "./invite-qrcode";
import { InviteWaitlist } from "./invite-waitlist";
import JoinFamily from "./join-family";
import { JoinFamily } from "./join-family";
import { Joined } from "./joined";

import "./family-invite.module.css";

type Step = "initial" | "qrcode" | "waitlist" | "join" | "join-success";
export const FamilyInviteDialog = (props: {
export function FamilyInviteDialog(props: {
id: string;
initialScreen?: Step;
}) => {
}) {
return (
<Popover
<MultiScreenPopover
id={props.id}
placement="center"
aria-labelledby={`${props.id}-headline`}
role="dialog"
class="view-transition-[family-invite-dialog] mt-[16vh] flex w-[94svw] max-w-[420px] flex-col gap-6 bg-gradient-to-b from-surface via-65% via-surface to-primary/10 p-6 md:mt-[20vh]"
// class="view-transition-[family-invite-dialog] mt-[16vh] flex w-[94svw] max-w-[420px] flex-col gap-6 bg-gradient-to-b from-surface via-65% via-surface to-primary/10 p-6 md:mt-[20vh]"
>
{(open) => (
<Show when={open()}>
{(controls) => {
return (
<Suspense>
<InviteDialogContent id={props.id} initialScreen={props.initialScreen} />
<InviteDialogContent
controls={controls}
id={props.id}
initialScreen={props.initialScreen}
/>
</Suspense>
</Show>
)}
</Popover>
);
}}
</MultiScreenPopover>
);
};
}

/**
* The component is separated to ensure the `step` is reset after the dialog is closed.
*/
const InviteDialogContent = (ownProps: {
id: string;
initialScreen?: Step;
controls: MultiScreenPopoverControls;
}) => {
const t = createTranslator("family");
const props = mergeDefaultProps(ownProps, { initialScreen: "initial" });
const [step, setStep] = createSignal<Step>(props.initialScreen);
const update = async (newStep: Step, direction: "forwards" | "backwards" = "forwards") => {
const transition = startViewTransition({
update: () => {
setStep(newStep);
},
types: ["slide", direction],
});
await transition.updateCallbackDone;
const popover = document.getElementById(props.id);
popover?.focus();
props.controls.update(() => {
setStep(newStep);
}, direction);
};

const closePopover = () => {
const popover = document.getElementById(props.id);
popover?.hidePopover();
};

return (
<>
<header class="view-transition-[family-invite-dialog-header] -m-4 z-10 flex flex-row items-center justify-between gap-2">
<Show when={!new Set<Step>(["initial", "join-success"]).has(step())} fallback={<div />}>
<Button
variant="ghost"
icon
label={t("invite.back")}
onClick={() => {
switch (step()) {
case "qrcode":
return update("initial", "backwards");
case "join":
return update("initial", "backwards");
case "waitlist":
return update("qrcode", "backwards");
default:
return null;
}
}}
>
<Icon use="chevron-left" />
</Button>
</Show>
<Text aria-hidden class="sr-only" id={`${props.id}-headline`} aria-live="polite">
<MultiScreenPopoverHeader
id={props.id}
backButton={
<Show when={!new Set<Step>(["initial", "join-success"]).has(step())} fallback={<div />}>
<Button
variant="ghost"
icon
label={t("invite.back")}
onClick={() => {
switch (step()) {
case "qrcode":
return update("initial", "backwards");
case "join":
return update("initial", "backwards");
case "waitlist":
return update("qrcode", "backwards");
default:
return null;
}
}}
>
<Icon use="chevron-left" />
</Button>
</Show>
}
headline={
<Switch>
<Match when={step() === "initial"}>{t("invite.step-aria-initial")}</Match>
<Match when={step() === "qrcode"}>{t("invite.step-aria-qrcode")}</Match>
<Match when={step() === "waitlist"}>{t("invite.step-aria-waitlist")}</Match>
<Match when={step() === "join"}>{t("invite.step-aria-join")}</Match>
<Match when={step() === "join-success"}>{t("invite.step-aria-join-success")}</Match>
</Switch>
</Text>
<Button
variant="ghost"
popoverTarget={props.id}
popoverTargetAction="hide"
icon
label={t("invite.close")}
>
<Icon use="x" />
</Button>
</header>
<div class="view-transition-[invite-dialog-content] flex flex-col gap-6">
}
/>
<MultiScreenPopoverContent>
<Switch>
<Match when={step() === "initial"}>
<div class="flex flex-col gap-6">
Expand All @@ -117,7 +105,9 @@ const InviteDialogContent = (ownProps: {
<FamilyInviteBenefits class="-mx-6 scroll-px-6 px-6" />
</div>
<div class="flex flex-col gap-4">
<Button onClick={() => update("qrcode")}>{t("invite.cta-invite")}</Button>
<Button variant="accent" onClick={() => update("qrcode")}>
{t("invite.cta-invite")}
</Button>
<div class="self-center">
<Button
variant="link"
Expand All @@ -140,13 +130,13 @@ const InviteDialogContent = (ownProps: {
/>
</Match>
<Match when={step() === "waitlist"}>
<InviteWaitlist onNext={closePopover} />
<InviteWaitlist onNext={props.controls.close} />
</Match>
<Match when={step() === "join-success"}>
<Joined popoverTarget={props.id} />
</Match>
</Switch>
</div>
</MultiScreenPopoverContent>
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/lib/family-invite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FamilyInviteDialog } from "./family-invite";
4 changes: 3 additions & 1 deletion packages/web/src/lib/family-invite/invite-qrcode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ export const FamilyInviteQRCode = (props: { onNext: () => void }) => {
</Suspense>
</div>
<div class="flex flex-col gap-4">
<Button onClick={props.onNext}>{t("invite.cta-ready")}</Button>
<Button variant="accent" onClick={props.onNext}>
{t("invite.cta-ready")}
</Button>
<Button variant="link" onClick={share}>
{t("invite.cta-share")}
</Button>
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/lib/family-invite/invite-waitlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const InviteWaitlist = (props: { onNext: () => void }) => {
</Match>
</Switch>
<Button
variant="accent"
onClick={() => {
props.onNext();
}}
Expand Down
11 changes: 6 additions & 5 deletions packages/web/src/lib/family-invite/join-family.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Match, Show, Suspense, Switch, createUniqueId, lazy } from "solid-js";
import { createTranslator } from "~/server/i18n";

const QRCodeScanner = lazy(() => import("./qr-scanner"));
const JoinFamily = (props: { onCancel: () => void; onSuccess: () => void }) => {
export function JoinFamily(props: { onCancel: () => void; onSuccess: () => void }) {
const t = createTranslator("family");

const supportsCamera = createAsync(async () => {
Expand Down Expand Up @@ -61,7 +61,10 @@ const JoinFamily = (props: { onCancel: () => void; onSuccess: () => void }) => {
<Match when={camera() !== "granted"}>
<div class="flex flex-col items-center gap-4">
<Show when={supportsCamera()}>
<Button onClick={() => navigator.mediaDevices.getUserMedia({ video: true })}>
<Button
variant="accent"
onClick={() => navigator.mediaDevices.getUserMedia({ video: true })}
>
{t("invite.join-scan-cta")}
</Button>
<Text with="body-sm">{orText}</Text>
Expand All @@ -75,6 +78,4 @@ const JoinFamily = (props: { onCancel: () => void; onSuccess: () => void }) => {
</Button>
</div>
);
};

export default JoinFamily;
}
6 changes: 6 additions & 0 deletions packages/web/src/lib/multi-screen-popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
MultiScreenPopover,
MultiScreenPopoverHeader,
MultiScreenPopoverContent,
type MultiScreenPopoverControls,
} from "./multi-screen-popover";
Original file line number Diff line number Diff line change
@@ -1,52 +1,54 @@
@tailwind utilities;

:global(html:active-view-transition-type(backwards, forwards)) {
&::view-transition-old(invite-dialog-content),
&::view-transition-new(invite-dialog-content) {
&::view-transition-old(dialog-content),
&::view-transition-new(dialog-content) {
@apply w-full overflow-clip object-none;
}
&::view-transition-old(invite-dialog-content) {
&::view-transition-old(dialog-content) {
@apply animate-out fade-out-0 fill-mode-forwards duration-200;
}
&::view-transition-new(invite-dialog-content) {
&::view-transition-new(dialog-content) {
@apply animate-in fade-in-0 fill-mode-forwards duration-300;
}
}

:global(::view-transition-old(family-invite-dialog)),
:global(::view-transition-new(family-invite-dialog)) {
:global(::view-transition-old(multi-screen-popover)),
:global(::view-transition-new(multi-screen-popover)) {
height: 100%;
}

:global(::view-transition-group(invite-dialog-content)) {
:global(::view-transition-group(dialog-content)) {
/* Clip the views as they overflow the group */
overflow: clip;
overflow-clip-margin: 1.5rem;
}

:global(
html:active-view-transition-type(backwards)::view-transition-old(
invite-dialog-content
dialog-content
)
) {
@apply slide-out-to-right-[20%];
}
:global(
html:active-view-transition-type(backwards)::view-transition-new(
invite-dialog-content
dialog-content
)
) {
@apply slide-in-from-left-[20%];
}
:global(
html:active-view-transition-type(forwards)::view-transition-old(
invite-dialog-content
dialog-content
)
) {
/* animation: slide-out-to-left 0.1s ease-in forwards; */
@apply slide-out-to-left-[20%];
}
:global(
html:active-view-transition-type(forwards)::view-transition-new(
invite-dialog-content
dialog-content
)
) {
@apply slide-in-from-right-[20%];
Expand Down
Loading

0 comments on commit a6840e2

Please sign in to comment.