Skip to content

Commit

Permalink
feat: implement progress dialog for forms (#223)
Browse files Browse the repository at this point in the history
* feat: implement progress dialog for forms

* fix: revert tailwind config

* chore: add header for notes
  • Loading branch information
joshxfi authored Jul 17, 2024
1 parent ee48ff9 commit 8c7cde5
Show file tree
Hide file tree
Showing 13 changed files with 399 additions and 132 deletions.
115 changes: 64 additions & 51 deletions apps/www/src/app/(user)/to/[username]/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import useBotDetection from "@/hooks/use-bot-detection";
import { Textarea } from "@umamin/ui/components/textarea";
import { useDynamicTextarea } from "@/hooks/use-dynamic-textarea";
import type { UserByUsernameQueryResult } from "../../../queries";
import { ProgressDialog } from "@/app/components/progress-dialog";

const CREATE_MESSAGE_MUTATION = graphql(`
mutation CreateMessage($input: CreateMessageInput!) {
Expand All @@ -27,7 +28,7 @@ const CREATE_MESSAGE_MUTATION = graphql(`

const createMessagePersisted = graphql.persisted(
"3550bab6df63cc9b4f891263677b487dbf67eba1b5cc9af9fec5fc037d2e49f0",
CREATE_MESSAGE_MUTATION,
CREATE_MESSAGE_MUTATION
);

type Props = {
Expand All @@ -40,6 +41,7 @@ export default function ChatForm({ currentUserId, user }: Props) {
const [content, setContent] = useState("");
const [message, setMessage] = useState("");
const [isFetching, setIsFetching] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);

const inputRef = useDynamicTextarea(content);

Expand Down Expand Up @@ -74,65 +76,76 @@ export default function ChatForm({ currentUserId, user }: Props) {
return;
}

setMessage(content.replace(/(\r\n|\n|\r){2,}/g, "\n\n"));
setContent("");
toast.success("Message sent");
setDialogOpen(true);
setIsFetching(false);

logEvent(analytics, "send_message");
} catch (err: any) {
toast.error(err.message);
toast.error("An error occured");
setIsFetching(false);
}
}

return (
<div
className={cn(
"flex flex-col justify-between pb-6 h-full max-h-[400px] relative w-full min-w-0",
user?.quietMode ? "min-h-[250px]" : "min-h-[350px]",
)}
>
<div className="flex flex-col h-full overflow-scroll pt-10 px-5 sm:px-7 pb-5 w-full relative min-w-0 ">
<ChatList
imageUrl={user?.imageUrl}
question={user?.question ?? ""}
reply={message}
/>
</div>

{user?.quietMode ? (
<span className="text-muted-foreground text-center text-sm">
User has enabled quiet mode
</span>
) : (
<form
onSubmit={handleSubmit}
className="px-5 sm:px-7 flex items-center space-x-2 w-full self-center pt-2 max-w-lg"
>
<Textarea
id="message"
required
ref={inputRef}
value={content}
onChange={(e) => {
setContent(e.target.value);
}}
maxLength={500}
placeholder="Type your message..."
className="focus-visible:ring-transparent text-base resize-none min-h-10 max-h-20"
autoComplete="off"
<>
<ProgressDialog
type="Message"
description="Your message is anonymous and encrypted. It will be delivered to the recipient's inbox."
open={dialogOpen}
onOpenChange={setDialogOpen}
onProgressComplete={() => {
setMessage(content.replace(/(\r\n|\n|\r){2,}/g, "\n\n"));
setContent("");
}}
/>
<div
className={cn(
"flex flex-col justify-between pb-6 h-full max-h-[400px] relative w-full min-w-0",
user?.quietMode ? "min-h-[250px]" : "min-h-[350px]"
)}
>
<div className="flex flex-col h-full overflow-scroll pt-10 px-5 sm:px-7 pb-5 w-full relative min-w-0 ">
<ChatList
imageUrl={user?.imageUrl}
question={user?.question ?? ""}
reply={message}
/>
<Button type="submit" size="icon" disabled={isFetching}>
{isFetching ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
<span className="sr-only">Send</span>
</Button>
</form>
)}
</div>
</div>

{user?.quietMode ? (
<span className="text-muted-foreground text-center text-sm">
User has enabled quiet mode
</span>
) : (
<form
onSubmit={handleSubmit}
className="px-5 sm:px-7 flex items-center space-x-2 w-full self-center pt-2 max-w-lg"
>
<Textarea
id="message"
required
ref={inputRef}
value={content}
disabled={isFetching}
onChange={(e) => {
setContent(e.target.value);
}}
maxLength={500}
placeholder="Type your message..."
className="focus-visible:ring-transparent text-base resize-none min-h-10 max-h-20"
autoComplete="off"
/>
<Button type="submit" size="icon" disabled={isFetching}>
{isFetching ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
<span className="sr-only">Send</span>
</Button>
</form>
)}
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import {
AlertDialogTitle,
} from "@umamin/ui/components/alert-dialog";

export default function UnauthenticatedDialog({ isLoggedIn }: { isLoggedIn: boolean }) {
const [open, setOpen] = useState(!isLoggedIn);
export default function UnauthenticatedDialog({
isLoggedIn,
}: {
isLoggedIn: boolean;
}) {
const [open, onOpenChange] = useState(!isLoggedIn);

return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center justify-center">
Expand Down
6 changes: 1 addition & 5 deletions apps/www/src/app/(user)/to/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { getUserByUsername } from "../../queries";
import { ShareButton } from "@/app/components/share-button";
import { Card, CardHeader } from "@umamin/ui/components/card";

const AdContainer = dynamic(() => import("@umamin/ui/ad"));
const ChatForm = dynamic(() => import("./components/form"));
const UnauthenticatedDialog = dynamic(
() => import("./components/unauthenticated"),
Expand Down Expand Up @@ -65,10 +64,7 @@ export default async function SendMessage({
const { session } = await getSession();

return (
<main className="pb-24 min-h-screen flex flex-col">
{/* v2-send-to */}
<AdContainer className="mb-5 w-full mt-20 max-w-2xl mx-auto" slotId="9163326848" />

<main className="pb-24 min-h-screen flex flex-col justify-center">
<div className="container w-full max-w-2xl">
<Card className="border flex flex-col w-full">
<CardHeader className="bg-background border-b w-full item-center rounded-t-2xl flex justify-between flex-row">
Expand Down
86 changes: 86 additions & 0 deletions apps/www/src/app/components/progress-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { toast } from "sonner";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@umamin/ui/components/alert-dialog";
import { Progress } from "@umamin/ui/components/progress";

const AdContainer = dynamic(() => import("@umamin/ui/ad"));

type Props = {
type: string;
description: string;
open: boolean;
onProgressComplete?: () => void;
// eslint-disable-next-line no-unused-vars
onOpenChange: (open: boolean) => void;
};

export function ProgressDialog({
type,
description,
open,
onOpenChange,
onProgressComplete,
}: Props) {
const [progress, setProgress] = useState(0);

useEffect(() => {
if (open) {
setProgress(0);

const duration = 5000;
const intervalTime = 100;
const totalIntervals = duration / intervalTime;
let currentInterval = 0;

const interval = setInterval(() => {
currentInterval += 1;
setProgress(Math.min(100, (currentInterval / totalIntervals) * 100));

if (currentInterval >= totalIntervals) {
clearInterval(interval);
toast.success(`${type} sent successfully`);

if (onProgressComplete) {
onProgressComplete();
}
}
}, intervalTime);

return () => clearInterval(interval);
}
}, [open, type]);

return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="px-0 max-w-full">
<AlertDialogHeader className="px-4">
<AlertDialogTitle>
{progress === 100 ? `${type} Sent` : `Sending ${type}`}
</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>

<AdContainer className="w-full" slotId="9163326848" />

<AlertDialogFooter className="px-4">
{progress !== 100 ? (
<Progress value={progress} className="h-2" />
) : (
<AlertDialogAction disabled={progress !== 100}>
Continue
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
52 changes: 33 additions & 19 deletions apps/www/src/app/notes/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Switch } from "@umamin/ui/components/switch";
import useBotDetection from "@/hooks/use-bot-detection";
import { Textarea } from "@umamin/ui/components/textarea";
import { useDynamicTextarea } from "@/hooks/use-dynamic-textarea";
import { ProgressDialog } from "@/app/components/progress-dialog";

const UPDATE_NOTE_MUTATION = graphql(`
mutation UpdateNote($content: String!, $isAnonymous: Boolean!) {
Expand All @@ -43,12 +44,12 @@ const DELETE_NOTE_MUTATION = graphql(`

const updateNotePersisted = graphql.persisted(
"ee74fb98a70e158ec538193fef5c090523d87c18151e2d3687bc60def53169f2",
UPDATE_NOTE_MUTATION,
UPDATE_NOTE_MUTATION
);

const deleteNotePersisted = graphql.persisted(
"fc93cc2e396e0300768942f32a039bf1e92ddf6e2bcea99af54c537feacdf133",
DELETE_NOTE_MUTATION,
DELETE_NOTE_MUTATION
);

type Props = {
Expand All @@ -59,6 +60,7 @@ type Props = {
export default function NoteForm({ user, currentNote }: Props) {
useBotDetection();
const [content, setContent] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [isFetching, setIsFetching] = useState(false);
const [isAnonymous, setIsAnonymous] = useState(false);
const [textAreaCount, setTextAreaCount] = useState(0);
Expand Down Expand Up @@ -97,29 +99,41 @@ export default function NoteForm({ user, currentNote }: Props) {
e.preventDefault();
setIsFetching(true);

const res = await client.mutation(updateNotePersisted, {
content,
isAnonymous,
});
try {
const res = await client.mutation(updateNotePersisted, {
content,
isAnonymous,
});

if (res.error) {
toast.error(formatError(res.error.message));
setIsFetching(false);
return;
}
if (res.error) {
toast.error(formatError(res.error.message));
setIsFetching(false);
return;
}

if (res.data) {
setContent("");
updateNote(res.data.updateNote);
toast.success("Note updated");
}
if (res.data) {
setDialogOpen(true);
setContent("");
updateNote(res.data.updateNote);
}

setIsFetching(false);
logEvent(analytics, "update_note");
setIsFetching(false);
logEvent(analytics, "update_note");
} catch (err: any) {
toast.error("An error occured");
setIsFetching(false);
}
};

return (
<section>
<ProgressDialog
type="Note"
description="Your previous note will be replaced with the new one."
open={dialogOpen}
onOpenChange={setDialogOpen}
/>

<form
onSubmit={handleSubmit}
className="mb-8 flex flex-col gap-y-4 items-end container"
Expand Down Expand Up @@ -156,7 +170,7 @@ export default function NoteForm({ user, currentNote }: Props) {
<span
className={cn(
textAreaCount > 500 ? "text-red-500" : "text-zinc-500",
"text-sm",
"text-sm"
)}
>
{textAreaCount >= 450 ? 500 - textAreaCount : null}
Expand Down
Loading

0 comments on commit 8c7cde5

Please sign in to comment.