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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 Link from 'next/link';
import MotionContainer from '@/app/_components/ui/layout/LoaderAnimation';
export interface User {
id: string;
avatarUrl: string;
@ -67,16 +69,18 @@ import React from 'react';
const AvatarGrid = ({ users }: { users: User[] }) => {
return (
<AvatarGridContainer>
{users.map((user) => (
<Link href={`/contributors/${user.id}`} key={`l_${user.id}`}>
<AvatarItem key={user.id}>
<img src={user.avatarUrl} alt={user.id} />
<span className="username">{user.id}</span>
</AvatarItem>
</Link>
))}
</AvatarGridContainer>
<MotionContainer>
<AvatarGridContainer>
{users.map((user) => (
<Link href={`/contributors/${user.id}`} key={`l_${user.id}`}>
<AvatarItem key={user.id}>
<img src={user.avatarUrl} alt={user.id} />
<span className="username">{user.id}</span>
</AvatarItem>
</Link>
))}
</AvatarGridContainer>
</MotionContainer>
);
};

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import { JSXElementConstructor, ReactElement } from 'react';
import styled from '@emotion/styled';
import { Gabarito } from 'next/font/google';
import MotionContainer from '@/app/_components/ui/layout/LoaderAnimation';
import { Theme } from '@/app/_components/ui/theme/theme';
import { ReleaseNote } from '@/app/releases/api/route';
@ -97,16 +98,18 @@ export const Release = ({
mdxReleaseContent: ReactElement<any, string | JSXElementConstructor<any>>;
}) => {
return (
<StyledContainer className={gabarito.className}>
<StyledVersion>
<StyledRelease>{release.release}</StyledRelease>
<StyledDate>
{release.date.endsWith(new Date().getFullYear().toString())
? release.date.slice(0, -5)
: release.date}
</StyledDate>
</StyledVersion>
<StlyedContent>{mdxReleaseContent}</StlyedContent>
</StyledContainer>
<MotionContainer>
<StyledContainer className={gabarito.className}>
<StyledVersion>
<StyledRelease>{release.release}</StyledRelease>
<StyledDate>
{release.date.endsWith(new Date().getFullYear().toString())
? release.date.slice(0, -5)
: release.date}
</StyledDate>
</StyledVersion>
<StlyedContent>{mdxReleaseContent}</StlyedContent>
</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;