Skip to content

Commit

Permalink
style: πŸ’„ added stagger animation for list items
Browse files Browse the repository at this point in the history
βœ… Closes: #210
  • Loading branch information
growupanand committed Mar 3, 2024
1 parent 7828ac4 commit 1961d0f
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";

import { useEffect } from "react";
import Link from "next/link";
import { Skeleton } from "@convoform/ui/components/ui/skeleton";
import { motion, stagger, useAnimate } from "framer-motion";

import { ListCard } from "@/components/common/list";
import { ListItem } from "@/components/common/listItem";
Expand All @@ -11,38 +13,62 @@ import { api } from "@/trpc/react";
export function RecentResponseList({
organizationId,
}: Readonly<{ organizationId: string }>) {
const { data, isFetching } = api.conversation.getRecentResponses.useQuery({
const [scope, animate] = useAnimate();
const { data, isLoading } = api.conversation.getRecentResponses.useQuery({
organizationId,
take: 10,
});
const emptyData = data?.length === 0;

if (isFetching) {
const loadListItems = async () => {
if (!isLoading && !emptyData) {
animate(
".slide-down-list-item",
{ opacity: 1, translate: 0 },
{ delay: stagger(0.1), duration: 0.2 },
);
}
};

useEffect(() => {
loadListItems();
}, [isLoading, emptyData]);

if (isLoading) {
return <RecentResponseListLoading />;
}

if (data) {
return (
<ListCard>
{data.map((response) => (
<ListItem key={response.id}>
<Link
href={`/forms/${response.formId}/conversations/${response.id}`}
<motion.div ref={scope}>
<ListCard>
{data.map((response) => (
<motion.div
className="slide-down-list-item"
key={response.id}
initial={{ opacity: 0, translate: "0 -0.5rem" }}
>
<div className="flex items-center justify-between py-2">
<div className="grid">
<span>{response.name}</span>
<span className="text-muted-foreground text-xs">
{response.form.name}
</span>
</div>
<span className="text-muted-foreground text-sm">
{timeAgo(response.createdAt)}
</span>
</div>
</Link>
</ListItem>
))}
</ListCard>
<ListItem>
<Link
href={`/forms/${response.formId}/conversations/${response.id}`}
>
<div className="flex items-center justify-between py-2">
<div className="grid">
<span>{response.name}</span>
<span className="text-muted-foreground text-xs">
{response.form.name}
</span>
</div>
<span className="text-muted-foreground text-sm">
{timeAgo(response.createdAt)}
</span>
</div>
</Link>
</ListItem>
</motion.div>
))}
</ListCard>
</motion.div>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"use client";

import { useEffect } from "react";
import { motion, stagger, useAnimate } from "framer-motion";

import { ListCard } from "@/components/common/list";
import { api } from "@/trpc/react";
import { FormListItem } from "./formListItem";
Expand All @@ -11,6 +14,8 @@ type Props = {
};

export default function FormList({ workspaceId, orgId }: Readonly<Props>) {
const [scope, animate] = useAnimate();

const { isLoading, data } = api.form.getAll.useQuery({
workspaceId,
organizationId: orgId,
Expand All @@ -19,17 +24,37 @@ export default function FormList({ workspaceId, orgId }: Readonly<Props>) {
const forms = data ?? [];
const emptyForms = forms.length === 0;

const loadListItems = async () => {
if (!isLoading && !emptyForms) {
animate(
".slide-down-list-item",
{ opacity: 1, translate: 0 },
{ delay: stagger(0.1), duration: 0.2 },
);
}
};

useEffect(() => {
loadListItems();
}, [isLoading, emptyForms]);

if (isLoading) {
return <FormListLoading />;
}

return (
<div className="h-full">
<div className="h-full" ref={scope}>
{emptyForms && <p className="text-muted-foreground">No form</p>}
{!emptyForms && (
<ListCard>
{forms.map((form) => (
<FormListItem key={form.id} form={form} />
<motion.div
className="slide-down-list-item"
initial={{ opacity: 0, translate: "0 -0.5rem" }}
key={form.id}
>
<FormListItem form={form} />
</motion.div>
))}
</ListCard>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@convoform/ui/components/ui/dropdown-menu";
import { toast } from "@convoform/ui/components/ui/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import { motion, useAnimate } from "framer-motion";
import { ExternalLink, Loader2, MoreVertical, Trash } from "lucide-react";

import { ConfirmAction } from "@/components/common/confirmAction";
Expand All @@ -24,6 +25,7 @@ type Props = {
};

export function FormListItem({ form }: Readonly<Props>) {
const [scope, animate] = useAnimate();
const queryClient = useQueryClient();
const deleteForm = api.form.delete.useMutation({
onSuccess: () => {
Expand Down Expand Up @@ -53,78 +55,86 @@ export function FormListItem({ form }: Readonly<Props>) {
});

return (
<ListItem>
<div className="flex items-center justify-between">
<div className="grow">
<LinkN href={`/forms/${form.id}`}>
<Button
variant="link"
className="w-full justify-start ps-0 font-normal hover:no-underline "
>
{form.name}
</Button>
</LinkN>
</div>

<div className="flex items-center gap-3">
<div className="max-lg:hidden">
{form.isPublished ? (
<Link href={`/view/${form.id}`} target="_blank">
<Button variant="link">
View form <ExternalLink className="ms-2 h-4 w-4" />
</Button>
</Link>
) : (
<Button variant="link" disabled>
Not published
</Button>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger disabled={isDeleting}>
<motion.div
ref={scope}
onHoverStart={() =>
animate(".form-name", { translate: "0.5rem" }, { duration: 0.2 })
}
onHoverEnd={() => animate(".form-name", { translate: "0" })}
>
<ListItem>
<div className="flex items-center justify-between">
<div className="grow">
<LinkN href={`/forms/${form.id}`}>
<Button
variant="link"
size="icon"
className="h-8 w-8 hover:no-underline"
disabled={isDeleting}
className="w-full justify-start ps-0 font-normal hover:no-underline "
>
{isDeleting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<MoreVertical className="h-4 w-4" />
)}
<span className="form-name">{form.name}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
</LinkN>
</div>

<div className="flex items-center gap-3">
<div className="max-lg:hidden">
{form.isPublished ? (
<Link href={`/view/${form.id}`} target="_blank">
<DropdownMenuItem className="cursor-pointer lg:hidden">
<ExternalLink className="mr-2 h-4 w-4" /> View form
</DropdownMenuItem>
<Button variant="link">
View form <ExternalLink className="ms-2 h-4 w-4" />
</Button>
</Link>
) : (
<DropdownMenuLabel className="text-muted-foreground font-normal lg:hidden">
<Button variant="link" disabled>
Not published
</DropdownMenuLabel>
</Button>
)}
<ConfirmAction
title="Are you sure you want to delete this form?"
description="This action will delete all data related to this form. This action cannot be undone."
onConfirm={handleDeleteForm}
confirmText="Yes, delete form"
>
<DropdownMenuItem
className="text-destructive focus:text-destructive cursor-pointer"
onSelect={(e) => e.preventDefault()}
</div>
<DropdownMenu>
<DropdownMenuTrigger disabled={isDeleting}>
<Button
variant="link"
size="icon"
className="h-8 w-8 hover:no-underline"
disabled={isDeleting}
>
<Trash className="mr-2 h-4 w-4" />
Delete form
</DropdownMenuItem>
</ConfirmAction>
</DropdownMenuContent>
</DropdownMenu>
{isDeleting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<MoreVertical className="h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{form.isPublished ? (
<Link href={`/view/${form.id}`} target="_blank">
<DropdownMenuItem className="cursor-pointer lg:hidden">
<ExternalLink className="mr-2 h-4 w-4" /> View form
</DropdownMenuItem>
</Link>
) : (
<DropdownMenuLabel className="text-muted-foreground font-normal lg:hidden">
Not published
</DropdownMenuLabel>
)}
<ConfirmAction
title="Are you sure you want to delete this form?"
description="This action will delete all data related to this form. This action cannot be undone."
onConfirm={handleDeleteForm}
confirmText="Yes, delete form"
>
<DropdownMenuItem
className="text-destructive focus:text-destructive cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
<Trash className="mr-2 h-4 w-4" />
Delete form
</DropdownMenuItem>
</ConfirmAction>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</ListItem>
</ListItem>
</motion.div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export function ConversationsNavigation({
<p className="text-sm text-gray-500">No Conversations</p>
</div>
) : (
<SecondaryNavigation items={navigationItems} />
<SecondaryNavigation
items={navigationItems}
enableStaggerListAnimation
/>
)}
</>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/common/listItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export function ListItem({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<div className=" py-1 ps-1 align-middle transition-all hover:bg-gray-50 hover:ps-2">
<div className=" py-1 ps-1 align-middle transition-all hover:bg-gray-50">
{children}
</div>
);
Expand Down

0 comments on commit 1961d0f

Please sign in to comment.