Skip to content

Commit af75f17

Browse files
committed
feat: add carousel in testimonials and animation in license card
1 parent b702ed5 commit af75f17

File tree

4 files changed

+127
-15
lines changed

4 files changed

+127
-15
lines changed

src/Common/Helper.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,5 +1078,8 @@ export const getHashedValue = async (value: string): Promise<string | null> => {
10781078
}
10791079
}
10801080

1081-
export const getTTLInHumanReadableFormat = (ttl: number): string =>
1082-
moment.duration(Math.abs(ttl), 'seconds').humanize(false)
1081+
export const getTTLInHumanReadableFormat = (ttl: number): string => {
1082+
const humanizedDuration = moment.duration(Math.abs(ttl), 'seconds').humanize(false)
1083+
// Since moment.js return "a" or "an" for singular values so replacing with 1.
1084+
return humanizedDuration.replace(/^(a|an) /, '1 ');
1085+
}

src/Shared/Components/DevtronLicenseCard/DevtronLicenseCard.tsx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useEffect, useRef } from 'react'
2+
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion'
13
import { ClipboardButton, getTTLInHumanReadableFormat } from '@Common/index'
24
import { ReactComponent as ICChatSupport } from '@IconsV2/ic-chat-circle-dots.svg'
35
import { DevtronLicenseCardProps, ENTERPRISE_SUPPORT_LINK, LicenseStatus } from '@Shared/index'
@@ -6,6 +8,8 @@ import { Icon } from '../Icon'
68
import { getLicenseColorsAccordingToStatus } from './utils'
79
import './licenseCard.scss'
810

