Added loader and counter animations (#4931)

Added loader animation: 


https://github.com/twentyhq/twenty/assets/102751374/c569762a-f512-4995-ac4d-47570bacdcaa

Added counter animation: 


https://github.com/twentyhq/twenty/assets/102751374/7d96c625-b56a-4ef6-8042-8e71455caf67

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Ady Beraud
2024-04-12 10:27:32 +02:00
committed by GitHub
parent 138fcbf45f
commit 432d041203
6 changed files with 110 additions and 25 deletions

View File

@ -0,0 +1,50 @@
import { useEffect, useRef } from 'react';
import styled from '@emotion/styled';
import {
animate,
motion,
useInView,
useMotionValue,
useTransform,
} from 'framer-motion';
import { Theme } from '@/app/_components/ui/theme/theme';
const Container = styled.div`
display: flex;
flex-direction: row;
font-size: 56px;
font-weight: 700;
color: ${Theme.text.color.secondary};
@media (max-width: 810px) {
font-size: 32px;
}
`;
interface AnimatedFiguresProps {
value: number;
children?: React.ReactNode;
}
export const AnimatedFigures = ({ value, children }: AnimatedFiguresProps) => {
const count = useMotionValue(0);
const rounded = useTransform(count, (latest) => {
return Math.round(latest);
});
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref);
useEffect(() => {
if (inView) {
animate(count, value, { duration: 2 });
}
}, [count, inView, value]);
return (
<Container>
<motion.span ref={ref}>{rounded}</motion.span>
{children}
</Container>
);
};

View File

@ -3,6 +3,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import Link from 'next/link'; import Link from 'next/link';
import MotionContainer from '@/app/_components/ui/layout/LoaderAnimation';
export interface User { export interface User {
id: string; id: string;
avatarUrl: string; avatarUrl: string;
@ -67,6 +69,7 @@ import React from 'react';
const AvatarGrid = ({ users }: { users: User[] }) => { const AvatarGrid = ({ users }: { users: User[] }) => {
return ( return (
<MotionContainer>
<AvatarGridContainer> <AvatarGridContainer>
{users.map((user) => ( {users.map((user) => (
<Link href={`/contributors/${user.id}`} key={`l_${user.id}`}> <Link href={`/contributors/${user.id}`} key={`l_${user.id}`}>
@ -77,6 +80,7 @@ const AvatarGrid = ({ users }: { users: User[] }) => {
</Link> </Link>
))} ))}
</AvatarGridContainer> </AvatarGridContainer>
</MotionContainer>
); );
}; };

View File

@ -2,6 +2,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import MotionContainer from '@/app/_components/ui/layout/LoaderAnimation';
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -22,5 +24,9 @@ export const ContentContainer = ({
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
}) => { }) => {
return <Container>{children}</Container>; return (
<MotionContainer>
<Container>{children}</Container>
</MotionContainer>
);
}; };

View File

@ -1,7 +1,9 @@
'use client'; 'use client';
import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { AnimatedFigures } from '@/app/_components/contributors/AnimatedFigures';
import { CardContainer } from '@/app/_components/contributors/CardContainer'; import { CardContainer } from '@/app/_components/contributors/CardContainer';
import { Theme } from '@/app/_components/ui/theme/theme'; import { Theme } from '@/app/_components/ui/theme/theme';
@ -64,22 +66,23 @@ export const ProfileInfo = ({
rank, rank,
activeDays, activeDays,
}: ProfileInfoProps) => { }: ProfileInfoProps) => {
const parsedValue = parseFloat(rank.replace('%', ''));
return ( return (
<> <>
<Container> <Container>
<div className="item"> <div className="item">
<p className="title">Merged PR</p> <p className="title">Merged PR</p>
<span className="value">{mergedPRsCount}</span> <AnimatedFigures value={mergedPRsCount} />
</div> </div>
<div className="separator"></div> <div className="separator"></div>
<div className="item"> <div className="item">
<p className="title">Ranking</p> <p className="title">Ranking</p>
<span className="value">{rank}%</span> <AnimatedFigures value={parsedValue}>%</AnimatedFigures>
</div> </div>
<div className="separator"></div> <div className="separator"></div>
<div className="item"> <div className="item">
<p className="title">Active Days</p> <p className="title">Active Days</p>
<span className="value">{activeDays}</span> <AnimatedFigures value={activeDays} />
</div> </div>
</Container> </Container>
</> </>

View File

@ -4,6 +4,7 @@ import { JSXElementConstructor, ReactElement } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Gabarito } from 'next/font/google'; import { Gabarito } from 'next/font/google';
import MotionContainer from '@/app/_components/ui/layout/LoaderAnimation';
import { Theme } from '@/app/_components/ui/theme/theme'; import { Theme } from '@/app/_components/ui/theme/theme';
import { ReleaseNote } from '@/app/releases/api/route'; import { ReleaseNote } from '@/app/releases/api/route';
@ -97,6 +98,7 @@ export const Release = ({
mdxReleaseContent: ReactElement<any, string | JSXElementConstructor<any>>; mdxReleaseContent: ReactElement<any, string | JSXElementConstructor<any>>;
}) => { }) => {
return ( return (
<MotionContainer>
<StyledContainer className={gabarito.className}> <StyledContainer className={gabarito.className}>
<StyledVersion> <StyledVersion>
<StyledRelease>{release.release}</StyledRelease> <StyledRelease>{release.release}</StyledRelease>
@ -108,5 +110,6 @@ export const Release = ({
</StyledVersion> </StyledVersion>
<StlyedContent>{mdxReleaseContent}</StlyedContent> <StlyedContent>{mdxReleaseContent}</StlyedContent>
</StyledContainer> </StyledContainer>
</MotionContainer>
); );
}; };

View File

@ -0,0 +1,19 @@
import React from 'react';
import { motion } from 'framer-motion';
const containerVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.2, ease: 'easeOut' },
},
};
const MotionContainer = ({ children }: { children?: React.ReactNode }) => (
<motion.div variants={containerVariants} initial="hidden" animate="visible">
{children}
</motion.div>
);
export default MotionContainer;