Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namadillo: timeline animations #1454

Merged
merged 7 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/namadillo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@tanstack/query-core": "^5.40.0",
"@tanstack/react-query": "^5.40.0",
"@tanstack/react-query-persist-client": "^5.40.0",
"animejs": "^3.2.2",
"bignumber.js": "^9.1.1",
"clsx": "^2.1.1",
"comlink": "^4.4.1",
Expand Down Expand Up @@ -91,6 +92,7 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/animejs": "^3.1.12",
"@types/invariant": "^2.2.37",
"@types/jest": "^29.5.12",
"@types/lodash.debounce": "^4.0.9",
Expand Down
139 changes: 131 additions & 8 deletions apps/namadillo/src/App/Common/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import anime from "animejs";
import clsx from "clsx";
import { useScope } from "hooks/useScope";
import { useRef } from "react";
import { FaCheckCircle } from "react-icons/fa";
import { twMerge } from "tailwind-merge";
export type TransactionStep = {
bullet?: boolean;
Expand Down Expand Up @@ -35,17 +39,17 @@ const StepBullet = ({ disabled }: DisabledProps): JSX.Element => (

const StepContent = ({
children,
isNextStep,
isCurrentStep,
hasError,
disabled,
}: React.PropsWithChildren & {
isNextStep: boolean;
isCurrentStep: boolean;
hasError: boolean;
disabled: boolean;
}): JSX.Element => (
<div
className={clsx("text-center", {
"animate-pulse": isNextStep && !hasError,
"animate-pulse": isCurrentStep && !hasError,
"opacity-20": disabled,
})}
>
Expand All @@ -59,8 +63,109 @@ export const Timeline = ({
hasError,
complete,
}: TransactionTimelineProps): JSX.Element => {
const containerRef = useRef<HTMLDivElement>(null);

useScope(
(query, container) => {
if (!complete) return;

const timeline = anime.timeline({
easing: "easeOutExpo",
duration: 1000,
});

const containerRect = container.getBoundingClientRect();
const items = Array.from(query("i,div"));
const hiding = items.slice(0, -1);
const lastItem = items.slice(-1)[0];
const lastItemText = lastItem.querySelector("p");
const rings = query('[data-animation="ring"]');
const confirmation = query('[data-animation="confirmation"]');

const lastItemTextHeight =
lastItemText ? lastItemText.getBoundingClientRect().height : 0;

const marginTop = 4; // ?

const centerLastItemY =
-containerRect.height / 2 +
lastItem.getBoundingClientRect().height / 2 +
lastItemTextHeight / 2 +
marginTop;

// Hide items, except last one
timeline.add({
targets: hiding,
opacity: 0,
delay: anime.stagger(30),
});

// Move last item to the center of the screen
timeline.add(
{
targets: lastItem,
translateY: centerLastItemY,
},
"-=700"
);

// Try to hide any existing text contained on the last item, as soon
// as the item goes up
timeline.add(
{
targets: lastItem.querySelector("p"),
opacity: 0,
duration: 400,
},
"-=1000"
);

// Display concentric rings, one by one
timeline.add(
{
targets: rings,
opacity: [0, 1],
duration: 800,
scale: [0, 1],
delay: anime.stagger(100),
},
"-=600"
);

// Sucks everything into the screen
timeline.add({
targets: [...rings, lastItem],
scale: 0,
duration: 300,
});

// Displays success box confirmation
timeline.add({
targets: confirmation,
duration: 1000,
scale: [0, 1],
opacity: [0, 1],
});
},
containerRef,
[complete]
);

const renderRing = (className: string): JSX.Element => (
<span
data-animation="ring"
className={clsx(
"absolute aspect-square border-2 border-yellow rounded-full",
className
)}
/>
);

return (
<div>
<div
className={clsx("relative", { "pointer-events-none": complete })}
ref={containerRef}
>
<ul className="flex flex-col items-center gap-3">
{steps.map((step, index: number) => {
return (
Expand All @@ -80,18 +185,36 @@ export const Timeline = ({
<StepBullet disabled={index > currentStepIndex} />
)}
<StepContent
isNextStep={
index === currentStepIndex && !hasError && !complete
}
isCurrentStep={index === currentStepIndex}
disabled={index > currentStepIndex}
hasError={Boolean(hasError)}
hasError={!!hasError}
>
{step.children}
</StepContent>
</li>
);
})}
</ul>
<span
className={clsx(
"absolute w-full h-full circles top-0 left-0",
"flex items-center justify-center pointer-events-none",
{ hidden: !complete }
)}
>
{renderRing("h-30")}
{renderRing("h-60")}
{renderRing("h-96")}
<span
data-animation="confirmation"
className={clsx(
"absolute text-success text-[70px] aspect-square bg-white rounded-full",
{ "opacity-0": complete }
)}
>
<FaCheckCircle />
</span>
</span>
</div>
);
};
53 changes: 30 additions & 23 deletions apps/namadillo/src/App/Sidebars/YourStakingDistribution.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PieChart, PieChartData } from "@namada/components";
import { shortenAddress } from "@namada/utils";
import BigNumber from "bignumber.js";
import { AnimatePresence, motion } from "framer-motion";
import clsx from "clsx";
import { AnimatePresence } from "framer-motion";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { MyValidator } from "types";

type YourStakingDistributionProps = {
Expand Down Expand Up @@ -62,29 +64,34 @@ export const YourStakingDistribution = ({
setDisplayedValidator(myValidators[index]);
}}
>
<div className="max-w-[75%] mx-auto leading-tight">
<div className="relative flex items-center justify-center max-w-[75%] mx-auto leading-tight">
<AnimatePresence>
{displayedValidator === undefined && (
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Your Stake Distribution
</motion.span>
)}
{displayedValidator && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{displayedValidator.validator.alias}
<span className="block text-neutral-500 text-sm">
{getFormattedPercentage(displayedValidator)}
</span>
</motion.div>
)}
<span
className={twMerge(
clsx("absolute transition-opacity duration-300 opacity-100", {
"opacity-0 pointer-events-none": displayedValidator,
})
)}
>
Your Stake Distribution
</span>
<span
className={twMerge(
clsx(
"flex flex-col text-neutral-500 text-sm opacity-0 pointer-events-none",
"transition-opacity duration-300",
{
"opacity-100 pointer-events-auto": displayedValidator,
}
)
)}
>
{displayedValidator?.validator.alias}
<span className="block">
{displayedValidator &&
getFormattedPercentage(displayedValidator)}
</span>
</span>
</AnimatePresence>
</div>
</PieChart>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export const TransferTimelineErrorEntry = ({
}: TransferTimelineErrorEntryProps): JSX.Element => {
return (
<>
<i className="flex justify-center text-2xl mb-1 opacity-70 text-fail">
<i className="flex justify-center text-4xl mb-1 text-fail">
<GoXCircle />
</i>
<div className="opacity-70">{children}</div>
<div className="opacity-50 select-none line-through">{children}</div>
<span
className={clsx(
"block text-sm text-fail selection:text-white selection:bg-fail"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ export const TransferTransactionTimeline = ({
transaction,
}: TransactionTransferTimelineProps): JSX.Element => {
const textSteps = [...allTransferStages[transaction.type]];

const hasError = transaction.status === "error";
const isTransparentTransfer = transparentTransferTypes.includes(
transaction.type
Expand All @@ -137,9 +136,13 @@ export const TransferTransactionTimeline = ({
initialImage.length
);

const filteredSteps = textSteps.filter(
(step) => step !== TransferStep.WaitingConfirmation
);

const stepsWithDescription = buildStepEntries(
textSteps.filter((step) => step !== TransferStep.WaitingConfirmation),
currentStepIndex,
filteredSteps,
currentStepIndex + filteredSteps.length - textSteps.length,
hasError,
transaction,
!isTransparentTransfer
Expand Down
22 changes: 22 additions & 0 deletions apps/namadillo/src/hooks/useScope.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RefObject, useEffect } from "react";

type QueryFnOutput = ReturnType<typeof document.querySelectorAll>;
type CallbackFn = (query: string) => QueryFnOutput;
type CallbackProps = (callback: CallbackFn, container: HTMLElement) => void;

export const useScope = (
callback: CallbackProps,
scope: RefObject<HTMLElement>,
dependencies: unknown[] = []
): void => {
useEffect(() => {
const queryFn = (query: string): QueryFnOutput => {
if (!scope.current)
throw "You must pass a valid scope for useAnimation hook";
return scope.current.querySelectorAll(query);
};

if (!scope.current) return;
callback(queryFn, scope.current);
}, [...dependencies]);
};
16 changes: 16 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3671,6 +3671,7 @@ __metadata:
"@testing-library/jest-dom": "npm:^6.5.0"
"@testing-library/react": "npm:^16.0.0"
"@testing-library/user-event": "npm:^14.5.2"
"@types/animejs": "npm:^3.1.12"
"@types/invariant": "npm:^2.2.37"
"@types/jest": "npm:^29.5.12"
"@types/lodash.debounce": "npm:^4.0.9"
Expand All @@ -3682,6 +3683,7 @@ __metadata:
"@types/styled-components": "npm:^5.1.22"
"@types/traverse": "npm:^0.6.36"
"@vitejs/plugin-react": "npm:^4.3.1"
animejs: "npm:^3.2.2"
autoprefixer: "npm:^10.4.16"
axios: "npm:^1.7.9"
bignumber.js: "npm:^9.1.1"
Expand Down Expand Up @@ -4875,6 +4877,13 @@ __metadata:
languageName: node
linkType: hard

"@types/animejs@npm:^3.1.12":
version: 3.1.12
resolution: "@types/animejs@npm:3.1.12"
checksum: 21d10ed4fbaa99572e252b4d6e666b62a19089f63b13b0e9f8edf5fb0b708afc2b38c3055c52f6a4771bc71793d7a7aec83754c16d0e5606a0ad92b34778ba61
languageName: node
linkType: hard

"@types/aria-query@npm:^5.0.1":
version: 5.0.4
resolution: "@types/aria-query@npm:5.0.4"
Expand Down Expand Up @@ -6360,6 +6369,13 @@ __metadata:
languageName: node
linkType: hard

"animejs@npm:^3.2.2":
version: 3.2.2
resolution: "animejs@npm:3.2.2"
checksum: f43dfcc0c743a2774e76fbfcb16a22350da7104f413d9d1b85c48128b0c078090642809deb631e21dfa0a40651111be39d9d7f694c9c1b70d8637ce8b6d63116
languageName: node
linkType: hard

"ansi-align@npm:^3.0.1":
version: 3.0.1
resolution: "ansi-align@npm:3.0.1"
Expand Down
Loading