11+
const DAMPEN_FACTOR = 40
12+
913
export const DevtronLicenseCard = ({
1014
enterpriseName,
1115
licenseKey,
@@ -18,10 +22,59 @@ export const DevtronLicenseCard = ({
1822
const { bgColor, textColor } = getLicenseColorsAccordingToStatus(licenseStatus)
1923
const remainingTime = getTTLInHumanReadableFormat(ttl)
2024
const remainingTimeString = ttl < 0 ? `Expired ${remainingTime} ago` : `${remainingTime} remaining`
25+
const isLicenseValid = ttl > 0
26+
27+
const cardRef = useRef<HTMLDivElement>(null)
28+
29+
const mouseX = useMotionValue(window.innerWidth)
30+
const mouseY = useMotionValue(window.innerHeight)
31+
32+
const rotateX = useTransform<number, number>(mouseY, (newMouseY) => {
33+
if (!cardRef.current) return 0
34+
const rect = cardRef.current.getBoundingClientRect()
35+
const newRotateX = newMouseY - rect.top - rect.height / 2
36+
return -newRotateX / DAMPEN_FACTOR
37+
})
38+
const rotateY = useTransform(mouseX, (newMouseX) => {
39+
if (!cardRef.current) return 0
40+
const rect = cardRef.current.getBoundingClientRect()
41+
const newRotateY = newMouseX - rect.left - rect.width / 2
42+
return newRotateY / DAMPEN_FACTOR
43+
})
44+
45+
useEffect(() => {
46+
const handleMouseMove = (e: MouseEvent) => {
47+
animate(mouseX, e.clientX)
48+
animate(mouseY, e.clientY)
49+
}
50+
51+
window.addEventListener('mousemove', handleMouseMove)
52+
53+
return () => {
54+
window.removeEventListener('mousemove', handleMouseMove)
55+
}
56+
}, [])
57+
58+
const diagonalMovement = useTransform<number, number>([rotateX, rotateY], ([newRotateX, newRotateY]) => {
59+
const position: number = newRotateX + newRotateY
60+
return position
61+
})
62+
const sheenPosition = useTransform(diagonalMovement, [-5, 5], [-100, 200])
63+
64+
const sheenOpacity = useTransform(sheenPosition, [-100, 50, 200], [0, 0.05, 0])
65+
const sheenGradient = useMotionTemplate`linear-gradient(
66+
55deg,
67+
transparent,
68+
rgba(255 255 255 / ${sheenOpacity}) ${sheenPosition}%,
69+
transparent)`
2170

2271
return (
2372
<div className="flexbox-col p-8 br-16" style={{ backgroundColor: bgColor }}>
24-
<div className="license-card border__secondary-translucent flexbox-col br-12 h-200 bg__tertiary">
73+
<motion.div
74+
className="license-card border__secondary-translucent flexbox-col br-12 h-200 bg__tertiary"
75+
ref={cardRef}
76+
style={{ rotateX, rotateY, backgroundImage: sheenGradient }}
77+
>
2578
<div className="p-20 flexbox-col dc__content-space flex-grow-1">
2679
<div className="flexbox dc__align-items-center dc__content-space">
2780
<span className="font-merriweather cn-9 fs-16 fw-7 lh-1-5 dc__truncate">{enterpriseName}</span>
@@ -48,7 +101,7 @@ export const DevtronLicenseCard = ({
48101
TRIAL LICENSE
49102
</span>
50103
)}
51-
</div>
104+
</motion.div>
52105
{licenseStatus !== LicenseStatus.ACTIVE && (
53106
<div className="p-16 flexbox-col dc__gap-8">
54107
<div className="flexbox dc__gap-8">
@@ -57,7 +110,11 @@ export const DevtronLicenseCard = ({
57110
<a href={`mailto:${ENTERPRISE_SUPPORT_LINK}`}>{ENTERPRISE_SUPPORT_LINK}</a> or contact your
58111
Devtron representative.
59112
</span>
60-
<Icon name={ttl < 0 ? 'ic-timer' : 'ic-error'} color={ttl < 0 ? 'Y500' : 'R500'} size={16} />
113+
<Icon
114+
name={isLicenseValid ? 'ic-timer' : 'ic-error'}
115+
color={isLicenseValid ? 'Y500' : 'R500'}
116+
size={16}
117+
/>
61118
</div>
62119
{/* TODO: Add onClick, and common out the button */}
63120
<Button

src/Shared/Components/LoginBanner/LoginBanner.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { motion } from 'framer-motion'
2+
import { useEffect, useMemo, useState } from 'react'
23
import { Icon } from '../Icon'
3-
import { TESTIMONIAL_CARD_DATA } from './constants'
4+
import { TESTIMONIAL_CARD_DATA, TESTIMONIAL_CARD_INTERVAL, TRANSITION_EASE_CURVE } from './constants'
45

56
const AnimatedBackground = () => (
67
<motion.div>
@@ -96,7 +97,20 @@ const AnimatedBackground = () => (
9697
)
9798

9899
const LoginBanner = () => {
99-
const { quote, name, designation, iconName } = TESTIMONIAL_CARD_DATA
100+
const [currentIndex, setCurrentIndex] = useState<number>(0)
101+
const testimonialCount = useMemo(() => TESTIMONIAL_CARD_DATA.length, [])
102+
103+
useEffect(() => {
104+
const interval = setInterval(() => {
105+
setCurrentIndex((prevIndex) => (prevIndex + 1) % testimonialCount)
106+
}, TESTIMONIAL_CARD_INTERVAL)
107+
108+
return () => {
109+
clearInterval(interval)
110+
}
111+
}, [testimonialCount])
112+
113+
const { quote, name, designation, iconName } = TESTIMONIAL_CARD_DATA[currentIndex]
100114

101115
return (
102116
<div className="flexbox-col br-12 border__primary dc__position-rel dc__overflow-hidden">
@@ -111,7 +125,22 @@ const LoginBanner = () => {
111125
<Icon name="ic-quote" color="N900" />
112126
<div className="border__primary w-1 flex-grow-1" />
113127
</div>
114-
<div className="flexbox-col dc__gap-20">
128+
<motion.div
129+
key={currentIndex}
130+
variants={{
131+
hidden: { opacity: 0, x: 200 },
132+
visible: { opacity: 1, x: 0 },
133+
}}
134+
initial="hidden"
135+
animate="visible"
136+
exit="hidden"
137+
transition={{
138+
staggerChildren: 0.3,
139+
opacity: { duration: 0.75, ease: TRANSITION_EASE_CURVE },
140+
x: { duration: 0.85, ease: TRANSITION_EASE_CURVE },
141+
}}
142+
className="flexbox-col dc__gap-20"
143+
>
115144
<div className="fs-14 fw-4 lh-1-5 cn-9">{quote}</div>
116145
<div className="flexbox dc__content-space dc__align-items-center">
117146
<div>
@@ -120,7 +149,7 @@ const LoginBanner = () => {
120149
</div>
121150
<Icon name={iconName} color={null} size={24} />
122151
</div>
123-
</div>
152+
</motion.div>
124153
</div>
125154
</div>
126155
)
Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
import { TestimonialCardConfig } from './types'
22

33
// TODO: add more testimonials (pending on product team)
4-
export const TESTIMONIAL_CARD_DATA: TestimonialCardConfig = {
5-
quote: "Devtron has been instrumental in our transition to Kubernetes. Its platform helped us streamline our CI/CD processes, ensuring consistent and secure deployments. We've seen significantly improved our deployment speed and security posture thanks to Devtron's built-in DevSecOps features.",
6-
name: 'Sathish Kumar',
7-
designation: 'CloudOps/DevOps Lead at Ather Energy Pvt. Ltd.',
8-
iconName: 'ic-devtron', // TODO: add ather icon
9-
}
4+
export const TESTIMONIAL_CARD_DATA: TestimonialCardConfig[] = [
5+
{
6+
quote: "Devtron has been instrumental in our transition to Kubernetes. Its platform helped us streamline our CI/CD processes, ensuring consistent and secure deployments. We've seen significantly improved our deployment speed and security posture thanks to Devtron's built-in DevSecOps features.",
7+
name: 'Sathish Kumar',
8+
designation: 'CloudOps/DevOps Lead at Ather Energy Pvt. Ltd.',
9+
iconName: 'ic-devtron',
10+
},
11+
{
12+
quote: 'One of the best tools in the market. A production-ready platform that helps in automating all the requirements on Kubernetes. The platform is easy to get started with. Well done product! We run a lot of migrations and backup jobs daily, and Devtron makes it super easy and auditable for us.',
13+
name: 'Arun Jain',
14+
designation: 'Froent developer',
15+
iconName: 'ic-devtron',
16+
},
17+
{
18+
quote: "Devtron has been instrumental in our transition to Kubernetes. Its platform helped us streamline our CI/CD processes, ensuring consistent and secure deployments. We've seen significantly improved our deployment speed and security posture thanks to Devtron's built-in DevSecOps features.",
19+
name: 'Abhishek',
20+
designation: 'CloudOps Lead at Ather Energy Pvt. Ltd.',
21+
iconName: 'ic-devtron',
22+
},
23+
{
24+
quote: 'Devtron has changed the way we used to operate, from bunch of scripts to now just a dashboard. The UI is pretty good and fast. Devtron has truly removed the dependencies of our developers from DevOps peeps. The UI is user-friendly and simple for new users to get acquainted with the tool.',
25+
name: 'Utkarsh Arya',
26+
designation: 'Product Manager',
27+
iconName: 'ic-devtron',
28+
},
29+
]
30+
31+
export const TESTIMONIAL_CARD_INTERVAL = 5000 // duration (in ms) for each testimonial card slide animation
32+
export const TRANSITION_EASE_CURVE = [0.25, 0.8, 0.25, 1]

0 commit comments

Comments
 (0)